Android 重學(xué)系列 SharedPreferences源碼解析

前言

分析了MMKV的源碼解析后,我們來(lái)看看Android中常用的鍵值對(duì)組件SharedPreferences的實(shí)現(xiàn)税肪。究竟源碼中出現(xiàn)了什么問(wèn)題芙扎,導(dǎo)致了SharedPreferences的卡頓和ANR呢涌献?

如果有什么問(wèn)題胚宦,請(qǐng)到本文http://www.reibang.com/p/ca1a2129523b下進(jìn)行討論

正文

關(guān)于SharedPreferences的用法,這里就不多贅述了,如果不懂用法的枢劝,隨意找一篇看一下就好了井联。我們一般都是通過(guò)context獲取SharePreferences。

SharedPreferences sharedPreferences = getSharedPreferences("test", Context.MODE_PRIVATE);

我們就從這個(gè)方法看看究竟做了什么您旁。

獲取SharedPreferences 實(shí)例

文件:/frameworks/base/core/java/android/content/ContextWrapper.java

    @Override
    public SharedPreferences getSharedPreferences(String name, int mode) {
        return mBase.getSharedPreferences(name, mode);
    }

這里面的mBase實(shí)際上是ContextImpl烙常,這個(gè)邏輯的解析可以在資源管理或者ActivityThread的初始化兩篇文章中了解到。

文件:/frameworks/base/core/java/android/app/ContextImpl.java

    @Override
    public SharedPreferences getSharedPreferences(String name, int mode) {
        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);
                mSharedPrefsPaths.put(name, file);
            }
        }
        return getSharedPreferences(file, mode);
    }
  • 1.在每一個(gè)ContextWrapper中都會(huì)緩存一個(gè)mSharedPrefsPaths鹤盒,這個(gè)ArrayMap緩存了SharedPreferences的名字為鍵蚕脏,file文件對(duì)象為值。如果發(fā)現(xiàn)mSharedPrefsPaths沒(méi)有緩存侦锯,則會(huì)通過(guò)getSharedPreferencesPath創(chuàng)建一個(gè)file文件出來(lái)驼鞭。
    private File getPreferencesDir() {
        synchronized (mSync) {
            if (mPreferencesDir == null) {
                mPreferencesDir = new File(getDataDir(), "shared_prefs");
            }
            return ensurePrivateDirExists(mPreferencesDir);
        }
    }

    private File makeFilename(File base, String name) {
        if (name.indexOf(File.separatorChar) < 0) {
            return new File(base, name);
        }
        throw new IllegalArgumentException(
                "File " + name + " contains a path separator");
    }

    @Override
    public File getSharedPreferencesPath(String name) {
        return makeFilename(getPreferencesDir(), name + ".xml");
    }

能看到實(shí)際上這個(gè)文件就是應(yīng)用目錄下的一個(gè)xml文件

data/shared_prefs/+ sp的名字 + .xml

  • 2.getSharedPreferences通過(guò)file和mode獲取SharedPreferences實(shí)例。

getSharedPreferences

    @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;
    }
  • 1.先調(diào)用getSharedPreferencesCacheLocked 獲取緩存好的SharedPreferencesImpl實(shí)例尺碰。如果找不到實(shí)例挣棕,則檢查設(shè)置的mode是否合法,并且實(shí)例化一個(gè)新的SharedPreferencesImpl對(duì)象葱蝗,并保存在cache中穴张,同時(shí)返回sp對(duì)象细燎。
    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;
    }

能看到在ContextImpl的靜態(tài)變量sSharedPrefsCache中两曼,根據(jù)包名緩存了一個(gè)以File和SharedPreferencesImpl為鍵值對(duì)的ArrayMap。當(dāng)前ContextWrapper是從這個(gè)靜態(tài)變量中檢查是否緩存了對(duì)應(yīng)的SharedPreferencesImpl對(duì)象玻驻。

  • 2.如果mode是MODE_MULTI_PROCESS 多進(jìn)程模式悼凑,通過(guò)api低于3.0則調(diào)用sp的startReloadIfChangedUnexpectedly方法。這里比較特殊就不展開(kāi)討論了璧瞬。

SharedPreferencesImpl 實(shí)例化

文件:/frameworks/base/core/java/android/app/SharedPreferencesImpl.java

    SharedPreferencesImpl(File file, int mode) {
        mFile = file;
        mBackupFile = makeBackupFile(file);
        mMode = mode;
        mLoaded = false;
        mMap = null;
        mThrowable = null;
        startLoadFromDisk();
    }

在這個(gè)過(guò)程中有兩個(gè)很重要的過(guò)程:

  • 1.makeBackupFile 根據(jù)當(dāng)前的xml的file 獲取一個(gè)備份的file,就是原來(lái)file的路徑后加一個(gè).bak
    static File makeBackupFile(File prefsFile) {
        return new File(prefsFile.getPath() + ".bak");
    }

  • 2.startLoadFromDisk 開(kāi)始從磁盤中加載數(shù)據(jù)户辫。
    private void startLoadFromDisk() {
        synchronized (mLock) {
            mLoaded = false;
        }
        new Thread("SharedPreferencesImpl-load") {
            public void run() {
                loadFromDisk();
            }
        }.start();
    }

能看到此時(shí)是新增了一個(gè)線程調(diào)用loadFromDisk進(jìn)行磁盤的讀取操作。

loadFromDisk

    private void loadFromDisk() {
        synchronized (mLock) {
            if (mLoaded) {
                return;
            }
            if (mBackupFile.exists()) {
                mFile.delete();
                mBackupFile.renameTo(mFile);
            }
        }

...

        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);
                    map = (Map<String, Object>) XmlUtils.readMapXml(str);
                } catch (Exception e) {
                    Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
                } finally {
                    IoUtils.closeQuietly(str);
                }
            }
        } catch (ErrnoException e) {
            // An errno exception means the stat failed. Treat as empty/non-existing by
            // ignoring.
        } catch (Throwable t) {
            thrown = t;
        }

        synchronized (mLock) {
            mLoaded = true;
            mThrowable = thrown;

            // It's important that we always signal waiters, even if we'll make
            // them fail with an exception. The try-finally is pretty wide, but
            // better safe than sorry.
            try {
                if (thrown == null) {
                    if (map != null) {
                        mMap = map;
                        mStatTimestamp = stat.st_mtim;
                        mStatSize = stat.st_size;
                    } else {
                        mMap = new HashMap<>();
                    }
                }
                // In case of a thrown exception, we retain the old map. That allows
                // any open editors to commit and store updates.
            } catch (Throwable t) {
                mThrowable = t;
            } finally {
                mLock.notifyAll();
            }
        }
    }

