很久以前就看过使用Android studio的Method Trace工具调查函数性能或者效率的问题,一直没有在解决日常问题过程中实战过。最近刚好遇到了一个关闭wifi开关时wifi设置页面卡顿的问题,所以就想着尝试一下。虽然解决UI卡顿一般都用systrace工具,但是method trace可以查看各个线程中某个函数执行情况,理论上也是可以用来定位UI卡顿问题的。
首先启动method trace功能,然后关闭wifi,待卡顿过后,停止trace。Android studio会自动打开抓取的内容。
虽然Eclipse通过ddms抓取的traceview也是可以查看函数调用情况,但是Adroid studio的method trace功能更强大,尤其是可以分线程查看,这就更方便我们查找问题了。由于UI卡顿肯定是主线程被block了,必然我们要看这段时间内主线程的执行情况。选中上图中的主线程,可以看到中间有个很长的长条,鼠标放上去之后,发现该函数执行了约1.5s(注意起始时间,与后面做对比时可以很方便确认多线程调用时函数执行先后顺序),那么可以确认,就是这个函数出的问题了。
定位到源码中的copyAndNotifyListeners函数,去掉其他逻辑后主逻辑如下
private void copyAndNotifyListeners(boolean notifyListeners) {
……1……
synchronized (mLock) {
……2……
}
……3……
}
直观的怀疑是执行完1处的逻辑之后,主线程要去拿锁,此时由于锁被其他线程占用,所以主线程要一直等待。但是这只是我们的怀疑,要找到证据才行。这又要体现出Method Trace工具的强大之处。图中的每个小方格都代表了函数的执行情况,包括函数名、函数执行时间以及各个函数之间的调用情况。原理上,如果逻辑2中有函数调用的话,必定能在图中体现出来。把图中卡顿地方前后位置放大之后,发现卡顿之前确实在调用1处的函数,而在卡顿之后恰好在调用2处的逻辑。这样,就为我们之前的假设找到了证据。
既然主线程要取的锁被占用,肯定是被其他线程占用着,具体是什么线程,可以通过图中左上角查看每个线程的调用情况来确认。果然,在某个线程中我们发现了与主线程类似的长条。查看函数调用堆栈的情况,确实在该函数中持有对应的锁,且该锁刚好是在主线程之前拿到的。这样卡顿的原因就找到了。进一步分析,该部分卡顿是由于子线程在持锁期间调用了WifiManager的接口,而这个接口在向WifiManager取数据时采用的是同步调用,而WifiManager最终是向WifiStateMachine中取数据,WifiStateMachine线程此时正在忙碌调用driver中的接口,无法及时处理这个请求,因此这就是应用层被卡住的原因。
虽然经过上面的分析,问题是WifiStateMachine引起的,但是一款优秀的应用,还是要首先检查自己能否兼容这种场景,自己的逻辑是否也存在问题。深入分析之后发现,子线程确实存在持锁范围过大的问题,可以进行优化,避免不必要的等待。
补充问题
在解决上面问题时,对android源码的@GuardedBy标记有些疑惑,这个标记有啥作用呢?我直观的以为这个可以当成一把锁来用,但这只是我的假设而已,肯定要验证一下才能踏实。查阅了一些资料,感觉还是云里雾里,所以还是自己做实验看看把。
写了上面的测试代码,编译之后得到.class文件,通过javap -c Test.class后查看编译后的代码
从这个结果可以看出,@GuardedBy标签并没有对编译后的结果产生影响,也就没有同步作用。事实上该标签和@Override标签的功能是一致的,更多的作用是一种标记,标记这个变量或者这个函数是需要同步的,同时编译器在编译时可以根据这些标签做lint检查之类的工作。在日常工作中,可以合理的使用这些标签以增强代码逻辑的可读性,同时也方便其他同学阅读此处逻辑时能更快的领会设计意图。