前语
kotlin-android-extensions 插件早在 2020 年就现已被宣布抛弃了,并且将在 Kotlin 1.8 中被正式移除:Discontinuing Kotlin synthetics for views
如上图示,移除 kotlin-android-extensions 的代码现已被 Merge 了,因而假如咱们需求升级到 Kotlin 1.8,则必需求移除 KAE
那么移除 kotlin-android-extensions 后,咱们该怎么搬迁呢?
搬迁计划
官方的搬迁计划如上所示,官方主张咱们老项目搬迁到 ViewBinding,新项目直接搬迁到 Jetpack Compose
关于新代码咱们当然能够这么做,可是关于很多存量代码,咱们该怎么搬迁?因为 KAE 简略易用的特性,它在项目中经常被很多运用,要搬迁如此多的存量代码,并不是一个简略的作业
存量代码搬迁计划
KAE 存量代码首要有如图3种搬迁办法
最简略也最直接的当然便是直接手动修正,这种办法的问题在于要搬迁的代码数量庞大,搬迁本钱高。一起手动搬迁简略犯错,也不简略回测,测验不能覆盖到一切的页面,导致引进线上 bug
第二个计划,是把 KAE 直接从 Kotlin 源码中抽取出来独自保护,可是 KAE 中也很多依赖了 Kotlin 的源码,抽取本钱较高。一起 KAE 中很多运用了 Kotlin 编译器插件的 API,而这部分 API 并没有安稳,当 K2 编译器正式发布的时分很或许还会有较大的改动,而这也带来较高的保护本钱。
第三个计划便是本篇要重点介绍的 Kace
Kace 是什么?
Kace 即 kotlin-android-compatible-extensions,一个用于协助从 kotlin-android-extensions 无缝搬迁的结构
现在现已开源,开源地址可见:github.com/kanyun-inc/…
比较其它计划,Kace 首要有以下长处
- 接入方便,不需求手动修正旧代码,能够真实做到无缝搬迁
- 与 KAE 体现一致(都支撑 viewId 缓存,并在页面毁掉时铲除),不会引进预期外的 bug
- 一致搬迁,回测方便,假如存在问题时,应该是批量存在的,避免手动修正或许引进线上 bug 的问题
- 经过生成源码的办法兼容 KAE,保护本钱低
快速搬迁
运用 Kace 完成搬迁首要分为以下几步
1. 增加插件到 classpath
// 办法 1
// 传统办法,在根目录的 build.gradle.kts 中增加以下代码
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath("com.kanyun.kace:kace-gradle-plugin:1.0.0")
}
}
// 办法 2
// 引用插件新办法,在 settings.gradle.kts 中增加以下代码
pluginManagement {
repositories {
mavenCentral()
}
plugins {
id("com.kanyun.kace") version "1.0.0" apply false
}
}
2. 应用插件
移除kotlin-android-extensions
插件,并增加以下代码
plugins {
id("com.kanyun.kace")
id("kotlin-parcelize") // 可选,当运用了`@Parcelize`注解时需求增加
}
3. 装备插件(可选)
默许情况下 Kace 会解析模块内的每个 layout 并生成代码,用户也能够自定义需求解析的 layout
kace {
whiteList = listOf() // 当 whiteList 不为空时,只要 whiteList 中的 layout 才会被解析
blackList = listOf("activity_main.xml") // 当 blackList 不为空时,blackList 中的 layout 不会被解析
}
经过以上几步,搬迁就彻底啦~
支撑的类型
如上所示,Kace 现在支撑了以上四种最常用的类型,其他 kotlin-android-extensions 支撑的类型如 android.app.Fragment, android.app.Dialog, kotlinx.android.extensions.LayoutContainer 等,因为被抛弃或许运用较少,Kace 现在没有做支撑
版别兼容
Kotlin | AGP | Gradle | |
---|---|---|---|
最低支撑版别 | 1.7.0 | 4.2.0 | 6.7.1 |
因为 Kace 的方针是协助开发者更方便地搬迁到 Kotlin 1.8,因而 Kotlin 最低支撑版别比较高
原理解析:前置常识
编译器插件是什么?
Kotlin 的编译进程,简略来说便是将 Kotlin 源代码编译成方针产品的进程,具体步骤如下图所示:
Kotlin 编译器插件,经过运用编译进程中供给的各种Hook时机,让咱们能够在编译进程中插入自己的逻辑,以达到修正编译产品的意图。比方咱们能够经过 IrGenerationExtension 来修正 IR 的生成,能够经过 ClassBuilderInterceptorExtension 修正字节码生成逻辑
Kotlin 编译器插件能够分为 Gradle 插件,编译器插件,IDE 插件三部分,如下图所示
kotlin-android-extensions 是怎么完成的
咱们知道,KAE 是一个 Kotlin 编译器插件,当然也能够分为 Gradle 插件,编译器插件,IDE 插件三部分。咱们这儿只剖析 Gradle 插件与编译器插件的源码,它们的具体结构如下:
-
AndroidExtensionsSubpluginIndicator
是KAE
插件的进口 -
AndroidSubplugin
用于装备传递给编译器插件的参数 -
AndroidCommandLineProcessor
用于接纳编译器插件的参数 -
AndroidComponentRegistrar
用于注册如图的各种Extension
关于更细节的剖析能够参阅:kotlin-android-extensions 插件到底是怎么完成的?
总的来说,其实 KAE 首要做了两件事
- KAE 会将 viewId 转化为 findViewByIdCached 办法调用
- KAE 会在页面关闭时铲除 viewId cache
那么咱们要无缝搬迁,就也要完成相同的效果
Kace 原理解析
第一次测验
咱们首要想到的是解析 layout 主动生成扩展特点,如下图所示
// 生成的代码
val AndroidExtensions.button1
get() = findViewByIdCached<Button>(R.id.button1)
val AndroidExtensions.buttion2
get() = findViewByIdCached(R.id.button1)
// 给 Activity 增加 AndroidExtensions 接口
class MainActivity : AppCompatActivity(), AndroidExtensions {
private val androidExtensionImpl by lazy { AndroidExtensionsImpl() }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycle.addObserver(androidExtensionImpl)
}
override fun <T : View?> findViewByIdCached(owner: AndroidExtensionsBase, id: Int): T {
return androidExtensionImpl.findViewByIdCached(id)
}
}
如上所示,首要做了这么几件事:
- 经过 gradle 插件,主动解析 layout 生成
AndroidExtensions
接口的扩展特点 - 给 Activity 增加
AndroidExtensions
接口 - 因为需求支撑缓存,因而也需求增加一个大局的变量:
androidExtensionImpl
- 因为需求在页面关闭时铲除缓存,因而也需求增加
lifecycle Observer
- 重写
findViewByIdCached
办法,将具体作业委托给AndroidExtensionsImpl
经过以上步骤,其实 KAE 的功用现已完成了,咱们能够在 Activity 中经过button1
,button2
等 viewId 获取对应的 View
可是这样仍是太麻烦了,修正一个页面需求增加这么多代码,还能再优化吗?
第二次测验
private inline val AndroidExtensions.button1
get() = findViewByIdCached<Button>(this, R.id.button1)
val AndroidExtensions.buttion2
get() = findViewByIdCached(this, R.id.button1)
class MainActivity : AppCompatActivity(), AndroidExtensions by AndroidExtensionsImpl() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
}
- 咱们经过委托简化了代码调用,只需求增加一行
AndroidExtensions by AndroidExtensionsImpl()
就能够完成搬迁 - 咱们不需求在初始化的时分手动增加
lifecycle observer
,这是因为咱们在调用findViewByIdCached
办法时会将this
传递曩昔,因而能够在第一次调用时初始化,主动增加lifecycle observer
能够看出,现在现已比较简练了,只需求增加一行代码就能够完成搬迁,但假如项目中有几百个页面运用了 KAE 的话,改起来仍是有点苦楚的,现在还不能算是真实的无缝搬迁
那么还能再优化吗?
第三次测验
第3次测验便是 Kace 的终究计划,结构如图所示
下面咱们就来介绍一下
kace-compiler 完成
kace-compiler 是一个 Kotlin 编译器插件,它的效果是给方针类型(Activity 或许 Fragment)主动增加接口与完成
如上所示,kace-compiler 的效果便是经过KaceSyntheticResolveExtension
扩展增加接口,以及KaceIrGenerationExtension
扩展增加完成
处理后的代码如下所示:
class MainActivity : AppCompatActivity(), AndroidExtensions {
private val $$androidExtensionImpl by lazy { AndroidExtensionsImpl() }
override fun <T : View?> findViewByIdCached(owner: AndroidExtensionsBase, id: Int): T {
return $$androidExtensionImpl.findViewByIdCached(id)
}
}
你或许还记得,前面说过因为编译器插件 API 还没有安稳,因而将 KAE 抽取出来独立保护本钱较高,那么咱们这儿为什么还运用了编译器插件呢?
这是因为咱们这儿运用的编译器插件是比较少的,生成的代码也很简略,将来保护起来并不复杂,可是能够大幅的降低搬迁本钱,完成真实的无缝搬迁
kace-gradle-plugin 生成代码
kace-gradle-plugin 的首要效果便是解析 layout 然后生成代码,生成的代码如下所示
package kotlinx.android.synthetic.debug.activity_main
private inline val AndroidExtensionsBase.button1
get() = findViewByIdCached<android.widget.Button>(this, R.id.button1)
internal inline val Activity.button1
get() = (this as AndroidExtensionsBase).button1
internal inline val Fragment.button1
get() = (this as AndroidExtensionsBase).button1
package kotlinx.android.synthetic.main.activity_main.view
internal inline val View.button1
get() = findViewById<android.widget.Button>(R.id.button1)
- 给 Activity, Fragment, View 等类型增加扩展特点
- 给 View 增加的扩展特点现在不支撑缓存,而是直接经过
finidViewById
完成 - 支撑根据不同的
variant
,生成不同的package
的代码,比方debug
Kace 功能优化
清晰输入输出
前面介绍了 kace-gradle-plugin 的首要效果便是解析 layout 然后生成代码,可是关于一个比较大的模块,layout 或许有几百个,假如每次编译时都要运转这个 Task,会带来一定的功能损耗
抱负情况下,在输入输出没有发生变化的情况下,应该越过这个 Task
比方 Gradle 中内置的 JavaCompilerTask,在源码与 jdk 版别没有发生变化的时分,会主动越过(标记为 up-to-date)
Gradle 需求咱们清晰 Task 的输入与输出是什么,这样它才干决议是否能够主动越过这个Task,如下所示:
abstract class KaceGenerateTask : DefaultTask() {
@get:Internal
val layoutDirs: ConfigurableFileCollection = project.files()
@get:Incremental
@get:InputFiles
@get:PathSensitive(PathSensitivity.RELATIVE)
internal open val androidLayoutResources: FileCollection = layoutDirs
.asFileTree
.matching { patternFilterable ->
patternFilterable.include("**/*.xml")
}
@get:Input
abstract val layoutVariantMap: MapProperty<String, String>
@get:Input
abstract val namespace: Property<String>
@get:OutputDirectory
abstract val sourceOutputDir: DirectoryProperty
}
如上所示,经过注解的办法清晰了 Task 的输入输出,在输入与输出都没有发生改动的时分,该 Task 会被标记为 up-to-date ,经过编译避免的办法提高编译功能
并行 Task
KaceGenerateTask
的首要效果其实便是解析 layout 然后生成代码,每个 layout 都是相互独立的,在这种情况下就特别适合运用并行 Task
要完成并行 Task,首要要将 Task 转化为 Worker API
abstract class KaceGenerateAction : WorkAction<KaceGenerateAction.Parameters> {
interface Parameters : WorkParameters {
val destDir: DirectoryProperty
val layoutFile: RegularFileProperty
val variantName: Property<String>
val namespace: Property<String>
}
override fun execute() {
val item = LayoutItem(
parameters.destDir.get().asFile,
parameters.layoutFile.get().asFile,
parameters.variantName.get()
)
val namespace = parameters.namespace.get()
val file = item.layoutFile
val layoutNodeItems = parseXml(saxParser, file, logger)
writeActivityFragmentExtension(layoutNodeItems, item, namespace)
writeViewExtension(layoutNodeItems, item, namespace)
}
}
- 第一步:首要咱们需求定义一个接口来表明每个
Action
需求的参数,即KaceGenerateAction.Parameters
- 第二步:您需求将自定义
Task
中为每个独自文件执行作业的部分重构为独自的类,即KaceGenerateAction
- 第三步:您应该重构自定义
Task
类以将作业提交给WorkerExecutor
,而不是自己完成作业
接下来便是将KaceGenerateAction
提交给WorkerExector
abstract class KaceGenerateTask : DefaultTask() {
@get:Inject
abstract val workerExecutor: WorkerExecutor
@TaskAction
fun action(inputChanges: InputChanges) {
val workQueue = workerExecutor.noIsolation()
// ...
changedLayoutItemList.forEach { item ->
workQueue.submit(KaceGenerateAction::class.java) { parameters ->
parameters.destDir.set(destDir)
parameters.layoutFile.set(item.layoutFile)
parameters.variantName.set(item.variantName)
parameters.namespace.set(namespace)
}
}
workQueue.await() // 等待一切 Action 完成,计算耗时
val duration = System.currentTimeMillis() - startTime
}
}
- 您需求拥有
WorkerExecutor
服务才干提交Action
。这儿咱们增加了一个抽象的workerExecutor
并增加注解,Gradle
将在运转时注入服务 - 在提交
Action
之前,咱们需求经过不同的隔离形式获取WorkQueue
,这儿运用的是线程隔离形式 - 提交
Action
时,指定Action
完成,在这种情况下调用KaceGenerateAction
并装备其参数
经过测验,在一个包括 500 个 layout 的模块中,在开启并行 Task 前全量编译耗时约 4 秒,而开启后全量编译耗时减少到 2 秒左右,能够有 100% 左右的提升
支撑增量编译
还有一种常见的场景,当咱们只修正了一个 layout 时,假如模块内的一切 layout 都需求从头解析并生成代码,也是非常浪费功能的
抱负情况下,应该只需求从头解析与处理咱们修正的 layout 就行了,Gradle 同样供给了 API 供咱们完成增量编译
abstract class KaceGenerateTask : DefaultTask() {
@get:Incremental
@get:InputFiles
@get:PathSensitive(PathSensitivity.RELATIVE)
internal open val androidLayoutResources: FileCollection = layoutDirs
.asFileTree
.matching { patternFilterable ->
patternFilterable.include("**/*.xml")
}
@TaskAction
fun action(inputChanges: InputChanges) {
val changeFiles = getChangedFiles(inputChanges, androidLayoutResources)
// ...
}
private fun getChangedFiles(
inputChanges: InputChanges,
layoutResources: FileCollection
) = if (!inputChanges.isIncremental) {
ChangedFiles.Unknown()
} else {
inputChanges.getFileChanges(layoutResources)
.fold(mutableListOf<File>() to mutableListOf<File>()) { (modified, removed), item ->
when (item.changeType) {
ChangeType.ADDED, ChangeType.MODIFIED -> modified.add(item.file)
ChangeType.REMOVED -> removed.add(item.file)
else -> Unit
}
modified to removed
}.run {
ChangedFiles.Known(first, second)
}
}
}
经过以下步骤,就能够完成增量编译
-
androidLayoutResources
运用@Incremental
注解标识,表明支撑增量处理的输入 - 给
TaskAction
办法增加inputChange
参数 - 经过
inputChanges
办法获取输入中发生了更改的文件,假如发生了更改则从头处理,假如被删去了则同样删去方针目录中的文件,没有发生更改的文件则不处理
经过支撑增量编译,当只修正或许增加一个 layout 时,增量编译耗时能够减少到 8ms 左右,大幅减少了编译耗时
总结
本文首要介绍了怎么运用 Kace ,以及 Kace 到底是怎么完成的,假如有任何问题,欢迎提出 Issue,假如对你有所协助,欢迎点赞收藏 Star ~
开源地址
github.com/kanyun-inc/…