這個(gè)方法分為三部分:

  • 1.如果mBackupFile存在嗤锉,說(shuō)明有備份文件渔欢,則把構(gòu)造函數(shù)傳遞進(jìn)來(lái)的mFile刪除,并把mBackupFile移動(dòng)到為mFile的path瘟忱。
  • 2.先讀取當(dāng)前路徑下文件的權(quán)限奥额,讀取file中xml中所有的存儲(chǔ)的鍵值對(duì)數(shù)據(jù),保存在一個(gè)臨時(shí)的HashMap中访诱。
  • 3.如果發(fā)現(xiàn)沒(méi)有任何的異常垫挨,則把臨時(shí)的map賦值給全局的mMap中,并記錄文件下的大小触菜,以及上次一次修改的時(shí)間九榔。

記住這里面有一個(gè)mLock全局對(duì)象十分重要。當(dāng)沒(méi)有初始化讀取文件中緩存的數(shù)據(jù)之前,就是通過(guò)該對(duì)象進(jìn)行阻塞哲泊。

SharedPreferences 增刪查改

SharedPreferences增加鍵值對(duì)

提一下SharedPreferences是如何進(jìn)行增刪查改的剩蟀。當(dāng)獲取到SharedPreferences對(duì)象后會(huì)調(diào)用edit方法,實(shí)例化一個(gè)SharedPreferences.Editor對(duì)象切威。

    @GuardedBy("mLock")
    private void awaitLoadedLocked() {
        if (!mLoaded) {
            BlockGuard.getThreadPolicy().onReadFromDisk();
        }
        while (!mLoaded) {
            try {
                mLock.wait();
            } catch (InterruptedException unused) {
            }
        }
        if (mThrowable != null) {
            throw new IllegalStateException(mThrowable);
        }
    }

    @Override
    public Editor edit() {
        synchronized (mLock) {
            awaitLoadedLocked();
        }

        return new EditorImpl();
    }

能看到這這個(gè)過(guò)程中喻旷,實(shí)際上會(huì)進(jìn)行一次mLock的阻塞,知道讀取磁盤的Thread工作完成后牢屋,才能實(shí)例化一個(gè)新的EditorImpl對(duì)象且预,進(jìn)行增加鍵值對(duì)的操作。

我們就以增加一個(gè)String為例子

private final Object mEditorLock = new Object();

        @GuardedBy("mEditorLock")
        private final Map<String, Object> mModified = new HashMap<>();

        @GuardedBy("mEditorLock")
        private boolean mClear = false;

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

能看到這個(gè)過(guò)程中能看到本質(zhì)上就是把鍵值對(duì)烙无,暫時(shí)存到mModified一個(gè)map中锋谐。

SharedPreferences刪除鍵值對(duì)

        @Override
        public Editor remove(String key) {
            synchronized (mEditorLock) {
                mModified.put(key, this);
                return this;
            }
        }

這里面也很簡(jiǎn)單,把當(dāng)前的Key對(duì)應(yīng)鍵值設(shè)置為Editor截酷,并沒(méi)有像常見(jiàn)設(shè)置為null涮拗。

SharedPreferences 查詢鍵值對(duì)

查詢鍵值對(duì),邏輯和增加刪除的不一致迂苛。

    @Override
    @Nullable
    public String getString(String key, @Nullable String defValue) {
        synchronized (mLock) {
            awaitLoadedLocked();
            String v = (String)mMap.get(key);
            return v != null ? v : defValue;
        }
    }

查詢鍵值對(duì)則會(huì)通過(guò)從xml中獲取的mMap緩存數(shù)據(jù)中進(jìn)行查詢三热。

SharedPreferences 同步數(shù)據(jù)

當(dāng)editor操作完成后,就會(huì)進(jìn)行數(shù)據(jù)的同步三幻。SharedPreferences同步數(shù)據(jù)到磁盤有兩種就漾,一種是commit同步,另一種是apply異步同步念搬。

SharedPreferences commit同步到磁盤中

        @Override
        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 {
              ...
            }
            notifyListeners(mcr);
            return mcr.writeToDiskResult;
        }

在這個(gè)過(guò)程中抑堡,可以分為三步:

  • 1.commitToMemory 把剛才緩存在Editor的HashMap生成一個(gè)內(nèi)存提交對(duì)象MemoryCommitResult。
  • 2.調(diào)用enqueueDiskWrite的enqueueDiskWrite朗徊,把這個(gè)提交對(duì)象提交到磁盤中首妖,并且調(diào)用writtenToDiskLatch進(jìn)行等待。
  • 3.完成后調(diào)用notifyListeners通知監(jiān)聽(tīng)已經(jīng)完成爷恳。

先來(lái)看看MemoryCommitResult 對(duì)象中承載了什么數(shù)據(jù)有缆。

MemoryCommitResult

    private static class MemoryCommitResult {
        final long memoryStateGeneration;
        @Nullable final List<String> keysModified;
        @Nullable final Set<OnSharedPreferenceChangeListener> listeners;
        final Map<String, Object> mapToWriteToDisk;
        final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);

        @GuardedBy("mWritingToDiskLock")
        volatile boolean writeToDiskResult = false;
        boolean wasWritten = false;

        private MemoryCommitResult(long memoryStateGeneration, @Nullable List<String> keysModified,
                @Nullable Set<OnSharedPreferenceChangeListener> listeners,
                Map<String, Object> mapToWriteToDisk) {
            this.memoryStateGeneration = memoryStateGeneration;
            this.keysModified = keysModified;
            this.listeners = listeners;
            this.mapToWriteToDisk = mapToWriteToDisk;
        }

        void setDiskWriteResult(boolean wasWritten, boolean result) {
            this.wasWritten = wasWritten;
            writeToDiskResult = result;
            writtenToDiskLatch.countDown();
        }
    }

能看到這個(gè)對(duì)象中有一個(gè)關(guān)鍵的對(duì)象mapToWriteToDisk,這個(gè)散列表將會(huì)持有SharePreferenceImpl.Editor用于提交到磁盤的臨時(shí)散列表温亲。

另外棚壁,這個(gè)過(guò)程中,還有writtenToDiskLatch進(jìn)行線程工作完成的計(jì)數(shù)铸豁。每當(dāng)一個(gè)線程完成工作后灌曙,將會(huì)調(diào)用writtenToDiskLatch的計(jì)數(shù)減一,實(shí)現(xiàn)阻塞放開(kāi),最后在apply或者commit的末尾通知監(jiān)聽(tīng)者已經(jīng)完成了一次操作节芥。

我們暫時(shí)放一放commit的后續(xù)流程在刺,來(lái)看看apply異步同步磁盤方法中有多少和commit相似的邏輯逆害。

apply 異步同步磁盤

        @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.addFinisher(awaitCommit);

            Runnable postWriteRunnable = new Runnable() {
                    @Override
                    public void run() {
                        awaitCommit.run();
                        QueuedWork.removeFinisher(awaitCommit);
                    }
                };

            SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

            notifyListeners(mcr);
        }

能看到大體上的邏輯和commit很相似。一樣通過(guò)commitToMemory生成一個(gè)MemoryCommitResult對(duì)象蚣驼。同樣是通過(guò)enqueueDiskWrite把寫(xiě)入磁盤的事務(wù)放入事件隊(duì)列中魄幕。

唯一不同的是多了兩個(gè)Runnable,一個(gè)是awaitCommit颖杏,另一個(gè)是postWriteRunnable纯陨。awaitCommit會(huì)進(jìn)行MemoryCommitResult的阻塞等待,會(huì)添加到QueuedWork中留储,并在postWriteRunnable中執(zhí)行awaitCommit的run方法翼抠。postWriteRunnable傳遞給enqueueDiskWrite。

commitToMemory

        private MemoryCommitResult commitToMemory() {
            long memoryStateGeneration;
            List<String> keysModified = null;
            Set<OnSharedPreferenceChangeListener> listeners = null;
            Map<String, Object> mapToWriteToDisk;

            synchronized (SharedPreferencesImpl.this.mLock) {
                // We optimistically don't make a deep copy until
                // a memory commit comes in when we're already
                // writing to disk.
                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);
                }
                mapToWriteToDisk = mMap;
                mDiskWritesInFlight++;

                boolean hasListeners = mListeners.size() > 0;
                if (hasListeners) {
                    keysModified = new ArrayList<String>();
                    listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
                }

                synchronized (mEditorLock) {
                    boolean changesMade = false;

                    if (mClear) {
                        if (!mapToWriteToDisk.isEmpty()) {
                            changesMade = true;
                            mapToWriteToDisk.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.
                        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, keysModified, listeners,
                    mapToWriteToDisk);
        }
  • 1.在這段邏輯中mDiskWritesInFlight這個(gè)計(jì)數(shù)器十分重要获讳,如果mDiskWritesInFlight這個(gè)計(jì)數(shù)大于0阴颖,說(shuō)明有其他線程在異步的進(jìn)行commit處理,由于HashMap本身不是一個(gè)線程安全的集合丐膝,因此會(huì)對(duì)全局的mMap進(jìn)行一次拷貝量愧,讓其他線程可以正常的查詢數(shù)據(jù)。

  • 2.判斷此時(shí)mModified中的value是不是null或者Editor對(duì)象帅矗,是則說(shuō)明鍵對(duì)應(yīng)的值已經(jīng)設(shè)置為null偎肃。在這里以Editor為value判空只是一個(gè)避免多線程的修改而處理的魔數(shù)。

  • 3.最后把mModified中的數(shù)據(jù)拷貝到mapToWriteToDisk浑此。一旦出現(xiàn)了數(shù)據(jù)的變化累颂,mCurrentMemoryStateGeneration的計(jì)數(shù)就會(huì)加1.最后生成MemoryCommitResult返回。

enqueueDiskWrite 推入SP的寫(xiě)入磁盤隊(duì)列

    private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                  final Runnable postWriteRunnable) {
        final boolean isFromSyncCommit = (postWriteRunnable == null);

        final Runnable writeToDiskRunnable = new Runnable() {
                @Override
                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);
    }

  • 1.writeToDiskRunnable 這個(gè)runnable能看到實(shí)際上是真正的調(diào)用writeToFile寫(xiě)入到磁盤中尤勋,每一次寫(xiě)入完畢就會(huì)減少mDiskWritesInFlight的計(jì)數(shù)喘落,說(shuō)明一個(gè)線程已經(jīng)工作了,最后再執(zhí)行postWriteRunnable的run方法最冰。

  • 2.isFromSyncCommit這個(gè)判斷的是否進(jìn)行同步處理的標(biāo)志位,就是通過(guò)postWriteRunnable是否為null判斷的稀火。如果為空說(shuō)明是commit方法進(jìn)行處理暖哨。此時(shí)會(huì)判斷mDiskWritesInFlight是否為1,為1說(shuō)明只有一個(gè)線程在執(zhí)行凰狞,那就可以直接執(zhí)行的writeToDiskRunnable的方法直接寫(xiě)入磁盤篇裁。但是mDiskWritesInFlight大于1說(shuō)明有其他線程正在準(zhǔn)備提交,那么還是和apply一樣需要放到QueuedWork中排隊(duì)執(zhí)行赡若。

在SP中所有的異步操作都會(huì)進(jìn)入到QueuedWork中進(jìn)行排隊(duì)操作达布,我們來(lái)看看
QueuedWork是怎么設(shè)計(jì)的。

QueuedWork

文件:/frameworks/base/core/java/android/app/QueuedWork.java
我們首先來(lái)看看addFinish是做了什么

    /** Finishers {@link #addFinisher added} and not yet {@link #removeFinisher removed} */
    @GuardedBy("sLock")
    private static final LinkedList<Runnable> sFinishers = new LinkedList<>();

    /** Lock for this class */
    private static final Object sLock = new Object();

    public static void addFinisher(Runnable finisher) {
        synchronized (sLock) {
            sFinishers.add(finisher);
        }
    }

    /**
     * Remove a previously {@link #addFinisher added} finisher-runnable.
     *
     * @param finisher The runnable to remove.
     */
    public static void removeFinisher(Runnable finisher) {
        synchronized (sLock) {
            sFinishers.remove(finisher);
        }
    }

在apply方法中逾冬,會(huì)把a(bǔ)waitCommit這個(gè)runnable對(duì)象存放到一個(gè)靜態(tài)集合sFinishers黍聂。

QueuedWork queue

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

這個(gè)方法中躺苦,先獲取Queuework內(nèi)部的保存的靜態(tài)Handler對(duì)象,接著根據(jù)shouldDelay標(biāo)志位是否需要延時(shí)产还,而決定在handler發(fā)送MSG_RUN消息的時(shí)候有沒(méi)有100毫秒的延時(shí)匹厘。

