SharedPreferences 源码分析及注意事项

SharedPreferences 源码分析及使用事项

作为Android 轻量级的存储工具,SharedPreferences被广泛使用,API 简洁明了,易学易用,为广大程序小哥哥们喜闻乐见。殊不知,一片和谐的环境下,蕴藏着不少危机,本文将从源码角度进行解析,并附上踩过的一些坑。

一般用法

SharedPreferences pref = mAppContext.getSharedPreferences(prefName, Context.MODE_PRIVATE);

源码分析

Conext#getSharedPreferences的内部实现,具体实现在

ContextImpl.getSharedPreferences()
 @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";
            }
        }

        File file;
        synchronized (ContextImpl.class) {
            if (mSharedPrefsPaths == null) {
                mSharedPrefsPaths = new ArrayMap<>();
            }
            file = mSharedPrefsPaths.get(name);
            if (file == null) {
                file = getSharedPreferencesPath(name);//一个context可以有N多SharedPreferences文件
                mSharedPrefsPaths.put(name, file);
            }
        }
        return getSharedPreferences(file, mode);//初始化后,以后从缓存读取file,再构建SharedPreference,mSharedPrefsPaths没有做销毁处理.如果SharedPreferences很多,map会很大,会占用更多内存。
再看getSharedPreferences(file, mode)方法
@Override
    public SharedPreferences getSharedPreferences(File file, int mode) {
        checkMode(mode);
        if (getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.O) {//8.0新特性,用户加锁时,会抛异常,实测暂未开启
            if (isCredentialProtectedStorage()
                    && !getSystemService(StorageManager.class).isUserKeyUnlocked(
                            UserHandle.myUserId())
                    && !isBuggy()) {
                throw new IllegalStateException("SharedPreferences in credential encrypted "
                        + "storage are not available until after user is unlocked");
            }
        }
        SharedPreferencesImpl sp;
        synchronized (ContextImpl.class) {
            final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();//SharedPreferencesImpl缓存池
            sp = cache.get(file);
            if (sp == null) {
                sp = new SharedPreferencesImpl(file, mode);//一个name对应一个SharedPreferences,可理解为单例
                cache.put(file, sp);
                return sp;
            }
        }
        if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||//跨进程,Google不推荐
            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;
    }
具体分析SharePerferenceImpl
     // Lock ordering rules: //一共3把锁,注意加锁顺序
     // - acquire SharedPreferencesImpl.mLock before EditorImpl.mLock
     // - acquire mWritingToDiskLock before EditorImpl.mLock
构造器
SharedPreferencesImpl(File file, int mode) {
        mFile = file;
        mBackupFile = makeBackupFile(file);//灾备文件
        mMode = mode;
        mLoaded = false;
        mMap = null;
        startLoadFromDisk();//核心方法
    }

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

    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);
                    map = XmlUtils.readMapXml(str);
                } catch (Exception e) {
                    Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
                } finally {
                    IoUtils.closeQuietly(str);
                }
            }
        } catch (ErrnoException e) {
            /* ignore */
        }

        synchronized (mLock) {
            mLoaded = true;
            if (map != null) {
                mMap = map;//mMap 是该SP文件,所包含的所有Key,Value
                mStatTimestamp = stat.st_mtime;
                mStatSize = stat.st_size;
            } else {
                mMap = new HashMap<>();
            }
            mLock.notifyAll();//Load完成,发一个notifyAll,表示已经准备好SP文件,阻塞结束
        }
    }

get方法

    @Nullable
    public String getString(String key, @Nullable String defValue) {
        synchronized (mLock) { //线程安全 如果同时有多个方法,操作SP,这里可能会阻塞,给我们一个启示,优先级高的SP文件,最好单独保存
            awaitLoadedLocked();// 对于单进程来说,get方法,应该不会在这里阻塞
            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 {
                mLock.wait();
            } catch (InterruptedException unused) {
            }
        }
    }

Put方法

put方法,实际通过EditorImpl 完成,Editor是专为SharedPreferences 私人定制

 @GuardedBy("mLock")
 private final Map<String, Object> mModified = Maps.newHashMap();

public Editor putString(String key, @Nullable String value) {
            synchronized (mLock) {
                mModified.put(key, value);
                return this;
            }
        }

