腾讯 Matrix 增量编译 bug 解决之路,PR 已通过

Posted gdutxiaoxu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了腾讯 Matrix 增量编译 bug 解决之路,PR 已通过相关的知识,希望对你有一定的参考价值。

本文首发我的微信公众号:徐公,想成为一名优秀的 android 开发者,需要一份完备的 知识体系,在这里,让我们一起成长,变得更好~。

前言

最近,我们项目在接入微信 Matrix,刚开始接入的时候,还蛮顺利的。到了下午,运行项目,偶现 crash。看了一下报错信息,某些 class 文件在 dex 文件中没有找到,即 ClassNotFoundException 。

clean 了一下,发现好了,就继续开发,跑了几次,发现突然又 crash 了,这时候我第一感觉怀疑是 matrix 导致的。

于是,我把 matrix trace 插件关了之后,本地全量编译,还有增量编译,发现都没有这个问题了,于是我可以确定,这肯定是引入 Matrix 带来的问题。

这时候,我就去 github 上面搜 issue,关键字是 ClassNotFoundException ,发现很多人都遇到这个问题,但是一直没有修复。

这时候怎么办呢?是偶现的,不是必现的。那当然要找出复现路径呢?于是,又折腾了半天多,终于发现了复现路径。在增量编译的情况下,修改某个 library moudle 一行代码,可以稳定复现。
于是,又上去上面搜了一波,关键字是增量编译

果不其然,也有挺多人遇到,而且官方也明确标记为 bug,这时候我是怎么解决的呢?

欲知下事如何,请看下文,哈哈,卖一下关子

现象

我们回到问题的本身,先描述一下现象,问题描述清楚真的很重要,尤其是在网上想别人请教的时候,你懂的

异常类型:编译异常& app crash
matrix版本:2.0.1
gradle版本:4.1.0
问题描述:第一次编译正常运行,第二次编译运行,会出现某些 class 找不到,报 ClassNotFoundException,出现问题之后需要 clean 项目,运行项目才正常

堆栈信息:

java.lang.NullPointerException
	at java.util.concurrent.ConcurrentHashMap.putVal(ConcurrentHashMap.java:1011)
	at java.util.concurrent.ConcurrentHashMap.put(ConcurrentHashMap.java:1006)
	at com.tencent.matrix.trace.MethodCollector$TraceClassAdapter.visit(MethodCollector.java:284)
	at org.objectweb.asm.ClassReader.accept(ClassReader.java:524)
	at org.objectweb.asm.ClassReader.accept(ClassReader.java:391)
	at com.tencent.matrix.trace.MethodCollector$CollectJarTask.run(MethodCollector.java:171)
	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
java.lang.NullPointerException
	at java.util.concurrent.ConcurrentHashMap.putVal(ConcurrentHashMap.java:1011)
	at java.util.concurrent.ConcurrentHashMap.put(ConcurrentHashMap.java:1006)
	at com.tencent.matrix.trace.MethodCollector$TraceClassAdapter.visit(MethodCollector.java:284)
	at org.objectweb.asm.ClassReader.accept(ClassReader.java:524)
	at org.objectweb.asm.ClassReader.accept(ClassReader.java:391)
	at com.tencent.matrix.trace.MethodCollector$CollectJarTask.run(MethodCollector.java:171)
	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
java.lang.NullPointerException
	at java.util.concurrent.ConcurrentHashMap.putVal(ConcurrentHashMap.java:1011)
	at java.util.concurrent.ConcurrentHashMap.put(ConcurrentHashMap.java:1006)
	at com.tencent.matrix.trace.MethodCollector$TraceClassAdapter.visit(MethodCollector.java:284)
	at org.objectweb.asm.ClassReader.accept(ClassReader.java:524)
	at org.objectweb.asm.ClassReader.accept(ClassReader.java:391)
	at com.tencent.matrix.trace.MethodCollector$CollectJarTask.run(MethodCollector.java:171)
	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