QueuedWork getHandler

    /** {@link #getHandler() Lazily} created handler */
    @GuardedBy("sLock")
    private static Handler sHandler = null;

    private static Handler getHandler() {
        synchronized (sLock) {
            if (sHandler == null) {
                HandlerThread handlerThread = new HandlerThread("queued-work-looper",
                        Process.THREAD_PRIORITY_FOREGROUND);
                handlerThread.start();

                sHandler = new QueuedWorkHandler(handlerThread.getLooper());
            }
            return sHandler;
        }
    }

能看到在QueueWork對(duì)象中,保存著一個(gè)單例設(shè)計(jì)的sHandler對(duì)象脐区。這個(gè)Handler是基于handlerThread中的線程生成的Looper愈诚。也就說(shuō),所有的入隊(duì)操作最后都會(huì)切換到handlerThread這個(gè)名為queued-work-looper的線程中牛隅。

QueuedWorkHandler

    private static class QueuedWorkHandler extends Handler {
        static final int MSG_RUN = 1;

        QueuedWorkHandler(Looper looper) {
            super(looper);
        }

        public void handleMessage(Message msg) {
            if (msg.what == MSG_RUN) {
                processPendingWork();
            }
        }
    }

在QueuedWorkHandler中只接受一種MSG_RUN消息的炕柔,并執(zhí)行processPendingWork的方法。

processPendingWork 執(zhí)行等待執(zhí)行的任務(wù)

    private static void processPendingWork() {
        long startTime = 0;

        synchronized (sProcessingWork) {
            LinkedList<Runnable> work;

            synchronized (sLock) {
                work = (LinkedList<Runnable>) sWork.clone();
                sWork.clear();

                // Remove all msg-s as all work will be processed now
                getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);
            }

            if (work.size() > 0) {
                for (Runnable w : work) {
                    w.run();
                }

            }
        }
    }

在這過(guò)程中媒佣,實(shí)際上十分簡(jiǎn)單汗唱,就是把之前通過(guò)queue方法加入的runnable,從sWork只能夠取出丈攒,并且移除掉所有的MSG_RUN的消息哩罪,清掉sWork的緩存,并執(zhí)行所有的queue的Runnable對(duì)象巡验。

知道怎么執(zhí)行之后际插,我們回頭看看設(shè)置進(jìn)來(lái)的runnable對(duì)象writeToDiskRunnable。

writeToDiskRunnable 執(zhí)行流程

文件:/frameworks/base/core/java/android/app/SharedPreferencesImpl.java

        final Runnable writeToDiskRunnable = new Runnable() {
                @Override
                public void run() {
                    synchronized (mWritingToDiskLock) {
                        writeToFile(mcr, isFromSyncCommit);
                    }
                    synchronized (mLock) {
                        mDiskWritesInFlight--;
                    }
                    if (postWriteRunnable != null) {
                        postWriteRunnable.run();
                    }
                }
            };

當(dāng)執(zhí)行完writeToFile寫(xiě)入磁盤方法后显设,減少mDiskWritesInFlight的計(jì)數(shù)已旧,同時(shí)調(diào)用postWriteRunnable方法此再。這個(gè)方法實(shí)際上就是awaitCommit的runnable,實(shí)際上就是調(diào)用了mcr中的writtenToDiskLatch的阻塞await方法。

writeToFile
    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;


        boolean fileExists = mFile.exists();


        // 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) {
                        if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
                            needsWrite = true;
                        }
                    }
                }
            }

            if (!needsWrite) {
                mcr.setDiskWriteResult(false, true);
                return;
            }

            boolean backupFileExists = mBackupFile.exists();


            if (!backupFileExists) {
                if (!mFile.renameTo(mBackupFile)) {
                    mcr.setDiskWriteResult(false, false);
                    return;
                }
            } else {
                mFile.delete();
            }
        }

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


            try {
                final StructStat stat = Os.stat(mFile.getPath());
                synchronized (mLock) {
                    mStatTimestamp = stat.st_mtim;
                    mStatSize = stat.st_size;
                }
            } catch (ErrnoException e) {
                // Do nothing
            }

            // Writing was successful, delete the backup file if there is one.
            mBackupFile.delete();


            mDiskStateGeneration = mcr.memoryStateGeneration;

            mcr.setDiskWriteResult(true, true);

            long fsyncDuration = fsyncTime - writeTime;
            mSyncTimes.add((int) fsyncDuration);
            mNumSync++;


            return;
        } catch (XmlPullParserException e) {
            ...
        } catch (IOException e) {
            ...
        }

        // Clean up an unsuccessfully written file
        if (mFile.exists()) {
            if (!mFile.delete()) {
                ...
            }
        }
        mcr.setDiskWriteResult(false, false);
    }

這里面的可以分為如下幾個(gè)步驟:
判斷到mFile责球,需要緩存的文件存在則執(zhí)行如下:

  • 1.如果是commit的方式或者全局異步寫(xiě)入次數(shù)mDiskStateGeneration和mcr需要提交的memoryStateGeneration一致,此時(shí)needsWrite就為true秉氧。這么做有什么好處呢址儒?有一個(gè)全局的SP操作計(jì)數(shù),就能知道當(dāng)前有多少線程進(jìn)行了SP的操作允悦,那么就沒(méi)有必要膝擂,每一次都寫(xiě)入到磁盤,只需要寫(xiě)入最后一次的內(nèi)存提交即可隙弛。如果發(fā)現(xiàn)需要提交的數(shù)據(jù)不是最后一次架馋,則會(huì)調(diào)用mcr的setDiskWriteResult結(jié)束當(dāng)前線程的等待。
            if (mDiskStateGeneration < mcr.memoryStateGeneration) {
                if (isFromSyncCommit) {
                    needsWrite = true;
                } else {
                    synchronized (mLock) {
                        if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
                            needsWrite = true;
                        }
                    }
                }
            }

            if (!needsWrite) {
                mcr.setDiskWriteResult(false, true);
                return;
            }
  • 2.如果備份文件不存在全闷,則會(huì)把當(dāng)前的mFIle嘗試著移動(dòng)路徑為備份文件mBackFile叉寂。如果連mFile的重命名失敗就直接返回了。如果備份文件存在总珠,則刪除掉原來(lái)的mFile文件屏鳍。
            if (!backupFileExists) {
                if (!mFile.renameTo(mBackupFile)) {
                    mcr.setDiskWriteResult(false, false);
                    return;
                }
            } else {
                mFile.delete();
            }

