Android 之不要濫用 SharedPreferences(上)

閃存
Android 存儲優(yōu)化系列專題
  • SharedPreferences 系列

Android 之不要濫用 SharedPreferences(上)
Android 之不要濫用 SharedPreferences(下)

  • ContentProvider 系列(待完善)

Android 存儲選項之 ContentProvider 啟動存在的暗坑
《Android 存儲選項之 ContentProvider 深入分析》

  • 對象序列化系列

Android 對象序列化之你不知道的 Serializable
Android 對象序列化之 Parcelable 深入分析
Android 對象序列化之追求完美的 Serial

  • 數(shù)據(jù)序列化系列(待更)

《Android 數(shù)據(jù)序列化之 JSON》
《Android 數(shù)據(jù)序列化之 Protocol Buffer 使用》
《Android 數(shù)據(jù)序列化之 Protocol Buffer 源碼分析》

  • SQLite 存儲系列

Android 存儲選項之 SQLiteDatabase 創(chuàng)建過程源碼分析
Android 存儲選項之 SQLiteDatabase 源碼分析
數(shù)據(jù)庫連接池 SQLiteConnectionPool 源碼分析
SQLiteDatabase 啟用事務(wù)源碼分析
SQLite 數(shù)據(jù)庫 WAL 模式工作原理簡介
SQLite 數(shù)據(jù)庫鎖機制與事務(wù)簡介
SQLite 數(shù)據(jù)庫優(yōu)化那些事兒


前言

本文不是與大家一起探討關(guān)于 SharedPreferences 的基本使用,而是結(jié)合源碼的角度分析對 SharedPreferences 使用不當(dāng)可能引發(fā)的“嚴重后果”以及該如何正確的使用 SharedPreferences。

SharedPreferences 是 Android 平臺為應(yīng)用開發(fā)者提供的一個輕量級的存儲輔助類,用來保存應(yīng)用的一些常用配置,它提供了 putString()、putString(Set<String>)、putInt()、putLong()、putFloat()、putBoolean() 六種數(shù)據(jù)類型。數(shù)據(jù)最終是以 XML 形式進行存儲。在應(yīng)用中通常做一些簡單數(shù)據(jù)的持久化存儲。SharedPreferences 作為一個輕量級存儲,所以就限制了它的使用場景,如果對它使用不當(dāng)可能會引發(fā)“嚴重后果”。

從源碼角度出發(fā)(基于 API Level 28)
1、 SharedPreferences 文件保存位置
SharedPreferences config = context.getSharedPreferences("config", Context.MODE_PRIVATE);
String value = config.getString("key", "default");

通過 Context 的 getSharedPreferences() 方法得到 SharedPreferences 對象,這里實際調(diào)用的是 ContextImpl.getSharedPreferences() 方法。

@Override
public SharedPreferences getSharedPreferences(String name, int mode){
    //mBase實際類型是 ContextImpl
    return mBase.getSharedPrefenences(name, mode);
}

mBase 的實際類型是 ContextImpl(不熟悉的朋友,可以去看下 Activity 的創(chuàng)建過程,在 ActivityThread 中)。

ContextImpl 中 getSharedPreferences(String name, int mode) 調(diào)用過程如下:

@Override
public SharedPreferences getSharedPreferences(String name, int mode) {  
    if (mPackageInfo.getApplicationInfo().targetSdkVersion <
            Build.VERSION_CODES.KITKAT) {
        if (name == null) {
            //如果 targetSdkVersion 小于 19 版本,name 傳遞 null,
            //則直接將文件名設(shè)置為null,既文件名為:null.xml
            name = "null";
        }
    }

    File file;
    synchronized (ContextImpl.class) {
        if (mSharedPrefsPaths == null) {
            //mSharedPrefsPaths維護文件名name和文件File 的映射關(guān)系
            //這個在較早版本中不存在
            mSharedPrefsPaths = new ArrayMap<>();
        }
        //通過文件名name獲取對應(yīng)的文件File
        file = mSharedPrefsPaths.get(name);
        if (file == null) {
            //SharedPreferences文件目錄創(chuàng)建過程
            file = getSharedPreferencesPath(name);
            mSharedPrefsPaths.put(name, file);
        }
    }
    //根據(jù)File創(chuàng)建SharedPreferences
    return getSharedPreferences(file, mode);
}

代碼中標(biāo)注了詳細的注釋,這里主要維護了 SharedPreferences 文件名 name 和文件 File 的映射關(guān)系,既根據(jù)文件名 name 得到文件 File,每個 Activity 都會包含一個 ContextImpl 對象,mSharedPrefsPaths 是它的成員變量,既僅在當(dāng)前對象有效。這里重點跟蹤下 SharedPreferences 文件的保存目錄,SharedPreferences 文件路徑創(chuàng)建過程:

/**
 * 根據(jù)文件名創(chuàng)建File對象
 */
@Override
public File getSharedPreferencesPath(String name){
  return makeFilename(getPreferencesDir(), name+".xml");
}

getPreferencesDir 方法如下:

@Override
private File getPreferencesDir(){
  synchronized(mSync){
      if(mPreferencesDir == null){
           //創(chuàng)建SharedPreferences文件保存目錄
          //getDataDir返回:/data/data/packageName/
          mPreferencesDir = new File(getDataDir(), "shared_prefs");
      } 
      //確保應(yīng)用私有文件目錄已經(jīng)存在
      return ensurePrivateDirExists(mPreferencesDir);
  }
}

從這里可以看出 SharedPreferences 文件的存儲位置是在應(yīng)用程序包名下 shared_prefs 目錄內(nèi)。

這里需要注意的是文件名 name 不能是路徑形式如:“/config”,如下將會拋出異常:

@override
private File makeFilename(File base, String name){
  if(name.indexOf(File.separatorChar) < 0){
     //SharedPreferences文件名中如果包含“/”字符將會拋出異常
     return new File(base, name);
  }
  throw new IllegalArgumentException("File " + name + " contains a path     separator" );
}

跟蹤到這里,SharedPreferences 的文件保存路徑我們就算是找到了。這一步中主要通過文件名 name 創(chuàng)建對應(yīng)文件 File 對象。并且會將其緩存在 ContextImpl 的 Map(mSharedPerfsPaths)容器中。 接著我們看 SharedPreferences 的創(chuàng)建過程。


2、SharedPreferences 創(chuàng)建過程
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
    SharedPreferencesImpl sp;
    synchronized (ContextImpl.class) {
        //得到用于緩存SharedPreferences的Map容器
        //該Map容器在ContextImpl單例方式聲明
        final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
        sp = cache.get(file);
        if (sp == null) {
            //Android N之后不在支持MODE_WORLD_READABLE 和 MODE_WORLD_WRITEABLE
            checkMode(mode);
            //Android 7.0之后的文件級加密相關(guān)
            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");
                }
            }
            //SharedPreferences首次創(chuàng)建,實際類型是SharedPreferencesImpl
            //SharedPreferences只是一個接口,定義了操作的基本API。
            //真正實現(xiàn)是在SharedPreferencesImpl中
            sp = new SharedPreferencesImpl(file, mode);
            //保存在Map容器中,該Map容器為單例
            cache.put(file, sp);
            return sp;
        }
    }
    if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
        getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
        //MODE_MULTI_PROCESS的加載策略
        sp.startReloadIfChangedUnexpectedly();
    }
    return sp;
}

在該方法中首先看下 getSharedPreferencesCacheLocked 方法如下:

private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
    if (sSharedPrefsCache == null) {
        //sSharedPrefsCache是static ArrayMap容器
        //早期是HashMap,ArrayMap相比HashMap在內(nèi)存占用上略有一定優(yōu)勢
        sSharedPrefsCache = new ArrayMap<>();
    }

    final String packageName = getPackageName();
    //根據(jù)應(yīng)用包名,獲取ArrayMap對象
    ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
    if (packagePrefs == null) {
        packagePrefs = new ArrayMap<>();
        //這里的存儲是根據(jù)包名,保存所有SharedPreferencesImpl集合
        sSharedPrefsCache.put(packageName, packagePrefs);
    }

    return packagePrefs;
}

sSharedPrefsCache 聲明為 static ArrayMap 對象,根據(jù)當(dāng)前應(yīng)用包名 packageName 得到保存 SharedPreferences 的集合并返回。

回到上面的方法中,根據(jù) File 從返回保存 SharedPreferences 的集合中獲取,如果是第一次創(chuàng)建,直接創(chuàng)建 SharedPreferencesImpl 對象,并將其緩存在 Map(sSharedPrefsCache) 容器中。跟蹤到這里我們可以確定 SharedPreferences 的實際返回類型是 SharedPreferencesImpl。

本文基于 API Level 28 分析的 ContextImpl 中關(guān)于 SharedPreferences 處理機制,這相較于較早版本的管理策略有所不同,具體你可以參考之前基于 API Level 16 源碼分析的 SharedPreferences


小結(jié)一下
  • SharedPreferences 只是一個接口,定義了標(biāo)準(zhǔn)操作 API,而真正實現(xiàn)的是 SharedPreferencesImpl,我們后續(xù)的一系列對 SharedPreferences 的操作實際都是通過 SharedPreferencesImpl 完成的。

  • 系統(tǒng)會將每個 SharedPreferences 文件對應(yīng)的操作對象(實際為 SharedPreferencesImpl)進行緩存,后續(xù)相關(guān) Context.getSharedPreferences("name", mode) 都是從該緩存中直接獲取。

  • SharedPreferences 為我們提供了 Context.MODE_MULTI_PROCESS 的加載模式,不知道在上面 getSharedPreferences(File file, int mode) 方法中,你有沒有注意到:

    if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
          getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
          //MODE_MULTI_PROCESS的加載策略
          sp.startReloadIfChangedUnexpectedly();
      }
    

當(dāng)應(yīng)用指定的 targetSdkVersion 小于 API Level 11 時,則重新從文件中加載一遍數(shù)據(jù)到內(nèi)存中,所以指望 SharedPreferences 能夠跨進程通信的可以死心了。關(guān)于 SharedPreferences 跨進程使用分析你可以參考《Android 之不要濫用SharedPreferences(下)


3、SharedPreferences 的數(shù)據(jù)加載過程

終于說到 SharedPreferences 數(shù)據(jù)操作的相關(guān)內(nèi)容了,這部分也是我們要重點討論的內(nèi)容,因為這里面或多或少存在一些暗坑,如果對它不足夠了解,很容易引發(fā)相關(guān)性能問題。

上面有分析到 SharedPreferences 的實際操作類型是 SharedPreferencesImpl,它的構(gòu)造方法如下:

SharedPreferencesImpl(File file, int mode) {
    //SharedPreferences保存文件,前面有分析到
    mFile = file;
    //SharedPreferences備份文件
    mBackupFile = makeBackupFile(file);
    //加載模式
    mMode = mode;
    //標(biāo)志位,表示是否正在加載
    mLoaded = false;
    mMap = null;
    mThrowable = null;
    //開啟線程,加載對應(yīng)文件數(shù)據(jù)到Map容器中
    startLoadFromDisk();
}

有關(guān) SharedPreferences 的備份文件 mBackupFile 的作用,由于這部分內(nèi)容也比較多,主要涉及到 SharedPreferences 的數(shù)據(jù)丟失,和多進程使用場景,如果想更深入了解該部分內(nèi)容你可以參考這里。

在 SharedPreferencesImpl 的構(gòu)造方法中,我們需要重點跟蹤方法的最后 startLoadFromDisk 方法如下:

private void startLoadFromDisk() {
    synchronized (mLock) {
        //加載狀態(tài)標(biāo)志位,每當(dāng)需要加載時,先將其置為false
        //加載完成之后再置為true
        mLoaded = false;
    }
    new Thread("SharedPreferencesImpl-load") {
        
        public void run() {
            //開啟獨立線程進行數(shù)據(jù)加載
            loadFromDisk();
        }
    }.start();
}

mLoaded 起到加載狀態(tài)標(biāo)志的作用,該標(biāo)志狀態(tài)非常重要(主要是多線程訪問等待),如果此時在 UI 線程操作 SharedPreferences 數(shù)據(jù),可能導(dǎo)致 UI 線程等待。后面會詳細分析到該部分。

SharedPreferences 文件內(nèi)容加載使用了異步線程,真正開始加載 loadFromDisk 方法如下:

