前言
最近负责移动端基础技术的工作,其中crash治理碰到几个奇怪的bug再此记录。
治理背景
1.crash率超过千分之一
2.非业务因素的灰度失败率居高不下。
一个奇怪的native异常
1.摘要:TrichromeLibrary.apk 00 pc 0194c6aa /product/app/TrichromeLibrary/TrichromeLibrary.apk
2.恶果:直接导致了灰度期间的crash超标失败
class XXXFragment {
// bug在这里
override fun onPause() {
super.onPause()
webView.onResume()
}
}
奇怪的空指针1
1.摘要:java.lang.NullPointerException Attempt to invoke virtual method ‘int android.view.View.getVisibility()’ on a null object reference at android.widget.FrameLayout.layoutChildren(FrameLayout.java:275)
2.恶果:直接导致了灰度期间的crash超标失败,无业务线认领
class XXXManager {
// bug在这里
private fun hideDelay() {
coroutine.launch {
delay(DISAPPEAR_AFTER_MS)
if(contentView != null){
parentView?.removeView(contentView)
}
}
}
}
奇怪的空指针2
1.摘要:java.lang.NullPointerException Attempt to read from field ‘int android.graphics.Rect.left’ on a null object reference at io.flutter.view.AccessibilityBridge.createAccessibilityNodeInfo(Unknown Source:535)
2.恶果:直接导致了灰度期间的crash超标失败,项目中flutter大量使用,长期存在导致crash率居高不下。
import com.idlefish.flutterboost.XFlutterView
class X FlutterView {
// bug在这里
//无障碍模式导致,直接返回null即可
@Override
@Nullable
public AccessibilityNodeProvider getAccessibilityNodeProvider() {
if (accessibilityBridge != null && accessibilityBridge.isAccessibilityEnabled()) {
return accessibilityBridge;
} else {
// TODO(goderbauer): when a11y is off this should return a one-off snapshot of
// the a11y
// tree.
return null;
}
}
}
自建热修复,patch包过大极易修复失败
1.背景:基于tinker自研热修复(前后端一体集成到CI/Cd),自测阶段下发合并几乎都是失败的
2.导致结果:排查发现,即使极小的改动;两次apk做diff生成的patch包将近20M。
原因是自研APM对大部分方法做了插桩,对每个方法都会生成一个方法Id;
方法Id的生成是异步随机的
private class TraceMethodAdapter extends AdviceAdapter{
...
protected void onMethodEnter(){
TraceMethod traceMethod = collectedMethodMap.get(methodName);
if (traceMethod != null) {
traceMethodCount.incrementAndGet();
mv.visitLdcInsn(traceMethod.id);
mv.visitMethodInsn(INVOKESTATIC, TraceBuildConstants.MATRIX_TRACE_CLASS, "i", "(I)V", false);
// 插入systrace的调用
String sectionName = methodName;
int length = sectionName.length();
if (length > TraceBuildConstants.MAX_SECTION_NAME_LEN) {
// 先去掉参数
int parmIndex = sectionName.indexOf('(');
sectionName = sectionName.substring(0, parmIndex);
// 如果依然更大,直接裁剪
length = sectionName.length();
if (length > TraceBuildConstants.MAX_SECTION_NAME_LEN) {
sectionName = sectionName.substring(length - TraceBuildConstants.MAX_SECTION_NAME_LEN);
}
}
mv.visitLdcInsn(sectionName);
mv.visitMethodInsn(INVOKESTATIC, TraceBuildConstants.MATRIX_TRACE_SYSTRACE_CLASS, "i", "(Ljava/lang/String;)V", false);
}
}
}
...
}
解决思路是把上次的trace方法的给记录生成一份mapping文件,做成可配置
public class MethodTracer {
public MethodTracer(ExecutorService executor, MappingCollector mappingCollector, Configuration config, ConcurrentHashMap<String, TraceMethod> collectedMap, ConcurrentHashMap<String, String> collectedClassExtendMap) {
this.configuration = config;
this.mappingCollector = mappingCollector;
this.executor = executor;
this.collectedClassExtendMap = collectedClassExtendMap;
this.collectedMethodMap = collectedMap;
}
private MappingCollector mappingCollector;
private class TraceClassAdapter extends ClassVisitor {
...
private boolean isNeedTrace;
// bug在这里无障碍模式导致,直接返回null即可
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
this.className = name;
this.isActivityOrSubClass = isActivityOrSubClass(className, collectedClassExtendMap);
this.isNeedTrace = MethodCollector.isNeedTrace(configuration, className, mappingCollector);
if ((access & Opcodes.ACC_ABSTRACT) > 0 || (access & Opcodes.ACC_INTERFACE) > 0) {
this.isABSClass = true;
}
}
@Override
public void visitEnd() {
if (!hasWindowFocusMethod && isActivityOrSubClass && isNeedTrace) {
insertWindowFocusChangeMethod(cv, className);
}
super.visitEnd();
}
...
}
}
在build.gradle的配置代码
trace {
enable = true //if you don't want to use trace canary, set false
baseMethodMapFile = "${project.buildDir}/baseBakApk/methodMapping.txt"
blackListFile = "${project.projectDir}/matrixTrace/blackMethodList.txt"
}
自建热修复,线上稳定工程实践;tinker版本是1.9.14.9
1.背景:热修复上线后用了几次,总会新增一些奇奇怪怪的crash以及在修复crash的补丁中并未有效降低crash率
2.恶果:直接导致了业务线不敢使用基础技术的热修复,一直冷藏了将近一年时间。
原因探究
典型的crash表现如下,运行时HLSCTXArg1类没找到:
java.lang.NoClassDefFoundError: Invalid descriptor: ex
at com.hellobike.map.sctx.utils.HLSCTXUtils.getBaseHttpArg1$map_hl_sctx_release(Unknown Source:13)
at com.hellobike.map.sctx.driver.HLSCTXDriverPositionProcessor$uploadDriverPosition$1.invokeSuspend(Unknown Source:220)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(Unknown Source:14)
at kotlinx.coroutines.DispatchedTask.run(Unknown Source:88)
at android.os.Handler.handleCallback(Handler.java:900)
at android.os.Handler.dispatchMessage(Handler.java:103)
at android.os.Looper.loop(Looper.java:219)
at android.app.ActivityThread.main(ActivityThread.java:8668)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:513)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1109)
其实修复的是这个类:
com.xxx.OrderCreateData
补丁中HLSCTXArg1类的位置
源码分析,跟加固支持的嫌疑很大;源码可以看出,当版本系统版本高于Android7.0非加固包模式下, 才创建ClassLoader来规避7.0以后的混合编译模式带来的ClassTable的缓存。 Android 7.0开启混合编译请看 https://mp.weixin.qq.com/s?__biz=MzAwNDY1ODY2OQ==&mid=2649286341&idx=1&sn=054d595af6e824cbe4edd79427fc2706&scene=0
开启支持加固模式后,因为因为补丁的HLSCTXArg1类是不完整的:
- HLSCTXArg1和HLSCTXUtils等类已经被缓存,默认直接用缓存的类,补丁的代码根本没被加载; 补丁虽然应用成功了,但是没起作用。
- HLSCTXArg1未被缓存,无论HLSCTXUtils已经被缓存,app就可能会出现地址错乱而出现crash
- 查看打包命令,线上确实开启了支持加固。
buildConfig {
/**
* optional, default 'false'
* Whether tinker should treat the base apk as the one being protected by app
* protection tools.
* If this attribute is true, the generated patch package will contain a
* dex including all changed classes instead of any dexdiff patch-info files.
*/
isProtectedApp = buildWithProtectedApp()
}
def buildWithProtectedApp() {
return hasProperty("isProtectedApp") ? Boolean.parseBoolean(isProtectedApp) : false
}
混合编译知识点:
- Android N混合使用AOT(ahead-of-time)编译,解释和JIT三种运行时
-
时机: 2.1 install(应用安装)与first-boot(应用首次启动)使用的[interpret-only],即只verify(JIT); 代码解释执行即不编译任何的机器码,它的性能与Dalvik时完全一致 2.2 ab-ota(系统升级)与bg-dexopt(后台编译)使用的是[speed-profile],即AOT。 这也是N中混合编译的核心模式。
- 运行原理 apk启动时我们需要加载应用的oat文件以及可能存在的app image文件,
大致流程如下:
通过OpenDexFilesFromOat加载oat时,若app image存在,则通过调用OpenImageSpace函数加载;
在加载app image文件时,通过UpdateAppImageClassLoadersAndDexCaches函数, 将art文件中的dexcache中dex的所有class插入到ClassTable,同时将method更新到dexcache;
在类加载时,使用时ClassLinker::LookupClass会先从ClassTable中去查找,找不到时才会走到DefineClass中。
tinker的解决方式: 运行时替换PathClassLoader方案 事实上,App image中的class是插入到PathClassloader中的ClassTable中。 我们完全废弃掉PathClassloader,而采用一个新建Classloader来加载后续的所有类;
Application类是一定会通过PathClassloader加载,采用代理Application实现的方法; 即Application的所有实现都会被代理到其他类(TinkerApplication); Application类不会再被使用到,这种方式没有兼容性的问题,但是会带来一定的接入成本。
解决办法:
- 修改打包命令,热修复不支持加固。
- Tinker升级到1.9.14.18,热修复只支持Dex,关闭资源和so热修复,极简设计。
解bug的技巧
- 上帝喜欢笨人,心法上没有太大的技巧;坚信肯定是哪个地方代码写错了;
- 确认一个可疑模块,然后逐行逐行的排查。