Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix incremental build - zip file is empty #661

Merged

Conversation

gdutxiaoxu
Copy link

@gdutxiaoxu gdutxiaoxu commented Nov 1, 2021

异常类型:编译异常& app crash

matrix版本:2.0.1

gradle版本:4.0.0

问题描述:第一次编译正常运行,第二次编译正常,但是运行的时候会 crash,报 dex 文件没有这个 class 文件,每次需要 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.

问题直接原因

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

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

说干就干,于是我把 MatrixTraceTransform#isIncremental,MatrixTraceLegacyTransform##isIncremental 都返回 false,发现我们项目增量编译也 ok 了,不会 crash 了。特意去看了一下编译耗时,在我们项目中,编译一次,transformClassesWithRealmTransformerForDebug,耗时大概是 20 - 30 ms 左右,增量编译在 10 - 15 ms,关闭 matrix transfrom 增量编译的话,大概慢 10 - 15 ms,貌似也可以接受。

问题探索

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

想着想着,我再次进入这个坑。再次去看编译信息,怀疑有四个地方

  • 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 位于
jetified-kotlin-stdlib-jdk7-1.5.20.jar 里面,具体为什么为 null,还没有去看

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,MyGold, 我的天。

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 input 是依赖于上一个 transfrom 的 output 传递过来的,那有没有可能是上一个 transform 传递过来的时候出错。

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

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

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

我们重点怀疑 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 了。

但是这样会带来一个新的问题,增量编译的时候,不进行 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 功能了。

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

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

@gryamy
Copy link
Collaborator

gryamy commented Nov 9, 2021

看了你的分析和修改,这里确实是存在问题的,mapOfChangedFiles 包含了所有的被修改文件不应该把每个 dirInput 都判断一遍。

PR 我合入了,非常感谢 gdutxiaoxu 细致的分析!

@gryamy gryamy merged commit 87688f4 into Tencent:master Nov 9, 2021
@gdutxiaoxu
Copy link
Author

好的,第一次开源项目提 pr,能够合入,挺开心的,谢谢认可

@cjunopen
Copy link

cjunopen commented Dec 6, 2021

好的,第一次开源项目提 pr,能够合入,挺开心的,谢谢认可

为大神鼓掌👏
目前已用主干上“87688f448756137fd0b01213c1a40d20edb4b6cf”的提交记录解决需要clean才能再次编译的问题
只是比较奇怪的是,会报出很多条类似
"build\intermediates\transforms\MatrixTraceTransform\debug\99.jar: 另一个程序正在使用此文件,进程无法访问" 的信息
请问是否会有什么影响呢?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants