SharedPreference 源碼分析

一、 本節(jié)目標(biāo)

SharedPreference 是一個輕量級的 key-value 存儲框架道偷。開發(fā)者很容易地可以使用它的 api 缀旁,但是如果不恰當(dāng)?shù)氖褂每赡軙?dǎo)致一些問題,所以針對如何使用和處理這些問題勺鸦,列出了以下幾個小點诵棵。

  • 1、sp 實例的獲取祝旷。

  • 2、sp 是如何進行讀寫操作和緩存處理的嘶窄?

  • 3怀跛、commit 和 apply 的區(qū)別?

  • 4柄冲、不恰當(dāng)使用 sp 的一些坑吻谋。

  • 5、sp 中幾種模式的選擇现横。

我們這篇博客主要就是來學(xué)習(xí)一下上面所提到的幾個點漓拾,并從源碼的角度來分析各個問題。

二戒祠、sp 實例的獲取

2.1骇两、 初始化

SharedPreference 是一個接口,它的實現(xiàn)類是 SharedPreferenceImpl姜盈。有三種方式可以來獲鹊颓А:

  • 方式一 Activity#getPreferences
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
    return mBase.getSharedPreferences(name, mode);
}
  • 方式二 PreferenceManager#getDefaultSharedPreferences
public static SharedPreferences getDefaultSharedPreferences(Context context) {
    return context.getSharedPreferences(getDefaultSharedPreferencesName(context),
            getDefaultSharedPreferencesMode());
}
  • 方式三 ContextImpl#getSharedPreferences
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
  ...
}

通過源碼可以知道,最終都是用調(diào)用第三種方式來實例化 SharedPreferenceImpl 這個實例的馏颂。

2.2示血、 getSharedPreferences源碼分析

這里使用了一個集合來緩存實例化后的 SharedPreferenceImpl 實例,這樣下次調(diào)用該方法時救拉,就直接從內(nèi)存中獲取這個對象难审。

@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
    ...
    SharedPreferencesImpl sp;
    //加鎖,線程安全亿絮。
    synchronized (ContextImpl.class) {
        final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();

        //1. 從緩存存取 SharedPreferencesImpl 實例
        sp = cache.get(file);
        if (sp == null) {
            sp = new SharedPreferencesImpl(file, mode);
            cache.put(file, sp);
            return sp;
        }
    }
    return sp;
}

2.3告喊、 SharedPreferencesImpl 的構(gòu)造

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

從構(gòu)造方法中可以看出

  • mFile 就是當(dāng)前 SharedPreferencesImpl 所映射的文件麸拄,所有的 key-value 的數(shù)據(jù)都會保存到這個文件中。

  • mBackupFile 可以理解為是一個備份文件葱绒,當(dāng)保存數(shù)據(jù)失敗等操作感帅,可以從這個文件進行恢復(fù)數(shù)據(jù)。

  • mMode 表示文件的模式地淀。

  • mMap 用于存儲 key-value 鍵值對信息失球。

  • mLoaded 理解這個變量之前,首先要知道帮毁,sp 使用來存儲 key-value 的实苞,這些鍵值對數(shù)據(jù)在內(nèi)存中是保存到一個 mMap 集合中,對應(yīng)寫入的磁盤文件就是 mFile 烈疚。這個變量就是標(biāo)記 mFile 文件的數(shù)據(jù)是否已經(jīng)加載到 mMap 集合中黔牵。具體的使用,下面會描述爷肝。

  • startLoadFromDisk() 內(nèi)部開啟線程去從文件中加載數(shù)據(jù)到 mMap 集合中猾浦。具體的使用,下面會描述灯抛。

2.4金赦、 startLoadFromDisk 加載數(shù)據(jù)

在 2.3 中可以知道, mMap 是用于存儲鍵值對的集合对嚼,而 mFile 是最終保存的本地文件夹抗。startLoadFromDisk 就是從 mFile 本地文件中去加載數(shù)據(jù)到 mMap 集合中,下面我們來看看源碼是如何實現(xiàn)的纵竖。

