SharedPreferences学习

248 阅读5分钟

主要api

1. edit()
2. commit()
3. apply()
4. putXXX()
5. getXXX()

面对的问题:

1、SharedPreferences是如何保证线程安全的, 其内部的实现用到了哪些锁
2、进程不安全是否会导致数据丢失
3、数据丢失时, 其最终的屏障--文件备份机制是如何实现的
4、如何实现进程安全的SharedPreferences

SharedPreferences每个方法都加了synchronized保证了线程同步, 针对面试中提到SharedPreferences如何支持多进程? 最近插件化和组件化中都发现了ContentProvider的身影, 可以使用ContentProvider来辅助SharedPreferences实现多进程, SharedPreferences所在的进程提供一个ContentProvider, 其他进程通过该ContentProvider对SharedPreferences进行访问

1.SharedPreferences构造函数

SharedPreferencesImpl(File file, int mode) {
    mFile = file;
    // 1.创建备份文件
    mBackupFile = makeBackupFile(file);
    mMode = mode;
    mLoaded = false;
    mMap = null;
    mThrowable = null;
    // 2.将磁盘中的数据拷贝到内存中
    startLoadFromDisk();
}

关于这段代码主要关注makeBackupFile与startLoadFromDisk这两个方法, 这两个方法解决了一个异常情况下数据丢失的问题, 也引起另外一个问题, 当一个SP文件数据过大时, 存在耗时的情况.

1.1 问题一:异常情况数据丢失的问题
static File makeBackupFile(File prefsFile) {
    return new File(prefsFile.getPath() + ".bak");
}

针对当前SP文件, 创建一个与之对应的备份文件对象, 该对象在接下来的 startLoadFromDisk 中会用来解决异常情况下数据丢失的问题

private void startLoadFromDisk() {
    synchronized (mLock) {
    	mLoaded = false;
    }
    new Thread("SharedPreferencesImpl-load") {
        public void run() {
            // 数据拷贝
            loadFromDisk();
        }
    }.start();
}
1.2 解决数据异常丢失, 数据量过大两个问题
下面这段代码重点关注mBackupFile、map赋值、loadFromDisk结束时将mLoaded置为true三个地方, 其他的都是代码细节与代码架构设计

private void loadFromDisk() {
    synchronized (mLock) {
    	// 1.先判断该备份文件是否存在, 如果存在, 则直接将源文件删除掉, 将备份文件修改为源文件
    	if (mBackupFile.exists()) {
            mFile.delete();
            mBackupFile.renameTo(mFile);
        }
    }
    Map<String, Object> map = null;
    StructStat stat = null;
    Throwable thrown = null;
    try {
        stat = Os.stat(mFile.getPath());
        if (mFile.canRead()) {
            BufferedInputStream str = null;
            try {
            	// 2.数据: 磁盘 -> map
                //   读取磁盘中指定SP文件数据到内存(map)中
                // 现在做了限制, 数据大小限制为16M, 问题就出在这里, 如果数据量很大, 读取就非常耗时
                str = new BufferedInputStream(new FileInputStream(mFile), 16 * 1024);
                map = (Map<String, Object>) XmlUtils.readMapXml(str);
            } catch (Exception e) {
                Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
            } finally {
                IoUtils.closeQuietly(str);
            }
        }
    } catch (ErrnoException e) {
        // An errno exception means the stat failed. Treat as empty/non-existing by
        // ignoring.
    } catch (Throwable t) {
        thrown = t;
    }
    synchronized (mLock) {
        mLoaded = true;
        mThrowable = thrown;
        try {
            if (thrown == null) {
                if (map != null) {
                    mMap = map;
                    mStatTimestamp = stat.st_mtim;
                    mStatSize = stat.st_size;
                } else {
                    mMap = new HashMap<>();
                }
            }
        // In case of a thrown exception, we retain the old map. That allows
        // any open editors to commit and store updates.
        } catch (Throwable t) {
            mThrowable = t;
        } finally {
            mLock.notifyAll();
        }
    }
}

其他的都是代码细节, 在平时工作写代码时可以参考这几处的代码. 上面这段代码比较长, 带着问题看代码, 只关注三处:

  • 1、mBackupFile: 加载磁盘中数据之前, 先判断是否存在备份文件, 如果存在备份文件, 则删除源文件, 将备份文件修改为源文件. 解决了异常情况下数据丢失的问题, 至于该备份文件何时被写入数据, 下面分析到commit与apply时具体分析.
  • 2、map被赋值: 读取SP文件内容到map中(这里对SP文件大小做了限制16M), 当数据量过大时会比较耗时, 虽然这个读取操作是在子线程中, 但是它使用了一个标志位mLoaded, 如果进行读写操作时, 该变量为false, 则会将读写所在线程进行挂起, 这个在分析到edit时会涉及到.
  • 3、mLoaded: 具体细节这里先暂时不表, 只记住结论, 创建SP对象时将该变量置为false, 当SP数据全部读取到map之后再将该变量置为true, 在这个阶段中, 其他操作该SP文件的线程将会被挂起, 直到该阶段完成(如果该过程耗时比较长, 就可能导致主线程的ANR).

