利用DDMS中的TraceView检测应用中的黑屏无响应问题

测试提的一个Bug:在app首次安装后,从欢迎页点击跳过,接着出现了黑屏并且一直没有响应.只能杀掉了进程,重新启动后正常.也就是说应用出现了假死的现象,这是个很严重的问题,赶紧打开IDE调试:

排查原因

应用第一次启动,欢迎页加载是没有问题.在跳过进入首页时出现了黑屏现象.那来看首页,首页在第一次启动时做了开启Service、第三方SDK初始化、请求权限处理授权以及三个网络请求.到底是哪个环节出了问题?看上去黑屏是因为应用长时间没有响应引起的,那就用自带的DDMS(Android Device Monitor)中的TraceView来检测一下耗时操作.

DDMS检测

常规操作,选中应用所在的进程,在点击按钮跳转首页时点击Start Method Profiling,准备结束分析时点击Stop Method Profiling.
第一次应用无响应大概1min后结束分析,生成了TraceView信息.观察不同方法的执行时间信息,直观一点,就看Real Time吧,代表了该方法的平均执行时间,包括切换、阻塞的时间.将Real Time按从高到低排序:
利用DDMS中的TraceView检测应用中的黑屏无响应问题_12.png

很明显,Daemon$FinalizerWatchdogDaemon中的方法出现了长达45s的耗时操作,说明确实是出现了ANR.再次缩短分析时间,这次是30s,再看执行时间信息:
利用DDMS中的TraceView检测应用中的黑屏无响应问题_2.png
感觉应该是找到了应用无响应的原因:com.android.okhttp.okio.AsyncTimeout类中的方法.再看563、564、565三条信息,最终找到了发生了耗时操作的子方法:

AsyncTimeout.class.wait(waitMillis, (int) waitNanos);

看到这也证实了,由于网络请求超时引发了AsynTimeout类中的方法,最终当前线程发生阻塞.AsyncTimeout是okio包下面的一个的类,它提供了异步的超时机制.它内部提供了一个叫Watchdog的线程,在等待超时的过程中调用上述方法进入休眠状态.

定位出现问题的地方

通过上面,可以定位出应该是网络请求的时候出现了问题.在应用首次启动进入首页时有三个网络请求.Charles抓包发现,三个请求都没有发出.那是在进行哪个请求的时候出问题了?从之前DDMS分析的时间信息看,三个方法(Parents+Children)的Incl Cpu Time、Cpu Time、Real Time都差不多.这时候又要请出Android Profiler了(之前有介绍过),当然也可以挨个请求调试的笨办法,来找出到底是哪个请求导致的问题(方法多了怎么办?)

Tips:分析的时候如果勾选了Enable advanced profiling,分析结束后要取消,否则编译出的APK运行时会发生异常
准备调试时进入Android Profiler,在无响应现象出现前点击Record a method trace,等一段时间再次点击收集信息.这期间的所有方法按占用总时间的比例由高到低排列,比较直观的就看出,只有三个方法是属于工程内部的方法,其它的是系统方法和第三方库:
利用DDMS中的TraceView检测应用中的黑屏无响应问题_4.png

为了证实,将该请求相关代码注释掉,运行,正常.确实是这段请求代码的问题.点进去看看,里面是用Retrofit发起了请求,并创建了一个接口HttpApi,都是正常操作.而生成Retrofit实例的方法:

  public static Retrofit getPayInstance() {
        if (mPayRetrofit == null) {
            synchronized (Object.class) {
                if (mPayRetrofit == null) {
                    OkHttpClient client = MainApplication.getInstance().getClient();
                    mPayRetrofit = new Retrofit.Builder()
                            .baseUrl(ZHIFU_URL)
                            .client(client)
                            .addConverterFactory(GsonConverterFactory.create())
                            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                            .build();
                    return mPayRetrofit;
                }
            }
        }
        return mPayRetrofit;
    }

正是这段代码导致了应用ANR!

问题出现的原因

上述方法是一个很常见的单例模式,并且为了避免多线程重复创建而采用了加锁的操作.问题就出在 了锁对象: synchronized(Object.class)

当时写的时候也比较随意,没有考虑锁的特性.锁常用的无非是synchronized(this)、synchronized(class)和synchronized(Object).这里创建单例类是静态方法,因此使用的锁只能是后两种.而使用Object.class作为锁对象的问题就是,在当前进程内只有一个该对象,那么凡是以之为锁的代码都同步.也就是说,synchronized(*.class)代码块的作用和synchronized static方法的作用是一样的,该锁对类的所有对象实例起作用.

该段请求是跟在前两个请求之后的,应用里统一用OKHttp进行请求,而OKHttp在其内部封装的线程中做了很多操作,其中有一个类StreamAllocation,是用来建立执行HTTP请求所需网络设施的组件.这个类里面有很多同步代码块.Debug的时候可以看到执行到了这里:
利用DDMS中的TraceView检测应用中的黑屏无响应问题_5.png

其中的同步代码块用自定义的对象作为同步锁.这样就会导致:由于前面的网络请求和该单例创建相隔的时间很短,因为前者是异步的,因此StreamAllocation中需要锁对象时,这边的创建单例已经持有了这个全局锁,没有释放,导致线程阻塞.

解决方法

最简单的方法就是直接将该单例改为饿汉式,不在做同步的判断.再就是用创建单例的类作为锁对象,更精确的与之关联,就不会发生ANR了.

通过这次Bug,实现同步是要比较大的系统开销作为代价的,甚至造成死锁,尽量避免无谓的同步控制.如果一定要做,选择锁对象还是要慎重一些.

猜你喜欢

转载自blog.csdn.net/wzhseu/article/details/81317854