DirectBoot与SharedPreference丢失之谜

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 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 getSharedPreferencesCacheLocked() {
        if (sSharedPrefsCache == null) {
            sSharedPrefsCache = new ArrayMap<>();
        }

        final String packageName = getPackageName();
        // 这里的sSharedPrefsCache缓存了sp的实例
        ArrayMap packagePrefs = sSharedPrefsCache.get(packageName); 
        if (packagePrefs == null) {
            packagePrefs = new ArrayMap<>();
            sSharedPrefsCache.put(packageName, packagePrefs);
        }

        return packagePrefs;
    }

你可能感兴趣的:(DirectBoot与SharedPreference丢失之谜)