购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

第1章
元编程概述

本章主要介绍元编程的需求背景、基本概念和学习方法,这是本书后续内容的重要基础。同时,由于元编程与Java及Kotlin的编译器、IntelliJ平台的关系非常紧密,因此本章也会介绍相关项目的环境配置,这将对读者深入理解并掌握元编程的方法和技巧有非常大的帮助。

1.1 元编程的需求背景

我们在编写程序时经常会遇到一个问题—重复,而工程师最不能忍受的就是重复。重复意味着低效,也意味着低级。我们造的每一个“轮子”在某种意义上都是为了解决重复的问题。业内也有一句名言:“不要重复你自己(Don't repeat yourself)。”

然而在实际的业务场景中,我们经常会见到大量重复或者相似的模板代码,这些代码很多时候难以使用编程语言的基本特性进行抽象,因此我们不得不忍受它们的存在。

Java的Getter方法和Setter方法就是很典型的例子,如代码清单1-1所示。

代码清单1-1 Java的Getter方法和Setter方法

幸运的是,Kotlin把属性作为正式的特性提供给开发者,使得开发者终于不用忍受充斥着Getter和Setter的模板代码了,如代码清单1-2所示。

代码清单1-2 Kotlin的属性

不过,不是所有模板代码问题都能通过添加语法特性解决。

业务代码中最常见的网络请求就是高度模板化的代码。不同的业务接口虽然各不相同,但网络请求本身的代码极为相似。以OkHttp为例,如代码清单1-3所示。

代码清单1-3 使用OkHttp发送网络请求

我们定义了两个函数getUser和getRepository,分别用于请求GitHub的API以获取用户和仓库的相关信息。这两个函数除了参数、返回值和请求的URL不同以外,剩下的代码完全相同,而这部分相同的代码才是这两个函数的主体内容。如果调用者希望接口同时提供异步回调和协程的版本,情况只会更糟糕。异步回调和协程版本的接口函数如代码清单1-4所示。

代码清单1-4 异步回调和协程版本的接口函数

在实际的业务场景中,请求几十甚至上百个接口是非常常见的事情。可以想象,重复的接口代码实现将会给项目带来巨大的维护成本。

解决此类问题最常用的手段就是元编程。Kotlin提供了非常丰富的元编程手段,包括运行时反射(Kotlin JVM)、编译时符号处理等。

Retrofit就是一个充分运用运行时反射能力来简化HTTP接口代码的框架。它可以以非常优雅的方式解决代码清单1-3和代码清单1-4中存在的问题,如代码清单1-5所示。

代码清单1-5 使用Retrofit定义HTTP接口函数

3.3节将对Retrofit的工作机制进行剖析。

1.2 元编程的基本概念

元编程这个词看上去有些晦涩。元字的意思较多,包含开端、根源等诸多含义。本节将简单介绍元编程的定义和分类。

1.2.1 元编程的定义

元编程(Meta Programming) 就是以程序为数据的编程。这意味着元编程往往是以访问和修改程序本身为目的的。其中,编写元程序的语言被称为 元语言 ,被操作的语言则被称为 目标语言

元编程与普通编程的区别在于处理的对象不同。大家可能已经有过很多元编程的实践,例如Java/Kotlin的反射、APT(Annotation Processing Tool,注解处理器)、代码规范检查工具等。

元编程的概念不吓人,但也不是没有门槛。不管是什么类型的程序,程序设计者都需要在充分了解其业务需求背景和数据结构之后才能够将程序逐步落地。元编程也是如此,我们需要对目标语言的元数据有一定的认识,才能够设计出符合预期的元程序。

Kotlin是一门支持多平台的编程语言,因而相关的元数据种类也较为丰富。本书将在第2章专门对Kotlin的元数据进行介绍,这将是我们后续实践Kotlin元编程的重要基础。

1.2.2 元编程的分类

Kotlin支持两种不同阶段的元编程,即运行时元编程和编译时元编程。

运行时元编程通常是指运行时反射。我们可以使用反射在目标程序运行时修改其程序结构的信息,控制函数的调用,实现一定程度上的动态能力。在Kotlin中,我们既可以使用Java反射,也可以使用Kotlin反射,反射相关的内容将在第3章详细介绍。

编译时元编程涵盖了程序从编写到编译的所有阶段。按照目标程序的形式,又可以将编译时元编程分为源代码处理、编译中间产物处理和编译产物处理。

Kotlin元编程的分类如图1-1所示。

图1-1 Kotlin元编程的分类

绝大多数情况下我们编写元程序是为了生成源代码。本书第4章会详细介绍源代码生成相关的内容,第5章介绍的符号处理器通常也是以生成源代码为目的的。

除了生成源代码以外,我们还可以通过编写元程序来实现对源代码的分析。有关程序静态分析的内容将在第6章展开讨论。

Kotlin编译器在编译过程中会产生一些中间代码,想要实现对中间代码的处理就需要借助编译器提供的扩展能力。我们将在第7章详细介绍Kotlin编译器插件(Kotlin Compiler Plugin),并在第9章对Jetpack Compose的编译器插件实现进行详细的剖析。

Kotlin对于符号处理的支持包括KAPT(Kotlin Annotation Processing Tool)和KSP(Kotlin Symbol Processing),二者均是基于Kotlin编译器提供的扩展能力实现的。我们将在第5章详细介绍Kotlin中与符号处理相关的内容,并在7.5节介绍符号处理器的实现原理。

编译产物的处理与对应平台的相关性很大,因此本书不打算对其展开重点讨论。不过,我们还是会在第10章中以AtomicFU框架为例,介绍如何对Kotlin JVM和Kotlin JS的编译产物进行处理。

1.3 元编程的学习方法

学习从来就不是一件轻松的事情,元编程的学习尤其如此。本节将介绍一些元编程技术的学习心得和方法,帮助读者做好学习元编程技术的准备。

1.3.1 培养兴趣

相比编程语言的基本语法,元编程相关的技术往往更有难度。为什么呢?一方面,元编程需要开发者对目标语言本身以及编译原理有一定的了解;另一方面,元编程需要开发者有一定的开发经验积累,能够从复杂的业务场景中抽象出需要解决的问题。

不过,换个角度来看,元编程又很简单。正如前面提到的,元编程在多数情况下就是处理程序中的模板代码,目的是提升研发效率,因而元编程的需求往往来自研发团队自身。相比多变的产品需求,元编程的需求往往更简单、更纯粹。

不仅如此,元程序的编写过程中往往富有挑战,解决问题的过程中又充满乐趣。元编程还可以实现四两拨千斤的效果,给开发者带来很大的成就感和价值感。

因此,要学习元编程技术首先要做的就是充分调动自己的技术热情,不要有畏难情绪。

1.3.2 付诸行动

“纸上得来终觉浅,绝知此事要躬行。”我们学习编程技术,最忌讳的就是“看了就是会了”,学习元编程技术尤其如此。元数据通常都比较抽象,不容易凭直觉想象,如果不动手试验,有些情况是很难直接想到的。

例如我在准备9.5.3节的内容时,需要研究$changed的计算方法(参见代码清单9-87),其中涉及的条件非常烦琐、复杂,为了搞清楚各个分支的关系,就必须要构造各种用例来反复尝试。

1.3.3 善用工具

由于元数据比较抽象,因此合理地利用一些工具来帮助我们快速找到问题的本质也是非常重要的。

在众多工具中,对Kotlin元编程最有帮助的莫过于PsiViewer了。PsiViewer是一款可以将IntelliJ平台的PSI(Program Structure Interface,程序结构接口)可视化的IntelliJ插件。

接下来看一个具体的例子,如代码清单1-6所示。

代码清单1-6 GitUser类

比如,我们希望搞清楚id的类型Int是什么类型的节点,就可以通过PsiViewer非常方便地看到,如图1-2所示。

图1-2 使用PsiViewer查看Int节点

PsiViewer可以用来查看所有IntelliJ平台支持的编程语言,它最常用的应用场景实际上是IntelliJ插件的开发。Kotlin编译器使用了PSI作为Kotlin的抽象语法树,因此PsiViewer同样可以用于Kotlin编译器插件的开发。

说明

尚未正式发布的K2编译器(K2Compiler,Kotlin的新一代编译器)已经不再使用PSI,而是自研了一套专用的FIR(Front-end Intermediate Representation,前端中间表示)作为新的抽象语法树。

1.3.4 多读源代码

元编程技术的参考资料远不及入门教程多,甚至连官方文档也对此讳莫如深。其实原因说来也简单,元编程技术相对于其他技术而言还是太小众了。

资料的缺乏,特别是成体系的资料的缺乏,自然是我希望本书能够解决的问题之一。不过,纵然我能把我知道的内容完全呈现到纸面上,也还会受到个人技术水平和内容篇幅的限制,不能解决的问题仍然是大多数。

怎么办呢?

很简单,经常翻阅编译器甚至IntelliJ社区版的源代码,所有问题的答案基本上都可以在编译器源代码中找到。如果你想要成为元编程技术的高手,那一定要养成有问题翻阅源代码的习惯。我们将在1.4节介绍如何配置编译器以及IntelliJ社区版的源代码调试环境,强烈建议读者将环境配置好并时常翻阅源代码。

1.4 常用项目的调试环境配置

由于本书涉及非常多Java编译器、Kotlin编译器甚至IntelliJ IDEA相关的概念,配置好相关源代码的编译和调试环境会非常有帮助。当然,这不是必需的,本书绝大多数内容并不直接依赖这些环境。

1.4.1 Java编译器

在开始之前,请大家准备好一台硬盘剩余容量不小于10GB、内存不小于16GB的计算机。操作系统选择Linux、macOS、Windows均可,推荐使用Linux。下面我们以Ubuntu 20.04为例,介绍如何配置Java编译器的编译和调试环境。配置完成之后的JDK所在的目录如图1-3所示。

图1-3 配置完成之后的JDK所在的目录

首先从GitHub下载JDK源代码,如代码清单1-7所示。为了叙述方便,我们使用$ws表示工作目录。工作目录应尽量避免层级过深,目录名称应避免出现空格、非ASCII字符以及特殊字符,以免遇到部分工具出错的问题。

代码清单1-7 下载OpenJDK源代码

源代码比较大,下载可能需要几分钟时间。下载完成之后切换到我们需要阅读的代码版本,例如JDK 17,如代码清单1-8所示。

代码清单1-8 切换到JDK 17对应的分支

接下来需要检查一下当前环境是否已经满足编译JDK的要求,如代码清单1-9所示。

代码清单1-9 检查当前编译环境

如果当前环境中缺少某些依赖,configure会运行失败,并给出错误信息。读者可以按照错误信息结合自己的实际环境安装相应的依赖,并重新运行configure命令直到成功为止,如图1-4所示。

图1-4 bash configure运行成功之后的输出

接下来我们就可以编译自己的Java编译器和Java虚拟机了,如代码清单1-10所示。

代码清单1-10 编译JDK

编译后可以根据提示找到对应的Java可执行程序,读者可以试着用它编译并运行一段Java源代码。

Java编译器的源代码可以使用IntelliJ IDEA来阅读。为了方便配置工程,JDK源代码中提供了创建相应工程的配置文件的脚本。

代码清单1-11 创建IntelliJ IDEA工程的配置文件

这个脚本会依赖Apache Ant,如果运行时提示需要设置ANT_HOME,那么请先安装Ant。可以从https://ant.apache.org/bindownload.cgi直接下载Ant的压缩包,解压之后将环境变量ANT_HOME的值设置为Ant的根目录,再次运行idea.sh脚本即可。

工程文件生成之后,使用IntelliJ IDEA打开$ws/jdk目录,就可以阅读JDK的Java部分源代码了,如图1-5所示。

图1-5 使用IntelliJ IDEA阅读JDK源代码

Java编译器也是一个普通的Java程序,我们可以直接在IntelliJ IDEA中运行并调试它,如图1-6所示。

图1-6 单步调试Java编译器

提示

配置环境时,最容易出问题的就是C/C++编译器。为了避免麻烦,建议读者严格按照官方的要求安装相应的编译工具链,更多细节参见https://openjdk.org/groups/build/doc/building.html。

1.4.2 Kotlin编译器

本节我们将为大家介绍如何下载和配置Kotlin源代码的编译和阅读环境,其中包括Kotlin编译器、标准库、官方编译器插件、官方Gradle插件等。时常翻阅Kotlin源代码,对于理解本书的内容将会有非常大的帮助。

在下载和编译Kotlin源代码之前,请大家准备好一台硬盘剩余容量不小于20GB、内存不小于16GB的计算机。Kotlin源代码的编译对操作系统没有明确要求,不过实测搭载Apple Silicon芯片的macOS在编译Kotlin源代码时会遇到一些依赖问题,例如找不到对应的JDK版本等,因此建议使用搭载了X86芯片的计算机,以避免出现不必要的麻烦。

首先下载源代码。从https://github.com/JetBrains/kotlin下载Kotlin源代码到本地目录$ws/kotlin中,默认分支为master,如代码清单1-12所示。

代码清单1-12 下载Kotlin源代码

代码下载完以后,Windows用户需要添加如代码清单1-13所示的配置,以支持长路径。

代码清单1-13 配置git以支持长路径

由于我们只是希望阅读源代码,因此不需要考虑不同JDK版本兼容的问题。在local.properties中添加如代码清单1-14所示的配置,以使用JDK 1.8.0及以上版本完成编译。

代码清单1-14 使用JDK 1.8.0及以上版本完成编译

这样Kotlin源代码调试环境就配置完成了。读者可以使用IntelliJ IDEA打开Kotlin源代码的目录,导入相应的Gradle工程,并开始阅读源代码。

编译Kotlin源代码比较容易,直接运行对应的Gradle任务即可,如代码清单1-15所示。

代码清单1-15 将Kotlin编译器编译到dist/kotlinc中

编译之后,我们就可以在$ws/kotlin/dist/kotlinc目录下找到Kotlin编译器的可执行程序,如图1-7所示。

我们可以直接在命令行运行这些程序,也可以通过cli-runner模块来运行它们。cli-runner模块的入口如图1-8所示。

图1-7 编译后的Kotlin编译器的可执行程序

图1-8 cli-runner模块的入口

找到cli-runner模块中的Main类,直接运行它的main函数,我们会得到如图1-9所示的错误信息。

这是因为cli-runner只是一个简单的程序入口,并没有包含Kotlin编译器的具体实现。我们可以通过设置虚拟机参数kotlin.home来明确需要运行的Kotlin编译器的路径,如图1-10所示。

图1-9 直接运行cli-runner的main函数时输出的错误信息

图1-10 添加参数kotlin.home指向编译好的Kotlin编译器

这样我们就可以轻松实现单步调试Kotlin编译器了。如图1-11所示,直接运行Kotlin编译器相当于在命令行运行kotlinc,会进入交互式命令行,我们在其中输入一行Kotlin代码,然后交互式命令行会读取该输入并且调用eval函数对其进行求值。

图1-11 单步调试Kotlin编译器

顺带提一句,这里指向的Kotlin编译器目录也可以是从官方下载的版本,但需要本地源代码版本与下载的二进制版本相对应。

1.4.3 IntelliJ社区版

本节我们将为大家简单介绍如何编译和调试IntelliJ社区版的源代码。

在此之前,请大家准备好一台硬盘剩余容量不小于30GB、内存不小于16GB的计算机。IntelliJ社区版的源代码只涉及Java程序,本书撰写时使用的JDK版本为17,对操作系统没有明确要求。

首先从https://github.com/JetBrains/intellij-community下载IntelliJ社区版源代码到$ws/intellij-community目录中,切换到希望调试的分支,例如222.4345,这是IntelliJ IDEA 2022.2的一个稳定版本的分支,如代码清单1-16所示。

代码清单1-16 下载IntelliJ社区版源代码并切换到222.4345分支

如果需要阅读Android插件相关的源代码,可以接着通过如代码清单1-17所示的命令将其下载到$ws/intellij-community/android目录中,选择与IntelliJ社区版源代码相同的分支。这实际上也是Android Studio的核心源代码。

代码清单1-17 下载Android插件的源代码

下载完成之后,使用IntelliJ IDEA打开$ws/intellij-community目录时会同时加载android目录下的模块。

顺带提一句,Jetpack Compose的IntelliJ插件在$ws/intellij-community/android/compose-ide-plugin目录中。

至此,IntelliJ社区版源代码就下载完成了。

读者此时可以使用IntelliJ IDEA打开$ws/intellij-community目录,尝试阅读IntelliJ社区版源代码了。其中,Kotlin的IntelliJ插件的源代码在$ws/intellij-community/plugins/kotlin目录下。

源代码打开之后需要配置JDK才可以编译,对于222.4345版本的源代码,建议选择JDK 11。接下来在如图1-12所示的运行选项下拉列表中选择IDEA开始运行,经过一段时间的编译之后就会启动一个调试用的IntelliJ IDEA实例。

如果想要运行安装了Android插件的IntelliJ IDEA,需要选择IDEA with Android。不过在运行之前,需要先单击下拉菜单中的Edit Configurations选项,对IDEA with Android的配置进行修改,如图1-13所示,删除框中的Run Gradle task 'dependencies:setupAndroid-PluginRuntimeForIdea',并单击OK按钮保存修改。

图1-12 IntelliJ源代码的运行配置

图1-13 修改IDEA with Android的配置

保存之后再运行IDEA with Android,就会看到一个启用了Android插件的IntelliJ IDEA运行起来了,如图1-14所示。我们甚至可以单步调试这个程序,以了解IntelliJ IDEA内部的运行细节。

图1-14 运行IDEA with Android会同时安装Android插件

提示

IDEA with Android运行配置的问题已经在master分支上修复了,如果读者拉取的是比较新的代码版本,就不需要额外对运行配置进行修改了。

这里稍微说明一下源代码版本的选择。IntelliJ社区版和Android插件的源代码版必须保持一致,以避免出现不兼容的情况。另外,建议读者根据JetBrains官方发布的IntelliJ社区版的版本号选择稳定版本的源代码进行阅读和调试,最近的稳定版的版本号可以在JetBrains ToolBox中看到,如图1-15所示。

图1-15 在JetBrains ToolBox中查看IntelliJ的版本

如果直接使用开发中的版本,运行调试时难免会遇到问题。我在撰写本书时,曾直接拉取了master分支的最新代码,运行IDEA with Android之后发现Android插件没有被正常启用。通过分析日志发现,Android插件的配置文件引用了android-navigator模块的配置文件,却没有把android-navigator模块添加到Android插件的依赖中。Android插件加载失败的异常日志如代码清单1-18所示。

代码清单1-18 Android插件加载失败的异常日志

当然这个问题还是比较容易解决的,在Android插件模块的依赖中添加android-navigator模块即可。不过,为了避免不必要的麻烦,我们在阅读和调试开源项目源代码时,应当尽量选用稳定版本。

另外,本书第9章中会有阅读和调试Jetpack Compose的IntelliJ插件的需求,默认情况下运行IDEA with Android时不会安装Jetpack Compose插件,因此我们需要将Compose的IntelliJ插件添加到程序的依赖中。

首先单击菜单File中的Project Structure,单击弹出的对话框左侧边栏中的Modules,找到intellij.idea.community.main.android的依赖,单击+,选择3 Module Dependency,如图1-16所示。

图1-16 为intellij.idea.community.main.android添加依赖

在选择模块对话框中直接输入compose搜索compose-ide-plugin模块,如图1-17所示,选中并单击OK按钮进行确认。

图1-17 搜索compose-ide-plugin模块

再在Project Structure对话框中单击OK按钮,使得新增加的依赖生效。重新运行IDEA with Android,启动调试用的IntelliJ IDEA实例之后,我们就可以在其中找到Jetpack Compose插件了,如图1-18所示。

图1-18 成功添加Jetpack Compose插件的IntelliJ IDEA

1.4.4 Jetpack Compose编译器插件

Jetpack Compose属于AOSP(Android Open Source Project,Android开源项目)的一部分,其源代码的下载和配置方式与Android的系统源代码、Android Studio的源代码非常类似。

在开始之前,请大家准备好一台硬盘剩余容量不小于50GB、内存不小于16GB的计算机。操作系统推荐使用Linux、macOS。下面我们以Ubuntu 20.04为例,介绍Jetpack Compose的编译和调试环境的配置。

下载AOSP的源代码时需要使用repo命令,因此我们需要先将repo命令下载下来,放到系统路径中。repo命令的安装方法如代码清单1-19所示。

代码清单1-19 安装repo命令

这里我们将repo命令下载到用户目录下的bin目录中,接着将bin目录添加到系统路径中,如代码清单1-20所示。

代码清单1-20 将bin目录添加到系统路径中

repo命令本质上就是一个Python脚本,目前repo命令支持的最低Python版本为3.6,因此需要强制使用Python 3来运行repo命令,如代码清单1-21所示。

代码清单1-21 强制使用Python 3运行repo命令

我们也可以把代码清单1-20和代码清单1-21中的两条命令写入~/.bash_profile中(如果使用zsh,则写入~/.zshrc中),方便后续使用。

重启命令行,或者执行source~/.bash_profile(如果使用zsh,则执行source~/.zshrc)以使新增的配置生效。

接下来就是枯燥的源代码下载工作了,如代码清单1-22所示。

代码清单1-22 下载androidx-main分支的代码

为了确保git能够识别大量的文件更改情况,还需要添加如代码清单1-23所示的配置。

代码清单1-23 提高git识别文件更改的数量限制

接下来就可以运行如代码清单1-24所示的命令,使用Android Studio打开Jetpack Compose的源代码了。

代码清单1-24 运行Gradle任务启动Android Studio

任务初次运行时会下载相关依赖和Android Studio的可执行程序,稍等片刻就可以看到如图1-19所示的效果。

图1-19 使用Android Studio打开Jetpack Compose工程

我们也可以找到Compose编译器插件的单元测试进行调试,如图1-20所示。

提示

Jetpack Compose源代码的编译和调试的详细说明请参考官方文档:https://android.googlesource.com/platform/frameworks/support/+/refs/heads/androidx-main/compose/README.md。

图1-20 单步调试Compose的编译器插件

1.5 本章小结

本章介绍了元编程的需求背景、基本概念,也简单分享了一些元编程的学习方法。元编程在不同的编程语言中都有不同的实现,虽然风格迥异,但需求背景和实现效果殊途同归。

元编程相关的技术通常是编程语言学习进阶的关键一环,本书即将陪同大家一起踏上这充满挑战和乐趣的旅途。 8U+Pk97dwzheKJuhw+YaaEdhSwV26PSA2JEHNOLS/Bmc1/Y4hXoQK0c4C19LHaG1

点击中间区域
呼出菜单
上一章
目录
下一章
×

打开