每日一問(wèn):談?wù)?SharedPreferences 的 apply() 和 commit()

SharedPreferences 應(yīng)該是任何一名 Android 初學(xué)者都知道的存儲(chǔ)類了,它輕量游昼,適合用于保存軟件配置等參數(shù)凿掂。以鍵值對(duì)的 XML 文件形式存儲(chǔ)在本地,程序卸載后也會(huì)一并清除顶考,不會(huì)殘留信息。

使用起來(lái)也非常簡(jiǎn)單妖泄。

// 讀取
val sharedPreferences = getSharedPreferences("123", Context.MODE_PRIVATE)
val string = sharedPreferences.getString("123","")
// 寫(xiě)入
val editor = sharedPreferences.edit()
editor.putString("123","123")
editor.commit()

當(dāng)我們寫(xiě)下這樣的代碼的時(shí)候驹沿,IDE 極易出現(xiàn)一個(gè)警告,提示我們用 apply() 來(lái)替換 commit()蹈胡。原因也很簡(jiǎn)單渊季,因?yàn)?commit() 是同步的朋蔫,而 apply() 采用異步的方式通常來(lái)說(shuō)效率會(huì)更高一些。但是却汉,當(dāng)我們把 editor.commit() 的返回值賦給一個(gè)變量的時(shí)候驯妄,這時(shí)候就會(huì)發(fā)現(xiàn) IDE 沒(méi)有了警告。這是因?yàn)?IDE 認(rèn)為我們想要使用 editor.commit() 的返回值了合砂,所以青扔,通常來(lái)說(shuō),在我們不關(guān)心操作結(jié)果的時(shí)候翩伪,我們更傾向于使用 apply() 進(jìn)行寫(xiě)入的操作微猖。

獲取 SharedPreferences 實(shí)例

我們可以通過(guò) 3 種方式來(lái)獲取 SharedPreferences 的實(shí)例。
首先當(dāng)然是我們最常見(jiàn)的寫(xiě)法缘屹。

getSharedPreferences("123", Context.MODE_PRIVATE)

Context 的任意子類都可以直接通過(guò) getSharedPreferences() 方法獲取到 SharedPreferences 的實(shí)例凛剥,接受兩個(gè)參數(shù),分別對(duì)應(yīng) XML 文件的名字和操作模式轻姿。其中 MODE_WORLD_READABLEMODE_WORLD_WRITEABLE 這兩種模式已在 Android 4.2 版本中被廢棄犁珠。

  • Context.MODE_PRIVATE: 指定該 SharedPreferences 數(shù)據(jù)只能被本應(yīng)用程序讀、寫(xiě)互亮;
  • Context.MODE_WORLD_READABLE: 指定該 SharedPreferences 數(shù)據(jù)能被其他應(yīng)用程序讀犁享,但不能寫(xiě);
  • Context.MODE_WORLD_WRITEABLE: 指定該 SharedPreferences 數(shù)據(jù)能被其他應(yīng)用程序讀豹休;
  • Context.MODE_APPEND:該模式會(huì)檢查文件是否存在炊昆,存在就往文件追加內(nèi)容,否則就創(chuàng)建新文件慕爬;

另外在 Activity 的實(shí)現(xiàn)中,還可以直接通過(guò) getPreferences() 獲取屏积,實(shí)際上也就把當(dāng)前 Activity 的類名作為文件名參數(shù)医窿。

public SharedPreferences getPreferences(@Context.PreferencesMode int mode) {
    return getSharedPreferences(getLocalClassName(), mode);
}   

此外,我們也可以通過(guò) PreferenceManagergetDefaultSharedPreferences() 獲取到炊林。

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

可以很明顯的看到姥卢,這個(gè)方式就是在直接把當(dāng)前應(yīng)用的包名作為前綴來(lái)進(jìn)行命名的。

注意:如果在 Fragment 中使用 SharedPreferences 時(shí)渣聚,SharedPreferences 的初始化盡量放在 onAttach(Activity activity) 里面進(jìn)行 独榴,否則可能會(huì)報(bào)空指針,即 getActivity() 會(huì)可能返回為空奕枝。

