专栏名称: 鸿洋
你好,欢迎关注鸿洋的公众号,每天为您推送高质量文章,让你每天都能涨知识。点击历史消息,查看所有已推送的文章,喜欢可以置顶本公众号。此外,本公众号支持投稿,如果你有原创的文章,希望通过本公众号发布,欢迎投稿。
目录
相关文章推荐
鸿洋  ·  再学安卓 - init进程 ·  2 天前  
郭霖  ·  从源码到定制:全面解析 Android ... ·  5 天前  
郭霖  ·  HarmonyOS ... ·  1 周前  
郭霖  ·  这一次,让EventBus纯粹一些 ·  6 天前  
郭霖  ·  HarmonyOS ... ·  1 周前  
51好读  ›  专栏  ›  鸿洋

Android anr排查之sp卡顿

鸿洋  · 公众号  · android  · 2024-12-24 08:35

主要观点总结

文章主要讨论了在排查ANR问题时遇到的SharedPreference卡顿问题,通过解析apply流程来发现问题并提出解决方案。

关键观点总结

关键观点1: SharedPreference的apply流程导致主线程卡顿的问题

SharedPreference的apply方法在执行时会将任务提交到QueuedWork执行,主线程会在页面退出前阻塞等待sp完成,造成block等待甚至ANR。

关键观点2: 问题解决的正向思路

简化数据存储,如果数据复杂考虑存到数据库而不是一股脑往sp写。

关键观点3: 问题解决的篡改思路

通过反射篡改SharedPreference的某些字段,避免主线程阻塞等待,降低ANR风险。具体实现包括替换sFinishers字段保证每次获取都是空列表,替换sWork字段让每次执行的时候在子线程启动。


正文

今天分享一下之前在排查anr的时候遇到的一个卡顿问题。因为隔得时间有点久了,所以堆栈找不到了。只能记得这个卡顿的堆栈是长时间block在 QueuedWork.waitToFinish 的调用处,业务触发点则是SharedPreference 的 apply。

SharedPreference apply不是运行在子线程吗,为什么还会导致主线程卡顿?我们从apply的流程看起:
1
问题分析


sp在commit的时候会直接在当前线程执行commitToMemoryenqueueDiskWrite:

// SharedPreferenceImpl.java
@Override
public boolean commit() {
  long startTime = 0;
  MemoryCommitResult mcr = commitToMemory();
  SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null);
  try {
    mcr.writtenToDiskLatch.await();
  } catch (InterruptedException e) {
    return false;
  } finally {
  }
  notifyListeners(mcr);
  return mcr.writeToDiskResult;
}
apply逻辑如下:
@Override
public void apply() {
  final long startTime = System.currentTimeMillis();
  final MemoryCommitResult mcr = commitToMemory();
  final Runnable awaitCommit = new Runnable() {
    @Override
    public void run() {
      mcr.writtenToDiskLatch.await();
    }
  };
  QueuedWork.addFinisher(awaitCommit);
  Runnable postWriteRunnable = new Runnable() {
    @Override
    public void run() {
      awaitCommit.run();
      QueuedWork.removeFinisher(awaitCommit);
    }
  };
  SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
  notifyListeners(mcr);
}
这里有2个Runnable:
  1. 添加给QueueWork