在sp第一次初始化時(shí)候勘纯,會(huì)把嘗試著備份文件轉(zhuǎn)化為存儲(chǔ)文件。如果是第一次創(chuàng)建sp對(duì)應(yīng)的存儲(chǔ)文件孕蝉,那么備份文件必定不存在屡律。在writeToFile這個(gè)方法中,就會(huì)把當(dāng)前的file重命名為備份的file. 如果存在備份文件降淮,說(shuō)明之前已經(jīng)存過(guò)東西了超埋,并從文件中讀取鍵值對(duì)到mMap全局變量中,可以直接刪除掉整個(gè)File文件佳鳖,其實(shí)就是相當(dāng)于把file的存儲(chǔ)文件給清空了霍殴。

  • 3.把mMap的數(shù)據(jù)通過(guò)io寫(xiě)入到XML mFile文件中。
            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();
  • 4.如果寫(xiě)入操作成功了系吩,那么備份文件也不需要存在了来庭,直接刪除了。并調(diào)用mcr的setDiskWriteResult穿挨,告訴阻塞的對(duì)象已經(jīng)完成了io的操作月弛。

到這里,大致上就完成了SP的讀寫(xiě)流程了科盛。

關(guān)于SP的思考與總結(jié)

總結(jié)

我們先來(lái)看那么幾個(gè)時(shí)序圖帽衙。
當(dāng)SP啟動(dòng)的時(shí)候,就會(huì)就會(huì)開(kāi)啟一個(gè)線程從磁盤中讀取贞绵,但是如果此時(shí)遇到了SP操作就會(huì)如下執(zhí)行:

SP啟動(dòng).jpg

十分簡(jiǎn)單的SP的初始化流程厉萝。

SP的備份機(jī)制

我們?cè)偃タ纯碨P的備份機(jī)制。在SP的備份機(jī)制中榨崩,實(shí)際上是由一個(gè).bak后綴的文件進(jìn)行備份谴垫。

當(dāng)loadFromDisk的時(shí)候,會(huì)生成一個(gè)線程讀取Xml的數(shù)據(jù)到mMap的緩存中母蛛。能看到翩剪,第一次讀取文件的時(shí)候,發(fā)現(xiàn)備份文件存在會(huì)進(jìn)行如下處理:

if (mBackupFile.exists()) {
                mFile.delete();
                mBackupFile.renameTo(mFile);
            }

把mFile的文件給刪除掉溯祸,把mBackupFile重新命名為mFile(xml的緩存文件名)肢专。那么這樣做就能把上一次保存在mBackupFile中完好的數(shù)據(jù)繼承到mFile中。

而mBackupFile一般來(lái)說(shuō)是實(shí)在writeToFile的時(shí)候附帶的副產(chǎn)物:

            if (!backupFileExists) {
                if (!mFile.renameTo(mBackupFile)) {
                    mcr.setDiskWriteResult(false, false);
                    return;
                }
            } else {
                mFile.delete();
            }

假設(shè)焦辅,如果我們上一次以及之前所有的磁盤讀寫(xiě)都成功了,那么備份文件就會(huì)和writeToFile小結(jié)中說(shuō)的一樣椿胯,每一次讀寫(xiě)完畢會(huì)刪除筷登。

backupFile都不存在,就會(huì)把mFile重命名路徑為mBackupFile哩盲,在每一次讀寫(xiě)之前進(jìn)行了一次備份前方。

從全局來(lái)看如果backupFile存在狈醉,說(shuō)明了之前的讀寫(xiě)出現(xiàn)了問(wèn)題。此時(shí)可以分為兩種情況:

  • 1.初始化的時(shí)候發(fā)現(xiàn)backupFile存在惠险,那么此時(shí)backupFile已經(jīng)重新刷新了原來(lái)的mFile對(duì)象苗傅,也就說(shuō)把之前數(shù)據(jù)出錯(cuò)了mFile回退到最后編輯的backupFile數(shù)據(jù)狀態(tài),并且保存到mMap中班巩。從之后就不會(huì)存在backupFile這個(gè)備份文件了渣慕。

  • 2.如果在讀寫(xiě)過(guò)程中出現(xiàn)了異常,讀寫(xiě)操作無(wú)法完成抱慌,則會(huì)生成一個(gè)備份文件backupFile逊桦。這個(gè)時(shí)候就會(huì)把之前那個(gè)mFile存儲(chǔ)出錯(cuò)的數(shù)據(jù)文件,直接刪除抑进。因?yàn)樽x寫(xiě)過(guò)程中被異常退出了强经,很有可能讀寫(xiě)的信息有誤,造成下一次啟動(dòng)無(wú)法正常讀取寺渗。干脆直接刪掉mFile匿情,重新建立一個(gè)新的mFile文件寫(xiě)入其中。如果這一次的成功了信殊,就刪掉備份文件炬称。

換句話說(shuō),SP的備份機(jī)制鸡号,實(shí)際上備份的是上一次正常讀寫(xiě)成功磁盤存儲(chǔ)機(jī)制转砖。

SP跨進(jìn)程的mode在其中的作用

在整個(gè)流程中mode正在起作用的位置在writeToFile中,寫(xiě)如磁盤操作結(jié)束后的下面一行代碼中:

ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);

文件:/frameworks/base/core/java/android/app/ContextImpl.java

    static void setFilePermissionsFromMode(String name, int mode,
            int extraPermissions) {
        int perms = FileUtils.S_IRUSR|FileUtils.S_IWUSR
            |FileUtils.S_IRGRP|FileUtils.S_IWGRP
            |extraPermissions;
        if ((mode&MODE_WORLD_READABLE) != 0) {
            perms |= FileUtils.S_IROTH;
        }
        if ((mode&MODE_WORLD_WRITEABLE) != 0) {
            perms |= FileUtils.S_IWOTH;
        }
        FileUtils.setPermissions(name, perms, -1, -1);
    }
    public static int setPermissions(String path, int mode, int uid, int gid) {
        try {
            Os.chmod(path, mode);
        } catch (ErrnoException e) {
            Slog.w(TAG, "Failed to chmod(" + path + "): " + e);
            return e.errno;
        }

        if (uid >= 0 || gid >= 0) {
            try {
                Os.chown(path, uid, gid);
            } catch (ErrnoException e) {
                Slog.w(TAG, "Failed to chown(" + path + "): " + e);
                return e.errno;
            }
        }

        return 0;
    }
