一些奇奇怪怪的bug

谈解bug技巧

Posted by River on December 28, 2021

前言

最近负责移动端基础技术的工作,其中crash治理碰到几个奇怪的bug再此记录。

治理背景

1.crash率超过千分之一
2.非业务因素的灰度失败率居高不下。 修修补补又一年

一个奇怪的native异常

1.摘要:TrichromeLibrary.apk 00 pc 0194c6aa /product/app/TrichromeLibrary/TrichromeLibrary.apk
2.恶果:直接导致了灰度期间的crash超标失败

nativecrash
nativecrash
nativecrash
nativecrash
nativecrash

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超标失败,无业务线认领

nativecrash
nativecrash
nativecrash
nativecrash

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率居高不下。

nativecrash

 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类的位置 nativecrash

源码分析,跟加固支持的嫌疑很大;源码可以看出,当版本系统版本高于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

nativecrash

开启支持加固模式后,因为因为补丁的HLSCTXArg1类是不完整的:

  1. HLSCTXArg1和HLSCTXUtils等类已经被缓存,默认直接用缓存的类,补丁的代码根本没被加载; 补丁虽然应用成功了,但是没起作用。
  2. HLSCTXArg1未被缓存,无论HLSCTXUtils已经被缓存,app就可能会出现地址错乱而出现crash
  3. 查看打包命令,线上确实开启了支持加固。
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
}

混合编译知识点:

  1. Android N混合使用AOT(ahead-of-time)编译,解释和JIT三种运行时
  2. 时机: 2.1 install(应用安装)与first-boot(应用首次启动)使用的[interpret-only],即只verify(JIT); 代码解释执行即不编译任何的机器码,它的性能与Dalvik时完全一致 2.2 ab-ota(系统升级)与bg-dexopt(后台编译)使用的是[speed-profile],即AOT。 这也是N中混合编译的核心模式。

  3. 运行原理 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类不会再被使用到,这种方式没有兼容性的问题,但是会带来一定的接入成本。

解决办法:

  1. 修改打包命令,热修复不支持加固。
  2. Tinker升级到1.9.14.18,热修复只支持Dex,关闭资源和so热修复,极简设计。

解bug的技巧

  1. 上帝喜欢笨人,心法上没有太大的技巧;坚信肯定是哪个地方代码写错了;
  2. 确认一个可疑模块,然后逐行逐行的排查。