Android 五种数据存储的方式分别为:

  1. SharedPreferences:以Map形式存放简单的配置参数;
  2. ContentProvider:将应用的私有数据提供给其他应用使用;
  3. 文件存储:以IO流形式存放,可分为手机内部和手机外部(sd卡等)存储,可存放较大数据;
  4. SQLite:轻量级、跨平台数据库,将所有数据都是存放在手机上的单一文件内,占用内存小;
  5. 网络存储 :数据存储在服务器上,通过连接网络获取数据;

Sharedpreferences是Android平台上一个轻量级的存储类,用来保存应用程序的各种配置信息,其本质是一个以“键-值”对的方式保存数据的xml文件,其文件保存在/data/data//shared_prefs目录下。在全局变量上看,其优点是不会产生Application 、 静态变量的OOM(out of memory)和空指针问题,其缺点是效率没有上面的两种方法高。

1. 获取SharedPreferences

要想使用 SharedPreferences 来存储数据,首先需要获取到 SharedPreferences 对象。Android中主要提供了三种方法用于得到 SharedPreferences 对象。

1.1 Context 类中的 getSharedPreferences()方法:

此方法接收两个参数,第一个参数用于指定 SharedPreferences 文件的名称,如果指定的文件不存在则会创建一个,第二个参数用于指定操作模式,主要有以下几种模式可以选择。MODE_PRIVATE 是默认的操作模式,和直接传入 0 效果是相同的。
MODE_WORLD_READABLE 和 MODE_WORLD_WRITEABLE 这两种模式已在 Android 4.2 版本中被废弃。

Context.MODE_PRIVATE: 指定该SharedPreferences数据只能被本应用程序读、写;
Context.MODE_WORLD_READABLE:  指定该SharedPreferences数据能被其他应用程序读,但不能写;
Context.MODE_WORLD_WRITEABLE:  指定该SharedPreferences数据能被其他应用程序读;
Context.MODE_APPEND:该模式会检查文件是否存在,存在就往文件追加内容,否则就创建新文件;
复制代码

1.2 Activity 类中的 getPreferences()方法:

这个方法和 Context 中的 getSharedPreferences()方法很相似,不过它只接收一个操作模式参数,因为使用这个方法时会自动将当前活动的类名作为 SharedPreferences 的文件名。

1.3 PreferenceManager 类中的 getDefaultSharedPreferences()方法:

这是一个静态方法,它接收一个 Context 参数,并自动使用当前应用程序的包名作为前缀来命名 SharedPreferences 文件。

2. SharedPreferences的使用

SharedPreferences对象本身只能获取数据而不支持存储和修改,存储修改是通过SharedPreferences.edit()获取的内部接口Editor对象实现。使用Preference来存取数据,用到了SharedPreferences接口和SharedPreferences的一个内部接口SharedPreferences.Editor,这两个接口在android.content包中;

 1)写入数据://步骤1:创建一个SharedPreferences对象SharedPreferences sharedPreferences= getSharedPreferences("data",Context.MODE_PRIVATE);//步骤2: 实例化SharedPreferences.Editor对象SharedPreferences.Editor editor = sharedPreferences.edit();//步骤3:将获取过来的值放入文件editor.putString("name", “Tom”);editor.putInt("age", 28);editor.putBoolean("marrid",false);//步骤4:提交               editor.commit();2)读取数据:SharedPreferences sharedPreferences= getSharedPreferences("data", Context .MODE_PRIVATE);String userId=sharedPreferences.getString("name","");3)删除指定数据editor.remove("name");editor.commit();4)清空数据editor.clear();editor.commit();
复制代码

注意:如果在 Fragment 中使用SharedPreferences 时,需要放在onAttach(Activity activity)里面进行SharedPreferences的初始化,否则会报空指针 即 getActivity()会可能返回null !

读写其他应用的SharedPreferences 步骤如下:
 1. 在创建SharedPreferences时,指定MODE_WORLD_READABLE模式,表明该SharedPreferences数据可以被其他程序读取;
 2. 创建其他应用程序对应的Context;
 3. 使用其他程序的Context获取对应的SharedPreferences;
 4. 如果是写入数据,使用Editor接口即可,所有其他操作均和前面一致;

