美文网首页io
SharedPreferences源码解析

SharedPreferences源码解析

作者: stevewang | 来源:发表于2021-06-07 16:25 被阅读0次

    SharedPreferences是开发中很常见的一个类,它的主要作用是持久化本地的一些基础数据,方便我们做一些简单的业务判断。基础用法如下:

    SharedPreferences sharedPrefs = context.getSharedPreferences("tts", Context.MODE_PRIVATE);
    
    // 持久化值
    SharedPreferences.Editor editor = sharedPrefs.edit();
    editor.putString("version", "1.0");
    editor.commit();
    
    // 取出值
    String version = sharedPrefs.getString("version", "");
    

    SharedPreferences具有简单和无结构化的特点,对于简单的业务场景来说,它比Database更为实用,其本质上就是一个xml文件和I/O操作的集合,上述操作生成的tts.xml内容如下:

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

    SharedPreferences会对磁盘的文件做操作,但磁盘操作都是较为耗时的,所以Android会将磁盘内容读取到内存中,从而直接对内存进行操作,这就是SharedPreferences缓存机制。

    源码分析

    以下源码基于Android 8.1,先看下SharedPreferences的两种获取方式:context.getSharedPreferences()Preferencemanager.getDefaultSharedPreferences(),其中Preferencemanager.getDefaultSharedPreferences()也是调用了 context.getSharedPreferences(),只是将 Packagename + “_preferences” 作为SP文件的名字,代码如下:

    /**
     * Gets a {@link SharedPreferences} instance that points to the default file that is used by
     * the preference framework in the given context.
     *
     * @param context The context of the preferences whose values are wanted.
     * @return A {@link SharedPreferences} instance that can be used to retrieve and listen
     *         to values of the preferences.
     */
    public static SharedPreferences getDefaultSharedPreferences(Context context) {
        return context.getSharedPreferences(getDefaultSharedPreferencesName(context),
                getDefaultSharedPreferencesMode());
    }
    
    /**
     * Returns the name used for storing default shared preferences.
     *
     * @see #getDefaultSharedPreferences(Context)
     */
    public static String getDefaultSharedPreferencesName(Context context) {
        return context.getPackageName() + "_preferences";
    }
    
    private static int getDefaultSharedPreferencesMode() {
        return Context.MODE_PRIVATE;
    }
    

    再看下context.getSharedPreferences() ,实现在 ContextImpl 里面:

    @Override
    public 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";
            }
        }
        // 1.通过name获取File
        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);
            }
        }
        // 2.通过File获取SharedPreferences
        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");
                    }
                }
                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;
    }
    

    分析上述代码,getSharedPreferences()会先判断 mSharedPrefsPaths 中是否有缓存(它维护从SharedPreferences name到File的映射),如果没有只能根据File path获取File对象,再缓存至 mSharedPrefsPaths 中。

    那么如何通过File对象获取SharedPreferences对象呢?主要依靠 sSharedprefsCache 缓存,sSharedprefsCache 的key为package name,value为File对象到SharedPreferences对象的映射,如果sSharedprefsCache 里面没有package name对应的缓存,则先创建File对象到SharedPreferences对象的映射,再new SharedpreferenceImpl(Sharedpreferences实现类)对象并缓存起来。

    从这段代码看,getSharedPreferences()不仅是线程安全的,还有一些对于多进程的保护措施:当模式为MODE_MULTI_PROCESS时,会通过sp.startReloadIfChangedUnexpectedly()去尝试再次加载xml文件内容,然而通过这种方式来保证多进程访问的安全性会有以下问题:

    1. 使用MODE_MULTI_PROCESS时,不要在本地自行缓存Sharedpreferences,必须每次都从context.getSharedPreferences()获取,否则无法触发reload,可能导致两个进程数据不同步。
    2. 从磁盘加载xml文件是耗时的,此时如果进行Sharedpreferences其他操作都会阻塞等待,这意味着很多时候获取Sharedpreferences数据都不得不从xml文件再读一遍,大大降低了内存缓存的作用。
    3. 修改Sharedpreferences数据时只能用commit(),保证修改时写入了文件,这样其他进程才能通过文件大小或修改时间感知到文件发生了变化。

    无论怎么说,MODE_MULTI_PROCESS都很糟糕,因此已被废弃,Android更建议使用ContentProvider来处理多进程间的文件共享。

    根据上面的分析,在冷启动的场景下首次调用getSharedPreferences()时,会执行new ShaedPreferenceImpl()

    SharedPreferencesImpl(File file, int mode) {
        mFile = file;
        mBackupFile = makeBackupFile(file);
        mMode = mode;
        mLoaded = false;
        mMap = null;          // SharedPreferences中所有键值对,从xml文件中读取
        startLoadFromDisk();  // 开启线程异步加载xml文件内容
    }
    
    private void startLoadFromDisk() {
        synchronized (mLock) {  // 悲观锁保证线程安全,可以优化成CAS乐观锁
            mLoaded = false;    // 标识xml文件未加载完成
        }
        new Thread("SharedPreferencesImpl-load") {
            public void run() {
                loadFromDisk();
            }
        }.start();
    }
    

    从代码看,主要是异步线程读取xml文件,线程的名字是 SharedpreferencesImpl-load,这个过程也是线程安全的。

    private void loadFromDisk() {
        synchronized (mLock) {
            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);  // 通过Java IO对文件进行读取
                    map = XmlUtils.readMapXml(str);  // xml解析
                } catch (Exception e) {
                    Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
                } finally {
                    IoUtils.closeQuietly(str);
                }
            }
        } catch (ErrnoException e) {
            /* ignore */
        }
    
        synchronized (mLock) {
            mLoaded = true; // xml文件加载完毕
            if (map != null) {
                mMap = map; // 读取的键值对缓存至mMap
                mStatTimestamp = stat.st_mtim;
                mStatSize = stat.st_size;
            } else {
                mMap = new HashMap<>();
            }
            mLock.notifyAll();// 激活正在等待的线程
        }
    }
    

    上述代码通过XmlUtils.readMapXml()读取xml中所有键值对,并缓存至 mMap。因此,Sharedpreferences在冷启动后首次使用时性能开销大,主要是把文件中所有的键值对读取到内存的过程。

    那么我们每次从Sharedpreferences中读取数据,都会立刻取到吗?让我们看下实现:

    @Nullable
    public String getString(String key, @Nullable String defValue) {
        synchronized (mLock) {
            awaitLoadedLocked();    // 阻塞等待xml文件加载完成
            String v = (String)mMap.get(key);
            return v != null ? v : defValue;
        }
    }
    
    @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);
        }
    }
    

    从代码看getString()会从 mMap 中直接获取,而且是线程安全的,如果此时xml文件还没有加载到内存,则会阻塞等待。这是SP的一个缺点。

    再看下写入过程,所有对于Sharedpreferences的修改操作都需要一个 Editor 对象,它的实现类是 EditorImpl:

    public final class EditorImpl implements Editor {
        private final Object mLock = new Object();
    
        @GuardedBy("mLock")
        private final Map<String, Object> mModified = Maps.newHashMap();  // 记录diff数据 
    
        @GuardedBy("mLock")
        private boolean mClear = false;
    
        public Editor putString(String key, @Nullable String value) {
            synchronized (mLock) {
                mModified.put(key, value);
                return this;
            }
        }
    }
    

    从代码中可以看出,执行putString()时,只是写到了 mModified 中,并没有写入 mMap,更没有写入磁盘xml文件。

    那什么时机写入呢?答案是在commit()apply()时:

    public boolean commit() {
        long startTime = 0;
    
        if (DEBUG) {
            startTime = System.currentTimeMillis();
        }
        // 1.提交到内存
        MemoryCommitResult mcr = commitToMemory();
        // 2.写盘操作,完成后由mcr释放锁,注意第二个参数为null,表示在当前线程同步写盘
        SharedPreferencesImpl.this.enqueueDiskWrite(
            mcr, null /* sync write on this thread okay */);
        try {
            // 3.利用mcr开启CountDownLatch阻塞
            mcr.writtenToDiskLatch.await();
        } catch (InterruptedException e) {
            return false;
        } finally {
            if (DEBUG) {
                Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                        + " committed after " + (System.currentTimeMillis() - startTime)
                        + " ms");
            }
        }
        notifyListeners(mcr);
        // 4.锁被释放后,返回写操作的执行结果
        return mcr.writeToDiskResult;
    }
    

    commit() 先将修改更新至内存 mMap,再将修改同步写入磁盘xml,它利用CountDownLatch保证等待写盘完成后返回执行结果。

    public void apply() {
        final long startTime = System.currentTimeMillis();
        // 1.提交到内存
        final MemoryCommitResult mcr = commitToMemory();
        final Runnable awaitCommit = new Runnable() {
                public void run() {
                    try {
                        mcr.writtenToDiskLatch.await();
                    } catch (InterruptedException ignored) {
                    }
    
                    if (DEBUG && mcr.wasWritten) {
                        Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                                + " applied after " + (System.currentTimeMillis() - startTime)
                                + " ms");
                    }
                }
            };
        // 2. 向QueuedWork中添加等待任务,确保即使Activity将要stop时仍要等待apply写盘操作执行完成
        // 详见ActivityThread#handleStopActivity()中调用的QueuedWork.waitToFinish()
        QueuedWork.addFinisher(awaitCommit);
    
        Runnable postWriteRunnable = new Runnable() {
                public void run() {
                    // 4. 写盘操作执行完成后,执行等待任务,并将其从QueuedWork中移出
                    awaitCommit.run();
                    QueuedWork.removeFinisher(awaitCommit);
                }
            };
        // 3.写盘操作,完成后由mcr释放锁,注意第二个参数不为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);
    }
    

    apply()则是先将修改更新至内存 mMap,再将修改异步写入磁盘xml,它并不关心写盘操作成功与否。

    无论是commit()还是apply()都会先执行commitToMemory(),它的作用就是将 mModified 和 mMap 的值进行比较,从而更新 mMap 中的值,逻辑比较简单,这里就不详细分析了。唯一需要注意的是,commitToMemory() 的返回值 mcr 中包含了一个 mapToWriteToDisk,它指向了更新后的 mMap,目的是为后边的写盘操作enqueueDiskWrite()做准备。

    /**
     * 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) {
        final boolean isFromSyncCommit = (postWriteRunnable == null);
    
        final Runnable writeToDiskRunnable = new Runnable() {
                public void run() {
                    synchronized (mWritingToDiskLock) {
                        writeToFile(mcr, isFromSyncCommit);
                    }
                    synchronized (mLock) {
                        mDiskWritesInFlight--;
                    }
                    if (postWriteRunnable != null) {
                        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) { // 如果没有其他线程在写盘,直接在当前线程执行
                writeToDiskRunnable.run();
                return;
            }
        }
        // 异步线程写盘
        QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
    }
    

    从代码可以看出,commit()和apply()的主要区别就是在调用 enqueueDiskWrite() 进行写盘操作时传入的 postWriteRunnable 是否为 null.

    如果是commit()且没有其他线程正在写盘,就会在当前线程上直接执行writeToDiskRunnable.run(),否则会将 writeToDiskRunnable 放入一个单线程队列中等待调度。

    writeToDiskRunnable 的主要工作就是执行writeToFile()

    // Note: must hold mWritingToDiskLock
    private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
        long startTime = 0;
        long existsTime = 0;
        long backupExistsTime = 0;
        long outputStreamCreateTime = 0;
        long writeTime = 0;
        long fsyncTime = 0;
        long setPermTime = 0;
        long fstatTime = 0;
        long deleteTime = 0;
    
        if (DEBUG) {
            startTime = System.currentTimeMillis();
        }
    
        boolean fileExists = mFile.exists();
    
        if (DEBUG) {
            existsTime = System.currentTimeMillis();
    
            // Might not be set, hence init them to a default value
            backupExistsTime = existsTime;
        }
    
        // Rename the current file so it may be used as a backup during the next read
        if (fileExists) {
            boolean needsWrite = false;
    
            // Only need to write if the disk state is older than this commit
            if (mDiskStateGeneration < mcr.memoryStateGeneration) {
                if (isFromSyncCommit) {
                    needsWrite = true;
                } else {
                    synchronized (mLock) {
                        // No need to persist intermediate states. Just wait for the latest state to
                        // be persisted.
                        if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
                            needsWrite = true;
                        }
                    }
                }
            }
    
            if (!needsWrite) {
                mcr.setDiskWriteResult(false, true);
                return;
            }
    
            boolean backupFileExists = mBackupFile.exists();
    
            if (DEBUG) {
                backupExistsTime = System.currentTimeMillis();
            }
    
            if (!backupFileExists) {
                if (!mFile.renameTo(mBackupFile)) {
                    Log.e(TAG, "Couldn't rename file " + mFile
                          + " to backup file " + mBackupFile);
                    mcr.setDiskWriteResult(false, 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 {
            FileOutputStream str = createFileOutputStream(mFile);
    
            if (DEBUG) {
                outputStreamCreateTime = System.currentTimeMillis();
            }
    
            if (str == null) {
                mcr.setDiskWriteResult(false, false);
                return;
            }
            // 全量写入xml文件
            XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
    
            writeTime = System.currentTimeMillis();
    
            FileUtils.sync(str);
    
            fsyncTime = System.currentTimeMillis();
    
            str.close();
            ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
    
            if (DEBUG) {
                setPermTime = System.currentTimeMillis();
            }
    
            try {
                final StructStat stat = Os.stat(mFile.getPath());
                synchronized (mLock) {
                    mStatTimestamp = stat.st_mtim;
                    mStatSize = stat.st_size;
                }
            } catch (ErrnoException e) {
                // Do nothing
            }
    
            if (DEBUG) {
                fstatTime = System.currentTimeMillis();
            }
    
            // Writing was successful, delete the backup file if there is one.
            mBackupFile.delete();
    
            if (DEBUG) {
                deleteTime = System.currentTimeMillis();
            }
    
            mDiskStateGeneration = mcr.memoryStateGeneration;
            // 操作成功
            mcr.setDiskWriteResult(true, true);
    
            if (DEBUG) {
                Log.d(TAG, "write: " + (existsTime - startTime) + "/"
                        + (backupExistsTime - startTime) + "/"
                        + (outputStreamCreateTime - startTime) + "/"
                        + (writeTime - startTime) + "/"
                        + (fsyncTime - startTime) + "/"
                        + (setPermTime - startTime) + "/"
                        + (fstatTime - startTime) + "/"
                        + (deleteTime - startTime));
            }
    
            long fsyncDuration = fsyncTime - writeTime;
            mSyncTimes.add((int) fsyncDuration);
            mNumSync++;
    
            if (DEBUG || mNumSync % 1024 == 0 || fsyncDuration > MAX_FSYNC_DURATION_MILLIS) {
                mSyncTimes.log(TAG, "Time required to fsync " + mFile + ": ");
            }
    
            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, false);
    }
    

    阅读上面代码可知,writeToFile() 通过XmlUtils.writeMapXml()将前面 commitToMemory() 返回的mapToWriteToDisk(即mMap)全量写入xml文件,即每次都是建立一个空文件,然后将所有数据一次性写入,而不是增量写。因此,数据越大耗时就越长,就越可能产生ANR。

    写盘操作最终会通过mcr.setDiskWriteResult()释放锁,对于apply()还会回调postWriteRunnable的run()方法去执行等待任务awaitCommit,并将它从QueuedWork中移除。

    通过上面的分析可知,尽管apply()是异步操作,它还是可能会阻塞UI线程导致ANR,因为系统要确保在Activity退出时数据可以正常保存,这也是SharedPreferences的一个缺陷。

    参考资料:
    《Android工程化最佳实践》第4章 SharedPreferences的再封装
    https://juejin.cn/post/6881442312560803853

    相关文章

      网友评论

        本文标题:SharedPreferences源码解析

        本文链接:https://www.haomeiwen.com/subject/hsgxeltx.html