通常状况下,Android项目常常开启ProGuard功能来混淆代码,一方面能够下降应用被反编译后代码的友善度,增长被逆向的难度,另外一方面开能够经过精简Java API的名字来减小代码的总量,从而精简应用编译后的体积。java
ProGuard有个比较坑爹的问题。在开发阶段,咱们通常不启用ProGuard,只有在构建Release包的时候才开启。所以,若是有一些API被混淆了会出现BUG,那么在开发阶段咱们每每没法察觉BUG,只有在构建发布包的时候才发现,甚至要等发布到线上了才能发现,这种时候解决问题的成本就很大了。android
不过今天被ProGuard坑的不是混淆API致使的BUG,这货在以前至关长的一段时间里一直相安无事,最近忽然又搞了个大新闻,并且问题排查起来至关蹊跷、诡异。git
最近在给项目的开发一个模块之间通信用的路由框架,它须要有一些处理注解的APT功能,大概是长这个样子的。github
@Route(uri = "action://sing/", desc = "念两句诗") public static class PoemAction { ... }
功能大概是这样的,我先编写一个叫作 PoemAction
,它的业务功能主要是帮你念上两句诗。而后客户只须要调用 Router.open("action://sing/")
就能够当场念上两句诗,这也是如今通常路由框架的功能。其中的desc
没有别的功能,只是为了在生成路由表的时候加上一些注释,说明当前的路由地址是干什么的,看起来像是这样的。架构
public static class AutoGeneratedRouteTable { public Route find(String uri) { ... if("action://sing/".equals(uri)) { // 念两句诗 return PoemActionRoute; } ... } }
嗯,代码很完美,单元测试和调试阶段都没有发现任何问题,好,合并进develop分支了。搞定收工,我都不由想赞美本身的才能了,先去栖霞路玩会儿先。半个小时候忽然收到了工头 Yrom·半仙·灵魂架构师·Wang 的电话,我还觉得他也想来玩呢,结果他说不知道谁在项目的代码里下毒,致使构建机上有已经有几十个构建任务失败了。我了个去,我刚刚提交的代码,该不会是个人锅吧,赶忙回来。app
异常看起来是这样的。框架
FAILURE: Build failed with an exception. * What went wrong: Execution failed for task ':app:transformClassesWithMultidexlistForRelease'. > java.lang.UnsupportedOperationException (no error message) * Try: Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. BUILD FAILED
这看起来好像是MultiDex的问题啊,可是没道理Debug构建没问题,而只有Release构建出问题了,transformClassesWithMultidexlistForRelease
任务的源码暂时也没有精力去看了,先解决阻塞同事开发的问题要紧。老规矩,使用 二分定位法 挨个回滚到develop上面的commit记录,逐个查看是那次提交致使的,结果还真是个人提交致使的。ide
难道是开了混淆,致使一些类找不到?可是类找不到只是运行时的异常而已,应该只会在运行APP的时候抛出“ClassNotFoundException”,不该该致使构建失败啊。难道是APT生成的类格式不对,致使Javac在编译该类的时候失败?因而我打开由APT工具生成的AutoGeneratedRouteTable.java
类文件瞧瞧,发文件类的格式很完美,没有问题,甚至因为担忧是中文引发的问题,我还把“念两句诗”改为“Sing two poems”,问题依旧。工具
总之一时半会没法排查出问题所在,仍是赶忙解决APK的构建问题,如今由于构建失败的缘由,旁边已经有一票同事正在摩拳擦掌准备把我狠狠的批判一番。因此我打算先去掉APT功能,不经过自动生成注册类的方式,而是经过手动代码注册的方式让路由工做,就当我觉得事情告一段落的时候,我才发现我仍是“too young”啊,构建机给了一样的错误反馈。单元测试
…………
……
…
这TM就尴尬了啊,我如今致使构建失败的提交与上一次正常构建的提交之间的差别就是给PeomAction
加多了注解而已啊,并且这个注解如今都没有用到了,难道是注解自己的存在就会致使构建失败?
忽然我想起来,注解类自己我是没有加入混淆的,由于代码里没有用反射的反射获取注解,并且我设计注解类自己的目的也只是为了帮我自动生成注册类而已,这些类是编译时生成的,因此不会受到混淆功能的影响。抱着死马当活马医的心态,我把注解里面的desc
字段去掉了,万万没想到构建问题竟然就解决了,并且就算我开启APT功能,问题仍是没有重现,这…… 这与构建出问题的状态的差异只有一段注释的差异啊,没问题的代码看起来是这样。
public static class AutoGeneratedRouteTable { public Route find(String uri) { ... if("action://sing/".equals(uri)) { (这里的注释没有了) return PoemActionRoute; } ... } }
这难道是真实存在的某种膜法在干扰个人构建过程?忽然我又想起来,由于注解类自己不须要写什么代码,因此我建立Route.java
这个类后基本就没有对它进行过编辑了,我甚至已经忘了我对它写过什么代码,因此我决定看看是否是我写错了些什么。
@Target(ElementType.TYPE) @Retention(RetentionPolicy.CLASS) public @interface Route { String[] value(); String desc() default ""; }
这个注解类看起来再普通不过,通常写完以后也不须要再怎么修改了,并且这个类我是直接参(co)考(py)另一个优秀的Java APT项目 DeepLinkDispatch 的,想必也不会有什么大坑。目前看起来惟一有更改可能性的地方就是Target
和Retention
这两个属性,至于这俩的做用不属于此文章的范畴,不作展开。
首先,我试着把Retention
的级别由原来的CLASS
改为SOURCE
级别,没想到就这么一个小改动,编译竟然经过了!若是不修改Retention
的级别,把注解里的desc
字段移除,只保留一个value
字段,问题也能解决,真是神奇啊,顿时我好像感觉到了一股来自古老东方的神秘力量。
在我一直以来的认知里,RetentionPolicy.SOURCE
是源码级别的注解,好比@Override
、@WorkerThread
、@VisibleForTest
等这些注解类,这类的注解通常是配合IDE工做的,不会给代码形成任何实际影响,IDE会获取这些注解,并向你提示哪些代码可能有问题,在编译阶段这类注解加与不加没有任何实际的影响。看一下源码的解释吧。
public enum RetentionPolicy { /** * Annotations are to be discarded by the compiler. */ SOURCE, /** * Annotations are to be recorded in the class file by the compiler * but need not be retained by the VM at run time. This is the default * behavior. */ CLASS, /** * Annotations are to be recorded in the class file by the compiler and * retained by the VM at run time, so they may be read reflectively. * * @see java.lang.reflect.AnnotatedElement */ RUNTIME }
原来如此,RetentionPolicy.CLASS
级别的注解会被保留到.class
文件里,因此混淆的时候,注解类也会参与混淆,大概是混淆的时候出的问题吧。总之,先看看注解类Route.java
被混淆后变成什么样子,查看 build/output/release/mapping.txt
文件。
... moe.studio.router.Route -> bl.buu: java.lang.String[] value() -> a java.lang.String desc() -> a ...
果真不出我所料,ProGuard工具在混淆注解类类Route.java
的时候,把它的两个字段都混淆成a
了(按道理应该是一个a和一个b,不知道是否是ProGuard的BUG,仍是Route与其余库冲突了)。
因此,最后的解决方案就是把Retention
的级别由原来的CLASS
降级成SOURCE
,或者把注解类的字段改为一个。顺便一说,如今大多的Java APT项目用的仍是CLASS
,它们之因此没有遇到相似的问题,大可能是由于他们都选择把整个注解类都KEEP住,不进行混淆了。
经过这个事件我也发现了很多问题。其一,不管单元测试写得再完美,集成进项目以前仍是有必要进行一次Release构建,以确保避免一些平时开发的时候容易忽略的问题,否则当心本身打本身的脸。如下是一次打脸现场。
因此我决定,给项目的构建机加上一次 Daily Building 的功能,天天都按期构建一次,以便尽早发现问题。
其二,除了构建的问题以外,年轻人果真仍是要多多学习,提升一下本身的知识水平。设想,若是个人Java基础够扎实的话,也就不会像此次同样,犯下RetentionPolicy
错用这样低级的错误。若是有仔细阅读过 transformClassesWithMultidexlistForRelease
任务以及ProGuard工具的的源码的话,也许能很快定位到问题发生的根本缘由,从而釜底抽薪一举解决问题,不像此次同样,阻塞一大半天开发进度。
如下放出此次定位问题的大体过程。
① 先定位 transformClassesWithMultidexlistForRelease
任务的源码。经过任务名字,能够很快地定位到 MultiDexTransform.java
这个类里面来,如下是这个类在执行任务时候作的工做。
@Override public void transform(@NonNull TransformInvocation invocation) throws IOException, TransformException, InterruptedException { // Re-direct the output to appropriate log levels, just like the official ProGuard task. LoggingManager loggingManager = invocation.getContext().getLogging(); loggingManager.captureStandardOutput(LogLevel.INFO); loggingManager.captureStandardError(LogLevel.WARN); try { File input = verifyInputs(invocation.getReferencedInputs()); shrinkWithProguard(input); computeList(input); } catch (ParseException | ProcessException e) { throw new TransformException(e); } }
能够看出,MultiDexTransform的主要工做是在shrinkWithProguard
和computeList
两个方法里面完成的。其中shrinkWithProguard
的工做能够定位到ProGuard工具的ProGuard#execute
方法里面。
public void execute() throws IOException { System.out.println(VERSION); GPL.check(); ... if (configuration.dump != null) { dump(); } }
能够定位到ProGuard最后执行的dump()
方法里面,该方法生成了一个dump.txt
文件,里面用文本的形式,记录了整个项目用到的全部类(混淆后的)的文件结构。查看任务的LOG信息以及dump.txt
文件的内容,发现全部内容都正常生成,所以能够初步肯定问题不是因为shrinkWithProguard
引发的。
接着看看computeList
方法,这个方法能够定位到如下代码。
public Set<String> createMainDexList( @NonNull File allClassesJarFile, @NonNull File jarOfRoots, @NonNull EnumSet<MainDexListOption> options) throws ProcessException { BuildToolInfo buildToolInfo = mTargetInfo.getBuildTools(); ProcessInfoBuilder builder = new ProcessInfoBuilder(); String dx = buildToolInfo.getPath(BuildToolInfo.PathId.DX_JAR); if (dx == null || !new File(dx).isFile()) { throw new IllegalStateException("dx.jar is missing"); } builder.setClasspath(dx); builder.setMain("com.android.multidex.ClassReferenceListBuilder"); if (options.contains(MainDexListOption.DISABLE_ANNOTATION_RESOLUTION_WORKAROUND)) { builder.addArgs("--disable-annotation-resolution-workaround"); } builder.addArgs(jarOfRoots.getAbsolutePath()); builder.addArgs(allClassesJarFile.getAbsolutePath()); CachedProcessOutputHandler processOutputHandler = new CachedProcessOutputHandler(); mJavaProcessExecutor.execute(builder.createJavaProcess(), processOutputHandler) .rethrowFailure() .assertNormalExitValue(); LineCollector lineCollector = new LineCollector(); processOutputHandler.getProcessOutput().processStandardOutputLines(lineCollector); return ImmutableSet.copyOf(lineCollector.getResult()); }
从源码能够看出,这里调用了Android SDK里面的dx.jar
工具,入口类是 com.android.multidex.ClassReferenceListBuilder
,并传入了两个参数,分别是jarOfRoots
文件和allClassesJarFile
文件。
② 定位到dx.jar
工具里具体出问题的地方,经过上面的分析以及构建失败输出的LOG,能够看到Gradle插件调用了dx.jar
并传入了build/intermediates/multi-dex/release/componentClasses.jar
和build/intermediates/transforms/proguard/release/jars/3/1f/main.jar
两个文件。直接调用该命令试试。
Exception in thread "main" com.android.dx.cf.iface.ParseException: name already added: string{"a"} at com.android.dx.cf.direct.AttributeListParser.parse(AttributeListParser.java:156) at com.android.dx.cf.direct.AttributeListParser.parseIfNecessary(AttributeListParser.java:115) at com.android.dx.cf.direct.AttributeListParser.getList(AttributeListParser.java:106) at com.android.dx.cf.direct.DirectClassFile.parse0(DirectClassFile.java:558) at com.android.dx.cf.direct.DirectClassFile.parse(DirectClassFile.java:406) at com.android.dx.cf.direct.DirectClassFile.parseToEndIfNecessary(DirectClassFile.java:397) at com.android.dx.cf.direct.DirectClassFile.getAttributes(DirectClassFile.java:311) at com.android.multidex.MainDexListBuilder.hasRuntimeVisibleAnnotation(MainDexListBuilder.java:191) at com.android.multidex.MainDexListBuilder.keepAnnotated(MainDexListBuilder.java:167) at com.android.multidex.MainDexListBuilder.<init>(MainDexListBuilder.java:121) at com.android.multidex.MainDexListBuilder.main(MainDexListBuilder.java:91) at com.android.multidex.ClassReferenceListBuilder.main(ClassReferenceListBuilder.java:58) Caused by: java.lang.IllegalArgumentException: name already added: string{"a"} at com.android.dx.rop.annotation.Annotation.add(Annotation.java:208) at com.android.dx.cf.direct.AnnotationParser.parseAnnotation(AnnotationParser.java:264) at com.android.dx.cf.direct.AnnotationParser.parseAnnotations(AnnotationParser.java:223) at com.android.dx.cf.direct.AnnotationParser.parseAnnotationAttribute(AnnotationParser.java:152) at com.android.dx.cf.direct.StdAttributeFactory.runtimeInvisibleAnnotations(StdAttributeFactory.java:616) at com.android.dx.cf.direct.StdAttributeFactory.parse0(StdAttributeFactory.java:93) at com.android.dx.cf.direct.AttributeFactory.parse(AttributeFactory.java:96) at com.android.dx.cf.direct.AttributeListParser.parse(AttributeListParser.java:142) ... 11 more
从异常的堆栈能够直接看出,dx工具在执行AnnotationParser#parseAnnotation
方法的时候出错了,缘由是有两个相同的字段a
,这也恰好印证了上面mapping.txt
文件里面的错误信息。
③ 最后定位到源码里具体出问题的地方,查看dx工具里的com.android.dx.rop.annotation.Annotation.java
的源码。
private final TreeMap<CstString, NameValuePair> elements; /** * Add an element to the set of (name, value) pairs for this instance. * It is an error to call this method if there is a preexisting element * with the same name. * * @param pair {@code non-null;} the (name, value) pair to add to this instance */ public void add(NameValuePair pair) { throwIfImmutable(); if (pair == null) { throw new NullPointerException("pair == null"); } CstString name = pair.getName(); if (elements.get(name) != null) { throw new IllegalArgumentException("name already added: " + name); } elements.put(name, pair); }
到此,从成功定位到产生异常的具体地方。
④ 此外,从:app:assembleRelease --debug --stacktrace
的异常堆栈里是没法直接看出具体出异常的地方的错误信息的,不过能够经过:app:assembleRelease --full-stacktrace
命令输出更多的错误堆栈,从而直观地看出一些猫腻来。
Caused by: com.android.ide.common.process.ProcessException: Error while executing java process with main class com.android.multidex.ClassReferenceListBuilder with arguments {build/intermediates/multi-dex/release/componentClasses.jar build/intermediates/transforms/proguard/release/jars/3/1f/main.jar} at com.android.build.gradle.internal.process.GradleProcessResult.buildProcessException(GradleProcessResult.java:74) at com.android.build.gradle.internal.process.GradleProcessResult.assertNormalExitValue(GradleProcessResult.java:49) at com.android.builder.core.AndroidBuilder.createMainDexList(AndroidBuilder.java:1384) at com.android.build.gradle.internal.transforms.MultiDexTransform.callDx(MultiDexTransform.java:309) at com.android.build.gradle.internal.transforms.MultiDexTransform.computeList(MultiDexTransform.java:265) at com.android.build.gradle.internal.transforms.MultiDexTransform.transform(MultiDexTransform.java:186)
从上面的堆栈信息能够直接看出Gradle插件在调用dx工具的时候出现异常了(Process的返回值不是0,也就是Java程序里面调用了System.exit(0)以外的结束方法),对应的类为ClassReferenceListBuilder
。
public static void main(String[] args) { int argIndex = 0; boolean keepAnnotated = true; while (argIndex < args.length -2) { if (args[argIndex].equals(DISABLE_ANNOTATION_RESOLUTION_WORKAROUND)) { keepAnnotated = false; } else { System.err.println("Invalid option " + args[argIndex]); printUsage(); System.exit(STATUS_ERROR); } argIndex++; } if (args.length - argIndex != 2) { printUsage(); System.exit(STATUS_ERROR); } try { MainDexListBuilder builder = new MainDexListBuilder(keepAnnotated, args[argIndex], args[argIndex + 1]); Set<String> toKeep = builder.getMainDexList(); printList(toKeep); } catch (IOException e) { System.err.println("A fatal error occurred: " + e.getMessage()); System.exit(STATUS_ERROR); return; } }
由其中的 MainDexListBuilder builder = new MainDexListBuilder(keepAnnotated, args[argIndex], args[argIndex + 1])
也能进一步定位到上面的 com.android.dx.rop.annotation.Annotation.java
出问题的地方。
推荐阅读 ProGuard在插件化里的应用。
著做信息:
本文章出自 Kaede 的博客,原创文章若无特别说明,均遵循 CC BY-NC 4.0 知识共享许可协议4.0(署名-非商用-相同方式共享),能够随意摘抄转载,但必须标明署名及原地址。