从上面可以看出,put方法,仅仅是把key,value存入内存mModified,并没有保存至磁盘。有点类似于事务处理,必须是最后一步提交事务,才算是正式生效了。

最重要的两个方法

 public void apply() {//异步回写磁盘
            final long startTime = System.currentTimeMillis();

            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");
                        }
                    }
                };

            QueuedWork.addFinisher(awaitCommit);

            Runnable postWriteRunnable = new Runnable() {
                    public void run() {
                        awaitCommit.run();
                        QueuedWork.removeFinisher(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);
        }

这次有出比较要命的地方, QueuedWork.addFinisher(awaitCommit);虽然是异步操作,但也可能会阻塞主线程

要点在ActivityThread handleXXActivity方法中

  private void handleStopActivity(IBinder token, boolean show, int configChanges, int seq) {
        ActivityClientRecord r = mActivities.get(token);
        if (!checkAndUpdateLifecycleSeq(seq, r, "stopActivity")) {
            return;
        }
        r.activity.mConfigChangeFlags |= configChanges;

        StopInfo info = new StopInfo();
        performStopActivityInner(r, info, show, true, "handleStopActivity");

        if (localLOGV) Slog.v(
            TAG, "Finishing stop of " + r + ": show=" + show
            + " win=" + r.window);

        updateVisibility(r, show);

        // Make sure any pending writes are now committed.
        if (!r.isPreHoneycomb()) {
            QueuedWork.waitToFinish();
        }
       .........

再看 这个 QueuedWork.waitToFinish();

 public static void waitToFinish() {
     .........
        try {
            while (true) {
                Runnable finisher;

                synchronized (sLock) {  //加锁,轮询,同步,忐忑不?
                    finisher = sFinishers.poll();
                }

                if (finisher == null) {
                    break;
                }

                finisher.run();
            }
        } finally {
            sCanDelay = true;
        }
        ......

    }
从上面能看出来,Activity Service 一些操作,是需要等到SP操作结束的。所以即使是异步的apply操作也是有可能阻塞主线程的。使用要慎重。

再来看commit方法

 public boolean commit() {
            long startTime = 0;

            if (DEBUG) {
                startTime = System.currentTimeMillis();
            }

            MemoryCommitResult mcr = commitToMemory(); //回写内存

            SharedPreferencesImpl.this.enqueueDiskWrite(
                mcr, null /* sync write on this thread okay */);
            try {
                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);
            return mcr.writeToDiskResult;
        }

SharedPreferencesImpl.this.enqueueDiskWrite(
mcr, null /* sync write on this thread okay */); 是异步的关键方法

几经辗转,会调用

    public static void queue(Runnable work, boolean shouldDelay) {
        Handler handler = getHandler();

        synchronized (sLock) {
            sWork.add(work);

            if (shouldDelay && sCanDelay) {
                handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
            } else {
                handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
            }
        }
    }

总结一下:

1、 apply 方法也有可能阻塞主线程,尽量保证SP操作时间可控,不要存储过大文件

以我们工程为例,一个key,value就有18k之巨,直接导致后台监控到大量的SP相关的卡顿

2、不建议通过SP,实现进程共享,完全质量无保障

3、getXXX方法,本身也有可能阻塞(别的地方也在操作SP),所以高优先级SP文件,建议独立开来

4、apply 异步不需要等待执行结果,但也使用到了线程池,因此尽量合并提交

5、commit 操作,同步操作,除非希望立即获取返回结果,否则尽量使用apply

6、如何解决恐怖的apply 方法阻塞主线程的问题???

有不少开发规范都规定,尽量使用apply方法,除非想理解获得提交状态,否则不要用commit方法。

对于小型SP存储来说,是没有问题的。 但大型项目,通常SP文件品种繁多,且单个SP文件,所包含的各色key,value(单个可能都不大,但好虎架不住狼多),会导致另一个致命问题,ANR。

那么解决之道是什么呢?
答案是为了性能计,弃用apply。
替代方案为:仿照apply源码的做法,需要apply的地方,采用子线程,异步提交commit。这样,既避免了apply方法,影响ActivityThread重要方法问题,也避免了同步操作阻塞主线程。

    原文作者:瀚海网虫
    原文地址: https://blog.csdn.net/jjbheda/article/details/80892840
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