public static void addFinisher(Runnable finisher{
 synchronized (sLock) {
   sFinishers.add(finisher);
 }
}
这里就是把Runnable存在一个List里,
  1. enqueueDiskWrite传入的Runnable

private void enqueueDiskWrite(final MemoryCommitResult mcr,final Runnable postWriteRunnable) {
  final boolean isFromSyncCommit = (postWriteRunnable == null);
  final Runnable writeToDiskRunnable = new Runnable() {
    @Override
    public void run() {
      synchronized (mWritingToDiskLock) {
        writeToFile(mcr, isFromSyncCommit);
      }
      synchronized (mLock) {
        mDiskWritesInFlight--;
      }
      if (postWriteRunnable != null) {
        postWriteRunnable.run();
      }
    }
  };
  if (isFromSyncCommit) {
    // 当前线程执行runable
    // ... ignore
    return
  }
  QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
可以看到写入本地文件的任务也是提交到QueuedWork执行的:
public static void queue(Runnable work, boolean shouldDelay) {
  Handler handler = getHandler();
  synchronized (sLock) {
    sWork.add(work);
    if (shouldDelay && sCanDelay) {
      handler.sendEmptyMessageDelayed(QueuedWorkHandler.`, DELAY);
    } else {
      handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
    }
  }
}
QueuedWork里面用swork保存任务,然后在HandlerThread里通过消息触发执行:
// QueuedWorkHandler
public void handleMessage(Message msg) {
  if (msg.what == MSG_RUN) {
    processPendingWork();
  }
}

private static void processPendingWork() {
  synchronized (sProcessingWork) {
    LinkedList work;

    synchronized (sLock) {
      work = sWork;
      sWork = new LinkedList<>();
      handlerRemoveMessages(QueuedWorkHandler.MSG_RUN);
    }

    if (work.size() > 0) {
      for (Runnable w : work) {
        w.run();
      }
    }
  }
}
在ActivityThread 里,处理Activity pause、stop的时候也会执行waitToFinish:
// ActivityThread
@Override
public void handleStopActivity(
  ActivityClientRecord r, 
  int configChanges,
  PendingTransactionActions pendingActions, 
  boolean finalStateRequest, 
  String reason
)
 
{
  // ...
  if (!r.isPreHoneycomb()) {
    QueuedWork.waitToFinish();
  }
  // ...
}
waitToFinish 会执行 sFinishers 里面的Runnable:
public static void waitToFinish() {
  // ...
  processPendingWork();
  while (true) {
    Runnable finisher;
    synchronized (sLock) {
      finisher = sFinishers.poll();
    }
    if (finisher == null) {
      break;
    }
    finisher.run();
  }
  // ...
}
我们把apply流程画一下:

这里能发现2个问题:

  • 主线程会在页面退出前阻塞等待sp完成,造成block等待,甚至造成anr。

  • 主线程退出之前会直接去主线程执行sp,主线程io,anr风险更大了。

sp如此设计的原因分析一下应该是:

  • 保证页面关闭前sp写入完成。

  • 页面关闭前拿到主线程来执行,提高优先级,能更大概率完成。
2
解决思路

正向思路

从流程分析我们可以知道,Activity stop阻塞太久导致anr,本质还是因为sp操作太慢了,大概率是你写入的内容太多。比较正向的思路是简化你的数据存储。如果比价复杂的缓存数据,可以考虑存到数据库,而不是一股脑往sp写。

篡改思路

现实情况下sp读写的地方比较多,本地存储配置、标记等也不是业务重点,花费大量时间去简化治理得不偿失,还容易引入新问题,所以可以考虑通过反射篡改一下,思路如下:

  • 我们可以不要阻塞等待完成的逻辑,讲道理其实也没有什么很强烈的需求说一定要等待保证页面结束之前sp写入完成。(修改sFinishers字段,保证每次获取都是空列表)

  • 主线程会调用processPendingWork,遍历执行sWork里的Runnable(修改sWork字段,让每次执行的时候在子线程启动)

如何修改?

1. sFinishers 替换成一个我们自己的 LinkedList,重写poll返回null:

class ProxyFinishList(private val finishs:LinkedList):LinkedList() {
  override fun poll(): Runnable? {
    return null
  }

  override fun add(element: Runnable)Boolean {
    return finishs.add(element)
  }

  override fun remove(): Runnable {
      return finishs.remove()
  }

  override fun isEmpty()Boolean {
      return true
  }
}
2. sWork是hide api,你需要找一个支持反射hide api的框架来支持一下。也替换为一个我们自己定义的LinkedList,这里有个问题,在android12之前,执行sWork的时候是clone一个新的:

在android12之后是直接赋值一个新对象:

所以hook策略上我们要区别一下,Android12以上在调用size的时候我们重新hook下,在调用size()的时候去触发启动任务:

class WorkProxyList(private val wrapper:LinkedList,private val handler:Handler,private val reHook:()->Unit):LinkedList() {
  override fun isEmpty()Boolean {
    return wrapper.isEmpty()
  }

  override fun add(element: Runnable)Boolean {
    return wrapper.add(WorkRunnableProxy(handler,element))
  }

  override fun remove(element: Runnable)Boolean {
    return wrapper.remove(element)
  }

  override val size: Int
    get() {
      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && Build.VERSION.SDK_INT         runWorks()
        reHook()
        return 0;
      } else {
        return wrapper.size
      }
    }

  override fun clone(): Any {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && Build.VERSION.SDK_INT       runWorks()
      return WorkProxyList(LinkedList(), handler, reHook)
    } else{
      return wrapper.clone()
    }
  }

  private fun runWorks() {
    if (wrapper.size==0) {
      return
    }
    val works:LinkedList = wrapper.clone() as LinkedList
    wrapper.clear()
    handler.post {
      works.forEach { it.run() }
    }
  }
}



最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!


扫一扫 关注我的公众号

如果你想要跟大家分享你的文章,欢迎投稿~


┏(^0^)┛明天见!