SharedPreferences 源碼(基于 API 28)

有較多 SharedPreferences 使用經(jīng)驗(yàn)的人棺榔,就會(huì)發(fā)現(xiàn) SharedPreferences 其實(shí)具備挺多的坑,但這些坑主要都是因?yàn)椴皇煜て渲姓嬲脑硭鶎?dǎo)致的隘道,所以症歇,筆者在這里郎笆,帶大家一起揭開(kāi) SharedPreferences 的神秘面紗。

SharedPreferences 實(shí)例獲取

前面講了 SharedPreferences 有三種獲取實(shí)例的方法忘晤,但歸根結(jié)底都是調(diào)用的 ContextgetSharedPreferences() 方法宛蚓。由于 Android 的 Context 類采用的是裝飾者模式,而裝飾者對(duì)象其實(shí)就是 ContextImpl设塔,所以我們來(lái)看看源碼是怎么實(shí)現(xiàn)的凄吏。

// 存放的是名稱和文件夾的映射,實(shí)際上這個(gè)名稱就是我們外面?zhèn)鬟M(jìn)來(lái)的 name
private ArrayMap<String, File> mSharedPrefsPaths;

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);
            mSharedPrefsPaths.put(name, file);
        }
    }
    return getSharedPreferences(file, mode);
}

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

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

可以很明顯的看到闰蛔,內(nèi)部是采用 ArrayMap 來(lái)做的處理痕钢,而這個(gè) mSharedPrefsPaths 主要是用于存放名稱和文件夾的映射,實(shí)際上這個(gè)名稱就是我們外面?zhèn)鬟M(jìn)來(lái)的 name钞护,這時(shí)候我們通過(guò) name 拿到我們的 File盖喷,如果當(dāng)前池子中沒(méi)有的話,則直接新建一個(gè) File难咕,并放入到 mSharedPrefsPaths 中课梳。最后還是調(diào)用的重載方法 getSharedPreferences(File,mode)

// 存放包名與ArrayMap鍵值對(duì),初始化時(shí)會(huì)默認(rèn)以包名作為鍵值對(duì)中的 Key余佃,注意這是個(gè) static 變量
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;

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

可以看到暮刃,又采用了一個(gè) ArrayMap 來(lái)存放文件和 SharedPreferencesImpl 組成的鍵值對(duì),然后通過(guò)通過(guò)單例的方式返回一個(gè) SharedPreferences 對(duì)象爆土,實(shí)際上是 SharedPreferences 的實(shí)現(xiàn)類 SharedPreferencesImpl椭懊,而且在其中還建立了一個(gè)內(nèi)部緩存機(jī)制。

所以步势,從上面的分析中氧猬,我們能知道 對(duì)于一個(gè)相同的 name,我們獲取到的都是同一個(gè) SharedPreferencesImpl 對(duì)象坏瘩。

SharedPreferencesImpl

在上面的操作中盅抚,我們可以看到在第一次調(diào)用 getSharedPreferences 的時(shí)候,我們會(huì)去構(gòu)造一個(gè) SharedPreferencesImpl 對(duì)象倔矾,我們來(lái)看看都做了什么妄均。

SharedPreferencesImpl(File file, int mode) {
    mFile = file;
    mBackupFile = makeBackupFile(file);
    mMode = mode;
    mLoaded = false;
    mMap = null;
    mThrowable = 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<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();
        }
    }
}

注意看我們的 startLoadFromDisk 方法,我們會(huì)去新開(kāi)一個(gè)子線程哪自,然后去通過(guò) XmlUtils.readMapXml() 方法把指定的 SharedPreferences 文件的所有的鍵值對(duì)都讀出來(lái)丰包,然后存放到一個(gè) map 中。

而眾所周知壤巷,文件的讀寫(xiě)操作都是耗時(shí)的邑彪,可想而知,在我們第一次去讀取一個(gè) SharedPreferences 文件的時(shí)候花上了太多的時(shí)間會(huì)怎樣胧华。

