SharedPreferences源码解析

241 阅读5分钟

一、简介

SharedPreferences是一种轻量级的数据存储方式,采用键值对的存储方式。

SharedPreferences只能存储少量数据,大量数据不能使用该方式存储,支持存储的数据类型有boolean, float, int, long, and string。

SharedPreferences存储到一个XML文件中的,路径在/data/data/$packagename/shared_prefs/下。该文件夹不root时无法通过文件系统访问,但可以通过如下步骤进入到这个文件夹。

adb devices  获取设备列表

adb -s <device name> shell 进入设备根目录

如果只连接了一个设备,可以直接使用 adb shell 进入根目录

run-as <package name> 进入应用目录,注意必须是debug包,本质上是打包时保证"debuggable true"。

ls 查看文件列表

cd shared_prefs

ls 查看文件列表

cat <package name> <filename>.xml 查看对应文件的内容

一个SP文件的示例

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <long name="key_settings_time" value="0" />
    <string name="key_ctx_info_Bullet">eyJlI</string>
</map>

data/data文件夹

二、用法示例

    /**
     * 向Preference中存储一条数据
     */
    private fun saveValue(key: String, value: String) {
        // 1.获取SharedPreferences实例,"data"表示SP文件名
        val sharedPreferences = getSharedPreferences("data", Context.MODE_PRIVATE)
        // 2.调用SharedPreferences对象的edit()方法来获取一个SharedPreferences.Editor对象
        val editor = sharedPreferences.edit()
        // 3.向eidtor中存放数据
        editor.putString(key, value)
        // 4.将数据同步到内存和文件中
        editor.apply()
    }

     /**
      * 从SharedPreferences中获取数据
      */
    private fun getValue(key: String) {
        // 1.获取SharedPreferences实例
        val sharedPreferences = getSharedPreferences("data", Context.MODE_PRIVATE)
        // 2.读取数据
        val value = sharedPreferences.getString(key, "default")
    }

三、获取SharedPreferences实例的方法

方法1:Context.getSharedPreferences(String name, int mode)
  • name:文件名,文件不存在的时候将会自动创建该文件

  • mode:操作模式,创建文件时对文件添加可操作的模式

  • user ID简介

    // Context.java
    
    // XML文件只允许创建它的应用访问,或者是拥有相同userId的应用访问(同应用多进程)
    public static final int MODE_PRIVATE = 0x0000;
    
    // 已废弃,允许所有其他应用读取该文件
    @Deprecated
    public static final int MODE_WORLD_READABLE = 0x0001;
        
    // 已废弃,允许所有其他应用写入该文件
    @Deprecated
    public static final int MODE_WORLD_WRITEABLE = 0x0002
        
    // 已废弃,允许其他所有应用访问该文件
    @Deprecated
    public static final int MODE_MULTI_PROCESS = 0x0004;
    

    也就是说mode现在只能填MODE_PRIVATE了

方法2:Activity.getPreferences(int mode)
// Activity.java

/**
 * 调用方法1,只是以当前Activity的类名作为文件名了
 */
public SharedPreferences getPreferences(int mode) {
    return getSharedPreferences(getLocalClassName(), mode);
}
方法3:PreferenceManager.getDefaultSharedPreferences(Context context)。PrefereceManager已废弃
// PreferenceManager.java

/**
 * 调用方法1,以当前应用的包名+"_preferences"作为文件名
 */
public static SharedPreferences getDefaultSharedPreferences(Context context) {
    return context.getSharedPreferences(getDefaultSharedPreferencesName(context),
            getDefaultSharedPreferencesMode());
}

public static String getDefaultSharedPreferencesName(Context context) {
    return context.getPackageName() + "_preferences";
}

private static int getDefaultSharedPreferencesMode() {
    return Context.MODE_PRIVATE;
}

四、源码解析

1.Context.getSharedPreferences(String name, int mode)获取SharedPreferences流程

抽象方法,其实现类是ContextImpl。关于Context与ContextImpl可以参考这篇文章

