Android SharePreferences 源码分析

来源:互联网 发布:俄罗斯聊天软件下载 编辑:程序博客网 时间:2024/06/08 08:20

在Android中, SharePreferences是一个轻量级的存储类,特别适合用于保存软件配置参数。使用SharedPreferences保存数据,其背后是用xml文件存放数据,文件存放在/data/data/ < package name > /shared_prefs目录下:

简单使用

SharedPreferences sharedPreferences = getSharedPreferences("ytr", Context.MODE_PRIVATE);Editor editor = sharedPreferences.edit();//获取编辑器editor.putString("name", "MyName");editor.putInt("age", 21);editor.commit();//提交修改

会在shared_pref目录下生成如下的文件,文件名为ytr.xml

<?xml version='1.0' encoding='utf-8' standalone='yes' ?><map>   <string name="name">MyName</string>   <int name="age" value="21" /></map>

因为SharedPreferences背后是使用xml文件保存数据,getSharedPreferences(name,mode)方法的第一个参数用于指定该文件的名称,名称不用带后缀,后缀会由Android**自动加上**。方法的第二个参数指定文件的操作模式,共有四种操作模式,

这四种模式代表的含义为:

  • Context.MODE_PRIVATE = 0

  • Context.MODE_APPEND = 32768

  • Context.MODE_WORLD_READABLE = 1

  • Context.MODE_WORLD_WRITEABLE = 2

Context.MODE_PRIVATE:为默认操作模式,代表该文件是私有数据,只能被应用本身访问.

Context.MODE_APPEND:模式会检查文件是否存在,存在就往文件追加内容,否则就创建新文件,对MODE_PRIVATE来说没有区别,都是追加文件。

Context.MODE_WORLD_READABLE和Context.MODE_WORLD_WRITEABLE用来控制其他应用是否有权限读写该文件。

MODE_WORLD_READABLE:表示当前文件可以被其他应用读取;
MODE_WORLD_WRITEABLE:表示当前文件可以被其他应用写入。

获取SPref中的值

SharedPreferences sharedPreferences = getSharedPreferences("ytr", Context.MODE_PRIVATE);// getString()第二个参数为缺省值,如果preference中不存在该key,将返回缺省值String name = sharedPreferences.getString("name", "");int age = sharedPreferences.getInt("age", 1);

context.getSharedPreferences() 分析

从Context中获取SharedPref对象