//用戶
    public static final int S_IRWXU = 00700;
    public static final int S_IRUSR = 00400;
    public static final int S_IWUSR = 00200;
    public static final int S_IXUSR = 00100;
//組
    public static final int S_IRWXG = 00070;
    public static final int S_IRGRP = 00040;
    public static final int S_IWGRP = 00020;
    public static final int S_IXGRP = 00010;
//其他人
    public static final int S_IRWXO = 00007;
    public static final int S_IROTH = 00004;
    public static final int S_IWOTH = 00002;
    public static final int S_IXOTH = 00001;

其實(shí)可以看到這個(gè)過(guò)程中就是通過(guò)chmod方法設(shè)置了文件的在系統(tǒng)中的權(quán)限。從名稱就能知道鲸伴,這個(gè)方法每一次讀寫(xiě)之后都會(huì)默認(rèn)給用戶和組都能夠進(jìn)行讀寫(xiě)府蔗。

在這個(gè)方法會(huì)判斷從上面?zhèn)飨聛?lái)的mode,是否打開(kāi)了MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE汞窗。

public static final int MODE_WORLD_WRITEABLE = 0x0002;
public static final int MODE_WORLD_READABLE = 0x0001;

我們來(lái)看看SP設(shè)置多進(jìn)程讀寫(xiě)時(shí)候的標(biāo)志位:

public static final int MODE_MULTI_PROCESS = 0x0004;

換算到二進(jìn)制就是100和010姓赤,以及100和001相與都是0.連這里都沒(méi)有進(jìn)行處理的話,說(shuō)明SP在Android 9.0根本沒(méi)有進(jìn)行多進(jìn)程讀寫(xiě)文件的互斥處理.

我們看看MODE_MULTI_PROCESS的注釋:

MODE_MULTI_PROCESS does not work reliably in
     * some versions of Android, and furthermore does not provide any
     * mechanism for reconciling concurrent modifications across
     * processes.  Applications should not attempt to use it.  Instead,
     * they should use an explicit cross-process data management
     * approach such as {@link android.content.ContentProvider ContentProvider}.

這里面已經(jīng)說(shuō)明了仲吏,在某些Android版本中SP將不會(huì)提供跨進(jìn)程讀寫(xiě)文件的保護(hù)不铆,如果有需求,請(qǐng)使用ContentProvider裹唆。

最后上一副流程圖進(jìn)行總結(jié)整個(gè)流程:


SP時(shí)序圖.jpg

思考

能看到實(shí)際上我們通過(guò)Context獲取sp對(duì)象誓斥,在這個(gè)過(guò)程中,我們完全可以復(fù)寫(xiě)Context中g(shù)etSharePreferences的方法许帐,進(jìn)而返回自己定義的SharePreferences劳坑。當(dāng)然我們需要自己實(shí)現(xiàn)SharePreferences以及SharePreferences.Editor.

同理我們可以看到MMKV中的實(shí)現(xiàn):

public class MMKV implements SharedPreferences, SharedPreferences.Editor 

那么,實(shí)際上我們這么無(wú)縫遷移MMKV到SP中:

    override fun getSharedPreferences(name: String?, mode: Int): SharedPreferences {
        val mmkv = MMKV.mmkvWithID(name,mode)

        if(mmkv.getBoolean("hasTransport",false)){
            var originPrefences = super.getSharedPreferences(name, mode)
            mmkv.importFromSharedPreferences(originPrefences)
            originPrefences.edit().clear().apply()
            mmkv.encode("hasTransport",true)
        }
        return mmkv
    }

只需要在Application成畦,Activity距芬,CP下復(fù)寫(xiě)該方法為如上涝开,就能在上層使用了SP的方式,實(shí)際上底層卻是調(diào)用了mmkv的方法框仔。

了解到如何無(wú)縫使用MMKV之后舀武,我們?cè)賮?lái)聊一下在MMKV一文中和大家提過(guò)SP的幾個(gè)問(wèn)題:

  • 1.跨進(jìn)程不安全
  • 2.加載緩慢
  • 3.全量寫(xiě)入
  • 4.卡頓

SP跨進(jìn)程不安全

對(duì)于第一個(gè)問(wèn)題,在上一節(jié)我們聊過(guò)了SP的實(shí)現(xiàn)离斩。實(shí)際上它并沒(méi)有對(duì)多進(jìn)程讀寫(xiě)進(jìn)行保護(hù)

SP加載緩慢

對(duì)于這個(gè)問(wèn)題银舱,其實(shí)就是指初始化的時(shí)候SPImpl的時(shí)候,新建立了一個(gè)線程進(jìn)行讀取Xml的數(shù)據(jù):

        new Thread("SharedPreferencesImpl-load") {
            public void run() {
                loadFromDisk();
            }
        }.start();

然而在SP每一次操作都必須等待這個(gè)線程讀取完磁盤才能進(jìn)行下一步的操作捐腿。很多開(kāi)發(fā)者估計(jì)都是把SP操作放到ui線程中進(jìn)行的吧纵朋。如果開(kāi)發(fā)者不注意,保存的XML數(shù)據(jù)過(guò)于龐大茄袖,就會(huì)造成ui卡頓甚至ANR操软。

為了讓該線程更加快速的處理,Android系統(tǒng)應(yīng)該要把SP讀取磁盤使用一個(gè)緩存線程池進(jìn)行處理宪祥,線程可以立即執(zhí)行

private static volatile ExecutorService sCachedThreadPool = Executors.newCachedThreadPool();
private void startLoadFromDisk() {
        synchronized (this) {
            mLoaded = false;
        }

        sCachedThreadPool.execute(new Runnable() {
            @Override
            public void run() {
                loadFromDisk();
            }
        });
    }

通過(guò)這種方式聂薪,就能讓在線程池中立即獲取還存活的線程進(jìn)行直接的處理磁盤讀取任務(wù)。

當(dāng)然蝗羊,還有一點(diǎn)需要注意由于磁盤讀取的和MMKV不一樣藏澳,MMKV是直接通過(guò)共享內(nèi)存的方式直接把內(nèi)存文件映射到虛擬內(nèi)存中,應(yīng)用可以直接訪問(wèn)耀找。而SP的讀取則是通過(guò)普通的io讀寫(xiě)翔悠,這樣需要經(jīng)過(guò)一次進(jìn)入內(nèi)核一次,就會(huì)造成速度上比mmap要慢上不少野芒。