// ContextImpl.java

    // SP文件与其对应的SharedPreferencesImpl的集合,static修饰,全局只有一个。
    private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;

    @Override
    public SharedPreferences getSharedPreferences(String name, int mode) {
        // 4.4之前,name = null时默认使用 "null"作为文件名
        if (mPackageInfo.getApplicationInfo().targetSdkVersion <
                Build.VERSION_CODES.KITKAT) {
            if (name == null) {
                name = "null";
            }
        }

        File file;
        synchronized (ContextImpl.class) {
            // 使用ArrayMap()来保存name与其对应的文件
            if (mSharedPrefsPaths == null) {
                mSharedPrefsPaths = new ArrayMap<>();
            }
            file = mSharedPrefsPaths.get(name);
            if (file == null) {
                // 在特定目录下创建文件
                file = getSharedPreferencesPath(name);
                // 将name与file的对应关系放到ArrayMap()中
                mSharedPrefsPaths.put(name, file);
            }
        }
        // 通过文件对象去取sp
        // mSharedPrefsPaths不是单例的,每个Context有一份,他们拿到的file对象不一致
        // 但是不同的file却对应着同一个磁盘文件,它们的HashCode是一致的,因此都能获取到唯一对应的SharedPreferencesImpl对象
        // File的hashCode计算参考 UnixFileSystem.hashCode(file)方法 f.getPath().hashCode() ^ 1234321;
        return getSharedPreferences(file, mode);
    }

    @Override
    public SharedPreferences getSharedPreferences(File file, int mode) {
        SharedPreferencesImpl sp;
        synchronized (ContextImpl.class) {
            final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
            sp = cache.get(file);
            if (sp == null) {
                checkMode(mode);
                if (getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.O) {
                    if (isCredentialProtectedStorage()
                            && !getSystemService(UserManager.class)
                                    .isUserUnlockingOrUnlocked(UserHandle.myUserId())) {
                        throw new IllegalStateException("SharedPreferences in credential encrypted "
                                + "storage are not available until after user is unlocked");
                    }
                }
                // 将file与对应的sp对象放入到cache(ArrayMap中)
                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;
    }

    @GuardedBy("ContextImpl.class")
    private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
        // sSharedPrefsCache是静态的,进程中只有一个
        if (sSharedPrefsCache == null) {
            sSharedPrefsCache = new ArrayMap<>();
        }

        // sSharedPrefsCache中保存着需要的ArrayMap()
        final String packageName = getPackageName();
        ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
        if (packagePrefs == null) {
            packagePrefs = new ArrayMap<>();
            sSharedPrefsCache.put(packageName, packagePrefs);
        }

        return packagePrefs;
    }
2.SharedPreferences解析数据流程
    //  SharedPreferencesImpl.java

    SharedPreferencesImpl(File file, int mode) {
        ...
        startLoadFromDisk();
    }

    @UnsupportedAppUsage
    private void startLoadFromDisk() {
        // 构造方法会加载文件,并阻塞SharedPreferencesImpl。如果获得SharedPreferencesImpl后立即调用get()方法,将会阻塞调用get()的线程
        synchronized (mLock) {
            mLoaded = false;
        }
        // 在子线程中加载
        new Thread("SharedPreferencesImpl-load") {
            public void run() {
                loadFromDisk();
            }
        }.start();
    }

    private void loadFromDisk() {
        ...
        Map<String, Object> map = null;
        StructStat stat = null;
        Throwable thrown = null;
        try {
            stat = Os.stat(mFile.getPath());
            if (mFile.canRead()) {
                BufferedInputStream str = null;
                try {
                    // 读取文件字节流
                    str = new BufferedInputStream(new FileInputStream(mFile), 16 * 1024);
                    // 使用XmlUtils将数据,解析成HashMap()
                    map = (Map<String, Object>) XmlUtils.readMapXml(str);
                } catch (Exception e) {
                    Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
                } finally {
                    IoUtils.closeQuietly(str);
                }
            }
        } 
        ...
        mMap = map;
        ...
    }
3.SharedPreferences获取数据流程
    // SharedPreferencesImpl.java

    public String getString(String key, @Nullable String defValue) {
        // 所有的get方法都是同步方法,这个获取数据的代价相对较高了
        synchronized (mLock) {
            awaitLoadedLocked();
            String v = (String)mMap.get(key);
            return v != null ? v : defValue;
        }
    }

    /**
     * 由此可以看出,当调用getString()方法时,如果子线程还没有将文件解析完毕,这里将会阻塞
     */
    @GuardedBy("mLock")
    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 {
                mLock.wait();
            } catch (InterruptedException unused) {
            }
        }
        if (mThrowable != null) {
            throw new IllegalStateException(mThrowable);
        }
    }
