細(xì)數(shù)SharedPreferences的5大缺陷及ANR原因

我們經(jīng)常使用的SharedPreferences其實是存在很多缺陷的,主要表現(xiàn)在

  • 占用內(nèi)存
  • getValue時可能導(dǎo)致ANR
  • 不支持多進(jìn)程
  • 不支持局部更新
  • commit或apply都可能導(dǎo)致ANR

以下參考安卓源碼的基礎(chǔ)上海洼,使用大白話和部分代碼片段和大家一起探討分享纵装。

占用內(nèi)存

final class SharedPreferencesImpl implements SharedPreferences {
    ......
        //構(gòu)造方法
        SharedPreferencesImpl(File file, int mode) {
        mFile = file;
        mBackupFile = makeBackupFile(file);
        mMode = mode;
        mLoaded = false;
        mMap = null;
        mThrowable = null;
        //從磁盤里獲取xml里的數(shù)據(jù)
        startLoadFromDisk();
    }
    
    .....
}

我們都知道Context的上下文實現(xiàn)是依靠ContextImpl這個類擂煞,而我們的SharedPreferences的實現(xiàn)是依靠SharedPreferencesImpl類骑脱,

ContextImpl.java
    /**
     * Map from package name, to preference name, to cached preferences.
     */
    private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;

在我們的ContextImpl類中存在一個靜態(tài)的ArrayMap對象用于緩存當(dāng)前packageName下的所有sp文件對象昆咽,

image.png

但是在這個類里面我們可以看到緩存數(shù)組的探空 初始化和賦值,但卻沒有對數(shù)組對象里的數(shù)據(jù)進(jìn)行移除或者釋放的操作铛碑,

由此我們也就可以知道狠裹,在我們APP運行的過程中虽界,APP對應(yīng)包目錄下的sp文件都會被緩存到方法區(qū)里去,
而這種機制的話會導(dǎo)致很占內(nèi)存涛菠,而且寧愿OOM也不會主動釋放內(nèi)存空間莉御。

getValue的時候可能導(dǎo)致線程阻塞或ANR

在我們的SharedPreferencesImpl構(gòu)造函數(shù)里,會啟動一個子線程去加載磁盤文件俗冻,把xml文件轉(zhuǎn)換成map對象礁叔,如果文件很大或者線程調(diào)度沒有馬上啟動這個線程的話,那么這個加載的操作需要一段時間后才能執(zhí)行完成迄薄,

 private void startLoadFromDisk() {
        synchronized (mLock) {
            mLoaded = false;
        }
        new Thread("SharedPreferencesImpl-load") {
            public void run() {
                loadFromDisk();
            }
        }.start();
    }

而假如我們剛好初始化的時候緊接著去getValue的話琅关,getValue里面又會通過awaitLoadedLocked方法來校驗是否要阻塞外部線程,

  private void awaitLoadedLocked() {
        if (!mLoaded) {
            BlockGuard.getThreadPolicy().onReadFromDisk();
        }
        while (!mLoaded) {
            try {
            //如果沒有加載完成 就一直持有鎖
                mLock.wait();
            } catch (InterruptedException unused) {
            }
        }
        if (mThrowable != null) {
            throw new IllegalStateException(mThrowable);
        }
    }

確保取值操作前一定是執(zhí)行完成了file文件的加載和轉(zhuǎn)換成功讥蔽,最后在磁盤加載完成時才會notify操作 把我們外部讀取value的線程給喚醒涣易。

在上述的操作場景都是我們APP經(jīng)常會出現(xiàn)的,同時當(dāng)我們sp離數(shù)據(jù)存儲量很大的話冶伞,那這個磁盤加載并阻塞外部線程的時間會比較大 直接就導(dǎo)致了我們主線程獲取sp值的時候直接就芭比Q anr了新症。

不支持多進(jìn)程

名義上我們在獲取sp實例的時候可以傳參支持多進(jìn)程模式,但這個mode參數(shù)也只是起到一個多進(jìn)程數(shù)據(jù)同步的作用响禽,

 static void setFilePermissionsFromMode(String name, int mode,
            int extraPermissions) {
        int perms = FileUtils.S_IRUSR|FileUtils.S_IWUSR
            |FileUtils.S_IRGRP|FileUtils.S_IWGRP
            |extraPermissions;
        if ((mode&MODE_WORLD_READABLE) != 0) {
            perms |= FileUtils.S_IROTH;
        }
        if ((mode&MODE_WORLD_WRITEABLE) != 0) {
            perms |= FileUtils.S_IWOTH;
        }
        FileUtils.setPermissions(name, perms, -1, -1);
    }

這里的同步是指訪問這個sp實例的時候徒爹,會判斷當(dāng)前磁盤文件相對最后一次內(nèi)存修改是否被改動過,如果是的話就重新加載磁盤文件再同步到緩存上芋类,

  public static int setPermissions(String path, int mode, int uid, int gid) {
        try {
            Os.chmod(path, mode);
        } catch (ErrnoException e) {
            Slog.w(TAG, "Failed to chmod(" + path + "): " + e);
            return e.errno;
        }

        if (uid >= 0 || gid >= 0) {
            try {
                Os.chown(path, uid, gid);
            } catch (ErrnoException e) {
                Slog.w(TAG, "Failed to chown(" + path + "): " + e);
                return e.errno;
            }
        }
        return 0;
    }

