美文网首页
DirectBoot与SharedPreference丢失之谜

DirectBoot与SharedPreference丢失之谜

作者: denny_z | 来源:发表于2018-03-27 16:33 被阅读0次

    DirectBoot模式是什么

    DirectBoot(简称DB)是Android N新引入的一个特性,本质上是对数据访问做了限制。在用户开机但未解锁之前,应用只能访问这个安全区内的数据,从而保护用户隐私安全。
    Android N上把数据分成了两块,分别是:

    • 凭据保护存储区(credential-protected),这是所有应用的默认存储位置,仅在用户解锁设备后可用。
    • 设备保护存储区(device-protected),这是一个新的存储位置,当设备启动后(包括DB阶段)随时都可访问该位置。

    SharedPreference简述

    SharedPreference(简称SP)是Android原生提供的一种数据持久化方式,因为其API友好而收到开发者的青睐。
    先来看下SP是怎么存储数据的吧。
    SP的实现在SharedPreferenceImpl中,直接在Android Studio中无法查找到这个类,可以进入目录/Android/sdk/source/android-xx/android/app中找到这个类。
    SP的getInt方法:

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

    可以看到,所谓的SharedPreference其实就是维护了一个map,所有数据的存储和读取都通过操作这个map来实现。而且,这个map是常驻内存的。这就带来了一个问题:内存泄漏!当存储的数目过多或者其中一个kay-value键值对过大的时候,就很可能造成OOM!
    那么这个map是怎么来的呢?看看下面这个方法

       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();
            }
        }
    

    其中的关键方法是下面这行:

    map = XmlUtils.readMapXml(str);
    

    也就是说,在SharedPreferenceImpl的构造函数中,启动了一个线程,异步把外存上的数据(其实就是一个xml文件,每一行是一个key-value键值对)读入内存中,并以map的形式保存起来。
    那么将数据写回外存呢?SP提供了两个方法供开发者调用,分别是
    Editor#apply()Editor#commit,区别是,apply是异步的,而commit则是同步保存数据,在UI线程中调用,会阻塞主线程。
    看一下commit方法做了些什么:

            public boolean commit() {
                long startTime = 0;
    
                if (DEBUG) {
                    startTime = System.currentTimeMillis();
                }
                // 提交到内存中
                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;
            }
    

    关键方法commitToMemory()enqueueDiskWrite(MemoryCommitResult, Runnable postWriteRunnable)。前者把提交到editor中的新键值对提交到内存中(即前面提到的map中)。后者把map中的数据保存到xml文件中。

    DB与SP的矛盾

    说到这里,SP的消失之谜大概也有答案了:其实就是在DB模式中,由于没有访问凭据保护存储区的权限,因此无法将外存中的数据读取到内存中。在用户解除DB模式后,由于缓存了SP的实例,因此内存中的空白数据覆盖了xml文件,导致所有的键值对都消失。
    这里顺带附上Context#getSharedPreference(File file, int mode)的代码,看下Android源码中是怎么对SP对象进行缓存的:

        @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的实例
            ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName); 
            if (packagePrefs == null) {
                packagePrefs = new ArrayMap<>();
                sSharedPrefsCache.put(packageName, packagePrefs);
            }
    
            return packagePrefs;
        }
    

    相关文章

      网友评论

          本文标题:DirectBoot与SharedPreference丢失之谜

          本文链接:https://www.haomeiwen.com/subject/onqdcftx.html