SharedPreference小结
特点
- 轻量级的数据存储方式,其内部是以xml的结构保存,以键值对的形式进行存储,文件位于data/data/包名/shared_prefs目录下
只支持基本数据类型,不支持自定义数据类型
<?xml version='1.0' encoding='utf-8' standalone='yes' ?> <map> <float name="isFloat" value="1.5" /> <string name="isString">Android</string> <int name="isInt" value="1" /> <long name="isLong" value="1000" /> <boolean name="isBoolean" value="true" /> </map>
1.初始化
Context是一个抽象类,其核心实现类是ContextImpl,其中getSharedPreference()有两个参数,第一个参数为文件名,可随意取,无需后缀;第二个参数为模式,分为四种:Context.MODE_PRIVATE(只能该应用读写),Context.MODE_WORLD_READABLE(可以被其他应用读取,但不可写入),Context.MODE_WORLD_WRITEABLE(可被其他应用读写),Context.MODE_APPEND(检查文件是否存在,存在直接追加内容,不存在则直接创建)
@Override public SharedPreferences getSharedPreferences(String name, int mode) { SharedPreferencesImpl sp; synchronized (ContextImpl.class) { if (sSharedPrefs == null) { // sSharedPrefs 是 ContextImpl 的静态成员变量,通过 Map 维护着当前包名下的 SP Map 集合 sSharedPrefs = new ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>>(); } final String packageName = getPackageName(); ArrayMap<String, SharedPreferencesImpl> packagePrefs = sSharedPrefs.get(packageName); if (packagePrefs == null) { packagePrefs = new ArrayMap<String, SharedPreferencesImpl>(); sSharedPrefs.put(packageName, packagePrefs); } // 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) { // name 参数为 null 时,文件名使用 null.xml if (name == null) { name = "null"; } } sp = packagePrefs.get(name); if (sp == null) { // SP 集合是一个以 SP 的名字为 key , SP 为值的 Map File prefsFile = getSharedPrefsFile(name); // SP 的实现类是 SharedPreferencesImpl sp = new SharedPreferencesImpl(prefsFile, mode); packagePrefs.put(name, sp); return sp; } } // Android 3.0 以下或者支持 MODE_MULTI_PROCESS 模式时,如果文件被改动,就重新从文件读取,实现多进程数据同步,但是实际使用中效果不佳,可能会有很多坑。 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; }
说明:
* 从这里我们看出:sSharedPrefs为一个ArrayMap,其key为包名,value为一个Arraymap,而此arraymap的key为文件名,value为sharedpreference。 * 首先会判断sSharedPrefs是否为空,为空则创建一个 * 然后获取包名,通过包名来获取对应的arraymap,若为空则创建一个,反之返回对应的arraymap * 之后通过文件名获取对应的SharedPreferencesImpl若为空则创建一个,反之返回对应的SharedPreferencesImpl * 通过SharedPreferencesImpl创建对应的xml的文件
SharePreference是由SharedPreferenceImpl返回的,我们来看看SharedPreferenceImpl
SharedPreferencesImpl(File file, int mode) { mFile = file; mBackupFile = makeBackupFile(file); mMode = mode; mLoaded = false; mMap = null; startLoadFromDisk(); } private void startLoadFromDisk() { synchronized (this) { mLoaded = false; } // 开启异步线程从磁盘读取文件,加锁防止多线程并发操作 new Thread("SharedPreferencesImpl-load") { public void run() { synchronized (SharedPreferencesImpl.this) { loadFromDiskLocked(); } } }.start(); } private void loadFromDiskLocked() { if (mLoaded) { return; } // 备份文件存在,说明上次的写入操作失败,直接读取备份文件 if (mBackupFile.exists()) { mFile.delete(); mBackupFile.renameTo(mFile); } // Debugging if (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); // 从 XML 里面读取数据返回一个 Map,内部使用了 XmlPullParser map = XmlUtils.readMapXml(str); } catch (XmlPullParserException e) { Log.w(TAG, "getSharedPreferences", e); } catch (FileNotFoundException e) { Log.w(TAG, "getSharedPreferences", e); } catch (IOException e) { Log.w(TAG, "getSharedPreferences", e); } finally { IoUtils.closeQuietly(str); } } } catch (ErrnoException e) { } mLoaded = true; if (map != null) { mMap = map; mStatTimestamp = stat.st_mtime; mStatSize = stat.st_size; } else { mMap = new HashMap<String, Object>(); } // 唤醒等待的线程,到这文件读取完毕 notifyAll(); }
说明
1.mBackupFile 定义了备份文件,该文件用于在读取数据时,当上次写入数据失败时,直接从该文件读取 2.内部使用xmlPullParser读取xml文件数据
2.读数据
首先会加入SharedPreferencesImpl 对象锁,然后同步等待从磁盘加载数据完成,之后才将数据返回,因为当存储的数据比较大,那么在读取数据的时候会阻塞线程,所以建议尽量保存较小的数据。
@Nullable public 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) { } } }
写数据
- SharedPreferences只能用于读取数据,写入数据是通过Editor完成的
- Editor是一个接口,其实现类为EditorImpl,是sharedpreference的内部类
通过sharedpreference的edit方法返回一个EditorImpl对象
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(); }
在put过程中mModified是一个map,他将要修改的数据先存储起来,之后调用commit或apply进行保存
public Editor putString(String key, @Nullable String value) { synchronized (this) { mModified.put(key, value); return this; } }
那最后的commit和apply是将数据保存到磁盘中,他们首先会执行commitToMemory,将数据提交到内存中,返回MemoryCommitResult,里面保存着本次提交的状态。然后commit会调用enqueueDiskWrite将数据保存到磁盘,并阻塞当前线程;apply通过runable把写入磁盘的操作传递给enqueueDiskWrite,这样将数据写入到了磁盘中。
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; } 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); }
清除指定的数据
SharedPreferences userSettings = getSharedPreferences("setting", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = userSettings.edit();
editor.remove("KEY");
editor.apply();
清空数据
SharedPreferences userSettings = getSharedPreferences("setting", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = userSettings.edit();
editor.clear();
editor.apply();
监听SharedPreference
可以通过监听器监听SharedPreference的xml文件变化
public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { synchronized(this) { mListeners.put(listener, mContent); } } public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { synchronized(this) { mListeners.remove(listener); } }
apply与commit的区别
- commit()有返回值,表示是否执行成功;apply()没有返回值。失败后不会有任何提示
- commit()写入文件是先将数据提交到内存中,然后在当前线程同步执行的,在同一时间有多个commit时,其他commit会等待前面的commit执行之后再操作;apply()写入文件是先将数据提交到内存中,然后把操作放入runable中异步执行,在同一时间有多个commit的时候,后面的会直接将前面的覆盖(推荐使用)