4.SharedPreferences.Editor的put()流程
        //  SharedPreferencesImpl.java
        //  SharedPreferencesImpl$EditorImpl
        @GuardedBy("mEditorLock")
        private final Map<String, Object> mModified = new HashMap<>();

        @Override
        public Editor putString(String key, @Nullable String value) {
            synchronized (mEditorLock) {
                // 数据暂存到mModified中
                mModified.put(key, value);
                return this;
            }
        }
5.SharedPreferences.Editor的apply()
        //  SharedPreferencesImpl.java
        //  SharedPreferencesImpl$EditorImpl       
        
        @Override
        public void apply() {
            final long startTime = System.currentTimeMillis();
            // 将数据保存到内存中
            final MemoryCommitResult mcr = commitToMemory();
            final Runnable awaitCommit = new Runnable() {
                    @Override
                    public void run() {
                        try {
                            mcr.writtenToDiskLatch.await();
                        } catch (InterruptedException ignored) {
                        }
                    }
                };

            // 将计数器加入QueuedWork
            QueuedWork.addFinisher(awaitCommit);

            // 写入完成之后,应该调用本方法
            Runnable postWriteRunnable = new Runnable() {
                    @Override
                    public void run() {
                        awaitCommit.run();
                        QueuedWork.removeFinisher(awaitCommit);
                    }
                };

            SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
            notifyListeners(mcr);
        }
6. SharedPreferences.Editor的commit()
       //  SharedPreferencesImpl.java
       //  SharedPreferencesImpl$EditorImpl   

       @Override
        public boolean commit() {
            ...
            // 先写入内存
            MemoryCommitResult mcr = commitToMemory();
            // 提交到写入队列
            SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null);
            try {
                mcr.writtenToDiskLatch.await();
            } catch (InterruptedException e) {
                return false;
            }
            notifyListeners(mcr);
            return mcr.writeToDiskResult;
        }
7.SharedPreferences.Editor的commitToMemory()
        // 将本次的改动先同步到内存当中,并返回一个MemoryCommitResult对象
        private MemoryCommitResult commitToMemory() {
            ...
            synchronized (SharedPreferencesImpl.this.mLock) {
                ...
                // 将之前已经有的数据取出来
                mapToWriteToDisk = mMap;
                // 需要向磁盘中写入的任务数+1
                mDiskWritesInFlight++;
                ...
                synchronized (mEditorLock) {
                    boolean changesMade = false;
                    // 如果是清除操作
                    if (mClear) {
                        if (!mapToWriteToDisk.isEmpty()) {
                            changesMade = true;
                            mapToWriteToDisk.clear();
                        }
                        keysCleared = true;
                        mClear = false;
                    }
                    // 将mModified中的数据同步进来
                    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 (!mapToWriteToDisk.containsKey(k)) {
                                continue;
                            }
                            mapToWriteToDisk.remove(k);
                        } else {
                            if (mapToWriteToDisk.containsKey(k)) {
                                Object existingValue = mapToWriteToDisk.get(k);
                                if (existingValue != null && existingValue.equals(v)) {
                                    continue;
                                }
                            }
                            mapToWriteToDisk.put(k, v);
                        }

                        changesMade = true;
                        if (hasListeners) {
                            keysModified.add(k);
                        }
                    }

                    mModified.clear();

                    if (changesMade) {
                        mCurrentMemoryStateGeneration++;
                    }

                    memoryStateGeneration = mCurrentMemoryStateGeneration;
                }
            }
            return new MemoryCommitResult(memoryStateGeneration, keysCleared, keysModified,
                    listeners, mapToWriteToDisk);
        }
