记一次ANR问题的解决

  性能优化

Posted by MrCodeSniper on June 1, 2020

问题描述

界面加载过程中 出现ANR和明显的触摸卡顿

应用介绍

当前开发的应用是一个高度混合程度的应用(也就是Hybird App)

原生主要是作为一个壳容器来支撑H5应用的运行

所以经常在应用中使用的界面基本上架构都是 Activity or Fragment with WebView的形式

我们对WebView进行了高度自定义的扩展 包括JSAPI定制和白名单鉴权 容器全局代理定制 和底层功能的集成(移动网关,数据同步,配置中心)

复现场景和尝试分析思考

问题发生的地方正是通用的架构页面 但是只发生这个界面 进入界面可以很明显的发现ANR 之前的时序

页面加载->接口请求中->Input事件产生->ANR 而且这个过程是偶现的 只出现在进入应用的第一次 后续的N次都不会再出现

第一次看到这个问题 我的疑惑在于这是一个通用容器页面 如果容器出了问题 那其他的地方也应该会出现同样的问题

但是经过测试反馈 问题只发生在这个界面 某个步骤阻塞了主线程

我的第一反应是前端代码的问题 可能是这个界面的前端代码导致 但是前端的代码怎么会影响主线程 容器是运行在独立进程的

又让我的思绪蒙上一层阴影

既然没有头绪 我们不妨来看看ANR日志 看看有没有线索

日志生成

对于Crash日志 ANR日志 不管是Java 还是 Native 我都喜欢用一个开源框架来监控

它很大程度帮助我排查和解决问题

它就是爱奇艺出品的 XCrash

已经在非常多大量级的app中应用 稳定性有一定的保障

当然最大的优点在于生成的日志文件经过编排转换 清晰的排列出 堆栈 内存 CPU 线程信息 帮助我们排查问题

日志分析和解决

我们直接找到日志的核心部分 “main”主线程代码

"main" prio=5 tid=1 Waiting
  | group="main" sCount=1 dsCount=0 flags=1 obj=0x71584980 self=0xb400007669a1e010
  | sysTid=893 nice=-10 cgrp=default sched=0/0 handle=0x77f0e774f8
  | state=S schedstat=( 3635462262 83026468 4444 ) utm=309 stm=53 core=7 HZ=100
  | stack=0x7fdd2d8000-0x7fdd2da000 stackSize=8192KB
  | held mutexes=
  at sun.misc.Unsafe.park(Native method)
  - waiting on an unknown object
  at java.util.concurrent.locks.LockSupport.park(LockSupport.java:190)
  at java.util.concurrent.FutureTask.awaitDone(FutureTask.java:450)
  at java.util.concurrent.FutureTask.get(FutureTask.java:192)
  at com.alibaba.yihutong.common.utils.GlobalKt.updateDictionary(Global.kt:27)
  at com.alibaba.yihutong.common.h5plugin.dictionary.DictionaryDelegate.getValue(DictionaryDelegate.kt:15)
  at com.alibaba.yihutong.common.utils.GlobalKt.getDictionary(Global.kt:-1)
  at com.alibaba.yihutong.common.h5plugin.dictionary.DictionaryJsPlugin.abstractHandleEvent(DictionaryJsPlugin.kt:24)
  at com.alibaba.yihutong.common.h5plugin.BaseJsPlugin.handleEvent(BaseJsPlugin.kt:74)
  at com.alipay.mobile.nebulacore.manager.H5PluginManagerImpl.handleEvent(H5PluginManagerImpl.java:281)
  - locked <0x0262e156> (a com.alipay.mobile.nebulacore.manager.H5PluginManagerImpl)
  at com.alipay.mobile.nebulacore.manager.H5PluginManagerImpl.handleEvent(H5PluginManagerImpl.java:416)
  at com.alipay.mobile.nebulacore.core.H5CoreTarget.handleEvent(H5CoreTarget.java:133)
  at com.alipay.mobile.nebulacore.core.H5EventDispatcher.c(H5EventDispatcher.java:227)
  at com.alipay.mobile.nebulacore.core.H5EventDispatcher.b(H5EventDispatcher.java:113)
  at com.alipay.mobile.nebulacore.core.H5EventDispatcher.a(H5EventDispatcher.java:32)
  at com.alipay.mobile.nebulacore.core.H5EventDispatcher$1.run(H5EventDispatcher.java:84)
  at com.alipay.mobile.nebula.util.H5Utils.runOnMain(H5Utils.java:602)
  at com.alipay.mobile.nebulacore.core.H5EventDispatcher.dispatch(H5EventDispatcher.java:80)
  at com.alipay.mobile.nebulacore.core.H5EventDispatcher.dispatch(H5EventDispatcher.java:64)
  at com.alipay.mobile.nebulacore.bridge.H5BridgeImpl.b(H5BridgeImpl.java:307)
  at com.alipay.mobile.nebulacore.bridge.H5BridgeImpl.a(H5BridgeImpl.java:44)
  at com.alipay.mobile.nebulacore.bridge.H5BridgeImpl$2.run(H5BridgeImpl.java:150)
  at com.alipay.mobile.nebula.util.H5Utils.runOnMain(H5Utils.java:602)
  at com.alipay.mobile.nebulacore.bridge.H5BridgeImpl.a(H5BridgeImpl.java:147)
  at com.alipay.mobile.nebulacore.bridge.H5BridgeImpl.sendToNative(H5BridgeImpl.java:119)
  at com.alipay.mobile.nebulacore.web.H5WebChromeClient$2.run(H5WebChromeClient.java:319)
  at android.os.Handler.handleCallback(Handler.java:938)
  at android.os.Handler.dispatchMessage(Handler.java:99)
  at android.os.Looper.loop(Looper.java:233)
  at android.app.ActivityThread.main(ActivityThread.java:8010)
  at java.lang.reflect.Method.invoke(Native method)
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:631)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:978)

