SharedPreferences使用优化

188 阅读3分钟

简述

SharedPreference(以下简称 sp)是Android系统一套轻量级的数据持久化方案,因为使用简单而受到开发者的喜爱。在项目实际开发中,因开发者滥用sp导致ANR问题。接下来分析一下产生ANR问题的原因,以及解决方案。

ANR产生的原因

1.SP第一次加载数据需要全量加载,当数据量大时可能会阻塞UI线程造成卡顿: sp文件创建后,会创建一个线程会加载解析对应的sp文件。但是当ui线程要访问sp中的内容时,如果此时sp文件还未完全加载解析到内存,此时UI线程会被block,直接sp文件加载到内存中为止。


SharedPreferencesImpl(File file, int mode) {
    mFile = file;
    mBackupFile = makeBackupFile(file);
    mMode = mode;
    mLoaded = false;
    mMap = null;
    mThrowable = null;
    startLoadFromDisk();
}

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

private void loadFromDisk() {
    synchronized (mLock) {
        //  sp未加载到内存时,不可用
        if (mLoaded) {
            return;
        }
        if (mBackupFile.exists()) {
            mFile.delete();
            mBackupFile.renameTo(mFile);
        }
    }
    // 省略。。。。
    }

sp在读或写时,都会走到 awaitLoadedLocke()逻辑 ,在mLock =false即sp未加载到内存,此时读写会block在 mLock锁上,直到loadFromDisk()执行完毕。


@GuardedBy("mLock")
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);
    }
}

@Override
public Map<String, ?> getAll() {
    synchronized (mLock) {
        awaitLoadedLocked();
        //noinspection unchecked
        return new HashMap<String, Object>(mMap);
    }
}

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

2.commit/apply操作可能造成ANR commit 是同步操作,会在主线程直接操作io,当写入操作比较耗时就会导致ui线程阻塞,进而产生ANR;apply虽然是异步提交,但是异步写入磁盘时,如果执行了Activity/Service/BroadcastRecieve中的Stop方法,那么一样会等待sp写入完毕,等待时间过长也会引起ANR。针对apply()展开看一下:

@Override
public void apply() {

    //  同步写入到内存
    final MemoryCommitResult mcr = commitToMemory();
    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);
            }
        };

    //  异步写入到磁盘 
    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
    notifyListeners(mcr);
}

构造一个名为awaitCommit的Runnable任务并将其加入到QueuedWork中,该任务内部直接调用了CountDownLatch.await()方法,即直接在UI线程执行等待操作,那么需要看QueuedWork中何时执行这个任务。

public static void waitToFinish() {
    try {
        while (true) {
            Runnable finisher;

            synchronized (sLock) {
                //  从队列中取出任务 
                finisher = sFinishers.poll();
            }

            //  如果任务为空,则跳出循环,UI线程可以继续往下执行 
            if (finisher == null) {
                break;
            }
            //  任务不为空,执行 CountDownLatch.await(),即UI线程会阻塞等待
            finisher.run();
        }
    } finally {
        sCanDelay = true;
    }

ActivityThread

private void handleStopActivity(IBinder token, boolean show, int configChanges, int seq) {  
  //......其他......  
  QueuedWork.waitToFinish();  
}  
  
private void handleStopService(IBinder token) {  
  //......其他......  
  QueuedWork.waitToFinish();  
}
省略了一些代码细节,可以看到在ActivityThread中handleStopActivity、handleStopService方法中都会调用waitToFinish()方法,即在Activity的onStop()中、Service的onStop()中都会先同步等待写入任务完成才会继续执行。

所以apply()虽然是异步写入磁盘,但是如果此时执行到Activity/Service的onStop(),依然可能会阻塞UI线程导致ANR。

解决方案

  1. 今日头条方案mp.weixin.qq.com/s/kfF83UmsG…
  2. 对sp进行迁移(MMKV或DataStore)
  3. 不在sp中存储非轻量级的数据
  4. 限制每个sp文件大小,将不相关的配置项保存在不同的文件中
  5. 预加载sp对象

其他

commit和apply异同:commit 提交后有结果返回,apply没有返回值。都会把数据同步提交到内存中,apply再异常提交到磁盘,commit是同步提交到磁盘。