兩者都應(yīng)該注意寫(xiě)如數(shù)據(jù)的大行畛睢:
MMKV雖然有trim方法,但是并沒(méi)有幫你監(jiān)控虛擬內(nèi)存的情況狞悲,這也是MMKV可以后續(xù)優(yōu)化的地方撮抓,如果不注意數(shù)據(jù)的大小一味的存儲(chǔ),會(huì)造成虛擬內(nèi)存爆炸導(dǎo)致應(yīng)用異常摇锋。

SP由于是一口氣從磁盤中讀取所有的數(shù)據(jù)丹拯,數(shù)據(jù)過(guò)于龐大就會(huì)造成SP初始化十分慢,導(dǎo)致后續(xù)操作產(chǎn)生ANR荸恕。

全量寫(xiě)入

這個(gè)問(wèn)題可以很容易的看到乖酬,在writeToFile方法中,是把所有從Xml緩存文件解析到的數(shù)據(jù)統(tǒng)統(tǒng)保存會(huì)緩存文件中融求,這樣就會(huì)造成了寫(xiě)入十分緩慢剑刑。而反觀MMKV,由于它本身就是支持append模式在后面映射內(nèi)存末尾繼續(xù)添加鍵值對(duì)双肤,這樣寫(xiě)入速度比起SP快的不是一星半點(diǎn)施掏。

當(dāng)然也是因?yàn)檫@種機(jī)制的問(wèn)題,SP和MMKV的recover模式從根本的策略上不同茅糜。SP由于是全量讀寫(xiě)七芭,這樣就能完成的保存一份備份文件。而MMKV一般是內(nèi)存末尾追加模式以及多進(jìn)程讀寫(xiě)保護(hù)的策略蔑赘,雖然讀寫(xiě)很快狸驳,但是這也造成了MMKV很難對(duì)全文件進(jìn)行一次備份處理,只能不斷的保證最后一次讀寫(xiě)正常缩赛,并嘗試讀取緩存文件中完好數(shù)據(jù)盡可能恢復(fù)完好的數(shù)據(jù)耙箍。

接下來(lái)比較Android 7.0低版本中SP的實(shí)現(xiàn),我們翻翻以前老版本的SP的源碼酥馍。我們看看Android 7.0的源碼:

 private void writeToFile(MemoryCommitResult mcr) {
        if (mFile.exists()) {
            if (!mcr.changesMade) {
                mcr.setDiskWriteResult(true);
                return;
            }
            if (!mBackupFile.exists()) {
                if (!mFile.renameTo(mBackupFile)) {
                    mcr.setDiskWriteResult(false);
                    return;
                }
            } else {
                mFile.delete();
            }
        }

        try {
            FileOutputStream str = createFileOutputStream(mFile);
            if (str == null) {
                mcr.setDiskWriteResult(false);
                return;
            }
            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
            }
            mBackupFile.delete();
            mcr.setDiskWriteResult(true);
            return;
        } catch (XmlPullParserException e) {
        } catch (IOException e) {
        }

        mcr.setDiskWriteResult(false);
    }

能看到在Android 7.0中并沒(méi)有像Android 9.0一樣辩昆,對(duì)apply異步寫(xiě)入進(jìn)行一次needWrites的標(biāo)志位判斷,避免多次寫(xiě)入磁盤旨袒。在Android 7.0中只要有一個(gè)apply的操作汁针,就會(huì)進(jìn)行一次磁盤的讀寫(xiě),這樣就會(huì)造成io的上繁忙砚尽,性能大大的降低施无。

在我看來(lái),Android 9.0的優(yōu)化方案也不是最好的必孤。

在SP中有一個(gè)參數(shù)mDiskWritesInFlight對(duì)apply或者commit的同步操作進(jìn)行計(jì)數(shù)猾骡。我們完全可以做成把多個(gè)apply合并成一個(gè)apply操作,只需要判斷到mDiskWritesInFlight小于等于0敷搪,說(shuō)明SP其他的操作經(jīng)完成了兴想,可以進(jìn)行SharedPreferencesImpl.this.enqueueDiskWrite操作,這樣的結(jié)果也不會(huì)發(fā)生變化购啄,因?yàn)樵谶@之前一直在操作內(nèi)存襟企。

        public void apply() {
            final MemoryCommitResult mcr = commitToMemory();

            boolean hasDiskWritesInFlight = false;
            synchronized (SharedPreferencesImpl.this) {
                hasDiskWritesInFlight = mDiskWritesInFlight > 0;
            }

            if (!hasDiskWritesInFlight) {
                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);
                    }
                };

                SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
            }

            notifyListeners(mcr);
        }

能通過(guò)這種方式減少apply的操作,減少Q(mào)ueueWork中遍歷的任務(wù)隊(duì)列狮含。

卡頓

那么除了上文我說(shuō)過(guò)的顽悼,因?yàn)镾P讀取XML緩存文件過(guò)大使得初始化時(shí)間太長(zhǎng)而導(dǎo)致ANR之外。其實(shí)在AcitivtyThread中有什么一段代碼几迄,不知道你們有沒(méi)有在我的Activity啟動(dòng)流程系列文章中有沒(méi)有發(fā)現(xiàn)在onPause以及onStop生命周期一文中有這么一段代碼:

    public void handlePauseActivity(IBinder token, boolean finished, boolean userLeaving,
            int configChanges, PendingTransactionActions pendingActions, String reason) {
        ActivityClientRecord r = mActivities.get(token);
        if (r != null) {
            if (userLeaving) {
                performUserLeavingActivity(r);
            }

            r.activity.mConfigChangeFlags |= configChanges;
            performPauseActivity(r, finished, reason, pendingActions);

            // Make sure any pending writes are now committed.
            if (r.isPreHoneycomb()) {
                QueuedWork.waitToFinish();
            }
            mSomeActivitiesChanged = true;
        }
    }
    public void handleStopActivity(IBinder token, boolean show, int configChanges,
            PendingTransactionActions pendingActions, boolean finalStateRequest, String reason) {
        final ActivityClientRecord r = mActivities.get(token);
        r.activity.mConfigChangeFlags |= configChanges;

        final StopInfo stopInfo = new StopInfo();
        performStopActivityInner(r, stopInfo, show, true /* saveState */, finalStateRequest,
                reason);

        updateVisibility(r, show);

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

        stopInfo.setActivity(r);
        stopInfo.setState(r.state);
        stopInfo.setPersistentState(r.persistentState);
        pendingActions.setStopInfo(stopInfo);
        mSomeActivitiesChanged = true;
    }

