DirectBoot與SharedPreference丟失之謎

DirectBoot模式是什么

DirectBoot(簡(jiǎn)稱DB)是Android N新引入的一個(gè)特性蔚润,本質(zhì)上是對(duì)數(shù)據(jù)訪問(wèn)做了限制奸远。在用戶開(kāi)機(jī)但未解鎖之前既棺,應(yīng)用只能訪問(wèn)這個(gè)安全區(qū)內(nèi)的數(shù)據(jù)讽挟,從而保護(hù)用戶隱私安全。
Android N上把數(shù)據(jù)分成了兩塊丸冕,分別是:

  • 憑據(jù)保護(hù)存儲(chǔ)區(qū)(credential-protected)耽梅,這是所有應(yīng)用的默認(rèn)存儲(chǔ)位置,僅在用戶解鎖設(shè)備后可用晨仑。
  • 設(shè)備保護(hù)存儲(chǔ)區(qū)(device-protected)褐墅,這是一個(gè)新的存儲(chǔ)位置,當(dāng)設(shè)備啟動(dòng)后(包括DB階段)隨時(shí)都可訪問(wèn)該位置洪己。

SharedPreference簡(jiǎn)述

SharedPreference(簡(jiǎn)稱SP)是Android原生提供的一種數(shù)據(jù)持久化方式妥凳,因?yàn)槠銩PI友好而收到開(kāi)發(fā)者的青睞。
先來(lái)看下SP是怎么存儲(chǔ)數(shù)據(jù)的吧答捕。
SP的實(shí)現(xiàn)在SharedPreferenceImpl中逝钥,直接在Android Studio中無(wú)法查找到這個(gè)類,可以進(jìn)入目錄/Android/sdk/source/android-xx/android/app中找到這個(gè)類拱镐。
SP的getInt方法:

public int getInt(String key, int defValue) {
        synchronized (mLock) {
            awaitLoadedLocked();
            Integer v = (Integer)mMap.get(key);
            return v != null ? v : defValue;
        }
    }