最后再思考一个问题, 如果是我自己做, 估计直接就做成了锁方法, 而这里锁的是代码块, 锁了代码块之后, 如果代码块执行完成之后, 多线程问题, 其他线程开始操作这个SP了会出现什么情况? 一个mLoaded标识位解决了这个问题, 如何解决?就在edit()方法中

2、edit方法

@Override
public Editor edit() {
    synchronized (mLock) {
        awaitLoadedLocked();
    }
    return new EditorImpl();
}

结合 loadFromDisk 方法, 只是锁了代码块, 并没有对整个方法加锁, 如果在磁盘数据还没结束, 其他线程拿到SP对象调用edit之后如何处理? 会不会出现并发读写的问题? 并不会

2.1 awaitLoadedLocked如何解决并发读写问题
private void awaitLoadedLocked() {
    if (!mLoaded) {
        // Raise an explicit StrictMode onReadFromDisk for this
        // thread, since the real read will be in a different
        // thread and otherwise ignored by StrictMode.
        BlockGuard.getThreadPolicy().onReadFromDisk();
    }
    while (!mLoaded) {
        try {
            mLock.wait();
        } catch (InterruptedException unused) {
        }
    }
    if (mThrowable != null) {
        throw new IllegalStateException(mThrowable);
    }
}

虽然 loadFromDisk 方法将数据从磁盘读取到内存只是对代码块进行了加锁, 但是通过标识位mLoaded将多线程串了起来, 其他线程再拿到SP对象之后, 调用edit时判断如果mLoaded为false, 表示数据初始化还未完成, 此时直接将当前线程挂起, 同时将锁释放, 如果数据初始化线程在loadFromDisk的第二个synchronized处被挂起, 此时就会重新开始与其他线程一起竞争锁, 其他线程 如果拿到锁, 又都会在edit这里挂起, 只有初始化数据的线程会进入到synchronized内部, 至此为何锁代码块不会出现问题, 通过一个mLoaded和适当的wait、notify解决.

3. putXXX与getXXX

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

public Editor putString(String key, @Nullable String value) {
    synchronized (mEditorLock) {
        mModified.put(key, value);
        return this;
    }
}

不同的锁对象与数据集将读写操作进行分离, 保证多线程情况下的读写操作安全, mModified数据何时被同步到map中? 继续向下分析commit与apply方法

4、commit 同步数据提交

public boolean commit() {
    long startTime = 0;
    MemoryCommitResult mcr = commitToMemory();
    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null);
    try {
        mcr.writtenToDiskLatch.await();
    } catch (InterruptedException e) {
        return false;
    } finally {
    }
    notifyListeners(mcr);
    return mcr.writeToDiskResult;
}

这套流程代码比较长, 以这个流程图为线进行分析, 在分析时带着以下几个问题:

1、写数据map如何同步给读数据map?

2、写数据时如果遇到异常情况了如何保证源文件不会损坏, 数据不会丢失

4.1 EditorImpl.commitToMemory

这段代码也是非常的长, 只关注两件事: 1.在某处加锁, 保证数据同步与数据读取时的并发问题; 2.将写数据集mModified同步到读数据集mMap中

private MemoryCommitResult commitToMemory() {
    long memoryStateGeneration;
    List<String> keysModified = null;
    Set<OnSharedPreferenceChangeListener> listeners = null;
    Map<String, Object> mapToWriteToDisk;
    // 1.加锁, 保证mMap同步
    synchronized (SharedPreferencesImpl.this.mLock) {
        if (mDiskWritesInFlight > 0) {
            mMap = new HashMap<String, Object>(mMap);
        }
        mapToWriteToDisk = mMap;
        mDiskWritesInFlight++;
        boolean hasListeners = mListeners.size() > 0;
        if (hasListeners) {
            keysModified = new ArrayList<String>();
            listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
        }
        // 2.加锁, 保证mModified同步问题
        synchronized (mEditorLock) {
            boolean changesMade = false;
            if (mClear) {
                if (!mapToWriteToDisk.isEmpty()) {
                    changesMade = true;
                    mapToWriteToDisk.clear();
                }
                mClear = false;
            }
            // 3.数据拷贝: 将mModified数据拷贝置mMap中
            for (Map.Entry<String, Object> e : mModified.entrySet()) {
                String k = e.getKey();
                Object v = e.getValue();
                if (v == this || v == null) {
                    if (!mapToWriteToDisk.containsKey(k)) {
                        continue;
                    }
                    mapToWriteToDisk.remove(k);
                } else {
                    if (mapToWriteToDisk.containsKey(k)) {
                        Object existingValue = mapToWriteToDisk.get(k);
                        if (existingValue != null && existingValue.equals(v)) {
                            continue;
                        }
                    }
                    mapToWriteToDisk.put(k, v);
                }
                changesMade = true;
                if (hasListeners) {
                    keysModified.add(k);
                }
            }
            mModified.clear();
            if (changesMade) {
                mCurrentMemoryStateGeneration++;
            }
            memoryStateGeneration = mCurrentMemoryStateGeneration;
        }
    }
    return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
                    mapToWriteToDisk);
}