private void startLoadFromDisk() {

    // mLoaded 標(biāo)記為 false
    synchronized (this) {
        mLoaded = false;
    }

    //開啟線程漠烧,讀取磁盤數(shù)據(jù)
    new Thread("SharedPreferencesImpl-load") {
        public void run() {
            loadFromDisk();
        }
    }.start();
}

loadFromDisk 在子線程讀取磁盤數(shù)據(jù),源碼如下:

private void loadFromDisk() {
    synchronized (SharedPreferencesImpl.this) {

        //mLoaded 避免重復(fù)讀取
        if (mLoaded) {
            return;
        }


        //如果備份文件存在靡砌,使用備份文件恃泪,回滾數(shù)據(jù)柬采。
        if (mBackupFile.exists()) {
            mFile.delete();
            mBackupFile.renameTo(mFile);
        }
    }

    Map map = null;
    StructStat stat = null;
    try {
        stat = Os.stat(mFile.getPath());
        if (mFile.canRead()) {
            BufferedInputStream str = null;
            try {
                //讀取數(shù)據(jù),保存到 map 對象中
                str = new BufferedInputStream(
                        new FileInputStream(mFile), 16*1024);
                map = XmlUtils.readMapXml(str);
            } catch (XmlPullParserException | IOException e) {
                Log.w(TAG, "getSharedPreferences", e);
            } finally {
                IoUtils.closeQuietly(str);
            }
        }
    } catch (ErrnoException e) {
        /* ignore */
    }
    synchronized (SharedPreferencesImpl.this) {
        //標(biāo)記讀取成功
        mLoaded = true;
        if (map != null) {
            //給成員變量賦值
            mMap = map;
            mStatTimestamp = stat.st_mtime;
            mStatSize = stat.st_size;
        } else {
            //首次可能文件為空,所以在這里 new 一個對象泉手。
            mMap = new HashMap<>();
        }
        //讀取完畢汇恤,激活其他線程索守。
        notifyAll();
    }
}

以上兩個方法概括起來主要做了以下幾件事:

  • 1胜茧、 mLoaded標(biāo)記的設(shè)置,開始讀取數(shù)據(jù)時骗炉,將其設(shè)置為 false照宝,表示還未加載完成,讀取成功之后標(biāo)記為 true句葵,表示加載完成厕鹃。

  • 2兢仰、 mBackupFile首先判斷備份文件是否存在,存在則優(yōu)先從備份文件中恢復(fù)數(shù)據(jù)剂碴。

  • 3把将、 按照指定的格式讀取文件數(shù)據(jù),保存到 mMap 集合中忆矛。

  • 4察蹲、 激活其它阻塞的線程。具體如何使用催训,下面會介紹洽议。

三、 sp 對集合存取的操作

3.1漫拭、 從集合中取出數(shù)據(jù)

public int getInt(String key, int defValue) {
    synchronized (this) {
        //判斷 mLoaded 數(shù)據(jù)是否為 true亚兄,不是則 wait 等待數(shù)據(jù)加載線程執(zhí)行完畢。
        awaitLoadedLocked();
        //從 mMap 集合中獲取數(shù)據(jù)采驻。
        Integer v = (Integer)mMap.get(key);
        return v != null ? v : defValue;
    }
}

get 操作都是加了 synchronized 的审胚,因此它是線程安全的。在獲取數(shù)據(jù)之前首先 awaitLoadedLocked() 方法礼旅,判斷數(shù)據(jù)是否加載成功菲盾。 我們知道如果調(diào)用 getSharedPreference() 方法之后很快又去調(diào)用 getXxx() 方法,那么可能此時線程還在加載加載文件中的數(shù)據(jù)到內(nèi)存中各淀,這時的 mLoadedfalse,那么 awaitLoadedLocked(); 這個方法就會使當(dāng)前線程阻塞直到數(shù)據(jù)加載成功诡挂,這時會 notifyAll()這樣就可以正常的讀取數(shù)據(jù)了碎浇。

3.2、 存入數(shù)據(jù)到集合中

存取一個整型數(shù)據(jù):

context.getSharedPreference
          .edit()
          .putInt("key",value)
          .commit();

