本文转载自微信公众号《咸鱼翻身》,作者多多。转载本文请联系闲鱼正翻车公众号。前言人在空闲的时候会对各种各样的事物产生好奇,而当对很多事物产生好奇的时候,就会发现它们是真正的食物。今天之所以写这篇文章,是源于一个非常非常“诡异”的IDE语法错误提示。文章介绍的是android的知识,但真正想讲的是编译原理。所以:只有标题?。因此,读者大可不必过于纠结。没学过android和Java,不影响阅读。复现语法错误的代码:Text1.android知识部分的IDE提示也很明确:res的id不能在library级的module中。应用于.中的switch语法。原因是res的id不是常量。注意:同样的代码在应用层模块中没有语法问题。所以对于res的id,在应用中是常量,在库中不是。如果有同学看过R的内容,会发现确实如此:这是应用程序中的R文件:这是库中的R文件:这个表象引出了android打包的一个知识点:资源中的资源aapt[1]Merge[2]的过程。一句话描述这个知识点:不同模块之间重复的资源会按照优先级进行合并覆盖。这个过程带来的问题很多老司机都遇到过。资源是覆盖的,我们引用的资源永远指向唯一的res。这当然不是预期的。因此,资源名称前缀等方案应运而生。为什么不是final这里先说一个问题:常量有什么特别的地方?编译如下代码后,可以看出常量的特殊之处:classTestFinal{staticfinalintsInt=1;voidtestFinal(){inttemp=sInt;System.out。println(temp);}}编译后的代码会是这样:publicvoidtestFinal(){System.out.println(1);}会发现编译器的优化,会直接把常量内联到代码所在的地方被引用。那么我们想一想:如果库中的res也是一个常量会怎样?常量是内联的。一旦项目中出现资源重复,在打包过程中会被覆盖,内联常量无法再映射到真正的资源中,在所有资源都被覆盖之后。也就是说会出现:找不到资源导致崩溃的问题不是最终的。库中的R引用不是一个常量,也就是说这个用法是行不通的:可以看到,注解也是一个常量,所以这个问题对于我们日常的影响还是挺大的……等等!Butterknife是注解的用法,为什么没有问题??深入了解Butterknife的同学应该知道,Butterknife针对这种情况做了特殊处理:Butterknife的解决方案Butterknife是不让注解出现语法错误,创建一个类叫R2。这个类实际上是R的一个副本,唯一的区别是R2是一个常量。确实这样不会有语法错误,但是刚才我们也分析过了:constantinlining,resourcecoverage。所以一旦case满足了,那就是crash。那么Butterknife是如何避免这个问题的呢?看过Butterknife中findViewById()源码的同学应该知道,这里Butterknife的实现大概是这样的:.id.test,"field'parentLayout'",ViewGroup.class);我们可以看到Butterknife最后打包的代码并没有常量内联!那么怎么样你做了什么?看到这里的学生,不妨停下来想一想。如果是你,你会如何解决这个问题?这里说说我能想到的解决方案:ASM阶段,将内联代码改写成R普通引用。问题来了:ASM的输入是class,此时我已经无法正常获取到R的引用了。如果继续推进干预进程,放到APT阶段呢?我试过了,但没有成功。APT阶段拿到的注解值已经是inline常量了……这就有点奇怪了。Butterknife是如何实现行内常量和R引用的映射的?查看Butterknife的源码,发现Butterknife是在APT阶段执行的,关键类是ButterKnifeProcessor[3]。Butterknife通过JCTreeapi获取到R引用,然后将内联代码改回R引用。具体的api实现我们就不看了,有兴趣的同学可以自行github。下面说说这个JCTree是干什么的。第二,我们都知道编译的原理:我们每天写的代码要想在目标机上运行,??就需要编译成目标机可以识别的机器码。从事这项工作的人称为编译器。一般的编译器会做以下几件事:图片来自《编译原理》第二版在各种源码编译的实现中,基本都是用同样的方式抽象出一个概念:抽象语法树(AST),这样就可以在整个编译和执行过程中使用起来更加方便。一句话解释抽象语法树:源代码语法结构的抽象表示。它以树的形式表示编程语言的语法结构,树上的每个节点代表源代码中的一个结构。编译器的实现过程我们大概了解了,那么编译器是如何实现的呢?当然是用代码实现的,而且他们的实现往往和我们很接近……以我们的java编译器为例。初入Java,我们应该都尝试过javac。这个命令的实现在哪里?在JDK的tools.jar中com.sun.tools.javac.Main包下。核心逻辑在com.sun.tools.javac.main.JavaCompiler。下面介绍如何分析我们的源代码以及如何将其转换为类。这就是上图中的编译器应该做的。那么JCTree在整个编译过程中起到什么作用呢?一句话:JCTree是一个API级的源码描述。也就是说,JCTree是java编译过程中语法树的实现。也就是说,通过JCTree相关的API,我们可以访问到源码结构。看起来很抽象,我们可以通过调试一段代码来了解它存在的意义:/xx/xxx/TestAutoCode.java")ToolProvider.getSystemJavaCompiler().getStandardFileManager(DiagnosticCollector(),null,null).getJavaFileObjectsFromFiles(listOf(testJavaCodeFile)).forEach{javaCompiler.parse(it).defs.forEach{扫描器。scan(it)}}}classRScanner:TreeScanner(){overridefunvisitMethodDef(tree:JCTree.JCMethodDecl?){super.visitMethodDef(tree)}}基于这套api,我们可以得到源码的任何信息。而且这个demo代码只需要导入tools.jar就可以快速运行,成本非常低。3.使用代码运行代码上面我们通过JavaCompiler的例子动态编译了java源码,得到的就是java源码的class文件。有了class文件,我们就可以通过ClassLoader加载类了。有了上面的基础,实现源码就不重要了。这里有一个链接大家自己拿:Howdoyoudynamicallycompileandloadexternaljavaclasses?[4]最后,我个人并没有认真学过编译原理,所以理解这部分内容还是蛮牛的。也希望这篇文章能给没有学过编译原理的同学带来一些思考和启发~参考文献[1]aapt:https://developer.android.com/studio/command-line/aapt2?hl=zh_cn[2]资源合并:https://developer.android.com/studio/write/add-resources?hl=zh-cn#resource_merging[3]ButterKnifeProcessor:https://github.com/JakeWharton/butterknife/blob/fcdebedf3276096db2f51bf6372b849b5a9c75ed/butterknife-compiler/src/main/java/butterknife/compiler/ButterKnifeProcessor.java#L1470[4]如何动态编译和加载外部java类?:https://stackoverflow.com/questions/21544446/how-你动态编译和加载外部java类吗