//代碼中省略了部分
private void loadFromDisk() {
    synchronized (mLock) {
        if (mLoaded) {
            //防止重復(fù)加載
            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");
    }
    //保存即將加載的數(shù)據(jù)容器
    Map<String, Object> map = null;
    //主要在MODE_MULTI_PROCESS起到作用
    StructStat stat = null; 
    //確定加載過程中是否發(fā)生過異常
    Throwable thrown = null;
    try {
        stat = Os.stat(mFile.getPath());
        if (mFile.canRead()) {
            BufferedInputStream str = null;
            try {
                //通過BufferedInputStream從文件中讀取內(nèi)容
                str = new BufferedInputStream(
                        new FileInputStream(mFile), 16 * 1024);
                //SharedPreferences的文件操作都封裝在XmlUtils中
                //返回Map實例
                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) {
        //表示SharedPreferences文件中數(shù)據(jù)已經(jīng)加載到內(nèi)存Map中
        mLoaded = true;
        mThrowable = thrown;
        try {
            //表示加載過程未發(fā)生異常
            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();
        }
    }
}

代碼篇幅雖然較長,但是不難理解,源碼可以看出通過 BufferedInputStream 加載對應(yīng) SharedPreferences 文件內(nèi)容,系統(tǒng)封裝了 XmlUtils 進行 XML 文件數(shù)據(jù)讀寫,并且將數(shù)據(jù)封裝在 Map 容器并返回,如果整個過程未發(fā)生任何異常,則直接將其賦值給 SharedPreferencesImpl 的成員 mMap,聲明如下:

private Map<String, Object> mMap;

跟蹤到這里 SharedPreferences 的首次加載機制就已經(jīng)明確了,每個 SharedPreferences 存儲都會對應(yīng)一個 name.xml 文件,在使用時,系統(tǒng)通過異步線程一次性將該文件內(nèi)容加載到內(nèi)存中,保存在 Map 容器中。實際后續(xù)我們對 SharedPreferences 的一些列 getXxx() 操作都是直接操作的該 Map 容器。后面我們將驗證到該部分內(nèi)容。

小結(jié)一下

SharedPreferencesImpl 在初始化時,會開啟異步線程加載對應(yīng) name 的 XML 文件內(nèi)容到 Map 容器中,如果文件內(nèi)容較大,這一過程耗時還是不能忽視的,主要體現(xiàn)在如果此時我們操作 SharedPreferences 會導(dǎo)致線程等待問題,這里主要根據(jù)前面分析到的加載狀態(tài)標(biāo)志 mLoaded 變量有關(guān),接下來我們就對其進行分析。


4、一系列 getXxx() 操作

通過前面的分析,你肯定也能想到:SharedPreferences 的數(shù)據(jù)都保存在 Map 容器中,此時就是根據(jù) Key 到該 Map 容器中查找對應(yīng)的數(shù)據(jù)即可,以 getString() 為例:

@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
    synchronized (mLock) {
        //這里就是根據(jù)前面分析到的mLoaded加載狀態(tài)標(biāo)志
        //判斷當(dāng)前SharedPreferences文件內(nèi)容是否加載完成
        //否則調(diào)用方線程進入等待wait
        awaitLoadedLocked();
        //這里直接就是從Map容器中獲取
        String v = (String)mMap.get(key);
        return v != null ? v : defValue;
    }
}

可以看到直接根據(jù) key 到 Map 中查找對應(yīng)的數(shù)據(jù)并返回。
這里我們還需要重點跟蹤 mLoaded 標(biāo)志起到的作用,awaitLoadedLocked 方法如下:

private void awaitLoadedLocked() {
    if (!mLoaded) {
        BlockGuard.getThreadPolicy().onReadFromDisk();
    }
    while (!mLoaded) {
        //加載狀態(tài)標(biāo)志位,如果未加載完成,該變量為false,會將調(diào)用線程wait住
        try {
            mLock.wait();
        } catch (InterruptedException unused) {
        }
    }
    if (mThrowable != null) {
        throw new IllegalStateException(mThrowable);
    }
}

還記得前面分析 SharedPreferences 數(shù)據(jù)加載過程 mLoaded 標(biāo)志位,在開始加載文件數(shù)據(jù)之前先將該標(biāo)志位置為 false,從文件加載完成之后,重新將其置為 true,表示此次文件內(nèi)容加載完成。如果加載過程較為耗時,此時我們在 UI 線程中對 SharedPreferences 做相關(guān)數(shù)據(jù)操作,該線程就會進入 wait 狀態(tài)。這就導(dǎo)致出現(xiàn)主線程等待低優(yōu)先級線程鎖的問題,比如一個 100KB 的 SP 文件讀取等待時間大約需要 50 ~ 100ms。此時非常容易造成卡頓,如果再嚴重甚至?xí)l(fā) ANR。這里涉及到一個優(yōu)化點,最后會給大家總結(jié)出。

mLock 鎖的喚醒操作,在 loadFromDisk 方法最后,喚醒所有等待線程(如果存在)

       try {              
            // ... 省略
        } catch (Throwable t) {
            mThrowable = t;
        } finally {
            mLock.notifyAll();
        }
小結(jié)一下
  • mLoaded 標(biāo)志起到 SharedPreferences 文件內(nèi)容是否加載完成(加載到 Map 容器中),如果未加載完成,此時對其做相關(guān)數(shù)據(jù)操作就會導(dǎo)致 awaitLoadedLocked 方法的等待。

  • 通過 SharedPreferences 存儲的數(shù)據(jù)都會在內(nèi)存中保留一份(Map 變量中),后續(xù)的一系列 getXxx() 操作直接在該容器中獲取數(shù)據(jù)。


5、一系列 putXxx() 操作

前面分析到對 SharedPreferences 的一系列 getXxx() 操作,大家此時是否會認為 putXxx() 操作也是直接對該 Map 容器操作呢?顯然不是的,修改數(shù)據(jù)操作相比 getXxx() 操作要麻煩很多,繼續(xù)結(jié)合源碼進行分析:

SharedPreferences config = context.getSharedPreferences("config", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = config.edit().putString("key", "value");
//提交
editor.apply();

put 操作要首先經(jīng)過 edit 方法返回 Editor 對象:

@Override
public Editor edit() {
    synchronized (mLock) {
        //這里與一系列g(shù)etXxx()作用一致
        //同樣受到mLoaded標(biāo)志狀態(tài)的作用
        awaitLoadedLocked();
    }
    //實際返回的是EditorImpl
    return new EditorImpl();
}

SharedPreferences 的 edit 方法實際返回的是 EditorImpl 對象:

public final class EditorImpl implements Editor {
    private final Object mEditorLock = new Object();

    /**
     * 保存修改數(shù)據(jù)的容器
     * 一系列添加、修改、刪除數(shù)據(jù)都保存在該臨時容器Map中
     */
    @GuardedBy("mEditorLock")
    private final Map<String, Object> mModified = new HashMap<>();

    //標(biāo)志當(dāng)前是否是清除操作
    @GuardedBy("mEditorLock")
    private boolean mClear = false;

    //添加String類型數(shù)據(jù)
    @Override
    public Editor putString(String key, @Nullable String value) {
        synchronized (mEditorLock) {
            //添加到一個臨時Map容器
            mModified.put(key, value);
            return this;
        }
    }

   //其它數(shù)據(jù)類型添加省略

}

Editor 只是一個接口,與 SharedPreferences 功能類似,定義基礎(chǔ)操作 API,我們一系列的 putXxx()、remove()、clear()、apply()、commit() 實際都是在 EditorImpl 中完成。

從源碼中我們可以看出,操作數(shù)據(jù)都保存在 EditorImpl 中的 mModified 容器中,最后我們必須通過 commit 或 apply 進行提交,這里也是我們重點要分析的。

這里也需要注意每次通過 SharedPreferences.edit() 都會創(chuàng)建一個新的 EditorImpl 對象,應(yīng)該盡量批量操作統(tǒng)一提交。最后會一起總結(jié)出。

任務(wù)提交 commIt 或 apply 方法調(diào)用幾乎一致,都會經(jīng)過 commitToMemory 方法后調(diào)用 enqueueDiskWrite 方法。不同之處在于 enqueueDiskWrite 方法,如果當(dāng)前是 commit 提交,則將數(shù)據(jù)寫入文件任務(wù)在當(dāng)前線程執(zhí)行;否則 apply 提交則將寫入文件任務(wù)在工作線程中完成,看下詳細過程:

    @Override
    public boolean commit() {
        long startTime = 0;

        if (DEBUG) {
            startTime = System.currentTimeMillis();
        }

        //將mModified容器中數(shù)據(jù)提交到SharedPreferencesImpl成員Map容器中
        //后者數(shù)據(jù)要寫入文件時使用
        MemoryCommitResult mcr = commitToMemory();
        //將MemoryCommitResult作為參數(shù)
        //根據(jù)策略 commit/apply決定任務(wù)在工作線程還是在當(dāng)前線程
        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");
            }
        }
        //通知外部監(jiān)聽
        notifyListeners(mcr);
        return mcr.writeToDiskResult;
    }