這里會涉及到一個 Editor的對象璃俗,它也是一個接口奴璃,它的實現(xiàn)類是 EditorImpl
而 putInt,putBoolean,putLong 等操作都是通過 Editor 實現(xiàn)的。我們可以通過 getSharedPreference.edit()就可以獲取 Editor 的實例對象城豁。

而對 EdiatorImpl 的一系列的 putXxx() 和 getXxx() 就是在操作 mModified 集合苟穆,而最后通過調(diào)用 commit()或者apply()就會將數(shù)據(jù)寫入 mMap 和對應(yīng)的 mFile 中。

public final class EditorImpl implements Editor {
    private final Map<String, Object> mModified = Maps.newHashMap();
 
    ...
    public Editor putInt(String key, int value) { ... }
    public boolean commit() { ... }
    public boolean apply() { ... }
    ...
}

四唱星、 保存數(shù)據(jù) commit與 apply 的區(qū)別

4.1雳旅、 異步式 apply 保存數(shù)據(jù)

public void apply() {
    //1.保存數(shù)據(jù)到mMap內(nèi)存中,并返回一個MemoryCommitResult 對象
    final MemoryCommitResult mcr = commitToMemory();
    final Runnable awaitCommit = new Runnable() {
            public void run() {
                try {
                    //3.阻塞調(diào)用者
                    mcr.writtenToDiskLatch.await();
                } catch (InterruptedException ignored) {
                }
            }
        };
    //2.往 QueuedWork 添加一個任務(wù)间聊。
    QueuedWork.add(awaitCommit);
    Runnable postWriteRunnable = new Runnable() {
            public void run() {
                //從 QueuedWork 中移除
                awaitCommit.run();
                QueuedWork.remove(awaitCommit);
            }
        };
    //4.執(zhí)行寫入的任務(wù)攒盈。
    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
    
    notifyListeners(mcr);
}

通過上面的apply()源碼可以看出大致流程是

  • 1、 將數(shù)據(jù)寫入到內(nèi)存 mMap 中哎榴,并返回一個 MemoryCommitResult 對象型豁,它封裝要寫入的數(shù)據(jù)到文件中的僵蛛。

  • 2、 QueuedWork.add(awaitCommit);往 QueuedWork 添加一個任務(wù)迎变。

  • 3充尉、 當(dāng) awaitCommit被調(diào)用時,那么當(dāng)前線程會被阻塞衣形,直到 postWriteRunnable這個任務(wù)執(zhí)行驼侠,才將 awaitCommit從QueuedWork中移除。

  • 4泵喘、 執(zhí)行寫入的任務(wù)泪电。

4.2、 commitToMemory()

簡單描述就是使用 MemoryCommitResult 來封裝需要寫入到文件的數(shù)據(jù) mMap 和使用 changesMade 標(biāo)記當(dāng)前這次 apply 或者 commit 操作是否有新的數(shù)據(jù)要提交纪铺。

// Returns true if any changes were made
private MemoryCommitResult commitToMemory() {
    MemoryCommitResult mcr = new MemoryCommitResult();
    synchronized (SharedPreferencesImpl.this) {
        // 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);
        }
        //存儲要寫入到文件的數(shù)據(jù)
        mcr.mapToWriteToDisk = mMap;
        mDiskWritesInFlight++;
        boolean hasListeners = mListeners.size() > 0;
        if (hasListeners) {
            mcr.keysModified = new ArrayList<String>();
            mcr.listeners =
                    new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
        }
        synchronized (this) {
            if (mClear) {
                if (!mMap.isEmpty()) {
                    mcr.changesMade = true;
                    mMap.clear();
                }
                mClear = false;
            }
            //遍歷 mModified 將數(shù)據(jù)保存到 mMap 集合中相速。
            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 (!mMap.containsKey(k)) {
                        continue;
                    }
                    mMap.remove(k);
                } else {
                    if (mMap.containsKey(k)) {
                        Object existingValue = mMap.get(k);
                        if (existingValue != null && existingValue.equals(v)) {
                            continue;
                        }
                    }
                    mMap.put(k, v);
                }
                //標(biāo)記是有讀寫操作。
                mcr.changesMade = true;
                if (hasListeners) {
                    mcr.keysModified.add(k);
                }
            }
            mModified.clear();
        }
    }
    return mcr;
}

