背景
事情是这样的,我们组的模块收到了测试提的bug,说是存在主线程IO问题,毕竟很多代码都是祖传老代码,大家都在忙需求,也没有精力关注,所以我就扛起了这个活(毕竟我可能是组里面的闲人。。。)
排查方式
遇到主线程IO问题,当然是需要找到哪些代码存在问题了,那么怎么去找呢?可以使用Android的StrictMode,这是一个Android提供的用来检测程序中违例情况的开发者工具,最常用的场景就是检测主线程中本地磁盘和网络读写等耗时的操作。详细了解可阅读Android性能调优利器StrictMode。
1.我在为了检测主线程IO问题,就在Application的onCreate()中加入
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()判断。