我們先來跟蹤下 commitToMemory 方法過程:

private MemoryCommitResult commitToMemory() {
        long memoryStateGeneration;
        //保存發(fā)生變化的key
        List<String> keysModified = null;
        //外部監(jiān)聽器
        Set<OnSharedPreferenceChangeListener> listeners = null;
        Map<String, Object> mapToWriteToDisk;

        synchronized (SharedPreferencesImpl.this.mLock) {
            if (mDiskWritesInFlight > 0) {
                //數(shù)據(jù)拷貝
                mMap = new HashMap<String, Object>(mMap);
            }
            //將成員mMap賦值給局部變量,后續(xù)for循環(huán)中
            mapToWriteToDisk = mMap;
            mDiskWritesInFlight++;

            //我們可以監(jiān)聽SharedPreferences數(shù)據(jù)提交完成
            boolean hasListeners = mListeners.size() > 0;
            if (hasListeners) {
                keysModified = new ArrayList<String>();
                //這里收集回調(diào)通知
                listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
            }

            synchronized (mEditorLock) {
                //該標(biāo)志主要作用是確保當(dāng)前是否真正發(fā)生變化,避免無謂的I/O操作。
                boolean changesMade = false;

                if (mClear) {
                    //如果是clear操作,可以看出直接清空數(shù)據(jù)
                    if (!mapToWriteToDisk.isEmpty()) {
                        changesMade = true;
                        mapToWriteToDisk.clear();
                    }
                    mClear = false;
                }
                //這里開始遍歷一系列修改后的數(shù)據(jù)容器mModified
                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)) {
                            //value等于null,然后mMap由不包含該key
                            //可以直接跳過
                            continue;
                        }
                        //如果value==null,可以直接將其移除
                        mapToWriteToDisk.remove(k);
                    } else {
                        if (mapToWriteToDisk.containsKey(k)) {
                            //如果mMap容器中包含該key,則直接修正為最新提交數(shù)據(jù)value
                            Object existingValue = mapToWriteToDisk.get(k);
                            if (existingValue != null && existingValue.equals(v)) {
                                //如果value相等則跳過本次
                                //主要是考慮changesMode標(biāo)志位,確認當(dāng)前數(shù)據(jù)是否真正發(fā)生變化
                                continue;
                            }
                        }
                        //否則直接添加新的key:value
                        mapToWriteToDisk.put(k, v);
                    }
                    //這里在for循環(huán)中,如果發(fā)生數(shù)據(jù)變化,該changeMade將會置為true
                    //表示當(dāng)前數(shù)據(jù)發(fā)生變化
                    changesMade = true;
                    if (hasListeners) {
                        keysModified.add(k);
                    }
                }
                //清空臨時修改數(shù)據(jù)容器
                mModified.clear();

                if (changesMade) {
                    mCurrentMemoryStateGeneration++;
                }

                memoryStateGeneration = mCurrentMemoryStateGeneration;
            }
        }
        return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
                mapToWriteToDisk);
    }