4.3鲜锚、 enqueueDiskWrite(...)

這個方法主要是創(chuàng)建一個任務(wù) writeToDiskRunnable并且交給線程池去執(zhí)行突诬。在這個任務(wù)中是負(fù)責(zé)將 mMap 中的數(shù)據(jù)寫入到文件中,并且在寫入完成后芜繁,調(diào)用 postWriteRunnable.run();來執(zhí)行寫入任務(wù)完畢后的后續(xù)操作旺隙,例如從 QueueWork移除任務(wù)等。

private void enqueueDiskWrite(final MemoryCommitResult mcr,
                              final Runnable postWriteRunnable) {
    final Runnable writeToDiskRunnable = new Runnable() {
            public void run() {
                synchronized (mWritingToDiskLock) {

                    //寫入文件的任務(wù)
                    writeToFile(mcr);
                }
                synchronized (SharedPreferencesImpl.this) {
                    mDiskWritesInFlight--;
                }
                if (postWriteRunnable != null) {
                    //寫入任務(wù)執(zhí)行完畢后的后續(xù)操作骏令,例如從 QueueWork移除任務(wù)等蔬捷。
                    postWriteRunnable.run();
                }
            }
        };
      ...
    //在線程池中執(zhí)行寫入任務(wù)。
    QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}

4.4榔袋、writeToFile(mcr)寫入數(shù)據(jù)到文件中

在這個方法中真正去執(zhí)行寫入操作周拐,我大致關(guān)心的是整體流程,因此具體的寫操作就不必要深究了凰兑。我們這里處理寫的操作外妥粟,還有一個方法需要注意的是,那就是mcr.setDiskWriteResult(...),這個方法內(nèi)部會調(diào)用 writtenToDiskLatch.countDown();遞減等待線程的數(shù)量吏够。

private void writeToFile(MemoryCommitResult mcr) {
    // Rename the current file so it may be used as a backup during the next read
    if (mFile.exists()) {
        //當(dāng)前沒有要寫入的操作
        if (!mcr.changesMade) {
            //標(biāo)記寫入完成
            mcr.setDiskWriteResult(true);
            return;
        }
        ...   
    }
    // 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 (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
        }
        // Writing was successful, delete the backup file if there is one.
        mBackupFile.delete();
        //標(biāo)記寫入成功
        mcr.setDiskWriteResult(true);
        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);
        }
    }
    //標(biāo)記寫入失敗
    mcr.setDiskWriteResult(false);
}


public void setDiskWriteResult(boolean result) {
    writeToDiskResult = result;
    //遞減等待線程
    writtenToDiskLatch.countDown();
}

4.4勾给、setDiskWriteResult的內(nèi)部實現(xiàn)

我們知道在 4.1 中有如下代碼,當(dāng)時并沒有解釋 mcr.writtenToDiskLatch.await();這段代碼的作用锅知。我們知道 QueueWork 只要執(zhí)行 awaitCommit 這個任務(wù)播急,那么當(dāng)前的線程會被阻塞,直到寫入任務(wù)完成售睹。

//1.保存數(shù)據(jù)到mMap內(nèi)存中旅择,并返回一個MemoryCommitResult 對象
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
        public void run() {
                try {
                    //3.阻塞調(diào)用者
                    mcr.writtenToDiskLatch.await();
                } catch (InterruptedException ignored) {
            }
    }
 };
 Runnable postWriteRunnable = new Runnable() {
            public void run() {
                //從 QueuedWork 中移除
                awaitCommit.run();
                QueuedWork.remove(awaitCommit);
            }
        };

setDiskWriteResult這個方法是在writeToFile中調(diào)用的,它內(nèi)部會調(diào)用 writtenToDiskLatch.countDown();遞減等待線程的數(shù)量侣姆,此時的等待線程就是寫入數(shù)據(jù)的線程生真。當(dāng)?shù)却€程的數(shù)量遞減到數(shù)量為 0 時也就是writeToFile方法執(zhí)行完畢沉噩,那么這 postWriteRunnable 任務(wù)被調(diào)用時,這時就可以將 awaitCommit從 QueueWork 內(nèi)部的隊列中移除柱蟀。

