⚠️本文为稀土技术社区首发签约文章,14天内制止转载,14天后未获授权制止转载,侵权必究!
前语
kotlin-android-extensions 插件是 Kotlin 官方供给的一个编译器插件,用于替换 findViewById 模板代码,降低开发本钱
尽管 kotlin-android-extensions 现在已经过期了,但比起其他替换 findViewById 的方案,比方第三方的 ButterKnife 与官方现在引荐的 ViewBinding
kotlin-android-extensions 还是有着一个明显的优点的:极其简练的 API,KAE
方案比起其他方案写起来更加简便,这是怎样完成的呢?咱们一起来看下
原理浅析
当咱们接入KAE
后就能够通过以下办法直接获取 View
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewToShowText.text = "Hello"
}
}
而它的原理也很简略,KAE
插件将上面这段代码转换成了如下代码
public final class MainActivity extends AppCompatActivity {
private HashMap _$_findViewCache;
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.setContentView(1300023);
TextView var10000 = (TextView)this._$_findCachedViewById(id.textView);
var10000.setText((CharSequence)"Hello");
}
public View _$_findCachedViewById(int var1) {
if (this._$_findViewCache == null) {
this._$_findViewCache = new HashMap();
}
View var2 = (View)this._$_findViewCache.get(var1);
if (var2 == null) {
var2 = this.findViewById(var1);
this._$_findViewCache.put(var1, var2);
}
return var2;
}
public void _$_clearFindViewByIdCache() {
if (this._$_findViewCache != null) {
this._$_findViewCache.clear();
}
}
}
能够看到,实际上 KAE
插件会帮咱们生成一个 _$_findCachedViewById()
函数,在这个函数中首先会测验从一个 HashMap 中获取传入的资源 id 参数所对应的控件实例缓存,假如还没有缓存的话,就调用findViewById()
函数来查找控件实例,并写入 HashMap 缓存傍边。这样当下次再获取相同控件实例的话,就能够直接从 HashMap 缓存中获取了。
当然KAE
也帮咱们生成了_$_clearFindViewByIdCache()
函数,不过在 Activity 中没有调用,在 Fragment 的 onDestroyView 办法中会被调用到
整体结构
在了解了KAE
插件的简略原理后,咱们一步一步来看一下它是怎样完成的,首先来看一下整体结构
KAE
插件能够分为 Gradle 插件,编译器插件,IDE 插件三部分,如下图所示
咱们今天只剖析 Gradle 插件与编译器插件的源码,它们的详细结构如下:
-
AndroidExtensionsSubpluginIndicator
是KAE
插件的进口 -
AndroidSubplugin
用于装备传递给编译器插件的参数 -
AndroidCommandLineProcessor
用于接纳编译器插件的参数 -
AndroidComponentRegistrar
用于注册如图的各种Extension
源码剖析
插件进口
当咱们查看 kotlin-gradle-plugin 的源码,能够看到 kotlin-android-extensions.properties 文件,这便是插件的进口
implementation-class=org.jetbrains.kotlin.gradle.internal.AndroidExtensionsSubpluginIndicator
接下来咱们看一下进口类做了什么工作
class AndroidExtensionsSubpluginIndicator @Inject internal constructor(private val registry: ToolingModelBuilderRegistry) :
Plugin<Project> {
override fun apply(project: Project) {
project.extensions.create("androidExtensions", AndroidExtensionsExtension::class.java)
addAndroidExtensionsRuntime(project)
project.plugins.apply(AndroidSubplugin::class.java)
}
private fun addAndroidExtensionsRuntime(project: Project) {
project.configurations.all { configuration ->
val name = configuration.name
if (name != "implementation") return@all
configuration.dependencies.add(
project.dependencies.create(
"org.jetbrains.kotlin:kotlin-android-extensions-runtime:$kotlinPluginVersion"
)
)
}
}
}
open class AndroidExtensionsExtension {
open var isExperimental: Boolean = false
open var features: Set<String> = AndroidExtensionsFeature.values().mapTo(mutableSetOf()) { it.featureName }
open var defaultCacheImplementation: CacheImplementation = CacheImplementation.HASH_MAP
}
AndroidExtensionsSubpluginIndicator
中首要做了这么几件事
- 创立
androidExtensions
装备,能够看出其间能够装备是否敞开实验特性,启用的feature
(因为插件中包括views
与parcelize
两个功能),viewId
缓存的详细完成(是hashMap
还是sparseArray
) - 自动增加
kotlin-android-extensions-runtime
依靠,这样就不用在接入了插件之后,再手动增加依靠了,这种写法能够学习一下 - 装备
AndroidSubplugin
插件,开端装备给编译器插件的传参
装备编译器插件传参
class AndroidSubplugin : KotlinCompilerPluginSupportPlugin {
// 1. 是否敞开编译器插件
override fun isApplicable(kotlinCompilation: KotlinCompilation<*>): Boolean {
if (kotlinCompilation !is KotlinJvmAndroidCompilation)
return false
// ...
return true
}
// 2. 传递给编译器插件的参数
override fun applyToCompilation(
kotlinCompilation: KotlinCompilation<*>
): Provider<List<SubpluginOption>> {
//...
val pluginOptions = arrayListOf<SubpluginOption>()
pluginOptions += SubpluginOption("features",
AndroidExtensionsFeature.parseFeatures(androidExtensionsExtension.features).joinToString(",") { it.featureName })
fun addVariant(sourceSet: AndroidSourceSet) {
val optionValue = lazy {
sourceSet.name + ';' + sourceSet.res.srcDirs.joinToString(";") { it.absolutePath }
}
pluginOptions += CompositeSubpluginOption(
"variant", optionValue, listOf(
SubpluginOption("sourceSetName", sourceSet.name),
//use the INTERNAL option kind since the resources are tracked as sources (see below)
FilesSubpluginOption("resDirs", project.files(Callable { sourceSet.res.srcDirs }))
)
)
kotlinCompilation.compileKotlinTaskProvider.configure {
it.androidLayoutResourceFiles.from(
sourceSet.res.sourceDirectoryTrees.layoutDirectories
)
}
}
addVariant(mainSourceSet)
androidExtension.productFlavors.configureEach { flavor ->
androidExtension.sourceSets.findByName(flavor.name)?.let {
addVariant(it)
}
}
return project.provider { wrapPluginOptions(pluginOptions, "configuration") }
}
// 3. 界说编译器插件的仅有 id,需求与后面编译器插件中界说的 pluginId 保持一致
override fun getCompilerPluginId() = "org.jetbrains.kotlin.android"
// 4. 界说编译器插件的 `Maven` 坐标信息,便于编译器下载它
override fun getPluginArtifact(): SubpluginArtifact =
JetBrainsSubpluginArtifact(artifactId = "kotlin-android-extensions")
}
首要也是重写以上4个函数,各自的功能在文中都有注释,其间首要需求注意applyToCompilation
办法,咱们传递了features
,variant
等参数给编译器插件
variant
的首要作用是为不同 buildType
,productFlavor
目录的 layout 文件生成不同的包名
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.debug.activity_debug.*
import kotlinx.android.synthetic.demo.activity_demo.*
比方如上代码,activity_debug
文件放在debug
目录下,而activiyt_demo
文件则放在demo
这个flavor
目录下,这种情况下它们的包名是不同的
编译器插件接纳参数
class AndroidCommandLineProcessor : CommandLineProcessor {
override val pluginId: String = ANDROID_COMPILER_PLUGIN_ID
override val pluginOptions: Collection<AbstractCliOption>
= listOf(VARIANT_OPTION, PACKAGE_OPTION, EXPERIMENTAL_OPTION, DEFAULT_CACHE_IMPL_OPTION, CONFIGURATION, FEATURES_OPTION)
override fun processOption(option: AbstractCliOption, value: String, configuration: CompilerConfiguration) {
when (option) {
VARIANT_OPTION -> configuration.appendList(AndroidConfigurationKeys.VARIANT, value)
PACKAGE_OPTION -> configuration.put(AndroidConfigurationKeys.PACKAGE, value)
EXPERIMENTAL_OPTION -> configuration.put(AndroidConfigurationKeys.EXPERIMENTAL, value)
DEFAULT_CACHE_IMPL_OPTION -> configuration.put(AndroidConfigurationKeys.DEFAULT_CACHE_IMPL, value)
else -> throw CliOptionProcessingException("Unknown option: ${option.optionName}")
}
}
}
这段代码很简略,首要是解析variant
,包名,是否敞开实验特性,缓存完成办法这几个参数
注册各种Extension
接下来到了编译器插件的中心部分,通过注册各种Extension
的办法修正编译器的产物
class AndroidComponentRegistrar : ComponentRegistrar {
companion object {
fun registerViewExtensions(configuration: CompilerConfiguration, isExperimental: Boolean, project: MockProject) {
ExpressionCodegenExtension.registerExtension(project,
CliAndroidExtensionsExpressionCodegenExtension(isExperimental, globalCacheImpl))
IrGenerationExtension.registerExtension(project,
CliAndroidIrExtension(isExperimental, globalCacheImpl))
StorageComponentContainerContributor.registerExtension(project,
AndroidExtensionPropertiesComponentContainerContributor())
ClassBuilderInterceptorExtension.registerExtension(project,
CliAndroidOnDestroyClassBuilderInterceptorExtension(globalCacheImpl))
PackageFragmentProviderExtension.registerExtension(project,
CliAndroidPackageFragmentProviderExtension(isExperimental))
}
}
override fun registerProjectComponents(project: MockProject, configuration: CompilerConfiguration) {
if (AndroidExtensionsFeature.VIEWS in features) {
registerViewExtensions(configuration, isExperimental, project)
}
}
}
能够看出,首要便是在敞开了AndroidExtensionsFeature.VIEWS
特性时,注册了5个Extension
,接下来咱们来看下这5个Extension
都做了什么
IrGenerationExtension
IrGenerationExtension
是KAE
插件的中心部分,在生成 IR 时回调,咱们能够在这个时分修正与增加 IR,KAE
插件生成的_findCachedViewById
办法都是在这个时分生成的,详细完成如下:
private class AndroidIrTransformer(val extension: AndroidIrExtension, val pluginContext: IrPluginContext) :
IrElementTransformerVoidWithContext() {
override fun visitClassNew(declaration: IrClass): IrStatement {
if ((containerOptions.cache ?: extension.getGlobalCacheImpl(declaration)).hasCache) {
val cacheField = declaration.getCacheField()
declaration.declarations += cacheField // 增加_$_findViewCache特点
declaration.declarations += declaration.getClearCacheFun() // 增加_$_clearFindViewByIdCache办法
declaration.declarations += declaration.getCachedFindViewByIdFun() // 增加_$_findCachedViewById办法
}
return super.visitClassNew(declaration)
}
override fun visitCall(expression: IrCall): IrExpression {
val result = if (expression.type.classifierOrNull?.isFragment == true) {
// this.get[Support]FragmentManager().findFragmentById(R$id.<name>)
createMethod(fragmentManager.child("findFragmentById"), createClass(fragment).defaultType.makeNullable()) {
addValueParameter("id", pluginContext.irBuiltIns.intType)
}.callWithRanges(expression).apply {
// ...
}
} else if (containerHasCache) {
// this._$_findCachedViewById(R$id.<name>)
receiverClass.owner.getCachedFindViewByIdFun().callWithRanges(expression).apply {
dispatchReceiver = receiver
putValueArgument(0, resourceId)
}
} else {
// this.findViewById(R$id.<name>)
irBuilder(currentScope!!.scope.scopeOwnerSymbol, expression).irFindViewById(receiver, resourceId, containerType)
}
return with(expression) { IrTypeOperatorCallImpl(startOffset, endOffset, type, IrTypeOperator.CAST, type, result) }
}
}
如上所示,首要做了两件事:
- 在
visitClassNew
办法中给对应的类(比方 Activity 或者 Fragment )增加了_$_findViewCache
特点,以及_$_clearFindViewByIdCache
与_$_findCachedViewById
办法 - 在
visitCall
办法中,将viewId
替换为相应的表达式,比方this._$_findCachedViewById(R$id.<name>)
或者this.findViewById(R$id.<name>)
能够看出,其实KAE
插件的大部分功能都是通过IrGenerationExtension
完成的
ExpressionCodegenExtension
ExpressionCodegenExtension
的作用其实与IrGenerationExtension
根本一致,都是用来生成_$_clearFindViewByIdCache
等代码的
首要差异在于,IrGenerationExtension
在使用IR
后端时回调,生成的是IR
。
而ExpressionCodegenExtension
在使用 JVM 非IR
后端时回调,生成的是字节码
在 Kotlin 1.5 之后,JVM 后端已经默许敞开 IR
,能够以为这两个 Extension
便是新老版别的两种完成
StorageComponentContainerContributor
StorageComponentContainerContributor
的首要作用是检查调用是否正确
class AndroidExtensionPropertiesCallChecker : CallChecker {
override fun check(resolvedCall: ResolvedCall<*>, reportOn: PsiElement, context: CallCheckerContext) {
// ...
with(context.trace) {
checkUnresolvedWidgetType(reportOn, androidSyntheticProperty)
checkDeprecated(reportOn, containingPackage)
checkPartiallyDefinedResource(resolvedCall, androidSyntheticProperty, context)
}
}
}
如上,首要做了是否有无法解析的回来类型等检查
ClassBuilderInterceptorExtension
ClassBuilderInterceptorExtension
的首要作用是在onDestroyView
办法中调用_$_clearFindViewByIdCache
办法,铲除KAE
缓存
private class AndroidOnDestroyCollectorClassBuilder(
private val delegate: ClassBuilder,
private val hasCache: Boolean
) : DelegatingClassBuilder() {
override fun newMethod(
origin: JvmDeclarationOrigin,
access: Int,
name: String,
desc: String,
signature: String?,
exceptions: Array<out String>?
): MethodVisitor {
val mv = super.newMethod(origin, access, name, desc, signature, exceptions)
if (!hasCache || name != ON_DESTROY_METHOD_NAME || desc != "()V") return mv
hasOnDestroy = true
return object : MethodVisitor(Opcodes.API_VERSION, mv) {
override fun visitInsn(opcode: Int) {
if (opcode == Opcodes.RETURN) {
visitVarInsn(Opcodes.ALOAD, 0)
visitMethodInsn(Opcodes.INVOKEVIRTUAL, currentClassName, CLEAR_CACHE_METHOD_NAME, "()V", false)
}
super.visitInsn(opcode)
}
}
}
}
能够看出,只有在 Fragment 的onDestroyView
办法中增加了 clear 办法,这是因为 Fragment 的生命周期与其根 View 生命周期可能并不一致,而 Activity 的 onDestroy 中是没有也没必要增加的
PackageFragmentProviderExtension
PackageFragmentProviderExtension
的首要作用是注册各种包名,以及该包名下的各种提示
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.debug.activity_debug.*
import kotlinx.android.synthetic.demo.activity_demo.*
比方咱们在 IDE 中引进上面的代码,就能够引进 xml 文件中界说的各个 id 了,这便是通过这个Extension
完成的
总结
本文首要从原理浅析,整体架构,源码剖析等角度剖析了 kotlin-android-extensions 插件到底是怎样完成的
比较其它方案,KAE
使用起来能够说是十分简练高雅了,能够看出 Kotlin 编译器插件真的能够打造出极简的 API,因此尽管KAE
已经过期了,但还是有必要学习一下的
假如本文对你有所帮助,欢迎点赞保藏~