SharedPreferences 的讀取操作

上面講了初次獲取一個(gè)文件的 SharedPreferences 實(shí)例的時(shí)候锌蓄,會(huì)先去把所有鍵值對(duì)讀取到緩存中升筏,這明顯是一個(gè)耗時(shí)操作,而我們正常的去讀取數(shù)據(jù)的時(shí)候瘸爽,都是類似這樣的代碼您访。

val sharedPreferences = getSharedPreferences("123", Context.MODE_PRIVATE)
val string = sharedPreferences.getString("123","")

SharedPreferencesgetXXX() 方法可能會(huì)報(bào) ClassCastException 異常,所以我們?cè)谕粋€(gè) name 的時(shí)候剪决,對(duì)不一樣的類型灵汪,必須使用不同的 key。但是 putXXX 是可以用不同的類型值覆蓋相同的 key 的柑潦。

那勢(shì)必可能會(huì)導(dǎo)致這個(gè)操作需要等待一定的時(shí)間享言,我們姑且可以這么猜想,在 getXXX() 方法執(zhí)行的時(shí)候應(yīng)該是會(huì)等待前面的操作完成才能執(zhí)行的渗鬼。

因?yàn)?SharedPreferences 是一個(gè)接口览露,所以我們主要來(lái)看看它的實(shí)現(xiàn)類 SharedPreferencesImpl,這里以 getString() 為例譬胎。

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

awaitLoadedLocked() 方法應(yīng)該就是我們所想的等待執(zhí)行操作了差牛,我們看看里面做了什么。

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

可以看到堰乔,在 awaitLoadedLocked 方法里面我們使用了 mLock.wait() 來(lái)等待初始化的讀取操作偏化,而我們前面看到的 loadFromDiskLocked() 方法的最后也可以看到它調(diào)用了 mLock.notifyAll() 方法來(lái)喚醒后面這個(gè)阻塞的 getXXX()那么這里就會(huì)明顯出現(xiàn)一個(gè)問(wèn)題镐侯,我們的 getXXX() 方法是寫(xiě)在 UI 線程的侦讨,如果這個(gè)方法被阻塞的太久,勢(shì)必會(huì)出現(xiàn) ANR 的情況苟翻。所以我們一定在平時(shí)需要根據(jù)具體情況考慮是否需要把 SharedPreferences 的讀寫(xiě)操作放在子線程中韵卤。

SharedPreferences 的內(nèi)部類 Editor

我們?cè)趯?xiě)入數(shù)據(jù)之前,總是要先通過(guò)類似這樣的代碼獲取 SharedPreferences 的內(nèi)部類 Editor崇猫。

val editor = sharedPreferences.edit()

我們當(dāng)然要看看這個(gè)到底是什么東西沈条。

    @Override
    public Editor edit() {
        // TODO: remove the need to call awaitLoadedLocked() when
        // requesting an editor.  will require some work on the
        // Editor, but then we should be able to do:
        //
        //      context.getSharedPreferences(..).edit().putString(..).apply()
        //
        // ... all without blocking.
        synchronized (mLock) {
            awaitLoadedLocked();
        }

        return new EditorImpl();
    }

我們?cè)?/p>

可以看到,我們?cè)谧x取解析完 XML 文件的時(shí)候邓尤,直接返回了一個(gè) Editor 的實(shí)現(xiàn)類 EditorImpl拍鲤。我們隨便查看一個(gè) putXXX 的方法一看贴谎。

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

可以看到汞扎,我們?cè)?EditorImpl 里面使用了一個(gè) HashMap 來(lái)存放我們的鍵值對(duì)數(shù)據(jù),每次 put 的時(shí)候都會(huì)直接往這個(gè)鍵值對(duì)變量 mModified 中進(jìn)行數(shù)據(jù)的 put 操作擅这。

commit() 和 apply()

我們總是在更新數(shù)據(jù)后需要加上 commit() 或者 apply() 來(lái)進(jìn)行輸入的寫(xiě)入操作澈魄,我們不妨來(lái)看看他們的實(shí)現(xiàn)到底有什么區(qū)別。

