SharedPreference引发ANR原理是什么

其他教程   发布日期:2023年06月29日   浏览次数:466

这篇文章主要介绍“SharedPreference引发ANR原理是什么”,在日常操作中,相信很多人在SharedPreference引发ANR原理是什么问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”SharedPreference引发ANR原理是什么”的疑惑有所帮助!接下来,请跟着小编一起来学习吧!

正文

日常开发中,使用过

  1. SharedPreference
的同学,肯定在监控平台上看到过和
  1. SharedPreference
相关的ANR,而且量应该不小。如果使用比较多或者经常用sp存一些大数据,如json等,相关的ANR经常能排到前10。下面就从源码的角度来看看,为什么
  1. SharedPreference
容易产生ANR。

  1. SharedPreference
的用法,相信做过Android开发的同学都会,所以这里就只简单介绍一下,不详细介绍了。
  1. // 初始化一个sp
  2. SharedPreferences sharedPreferences = context.getSharedPreferences("name_sp", MODE_PRIVATE);
  3. // 修改key的值,有两种方法:commit和apply
  4. sharedPreferences.edit().putBoolean("key_test", true).commit();
  5. sharedPreferences.edit().putBoolean("key_test", true).apply();
  6. // 读取一个key
  7. sharedPreferences.getBoolean("key_test", false);

SharedPreference问题

  1. SharedPreference
的相关方法,除了
  1. commit
外,一般的开发同学都会直接在主线程调用,认为这样不耗时。但其实,
  1. SharedPreference
的很多方法都是耗时的,直接在主线程调很可能会引起ANR的问题。另外,虽然
  1. apply
方法的调用不耗时,但是会引起生命周期相关的ANR问题。

下面就来从源码的角度,看一下可能引起ANR的问题所在。

getSharedPreference(String name, int mode)

  1. @Override
  2. public SharedPreferences getSharedPreferences(String name, int mode) {
  3. File file;
  4. // 与sp相关的操作,都使用ContextImpl的类锁
  5. synchronized (ContextImpl.class) {
  6. if (mSharedPrefsPaths == null) {
  7. mSharedPrefsPaths = new ArrayMap<>();
  8. }
  9. // mSharedPrefsPaths是内存缓存的文件路径
  10. file = mSharedPrefsPaths.get(name);
  11. if (file == null) {
  12. // 此处获取SharedPreferences的文件路径,可能存在耗时
  13. file = getSharedPreferencesPath(name);
  14. mSharedPrefsPaths.put(name, file);
  15. }
  16. }
  17. return getSharedPreferences(file, mode);
  18. }

下面看下获取文件路径的方法:

  1. getSharedPreferencesPath()
,这个方法可能存在耗时。
  1. public File getSharedPreferencesPath(String name) {
  2. // 创建一个sp的存储文件
  3. return makeFilename(getPreferencesDir(), name + ".xml");
  4. }

调用

  1. getPreferencesDir()
获取
  1. sharedPrefs
的根路径
  1. private File getPreferencesDir() {
  2. // 所有和文件有关的操作,都会使用mSync锁,可能出现与其他线程抢锁的耗时
  3. synchronized (mSync) {
  4. if (mPreferencesDir == null) {
  5. mPreferencesDir = new File(getDataDir(), "shared_prefs");
  6. }
  7. // 这个方法,如果目录不存在,会创建目录,可能存在耗时
  8. return ensurePrivateDirExists(mPreferencesDir);
  9. }
  10. }

  1. ensurePrivateDirExists()
:确保文件目录存在
  1. private static File ensurePrivateDirExists(File file, int mode, int gid, String xattr) {
  2. if (!file.exists()) {
  3. final String path = file.getAbsolutePath();
  4. try {
  5. // 创建文件夹,会耗时
  6. Os.mkdir(path, mode);
  7. Os.chmod(path, mode);
  8. } catch (ErrnoException e) {
  9. }
  10. return file;
  11. }

再来看看

  1. getSharedPreferences
生成
  1. SharedPreferenceImpl
对象的流程。
  1. public SharedPreferences getSharedPreferences(File file, int mode) {
  2. SharedPreferencesImpl sp;
  3. synchronized (ContextImpl.class) {
  4. // 获取cache,先从cache中获取SharedPreferenceImpl
  5. final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
  6. sp = cache.get(file);
  7. if (sp == null) {
  8. // 如果没有cache,则创建一个SharedPreferencesImpl,此处可能存在耗时
  9. sp = new SharedPreferencesImpl(file, mode);
  10. cache.put(file, sp);
  11. return sp;
  12. }
  13. }
  14. return sp;
  15. }

先来看下cache的原理

  1. private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
  2. // sSharedPrefsCache是一个静态变量,全局有效
  3. if (sSharedPrefsCache == null) {
  4. sSharedPrefsCache = new ArrayMap<>();
  5. }
  6. // key:包名,value: ArrayMap<File, SharedPreferencesImpl>
  7. final String packageName = getPackageName();
  8. ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
  9. if (packagePrefs == null) {
  10. packagePrefs = new ArrayMap<>();
  11. sSharedPrefsCache.put(packageName, packagePrefs);
  12. }
  13. return packagePrefs;
  14. }

再来看看

  1. SharedPreferenceImpl
的构造方法,看看
  1. SharedPreference
是怎么初始化的。
  1. SharedPreferencesImpl(File file, int mode) {
  2. mFile = file;
  3. mBackupFile = makeBackupFile(file);
  4. // 设置是否load到内存的标志位为false
  5. mLoaded = false;
  6. startLoadFromDisk();
  7. }

  1. startLoadFromDisk()