8.SharedPreferences.Editor的enqueueDiskWrite()

根据apply()和commit()可以看出,两者的postWriteRunnable不同,commit()的postWriteRunnable为null。enqueueDiskWrite方法会根据postWriteRunnable判断出是否是同步提交。

private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                  final Runnable postWriteRunnable) {
         // 是否同步写入提交。commit()为true,apply()为false
        final boolean isFromSyncCommit = (postWriteRunnable == null);

        // 写入磁盘
        final Runnable writeToDiskRunnable = new Runnable() {
                @Override
                public void run() {
                    synchronized (mWritingToDiskLock) {
                        // 写入磁盘
                        writeToFile(mcr, isFromSyncCommit);
                    }
                    synchronized (mLock) {
                        // 写入成功,需要写入的任务-1
                        mDiskWritesInFlight--;
                    }
                    if (postWriteRunnable != null) {
                        // 写入成功,回调postWriteRunnable
                        postWriteRunnable.run();
                    }
                }
            };

        // Typical #commit() path with fewer allocations, doing a write on
        // the current thread.
        if (isFromSyncCommit) {
            boolean wasEmpty = false;
            synchronized (mLock) {
                wasEmpty = mDiskWritesInFlight == 1;
            }
            if (wasEmpty) {
                // 如果此时只有一个需要commit()的,直接调用writeToDiskRunnable.run()方法
                writeToDiskRunnable.run();
                return;
            }
        }
        // apply()放入QueuedWork中,在子线程执行
        QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
    }
9.SharedPreferences.Editor的writeToFile()
    /**
     * 将保存在内从中的mcr写入磁盘
     */
    private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
        ...
        try {
            FileOutputStream str = createFileOutputStream(mFile);

            if (str == null) {
                mcr.setDiskWriteResult(false, false);
                return;
            }
            XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);

            writeTime = System.currentTimeMillis();

            FileUtils.sync(str);

            fsyncTime = System.currentTimeMillis();

            str.close();
            ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
            ...
            mDiskStateGeneration = mcr.memoryStateGeneration;
            // 设置磁盘写入结果
            mcr.setDiskWriteResult(true, true);

            return;
        } 
        ...
        mcr.setDiskWriteResult(false, false);
    }
10.SharedPreferences$MemoryCommitResult的setDiskWriteResult()

内存当中的数据被写入到磁盘之后将会调用这个方法

        final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);    

        void setDiskWriteResult(boolean wasWritten, boolean result) {
            this.wasWritten = wasWritten;
            writeToDiskResult = result;
            // 将writtenToDiskLatch的值归0
            writtenToDiskLatch.countDown();
        }

注意到apply()和commit()中都会有以下操作。

mcr.writtenToDiskLatch.await();

当执行到这行代码时,当前线程会判断writtenToDiskLatch的值,该值为0时,才可以继续往下操作,否则线程将会阻塞在这里。详细介绍请参考CountDownLatch这个类的用法。

五、SP的缺陷

1.由于是对文件 IO 读取,因此在 IO 上的瓶颈是个大问题
2.多线程场景下效率比较低

因为 get 操作的时候,会锁定 SharedPreferencesImpl 里面的对象,互斥其他操作,而当 putcommit()apply() 操作的时候都会锁住 Editor 的对象,这样的情况下,效率会降低。

3.不建议用于跨进程通讯