先看 commit() 和 apply() 的源碼仲翎。

@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 {
        if (DEBUG) {
            Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                    + " committed after " + (System.currentTimeMillis() - startTime)
                    + " ms");
        }
    }
    notifyListeners(mcr);
    return mcr.writeToDiskResult;
}

@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) {
                }

                if (DEBUG && mcr.wasWritten) {
                    Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                            + " applied after " + (System.currentTimeMillis() - startTime)
                            + " ms");
                }
            }
        };

    QueuedWork.addFinisher(awaitCommit);

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

可以看到痹扇,apply()commit() 的區(qū)別是在 commit() 把內(nèi)容同步提交到了硬盤(pán)铛漓,而 apply() 是先立即把修改提交給了內(nèi)存,然后開(kāi)啟了一個(gè)異步的線程提交到硬盤(pán)鲫构。commit() 會(huì)接收 MemoryCommitResult 里面的一個(gè) boolean 參數(shù)作為結(jié)果浓恶,而 apply() 沒(méi)有對(duì)結(jié)果做任何關(guān)心。

我們可以看到结笨,文件寫(xiě)入更新的操作都是交給 commitToMemory() 做的包晰,這個(gè)方法返回了一個(gè) MemoryCommitResult 對(duì)象,我們來(lái)看看到底做了什么炕吸。

// Returns true if any changes were made
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);
}

可以看到伐憾,我們這里的 mMap 即存放當(dāng)前 SharedPreferences 文件中的鍵值對(duì),而 mModified 則存放的是當(dāng)時(shí) edit() 時(shí) put 進(jìn)去的鍵值對(duì)赫模,這個(gè)我們前面有所介紹树肃。這里有個(gè) mDiskWritesInFlight 看起來(lái)應(yīng)該是表示正在等待寫(xiě)的操作數(shù)量。

接下來(lái)我們首先處理了 edit().clear() 操作的 mClear 標(biāo)志瀑罗,當(dāng)我們?cè)谕饷嬲{(diào)用 clear() 方法的時(shí)候胸嘴,我們會(huì)把 mClear 設(shè)置為 true,這時(shí)候我們會(huì)直接通過(guò) mMap.clear() 清空此時(shí)文件中的鍵值對(duì)廓脆,然后再遍歷 mModified 中新 put 進(jìn)來(lái)的鍵值對(duì)數(shù)據(jù)放到 mMap 中筛谚。也就是說(shuō):在一次提交中,如果我們又有 put 又有 clear() 操作的話停忿,我們只能 clear() 掉之前的鍵值對(duì)驾讲,這次 put() 進(jìn)去的鍵值對(duì)還是會(huì)被寫(xiě)入到 XML 文件中。

// 讀取
val sharedPreferences = getSharedPreferences("123", Context.MODE_PRIVATE)
// 寫(xiě)入
val editor = sharedPreferences.edit()
editor.putInt("1", 123)
editor.clear()
editor.apply()
Log.e("nanchen2251", "${sharedPreferences.getInt("1", 0)}")

也就是說(shuō)席赂,當(dāng)我們編寫(xiě)下面的代碼的時(shí)候吮铭,得到的打印還是 123。

然后我們接著往下看颅停,又發(fā)現(xiàn)了另外一個(gè) commit()apply() 都做了調(diào)用的方法是 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() {
            @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);
}

在這個(gè)方法中,首先通過(guò)判斷 postWriteRunnable 是否為 null 來(lái)判斷是 apply() 還是 commit()癞揉。然后定義了一個(gè) Runnable 任務(wù)纸肉,在 Runnable 中先調(diào)用了 writeToFile() 進(jìn)行了寫(xiě)入和計(jì)數(shù)器更新的操作。

然后我們?cè)賮?lái)看看這個(gè) writeToFile() 方法做了些什么喊熟。

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