:开启一个子线程,将sp中的内容读取到内存中
  1. private void startLoadFromDisk() {
  2. // 改mLoaded标志位时,需要获取mLock锁
  3. synchronized (mLock) {
  4. // load之前先设置mLoaded标志位为false
  5. mLoaded = false;
  6. }
  7. // 开启一个线程,从文件中将sp中的内容读取到内存中
  8. new Thread("SharedPreferencesImpl-load") {
  9. public void run() {
  10. // 在子线程load
  11. loadFromDisk();
  12. }
  13. }.start();
  14. }

  1. loadFromDisk
:真正读取文件的地方
  1. private void loadFromDisk() {
  2. synchronized (mLock) {
  3. // 如果已经load过了,直接return,不需要再重新load
  4. if (mLoaded) {
  5. return;
  6. }
  7. stat = Os.stat(mFile.getPath());
  8. if (mFile.canRead()) {
  9. BufferedInputStream str = null;
  10. try {
  11. str = new BufferedInputStream(
  12. new FileInputStream(mFile), 16 * 1024);
  13. // 读取xml的内容到map中
  14. map = (Map<String, Object>) XmlUtils.readMapXml(str);
  15. } catch (Exception e) {
  16. Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
  17. } finally {
  18. IoUtils.closeQuietly(str);
  19. }
  20. }
  21. synchronized (mLock) {
  22. // 设置mLoaded标志位为true,表示已经load完,通知所有在等待的线程
  23. mLoaded = true;
  24. mLock.notifyAll();
  25. }
  26. }

总结:经过上面的分析,

  1. getSharedPreferences
主要的卡顿点在于,获取
  1. PreferencesDir
的时候,可能存在目录尚未创建的情况。如果这个时候调用了创建目录的方法,就会非常耗时。

getBoolean(String key, boolean defValue)

这个方法和所有获取key的方法一样,都可能存在耗时。

  1. SharedPreferencesImpl
的构造方法,我们知道会开启一个新的线程,将内容从文件中读取到缓存的map里,这个步骤我们叫load。
  1. public boolean getBoolean(String key, boolean defValue) {
  2. synchronized (mLock) {
  3. // 需要等待,直到load成功
  4. awaitLoadedLocked();
  5. // 从缓存中取value
  6. Boolean v = (Boolean)mMap.get(key);
  7. return v != null ? v : defValue;
  8. }
  9. }

主要耗时的方法,在awaitLoadedLocked里。

  1. private void awaitLoadedLocked() {
  2. // 只有当mLoaded为true时,才能跳出死循环
  3. while (!mLoaded) {
  4. try {
  5. // 调用wait后,会释放mLock锁,并且进入等待池,等待load完之后的唤醒
  6. mLock.wait();
  7. } catch (InterruptedException unused) {
  8. }
  9. }
  10. }

这个方法,调用了

  1. mLock.wait()
,释放了
  1. mLock
的对象锁,并且进入等待池,直到load完被唤醒。

总结:所以,

  1. getBoolean
等获取
  1. key
的方法,会等待,直到sp的内容从文件中copy到缓存map里。很可能存在耗时。

commit()

  1. commit()
方法,会进行同步写,一定存在耗时,不能直接在主线程调用。
  1. public boolean commit() {
  2. // 开始排队写
  3. SharedPreferencesImpl.this.enqueueDiskWrite(
  4. mcr, null /* sync write on this thread okay */);
  5. try {
  6. // 等待同步写的结果
  7. mcr.writtenToDiskLatch.await();
  8. } catch (InterruptedException e) {
  9. return false;
  10. } finally {
  11. }
  12. notifyListeners(mcr);
  13. return mcr.writeToDiskResult;
  14. }

apply()

大家都知道

  1. apply
方法是异步写,但是也可能造成ANR的问题。下面我们来看
  1. apply
方法的源码。
  1. public void apply() {
  2. // 先将更新写入内存缓存
  3. final MemoryCommitResult mcr = commitToMemory();
  4. // 创建一个awaitCommit的runnable,加入到QueuedWork中
  5. final Runnable awaitCommit = new Runnable() {
  6. @Override
  7. public void run() {
  8. try {
  9. // 等待写入完成
  10. mcr.writtenToDiskLatch.await();
  11. } catch (InterruptedException ignored) {
  12. }
  13. }
  14. };
  15. // 将awaitCommit加入到QueuedWork中
  16. QueuedWork.addFinisher(awaitCommit);
  17. Runnable postWriteRunnable = new Runnable() {
  18. @Override
  19. public void run() {
  20. awaitCommit.run();
  21. QueuedWork.removeFinisher(awaitCommit);
  22. }
  23. };
  24. // 真正执行sp持久化操作,异步执行
  25. SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
  26. // 虽然还没写入文件,但是内存缓存已经更新了,而listener通常都持有相同的sharedPreference对象,所以可以使用内存缓存中的数据
  27. notifyListeners(mcr);
  28. }

可以看到这里确实是在子线程进行的写入操作,但是为什么说

  1. apply
也会引起ANR呢?

因为在

  1. Activity
  1. Service
的一些生命周期方法里,都会调用
  1. QueuedWork.waitToFinish()
方法,这个方法会等待所有子线程写入完成,才会继续进行。主线程等子线程,很容易产生ANR问题。
  1. public static void waitToFinish() {
  2. Runnable toFinish;
  3. //等待所有的任务执行完成
  4. while ((toFinish = sPendingWorkFinishers.poll()) != null) {
  5. toFinish.run();
  6. }
  7. }

Android 8.0 在这里做了一些优化,但还是需要等写入完成,无法完成解决ANR的问题。

以上就是SharedPreference引发ANR原理是什么的详细内容,更多关于SharedPreference引发ANR原理是什么的资料请关注九品源码其它相关文章!