不要濫用SharedPreference

SharedPreference是Android上一種很是易用的輕量級存儲方式,因爲其API及其友好,獲得了不少不少開發者的青睞。可是,SharedPreference並非萬能的,若是把它用在不合適的使用場景,那麼將會帶來災難性的後果android

存儲超大的value

第一次看到下面這個sp的時候,個人心裏是崩潰的:json

 

 

一個默認的sp有90K,當我打開它的時候,我都快哭了:除了零零星星的幾個很小的key以外,存儲了一個炒雞大的key,這一個key至少佔了其中的89K。知道這是什麼概念嗎?緩存

在小米1S這種手機上,就算獲取這個sp裏面一個很小的key,會花費120+ms!!那個絕不相干的key拖慢了其餘全部key的讀取速度!固然,在性能稍好的手機上,這個問題不是特別嚴重。可是要知道,120ms這個是徹底不能忍的!app

之因此說SharedPreference(下文簡稱sp)是一種輕量級的存儲方式,是它的設計所決定的:sp在建立的時候會把整個文件所有加載進內存,若是你的sp文件比較大,那麼會帶來幾個嚴重問題:ide

  1. 第一次從sp中獲取值的時候,有可能阻塞主線程,使界面卡頓、掉幀。
  2. 解析sp的時候會產生大量的臨時對象,致使頻繁GC,引發界面卡頓。
  3. 這些key和value會永遠存在於內存之中,佔用大量內存。

也許有童鞋會說,sp的加載不是在子線程麼,怎麼會卡住主線程?子線程IO就必定不會阻塞主線程嗎?函數

下面是默認的sp實現SharedPreferenceImpl這個類的getString函數:post

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

繼續看看這個awaitLoadedLocked:性能

private void awaitLoadedLocked() {
    while (!mLoaded) {
        try {
            wait();
        } catch (InterruptedException unused) {
        }
    }
}

一把鎖就是掛在那裏!!這意味着,若是你直接調用getString,主線程會等待加載sp的那麼線程加載完畢!這不就把主線程卡住了麼?測試

另外,有一個叫訣竅能夠節省一下等待的時間:既然getString之類的操做會等待sp加載完成,而加載是在另一個線程執行的,咱們可讓sp先去加載,作一堆事情,而後再getString!以下:優化

// 先讓sp去另一個線程加載
SharedPreferences sp = getSharedPreferences("test", MODE_PRIVATE);
// 作一堆別的事情
setContentView(testSpJson);
// ...

// OK,這時候估計已經加載完了吧,就算沒完,咱們在本來應該等待的時間也作了一些事!
String testValue = sp.getString("testKey", null);

更爲嚴重的是,被加載進來的這些大對象,會永遠存在於內存之中,不會被釋放。咱們看看ContextImpl這個類,在getSharedPreference的時候會把全部的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;
}

注意這個static的sSharedPrefsCache,它保存了你全部使用的sp,而後sp裏面有一個成員mMap保存了全部的鍵值對;這樣,你程序中使用到的那些個sp永遠就呆在內存中,是否是毛骨悚然?!

因此,請不要在sp裏面存儲超級大的key碰到這樣的豬隊友,請讓他自行反省!!趕忙把自家App檢查一下!!

存儲JSON等特殊符號不少的value

還有一些童鞋,他在sp裏面存json或者HTML;這麼作不是不能夠,可是,若是這個json相對較大,那麼也會引發sp讀取速度的急劇降低。

JSON或者HTML格式存放在sp裏面的時候,須要轉義,這樣會帶來不少 & 這種特殊符號,sp在解析碰到這個特殊符號的時候會進行特殊的處理,引起額外的字符串拼接以及函數調用開銷。而JSON原本就是能夠用來作配置文件的,你幹嗎又把它放在sp裏面呢?畫蛇添足。下面我寫個demo驗證一下。

下面這個sp是某個app的換膚配置:

 

咱們先用sp進行讀取,而後用直接把它丟json文件,直接讀取而且解析;json使用的代碼以下:

public int getValueByJson(Context context, String key) {
    File jsonFile = new File(context.getFilesDir().getParent() + File.separator + SP_DIR_NAME, "skin_beta2.json");
    FileInputStream fis = null;
    ByteArrayOutputStream bao = new ByteArrayOutputStream();
    try {
        fis = new FileInputStream(jsonFile);
        FileChannel channel = fis.getChannel();
        ByteBuffer buffer = ByteBuffer.allocate(1 << 13); // 8K
        int i1;
        while ((i1 = channel.read(buffer)) != -1) {
            buffer.flip();
            bao.write(buffer.array(), 0, i1);
            buffer.clear();
        }

        String content = bao.toString();
        JSONObject jsonObject = new JSONObject(content);
        return jsonObject.getInt(key);
    } catch (IOException e) {
        e.printStackTrace();
    } catch (JSONException e) {
        throw new RuntimeException("not a json file");
    } finally {
        close(fis);
        close(bao);
    }
    return 0;
}

而後個人測試結果是:直接解析JSON比在xml裏面要快一倍!在小米1S上結果以下:

時間jsonspMi 1S8038Nexus5X6.53.5