代碼比較長(zhǎng)柏肪,做了一些時(shí)間的記錄和 XML 的相關(guān)處理,但最值得我們關(guān)注的還是其中打了標(biāo)注的對(duì)于 mBackupFile 的處理芥牌。我們可以明顯地看到烦味,在我們寫(xiě)入文件的時(shí)候,我們會(huì)把此前的 XML 文件改名為一個(gè)備份文件壁拉,然后再將要寫(xiě)入的數(shù)據(jù)寫(xiě)入到一個(gè)新的文件中谬俄。如果這個(gè)過(guò)程執(zhí)行成功的話柏靶,就會(huì)把備份文件刪除。由此可見(jiàn):即使我們每次只是添加一個(gè)鍵值對(duì)溃论,也會(huì)重新寫(xiě)入整個(gè)文件的數(shù)據(jù)屎蜓,這也說(shuō)明了 SharedPreferences 只適合保存少量數(shù)據(jù),文件太大會(huì)有性能問(wèn)題钥勋。

看完了這個(gè) writeToFile() 楞抡,我們?cè)賮?lái)看看下面做了啥述召。

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

可以看到褥伴,當(dāng)且僅當(dāng)是 commit() 并且只有一個(gè)待寫(xiě)入操作的時(shí)候才能直接執(zhí)行到 writeToDiskRunnable.run()萧恕,否則都會(huì)執(zhí)行到 QueuedWorkqueue() 方法,這個(gè) QueuedWork 又是什么東西乎婿?

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

/** Work queued via {@link #queue} */
@GuardedBy("sLock")
private static final LinkedList<Runnable> sWork = new LinkedList<>();
/**
 * Internal utility class to keep track of process-global work that's outstanding and hasn't been
 * finished yet.
 *
 * New work will be {@link #queue queued}.
 *
 * It is possible to add 'finisher'-runnables that are {@link #waitToFinish guaranteed to be run}.
 * This is used to make sure the work has been finished.
 *
 * This was created for writing SharedPreference edits out asynchronously so we'd have a mechanism
 * to wait for the writes in Activity.onPause and similar places, but we may use this mechanism for
 * other things in the future.
 *
 * The queued asynchronous work is performed on a separate, dedicated thread.
 *
 * @hide
 */
public class QueuedWork {
     /**
     * Add a finisher-runnable to wait for {@link #queue asynchronously processed work}.
     *
     * Used by SharedPreferences$Editor#startCommit().
     *
     * Note that this doesn't actually start it running.  This is just a scratch set for callers
     * doing async work to keep updated with what's in-flight. In the common case, caller code
     * (e.g. SharedPreferences) will pretty quickly call remove() after an add(). The only time
     * these Runnables are run is from {@link #waitToFinish}.
     *
     * @param finisher The runnable to add as finisher
     */
    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);
        }
    }

    /**
     * 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() {
        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);

                if (DEBUG) {
                    hadMessages = true;
                    Log.d(LOG_TAG, "waiting");
                }
            }

            // 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++;

                if (DEBUG || mNumWaits % 1024 == 0 || waitTime > MAX_WAIT_TIME_MILLIS) {
                    mWaitTimes.log(LOG_TAG, "waited: ");
                }
            }
        }
    }
    /**
     * Queue a work-runnable for processing asynchronously.
     *
     * @param work The new runnable to process
     * @param shouldDelay If the message should be delayed
     */
    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);
            }
        }
    }
}

簡(jiǎn)單地說(shuō)测僵,這個(gè) QueuedWork 類里面有一個(gè)專門(mén)存放 Runnable 的兩個(gè) LinkedList 對(duì)象,他們分別對(duì)應(yīng)未完成的操作 sFinishers 和正在工作的 sWork谢翎。
我們?cè)?waitToFinish() 方法中捍靠,會(huì)不斷地去遍歷執(zhí)行未完成的 Runnable。我們根據(jù)注釋也知道了這個(gè)方法會(huì)在 ActivityonPause()BroadcastReceiveronReceive() 方法后調(diào)用森逮。假設(shè)我們頻繁的調(diào)用了 apply()方法榨婆,并緊接著調(diào)用了 onPause() ,那么就可能會(huì)發(fā)生 onPause() 一直等待 QueuedWork.waitToFinish 執(zhí)行完成而產(chǎn)生 ANR褒侧。也就是說(shuō)良风,即使是調(diào)用了 apply() 方法去異步提交,也不是完全安全的闷供。如果 apply() 方法使用不當(dāng)烟央,也是可能出現(xiàn) ANR 的。

