SharedPreferences源碼分析

先給出結(jié)論茉继,如果不想跟隨源碼分析的寄狼,可以根據(jù)結(jié)論,對(duì)SharedPreferences有個(gè)大概的了解喻圃。

結(jié)論:

1.SharedPreferences 線程是安全的,內(nèi)部由大量synchronized代碼塊實(shí)現(xiàn)同步
2.SharedPreferences 進(jìn)程是不安全的(雖然官方提供了MODE_MULTI_PROCESS加載模式粪滤,該加載模式在API級(jí)別11中添加斧拍,在API級(jí)別23中被廢棄。該SharedPreference loading標(biāo)志:設(shè)置后杖小,即使已在此過(guò)程中加載了共享首選項(xiàng)實(shí)例肆汹,也會(huì)檢查磁盤(pán)上的文件是否已修改。在應(yīng)用程序具有多個(gè)進(jìn)程的情況下予权,有時(shí)需要此行為昂勉,所有進(jìn)程都寫(xiě)入相同的SharedPreferences文件。但是扫腺,通常在進(jìn)程之間存在更好的通信形式岗照。 ),但官方文檔也給出了如下說(shuō)明:

此常量在API級(jí)別23中已棄用
.MODE_MULTI_PROCESS在某些Android版本中無(wú)法可靠地工作笆环,并且不提供任何協(xié)調(diào)跨進(jìn)程的并發(fā)修改的機(jī)制攒至。應(yīng)用程序不應(yīng)嘗試使用它。相反躁劣,他們應(yīng)該使用明確的跨流程數(shù)據(jù)管理方法迫吐,例如ContentProvider

3.首次getSharedPreferences會(huì)把文件從磁盤(pán)加載到內(nèi)存(用Map進(jìn)行保存)账忘,之后調(diào)用getSharedPreferences獲取數(shù)據(jù)志膀,即從內(nèi)存中直接獲取,也就不會(huì)由于大量的synchronized而影響性能
4.apply:同步修改內(nèi)存中的數(shù)據(jù)闪萄,然后異步回寫(xiě)到磁盤(pán)且是將其放入單線程任務(wù)隊(duì)列中執(zhí)行
commit:同步修改內(nèi)存中的數(shù)據(jù)梧却,且同步回寫(xiě)到磁盤(pán),因此會(huì)阻塞當(dāng)前線程

建議:

1.不要使用多進(jìn)程操作SharedPreferences 败去,小概率會(huì)出現(xiàn)數(shù)據(jù)丟失(文件被刪除)
2.不要存儲(chǔ)大量的數(shù)據(jù)放航,從磁盤(pán)加載SharedPreferences 文件是進(jìn)行I/O操作的,并且由于文件結(jié)構(gòu)是xml格式的圆裕,加載解析都比較耗時(shí)广鳍,雖然在此過(guò)程中是異步的,但如果數(shù)據(jù)過(guò)大吓妆,首次調(diào)用getSharedPreferences后赊时,然后馬上執(zhí)行getXX()或者putXX()由于需要等待加載文件結(jié)束,可能會(huì)阻塞線程(如果在UI線程可能會(huì)引發(fā)ANR)行拢。并且SharedPreferences 中的數(shù)據(jù)會(huì)一次性從磁盤(pán)加載到內(nèi)存祖秒,.applycommit都是會(huì)將數(shù)據(jù)寫(xiě)到磁盤(pán)的,SharedPreferences 文件不應(yīng)過(guò)大會(huì)影響性能
3.盡量使用apply更新數(shù)據(jù)竭缝,因?yàn)樗钱惒綄?xiě)入磁盤(pán)的房维,不會(huì)阻塞線程

將您的首選項(xiàng)更改從此編輯器提交回它正在編輯的SharedPreferences對(duì)象。這將自動(dòng)執(zhí)行請(qǐng)求的修改抬纸,替換SharedPreferences中當(dāng)前的內(nèi)容咙俩。

注意,當(dāng)兩個(gè)編輯器同時(shí)修改首選項(xiàng)時(shí)湿故,最后一個(gè)調(diào)用apply的編輯器將獲勝阿趁。