系統(tǒng)考慮到本次提交數(shù)據(jù)是否真正發(fā)生變化:changesMade 變量的作用,否則在提交時會直接 return 處理,該部分內(nèi)容你可以參考下一篇《Android 之不要濫用 SharedPreferences(下)》。這算是一層優(yōu)化,避免無謂的 I/O 操作。

其實不難分析出 commitToMemory 方法主要工作是:前面我們一系列的 putXxx() 或 remove() 操作都會添加到 mModified 臨時容器中,mModified 保留著我們當(dāng)前的改變,通過遍歷該容器與 mMap(SharedPreferencesImpl 成員)容器做比較,比如相同 key 不同 value 此時將修改提交到 mMap 容器中,然后 mMap 中就保存了修正后,我們最后一次提交的數(shù)據(jù)。最后清空 mModified 容器。

重新回到前面 commit 方法,調(diào)用 enqueueDiskWrite 方法如下:

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

    //執(zhí)行寫入文件的Runnalbe任務(wù)
    //這里也主要區(qū)分commit或apply提交的區(qū)別
    //apply提交會將該任務(wù)丟入線程池,異步執(zhí)行
    final Runnable writeToDiskRunnable = new Runnable() {
        @Override
        public void run() {
            synchronized (mWritingToDiskLock) {
                //這里執(zhí)行寫入文件操作
                writeToFile(mcr, isFromSyncCommit);
            }
            synchronized (mLock) {
                mDiskWritesInFlight--;
            }
            if (postWriteRunnable != null) {
                postWriteRunnable.run();
            }
        }
    };

    //當(dāng)commit提交時,會在當(dāng)前線程執(zhí)行run方法
    if (isFromSyncCommit) {
        boolean wasEmpty = false;
        synchronized (mLock) {
            wasEmpty = mDiskWritesInFlight == 1;
        }
        if (wasEmpty) {
            //commit操作,直接在當(dāng)前線程中執(zhí)行
            writeToDiskRunnable.run();
            return;
        }
    }
    //如果是apply(),提交則將任務(wù)加入線程池排隊執(zhí)行
    QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}

