kotlin android extensions 插件实现示例详解

目录
  • 前言
  • 原理浅析
  • 总体结构
  • 源码分析
    • 插件入口
    • 配置编译器插件传参
    • 编译器插件接收参数
    • 注册各种Extension
    • IrGenerationExtension
    • ExpressionCodegenExtension
    • StorageComponentContainerContributor
    • ClassBuilderInterceptorExtension
    • PackageFragmentProviderExtension
  • 总结

前言

kotlin-android-extensions 插件是 Kotlin 官方提供的一个编译器插件,用于替换 findViewById 模板代码,降低开发成本

虽然 kotlin-android-extensions 现在已经过时了,但比起其他替换 findViewById 的方案,比如第三方的 ButterKnife 与官方现在推荐的 ViewBinding

kotlin-android-extensions 还是有着一个明显的优点的:极其简洁的 APIKAE 方案比起其他方案写起来更加简便,这是怎么实现的呢?我们一起来看下

原理浅析

当我们接入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 插件与编译器插件的源码,它们的具体结构如下:

  • AndroidExtensionsSubpluginIndicatorKAE插件的入口
  • 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(因为插件中包含viewsparcelize两个功能),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方法,我们传递了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目录下,这种情况下它们的包名是不同的

编译器插件接收参数

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

IrGenerationExtensionKAE插件的核心部分,在生成 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已经过时了,但还是有必要学习一下的

以上就是kotlin android extensions 插件实现示例详解的详细内容,更多关于kotlin android extensions 插件的资料请关注我们其它相关文章!

(0)