與同步地將其首選項(xiàng)寫(xiě)到持久存儲(chǔ)的commit()不同,apply()將其更改立即提交到內(nèi)存中的SharedPreferences坛猪,但是啟動(dòng)到磁盤(pán)的異步提交脖阵,并且不會(huì)通知您任何失敗。如果這個(gè)SharedPreferences上的另一個(gè)編輯器在apply()仍然未完成時(shí)執(zhí)行常規(guī)提交()砚哆,那么commit()將阻塞独撇,直到所有異步提交和提交本身都完成為止。

由于SharedPreferences實(shí)例是流程中的單例實(shí)例躁锁,如果已經(jīng)忽略了返回值纷铣,那么可以用apply()替換commit()的任何實(shí)例。

您不需要擔(dān)心Android組件的生命周期以及它們與向磁盤(pán)寫(xiě)入apply()的交互战转。該框架確保在切換狀態(tài)之前完成來(lái)自apply()的在途磁盤(pán)寫(xiě)操作搜立。

源碼分析:

我們通過(guò)調(diào)用getSharedPreferences獲取SharedPreferences對(duì)象時(shí),最終會(huì)調(diào)用ContextImpl.getSharedPreferences(File file, int mode)方法:

    @Override
    public SharedPreferences getSharedPreferences(File file, int mode) {
        SharedPreferencesImpl sp;
        synchronized (ContextImpl.class) {
            //獲取緩存中所有SharedPreferences對(duì)象集合
            final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
            sp = cache.get(file);
            //緩存中是否已存在我們需要的SharedPreferences
            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");
                    }
                }
                //如果緩存中不存在槐秧,則通過(guò)創(chuàng)建SharedPreferencesImpl對(duì)象啄踊,由其加載文件
                sp = new SharedPreferencesImpl(file, mode);
                //將SharedPreferences對(duì)象存入緩存集合中,以便下次直接從內(nèi)存中獲取
                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;
    }

先從緩存中獲取刁标,如果未命中則創(chuàng)建SharedPreferencesImpl對(duì)象颠通,并保存到緩存中,多次調(diào)用getSharedPreferences并不會(huì)多次創(chuàng)建對(duì)象膀懈,由于整個(gè)SharedPreferences獲取過(guò)程中都放在synchronized代碼塊中顿锰,因此在多線程下創(chuàng)建對(duì)象是安全的
SharedPreferencesImpl創(chuàng)建過(guò)程:

SharedPreferencesImpl(File file, int mode) {
        //需要加載的SharedPreferences文件對(duì)象
        mFile = file;
        //備份文件對(duì)象,寫(xiě)入失敗時(shí)會(huì)通過(guò)其進(jìn)行數(shù)據(jù)恢復(fù)
        mBackupFile = makeBackupFile(file);
        mMode = mode;
        mLoaded = false;
        //數(shù)據(jù)緩存集合启搂,用于存儲(chǔ)SharedPreferences文件中的數(shù)據(jù)
        mMap = null;
        mThrowable = null;
        //從磁盤(pán)加載數(shù)據(jù)
        startLoadFromDisk();
    }

下面我們看看從磁盤(pán)加載數(shù)據(jù)過(guò)程:

 private void startLoadFromDisk() {
        synchronized (mLock) {
            mLoaded = false;
        }
        new Thread("SharedPreferencesImpl-load") {
            public void run() {
                //從磁盤(pán)加載數(shù)據(jù)
                loadFromDisk();
            }
        }.start();
    }

另起一個(gè)線程加載SharedPreferences文件

 private void loadFromDisk() {
        synchronized (mLock) {
            if (mLoaded) {
                return;
            }
            if (mBackupFile.exists()) {
                mFile.delete();
                mBackupFile.renameTo(mFile);
            }
        }
        ···省略代碼···
        str = new BufferedInputStream(new FileInputStream(mFile), 16 * 1024);
        map = (Map<String, Object>) XmlUtils.readMapXml(str);
        ···省略代碼···
        synchronized (mLock) {
            mLoaded = true;
            mThrowable = thrown;
        ···省略代碼···
            if (map != null) {
                mMap = map;
                mStatTimestamp = stat.st_mtim;
                mStatSize = stat.st_size;
            } else {
                mMap = new HashMap<>();
            }
           ...     
            mLock.notifyAll();
    
        }
    }