[I][MethodCollector] [saveIgnoreCollectedMethod] size:9626 path:D:\\githubRep\\gradleLearing\\app\\build\\outputs\\mapping\\debug\\ignoreMethodMapping.txt
[I][MethodCollector] [saveCollectedMethod] size:24989 incrementCount:24988 path:D:\\githubRep\\gradleLearing\\app\\build\\outputs\\mapping\\debug\\methodMapping.txt
[E][Matrix.MethodTracer] [innerTraceMethodFromJar] input:C:\\Users\\N21616\\.gradle\\caches\\transforms-2\\files-2.1\\48590e038f1555cf787fe85359f8a35d\\jetified-kotlin-stdlib-jdk7-1.5.20.jar output:D:\\githubRep\\gradleLearing\\app\\build\\intermediates\\transforms\\MatrixTraceTransform\\debug\\36.jar e:java.lang.UnsupportedOperationException: This feature requires ASM6
java.nio.file.FileSystemException: D:\\githubRep\\gradleLearing\\app\\build\\intermediates\\transforms\\MatrixTraceTransform\\debug\\36.jar: 另一个程序正在使用此文件,进程无法访问。

	at sun.nio.fs.WindowsException.translateToIOException(WindowsException.java:86)
	at sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:97)
	at sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:102)
	at sun.nio.fs.WindowsFileCopy.copy(WindowsFileCopy.java:165)
	at sun.nio.fs.WindowsFileSystemProvider.copy(WindowsFileSystemProvider.java:278)
	at java.nio.file.Files.copy(Files.java:1274)
	at com.tencent.matrix.trace.MethodTracer.innerTraceMethodFromJar(MethodTracer.java:204)
	at com.tencent.matrix.trace.MethodTracer.access$100(MethodTracer.java:60)
	at com.tencent.matrix.trace.MethodTracer$2.run(MethodTracer.java:108)
	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)

> Task :app:transformClassesWithMatrixTraceTransformForDebug
[I][Matrix.Trace] [doTransform] Step(1)[Parse]... cost:48ms
[I][Matrix.Trace] [doTransform] Step(2)[Collection]... cost:1264ms

[E][Matrix.MethodTracer] [innerTraceMethodFromJar] input:C:\\Users\\N21616\\.gradle\\caches\\transforms-2\\files-2.1\\bb37a7de696e1bea72b3b0dd87cdc726\\jetified-kotlin-stdlib-jdk8-1.5.20.jar output:D:\\githubRep\\gradleLearing\\app\\build\\intermediates\\transforms\\MatrixTraceTransform\\debug\\35.jar e:java.lang.UnsupportedOperationException: This feature requires ASM6
java.nio.file.FileSystemException: D:\\githubRep\\gradleLearing\\app\\build\\intermediates\\transforms\\MatrixTraceTransform\\debug\\35.jar: 另一个程序正在使用此文件,进程无法访问。

	at sun.nio.fs.WindowsException.translateToIOException(WindowsException.java:86)
	at sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:97)
	at sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:102)
	at sun.nio.fs.WindowsFileCopy.copy(WindowsFileCopy.java:165)
	at sun.nio.fs.WindowsFileSystemProvider.copy(WindowsFileSystemProvider.java:278)
	at java.nio.file.Files.copy(Files.java:1274)
	at com.tencent.matrix.trace.MethodTracer.innerTraceMethodFromJar(MethodTracer.java:204)
	at com.tencent.matrix.trace.MethodTracer.access$100(MethodTracer.java:60)
	at com.tencent.matrix.trace.MethodTracer$2.run(MethodTracer.java:108)
	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
[E][Matrix.MethodTracer] [innerTraceMethodFromJar] input:C:\\Users\\N21616\\.gradle\\caches\\transforms-2\\files-2.1\\32898900927cbb3ddb95f2fe14af33ec\\jetified-kotlin-stdlib-1.5.20.jar output:D:\\githubRep\\gradleLearing\\app\\build\\intermediates\\transforms\\MatrixTraceTransform\\debug\\37.jar e:java.lang.UnsupportedOperationException: This feature requires ASM6
java.nio.file.FileSystemException: D:\\githubRep\\gradleLearing\\app\\build\\intermediates\\transforms\\MatrixTraceTransform\\debug\\37.jar: 另一个程序正在使用此文件,进程无法访问。

	at sun.nio.fs.WindowsException.translateToIOException(WindowsException.java:86)
	at sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:97)
	at sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:102)
	at sun.nio.fs.WindowsFileCopy.copy(WindowsFileCopy.java:165)
	at sun.nio.fs.WindowsFileSystemProvider.copy(WindowsFileSystemProvider.java:278)
	at java.nio.file.Files.copy(Files.java:1274)
	at com.tencent.matrix.trace.MethodTracer.innerTraceMethodFromJar(MethodTracer.java:204)
	at com.tencent.matrix.trace.MethodTracer.access$100(MethodTracer.java:60)
	at com.tencent.matrix.trace.MethodTracer$2.run(MethodTracer.java:108)
	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