能看到在低于Android api 11會(huì)在onPause時(shí)候調(diào)用QueuedWork的waitToFinish蔚龙。大于11則會(huì)在onStop調(diào)用QueuedWork.waitToFinish.這兩個(gè)方法都會(huì)對(duì)QueuedWork的中的任務(wù)執(zhí)行進(jìn)行等待,直到執(zhí)行完畢映胁。

QueuedWork waitToFinish

    public static void waitToFinish() {
        long startTime = System.currentTimeMillis();
        boolean hadMessages = false;

        Handler handler = getHandler();

        synchronized (sLock) {
            if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) {
                // Delayed work will be processed at processPendingWork() below
                handler.removeMessages(QueuedWorkHandler.MSG_RUN);
...
            }

            // We should not delay any work as this might delay the finishers
            sCanDelay = false;
        }

        StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
        try {
            processPendingWork();
        } finally {
            StrictMode.setThreadPolicy(oldPolicy);
        }

        try {
            while (true) {
                Runnable finisher;

                synchronized (sLock) {
                    finisher = sFinishers.poll();
                }

                if (finisher == null) {
                    break;
                }

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

        synchronized (sLock) {
            long waitTime = System.currentTimeMillis() - startTime;

            if (waitTime > 0 || hadMessages) {
                mWaitTimes.add(Long.valueOf(waitTime).intValue());
                mNumWaits++;

....
            }
        }
    }

能看到木羹,在這個(gè)過(guò)程中實(shí)際上就是上文我提到過(guò)的addFinisher的方法。這個(gè)方法實(shí)際上就是在apply異步同步到磁盤的Runnable對(duì)象awaitCommit。

從全局的設(shè)計(jì)來(lái)看坑填,一個(gè)Finisher會(huì)對(duì)應(yīng)一個(gè)執(zhí)行磁盤寫(xiě)入的方法抛人。所以在waitToFinish這個(gè)方法實(shí)際上就是檢查還有多少個(gè)Finisher沒(méi)有被銷毀,那么就有多少任務(wù)還沒(méi)有執(zhí)行完成脐瑰。

在processPendingWork執(zhí)行完成之前妖枚,都需要調(diào)用finisher的run方法,對(duì)mcr中的CountDownLatch進(jìn)行等待阻塞苍在。

換句話說(shuō)绝页,當(dāng)我們的SP寫(xiě)入耗時(shí)過(guò)大,就會(huì)造成Activity 暫停時(shí)候卡住寂恬,從而導(dǎo)致AMS服務(wù)那邊的倒計(jì)時(shí)超時(shí)爆了ANR续誉。而這種情況可能很會(huì)見(jiàn)的不少,因?yàn)镾P本身就全量寫(xiě)入初肉。

這四個(gè)缺點(diǎn)就是平時(shí)開(kāi)發(fā)中遇到酷鸦,并且通過(guò)源碼分析后,發(fā)現(xiàn)系統(tǒng)實(shí)現(xiàn)不合理的地方朴译。而MMKV都能對(duì)這四個(gè)問(wèn)題有很好的彌補(bǔ)以及提升井佑。

后話

關(guān)于存儲(chǔ)優(yōu)化的第一部分就完成了,不過(guò)關(guān)于存儲(chǔ)還有很多可以聊聊眠寿,比如數(shù)據(jù)庫(kù)等等躬翁。不過(guò)到這里我們先點(diǎn)到為止,后續(xù)我們繼續(xù)View的繪制流程源碼解析盯拱。

從這一篇文章來(lái)看盒发,源碼本身雖然經(jīng)過(guò)千錘百煉,不過(guò)還是有不少設(shè)計(jì)不是很好的地方狡逢。SP除了這四個(gè)性能的問(wèn)題之外宁舰,還有一些代碼設(shè)計(jì)層面上我個(gè)人覺(jué)得不夠好的地方,比如緩存對(duì)象為什么一定要用SharedPreferencesImpl奢浑,而不用SharedPreferences接口蛮艰,這樣系統(tǒng)就能緩存我們自定義的SP了。

不過(guò)這些都是后話了雀彼。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末壤蚜,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子徊哑,更是在濱河造成了極大的恐慌袜刷,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,591評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件莺丑,死亡現(xiàn)場(chǎng)離奇詭異著蟹,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門萧豆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)奸披,“玉大人,你說(shuō)我怎么就攤上這事炕横≡茨冢” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,823評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵份殿,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我嗽交,道長(zhǎng)卿嘲,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,204評(píng)論 1 292
  • 正文 為了忘掉前任夫壁,我火速辦了婚禮拾枣,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘盒让。我一直安慰自己梅肤,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,228評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布邑茄。 她就那樣靜靜地躺著姨蝴,像睡著了一般。 火紅的嫁衣襯著肌膚如雪肺缕。 梳的紋絲不亂的頭發(fā)上左医,一...
    開(kāi)封第一講書(shū)人閱讀 51,190評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音同木,去河邊找鬼浮梢。 笑死,一個(gè)胖子當(dāng)著我的面吹牛彤路,可吹牛的內(nèi)容都是我干的秕硝。 我是一名探鬼主播,決...
    沈念sama閱讀 40,078評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼洲尊,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼远豺!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起颊郎,我...
    開(kāi)封第一講書(shū)人閱讀 38,923評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤憋飞,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后姆吭,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體榛做,經(jīng)...
    沈念sama閱讀 45,334評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,550評(píng)論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了检眯。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片厘擂。...
    茶點(diǎn)故事閱讀 39,727評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖锰瘸,靈堂內(nèi)的尸體忽然破棺而出刽严,到底是詐尸還是另有隱情,我是刑警寧澤避凝,帶...
    沈念sama閱讀 35,428評(píng)論 5 343
  • 正文 年R本政府宣布舞萄,位于F島的核電站,受9級(jí)特大地震影響管削,放射性物質(zhì)發(fā)生泄漏倒脓。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,022評(píng)論 3 326
  • 文/蒙蒙 一含思、第九天 我趴在偏房一處隱蔽的房頂上張望崎弃。 院中可真熱鬧,春花似錦含潘、人聲如沸饲做。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,672評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)盆均。三九已至,卻和暖如春腾窝,著一層夾襖步出監(jiān)牢的瞬間缀踪,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,826評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工虹脯, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留驴娃,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,734評(píng)論 2 368
  • 正文 我出身青樓循集,卻偏偏與公主長(zhǎng)得像唇敞,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子咒彤,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,619評(píng)論 2 354