writeToDiskRunnable 是執(zhí)行寫入文件操作的任務(wù)(就是將最后一次 commitToMemory 之后的 mMap 數(shù)據(jù)寫回到文件)。

如果是 commit 操作,會直接在當(dāng)前線程中執(zhí)行 writeToDiskRunnable.run();除了 commit 提交之外,還可以 apply 進行提交,此時 writeToDiskRunnable 任務(wù)將被添加到線程池,該線程池只有一個線程,故所有提交的任務(wù)都需要經(jīng)過串行等待執(zhí)行。(注意:QueuedWork 早期版本實現(xiàn)是只有一個線程的線程池,本文依據(jù) API Level 28 分析,系統(tǒng)已經(jīng)改成 HandlerThread ,熟悉它的朋友都知道,這仍然是串行執(zhí)行)

無論是使用 commit 還是 apply 數(shù)據(jù)提交 ,即使我們只改動其中一個條目,都會把整個內(nèi)容(mMap)全部寫入到文件。而且即使我們多次寫入同一個文件,SP 也沒有將多次修改合并為一次,這也是 SharedPreferences 性能差的重要原因之一

分析到這里關(guān)于 SharedPreferences 數(shù)據(jù)提交過程:commit 發(fā)生在當(dāng)前線程,apply 發(fā)生在工作線程,如果要保證 I/O 操作不阻塞 UI 線程我們可以優(yōu)先考慮使用 apply 來提交修改,這樣是否就絕對安全了呢?這里先告訴大家絕對不是的?。?!


6、apply() 異步提交一定安全嗎?

前面說到 apply 使寫入文件任務(wù)發(fā)生在工作線程中,這樣防止 I/O 操作阻塞 UI 線程;但它同樣可能會引發(fā)卡頓性能問題,我們需要跟蹤另外一部分系統(tǒng)源碼:

首先 Android 四大組件的創(chuàng)建以及生命周期管理調(diào)用,都是通過進程間通信完成的,到我們自己應(yīng)用進程,通過調(diào)度完成過渡任務(wù)的是 ActivityThread,ActivityThread 是我們應(yīng)用進程的入口類(main 方法所在),來看下 Activity 的 onPause 的回調(diào)過程:

@Override
public void handlePauseActivity(IBinder token, boolean show, int configChanges, PendingTransactionActions pendingactions, boolean finalStateRequest, String reason){
    //... 省略
    if(!r.isPreHoneycomb()){
      //這里檢查,異步提交的SharedPreferences任務(wù)是否已經(jīng)完成
      //否則一直等到執(zhí)行完成
      QueuedWork.waitToFinish();
    }
    //... 省略
 }

