kotlin-android-extensions 插件到底是怎麼實現的?

語言: CN / TW / HK

theme: smartblue highlight: a11y-dark


⚠️本文為稀土掘金技術社區首發簽約文章,14天內禁止轉載,14天后未獲授權禁止轉載,侵權必究!

前言

kotlin-android-extensions 插件是 Kotlin 官方提供的一個編譯器插件,用於替換 findViewById 模板代碼,降低開發成本

雖然 kotlin-android-extensions 現在已經過時了,但比起其他替換 findViewById 的方案,比如第三方的 ButterKnife 與官方現在推薦的 ViewBinding

kotlin-android-extensions 還是有着一個明顯的優點的:極其簡潔的 APIKAE 方案比起其他方案寫起來更加簡便,這是怎麼實現的呢?我們一起來看下

原理淺析

當我們接入KAE後就可以通過以下方式直接獲取 View

```kotlin 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插件將上面這段代碼轉換成了如下代碼

```kotlin 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 插件與編譯器插件的源碼,它們的具體結構如下:

  1. AndroidExtensionsSubpluginIndicatorKAE插件的入口
  2. AndroidSubplugin用於配置傳遞給編譯器插件的參數
  3. AndroidCommandLineProcessor用於接收編譯器插件的參數
  4. AndroidComponentRegistrar用於註冊如圖的各種Extension

源碼分析

插件入口

當我們查看 kotlin-gradle-plugin 的源碼,可以看到 kotlin-android-extensions.properties 文件,這就是插件的入口

implementation-class=org.jetbrains.kotlin.gradle.internal.AndroidExtensionsSubpluginIndicator

接下來我們看一下入口類做了什麼工作

```kotlin class AndroidExtensionsSubpluginIndicator @Inject internal constructor(private val registry: ToolingModelBuilderRegistry) : Plugin { 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") [email protected]
        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中主要做了這麼幾件事

  1. 創建androidExtensions配置,可以看出其中可以配置是否開啟實驗特性,啟用的feature(因為插件中包含viewsparcelize兩個功能),viewId緩存的具體實現(是hashMap還是sparseArray)
  2. 自動添加kotlin-android-extensions-runtime依賴,這樣就不必在接入了插件之後,再手動添加依賴了,這種寫法可以學習一下
  3. 配置AndroidSubplugin插件,開始配置給編譯器插件的傳參

配置編譯器插件傳參

```kotlin 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方法,我們傳遞了featuresvariant等參數給編譯器插件

variant的主要作用是為不同 buildTypeproductFlavor目錄的 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目錄下,這種情況下它們的包名是不同的

編譯器插件接收參數

```kotlin 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的方式修改編譯器的產物

```kotlin 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

IrGenerationExtensionKAE插件的核心部分,在生成 IR 時回調,我們可以在這個時候修改與添加 IR,KAE插件生成的_findCachedViewById方法都是在這個時候生成的,具體實現如下:

```kotlin 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) }
}

} ```

如上所示,主要做了兩件事:
1. 在visitClassNew方法中給對應的類(比如 Activity 或者 Fragment )添加了_$_findViewCache屬性,以及_$_clearFindViewByIdCache_$_findCachedViewById方法 2. 在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的主要作用是檢查調用是否正確

kotlin 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緩存

kotlin 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的主要作用是註冊各種包名,以及該包名下的各種提示

kotlin 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已經過時了,但還是有必要學習一下的

如果本文對你有所幫助,歡迎點贊收藏~