從上面方法可知從磁盤(pán)加載SharedPreferences文件分別做了一下幾步處理:
1.如果由備份文件硼控,把原文件刪除,然后修改備份文件名稱(chēng)胳赌,使其成為原文件牢撼,達(dá)到文件恢復(fù)效果(備份文件名以.bak結(jié)尾)
2.加載文件并解析成Map
3.標(biāo)記加載完成
4.將Map賦值給mMap(即數(shù)據(jù)緩存集合,用于存儲(chǔ)SharedPreferences文件中的數(shù)據(jù))
5.通過(guò)notifyAll喚起所有等待加載的線程
現(xiàn)在SharedPreferences對(duì)象已經(jīng)初始化成功了疑苫,下面我們看一下對(duì)其數(shù)據(jù)操作的流程:

getXX()
@Override
@Nullable 
public String getString(String key, @Nullable String defValue) {
      synchronized (mLock) {
          //等待SharedPreferences加載完成
          awaitLoadedLocked();
          String v = (String)mMap.get(key);
          return v != null ? v : defValue;
      } 
}

通過(guò)synchronized代碼塊熏版,可見(jiàn)獲取數(shù)據(jù)是線程安全的纷责,僅僅是從緩存中獲取數(shù)據(jù),同步不會(huì)影響性能纳决。而awaitLoadedLocked()即是等待SharedPreferences文件從磁盤(pán)加載到內(nèi)存完畢后再執(zhí)行下面的獲取數(shù)據(jù)的方法碰逸,因此如果在首次調(diào)用getSharedPreferences時(shí)乡小,馬上調(diào)用getXX()阔加,會(huì)阻塞線程
接著看看awaitLoadedLocked()里面做了如何處理:

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

可見(jiàn),除了首次加載文件時(shí)满钟,mLoaded = false會(huì)卡在此處等待胜榔,當(dāng)文件加載完畢后會(huì)重置加載狀態(tài)即mLoaded = true,則不會(huì)等待直接獲取數(shù)據(jù)

putXX()

修改數(shù)據(jù)時(shí)首先得獲取Editor對(duì)象(SharedPreferencesImpl.java)

@Override
public Editor edit() {
      synchronized (mLock) {
          awaitLoadedLocked();
      }
      return new EditorImpl();
}
public final class EditorImpl implements Editor {
        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;
            }
        }
        ···代碼省略···
}

可見(jiàn)我們調(diào)用putXX()僅僅是將數(shù)據(jù)存到mModified中湃番,直到我們調(diào)用applycommit時(shí)才真正將數(shù)據(jù)同步到緩存并寫(xiě)入磁盤(pán)

apply()
      @Override
      public void apply() {
            final long startTime = System.currentTimeMillis();
            //同步數(shù)據(jù)到緩存
            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);
                    }
                };
            //將寫(xiě)入磁盤(pán)任務(wù)加入隊(duì)列
            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);
        }

下面看看同步緩存數(shù)據(jù)流程:

private MemoryCommitResult commitToMemory() {
    ···代碼省略···
    synchronized (SharedPreferencesImpl.this.mLock) {
         ···代碼省略···
         //緩存數(shù)據(jù)集合
        mapToWriteToDisk = mMap;
        ···代碼省略···
        synchronized (mEditorLock) {
             boolean changesMade = false;
             if (mClear) {
                     if (!mapToWriteToDisk.isEmpty()) {
                            changesMade = true;
                            mapToWriteToDisk.clear();
                      }
                      mClear = false;
             }
            //同步緩存數(shù)據(jù)
             for (Map.Entry<String, Object> e : mModified.entrySet()) {
                     String k = e.getKey();
                     Object v = e.getValue();
                     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);
}

然后我們瞅瞅?qū)懭氪疟P(pán)SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

private void enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable) {
    //如果是commit則postWriteRunnable == null即isFromSyncCommit = true
    //如果是apply則isFromSyncCommit = false
    final boolean isFromSyncCommit = (postWriteRunnable == null);

    final Runnable writeToDiskRunnable = new Runnable() {
        @Override
        public void run() {
            synchronized (mWritingToDiskLock) {
                //寫(xiě)入磁盤(pán)
                writeToFile(mcr, isFromSyncCommit);
            }
            synchronized (mLock) {
                mDiskWritesInFlight--;
            }
            if (postWriteRunnable != null) {
                postWriteRunnable.run();
            }
        }
    };

    // commit直接在當(dāng)前線程直接同步寫(xiě)入磁盤(pán)
    if (isFromSyncCommit) {
        boolean wasEmpty = false;
        synchronized (mLock) {
            wasEmpty = mDiskWritesInFlight == 1;
        }
        if (wasEmpty) {
            writeToDiskRunnable.run();
            return;
        }
    }
    //apply加入任務(wù)隊(duì)列執(zhí)行寫(xiě)入磁盤(pán)runnable
    QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
public static void queue(Runnable work, boolean shouldDelay) {
        Handler handler = getHandler();

        synchronized (sLock) {
            sWork.add(work);
            //sCanDelay用于紀(jì)錄當(dāng)前線程是否處于寫(xiě)入磁盤(pán)任務(wù)狀態(tài)
            if (shouldDelay && sCanDelay) {
                handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
            } else {
                handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
            }
        }
    }
private static Handler getHandler() {
        synchronized (sLock) {
            if (sHandler == null) {
                //首次創(chuàng)建一個(gè)線程
                HandlerThread handlerThread = new HandlerThread("queued-work-looper",
                        Process.THREAD_PRIORITY_FOREGROUND);
                handlerThread.start();
                //創(chuàng)建一個(gè)handler用于接收queue(Runnable work, boolean shouldDelay)發(fā)送的消息夭织,然后執(zhí)行寫(xiě)入磁盤(pán)runnable
                sHandler = new QueuedWorkHandler(handlerThread.getLooper());
            }
            return sHandler;
        }
    }
commit()
@Override
public boolean commit() {
    long startTime = 0;
    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;
}

由此可見(jiàn),不論commit/apply都回調(diào)用SharedPreferencesImpl.this.enqueueDiskWrite然后同步/異步執(zhí)行寫(xiě)入磁盤(pán)操作

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末吠撮,一起剝皮案震驚了整個(gè)濱河市尊惰,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌泥兰,老刑警劉巖弄屡,帶你破解...
    沈念sama閱讀 218,941評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異鞋诗,居然都是意外死亡膀捷,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)削彬,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)全庸,“玉大人,你說(shuō)我怎么就攤上這事融痛『” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,345評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵雁刷,是天一觀的道長(zhǎng)覆劈。 經(jīng)常有香客問(wèn)我,道長(zhǎng)安券,這世上最難降的妖魔是什么墩崩? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,851評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮侯勉,結(jié)果婚禮上鹦筹,老公的妹妹穿的比我還像新娘。我一直安慰自己址貌,他們只是感情好铐拐,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布徘键。 她就那樣靜靜地躺著,像睡著了一般遍蟋。 火紅的嫁衣襯著肌膚如雪吹害。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,688評(píng)論 1 305
  • 那天虚青,我揣著相機(jī)與錄音它呀,去河邊找鬼。 笑死棒厘,一個(gè)胖子當(dāng)著我的面吹牛纵穿,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播奢人,決...
    沈念sama閱讀 40,414評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼图云,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼莱找!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,319評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤筋岛,失蹤者是張志新(化名)和其女友劉穎石洗,沒(méi)想到半個(gè)月后循诉,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體贤斜,經(jīng)...
    沈念sama閱讀 45,775評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年搂妻,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了蒙保。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,096評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡欲主,死狀恐怖邓厕,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情扁瓢,我是刑警寧澤详恼,帶...
    沈念sama閱讀 35,789評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站引几,受9級(jí)特大地震影響昧互,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜伟桅,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評(píng)論 3 331
  • 文/蒙蒙 一敞掘、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧楣铁,春花似錦玖雁、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,993評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)浓镜。三九已至,卻和暖如春劲厌,著一層夾襖步出監(jiān)牢的瞬間膛薛,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,107評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工补鼻, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留哄啄,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,308評(píng)論 3 372
  • 正文 我出身青樓辽幌,卻偏偏與公主長(zhǎng)得像增淹,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子乌企,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評(píng)論 2 355