写数据集同步到读数据集之后, 接下来就是将数据写入到磁盘中

4.2 SP.writeToFile

  这个代码不贴了, 代码太长, 针对commit方式提交, 没有太多需要关注的地方, 只关注writeToFile时首先会将mFile数据保持在mBackupFile中, 然后将内存数据写入到mFile中, 如果写入成功, 删除mBackupFile文件.

  mBackupFile的操作对应SP初始化中mBackupFile -> mFile的过程

4.3 总结

分析commit方法时, 发现并没有创建额外的线程来执行 内存mMap -> 磁盘mFile 这个操作, 而是直接在当前线程做了这个操作, 一般情况下如果操作SP.commit是在主线程中执行, 当数据量过大时, 必然会影响到主线程, 存在耗时的情况. 可能会引起主线程ANR.

针对这个问题, 如果把数据写入到磁盘的操作放在子线程中执行, 会不会解决这个问题?

5. Edit.apply

apply是将数据写入到磁盘放在子线程中执行, 这个操作会不会解决数据量过大时的ANR问题?

public void apply() {
    final long startTime = System.currentTimeMillis();
    // 1.mModified -> mMap
    final MemoryCommitResult mcr = commitToMemory();
    final Runnable awaitCommit = new Runnable() {
        @Override
        public void run() {
            // 4.设置栅栏, 数据写入完成之前走到这里的线程会被挂起
            mcr.writtenToDiskLatch.await();
        }
    };
    // 2.写数据之前将任务添加到QueueWork中
    QueuedWork.addFinisher(awaitCommit);

    Runnable postWriteRunnable = new Runnable() {
        @Override
        public void run() {
            awaitCommit.run();
            // 3.数据写完之后, 将该task从QueueWork中移除
            QueuedWork.removeFinisher(awaitCommit);
        }
    };
    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
    notifyListeners(mcr);
}

enqueueDiskWrite() {
    // writeToDiskRunnable在子线程中执行
    QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}

简述一下上面代码的流程:

1、数据从mModified同步到mMap中

2、子线程执行writeToFile将mMap数据写入到SP文件中

3、通过mcr.writtenToDiskLatch.await()开启栅栏, 将所有调用awaitCommit.run方法的线程全部挂起, 直到writeToFile执行完毕, mcr.writterToDiskLatch将计数置为0, 唤醒被挂起的线程

4、QueuedWork.addFinisher添加这个awaitCommit, 为apply处理大数据导致主线程ANR埋下伏笔

5.1 apply引起主线程ANR

关注apply方法, 其内部会将awaitCommit添加到QueuedWork中

public static void addFinisher(Runnable finisher) {
    synchronized (sLock) {
        sFinishers.add(finisher);
    }
}
5.2 Activity生命周期回调(onStop、onPause)
class ActivityThread:

public void handlePauseActivity(IBinder token, boolean finished, boolean userLeaving,
            int configChanges, PendingTransactionActions pendingActions, String reason) {
    ActivityClientRecord r = mActivities.get(token);
    if (r != null) {
        if (userLeaving) {
            performUserLeavingActivity(r);
        }
        r.activity.mConfigChangeFlags |= configChanges;
        performPauseActivity(r, finished, reason, pendingActions);
        // Make sure any pending writes are now committed.
        if (r.isPreHoneycomb()) {
            // 数据量过大时SP.apply引起ANR就是在这里被触发的
            QueuedWork.waitToFinish();
        }
        mSomeActivitiesChanged = true;
    }
}
5.3 QueuedWork.waitToFinish
public static void waitToFinish() {
    long startTime = System.currentTimeMillis();
    boolean hadMessages = false;
    Handler handler = getHandler();

    synchronized (sLock) {
        if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) {
            // Delayed work will be processed at processPendingWork() below
            handler.removeMessages(QueuedWorkHandler.MSG_RUN);
        }
        // We should not delay any work as this might delay the finishers
        sCanDelay = false;
    }
    StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
    try {
        processPendingWork();
    } finally {
            StrictMode.setThreadPolicy(oldPolicy);
    }
    try {
        while (true) {
            Runnable finisher;
            synchronized (sLock) {
                finisher = sFinishers.poll();
            }
            if (finisher == null) {
                break;
            }
            // 如果finisher指向SP中的awaitCommit, 那么这里进入到awaitCommit内部的mcr.writtenToDiskLatch.await();
            // 超过指定时间, 触发ANR
            finisher.run();
        }
    } finally {
        sCanDelay = true;
    }
    synchronized (sLock) {
        long waitTime = System.currentTimeMillis() - startTime;
        if (waitTime > 0 || hadMessages) {
            mWaitTimes.add(Long.valueOf(waitTime).intValue());
            mNumWaits++;
        }
    }
}

apply时触发主线程的ANR, 如何避免这个问题? 头条给出了方案, 并且在其APP上面已经全量应用 剖析 SharedPreference apply 引起的 ANR 问题