這個JSON的讀取尚未作任何的優化,提高潛力巨大!所以,若是你須要用JSON作配置,請不要把它存放在sp裏面!!

屢次edit屢次apply

我見過這樣的使用代碼:

SharedPreferences sp = getSharedPreferences("test", MODE_PRIVATE);
sp.edit().putString("test1", "sss").apply();
sp.edit().putString("test2", "sss").apply();
sp.edit().putString("test3", "sss").apply();
sp.edit().putString("test4", "sss").apply();

每次edit都會建立一個Editor對象,額外佔用內存;固然多建立幾個對象也影響不了多少;可是,屢次apply也會卡界面你造嗎?

有童鞋會說,apply不是在別的線程些磁盤的嗎,怎麼可能卡界面?我帶你仔細看一下源碼。

public void apply() {
    final MemoryCommitResult mcr = commitToMemory();
    final Runnable awaitCommit = new Runnable() {
            public void run() {
                try {
                    mcr.writtenToDiskLatch.await();
                } catch (InterruptedException ignored) {
                }
            }
        };

    QueuedWork.add(awaitCommit);

    Runnable postWriteRunnable = new Runnable() {
            public void run() {
                awaitCommit.run();
                QueuedWork.remove(awaitCommit);
            }
        };

    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
    notifyListeners(mcr);
}

注意兩點,第一,把一個帶有await的runnable添加進了QueueWork類的一個隊列;第二,把這個寫入任務經過enqueueDiskWrite丟給了一個只有單個線程的線程池執行。

到這裏一切都OK,在子線程裏面寫入不會卡UI。可是,你去ActivityThread類的handleStopActivity裏看一看:

private void handleStopActivity(IBinder token, boolean show, int configChanges, int seq) {

    // 省略無關。。
    // Make sure any pending writes are now committed.
    if (!r.isPreHoneycomb()) {
        QueuedWork.waitToFinish();
    }

    // 省略無關。。
}

waitToFinish?? 又要等?源碼以下:

public static void waitToFinish() {
    Runnable toFinish;
    while ((toFinish = sPendingWorkFinishers.poll()) != null) {
        toFinish.run();
    }
}
做者:weishu
連接:https://zhuanlan.zhihu.com/p/22913991
來源:知乎
著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。

還記得這個toFinish的Runnable是啥嗎?就是上面那個awaitCommit它裏面就一句話,等待寫入線程!!若是在Activity Stop的時候,已經寫入完畢了,那麼萬事大吉,不會有任何等待,這個函數會立馬返回。可是,若是你使用了太屢次的apply,那麼意味着寫入隊列會有不少寫入任務,而那裏就只有一個線程在寫。當App規模很大的時候,這種狀況簡直就太常見了!

所以,雖然apply是在子線程執行的,可是請不要無節制地apply;commit我就很少說了吧?直接在當前線程寫入,若是你在主線程幹這個,當心捱揍。

用來跨進程

還有童鞋發現sp有一個貌似能夠提供「跨進程」功能的FLAG——MODE_MULTI_PROCESS,咱們看看這個FLAG的文檔:

@deprecated MODE_MULTI_PROCESS does not work reliably in
some versions of Android, and furthermore does not provide any mechanism for reconciling concurrent modifications across processes. Applications should not attempt to use it. Instead, they should use an explicit cross-process data management approach such as {@link android.content.ContentProvider ContentProvider}.

 

文檔也說了,這玩意在某些Android版本上不可靠,而且將來也不會提供任何支持,要是用跨進程數據傳輸須要使用相似ContentProvider的東西。並且,SharedPreference的文檔也特別說明:

Note: This class does not support use across multiple processes.

那麼咱們姑且看一看,設置了這個Flag到底幹了啥;在SharedPreferenceImpl裏面,沒有發現任何對這個Flag的使用;而後咱們去ContextImpl類裏面找找getSharedPreference的時候作了什麼:

@Override
public SharedPreferences getSharedPreferences(File file, int 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;
        }
    }
    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;
}

這個flag保證了啥?保證了在API 11之前的系統上,若是sp已經被讀取進內存,再次獲取這個sp的時候,若是有這個flag,會從新讀一遍文件,僅此而已!因此,若是仰仗這個Flag作跨進程存取,簡直就是丟人現眼。

小結

總價一下,sp是一種輕量級的存儲方式,使用方便,可是也有它適用的場景。要優雅滴使用sp,要注意如下幾點:

  1. 不要存放大的key和value!我就不重複三遍了,會引發界面卡、頻繁GC、佔用內存等等,好自爲之!
  2. 絕不相關的配置項就不要丟在一塊兒了!文件越大讀取越慢,不知不覺就被豬隊友給坑了;藍後,放進defalut的那個簡直就是愚蠢行爲!
  3. 讀取頻繁的key和不易變更的key儘可能不要放在一塊兒,影響速度。(若是整個文件很小,那麼忽略吧,爲了這點性能添加維護成本得不償失)
  4. 不要亂edit和apply,儘可能批量修改一次提交!
  5. 儘可能不要存放JSON和HTML,這種場景請直接使用json!
  6. 不要期望用這貨進行跨進程通訊!!!
相關文章
相關標籤/搜索