可以看到艘款,所謂的SharedPreference其實(shí)就是維護(hù)了一個(gè)map,所有數(shù)據(jù)的存儲(chǔ)和讀取都通過(guò)操作這個(gè)map來(lái)實(shí)現(xiàn)沃琅。而且哗咆,這個(gè)map是常駐內(nèi)存的。這就帶來(lái)了一個(gè)問(wèn)題:內(nèi)存泄漏益眉!當(dāng)存儲(chǔ)的數(shù)目過(guò)多或者其中一個(gè)kay-value鍵值對(duì)過(guò)大的時(shí)候晌柬,就很可能造成OOM!
那么這個(gè)map是怎么來(lái)的呢郭脂?看看下面這個(gè)方法

   private void loadFromDisk() {
        synchronized (mLock) {
            if (mLoaded) {
                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");
        }

        Map map = null;
        StructStat stat = null;
        try {
            stat = Os.stat(mFile.getPath());
            if (mFile.canRead()) {
                BufferedInputStream str = null;
                try {
                    str = new BufferedInputStream(
                            new FileInputStream(mFile), 16*1024);
                    map = XmlUtils.readMapXml(str);
                } catch (Exception e) {
                    Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
                } finally {
                    IoUtils.closeQuietly(str);
                }
            }
        } catch (ErrnoException e) {
            /* ignore */
        }

        synchronized (mLock) {
            mLoaded = true;
            if (map != null) {
                mMap = map;
                mStatTimestamp = stat.st_mtime;
                mStatSize = stat.st_size;
            } else {
                mMap = new HashMap<>();
            }
            mLock.notifyAll();
        }
    }

其中的關(guān)鍵方法是下面這行:

map = XmlUtils.readMapXml(str);

也就是說(shuō)年碘,在SharedPreferenceImpl的構(gòu)造函數(shù)中,啟動(dòng)了一個(gè)線程展鸡,異步把外存上的數(shù)據(jù)(其實(shí)就是一個(gè)xml文件屿衅,每一行是一個(gè)key-value鍵值對(duì))讀入內(nèi)存中,并以map的形式保存起來(lái)莹弊。
那么將數(shù)據(jù)寫(xiě)回外存呢涤久?SP提供了兩個(gè)方法供開(kāi)發(fā)者調(diào)用,分別是
Editor#apply()Editor#commit箱硕,區(qū)別是拴竹,apply是異步的,而commit則是同步保存數(shù)據(jù)剧罩,在UI線程中調(diào)用,會(huì)阻塞主線程座泳。
看一下commit方法做了些什么:

        public boolean commit() {
            long startTime = 0;

            if (DEBUG) {
                startTime = System.currentTimeMillis();
            }
            // 提交到內(nèi)存中
            MemoryCommitResult mcr = commitToMemory();
            // 將內(nèi)存數(shù)據(jù)寫(xiě)到外存上
            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;
        }

關(guān)鍵方法commitToMemory()enqueueDiskWrite(MemoryCommitResult, Runnable postWriteRunnable)惠昔。前者把提交到editor中的新鍵值對(duì)提交到內(nèi)存中(即前面提到的map中)幕与。后者把map中的數(shù)據(jù)保存到xml文件中。

DB與SP的矛盾

說(shuō)到這里镇防,SP的消失之謎大概也有答案了:其實(shí)就是在DB模式中啦鸣,由于沒(méi)有訪問(wèn)憑據(jù)保護(hù)存儲(chǔ)區(qū)的權(quán)限,因此無(wú)法將外存中的數(shù)據(jù)讀取到內(nèi)存中来氧。在用戶解除DB模式后诫给,由于緩存了SP的實(shí)例,因此內(nèi)存中的空白數(shù)據(jù)覆蓋了xml文件啦扬,導(dǎo)致所有的鍵值對(duì)都消失中狂。
這里順帶附上Context#getSharedPreference(File file, int mode)的代碼,看下Android源碼中是怎么對(duì)SP對(duì)象進(jìn)行緩存的:

    @Override
    public SharedPreferences getSharedPreferences(File file, int mode) {
        checkMode(mode);
        if (getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.O) {
            if (isCredentialProtectedStorage()
                    && !getSystemService(StorageManager.class).isUserKeyUnlocked(
                            UserHandle.myUserId())
                    && !isBuggy()) {
                throw new IllegalStateException("SharedPreferences in credential encrypted "
                        + "storage are not available until after user is unlocked");
            }
        }
        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;
    }

    private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
        if (sSharedPrefsCache == null) {
            sSharedPrefsCache = new ArrayMap<>();
        }

        final String packageName = getPackageName();
        // 這里的sSharedPrefsCache緩存了sp的實(shí)例
        ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName); 
        if (packagePrefs == null) {
            packagePrefs = new ArrayMap<>();
            sSharedPrefsCache.put(packageName, packagePrefs);
        }

        return packagePrefs;
    }
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末扑毡,一起剝皮案震驚了整個(gè)濱河市胃榕,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌瞄摊,老刑警劉巖勋又,帶你破解...
    沈念sama閱讀 216,843評(píng)論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異换帜,居然都是意外死亡楔壤,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,538評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)惯驼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)蹲嚣,“玉大人,你說(shuō)我怎么就攤上這事跳座《祟酰” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,187評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵疲眷,是天一觀的道長(zhǎng)禾蚕。 經(jīng)常有香客問(wèn)我,道長(zhǎng)狂丝,這世上最難降的妖魔是什么换淆? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,264評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮几颜,結(jié)果婚禮上倍试,老公的妹妹穿的比我還像新娘。我一直安慰自己蛋哭,他們只是感情好县习,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,289評(píng)論 6 390
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般躁愿。 火紅的嫁衣襯著肌膚如雪叛本。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,231評(píng)論 1 299
  • 那天彤钟,我揣著相機(jī)與錄音来候,去河邊找鬼。 笑死逸雹,一個(gè)胖子當(dāng)著我的面吹牛营搅,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播梆砸,決...
    沈念sama閱讀 40,116評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼转质,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了辫樱?” 一聲冷哼從身側(cè)響起峭拘,我...
    開(kāi)封第一講書(shū)人閱讀 38,945評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎狮暑,沒(méi)想到半個(gè)月后鸡挠,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,367評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡搬男,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,581評(píng)論 2 333
  • 正文 我和宋清朗相戀三年拣展,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片缔逛。...
    茶點(diǎn)故事閱讀 39,754評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡备埃,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出褐奴,到底是詐尸還是另有隱情按脚,我是刑警寧澤,帶...
    沈念sama閱讀 35,458評(píng)論 5 344
  • 正文 年R本政府宣布敦冬,位于F島的核電站辅搬,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏脖旱。R本人自食惡果不足惜堪遂,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,068評(píng)論 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望萌庆。 院中可真熱鬧溶褪,春花似錦、人聲如沸践险。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,692評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至于游,卻和暖如春毁葱,著一層夾襖步出監(jiān)牢的瞬間垫言,已是汗流浹背贰剥。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,842評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留筷频,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,797評(píng)論 2 369
  • 正文 我出身青樓凛捏,卻偏偏與公主長(zhǎng)得像担忧,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子坯癣,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,654評(píng)論 2 354

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