try {
//这里的com.example.mpreferences 就是应用的包名Context mcontext = createPackageContext("com.example.mpreferences", CONTEXT_IGNORE_SECURITY);SharedPreferences msharedpreferences = mcontext.getSharedPreferences("name_preference", MODE_PRIVATE);int count = msharedpreferences.getInt("count", 0);} catch (PackageManager.NameNotFoundException e) {e.printStackTrace();}
复制代码

3. SharedPreferences 的源码分析(API 25)

Context的getSharedPreferences:

  public abstract SharedPreferences getSharedPreferences(String name, int mode);
复制代码

我们知道Android中的Context类其实是使用了装饰者模式,而被装饰对象其实就是一个ContextImpl对象,ContextImpl的getSharedPreferences方法:

    /*** Map from preference name to generated path.* 从preference名称到生成路径的映射;*/@GuardedBy("ContextImpl.class")private ArrayMap<String, File> mSharedPrefsPaths;/*** Map from package name, to preference name, to cached preferences.* 从包名映射到preferences,以缓存preferences,这是个静态变量;*/@GuardedBy("ContextImpl.class")private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;@Overridepublic SharedPreferences getSharedPreferences(String name, int mode) {// At least one application in the world actually passes in a null// name.  This happened to work because when we generated the file name// we would stringify it to "null.xml".  Nice.if (mPackageInfo.getApplicationInfo().targetSdkVersion <Build.VERSION_CODES.KITKAT) {if (name == null) {name = "null";}}File file;synchronized (ContextImpl.class) {if (mSharedPrefsPaths == null) {mSharedPrefsPaths = new ArrayMap<>();}file = mSharedPrefsPaths.get(name);if (file == null) {file = getSharedPreferencesPath(name);mSharedPrefsPaths.put(name, file);}}return getSharedPreferences(file, mode);}@Overridepublic SharedPreferences getSharedPreferences(File file, int mode) {checkMode(mode);SharedPreferencesImpl sp;synchronized (ContextImpl.class) {final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();sp = cache.get(file);if (sp == null) {sp = new SharedPreferencesImpl(file, mode);cache.put(file, sp);return sp;}}if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {// If somebody else (some other process) changed the prefs// file behind our back, we reload it.  This has been the// historical (if undocumented) behavior.sp.startReloadIfChangedUnexpectedly();}return sp;}private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {if (sSharedPrefsCache == null) {sSharedPrefsCache = new ArrayMap<>();}final String packageName = getPackageName();ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);if (packagePrefs == null) {packagePrefs = new ArrayMap<>();sSharedPrefsCache.put(packageName, packagePrefs);}return packagePrefs;}
复制代码

从上面我们可以看出他们之间的关系:
(ArrayMap<String, File>)
mSharedPrefsPaths存放的是名称与文件夹的映射,这里的名称就是我们使用getSharedPreferences时传入的name,如果mSharedPrefsPaths为null则初始化,如果file为null则新建一个File并将其加入mSharedPrefsPaths中;

( ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> )
sSharedPrefsCache 存放包名与ArrayMap键值对 初始化时会默认以包名作为键值对中的Key,注意这是个static变量;

(ArrayMap<File, SharedPreferencesImpl>)
sSharedPrefs packagePrefs存放文件name与SharedPreferencesImpl键值对

注意:

  1. 对于一个相同的SharedPreferences name,获取到的都是同一个SharedPreferences对象,它其实是SharedPreferencesImpl对象。
  2. sSharedPrefs在程序中是静态的,如果退出了程序但Context没有被清掉,那么下次进入程序仍然可能取到本应被删除掉的值。而换了另一种清除SharedPreferences的方式:使用SharedPreferences.Editor的commit方法能够起作用,调用后不退出程序都马上生效。

SharedPreferencesImpl对象

  SharedPreferencesImpl(File file, int mode) {mFile = file;//  备份的FilemBackupFile = makeBackupFile(file);mMode = mode;mLoaded = false;mMap = null;startLoadFromDisk();}private void startLoadFromDisk() {synchronized (this) {mLoaded = false;}new Thread("SharedPreferencesImpl-load") {public void run() {loadFromDisk();}}.start();}private void loadFromDisk() {synchronized (SharedPreferencesImpl.this) {if (mLoaded) {return;}if (mBackupFile.exists()) {mFile.delete();mBackupFile.renameTo(mFile);}}// Debuggingif (mFile.exists() && !mFile.canRead()) {Log.w(TAG, "Attempt to read preferences file " + mFile + " without permission");}Map map = null;StructStat stat = null;try {stat = Os.stat(mFile.getPath());if (mFile.canRead()) {BufferedInputStream str = null;try {str = new BufferedInputStream(new FileInputStream(mFile), 16*1024);map = XmlUtils.readMapXml(str);} catch (XmlPullParserException | IOException e) {Log.w(TAG, "getSharedPreferences", e);} finally {IoUtils.closeQuietly(str);}}} catch (ErrnoException e) {/* ignore */}synchronized (SharedPreferencesImpl.this) {mLoaded = true;if (map != null) {mMap = map;mStatTimestamp = stat.st_mtime;mStatSize = stat.st_size;} else {mMap = new HashMap<>();}notifyAll();}}复制代码

可以看到对于一个SharedPreferences文件name,第一次调用getSharedPreferences时会去创建一个SharedPreferencesImpl对象,它会开启一个子线程,然后去把指定的SharedPreferences文件中的键值对全部读取出来,存放在一个Map中。

调用getString时那个SharedPreferencesImpl构造方法开启的子线程可能还没执行完(比如文件比较大时全部读取会比较久),这时getString当然还不能获取到相应的值,必须阻塞到那个子线程读取完为止,如getString方法:

    @Nullablepublic String getString(String key, @Nullable String defValue) {synchronized (this) {awaitLoadedLocked();String v = (String)mMap.get(key);return v != null ? v : defValue;}}private void awaitLoadedLocked() {if (!mLoaded) {// Raise an explicit StrictMode onReadFromDisk for this// thread, since the real read will be in a different// thread and otherwise ignored by StrictMode.BlockGuard.getThreadPolicy().onReadFromDisk();}while (!mLoaded) {try {wait();} catch (InterruptedException unused) {}}}
复制代码

显然这个awaitLoadedLocked方法就是用来等this这个锁的,在loadFromDiskLocked方法的最后我们也可以看到它调用了notifyAll方法,这时如果getString之前阻塞了就会被唤醒。那么这里会存在一个问题,我们的getString是写在UI线程中,如果那个getString被阻塞太久了,比如60s,这时就会出现ANR,所以要根据具体情况考虑是否需要把SharedPreferences的读写放在子线程中。

关于mBackupFile,SharedPreferences在写入时会先把之前的xml文件改成名成一个备份文件,然后再将要写入的数据写到一个新的文件中,如果这个过程执行成功的话,就会把备份文件删除。由此可见每次即使只是添加一个键值对,也会重新写入整个文件的数据,这也说明SharedPreferences只适合保存少量数据,文件太大会有性能问题。

注意:

  1. 在UI线程中调用getXXX可能会导致ANR。
  2. 我们在初始化SharedPreferencesImpl对象时会加SharedPreferencesImpl对应的xml文件中的所有数据都加载到内存中,如果xml文件很大,将会占用大量的内存,我们只想读取xml文件中某个key的值,但我们获取它的时候是会加载整个文件。
  3. 每添加一个键值对,都会重新写入整个文件的数据,不是增量写入; 综上原因能说明Sharedpreferences只适合做轻量级的存储。

SharedPreferences的内部类Editor

   SharedPreferences sharedPreferences= getSharedPreferences("data",Context.MODE_PRIVATE);SharedPreferences.Editor editor = sharedPreferences.edit();editor.putString("name", “Tom”);public Editor edit() {// TODO: remove the need to call awaitLoadedLocked() when// requesting an editor.  will require some work on the// Editor, but then we should be able to do:////      context.getSharedPreferences(..).edit().putString(..).apply()//// ... all without blocking.synchronized (this) {awaitLoadedLocked();}return new EditorImpl();}
复制代码

其实拿到的是一个EditorImpl对象,它是SharedPreferencesImpl的内部类:

  public final class EditorImpl implements Editor {private final Map<String, Object> mModified = Maps.newHashMap();private boolean mClear = false;public Editor putString(String key, @Nullable String value) {synchronized (this) {mModified.put(key, value);return this;}}.....
}
复制代码

可以看到它有一个Map对象mModified,用来保存“修改的数据”,也就是你每次put的时候其实只是把那个键值对放到这个mModified 中,最后调用apply或者commit才会真正把数据写入文件中,如上面的putString方法,其它putXXX代码基本也是一样的。

commit方法和apply方法的不同

    public void apply() {final MemoryCommitResult mcr = commitToMemory();final Runnable awaitCommit = new Runnable() {public void run() {try {mcr.writtenToDiskLatch.await();} catch (InterruptedException ignored) {}}};QueuedWork.add(awaitCommit);Runnable postWriteRunnable = new Runnable() {public void run() {awaitCommit.run();QueuedWork.remove(awaitCommit);}};SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);// Okay to notify the listeners before it's hit disk// because the listeners should always get the same// SharedPreferences instance back, which has the// changes reflected in memory.notifyListeners(mcr);}public boolean commit() {MemoryCommitResult mcr = commitToMemory();SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null /* sync write on this thread okay */);try {mcr.writtenToDiskLatch.await();} catch (InterruptedException e) {return false;}notifyListeners(mcr);return mcr.writeToDiskResult;}// Return value from EditorImpl#commitToMemory()private static class MemoryCommitResult {public boolean changesMade;  // any keys different?public List<String> keysModified;  // may be nullpublic Set<OnSharedPreferenceChangeListener> listeners;  // may be nullpublic Map<?, ?> mapToWriteToDisk;public final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);public volatile boolean writeToDiskResult = false;public void setDiskWriteResult(boolean result) {writeToDiskResult = result;writtenToDiskLatch.countDown();}}
复制代码

两种方式首先都会先使用commitTomemory函数将修改的内容写入到SharedPreferencesImpl当中,再调用enqueueDiskWrite写磁盘操作,commitToMemory就是产生一个“合适”的MemoryCommitResult对象mcr,然后调用enqueueDiskWrite时需要把这个对象传进去,commitToMemory方法:

        // Returns true if any changes were madeprivate MemoryCommitResult commitToMemory() {MemoryCommitResult mcr = new MemoryCommitResult();synchronized (SharedPreferencesImpl.this) {// We optimistically don't make a deep copy until// a memory commit comes in when we're already// writing to disk.if (mDiskWritesInFlight > 0) {// We can't modify our mMap as a currently// in-flight write owns it.  Clone it before// modifying it.// noinspection uncheckedmMap = new HashMap<String, Object>(mMap);}mcr.mapToWriteToDisk = mMap;mDiskWritesInFlight++;boolean hasListeners = mListeners.size() > 0;if (hasListeners) {mcr.keysModified = new ArrayList<String>();mcr.listeners =new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());}synchronized (this) {if (mClear) {if (!mMap.isEmpty()) {mcr.changesMade = true;mMap.clear();}mClear = false;}for (Map.Entry<String, Object> e : mModified.entrySet()) {String k = e.getKey();Object v = e.getValue();// "this" is the magic value for a removal mutation. In addition,// setting a value to "null" for a given key is specified to be// equivalent to calling remove on that key.if (v == this || v == null) {if (!mMap.containsKey(k)) {continue;}mMap.remove(k);} else {if (mMap.containsKey(k)) {Object existingValue = mMap.get(k);if (existingValue != null && existingValue.equals(v)) {continue;}}mMap.put(k, v);}mcr.changesMade = true;if (hasListeners) {mcr.keysModified.add(k);}}mModified.clear();}}return mcr;}
复制代码

这里需要弄清楚两个对象mMap和mModified,mMap是存放当前SharedPreferences文件中的键值对,而mModified是存放此时edit时put进去的键值对。mDiskWritesInFlight表示正在等待写的操作数量。
可以看到这个方法中首先处理了clear标志,它调用的是mMap.clear(),然后再遍历mModified将新的键值对put进mMap,也就是说在一次commit事务中,如果同时put一些键值对和调用clear后再commit,那么clear掉的只是之前的键值对,这次put进去的键值对还是会被写入的。
遍历mModified时,需要处理一个特殊情况,就是如果一个键值对的value是this(SharedPreferencesImpl)或者是null那么表示将此键值对删除,这个在remove方法中可以看到,如果之前有同样的key且value不同则用新的valu覆盖旧的value,如果没有存在同样的key则完整写入。需要注意的是这里使用了同步锁住edtor对象,保证了当前数据正确存入。

 public Editor remove(String key) {synchronized (this) {mModified.put(key, this);return this;}}public Editor clear() {synchronized (this) {mClear = true;return this;}}
复制代码

接下来就是调用enqueueDiskWrite方法:

private void enqueueDiskWrite(final MemoryCommitResult mcr,final Runnable postWriteRunnable) {final Runnable writeToDiskRunnable = new Runnable() {public void run() {synchronized (mWritingToDiskLock) {writeToFile(mcr);}synchronized (SharedPreferencesImpl.this) {mDiskWritesInFlight--;}if (postWriteRunnable != null) {postWriteRunnable.run();}}};final boolean isFromSyncCommit = (postWriteRunnable == null);// Typical #commit() path with fewer allocations, doing a write on// the current thread.if (isFromSyncCommit) {boolean wasEmpty = false;synchronized (SharedPreferencesImpl.this) {wasEmpty = mDiskWritesInFlight == 1;}if (wasEmpty) {writeToDiskRunnable.run();return;}}QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);}复制代码

定义一个Runnable任务,在Runnable中先调用writeToFile进行写操作,写操作需要先获得mWritingToDiskLock,也就是写锁。然后执行mDiskWritesInFlight–,表示正在等待写的操作减少1。
判断postWriteRunnable是否为null,调用commit时它为null,而调用apply时它不为null。isFromSyncCommit为true,而且有1个写操作需要执行,那么就调用writeToDiskRunnable.run(),注意这个调用是在当前线程中进行的。如果不是commit,那就是apply,这时调用QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable),这个QueuedWork类其实很简单,里面有一个SingleThreadExecutor,用于异步执行这个writeToDiskRunnable,commit的写操作是在调用线程中执行的,而apply内部是用一个单线程的线程池实现的,因此写操作是在子线程中执行的。

commit和apply的总结:

  1. apply没有返回值而commit返回boolean表明修改是否提交成功 ;
  2. commit是把内容同步提交到硬盘的,而apply先立即把修改提交到内存,然后开启一个异步的线程提交到硬盘,并且如果提交失败,你不会收到任何通知。
  3. 所有commit提交是同步过程,效率会比apply异步提交的速度慢,在不关心提交结果是否成功的情况下,优先考虑apply方法。
  4. apply是使用异步线程写入磁盘,commit是同步写入磁盘。所以我们在主线程使用的commit的时候,需要考虑是否会出现ANR问题。(不适合大量数据存储)

4. 查看Sharedpreferencesd 保存数据的xml文件

要想查看data文件首先要获取手机root权限,成功root后,修改data权限即可查看data里面的数据库。由于在xml文件内可以很清楚的查看到各个键-值”对数据,所以用Sharedpreferencesd保存比较重要的数据的时候最好先加密再保存。成功查看如下图所示:

data权限修改办法:

1. 打开cmd;
2. 输入’adb shell’;
3. 输入su,回车 ;
4. 输入chmod 777 /data/data/<package name>/shared_prefs 回车
该步骤设置data文件夹权限为777(drwxrwxrwx,也即administrators、power users和users组都有对该文件夹的读、写、运行权限) ;
复制代码

当你在Linux下用命令ll 或者ls -la的时候会看到类似drwxr-xr-x这样标识,具体代表什么意思呢?  这段标识总长度为10位(10个‘-’),第一位表示文件类型,如该文件是文件(用-表示),如该文件是文件夹(用d表示),如该文件是连接文件(用l表示),后面9个按照三个一组分,第一组:用户权限,第二组:组权限,第三组:其他权限。 每一组是三位,分别是读 r ,写 w,执行 x,这些权限都可以用数字来表示:r 4, w 2 , x 1。如果没有其中的某个权限则用‘-’表示。例如:  1. -rwxrwx---,第一位‘-’代表的是文件,第二位到第四位rwx代表此文件的拥有者有读、写、执行的权限,同组用户也有读、写、及执行权限,其他用户组没任何权限。用数字来表示的话则是770.
 2. drwx------,第一位‘d’代表的是文件夹,第二位到第四位rwx代表此文件夹的拥有者有读、写、执行的权限,第五位到第七位代表的是拥有者同组用户的权限,同组用户没有任何权限,第八位到第十位代表的是其他用户的权限,其他用户也没有任何权限。用数字来表示的话则是700.

Android SharedPreferences的理解与使用相关推荐

  1. Android Activity的理解

    Android Activity的理解 Activity 生命周期的四个状态 Activity的生命周期分为运行.暂停.停止.销毁四个状态. 运行状态:该Activity生命开始,Activity在前 ...

  2. android SharedPreferences数据存储

    android  SharedPreferences数据存储 很多时候我们开发的软件需要向用户提供软件参数设置功能,例如我们常用的QQ,用户可以设置是否允许陌生人添加自己为好友.对于软件配置参数的保存 ...

  3. android sharedpre,Android SharedPreferences四种操作模式使用详解_Android_脚本之家

    Android  SharedPreferences详解 获取SharedPreferences的两种方式: 1 调用Context对象的getSharedPreferences()方法 2 调用Ac ...

  4. android sharedpreferences工具类

    今天,简单讲讲如何写一个sharedpreferences的工具类. 很简单,把一些重复的操作封装在工具类里,其他地方调用就可以.在网上搜索了比较多的资料,找到一个比较好的工具类. 参考文章:http ...

  5. android SharedPreferences的使用优化

    今天,简单讲讲android如何优化使用SharedPreferences保存数据. 之前,我写代码是都是每次缓存一个键值对就commit一次,后来在完成看了看,发现应该是缓存完所有键值对后,再一次性 ...

  6. android SharedPreferences保存list数据

    今天,简单讲讲如何使用  SharedPreferences保存list数据. 网上找了很多资料,还是觉得这种比较简单.直接上代码: 保存: public static boolean saveA ...

  7. android sharedpreferences 工具类,android sharedpreferences工具类

    释放双眼,带上耳机,听听看~! 今天,简单讲讲如何写一个sharedpreferences的工具类. 很简单,把一些重复的操作封装在工具类里,其他地方调用就可以.在网上搜索了比较多的资料,找到一个比较 ...

  8. android 同步list数据,android SharedPreferences保存list数据

    释放双眼,带上耳机,听听看~! 今天,简单讲讲如何使用SharedPreferences保存list数据. 网上找了很多资料,还是觉得这种比较简单.直接上代码: 保存: public static b ...

  9. android SharedPreferences 存储对象

    原文地址为: android SharedPreferences 存储对象 我们知道SharedPreferences只能保存简单类型的数据,例如,String.int等. 如果想用SharedPre ...

最新文章

  1. JavaSE(三)——数组及继承
  2. Forefront_TMG_2010-TMG发布SSL OWA Exchange 2010
  3. 修改Linux内核的启动Logo和禁用启动光标
  4. 算法 | 一段C语言和汇编的对应分析,揭示函数调用的本质
  5. NOIP2012复赛 普及组 第一题
  6. unity 中文_Unity无情大爆料时间Unity3D的脚本语言
  7. GnuTLS传输层安全性库
  8. ML 12 13 mixture of gaussions and EM
  9. python中的运算符重载_Python中的操作符重载
  10. JavaScript中atEnd函数
  11. 深入剖析BIO到NIO演变史
  12. H5音乐播放器skPlayer,已支持拉取网易云音乐歌单。
  13. Kaldi WFST HCLG.fst 构图 学习
  14. 【知识图谱】Neo4j 导入数据构建知识图谱的三种方法
  15. linux a卡怎么切换n卡,手把手教您win10系统a卡切换独显的具体办法
  16. 完全免费一级域名强注册地址 免费二级域名注册地址
  17. 什么叫机械硬盘_机械硬盘内部是什么结构?
  18. 浮点数与十六进制转换
  19. 看大数据平台如何打造餐饮业务一体化?
  20. python 使用BeautifulSoup爬取天天基金网主要数据

热门文章

  1. Linux 关于解压
  2. jquery中prop()方法和attr()方法
  3. PAT:1001. 害死人不偿命的(3n+1)猜想 (15) AC
  4. 使用beanShell来解析执行java的String方法,类似javascipt的eval方法
  5. JAVA锁之可重入锁和递归锁及示例代码
  6. apipost--接口测试脚本编写if判断语句使用
  7. Servlet 自动刷新页面
  8. 08-07 性能测试--性能监听平台
  9. java对象属性的作用域类型_java 对象和类
  10. java applet 浏览器_浏览器不能加载Java中Applet小程序