SP是文件的一种,只适合正在对数据同步要求不高的进程之间进行并发的读写。而且SP自带内存缓存机制,在多进程模式下,系统对SP文件的控制也将不可靠。

4.加载缓慢,可能会引起卡顿

由于每次都会把整个文件加载到内存中,因此,如果 SharedPreferences 文件过大,或者在其中的键值对是大对象的 json 数据则会占用大量内存,读取较慢是一方面,同时也会引发程序频繁GC,导致的界面卡顿。

六、注意事项

1.不要在单个文件中存入过多的key-value或较大的数据

SharedPreference初始化的时候,会进入到SharedPreferencesImpl构造函数,从磁盘中加载文件资源到内存,加载过程使用了同步锁,而在get和put的操作中同样使用到同步锁,因此,若在SP没初始化完成的情况下,我们的get和put操作将只能等待初始化获得的锁释放。另外,我们看到初始化的时候是从磁盘进行文件读取并且解析xml文件到内存的,因此我们应该尽量保障SP文件不要过大,这样可以尽量保证初始化能快速完成。读取较慢是一方面,同时也会引发程序频繁GC,导致的界面卡顿。可以考虑使用多个SP文件来存储数据,对于实时性比较高的数据,应该单独创建较小的SP文件。

2.频繁修改的数据修改后统一提交,而不是修改过后马上提交

每次修改数据都会创建EditorImpl以及一系列相关对象,每次提交都是一个同步操作,每次提交都是一次I/O操作,因此应该减少提交的次数。

3.在不需要返回值的情况下,使用 apply() 方法可以极大的提高性能

apply()是一个异步方法,它会立即同步到内存当中,但是存储到磁盘的操作会放到子线程中去执行 apply()是在子线程中调用,不会阻塞主线程。但它会持有SP的锁,如果是在apply()之后,立即在主线程中操作SP,那依然是会阻塞的。

4.apply()一定不会阻塞主线程吗

注意到在apply()中有如下操作

             final Runnable awaitCommit = new Runnable() {
                    @Override
                    public void run() {
                        try {
                            mcr.writtenToDiskLatch.await();
                        } catch (InterruptedException ignored) {
                        }
                    }
                };            
            // 将计数器加入QueuedWork
            QueuedWork.addFinisher(awaitCommit);

            // 创建向磁盘写入的操作
            Runnable postWriteRunnable = new Runnable() {
                    @Override
                    public void run() {
                        // 当apply()写入文件成功之后会将awaitCommit移除。
                        awaitCommit.run();
                        QueuedWork.removeFinisher(awaitCommit);
                    }
                };

根据源码分析中,第10个方法的分析,我们知道mcr.writtenToDiskLatch.await()这段代码可能会导致执行它的线程阻塞。写入线程会在执行postWriteRunnable时,触发awaitCommit.run()。但是显然,这个时机是写入线程已经写入完成的时候,因此这里只是个判断标记,并不会真的阻塞。

但是我们注意到,QueuedWork.addFinisher(awaitCommit),同时将这个任务加入了QueuedWork中,如果我们的写入线程没有执行完毕,就不会执行QueuedWork.removeFinisher(awaitCommit)来移除awaitCommit。如果这个时候线程调用了QueuedWork.waitToFinish()方法,它就有可能会被阻塞,直到写入线程将数据全部写入到磁盘。

看下waitToFinish方法的注释

    /**
     * Trigger queued work to be processed immediately. The queued work is processed on a separate
     * thread asynchronous. While doing that run and process all finishers on this thread. The
     * finishers can be implemented in a way to check weather the queued work is finished.
     *
     * Is called from the Activity base class's onPause(), after BroadcastReceiver's onReceive,
     * after Service command handling, etc. (so async work is never lost)
     */
    public static void waitToFinish() {
        
    }

BroadcastReceiver的onReceive(),Activity的onPause(),Service的onCommond()方法都有可能调用waitToFinish()。如果这个时候,文件尚未写入完毕,就会阻塞,也有ANR的可能性。

参考文档

SP设计与实现