kotlin android extensions 插件實現(xiàn)示例詳解
前言
kotlin-android-extensions 插件是 Kotlin 官方提供的一個編譯器插件,用于替換 findViewById 模板代碼,降低開發(fā)成本
雖然 kotlin-android-extensions 現(xiàn)在已經(jīng)過時了,但比起其他替換 findViewById 的方案,比如第三方的 ButterKnife 與官方現(xiàn)在推薦的 ViewBinding
kotlin-android-extensions 還是有著一個明顯的優(yōu)點的:極其簡潔的 API,KAE 方案比起其他方案寫起來更加簡便,這是怎么實現(xiàn)的呢?我們一起來看下
原理淺析
當(dāng)我們接入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插件將上面這段代碼轉(zhuǎn)換成了如下代碼
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()函數(shù),在這個函數(shù)中首先會嘗試從一個 HashMap 中獲取傳入的資源 id 參數(shù)所對應(yīng)的控件實例緩存,如果還沒有緩存的話,就調(diào)用findViewById()函數(shù)來查找控件實例,并寫入 HashMap 緩存當(dāng)中。這樣當(dāng)下次再獲取相同控件實例的話,就可以直接從 HashMap 緩存中獲取了。
當(dāng)然KAE也幫我們生成了_$_clearFindViewByIdCache()函數(shù),不過在 Activity 中沒有調(diào)用,在 Fragment 的 onDestroyView 方法中會被調(diào)用到
總體結(jié)構(gòu)
在了解了KAE插件的簡單原理后,我們一步一步來看一下它是怎么實現(xiàn)的,首先來看一下總體結(jié)構(gòu)
KAE插件可以分為 Gradle 插件,編譯器插件,IDE 插件三部分,如下圖所示

我們今天只分析 Gradle 插件與編譯器插件的源碼,它們的具體結(jié)構(gòu)如下:

AndroidExtensionsSubpluginIndicator是KAE插件的入口AndroidSubplugin用于配置傳遞給編譯器插件的參數(shù)AndroidCommandLineProcessor用于接收編譯器插件的參數(shù)AndroidComponentRegistrar用于注冊如圖的各種Extension
源碼分析
插件入口
當(dāng)我們查看 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中主要做了這么幾件事
- 創(chuàng)建
androidExtensions配置,可以看出其中可以配置是否開啟實驗特性,啟用的feature(因為插件中包含views與parcelize兩個功能),viewId緩存的具體實現(xiàn)(是hashMap還是sparseArray) - 自動添加
kotlin-android-extensions-runtime依賴,這樣就不必在接入了插件之后,再手動添加依賴了,這種寫法可以學(xué)習(xí)一下 - 配置
AndroidSubplugin插件,開始配置給編譯器插件的傳參
配置編譯器插件傳參
class AndroidSubplugin : KotlinCompilerPluginSupportPlugin {
// 1. 是否開啟編譯器插件
override fun isApplicable(kotlinCompilation: KotlinCompilation<*>): Boolean {
if (kotlinCompilation !is KotlinJvmAndroidCompilation)
return false
// ...
return true
}
// 2. 傳遞給編譯器插件的參數(shù)
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` 坐標(biāo)信息,便于編譯器下載它
override fun getPluginArtifact(): SubpluginArtifact =
JetBrainsSubpluginArtifact(artifactId = "kotlin-android-extensions")
}
主要也是重寫以上4個函數(shù),各自的功能在文中都有注釋,其中主要需要注意applyToCompilation方法,我們傳遞了features,variant等參數(shù)給編譯器插件
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目錄下,這種情況下它們的包名是不同的
編譯器插件接收參數(shù)
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,包名,是否開啟試驗特性,緩存實現(xiàn)方式這幾個參數(shù)
注冊各種Extension
接下來到了編譯器插件的核心部分,通過注冊各種Extension的方式修改編譯器的產(chǎn)物
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 時回調(diào),我們可以在這個時候修改與添加 IR,KAE插件生成的_findCachedViewById方法都是在這個時候生成的,具體實現(xiàn)如下:
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方法中給對應(yīng)的類(比如 Activity 或者 Fragment )添加了_$_findViewCache屬性,以及_$_clearFindViewByIdCache與_$_findCachedViewById方法 - 在
visitCall方法中,將viewId替換為相應(yīng)的表達式,比如this._$_findCachedViewById(R$id.<name>)或者this.findViewById(R$id.<name>)
可以看出,其實KAE插件的大部分功能都是通過IrGenerationExtension實現(xiàn)的
ExpressionCodegenExtension
ExpressionCodegenExtension的作用其實與IrGenerationExtension基本一致,都是用來生成_$_clearFindViewByIdCache等代碼的
主要區(qū)別在于,IrGenerationExtension在使用IR后端時回調(diào),生成的是IR。
而ExpressionCodegenExtension在使用 JVM 非IR后端時回調(diào),生成的是字節(jié)碼
在 Kotlin 1.5 之后,JVM 后端已經(jīng)默認(rèn)開啟 IR,可以認(rèn)為這兩個 Extension 就是新老版本的兩種實現(xiàn)
StorageComponentContainerContributor
StorageComponentContainerContributor的主要作用是檢查調(diào)用是否正確
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方法中調(diào)用_$_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實現(xiàn)的
總結(jié)
本文主要從原理淺析,總體架構(gòu),源碼分析等角度分析了 kotlin-android-extensions 插件到底是怎么實現(xiàn)的
相比其它方案,KAE使用起來可以說是非常簡潔優(yōu)雅了,可以看出 Kotlin 編譯器插件真的可以打造出極簡的 API,因此雖然KAE已經(jīng)過時了,但還是有必要學(xué)習(xí)一下的
以上就是kotlin android extensions 插件實現(xiàn)示例詳解的詳細(xì)內(nèi)容,更多關(guān)于kotlin android extensions 插件的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android Studio gradle 編譯提示‘default not found’ 解決辦法
這篇文章主要介紹了Android Studio gradle 編譯提示‘default not found’ 解決辦法的相關(guān)資料,需要的朋友可以參考下2016-12-12
Android中利用Xposed框架實現(xiàn)攔截系統(tǒng)方法
這篇文章主要介紹了Android中利用Xposed框架實現(xiàn)攔截系統(tǒng)方法的相關(guān)資料,需要的朋友可以參考下2016-11-11