@Overridepublic SharedPreferences getSharedPreferences(File file, int mode) {    checkMode(mode);    SharedPreferencesImpl sp;    synchronized (ContextImpl.class) {        // 获取SharedPerf的文件集合        final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();        // 从SP集合中取出指定的File文件        sp = cache.get(file);        if (sp == null) {            // 缓存中没有时,构造一个SP对象            sp = new SharedPreferencesImpl(file, mode);            cache.put(file, sp);            return sp;        }    }    // 多进程的情况下,会进行Check,如果有其他进程修改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文件,如果需要,则重新加载        sp.startReloadIfChangedUnexpectedly();    }    return sp;}

SharedPref对象的构造

// 初始化一些变量SharedPreferencesImpl(File file, int mode) {    mFile = file;    mBackupFile = makeBackupFile(file);    mMode = mode;    mLoaded = false;    mMap = null;    // 将SP文件加载到内存中    startLoadFromDisk();}

startLoadFromDisk()

这里会启动一个新线程进行加载

private void startLoadFromDisk() {    synchronized (this) {        mLoaded = false;    }    new Thread("SharedPreferencesImpl-load") {        public void run() {            loadFromDisk();        }    }.start();}

loadFromDisk()

从Disk中获取SP文件

private void loadFromDisk() {    synchronized (SharedPreferencesImpl.this) {        if (mLoaded) {            return;        }        // 如果BackupFile存在,实际读取的是备份文件        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用来获取文件信息    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的形式装载进内存                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是Spref对象的内存缓存            mMap = map;            // 记录文件信息,从Linux系统获取            mStatTimestamp = stat.st_mtime;            mStatSize = stat.st_size;        } else {            // 第一次装载时,创建Map            mMap = new HashMap<>();        }        // 这个通知需要持有SharedPreferencesImpl.this执行        // 一般时所有的getXXX()        notifyAll();    }}

这个时候XML已经被装载到内存中了,内存中的SPref映射正是mMap对象

getXXX()

然后我们分析下getXXX()方法,这里以getString()为例

@Nullablepublic String getString(String key, @Nullable String defValue) {    synchronized (this) {        // 此处等待加载完毕,否则一直阻塞        awaitLoadedLocked();        String v = (String)mMap.get(key);        // 实际中的取值还是从内存中取得,如果没有,使用默认的值        return v != null ? v : defValue;    }}

使用awaitLoadedLocked()等待加载完毕

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(),如果loadFromDisk()方法没有执行完毕            // 会一直阻塞,直到内存中有数据,这里明白loadFromDisk()最后有个notifyAll()的原因了吧            wait();        } catch (InterruptedException unused) {        }    }}

putXXX()

这里以putString()为例

// 暂存用户设置的数据,待commit,或者apply()时进行写入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;    }}

这里有两个特殊的,remove()和clear()

public Editor remove(String key) {    synchronized (this) {        // remove是将对象自己放在HashMap中,下面会介绍如何进行处理        mModified.put(key, this);        return this;    }}public Editor clear() {    synchronized (this) {        // 写入一个clear标志,在提交时会进行check        mClear = true;        return this;    }}

commit和apply分析

apply是异步,commit是同步,在主线程中使用commit可能会影响性能,因为同步IO操作的耗时可能会比较长,两个方法都能保证value被正确的保存到磁盘上。两者都是Editor类的方法,它们的具体实现在EditorImpl类中,我们先大体比较一下这两个函数:

public boolean commit() {    // 首先提交到内存,然后决定是异步还是同步进行IO    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;}

commitToMemory() 内存同步操作

这个内部类用来处理内存中的数据和协助通知回调

// Return value from EditorImpl#commitToMemory()private static class MemoryCommitResult {    public boolean changesMade;  // any keys different?    public List<String> keysModified;  // may be null    public Set<OnSharedPreferenceChangeListener> listeners;  // may be null    public Map<?, ?> mapToWriteToDisk;    public final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);    public volatile boolean writeToDiskResult = false;    public void setDiskWriteResult(boolean result) {        writeToDiskResult = result;        writtenToDiskLatch.countDown();    }}
// Returns true if any changes were madeprivate MemoryCommitResult commitToMemory() {    // 构造一个MemoryCommitResult    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.        // mDiskWritesInFlight这个变量用来表示还有多少待写入Disk的请求        // 我们使用commitToMemory()修改一个值,这个变量会加1        if (mDiskWritesInFlight > 0) {            // We can't modify our mMap as a currently            // in-flight write owns it.  Clone it before            // modifying it.            // noinspection unchecked             // 如果并发的情况下,需要进行拷贝            mMap = new HashMap<String, Object>(mMap);        }        // mcr中实际需要写入文件的Map和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) {            // 调用clear()的情况            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.                // v==this是remove()一个key的情况                if (v == this || v == null) {                    if (!mMap.containsKey(k)) {                        continue;                    }                    mMap.remove(k);                } else {                    // 对putXXX()来的值进行修改                    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) {                    // 将所有需要回调Listener的字段加入ArrayList                    mcr.keysModified.add(k);                }            }            // 清空暂存缓存            mModified.clear();        }    }    return mcr;}

然后是调用执行操作

/** * Enqueue an already-committed-to-memory result to be written * to disk. * * They will be written to disk one-at-a-time in the order * that they're enqueued. * * @param postWriteRunnable if non-null, we're being called *   from apply() and this is the runnable to run after *   the write proceeds.  if null (from a regular commit()), *   then we're allowed to do this disk write on the main *   thread (which in addition to reducing allocations and *   creating a background thread, this has the advantage that *   we catch them in userdebug StrictMode reports to convert *   them where possible to apply() ...) */private void enqueueDiskWrite(final MemoryCommitResult mcr,                              final Runnable postWriteRunnable) {    // 构造一个Runnble对象,这里将写入文件的操作封装    final Runnable writeToDiskRunnable = new Runnable() {            public void run() {                synchronized (mWritingToDiskLock) {                    writeToFile(mcr);                }                synchronized (SharedPreferencesImpl.this) {                    //每次写入文件成功,会将mDiskWritesInFlight这个变量-1                    mDiskWritesInFlight--;                }                // 如果在写入文件完毕后有回调,在这里执行                // commit()时这个变量会为null                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;        // 需要同步IO的情况        synchronized (SharedPreferencesImpl.this) {            // 是否存在需要写入操作            wasEmpty = mDiskWritesInFlight == 1;        }        // 直接执行        if (wasEmpty) {            writeToDiskRunnable.run();            return;        }    }    // 异步的情况提交线程池进行执行    QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);}

writeToFile() 实际的IO操作

// Note: must hold mWritingToDiskLockprivate void writeToFile(MemoryCommitResult mcr) {    // Rename the current file so it may be used as a backup during the next read    if (mFile.exists()) {        // check changesMade 标志,在commitToMem()时如果字段需要修改,将写为true        if (!mcr.changesMade) {            // If the file already exists, but no changes were            // made to the underlying map, it's wasteful to            // re-write the file.  Return as if we wrote it            // out.            mcr.setDiskWriteResult(true);            return;        }        // 备份文件不存在,会将这个XML进行备份        if (!mBackupFile.exists()) {            if (!mFile.renameTo(mBackupFile)) {                Log.e(TAG, "Couldn't rename file " + mFile                      + " to backup file " + mBackupFile);                mcr.setDiskWriteResult(false);                return;            }        } else {            // 删除文件,便于下次写入            mFile.delete();        }    }    // Attempt to write the file, delete the backup and return true as atomically as    // possible.  If any exception occurs, delete the new file; next time we will restore    // from the backup.    try {        // 使用Stream修改文件        FileOutputStream str = createFileOutputStream(mFile);        if (str == null) {            mcr.setDiskWriteResult(false);            return;        }        // 将Map写成Xml        XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);        FileUtils.sync(str);        str.close();        ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);        try {            // 这里获取文件信息            final StructStat stat = Os.stat(mFile.getPath());            synchronized (this) {                mStatTimestamp = stat.st_mtime;                mStatSize = stat.st_size;            }        } catch (ErrnoException e) {            // Do nothing        }        // Writing was successful, delete the backup file if there is one.        // 写入成功时,清除备份文件        mBackupFile.delete();        // 设置标志位为写入成功        mcr.setDiskWriteResult(true);        return;    } catch (XmlPullParserException e) {        Log.w(TAG, "writeToFile: Got exception:", e);    } catch (IOException e) {        Log.w(TAG, "writeToFile: Got exception:", e);    }    // Clean up an unsuccessfully written file    // 这里时处理异常情况,清除没有写成功的文件    if (mFile.exists()) {        if (!mFile.delete()) {            Log.e(TAG, "Couldn't clean up partially-written file " + mFile);        }    }    mcr.setDiskWriteResult(false);}
public void setDiskWriteResult(boolean result) {    writeToDiskResult = result;    // 释放闭锁, 所有调用await()的地方会继续执行    writtenToDiskLatch.countDown();}

notifyListeners()通知改变字段的Listener回调

private void notifyListeners(final MemoryCommitResult mcr) {    if (mcr.listeners == null || mcr.keysModified == null ||        mcr.keysModified.size() == 0) {        return;    }    // 检测是否是主线程    if (Looper.myLooper() == Looper.getMainLooper()) {        for (int i = mcr.keysModified.size() - 1; i >= 0; i--) {            final String key = mcr.keysModified.get(i);            // 通知回调            for (OnSharedPreferenceChangeListener listener : mcr.listeners) {                if (listener != null) {                    listener.onSharedPreferenceChanged(SharedPreferencesImpl.this, key);                }            }        }    } else {        // 使用Handler post到主线程执行        // Run this function on the main thread.        ActivityThread.sMainThreadHandler.post(new Runnable() {                public void run() {                    notifyListeners(mcr);                }            });    }}

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);            }        };    // 准备执行IO,这时postWriteRunnable不为null,所以会提交给线程池执行    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);}

OnSharedPreferenceChangeListener

/** * Interface definition for a callback to be invoked when a shared * preference is changed. */public interface OnSharedPreferenceChangeListener {    /**     * Called when a shared preference is changed, added, or removed. This     * may be called even if a preference is set to its existing value.     *     * <p>This callback will be run on your main thread.     *     * @param sharedPreferences The {@link SharedPreferences} that received     *            the change.     * @param key The key of the preference that was changed, added, or     *            removed.     */    void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key);}

通过Reg和unReg注册到WeakHashMap中

// WeakReference组成的HashMap,不会造成内存泄漏private final WeakHashMap<OnSharedPreferenceChangeListener, Object> mListeners =           new WeakHashMap<OnSharedPreferenceChangeListener, Object>();public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {    synchronized(this) {        mListeners.put(listener, mContent);    }}public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {    synchronized(this) {        mListeners.remove(listener);    }}

对SharedPref的优化建议

SP的瓶颈分析

IO瓶颈

IO瓶颈造成SP性能差是最大的原因,解决了IO瓶颈,80%的性能问题就解决了。
SP的IO瓶颈包括读取数据到内存与数据写入磁盘两部分。

  1. 读取数据到内存有两个场景会触发:

    • SP文件没有被加载到内存时,调用getSharedPreferences方法会初始化文件并读入内存。
    • 版本低于android_H或使用了MULTI_PROCESS标志时,每次调用getSharedPreferences方法时都会读入。
      我们可以优化的就是它了。每次加载数据到内存太过影响效率。
      H以下版本留存率已经很低了,基本可以忽略。
      对于MULTI_PROCESS,可以采用ContentProvider等其他方式,效率更好,而且可避免SP数据丢失的情况。
  2. 数据写入磁盘也有两个场景会触发:

    • Editor的commit方法,每次执行时同步写入磁盘。

    • Editor的apply方法,每次执行时在单线程池中加入写入磁盘Task,异步写入。
      commit和apply的方法区别在于同步写入和异步写入,以及是否需要返回值。
      在不需要返回值的情况下,使用apply方法可以极大的提高性能。
      同时,多个写入操作可以合并为一个commit/apply,将多个写入操作合并后也能提高IO性能。

锁瓶颈

  • SP的get操作,会锁定SharedPreferences对象,互斥其他操作。

  • SP的put操作,getEditor及commitToMemory会锁定SharedPreferences对象,put操作会锁定Editor对象,写入磁盘更会锁定一个写入锁。

由于锁的缘故,SP操作并发时,耗时会徒增。减少锁耗时,是另一个优化点。

由于读写操作的锁均是针对SP实例对象的,将数据拆分到不同的sp文件中,便是减少锁耗时的直接方案。

降低单文件访问频率,多文件均摊访问,以减少锁耗时。

用开发机进行了简单的性能测试(写入均使用apply,若使用commit则多线程耗时更高):
读写同一文件,10个线程每个读写10次数据:
耗时80-130ms
读写10个文件,每个文件由1个线程读写10次数据:
耗时30-70ms

减少单个SPref文件的大小,将数据均摊到每个文件中,是提高SP访问性能的重要一步。

对SP操作的不当封装

我们采用ContentProvider方案支持跨进程访问,并对所有SP操作均套上了ContentProvider进行访问。
随着项目越来越庞大,通过ContentProvider访问造成的耗时性能也成了问题。
对ContentProvider操作SP测试,耗时是直接操作SP的4倍左右。
所以,最近项目中进行了SP的处理,对于不需要跨进程的SP操作去掉了ContentProvider,尽可能减少无谓耗时。

总结

  1. 尽量不要直接调用SharedPreferences进行读写操作。
    若直接调用getSharedPreferences(fileName,mode).edit().putString(key,value),则对数据的操作直接耦合了fileName和key,后续想调整file和key会比较困难。
    可以考虑封装一下,譬如:
    public void saveUserId(){
    getSharedPreferences(fileName,mode).edit().putString(“user_id”,value);
    }
    这样做可以直接对数据访问,而与fileName与key解耦,后续拆分与调整时会很方便。

  2. 将SP作为耗时操作对待,尽量减少无谓的调用。
    譬如以下代码,SP读一次即可:
    if(sp.getUserId()>0){
    int id=sp.getUserId();

    }

0 0
原创粉丝点击