记录遇到的Android主线程IO问题

727 阅读2分钟

背景

事情是这样的,我们组的模块收到了测试提的bug,说是存在主线程IO问题,毕竟很多代码都是祖传老代码,大家都在忙需求,也没有精力关注,所以我就扛起了这个活(毕竟我可能是组里面的闲人。。。)

排查方式

遇到主线程IO问题,当然是需要找到哪些代码存在问题了,那么怎么去找呢?可以使用AndroidStrictMode,这是一个Android提供的用来检测程序中违例情况的开发者工具,最常用的场景就是检测主线程中本地磁盘和网络读写等耗时的操作。详细了解可阅读Android性能调优利器StrictMode

1.我在为了检测主线程IO问题,就在ApplicationonCreate()中加入

StrictMode.setThreadPolicy(
    ThreadPolicy // 线程检测策略
        .Builder()
        .detectAll() // 包含自定义的耗时调用、磁盘读取操作、磁盘写入操作、网络操作
        .penaltyLog()
        .build()
)

2.编译出来apk后,在logcat中过滤StrictMode关键字,运行软件,查看堆栈信息,如果有打印出代码中的堆栈,就表示代码中可能存在主线程IO的“坏味道”。

问题分类和心得

在代码中,发现以下几类:
1.调用的sdk接口内存在耗时操作,但是调用者并不知道,这就让我想起了View绘制和leakcanary的源码,它们的代码会判断当前的线程,如下:

// 摘自ViewRootImpl.java
@Override
public void requestLayout() {
  if (!mHandlingLayoutInLayoutRequest) {
    checkThread(); // 检查线程
    mLayoutRequested = true;
    scheduleTraversals();
  }
}
// 摘自leakcanary源码AppWatcher.kt
@JvmOverloads
fun manualInstall(
  application: Application,
  retainedDelayMillis: Long = TimeUnit.SECONDS.toMillis(5),
  watchersToInstall: List<InstallableWatcher> = appDefaultWatchers(application)
) {
  checkMainThread() // 检查线程
  ...

可以看出,当写出的代码对外提供时,如果当前的方法需要在自线程中执行,建议提供者在方法起始处检查线程,便于调用者尽早发现代码中的调用不当。 我给出我在代码中判断主线程的方法:

// 萌萌的老曹推荐
fun checkMainThread() {
  if (Looper.getMainLooper() == Looper.myLooper()) {
    throw AssertionError("IO operations are forbidden on the main thread")
  }
}

2.SharedPreferences的使用不当,主要分为两方面:

  • 频繁的getSharedPreferences
  • 使用commit()而不是apply()

针对这两种情况,可以做以下改善:

  • SharedPreferences是线程安全的,所以建议在Application启动的时候获取一次SharedPreferences,后面的代码中只管使用它就可以了。
  • 尽量使用apply(),而不是commit(),因为apply和commit都是内存同步,不同的是apply是异步写入磁盘,一般不影响SharedPreferences的get方法。

3.在主线程中进行数据库调用,这类问题比较麻烦,我在遇到时也比较头疼,因为之前的开发者的不当编程,导致了如网状的调用,所以还是强烈建议开发者在提供接口时,对于可以放在自线程的耗时操作,建议加上checkMainThread()判断。