網站首頁 編程語言 正文
前言
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
已經過時了,但還是有必要學習一下的
原文鏈接:https://juejin.cn/post/7155491115645435917
相關推薦
- 2022-11-14 Python?prettytable模塊應用詳解_python
- 2022-11-30 Python利用yarl實現輕松操作url_python
- 2022-06-01 AutoMapper實體映射基本用法_實用技巧
- 2022-07-22 element table key的使用
- 2022-08-25 Zabbix對Kafka?topic積壓數據監控的問題(bug優化)_zabbix
- 2022-05-19 C++實現職工工資管理系統課程設計_C 語言
- 2022-04-10 C#實現簡單的計算器小功能_C#教程
- 2022-06-01 一起來學習C++的動態內存管理_C 語言
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支