从日志我们可以看到ANR发生在JSAPI的调用 导致主线程的 LockSupport.park 处于waiting状态 等待对象的释放 waiting on an unknown object

我们直接定位到 业务代码 DictionaryJsPlugin

class DictionaryJsPlugin : BaseJsPlugin() {
 override fun abstractHandleEvent(event: H5Event, context: H5BridgeContext?): Boolean {
        when (event?.action) {
            JS_DICTIONARY -> {
                val h5Response = H5Response<String?>(!dictionary.isNullOrEmpty(),"", dictionary)
                callbackToH5(context, h5Response)
                true
            }
        }
        return false
    }
}

JSAPI调用处为主线程

JSAPI使用到了dictionary的一个全局静态变量 使用了by特性

kotlin 中的委托模式依靠by关键字 这里采用属性委托

属性委托不需要实现任何接口但是要重写setValue 和 getValue 方法

var dictionary: String? by DictionaryDelegate()

class DictionaryDelegate {
    private var _dictionary: String? = null

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String?) {
        _dictionary = value
    }

    operator fun getValue(thisRef: Any?, property: KProperty<*>): String? {
        if (_dictionary == null) {
            updateDictionary()?.let {
                if (it.success) {
                    _dictionary = it.data
                } else {
                    _dictionary = null
                }
            }
        }
        return _dictionary
    }
}

/**
 * 更新字典数据
 */
fun updateDictionary(): ResultContainer<String>? = try {
    ThreadPoolManager.getInstance().submit {
        HttpClient.dictionaryService.getDictionaryData(GetDictionaryRequest())
    }.get()
} catch (e: Exception) {
    null
}

从上述代码中 我们能看到整个数据流程

在委托的get方法中 使用了线程池调用接口 程池调用接口获取数据 也就是表明 第一次get字典值时

会同步调用接口并获取值更新

从日志中我们发现 问题正是出现在

ThreadPoolManager.getInstance().submit {
        HttpClient.dictionaryService.getDictionaryData(GetDictionaryRequest())
    }.get()

也就是主线程中同步调用网络请求 当然会block主线程

我们只要对JSAPI调用处加上线程处理即可

小结

这个问题