[E][Matrix.MethodTracer] [innerTraceMethodFromJar] input:D:\\githubRep\\gradleLearing\\mylibrary\\build\\intermediates\\runtime_library_classes_jar\\debug\\classes.jar output:D:\\githubRep\\gradleLearing\\app\\build\\intermediates\\transforms\\MatrixTraceTransform\\debug\\60.jar e:java.util.zip.ZipException: zip file is empty
java.util.zip.ZipException: zip file is empty
	at java.util.zip.ZipFile.open(Native Method)
	at java.util.zip.ZipFile.<init>(ZipFile.java:225)
	at java.util.zip.ZipFile.<init>(ZipFile.java:155)
	at java.util.zip.ZipFile.<init>(ZipFile.java:169)
	at com.tencent.matrix.trace.MethodTracer.innerTraceMethodFromJar(MethodTracer.java:186)
	at com.tencent.matrix.trace.MethodTracer.access$100(MethodTracer.java:61)
	at com.tencent.matrix.trace.MethodTracer$2.run(MethodTracer.java:113)
	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
[E][Matrix.MethodTracer] [innerTraceMethodFromJar] input:D:\\githubRep\\gradleLearing\\mylibrary\\build\\intermediates\\runtime_library_classes_jar\\debug\\classes.jar is empty
[E][Matrix.MethodTracer] Close stream err!
[E][Matrix.MethodTracer] [innerTraceMethodFromJar] input:C:\\Users\\N21616\\.gradle\\caches\\transforms-2\\files-2.1\\e378b9fe89a5fe15cf3fa9c9da712ef7\\jetified-kotlin-stdlib-jdk8-1.5.20.jar output:D:\\githubRep\\gradleLearing\\app\\build\\intermediates\\transforms\\MatrixTraceTransform\\debug\\35.jar e:java.lang.UnsupportedOperationException: This feature requires ASM6
[E][Matrix.MethodTracer] Close stream err!
[E][Matrix.MethodTracer] [innerTraceMethodFromJar] input:C:\\Users\\N21616\\.gradle\\caches\\transforms-2\\files-2.1\\ca30333b1699ed3075710b30785c2fac\\jetified-kotlin-stdlib-1.5.20.jar output:D:\\githubRep\\gradleLearing\\app\\build\\intermediates\\transforms\\MatrixTraceTransform\\debug\\37.jar e:java.lang.UnsupportedOperationException: This feature requires ASM6
[E][Matrix.MethodTracer] Close stream err!
> Task :app:transformClassesWithMatrixTraceTransformForDebug
[I][Matrix.Trace] [doTransform] Step(3)[Trace]... cost:2304ms
[I][Matrix.TraceTransform]  Insert matrix trace instrumentations cost time: 3671ms.

问题直接原因

就像文章开头说的,在本地搞了半天多, 才终于发现必现路径,增编编译,运行的时候,会直接 crash。

于是,我先去官方 issue 上面搜索,一搜,发现很多人都遇到,但是一直没有解决,官方标记为 bug,issue 链接 issue 592, 这里特别感谢他们提供的思路。

可以看到,很多人出现都是增编编译的时候出现问题,
于是,我在想,我先把增量编译关了,看行不行。

说干就干,于是我把 MatrixTraceTransform#isIncremental,MatrixTraceLegacyTransform##isIncremental 都返回 false,发现我们项目增量编译也 ok 了,不会 crash 了。

特意去看了一下编译耗时,在我们项目中,编译一次,transformClassesWithRealmTransformerForDebug,耗时大概是 20 - 30 ms 左右,增量编译在 10 - 15 ms,关闭 matrix transfrom 增量编译的话,大概慢 10 - 15 ms,貌似也可以接受。