public void setDiskWriteResult(boolean result) {
    writeToDiskResult = result;
    //遞減等待線程
    writtenToDiskLatch.countDown();
}

4.5川蒙、阻塞式commit() 提交數(shù)據(jù)

看完 apply 那么現(xiàn)在來看 commit 應(yīng)該就很容明白了, commit 是阻塞式的长已,執(zhí)行完畢之后會返回一個 boolean 變量值表示數(shù)據(jù)是否寫入成功畜眨。

public boolean commit() {
    //保存到內(nèi)存中
    MemoryCommitResult mcr = commitToMemory();
    //寫入數(shù)據(jù)
    SharedPreferencesImpl.this.enqueueDiskWrite(
        mcr, null /* sync write on this thread okay */);
    try {
        //阻塞等待寫入線程完畢
        mcr.writtenToDiskLatch.await();
    } catch (InterruptedException e) {
        return false;
    }
    notifyListeners(mcr);
    return mcr.writeToDiskResult;
}

五、 不恰當(dāng)使用 sp 的一些坑

5.1术瓮、 示例1

在這里先看一個 ANR 的日志信息康聂。

"main" prio=5 tid=1 WAIT
  | group="main" sCount=1 dsCount=0 obj=0x4155cc90 self=0x41496408
  | sysTid=13523 nice=0 sched=0/0 cgrp=apps handle=1074110804
  | state=S schedstat=( 2098661082 1582204811 6433 ) utm=165 stm=44 core=0
  at java.lang.Object.wait(Native Method)
  - waiting on <0x4155cd60> (a java.lang.VMThread) held by tid=1 (main)
  at java.lang.Thread.parkFor(Thread.java:1205)
  at sun.misc.Unsafe.park(Unsafe.java:325)
  at java.util.concurrent.locks.LockSupport.park(LockSupport.java:157)
  at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:813)
  at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireSharedInterruptibly(AbstractQueuedSynchronizer.java:973)
  at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireSharedInterruptibly(AbstractQueuedSynchronizer.java:1281)
  at java.util.concurrent.CountDownLatch.await(CountDownLatch.java:202)
  at android.app.SharedPreferencesImpl$EditorImpl$1.run(SharedPreferencesImpl.java:364)
  at android.app.QueuedWork.waitToFinish(QueuedWork.java:88)
  at android.app.ActivityThread.handleServiceArgs(ActivityThread.java:2689)
  at android.app.ActivityThread.access$2000(ActivityThread.java:135)
  at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1494)
  at android.os.Handler.dispatchMessage(Handler.java:102)
  at android.os.Looper.loop(Looper.java:137)
  at android.app.ActivityThread.main(ActivityThread.java:4998)
  at java.lang.reflect.Method.invokeNative(Native Method)
  at java.lang.reflect.Method.invoke(Method.java:515)
  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:777)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:593)
  at dalvik.system.NativeStart.main(Native Method)

在上面的日志中,我們很明顯可以看到一個關(guān)鍵字await胞四,我們猜應(yīng)該是線程鎖的問題恬汁。代碼定位到 QueuedWork.waitToFinish中。

從 waitToFinish 方法中可以知道它會在Activity暫停時辜伟,BroadcastReceiver的onReceive方法調(diào)用后或者service的命令處理后被調(diào)用氓侧,并且調(diào)用這個方法的目的是為了確保異步任務(wù)被及時完成。

