Android SharePreferences 源码分析 及优化建议

在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对象

@Override
public 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()为例

@Nullable
public 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 made
private 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 mWritingToDiskLock
private 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();

    }

    原文作者:Android源码分析
    原文地址: https://juejin.im/entry/58b0fc2a128fe1006cc1e966
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