菜逼的我留下了眼泪。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c4nPvoEz-1637060739950)(https://raw.githubusercontent.com/gdutxiaoxu/blog_image/master/21/08/20211104173950.png)]

问题探索

于是,我先去接入 matrix 相关功能了,但是这个增量编译的问题,一直在想着,到底是什么问题了?有时候吃饭都在想。

想着想着,我再次进入这个坑。gradlew installDebug --stacktrace ,查看编译 error 级别的信息,主要有四个地方,也是我重点怀疑的。

  • java.lang.NullPointerException 空指针问题
  • ASM 版本的问题,java.lang.UnsupportedOperationException: This feature requires ASM6
  • windows 文件 fd 占用问题,对应的提醒信息是 另一个程序正在使用此文件,进程无法访问。
  • zip file is empty 问题

第一次尝试,java.lang.NullPointerException 空指针问题?

看堆栈信息,很快定位到 com.tencent.matrix.trace.MethodCollector.TraceClassAdapter#visit,里面有这样一个逻辑

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;
            if ((access & Opcodes.ACC_ABSTRACT) > 0 || (access & Opcodes.ACC_INTERFACE) > 0) 
                this.isABSClass = true;
            
            collectedClassExtendMap.put(className, superName);
        

debug 发现当 className 是 META-INF/versions/9/module-info.class,superName 为 null,导致报错。因为 ConcurrentHashMap 是不允许 key 或者 value 为 null 的

于是我增加了判空逻辑,代码运行,App crash。初步排除这个原因。

module-info.class 这个 的 superName 为 null,这个很奇怪,按理来说,是不可能为 null 的,因为 java 默认都会继承 Object 。

那这个 module-info.class 到底是什么东东?搜了一下,发现 module-info.class 不是标准的 class。

module kotlin.stdlib.jdk8 
    requires transitive kotlin.stdlib;
    requires kotlin.stdlib.jdk7;

    exports kotlin.collections.jdk8;
    exports kotlin.streams.jdk8;
    exports kotlin.text.jdk8;

    opens kotlin.internal.jdk8 to kotlin.stdlib;

简单来讲,就是JDK9支持模块化,类似Dart语言的包组织,JS的export,这样可以管理或者重新组织一个新的包,而不是像JDK8以下一样,只能通过Java修饰符来控制访问权限;而这个module-info.class就是来管理和描述这个包的;

在JDK8及以下,module-info.class并不会起作用,只有在JDK9以上才会起作用;
可以看到这个class并不是一个正常的class,并不包含类或者方法,所以asm和javassist处理这个class时,就会解析报错;

具体的可以看一下这篇文章

Android Gradle Plugin处理module-info.class报错

第二次尝试,ASM 版本问题?

一开始,编译日志提醒说 requires ASM6,以为是 asm 版本的问题,本地更新了 asm 版本,结果还是会出现 crash。排除,应该不是这个原因。

第三次尝试,windows 文件 fd 占用问题

看堆栈信息,通过代码,可看到是在这里报错 com.tencent.matrix.trace.MethodTracer#innerTraceMethodFromJar

具体报错的原因是插桩的过程中发生 exception,这时候调用 Files.copy(input.toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING); 出错了,这个只会在 windows 上面出现,linux, mac 都不会。突然想说一句, mac 真香,没有 windows 这些乱七八糟的问题。

于是我在 catch exception 的时候,关闭一下 IO 流,代码如下

   private void innerTraceMethodFromJar(File input, File output) 
        ZipOutputStream zipOutputStream = null;
        ZipFile zipFile = null;
        try 
           // 省略若干代码
         catch (Exception e) 
            try 
                if (zipOutputStream != null) 
                    zipOutputStream.finish();
                    zipOutputStream.flush();
                    zipOutputStream.close();
                    zipOutputStream = null;
                
                if (zipFile != null) 
                    zipFile.close();
                    zipFile = null;
                
             catch (Exception e2) 
                Log.e(TAG, "close stream err!, e2 is "+ e2);
            
            Log.e(TAG, "[innerTraceMethodFromJar] input:%s output:%s e:%s", input, output, e);
            if (e instanceof ZipException) 
                e.printStackTrace();
            
            try 
                if (input.length() > 0) 
                    Files.copy(input.toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING);
                 else 
                    Log.e(TAG, "[innerTraceMethodFromJar] input:%s is empty", input);
                
             catch (Exception e1) 
                e1.printStackTrace();
            
         finally 
            try 
                if (zipOutputStream != null) 
                    zipOutputStream.finish();
                    zipOutputStream.flush();
                    zipOutputStream.close();
                
                if (zipFile != null) 
                    zipFile.close();
                
             catch (Exception e) 
                Log.e(TAG, "close stream err!");
            
        
    

重新运行,项目跑起来,启动 App,还是一如既往得出人意料, App 直接 crash, 我的天。

你以为我要放弃了嘛,不不,起来,我还能再战个十万回合。

第四次尝试,zip file is empty

通过堆栈信息,报错的地方大概在这里 com.tencent.matrix.trace.MethodTracer#innerTraceMethodFromJar,大概的意思就是 zip file is empty。
这里为了方便,下文统一把 D:\\githubRep\\gradleLearing\\mylibrary\\build\\intermediates\\runtime_library_classes_jar\\debug\\classes.jar 简称为 classes.jar

对于 transfrom 有一定了解的人,我们都知道 transfrom input 是依赖于上一个 transfrom 的 output 传递过来的,那有没有可能是上一个 transform 传递过来的时候出错。

于是,我去看了我们项目的 transform task,发现还真的存在其他 transfrom,那有没有可能是这个原因呢?(貌似有这个可能呢)

于是,我新建了一个 Demo,确保只有 matrix 的 transfrom,增量编译,启动。。。。。

可惜,还是黑屏,那么,到这里,可以确定的是,一定是 matrix transfrom 的问题。这再次加强了我去看 matrix trace plugin 代码的决心。

看到这里,我们可能有点乱了?

我们先来梳理一下,开启增量编译之后, ClassNotFound 的问题基本可以确定是 trace plugin 插件引起的,而 class.jar 大小 size 为 0,那么很有可能在处理 class.jar 的时候出错了

带着这个怀疑,我们来看他们的调用关系, MethodTracer#innerTraceMethodFromJar(File input, File output) 的 input jar size 为 0 ,梳理它的调用逻辑,如下

com.tencent.matrix.plugin.trace.MatrixTrace#doTransform
methodTracer.trace(dirInputOutMap, jarInputOutMap) // dirInputOutMap 这里传递过去的
com.tencent.matrix.trace.MethodTracer#trace
com.tencent.matrix.trace.MethodTracer#traceMethodFromJar
com.tencent.matrix.trace.MethodTracer#innerTraceMethodFromJar(File input, File output) 

这里,我们主要关注一下 MatrixTrace#doTransform 方法里面的 methodTracer.trace(dirInputOutMap, jarInputOutMap),因为 input 就是从这里传递过去的。

    fun doTransform(classInputs: Collection<File>,
                    changedFiles: Map<File, Status>,
                    inputToOutput: Map<File, File>,
                    isIncremental: Boolean,
                    traceClassDirectoryOutput: File,
                    legacyReplaceChangedFile: ((File, Map<File, Status>) -> Object)?,
                    legacyReplaceFile: ((File, File) -> (Object))?
    ) 

        // 省略若干代码

        /**
         * step 1
         */
        var start = System.currentTimeMillis()

        val futures = LinkedList<Future><*>>()

        val mappingCollector = MappingCollector()
        val methodId = AtomicInteger(0)
        val collectedMethodMap = ConcurrentHashMap<String, TraceMethod>()

        futures.add(executor.submit(ParseMappingTask(
                mappingCollector, collectedMethodMap, methodId, config)))

        // dirInputOutMap 在这里初始化
        val dirInputOutMap = ConcurrentHashMap<File, File>()
        val jarInputOutMap = ConcurrentHashMap<File, File>()

        for (file in classInputs) 
            if (file.isDirectory) 
                futures.add(executor.submit(CollectDirectoryInputTask(
                        directoryInput = file,
                        mapOfChangedFiles = changedFiles,
                        mapOfInputToOutput = inputToOutput,
                        isIncremental = isIncremental,
                        traceClassDirectoryOutput = traceClassDirectoryOutput,
                        legacyReplaceChangedFile = legacyReplaceChangedFile,
                        legacyReplaceFile = legacyReplaceFile,

                        // 第一个地方,可能修改 dirInputOutMap 的值
                        resultOfDirInputToOut = dirInputOutMap
                )))
             else 
                val status = Status.CHANGED
                futures.add(executor.submit(CollectJarInputTask(
                        inputJar = file,
                        inputJarStatus = status,
                        inputToOutput = inputToOutput,
                        isIncremental = isIncremental,
                        traceClassFileOutput = traceClassDirectoryOutput,
                        legacyReplaceFile = legacyReplaceFile,

                        // 第二个地方,可能修改 dirInputOutMap 的值
                        resultOfDirInputToOut = dirInputOutMap,
                        resultOfJarInputToOut = jarInputOutMap
                )))
            
        

        for (future in futures) 
            future.get()
        
        futures.clear()

        Log.i(TAG, "[doTransform] Step(1)[Parse]... cost:%sms", System.currentTimeMillis() - start)

        /**
         * step 2
         */
        start = System.currentTimeMillis()
        val methodCollector = MethodCollector(executor, mappingCollector, methodId, config, collectedMethodMap)

        methodCollector.collect(dirInputOutMap.keys, jarInputOutMap.keys)
        Log.i(TAG, "[doTransform] Step(2)[Collection]... cost:%sms", System.currentTimeMillis() - start)

        /**
         * step 3
         */
        start = System.currentTimeMillis()
        val methodTracer = MethodTracer(executor, mappingCollector, config, methodCollector.collectedMethodMap, methodCollector.collectedClassExtendMap)
        // 第三个地方,可能修改 dirInputOutMap 的值
        methodTracer.trace(dirInputOutMap, jarInputOutMap)
        Log.i(TAG, "[doTransform] Step(3)[Trace]... cost:%sms", System.currentTimeMillis() - start)

    

主要关注可能修改 dirInputOutMap 的地方,上面的代码已经标注出来了,可以看到,主要有三个地方可能修改。

于是,我加上断点,断点的地方分别在 step1, step2 ,step3 注释的地方,debug 了一下

  • step1 的时候 classes.jar 大小不为 0
  • step2 的时候 classes.jar 大小不为0
  • step3 的时候 classes.jar 大小不为 0

这里可能会有人有这样的疑问,为什么是看 D:\\githubRep\\gradleLearing\\mylibrary\\build\\intermediates\\runtime_library_classes_jar\\debug\\classes.jar 这个文件,因为我们报错的堆栈,是这个 class.jar 大小为 0.

既然这三个地方都不为 0,那么很有可能,是在 methodTracer.trace(dirInputOutMap, jarInputOutMap) 方法 中修改了。

public void trace(Map<File, File> srcFolderList, Map<File, File> dependencyJarList) throws ExecutionException, InterruptedException 
        List<Future> futures = new LinkedList<>();
        traceMethodFromSrc(srcFolderList, futures);
        traceMethodFromJar(dependencyJarList, futures);
        for (Future future : futures) 
            future.get();
        
        futures.clear();


private void traceMethodFromSrc(Map<File, File> srcMap, List<Future> futures) 
        if (null != srcMap) 
            for (Map.Entry<File, File> entry : srcMap.entrySet()) 
                futures.add(executor.submit(new Runnable() 
                    @Override
                    public void run() 
                        innerTraceMethodFromSrc(entry.getKey(), entry.getValue());
                    
                ));
            
        
    

trace 方法主要执行了两个逻辑

  • 执行 traceMethodFromSrc
  • 执行 traceMethodFromJar 方法

而我们的 dirInputOutMap 参数对应的 trace 方法的 srcFolderList 参数,于是,我们在 innerTraceMethodFromSrc 方法的开始和结束的地方,设置条件断点,条件是 input.path.equals("D:\\\\githubRep\\\\gradleLearing\\\\mylibrary\\\\build\\\\intermediates\\\\runtime_library_classes_jar\\\\debug\\\\classes.jar")

debug 发现,在刚开始调用 innerTraceMethodFromSrc 方法的时候(这个方法很重要,下文还会涉及到),我们的 classes.jar文件大小不为 0,可以等到方法执行完成的时候, classes.jar 文件大小为 0。
这时候基本可以确定了是 innerTraceMethodFromSrc 方法修改了 classes.jar,导致大小为 0.

innerTraceMethodFromSrc 方法,可以看到有两个地方操作了文件

  • FileUtil.copyFileUsingStream(classFile, changedFileOutput)
  • Files.copy(input.toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING)
private void innerTraceMethodFromSrc(File input, File output) 

        ArrayList<File> classFileList = new ArrayList<>();
        if (input.isDirectory()) 
            listClassFiles(classFileList, input);
         else 
            classFileList.add(input);
        

        for (File classFile : classFileList) 
            InputStream is = null;
            FileOutputStream os = null;
            try 
                final String changedFileInputFullPath = classFile.getAbsolutePath();
                final File changedFileOutput = new File(changedFileInputFullPath.replace(input.getAbsolutePath(), output.getAbsolutePath()));
                if (!changedFileOutput.exists()) 
                    changedFileOutput.getParentFile().mkdirs();
                
                changedFileOutput.createNewFile();

                if (MethodCollector.isNeedTraceFile(classFile.getName())) 
                    is = new FileInputStream(classFile);
                    ClassReader classReader = new ClassReader(is);
                    ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
                    ClassVisitor classVisitor = new TraceClassAdapter(Opcodes.ASM5, classWriter);
                    classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
                    is.close();

                    if (output.isDirectory()) 
                        os = new FileOutputStream(changedFileOutput);
                     else 
                        os = new FileOutputStream(output);
                    
                    os.write(classWriter.toByteArray());
                    os.close();
                 else 
                     // 这里 对文件进行操作,当 classFile 和 changedFileOutput 路径相同时,导致 `classes.jar` 为 0
                    FileUtil.copyFileUsingStream(classFile, changedFileOutput);
                
             catch (Exception e) 
                Log.e(TAG, "[innerTraceMethodFromSrc] input:%s e:%s", input.getName(), e);
                try 
                   // 这里 对文件进行操作
                    Files.copy(input.toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING);
                 catch (Exception e1) 
                    e1.printStackTrace();
                
             finally 
                try 
                    is.close();
                    os.close();
                 catch (Exception e) 
                    // ignore
                
            
        
    

进行条件断点的时候,发现是 FileUtil.copyFileUsingStream 进行 copy 的时候,因为同时读写一个文件,导致 classes.jar 被更改,内容被抹除。到此,原因已经找到了,即 dirInputOutMap 中 input 和 output file 文件路径一致,导致内容错误,那要怎么解决?

小幸运,终于找到解决方案

前面我们说到 dirInputOutMap 中 input 和 output file 文件路径一致,导致内容错误。

那一个最直观的方式,我们尝试加上这样的条件,当 classFile 和 changedFileOutput 路径一致的时候,不进行 copy。

if (!classFile.getAbsolutePath().equals(changedFileOutput.getAbsolutePath())) 
    FileUtil.copyFileUsingStream(classFile, changedFileOutput
 else 
    Log.e(TAG, "error, name should not be equal, classFile.getAbsolutePath() is "+ classFile.getAbsolutePath());

编译本地 matrix trace plugin 版本,运行 demo,跑起来,你会发现 App 正常了,不会 crash 了。too young,too simple.

但是这样会带来一个新的问题,增量编译的时候,不进行 copy,那我们代码的变动,永远不会生效。所以,还是得找为什么 dirInputOutMap 中 input 和 output file 的路径是一样的

还记得前面的 MatrixTrace#doTransform 方法嘛,我们来看一下 step1 和 step2 之间执行的代码

 for (file in classInputs) 
            if (file.isDirectory) 
                futures.add(executor.submit(CollectDirectoryInputTask(
                        directoryInput = file,
                        mapOfChangedFiles = changedFiles,
                        mapOfInputToOutput = inputToOutput,
                        isIncremental = isIncremental,
                        traceClassDirectoryOutput = traceClassDirectoryOutput,
                        legacyReplaceChangedFile = legacyReplaceChangedFile,
                        legacyReplaceFile = legacyReplaceFile,

                        // 第一个地方,可能修改 dirInputOutMap 的值
                        resultOfDirInputToOut = dirInputOutMap
                )))
             else 
                val status = Status.CHANGED
                futures.add(executor.submit(CollectJarInputTask(
                        inputJar = file,
                        inputJarStatus = status,
                        inputToOutput = inputToOutput,
                        isIncremental = isIncremental,
                        traceClassFileOutput = traceClassDirectoryOutput,
                        legacyReplaceFile = legacyReplaceFile,

                        // 第二个地方,可能修改 dirInputOutMap 的值
                        resultOfDirInputToOut = dirInputOutMap,
                        resultOfJarInputToOut = jarInputOutMap
                )))
            
        

        for (future in futures) 
            future.get()
        
        futures.clear()

可以看到,这个方法主要干了两件事情

  • 遍历文件,如果 isDirectory 为 true, 执行 CollectDirectoryInputTask 任务
  • 如果是文件,执行 CollectJarInputTask 任务

我们先来看一下 CollectDirectoryInputTask 类,因为我们主要是关注 dirInputOutMap,我们 find usage 一下,发现 dirInputOutMapcom.tencent.matrix.plugin.trace.MatrixTrace.CollectDirectoryInputTask#handle 更改

因为是增量编译出现问题,所以,我们在 isIncremental 为 true 的时候设置断点,断点条件为 changedFileInput.absolutePath.equals("D:\\\\githubRep\\\\gradleLearing\\\\mylibrary\\\\build\\\\intermediates\\\\runtime_library_classes_jar\\\\debug\\\\classes.jar")

很快我们发现 changedFileInput 和 changedFileOutput 的路径是是一模一样的,即 resultOfDirInputToOut[changedFileInput] = changedFileOutput 中 resultOfDirInputToOut key 和 value 是一致的,那么很有可能就是这个原因。

于是,我对代码进行了修改,将 val changedFileOutput = File(changedFileInputFullPath.replace(inputFullPath, outputFullPath)) 修改为如下的代码。

val changedFileOutput = if (changedFileInputFullPath.contains(inputFullPath))
                        File(changedFileInputFullPath.replace(inputFullPath, outputFullPath))
                     else  // if not contains, changedFileOutput should be modify, else when we read and write the same file, the jar would be empty
                        File(outputFullPath, changedFileInput.name)
                    

本地编译 matrix trace plugin,发现完美运行,不管是全量编译,还是增量编译, perfect。到此问题终于解决了。

至于项目中 val changedFileOutput = File(changedFileInputFullPath.replace(inputFullPath, outputFullPath)) 的这行代码,我猜测可能跟 AGP 早期的版本有关吧,可能早期,inputFullPath 的路径一定是包含在 changedFileInputFullPath 里面的,然后就写了这样的代码,后面 AGP 升级,导致增量编译有问题,具体的没验证,猜测而已。

小结

其实,这次解决问题的过程我算是挺幸运的,能找到解决方案。很多时候,有一些疑难杂症,排查了好久,都没法找到根本原因。有结果当然是最好的,没有的话,其实我们也有很大收获,在这过程中我们培养了独立解决问题的能力,这对我们自身的成长有莫大的帮助。

再来简述一下这次历程,这一次,调试 matrix trace plugin 插件,刚开始真的是一脸懵逼。一会编出来的包,有问题,一会没有问题。

于是在本地尝试了好久,终于发现了复现路径,然后到 issue 上面也搜了一下,发现很多人遇到这个问题,但是还没有解决。

于是,就先关了 trace 插件的增量编译,发现 OK 了。但是这只是一个规避方案,不是一个解决方案。那时候,还比较忙,看了一天左右,也没找出原因,一脸懵逼。就先去加入 matrix 功能了。

可是,这个问题却一直在脑海中记着,过了三四天,差不多接入完成了。就硬着头条去看源代码了。真的没有捷径,一步步排查,刚开始的时候,总想着一步到位,想一口吃成胖子,看能不能一下子解决,看着看着就绕晕了。后面我就学乖了,一步步来,一步步调试,逐个排查,最终,运气比较好,终于找到原因了。

那一刻,真的是挺开心的,充满满满的成就感。

可以看到,这次我解决问题的思路是:

搜索有没有类似的问题 -》 尝试复现路径 -》 再次搜索类似的问题 -》 最小版本验证是增编编译的问题 -》 从日志找出关键信息 -》 根据错误信息一步步排查 -》 定位到原因 -》 一步步找到解决方案。

你学废了吗?如果是你,你会怎么解决呢?有更好的方案嘛,欢迎留言讨论。

pull request 地址, 提了 pr,官方暂时还没有处理,到时候不知道会不会打脸,哈哈。

推荐阅读

我的 5 年 Android 学习之路,那些年一起踩过的坑

职场上这四件事,越早知道越好

今天,说三件小事

技术人的未来在哪里

致刚入职场的你 - 程序员的成长笔记

我是站在巨人的肩膀上成长起来的,同样,我也希望成为你们的巨人。觉得不错的话可以关注一下我的微信公众号徐公

  1. 公众号徐公回复黑马,获取 Android 学习视频
  2. 公众号徐公回复徐公666,获取简历模板,教你如何优化简历,走近大厂
  3. 公众号徐公回复面试,可以获得面试常见算法,剑指 offer 题解
  4. 公众号徐公回复马士兵,可以获得马士兵学习视频一份
  5. 公众号徐公回复Java 电子书,可以获得我精心整理的 Java 电子数据

希望我们可以成为朋友,成长路上的忠实伙伴!

以上是关于腾讯 Matrix 增量编译 bug 解决之路,PR 已通过的主要内容,如果未能解决你的问题,请参考以下文章

腾讯 Matrix 增量编译 bug 解决之路,PR 已通过

腾信 Matrix 增量编译 bug 解决之路,PR 已通过

腾讯bugly干货分享微信Android热补丁实践演进之路

当前的 JDK 版本 1.8 有一个 bug 会阻止 Room 增量

腾讯Bugly干货分享你为什么需要 Kotlin

即将写入MySQL源码的官方bug解决之路