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

我们经常使用的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();
    }
    
    .....
}

我们都知道Context的上下文实现是依靠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文件对象,

image.png

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

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

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

在我们的SharedPreferencesImpl构造函数里,会启动一个子线程去加载磁盘文件,把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离数据存储量很大的话,那这个磁盘加载并阻塞外部线程的时间会比较大 直接就导致了我们主线程获取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和apply都会执行到一个enqueueDiskWrite操作,这个操作会把当前修改sp内存数据同步到Disk磁盘的任务加入到ActivityThread里的一个任务链表集合中, 那么我们肯定会想这个磁盘同步任务什么时候才会最终完成呢,

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

--》handleStopActivity方法(ActivityThread)
--》QueuedWork.waitToFinish()
--》 processPendingWork(); 再到下面最终执行磁盘回写任务

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

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

安卓官方推荐我们可以考虑使用jetpack里的DataStore ,或者可以考虑使用腾讯团队开发的MMKV框架

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容