奇怪的知识点:用代码run代码

前言

人闲下来就会对各种各样的东西感到好奇,好奇的东西多了就发现自己是真的菜。

今天这篇文章写出来的原因,源自一次非常非常“诡异的”IDE的语法错误提示。

文章是由android的知识引入,但真正想聊的东西是编译原理。所以:才有了标题《奇怪的知识》。因此各位看官没必要太纠结自己没有学过android或者Java,不影响阅读~

正文

复现一次语法错误的代码:

image.png

android知识部分

IDE提示的也很明白:res的id不能在library级别的module中的switch语法中应用。原因是res的id不是常量

注意:同样的代码在application级别的module中是没有语法问题的。所以对于res的id来说,application中是常量,library中不是常量。如果有同学看过R的内容,就会发现的确如此:

这个是application中的R文件:

image.png

这个是library中的R文件:

image.png

这个显现引申出一个android打包的知识点:aapt过程中的资源合并

一句话描述这个知识点:不同module之间的重复的资源会按优先级的进行合并覆盖。这个流程引发的问题,很多老司机都遇到过,资源被覆盖了,我们引用的资源永远会被指向唯一的res。这肯定是不符合预期的。

因此诸如给资源名加前缀的方案便应运而生。

为什么不是final

这里咱们聊一个问题:常量有什么特别之处?下面的代码,编译之后就是能看到常量的特别之处:

class TestFinal {
    static final int sInt = 1;

    void testFinal(){
        int temp = sInt;
        System.out.println(temp);
    }
}

编译后的代码会是这样:

public void testFinal(){
    System.out.println(1);
}

会发现编译器的优化,会把常量直接内联到代码引用之处。那么这边咱们想想:如果library里的res也是常量会出现什么问题?

常量被内联,一旦发生项目中资源重复,打包过程中就出现覆盖,那么内联的常量已经不能映射到真正的资源上了,因为资源已经被覆盖。

不是final引发的问题

library中的R引用不是常量,就意味着这种用法也是不能工作的:

image.png

可以看到,注解也是要常量的,所以这个问题对我们印象还是挺大的...等等!Butterknife就是注解的这种用法为什么没有问题??

深入了解过Butterknife的同学应该知道,Butterknife针对这种情况进行了特殊处理:

image.png

Butterknife的方案

Butterknife为了不让注解处出现语法错误,自己创造了一个叫做R2的类。这个类其实就是原样copy了R,唯一不同就是,R2都是常量。

的确这样不会有语法错误,但是咱们刚才也分析了:常量内联,资源覆盖。所以一旦满足case,那就是crash。所以Butterknife有时如何规避这个问题的呢?

看过Butterknife中findViewById()源码的同学应该都是到,此处Butterknife的实现大概是这样:

public TestActivity_ViewBinding(T target, View source) {
    this.target = target;
    target.parentLayout = Utils.findRequiredViewAsType(source, R.id.test, "field 'parentLayout'", ViewGroup.class);
}

我们能够看到,Butterknife最终打进包里的代码,并没有发生常量内联!所以它是怎么做的呢?

看到这里的同学,不妨停下来。自己想想如果是你,你会怎么解决这个问题?这里我说说我能想到的方案

ASM阶段,把内联的代码,再给它改写成R的正常引用。问题就来了:ASM的输入是class,这个时机我没办法再拿到R的正常引用了。那如果继续提前这个干预的过程,放到APT阶段呢?
试了一下,也没有搞定。APT阶段拿到的注解value也已经是被内联的常量了...

这就有点奇怪了,Butterknife是如何做到通过内联的常量和R引用的映射呢?翻看了Butterknife的源码,发现Butterknife是在APT阶段执行的,关键类在ButterKnifeProcessor

Butterknife通过JCTree这个api拿到了R的引用,然后把内联的代码又改回了R的引用。具体的api实现咱们就不看了,有兴趣的同学可以自行github。

咱们接下来聊一聊这个JCTree是干啥的?

编译原理

我们都知道:日常我们写下的代码,最终想要运行在目标机器上都需要编译成目标机器能够识别的机器码。而做这些工作的我们称之为编译器。一般编译器就是干了如下的事情:

图片来自《编译原理》第二版

image.png

在各种源码编译的实现中,基本都不约而同地抽象出一个概念个:抽象语法树(AST),以求在整个编译实现过程更加的方便。

一句话解释抽象语法树:源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。

咱们粗略了解了编译器的的实现流程,那么编译器又是怎么实现的呢?当然是用代码实现的咯,而且它们的实现往往离我们很近...以我们java编译器为例。

入坑Java时,我们应该都试过javac。而这个命令的实现在哪?就在JDK里的tools.jar中的com.sun.tools.javac.Main包下。核心逻辑在于com.sun.tools.javac.main.JavaCompiler

image.png

这里边就实现了如何分析我们的源码,如何转化成class。也就上那个图中编译器该干的事。

那么JCTree在整个编译过程中充当什么角色呢?一句话:JCTree是对源码的一种api级别的描述。或者说JCTree是java编译流程中语法树的实现。

也就是说通过JCTree相关api,我们可以访问到源码结构。说起来似乎很抽象,我们debug个一段代码就能get到它存在的意义了:

fun main() {
    val context = Context()
    val scanner = RScanner()
    val javaCompiler = JavaCompiler.instance(context)
    val testJavaCodeFile = File("/Users/x/xx/xxx/TestAutoCode.java")

    ToolProvider
        .getSystemJavaCompiler()
        .getStandardFileManager(DiagnosticCollector(), null, null)
        .getJavaFileObjectsFromFiles(listOf(testJavaCodeFile))
        .forEach {
            javaCompiler.parse(it).defs.forEach {
                scanner.scan(it)
            }
         }
}

class RScanner : TreeScanner() {
    override fun visitMethodDef(tree: JCTree.JCMethodDecl?) {
        super.visitMethodDef(tree)
    }
}
image.png

基于这一套api我们是能够获取到源码的任何信息的。而且这段demo代码,只需要导入tools.jar就可以快速运行,成本非常的低。

用代码run代码

上述我们同过JavaCompiler的实例,对java源码进行了动态的编译,拿到的结果就是这个java源码的class文件。有了class文件,我们就可以通过ClassLoader去加载这个class。

有了上边的基础,实现源码已经不重要,这里贴一个链接大家自取吧:How do you dynamically compile and load external java classes?

尾声

我个人没有正经的学过编译原理,所以了解这部分内容时,觉得还是挺神奇的。也希望这篇文章能对同样没有学过编译原理的同学带来一些思考和启发~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,724评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,104评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,142评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,086评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,076评论 5 370
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,914评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,220评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,871评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,318评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,834评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,951评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,574评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,162评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,162评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,383评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,349评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,652评论 2 343

推荐阅读更多精彩内容