但這種同步的作用不大隆嗅,因為當(dāng)多進(jìn)程同時修改sp值,但不同進(jìn)程里的內(nèi)存數(shù)據(jù)也不會實時同步侯繁,而且同時修改sp數(shù)據(jù)也會導(dǎo)致數(shù)據(jù)丟失和覆蓋的可能胖喳。

不支持局部更新

apply

public void apply() {
            final long startTime = System.currentTimeMillis();
            final MemoryCommitResult mcr = commitToMemory();
            //這個任務(wù)最終在ActivityThread里的 handleStopService  handlePauseActivity handleStopActivity方法里執(zhí)行
            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);
                    }
                };
            // 最終調(diào)用QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
            //把這個任務(wù)加入到ActivityThread中的QueueWork列表里
            SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
            // changes reflected in memory.
            notifyListeners(mcr);
        }

我們的同步修改commit方法 和異步修改apply方法都是全量更新,也就是即使我們修改的止損一個鍵值對巫击,它也會把數(shù)據(jù)重寫寫入到磁盤文件中禀晓,這樣就會導(dǎo)致不必要的內(nèi)存開銷。

commit或apply都可能導(dǎo)致ANR

在commit和apply的時候還有一個更致命的問題就是他們也會導(dǎo)致ANR坝锰。
這個主要是因為在調(diào)用commit和apply都會執(zhí)行到一個enqueueDiskWrite操作粹懒,這個操作會把當(dāng)前修改sp內(nèi)存數(shù)據(jù)同步到Disk磁盤的任務(wù)加入到ActivityThread里的一個任務(wù)鏈表集合中, 那么我們肯定會想這個磁盤同步任務(wù)什么時候才會最終完成呢顷级,

其實它是需要等到我們的應(yīng)用中service在stop的時候凫乖,或者activity暫停或停止的時候,才會for循環(huán)上面提到的任務(wù)鏈表集合任務(wù)帽芽,最終完成內(nèi)存數(shù)據(jù)到磁盤數(shù)據(jù)的删掀。 那這樣的話會因為有大量的讀寫同步到磁盤的任務(wù)導(dǎo)致activity或者service切換生命周期的時候被阻塞住了,最終導(dǎo)致了ANR导街。

--》handleStopActivity方法(ActivityThread)
--》QueuedWork.waitToFinish()
--》 processPendingWork(); 再到下面最終執(zhí)行磁盤回寫任務(wù)

for (Runnable w : work) {
                    w.run();
                }

綜上披泪,經(jīng)過這些分析想必我們對SharedPreferences有個更了解的地方。

安卓官方推薦我們可以考慮使用jetpack里的DataStore ,或者可以考慮使用騰訊團隊開發(fā)的MMKV框架搬瑰。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末款票,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子泽论,更是在濱河造成了極大的恐慌艾少,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,919評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件翼悴,死亡現(xiàn)場離奇詭異缚够,居然都是意外死亡,警方通過查閱死者的電腦和手機鹦赎,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,567評論 3 392
  • 文/潘曉璐 我一進(jìn)店門谍椅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人钙姊,你說我怎么就攤上這事毯辅。” “怎么了煞额?”我有些...
    開封第一講書人閱讀 163,316評論 0 353
  • 文/不壞的土叔 我叫張陵思恐,是天一觀的道長。 經(jīng)常有香客問我膊毁,道長胀莹,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,294評論 1 292
  • 正文 為了忘掉前任婚温,我火速辦了婚禮描焰,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘栅螟。我一直安慰自己荆秦,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,318評論 6 390
  • 文/花漫 我一把揭開白布力图。 她就那樣靜靜地躺著步绸,像睡著了一般。 火紅的嫁衣襯著肌膚如雪吃媒。 梳的紋絲不亂的頭發(fā)上瓤介,一...
    開封第一講書人閱讀 51,245評論 1 299
  • 那天吕喘,我揣著相機與錄音,去河邊找鬼刑桑。 笑死氯质,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的祠斧。 我是一名探鬼主播闻察,決...
    沈念sama閱讀 40,120評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼梁肿!你這毒婦竟也來了蜓陌?” 一聲冷哼從身側(cè)響起觅彰,我...
    開封第一講書人閱讀 38,964評論 0 275
  • 序言:老撾萬榮一對情侶失蹤吩蔑,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后填抬,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體烛芬,經(jīng)...
    沈念sama閱讀 45,376評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,592評論 2 333
  • 正文 我和宋清朗相戀三年飒责,在試婚紗的時候發(fā)現(xiàn)自己被綠了赘娄。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,764評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡宏蛉,死狀恐怖遣臼,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情拾并,我是刑警寧澤揍堰,帶...
    沈念sama閱讀 35,460評論 5 344
  • 正文 年R本政府宣布,位于F島的核電站嗅义,受9級特大地震影響屏歹,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜之碗,卻給世界環(huán)境...
    茶點故事閱讀 41,070評論 3 327
  • 文/蒙蒙 一蝙眶、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧褪那,春花似錦幽纷、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,697評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至冶忱,卻和暖如春尾菇,著一層夾襖步出監(jiān)牢的瞬間境析,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,846評論 1 269
  • 我被黑心中介騙來泰國打工派诬, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留劳淆,地道東北人。 一個月前我還...
    沈念sama閱讀 47,819評論 2 370
  • 正文 我出身青樓默赂,卻偏偏與公主長得像沛鸵,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子缆八,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,665評論 2 354

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