翻译:Android LightClasses
Android light classes
注意:Android light classes 与 “Kotlin light classes” 只有微弱的关系(见 kotlin-light-classes.md)。这两种机制都借用了"LightElement “的名字,它是 PsiElement 的一个实现类(但不是由 PsiParser 创建的), 但实现方式非常不同。
Background
Android 开发中一个常见模式是通过在构建时生成的 class 向代码暴露各种 assets 或 resources,这个想法从 Android 的早期就开始使用,最初的aapt
工具根据res
目录的内容生成 App 的 R
类,它后来被 Data Binding
和 View Binding
所采用,并被考虑用于未来的更多库。在通常情况下,build-time component 是在 Android Gradle Plugin 中实现的,但它也可以是 apt 或单独的 Gradle plugin。
从 IDE 的角度来看,这种方法是有问题的,因为在构建时产生了额外的代码,直到在这些代码被写入磁盘之前,IDE 很多的标准机制,如 code completion 等并没有意识到生成的类。这意味着如果没有 IDE 的额外支持,这些功能在第一次构建之前是无法使用的。即使在第一次构建之后,额外的修改也不会反映到生成的代码中,直到下一次构建。考虑到目前(甚至是增量的)构建的延迟,这不是一个好的用户体验。
这就是为什么 Android Studio 试图实时模拟已知的代码生成器,因为用户修改了他们的输入。例如,它将维护一个所有已知资源(通过解析 XML 文件)的最新数据,并使用它来创建 “fake” class,将其直接注入到 IDE 的编辑器机制中,如代码完成、引用解析等。
Limitations
为了让 code generator 生成合适的 light class,必须满足下面的条件:
- IDE 需要理解生成类的 API,这意味着生成类、方法和字段的逻辑必须相对简单,因为每当代码改动,IDE 便会重复这个过程。在实践中,这也意味着它不能改变的太频繁
- IDE 需要一种高效的方法来从输入文件中计算出它所需要的全部信息,并保证这些信息是最新的 Note: 这可能比完整的代码生成器所需要的信息要少。例如 IDE 只需要知道类的 API,而不是它的方法体或字段值
- IDE 需要知道这个功能是否应该被启用. 例如,
R
classes 是为全部 Android modules 生成的, 但是 Data Binding 应该只在build.gradle
中开启了这个功能的模块生效. 这个信息通常存储在 在 Gradle module 中,并在 IDE sync 时获得
Implementation details
实现基于 Light Class 的功能意味着 “欺骗” IDE,使其相信某些类的存在,尽管它们并没有在项目的任何源文件中定义。这意味着需要使用一些 IntelliJ 的 api 来扩展一些核心编辑器机制,支持 build time 的代码生成器意味着要实现以下组件:
Model
一种快速确定哪些类应该被提供的机制。其逻辑是基于特定功能的,下面的这些组件是我们随着时间推移发现的 IDE 的可扩展点,这些扩展点必须要实现才能使轻量级类发挥作用。但要知道在这些扩展点中放什么逻辑,IDE 需要对代码生成器的语义和影响生成代码的相关输入文件有一个了解。
对 Model 代码的唯一要求是,下面描述的扩展点需要一种方法来调用它,这意味着通常它最终是一个 IntelliJ module service. 在 “R” 类的情况下,我们使用整个 IDE 的资源库子系统(“ResourceRepositoryManager”),这些资源库被 “LightResourceClassService” 的每个 per-build-system 实现所使用。对于 Data Binding,我们有一个自定义的 IntelliJ 索引(BindingXmlIndex
),它是由ModuleDataBinding
使用。
对于不使用相关代码生成器的模块,避免不必要的工作很重要。另一个需要考虑的方面是内存的使用,避免内存泄漏,并在模块或项目关闭时正确处理相关的 Model 信息。
PsiClass
Implementation
生成的类的表示,将被传递给 IntelliJ platform API,以启用编辑器的功能,如 code completion 等。这通常是一个 AndroidLightClassBase
的子类,并拥有 getFields()
, getMethods()
, getInnerClasses()
及相关方法的正确实现
PsiElementFinder
JavaPsiFacade
使用的扩展点,根据其 fully qualified name 找到 class & package, 这最终被引用解析代码所调用,这意味着对生成的类的引用不会被高亮显示为红色。
ResolveScopeEnlarger
考虑到要用于参考解析和代码完成,新生成的 class 必须在正确的范围内,通常是它们所使用的文件的解析范围。Light Class 存在于 Light Virtual File System 中,这意味着它们不是项目的一部分。为了解决这个问题,我们提供了ResolveScopeEnlarger
和KotlinResolveScopeEnlarger
的实现
PsiShortNamesCache
code completion 时用于 unqualified class name 和 suggesting import 的扩展。为保证他们的行为正确,需要以 Light Class 的方式被实现。
GotoDeclarationHandler
or getNavigationElement
Light classes 和它们的成员没有相应的源代码,所以在引用它们的代码上调用 “go to declaration”,默认会显示一个小气球,说 “cannot find destination” 或类似的话。通常情况下,这些 Light Element 代表项目中的一些其他文件,所以我们需要覆盖 “go to declaration” 来打开这些其他文件。这可以通过覆盖 light class/field/method 上的 getNaviationElement
来实现在代码编辑器中跳转到一个PsiElement
.
如果上面的不能满足,也可以通过提供一个自定义 GotoDeclarationHandler
扩展,来得到更多的上下文或者完全不同的行为。
Modifying AGP model
为了避免 Light Class 的重复定义(可能其中一些已经过期),因此在构建时产生的源不能被 IDE 作为项目源包含。这意味着它们要么根本不在 source set module 中,要么在同步时根据 model 中的其他字段排除
其实读到这里我想到了,或许 ksp 不自动添加 sourceset 的原因就是因为可能之后它要结合 IDE 去生成一些 Light Class?或许这是一盘大棋。