總結(jié)

說(shuō)了這么多歪脏,我們當(dāng)然還是需要做一個(gè)總結(jié)疑俭。

  1. apply() 沒(méi)有返回值而 commit() 返回 boolean 表明修改是否提交成功 ;
  2. commit() 是把內(nèi)容同步提交到硬盤(pán)的婿失,而 apply() 先立即把修改提交到內(nèi)存钞艇,然后開(kāi)啟一個(gè)異步的線程提交到硬盤(pán),并且如果提交失敗豪硅,你不會(huì)收到任何通知哩照。
  3. 所有 commit() 提交是同步過(guò)程,效率會(huì)比 apply() 異步提交的速度慢舟误,在不關(guān)心提交結(jié)果是否成功的情況下葡秒,優(yōu)先考慮 apply() 方法姻乓。
  4. apply() 是使用異步線程寫(xiě)入磁盤(pán)嵌溢,commit() 是同步寫(xiě)入磁盤(pán)眯牧。所以我們?cè)谥骶€程使用的 commit() 的時(shí)候,需要考慮是否會(huì)出現(xiàn) ANR 問(wèn)題赖草。
  5. 我們每次添加鍵值對(duì)的時(shí)候学少,都會(huì)重新寫(xiě)入整個(gè)文件的數(shù)據(jù),所以它不適合大量數(shù)據(jù)存儲(chǔ)秧骑。
  6. 多線程場(chǎng)景下效率比較低版确,因?yàn)?get 操作的時(shí)候,會(huì)鎖定 SharedPreferencesImpl 里面的對(duì)象乎折,互斥其他操作绒疗,而當(dāng) putcommit()apply() 操作的時(shí)候都會(huì)鎖住 Editor 的對(duì)象骂澄,在這樣的情況下吓蘑,效率會(huì)降低。
  7. 由于每次都會(huì)把整個(gè)文件加載到內(nèi)存中坟冲,因此磨镶,如果 SharedPreferences 文件過(guò)大,或者在其中的鍵值對(duì)是大對(duì)象的 JSON 數(shù)據(jù)則會(huì)占用大量?jī)?nèi)存健提,讀取較慢是一方面琳猫,同時(shí)也會(huì)引發(fā)程序頻繁 GC,導(dǎo)致的界面卡頓私痹。
  8. get 操作都是線程安全的脐嫂, 并且 get 僅僅是從內(nèi)存中 (mMap) 獲取數(shù)據(jù), 所以無(wú)性能問(wèn)題。
    基于以上缺點(diǎn):
  9. 建議不要存儲(chǔ)較大數(shù)據(jù)到 SharedPreferences紊遵,也不要把較多數(shù)據(jù)存儲(chǔ)到同一個(gè) name 對(duì)應(yīng)的 SharedPreferences 中雹锣,最好根據(jù)規(guī)則拆分為多個(gè) SharedPreferences 文件。
  10. 頻繁修改的數(shù)據(jù)修改后統(tǒng)一提交癞蚕,而不是修改過(guò)后馬上提交蕊爵。
  11. 在跨進(jìn)程通訊中不去使用 SharedPreferences
  12. 獲取 SharedPreferences 對(duì)象的時(shí)候會(huì)讀取 SharedPreferences 文件桦山,如果文件沒(méi)有讀取完攒射,就執(zhí)行了 get 和 put 操作,可能會(huì)出現(xiàn)需要等待的情況恒水,因此最好提前獲取 SharedPreferences 對(duì)象会放。
  13. 每次調(diào)用 edit() 方法都會(huì)創(chuàng)建一個(gè)新的 EditorImpl 對(duì)象,不要頻繁調(diào)用 edit() 方法钉凌。
    參考鏈接:https://juejin.im/post/5adc444df265da0b886d00bc#heading-10
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末咧最,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌矢沿,老刑警劉巖滥搭,帶你破解...
    沈念sama閱讀 216,997評(píng)論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異捣鲸,居然都是意外死亡瑟匆,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,603評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)栽惶,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)愁溜,“玉大人,你說(shuō)我怎么就攤上這事外厂∶嵯螅” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,359評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵汁蝶,是天一觀的道長(zhǎng)交惯。 經(jīng)常有香客問(wèn)我,道長(zhǎng)穿仪,這世上最難降的妖魔是什么席爽? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,309評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮啊片,結(jié)果婚禮上只锻,老公的妹妹穿的比我還像新娘。我一直安慰自己紫谷,他們只是感情好齐饮,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,346評(píng)論 6 390
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著笤昨,像睡著了一般祖驱。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上瞒窒,一...
    開(kāi)封第一講書(shū)人閱讀 51,258評(píng)論 1 300
  • 那天捺僻,我揣著相機(jī)與錄音,去河邊找鬼崇裁。 笑死匕坯,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的拔稳。 我是一名探鬼主播葛峻,決...
    沈念sama閱讀 40,122評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼巴比!你這毒婦竟也來(lái)了术奖?” 一聲冷哼從身側(cè)響起礁遵,我...
    開(kāi)封第一講書(shū)人閱讀 38,970評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎采记,沒(méi)想到半個(gè)月后佣耐,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,403評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡挺庞,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,596評(píng)論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了稼病。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片选侨。...
    茶點(diǎn)故事閱讀 39,769評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖然走,靈堂內(nèi)的尸體忽然破棺而出援制,到底是詐尸還是另有隱情,我是刑警寧澤芍瑞,帶...
    沈念sama閱讀 35,464評(píng)論 5 344
  • 正文 年R本政府宣布晨仑,位于F島的核電站,受9級(jí)特大地震影響拆檬,放射性物質(zhì)發(fā)生泄漏洪己。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,075評(píng)論 3 327
  • 文/蒙蒙 一竟贯、第九天 我趴在偏房一處隱蔽的房頂上張望答捕。 院中可真熱鬧,春花似錦屑那、人聲如沸拱镐。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,705評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)沃琅。三九已至,卻和暖如春蜘欲,著一層夾襖步出監(jiān)牢的瞬間益眉,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,848評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工姥份, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留呜叫,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,831評(píng)論 2 370
  • 正文 我出身青樓殿衰,卻偏偏與公主長(zhǎng)得像朱庆,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子闷祥,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,678評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容

  • 所有的應(yīng)用程序都必然涉及數(shù)據(jù)的輸入與輸出娱颊。在Android系統(tǒng)中傲诵,主要有五種數(shù)據(jù)存儲(chǔ)模式: 1 .Sharedfe...
    bby08閱讀 1,975評(píng)論 0 3
  • 那天我突然看到有人說(shuō)使用SharedPreferences會(huì)出現(xiàn)ANR,要知道ANR可是個(gè)大問(wèn)題啊箱硕,于是我就想看看...
    俗人浮生閱讀 993評(píng)論 0 4
  • 一. 概述 SharedPreferences(簡(jiǎn)稱SP)是Android中很常用的數(shù)據(jù)存儲(chǔ)方式拴竹,SP采用key-...
    凱玲之戀閱讀 379評(píng)論 0 0
  • Android 五種數(shù)據(jù)存儲(chǔ)的方式分別為: SharedPreferences:以Map形式存放簡(jiǎn)單的配置參數(shù); ...
    ghroost閱讀 12,591評(píng)論 0 23
  • 突然想起圓心課程老師給我講的關(guān)于蟲(chóng)蛹蛻變成蝴蝶的“故事”剧罩,生命中會(huì)遇到很多人生的導(dǎo)師栓拜,可是我們是否認(rèn)可他們分享的他...
    哇噻tiger閱讀 219評(píng)論 0 1