我们经常使用的SharedPreferences其实是存在很多缺陷的,主要表现在

  • 占用内存
  • getValue时可能导致ANR
  • 不支持多进程
  • 不支持局部更新
  • commit或apply都可能导致ANR

以下参考安卓源码腾讯先游基础链表上,使用大白话和部分代码片段和大家一起探讨分享。

占用内存

final class SharedPreferencesImpl implements SharedPreferences {
    ......
        //构造方法
        SharedPreferencesImpl(File file, int mode) {
        mFile = file;
        mBackupFile = makeBackupFile(file);
        mMode = mode;
        mLoaded = false;
        mMap = null;
        mThrowable = null;
        //从磁盘里获取xml里的数据
        startLoadFromDisk();
    }
    .....
}

我们都知道Conte腾讯先游xt的上下文实现是approve依靠ContextImpl这个类,而我们的SharedPreferences的实现是依靠SharedPreferencesImpl类,

ContextImpl.java
    /**
     * Map from package name, to preference name, to cached preferences.
     */
    private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;

在我们的ContextImpl类中存在一个静态的ArrayMap对象缓存英文用于缓存不同packageName下的所有sp文件对象,

细数SharedPreferences的5大缺陷及ANR原因

但是在这个类里面我们可以看到缓链表不具有的特点是存数组的探空 初始化和赋值,但却没有对数组对象里的数据进行移除或者释放的操作,

由此我们也就可以知道源码编辑器,在我们APP运行的过程中,APP对应包目录下approve的sp文件都会被缓存到方法区里去, 而这种机制的话会导致很占内存,而且宁愿OOM也不会主动释放内存空间。

getValue的时候可能导致线程阻塞或ANR

在我们的SharedPrefe缓存视频怎样转入相册rencesImpl构造函数里,会启动一个子线程去加载磁腾讯先锋盘文件,把xml文件转换成map对象,如果文件很大或者线程调度没有马上启动这个线程的话,那么这个加载的操作需要一段时间后才能执行完成,

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

而假如我们刚好初始化的时候紧接着去getValue的话,getValue里面又会通过awaitLoadedLocked方法来校验是否要阻塞外部线程,

  private void awaitLoadedLocked() {
        if (!mLoaded) {
            BlockGuard.getThreadPolicy().onReadFromDisk();
        }
        while (!mLoaded) {
            try {
            //如果没有加载完成 就一直持有锁
                mLock.wait();
            } catch (InterruptedException unused) {
            }
        }
        if (mThrowable != null) {
            throw new IllegalStateException(mThrowable);
        }
    }

确保取值操作前一定是执行完成了file文件的加载和转换成功,最后在磁盘加载完成时才会notify操作 把腾讯体育我们外部读取value的线程给唤醒。

在上述的操作场景都是我们APP经常会出现的,同时当我们sp离数据存储量很大的话,那这个appear磁盘加载并阻塞外部线程的腾讯先游时间会比链表可以随机访问任意元素吗较大 直接就导致了我们主线腾讯先锋程获取sp值的时候直接就芭比Q anr了。

不支持多进程

名义上我们在获取sp实例的时候可以传参支持多进程模式,但这个mode参数也只是起到一个多进程数据同步的作用,

 static void setFilePermissionsFromMode(String name, int mode,
            int extraPermissions) {
        int perms = FileUtils.S_IRUSR|FileUtils.S_IWUSR
            |FileUtils.S_IRGRP|FileUtils.S_IWGRP
            |extraPermissions;
        if ((mode&MODE_WORLD_READABLE) != 0) {
            perms |= FileUtils.S_IROTH;
        }
        if ((mode&MODE_WORLD_WRITEABLE) != 0) {
            perms |= FileUtils.S_IWOTH;
        }
        FileUtils.setPermissions(name, perms, -1, -1);
    }

这里的同步是指访问这个sp实例的时候,会判断当前磁盘文件相对最后一次内存修改是否被改动过,如果是的话就重新加载磁盘文件再同步到缓存上,

  public static int setPermissions(String path, int mode, int uid, int gid) {
        try {
            Os.chmod(path, mode);
        } catch (ErrnoException e) {
            Slog.w(TAG, "Failed to chmod(" + path + "): " + e);
            return e.errno;
        }
        if (uid >= 0 || gid >= 0) {
            try {
                Os.chown(path, uid, gid);
            } catch (ErrnoException e) {
                Slog.w(TAG, "Failed to chown(" + path + "): " + e);
                return e.errno;
            }
        }
        return 0;
    }

但这种同步的作用不大,因为当多进程同时修改sp值,但不同进程里的内存数据也不会实时同步,而且腾讯地图同时修改sp数据也会导致数据丢失和覆盖的可能。

不支持局部更新

apply

public void apply() {
            final long startTime = System.currentTimeMillis();
            final MemoryCommitResult mcr = commitToMemory();
            //这个任务最终在ActivityThread里的 handleStopService  handlePauseActivity handleStopActivity方法里执行
            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);
                    }
                };
            // 最终调用QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
            //把这个任务加入到ActivityThread中的QueueWork列表里
            SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
            // changes reflected in memory.
            notifyListeners(mcr);
        }

我们的同步修改commit方法 和异步修改apply方法都是全量更新,也就是即使我们修改的止损一个键值对,它也会把数据重写写入到磁盘文件中,这样就会导致不必要的内存开销。

commit或apply都可能导致源码交易平台ANR

在commit和apply的时候还有一个更致命的问题就是他们也会导致ANR。 这个主要是因为在调用commit和链表c语言apply都会执行到一个enqueueDiskWriappstorete操作,这个操作会把当前修改sp内存数据同步到Disk磁盘的任务加入到ActivityThread里的一个任务链表集合中, 那么我们肯定会想这个磁盘同步任务腾讯先锋什么时候才会最终完成呢,

其实它是需要等到我们的应用中seappreciatervice在stop的时候,或者activity暂停或停止的时候,才会for循环上面提到的任务链表集缓存垃圾克星合任务,最终完成内存数据到磁盘数据的。缓存 那这样的话会因为有大量的读写同步到磁盘腾讯nba的任务导致a腾讯先游ctivity或者service切换生命周期的时候被阻塞住了,最终导致了ANR。

–》handleStopActivity方法(ActivityThr腾讯先游ead)

–》QueuedWork.waitToFinish() –》 processPendingWork();apple 再到下面最终执行磁盘回写任务appointment

for (Runnable w : work) {
                    w.run();
                }

综上,经过这些分析腾讯体育想必我们对SharedPreferences有个更了解的地方。

安卓官方推荐我们可以考虑使用jetpack里的DataStor腾讯e ,或者可以考虑源码交易平台使用腾讯团队开发的MMKV框架。