你沒有看錯又要等待,等待什么呢?
我們通過 SharedPreferences 一系列的 apply 提交的任務(wù),都會被加入到工作線程 QueueWork 中,該任務(wù)隊列以串行方式執(zhí)行(只有一個工作線程),如果我們 apply 提交非常多的任務(wù),此時判斷任務(wù)隊列還未執(zhí)行完成,就會一直等到全部執(zhí)行完成,這就非常容易發(fā)生卡頓,如果超過 5s 還會引發(fā) ANR。

由此可見 apply 提交也不是”絕對安全“的,試想當(dāng)你 apply 提交大量任務(wù),并且還都是大型 key 或 value 時?。?!

總結(jié)

SharedPreferences 的實際操作者是 SharedPreferencesImpl,當(dāng)首次創(chuàng)建 SharedPreferences 對象,會根據(jù)文件名將對應(yīng)文件內(nèi)容使用異步線程一次性加載到 Map 容器中,試想如果此時存儲了一些大型 key 或 value 它們一直在內(nèi)存中得不到釋放。如果加載過程中,對其做相關(guān)數(shù)據(jù)操作,會導(dǎo)致線程等待 awaitLoadedLocked。系統(tǒng)會緩存每個使用過的 SharedPreferencesImpl 對象。每當(dāng)我們 edit 都會創(chuàng)建一個新的 EditorImpl 對象,當(dāng)修改或者添加數(shù)據(jù)時會將其添加到 EditorImpl 的 mModifiled 容器中,通過 commit 或 apply 提交后會比較 mModifiled 與 mMap 容器數(shù)據(jù),修正(commitToMemory 方法作用) mMap 中最后一次數(shù)據(jù)提交后寫入文件。


優(yōu)化建議:

1、不要存放大的 key 或 value 在 SharedPreferences 中,否則會一直存儲在內(nèi)存中(Map 容器中)得不到釋放,內(nèi)存使用過高會頻繁引發(fā) GC,導(dǎo)致界面丟幀甚至 ANR。

2、不相關(guān)的配置選項最好不要放在一起,單個文件越大加載時間越長。(參照 SharedPreferences 初始化時會開啟異步線程讀取對應(yīng)文件,如果此時耗時較長,當(dāng)對其進行相關(guān)數(shù)據(jù)操作時會導(dǎo)致線程等待)

3、讀取頻繁的 key 和 不頻繁的 key 盡量不要放在一起。(如果整個文件本身就較小則可以忽略)

4、不要每次都 edit 操作,每次 edit 都會創(chuàng)建新的 EditorImpl 對象,最好批量處理統(tǒng)一提交。否則每次 edit().commit() 都會創(chuàng)建新的 EditorImpl 對象并進行一次 I/O 操作,嚴重影響性能。

5、commit 提交發(fā)生在 UI 線程,apply 提交發(fā)生在工作線程,對于數(shù)據(jù)的提交最好是批量操作統(tǒng)一提交。雖然 apply 任務(wù)發(fā)生在工作線程(不會因為 I/O 阻塞 UI 線程),但是如果添加過多任務(wù)也有可能帶來其它”嚴重后果“(參照系統(tǒng)源碼 ActivityThread - handlePauseActivity 方法實現(xiàn))。

6、盡量不要存放 JSON 或 HTML 類型數(shù)據(jù),這種可以直接文件存儲。

7、最好能夠提前初始化 SharedPreferences,避免 SharedPreferences 第一次創(chuàng)建時讀取文件內(nèi)容線程未結(jié)束而出現(xiàn)的等待情況,參照優(yōu)化點第 2 條。

8、不要指望它能夠跨進程通信:Context.MODE_MULTI_PROCESS


以上便是對 SharedPreferences 的學(xué)習(xí)心得和指導(dǎo)建議,文中如果不妥或有更好的分析結(jié)果,歡迎你的指正。

有關(guān) SharedPreferences 更深入分析請參考下篇《Android 之不要濫用SharedPreferences(下)》。

文章如果對你有幫助,請留個贊吧!如果喜歡我的分析還可以閱讀系列的其他相關(guān)文章。


擴展閱讀

...

UI 優(yōu)化系列

網(wǎng)絡(luò)優(yōu)化系列

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

相關(guān)閱讀更多精彩內(nèi)容

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