本文首发我的微信公众号:徐公,想成为一名优秀的 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.NullPointerExceptionat 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.NullPointerExceptionat 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.NullPointerExceptionat 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 emptyat 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.CHANGEDfutures.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() {@Overridepublic 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` 为 0FileUtil.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.CHANGEDfutures.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 emptyFile(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 已通过相关推荐

  1. 一个即将写入MySQL源码的官方bug解决之路

    作者:周信静,毕业于浙江大学,目前在CDB/CynosDB数据库内核团队参与TXSQL云数据库内核研发工作,参与了热点行更新以及一系列性能优化工作,并修复了多个MySQL官方bug. 1背景 Inno ...

  2. vscode 格式化某一段代码_VSCode格式化代码功能失效的bug解决方法

    VSCode格式化代码功能失效的bug解决方法 前不久我装上了 黑苹果,那么为了快速转移开发环境,我使用了VSCode(Visual Studio Code下面简称VSCode)的插件 Setting ...

  3. iphone XCode调试技巧之EXC_BAD_ACCESS中BUG解决

    http://mobile.51cto.com/iphone-279455.htm XCode调试技巧之EXC_BAD_ACCESS中BUG解决是本文要介绍的内容,在iphone开发的时候EXC_BA ...

  4. 在TFS中通过程序动态创建Bug并感知Bug解决状态

    为便于跟踪问题解决情况,预警引擎产生的比较严重的预警日志,需要在TFS中登记Bug,通过TFS的状态流转,利用TFS Bug的Web挂钩功能,动态感知Bug解决状态,从而跟踪预警问题的解决状态, 整体 ...

  5. WPF .NET 4.0 OpenClipboard 失败 (异常来自 HRESULT:0x800401D0 (CLIPBRD_E_CANT_OPEN)) BUG解决

    WPF .NET 4.0 OpenClipboard 失败 (异常来自 HRESULT:0x800401D0 (CLIPBRD_E_CANT_OPEN)) BUG解决 参考文章: (1)WPF .NE ...

  6. [记录]mscorlib recursive resource lookup bug解决方法

    [记录]mscorlib recursive resource lookup bug解决方法 参考文章: (1)[记录]mscorlib recursive resource lookup bug解决 ...

  7. ProxyStrike运行bug解决办法

     ProxyStrike运行bug解决办法 由于curl中参数CURLOPT_SSL_VERIFYHOST的值取消原有的值1,导致ProxyStrike无法正常运行.所以,要运行该工具,需要手动修改/ ...

  8. Kali Linux 2017中Scapy运行bug解决

    Kali Linux 2017中Scapy运行bug解决 Scapy是一款强大的网络数据包构建工具.在Kali Linux 2017中,当在scapy的命令行中,运行res.graph()生成图形时, ...

  9. 我的世界服务器伤害增加bug,我的世界服务器BUG解决办法大全

    小编为大家带来了<我的世界>服务器BUG解决办法大全,这个是写给各位想要开服的腐竹的一些建议和BUG的解决方法,让各位新人腐竹了解到很多MOD中存在的一些隐患,好让各位新人腐竹也能够很好的 ...

最新文章

  1. 可能是最好的跨域解决方案了
  2. Git入门教程(上)
  3. python3写unicode编码到文件
  4. 动态sql拼接单引号与 变量赋值
  5. MFC学习中遇到的小问题和解决方案
  6. JAVA入门级教学之(JAVA程序的加载和运行)
  7. 为什么腾讯云要自研云原生数据库 CynosDB?
  8. 华为机试-字符串通配符
  9. android Cursor用法
  10. promise的理解和使用-尚硅谷教程笔记
  11. Python 文本滚动播放
  12. SQL三个表关联查询
  13. mysqli_fetch_assoc()和mysqli_fetch_array()的区别与用法
  14. 汉字如何应用在平面设计中
  15. 专科计算机课程作业2理解题,计算机第二模块练习题
  16. 【入门篇】一、什么是单片机
  17. 微信小程序:王者荣耀改名神器
  18. 连载 大学生求职七大昏招(六)说谎 5
  19. 百变红包封面,助力品牌传播!让每一个发出的红包都有价值!
  20. TensorFlow - 求导

热门文章

  1. Android Studio设置HTTP代理地址
  2. ShareSDK的使用
  3. Milimeter-Wave UAV Communications(41-50)
  4. 关于.net framework4.0安装失败,“安装时发生严重错误”
  5. 世界上最远的距离——泰戈尔
  6. 2018《财富》世界500强出炉,比500强更多的财富在这里!
  7. Kafka组消费之Rebalance机制
  8. mysql数据库收缩
  9. python打印pdf文件_Python静默打印PDF到特定的打印机
  10. 最好用的网易邮箱工具-网易邮箱助手