SharedPreferences源碼分析

先給出結(jié)論,如果不想跟隨源碼分析的,可以根據(jù)結(jié)論,對SharedPreferences有個大概的了解。

結(jié)論:

1.SharedPreferences 線程是安全的,內(nèi)部由大量synchronized代碼塊實現(xiàn)同步
2.SharedPreferences 進程是不安全的(雖然官方提供了MODE_MULTI_PROCESS加載模式,該加載模式在API級別11中添加,在API級別23中被廢棄。該SharedPreference loading標志:設置后,即使已在此過程中加載了共享首選項實例,也會檢查磁盤上的文件是否已修改。在應用程序具有多個進程的情況下,有時需要此行為,所有進程都寫入相同的SharedPreferences文件。但是,通常在進程之間存在更好的通信形式。 ),但官方文檔也給出了如下說明:

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

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

建議:

1.不要使用多進程操作SharedPreferences ,小概率會出現(xiàn)數(shù)據(jù)丟失(文件被刪除)
2.不要存儲大量的數(shù)據(jù),從磁盤加載SharedPreferences 文件是進行I/O操作的,并且由于文件結(jié)構(gòu)是xml格式的,加載解析都比較耗時,雖然在此過程中是異步的,但如果數(shù)據(jù)過大,首次調(diào)用getSharedPreferences后,然后馬上執(zhí)行getXX()或者putXX()由于需要等待加載文件結(jié)束,可能會阻塞線程(如果在UI線程可能會引發(fā)ANR)。并且SharedPreferences 中的數(shù)據(jù)會一次性從磁盤加載到內(nèi)存,.applycommit都是會將數(shù)據(jù)寫到磁盤的,SharedPreferences 文件不應過大會影響性能
3.盡量使用apply更新數(shù)據(jù),因為它是異步寫入磁盤的,不會阻塞線程

將您的首選項更改從此編輯器提交回它正在編輯的SharedPreferences對象。這將自動執(zhí)行請求的修改,替換SharedPreferences中當前的內(nèi)容。

注意,當兩個編輯器同時修改首選項時,最后一個調(diào)用apply的編輯器將獲勝。

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

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

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

源碼分析:

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

    @Override
    public SharedPreferences getSharedPreferences(File file, int mode) {
        SharedPreferencesImpl sp;
        synchronized (ContextImpl.class) {
            //獲取緩存中所有SharedPreferences對象集合
            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");
                    }
                }
                //如果緩存中不存在,則通過創(chuàng)建SharedPreferencesImpl對象,由其加載文件
                sp = new SharedPreferencesImpl(file, mode);
                //將SharedPreferences對象存入緩存集合中,以便下次直接從內(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對象,并保存到緩存中,多次調(diào)用getSharedPreferences并不會多次創(chuàng)建對象,由于整個SharedPreferences獲取過程中都放在synchronized代碼塊中,因此在多線程下創(chuàng)建對象是安全的
SharedPreferencesImpl創(chuàng)建過程:

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

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

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

另起一個線程加載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();
    
        }
    }

從上面方法可知從磁盤加載SharedPreferences文件分別做了一下幾步處理:
1.如果由備份文件,把原文件刪除,然后修改備份文件名稱,使其成為原文件,達到文件恢復效果(備份文件名以.bak結(jié)尾)
2.加載文件并解析成Map
3.標記加載完成
4.將Map賦值給mMap(即數(shù)據(jù)緩存集合,用于存儲SharedPreferences文件中的數(shù)據(jù))
5.通過notifyAll喚起所有等待加載的線程
現(xiàn)在SharedPreferences對象已經(jīng)初始化成功了,下面我們看一下對其數(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;
      } 
}

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

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

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

putXX()

修改數(shù)據(jù)時首先得獲取Editor對象(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;
            }
        }
        ···代碼省略···
}

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

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);
                    }
                };
            //將寫入磁盤任務加入隊列
            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ū)懭氪疟PSharedPreferencesImpl.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) {
                //寫入磁盤
                writeToFile(mcr, isFromSyncCommit);
            }
            synchronized (mLock) {
                mDiskWritesInFlight--;
            }
            if (postWriteRunnable != null) {
                postWriteRunnable.run();
            }
        }
    };

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

        synchronized (sLock) {
            sWork.add(work);
            //sCanDelay用于紀錄當前線程是否處于寫入磁盤任務狀態(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)建一個線程
                HandlerThread handlerThread = new HandlerThread("queued-work-looper",
                        Process.THREAD_PRIORITY_FOREGROUND);
                handlerThread.start();
                //創(chuàng)建一個handler用于接收queue(Runnable work, boolean shouldDelay)發(fā)送的消息,然后執(zhí)行寫入磁盤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;
}

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容