相关推荐

  • 一文读懂Android Kotlin的数据流

    目录 一.Android分层架构 二.ViewModel + LiveData 2.1 LiveData 特性 观察者的回调永远发生在主线程 仅持有单个且最新数据 自动取消订阅 提供「可读可写」和「仅可读」两种方式 配合 DataBinding 实现「双向绑定」 2.2 LiveData的缺陷 value 可以是 nullable 的 传入正确的 lifecycleOwner 粘性事件 默认不防抖 transformation 工作在主线程 2.3 LiveData 小结 三.Flow 3.1

  • Android使用kotlin实现多行文本上下滚动播放

    最近在项目中用到了上下滚动展示条目内容,就使用kotlin简单编写实现了一下该功能. 使用kotlin实现viewflipper展示textview的上下滚动播放 其中包含了kotlin的一些简单的使用 - 首先是在布局文件中如下代码: <ViewFlipper         android:id="@+id/viewFlipper"         android:layout_width="match_parent"         android:la

  • Android Kotlin全面详细类使用语法学习指南

    目录 前言 1. 类的声明 & 实例化 2. 构造函数 2.1 主构造函数 2.2 次构造函数 3. 类的属性 4. 可见性修饰符 5. 继承 & 重写 6. 特殊类 6.1 嵌套类(内部类) 6.2 接口 6.3 数据类 6.4 枚举类 总结 前言 Kotlin被Google官方认为是Android开发的一级编程语言 今天,我将主要讲解kotlin中的类的所有知识,主要内容包括如下: 1. 类的声明 & 实例化 // 格式 class 类名(参数名1:参数类型,参数名2:参数类型

  • Kotlin实现Android系统悬浮窗详解

    目录 Android 弹窗浅谈 系统悬浮窗具体实现 权限申请 代码设计 具体实现 FloatWindowService 类 FloatWindowManager 类 FloatWindowManager 类代码 FloatLayout 类及其 Layout HomeKeyObserverReceiver 类 FloatWindowUtils 类 总结 Android 弹窗浅谈 我们知道 Android 弹窗中,有一类弹窗会在应用之外也显示,这是因为他被申明成了系统弹窗,除此之外还有2类弹窗分别是

  • Android开发Kotlin DSL使用技巧掌握

    目录 前言 什么是 DSL? 您使用任何 DSL 吗? 为什么我们使用 DSL? 我们如何编写自己的 DSL? 中缀 调用 现在,让我们讨论 Android 中 DSL 的用例和示例. 前言 在这篇文章中,我们将学习如何在您的 Android 项目中编写 Kotlin DSL. 这个文章会很长,所以花点时间,让我们一起来写你的 DSL.我们将讨论以下主题, 什么是简单英语中的 DSL? 您使用任何 DSL 吗? 为什么我们使用 DSL? 我们如何编写自己的 DSL 基本示例说明. 那么让我们开始

  • Android开发Kotlin实现圆弧计步器示例详解

    目录 效果图 定义控件的样式 自定义StepView 绘制文本坐标 Android获取中线到基线距离 效果图 定义控件的样式 看完效果后,我们先定义控件的样式 <!-- 自定义View的名字 StepView --> <!-- name 属性名称 format 格式 string 文字 color 颜色 dimension 字体大小 integer 数字 reference 资源或者颜色 --> <declare-styleable name="StepView&q

  • kotlin android extensions 插件实现示例详解

    目录 前言 原理浅析 总体结构 源码分析 插件入口 配置编译器插件传参 编译器插件接收参数 注册各种Extension IrGenerationExtension ExpressionCodegenExtension StorageComponentContainerContributor ClassBuilderInterceptorExtension PackageFragmentProviderExtension 总结 前言 kotlin-android-extensions 插件是 Ko

  • Spi机制在Android开发的应用示例详解

    目录 Spi机制介绍 举个例子 ServiceLoader.load 在Android中的应用 总结 Spi机制介绍 SPI 全称是 Service Provider Interface,是一种将服务接口与服务实现分离以达到解耦.可以提升程序可扩展性的机制.嘿嘿,看到这个概念很多人肯定是一头雾水了,没事,我们直接就可以简单理解为是一种反射机制,即我们不需要知道具体的实现方,只要定义好接口,我们就能够在运行时找到一个实现接口的类,我们具体看一下官方定义. 举个例子 加入我是一个库设计者,我希望把一

  • Kotlin协程Dispatchers原理示例详解

    目录 前置知识 demo startCoroutineCancellable intercepted()函数 DefaultScheduler中找dispatch函数 Runnable传入 Worker线程执行逻辑 小结 前置知识 Kotlin协程不是什么空中阁楼,Kotlin源代码会被编译成class字节码文件,最终会运行到虚拟机中.所以从本质上讲,Kotlin和Java是类似的,都是可以编译产生class的语言,但最终还是会受到虚拟机的限制,它们的代码最终会在虚拟机上的某个线程上被执行. 之

  • 图解 Kotlin SharedFlow 缓存系统及示例详解

    目录 前言 replay extraBufferCapacity onBufferOverflow SharedFlow Buffer 前言 Kotlin 为我们提供了两种创建“热流”的工具:StateFlow 和 SharedFlow.StateFlow 经常被用来替代 LiveData 充当架构组件使用,所以大家相对熟悉.其实 StateFlow 只是 SharedFlow 的一种特化形式,SharedFlow 的功能更强大.使用场景更多,这得益于其自带的缓存系统,本文用图解的方式,带大家更

  • 独立使用umi的核心插件模块示例详解

    目录 引言 实践 结语 引言 今天我们做一个有趣的尝试,将 umi 的核心插件模块独立出来作为另一个框架的基础架构,这里我们将它称为 konos. 介于 umi 自身的源码的独立拆分,要实现这个功能其实非常的简单.只需要单独使用 @umijs/core 就好. 实践 先看具体实践吧.以下步骤都是常规编写 cli 的一些步骤,我就不做过多的说明,如果你看不懂其中的某些代码,可以评论区留言,或者查看我的其他文章. 新建空白文件夹,mkdir konos 你可以根据你使用的电脑执行对应的命令来新建一个

  • android中webview定位问题示例详解

    前言 现在很多App里都内置了Web网页(Hyprid App),比如说很多电商平台,淘宝.京东.聚划算等等 京东首页 那么这种该如何实现呢?其实这是Android里一个叫WebView的组件实现的. 最近在做安卓的网页开发.有一个页面需要用到定位,但是一直定位获取失败.很难过.网上教程也很多,但是无一例外全部失败.最后老夫花了3天时间,呕心沥血,终于研制出了解决方案. 三步走战略: 一.获取权限 android 6.0 以后,需要动态的获取位置或者存储权限,按照各自的爱好放置位置.我是应用开启

  • Android中ExpandableListView使用示例详解

    本文实例为大家分享了ExpandableListView使用示例,供大家参考,具体内容如下 MainActivity: public class Expandable_test extends Activity { private ExpandableListView listView; private Map<String, List<String>> dataset = new HashMap<>(); private String[] parentList = n

  • Android多线程断点续传下载示例详解

    一.概述 在上一篇博文<Android多线程下载示例>中,我们讲解了如何实现Android的多线程下载功能,通过将整个文件分成多个数据块,开启多个线程,让每个线程分别下载一个相应的数据块来实现多线程下载的功能.多线程下载中,可以将下载这个耗时的操作放在子线程中执行,即不阻塞主线程,又符合Android开发的设计规范. 但是当下载的过程当中突然出现手机卡死,或者网络中断,手机电量不足关机的现象,这时,当手机可以正常使用后,如果重新下载文件,似乎不太符合大多数用户的心理期望,那如何实现当手机可以正

  • Kotlin对象比较注意点示例详解

    目录 背景 原因 另一个问题 解决办法 结论 背景 现有一个StateFlow及其监听 private val stateFlow = MutableStateFlow(kotlin.Pair<String, ArrayList<String>>("abc", ArrayList())) GlobalScope.launch { stateFlow.collect { // do something } } 更新ArrayList并尝试emit GlobalSc

  • Android 表格布局TableLayout示例详解

    一.表格布局 TableLayout 表格布局TableLayout以行列的形式管理子元素,每一行是一个TableRow布局对象,当然也可以是普通的View对象,TableRow离每放一个元素就是一列,总列数由列数最多的那一行决定. 我们看一个例子: <?xml version="1.0″ encoding="utf-8″?> <TableLayout android:id="@+id/TableLayout01″ android:layout_width=

随机推荐