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

23 篇文章 1 订阅
6 篇文章 0 订阅

我们经常使用的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文件对象,

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

由此我们也就可以知道,在我们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框架

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
SharedPreferences是一种轻量级的Android存储机制,用于存储应用程序的简单键值对数据,比如用户设置和应用程序状态。它可以用于保存和读取少量的数据,例如用户的偏好设置、应用程序的配置信息等。 以下是SharedPreferences使用的步骤: 1. 获取SharedPreferences对象 ``` SharedPreferences preferences = getSharedPreferences("my_preferences", MODE_PRIVATE); ``` 2. 获取SharedPreferences.Editor对象 ``` SharedPreferences.Editor editor = preferences.edit(); ``` 3. 存储数据 ``` editor.putString("username", "John"); editor.putInt("age", 30); editor.putBoolean("isMarried", true); editor.commit(); ``` 4. 读取数据 ``` String username = preferences.getString("username", ""); int age = preferences.getInt("age", 0); boolean isMarried = preferences.getBoolean("isMarried", false); ``` 其中,第一个参数是键名,第二个参数是默认值。如果SharedPreferences中不存在该键,则会返回默认值。 举例: 想要记录用户的登录状态,可以使用SharedPreferences来保存和读取用户的登录信息。在登录成功后,将用户的用户名和密码保存到SharedPreferences中;在下次启动应用程序时,从SharedPreferences中读取用户的登录信息,如果已经登录,则自动跳转到主界面。 存储用户信息: ``` SharedPreferences preferences = getSharedPreferences("user_info", MODE_PRIVATE); SharedPreferences.Editor editor = preferences.edit(); editor.putString("username", "John"); editor.putString("password", "123456"); editor.putBoolean("isLogin", true); editor.commit(); ``` 读取用户信息: ``` SharedPreferences preferences = getSharedPreferences("user_info", MODE_PRIVATE); String username = preferences.getString("username", ""); String password = preferences.getString("password", ""); boolean isLogin = preferences.getBoolean("isLogin", false); if (isLogin) { // 跳转到主界面 } else { // 显示登录界面 } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值