/**
 * Finishes or waits for async operations to complete.
 * (e.g. SharedPreferences$Editor#startCommit writes)
 *
 * 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() {
    Runnable toFinish;
    while ((toFinish = sPendingWorkFinishers.poll()) != null) {
        toFinish.run();
    }
}

waitToFinish()這個方法內(nèi)部是去遍歷 QueueWork 的隊列 sPendingWorkFinishers并且執(zhí)行對應(yīng)的任務(wù)导狡,而回到4.1 節(jié)中约巷,我們創(chuàng)建了一個 awaitCommit 并且添加到 QueueWork隊列中,如果此時 waitToFinish() 被執(zhí)行時旱捧,而這時 mcr. writtenToDiskLatch中的等待線程數(shù)量沒有遞減到 0独郎,也就是此時 commit 和 apply 的寫入文件操作還在進行,那么waitToFinish調(diào)用線程就會被阻塞枚赡,從而導(dǎo)致 ANR的問題囚聚。

    //1.保存數(shù)據(jù)到mMap內(nèi)存中,并返回一個MemoryCommitResult 對象
    final MemoryCommitResult mcr = commitToMemory();
    final Runnable awaitCommit = new Runnable() {
            public void run() {
                try {
                    //3.阻塞調(diào)用者
                    mcr.writtenToDiskLatch.await();
                } catch (InterruptedException ignored) {
                }
            }
        };
    //2.往 QueuedWork 添加一個任務(wù)标锄。
    QueuedWork.add(awaitCommit);

5.2、 示例2

ANR 的第二個場景是茁计,在調(diào)用 getSharedPreference() 之后料皇,馬山又調(diào)用 getXxx()方法,就有可能出現(xiàn) ANR 的情況星压。
因為加載文件數(shù)據(jù)到內(nèi)存中是在一個子線程中去執(zhí)行的践剂,因此在為了保證數(shù)據(jù)的同步性,在調(diào)用 getXxx()等方法時娜膘,會先調(diào)用 awaitLoadedLocked()判斷數(shù)據(jù)是否已經(jīng)加載到內(nèi)存中逊脯,如果 mLoaded = false 就表示沒有加載完成,這時是出于一個 wait 狀態(tài)竣贪,這時如果本地的 mFile 文件比較大的話军洼,那么如果在主線程調(diào)用 getXxx()巩螃,那么就有可能出現(xiàn) ANR 現(xiàn)象,因為它會等待直到數(shù)據(jù)加載完成匕争,這時 mLoaded = true避乏,才會去釋放鎖。

public int getInt(String key, int defValue) {
    synchronized (this) {
        awaitLoadedLocked();
        Integer v = (Integer)mMap.get(key);
        return v != null ? v : defValue;
    }
}

5.3甘桑、 示例3

如果通過 apply 來保存數(shù)據(jù)拍皮,但是馬上就調(diào)用 getXxx() 方法的話,這時保存數(shù)據(jù)和獲取數(shù)據(jù)是異步的跑杭,因此 getXxx() 得到的數(shù)據(jù)可能為空或者是舊的數(shù)據(jù)铆帽。

5.4、 示例4

在4.4節(jié)中德谅,調(diào)用writeToFile(mcr)將 mMap 數(shù)據(jù)寫入到文件中爹橱,這個操作是全量數(shù)據(jù)mMap寫入到文件中,而不是增量寫入女阀,因此不管是調(diào)用 commit還是 apply時宅荤,最好全部通過 Editor進行修改數(shù)據(jù)之后,再進行寫入操作浸策,而不是一次修改就一次寫入冯键。

XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);

六、 sp 中幾種文件創(chuàng)建模式

  • MODE_PRIVATE

默認(rèn)的模式庸汗,當(dāng)前創(chuàng)建的文件只能被當(dāng)前 Application 使用惫确。

  • MODE_WORLD_READABLE

讀模式,允許其他應(yīng)用程序讀取該文件蚯舱,在 Android N 之后會有一個 SecurityException 異常改化。@Deprecated

  • MODE_WORLD_WRITEABLE

寫模式,允許其他應(yīng)用程序?qū)懭朐撐募骰瑁?Android N 之后會有一個 SecurityException 異常陈肛。@Deprecated

  • MODE_MULTI_PROCESS

多進程模式,這種模式是不安全的兄裂,官方不建議使用句旱,可以使用 ContentProvider 來代替。當(dāng)設(shè)置MODE_MULTI_PROCESS模式, 則每次getSharedPreferences過程, 會檢查SP文件上次修改時間和文件大小, 一旦所有修改則會重新從磁盤加載文件晰奖。@Deprecated

@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
    //1. 檢查 mode 谈撒,不符合會拋出異常。
    checkMode(mode);
    SharedPreferencesImpl sp;
    synchronized (ContextImpl.class) {
        final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
        sp = cache.get(file);
        if (sp == null) {
            sp = new SharedPreferencesImpl(file, mode);
            cache.put(file, sp);
            return sp;
        }
    }
    //2匾南,多進程模式啃匿,重新從本地加載數(shù)據(jù)
    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 void checkMode(int mode) {
    if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.N) {
        if ((mode & MODE_WORLD_READABLE) != 0) {
            throw new SecurityException("MODE_WORLD_READABLE no longer supported");
        }
        if ((mode & MODE_WORLD_WRITEABLE) != 0) {
            throw new SecurityException("MODE_WORLD_WRITEABLE no longer supported");
        }
    }
}
  • 1、 checkMode 檢查傳入的 mode 是否合適。

從以下 checkMode 方法源碼可以知道溯乒, Google 在 Android N 之后夹厌,就不再支持 MODE_WORLD_READABLEMODE_WORLD_WRITEABLE模式了。這樣對用戶的隱私權(quán)限進一步的收緊橙数,如果有這方面的需求還是建議使用 FileProvider 內(nèi)容提供者來實現(xiàn)尊流。

  • 2、 如果是 MODE_MULTI_PROCESS 模式灯帮,那么建議不要在保存 SharedPreference 的實例崖技,每次調(diào)用時都應(yīng)該調(diào)用 getSharedPreference() 來確保數(shù)據(jù)是從本地加載到的。

記錄于 2019年1月31日

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末钟哥,一起剝皮案震驚了整個濱河市迎献,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌腻贰,老刑警劉巖吁恍,帶你破解...
    沈念sama閱讀 222,000評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異播演,居然都是意外死亡冀瓦,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,745評論 3 399
  • 文/潘曉璐 我一進店門写烤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來翼闽,“玉大人,你說我怎么就攤上這事洲炊「芯郑” “怎么了?”我有些...
    開封第一講書人閱讀 168,561評論 0 360
  • 文/不壞的土叔 我叫張陵暂衡,是天一觀的道長询微。 經(jīng)常有香客問我,道長狂巢,這世上最難降的妖魔是什么撑毛? 我笑而不...
    開封第一講書人閱讀 59,782評論 1 298
  • 正文 為了忘掉前任,我火速辦了婚禮唧领,結(jié)果婚禮上藻雌,老公的妹妹穿的比我還像新娘。我一直安慰自己疹吃,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 68,798評論 6 397
  • 文/花漫 我一把揭開白布西雀。 她就那樣靜靜地躺著萨驶,像睡著了一般。 火紅的嫁衣襯著肌膚如雪艇肴。 梳的紋絲不亂的頭發(fā)上腔呜,一...
    開封第一講書人閱讀 52,394評論 1 310
  • 那天叁温,我揣著相機與錄音,去河邊找鬼核畴。 笑死膝但,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的谤草。 我是一名探鬼主播跟束,決...
    沈念sama閱讀 40,952評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼丑孩!你這毒婦竟也來了冀宴?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,852評論 0 276
  • 序言:老撾萬榮一對情侶失蹤温学,失蹤者是張志新(化名)和其女友劉穎略贮,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體仗岖,經(jīng)...
    沈念sama閱讀 46,409評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡逃延,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,483評論 3 341
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了轧拄。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片揽祥。...
    茶點故事閱讀 40,615評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖紧帕,靈堂內(nèi)的尸體忽然破棺而出盔然,到底是詐尸還是另有隱情,我是刑警寧澤是嗜,帶...
    沈念sama閱讀 36,303評論 5 350
  • 正文 年R本政府宣布愈案,位于F島的核電站,受9級特大地震影響鹅搪,放射性物質(zhì)發(fā)生泄漏站绪。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,979評論 3 334
  • 文/蒙蒙 一丽柿、第九天 我趴在偏房一處隱蔽的房頂上張望恢准。 院中可真熱鬧,春花似錦甫题、人聲如沸馁筐。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,470評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽敏沉。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間盟迟,已是汗流浹背秋泳。 一陣腳步聲響...
    開封第一講書人閱讀 33,571評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留攒菠,地道東北人迫皱。 一個月前我還...
    沈念sama閱讀 49,041評論 3 377
  • 正文 我出身青樓,卻偏偏與公主長得像辖众,于是被迫代替她去往敵國和親卓起。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,630評論 2 359

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