Kotlin 從首次推出到如今,可謂發展的十分迅速,獨特的空安全特性吸引了不少 Android 開發者去使用,Google 也正式將 Kotlin 這門語言做爲 Android 開發的首選語言。Kotlin 官方也爲各位開發者提供了一系列的插件,開發文檔以及 IDE 支持,本文介紹的 Kotlin-Android-Extensions 就是一款 Kotlin 的安卓開發擴展插件。java
直接在 build.gradle 中引入該插件:android
apply plugin: 'kotlin-android-extensions'
複製代碼
模擬的業務場景以下:git
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*
/** * Created by Xu on 2020/02/05. * * @author Xu */
class MainActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button_test.setOnClickListener {
// todo
}
}
}
複製代碼
這裏能夠觀察到,並無熟悉的 findViewById() 方法,而是直接使用了 button_test 這個對象,該對象實際上是由插件根據佈局 xml 中所設置的控件 id 而自動生成的。github
爲何不須要使用到 findViewById() 方法呢?以前我在分析 ButterKnife 源碼的時候也問過相似的問題(傳送門),最後實際上是經過 APT(編譯時註解)的方式自動生成了 findViewById 方法,猜想這裏也是經過相似的自動生成代碼方式幫咱們補充了。緩存
咱們首先試着去反編譯 Kotlin ByteCode,具體是經過打開 Android Studio -> Tools -> Kotlin -> Show Kotlin Bytecode,而後選擇 build 文件夾下的 MainActivity.class,點擊 Decompile 便可。反編譯完代碼以下:安全
public final class MainActivity extends AppCompatActivity {
private HashMap _$_findViewCache;
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.setContentView(layout.activity_main);
((Button)this._$_findCachedViewById(id.button_test)).setOnClickListener((OnClickListener)null.INSTANCE);
}
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();
}
}
}
複製代碼
這裏會發現多了一個 _$_findViewCache 的成員變量以及 _$_findCachedViewById 的方法,而這個方法內部其實也是使用到了 findViewById,而且對 view 進行了緩存,避免了該方法的重複調用。bash
那麼這些代碼是怎麼生成的呢?經過谷歌的搜索,筆者找到了該插件的源代碼地址(傳送門),而後觀察到這個變量和方法命名是固定的,跟具體的類命名無關,猜想是一個固定的常量值,在代碼中進行全局搜索,找到如下這個相關類:app
AbstractAndroidExtensionsExpressionCodegenExtension.kt:ide
abstract class AbstractAndroidExtensionsExpressionCodegenExtension : ExpressionCodegenExtension {
companion object {
val PROPERTY_NAME = "_\$_findViewCache"
val CACHED_FIND_VIEW_BY_ID_METHOD_NAME = "_\$_findCachedViewById"
val CLEAR_CACHE_METHOD_NAME = "_\$_clearFindViewByIdCache"
val ON_DESTROY_METHOD_NAME = "onDestroyView"
fun shouldCacheResource(resource: PropertyDescriptor) = (resource as? AndroidSyntheticProperty)?.shouldBeCached == true
}
// 省略部分代碼
}
複製代碼
再進一步的去搜索,找到類中對應的 generateCacheField() 和 generateCachedFindViewByIdFunction() 方法。源碼分析
先看 generateCacheField():
private fun SyntheticPartsGenerateContext.generateCacheField() {
val cacheImpl = CacheMechanism.getType(containerOptions.getCacheOrDefault(classOrObject))
classBuilder.newField(JvmDeclarationOrigin.NO_ORIGIN, ACC_PRIVATE, PROPERTY_NAME, cacheImpl.descriptor, null, null)
}
複製代碼
這裏用到 CacheMechanism 的 getType 方法,而後經過 classBuilder#newField() 生成。
CacheMechanism#getType():
fun getType(cacheImpl: CacheImplementation): Type {
return Type.getObjectType(when (cacheImpl) {
CacheImplementation.SPARSE_ARRAY -> "android.util.SparseArray"
CacheImplementation.HASH_MAP -> HashMap::class.java.canonicalName
CacheImplementation.NO_CACHE -> throw IllegalArgumentException("Container should support cache")
}.replace('.', '/'))
}
複製代碼
這裏返回的是 _$_findViewCache 這個成員變量的類型,默認是 HashMap,也能夠在 build.gradle 中指定類型:
androidExtensions {
defaultCacheImplementation = "HASH_MAP" // or SPARSE_ARRAY、NONE
}
複製代碼
再看 generateCachedFindViewByIdFunction():
private fun SyntheticPartsGenerateContext.generateCachedFindViewByIdFunction() {
val containerAsmType = state.typeMapper.mapClass(container)
val viewType = Type.getObjectType("android/view/View")
val methodVisitor = classBuilder.newMethod(
JvmDeclarationOrigin.NO_ORIGIN, ACC_PUBLIC, CACHED_FIND_VIEW_BY_ID_METHOD_NAME, "(I)Landroid/view/View;", null, null)
methodVisitor.visitCode()
val iv = InstructionAdapter(methodVisitor)
val cacheImpl = CacheMechanism.get(containerOptions.getCacheOrDefault(classOrObject), iv, containerAsmType)
fun loadId() = iv.load(1, Type.INT_TYPE)
// Get cache property
cacheImpl.loadCache()
val lCacheNonNull = Label()
iv.ifnonnull(lCacheNonNull)
// Init cache if null
cacheImpl.initCache()
// Get View from cache
iv.visitLabel(lCacheNonNull)
cacheImpl.loadCache()
loadId()
cacheImpl.getViewFromCache()
iv.checkcast(viewType)
iv.store(2, viewType)
val lViewNonNull = Label()
iv.load(2, viewType)
iv.ifnonnull(lViewNonNull)
// Resolve View via findViewById if not in cache
iv.load(0, containerAsmType)
val containerType = containerOptions.containerType
when (containerType) {
AndroidContainerType.ACTIVITY, AndroidContainerType.ANDROIDX_SUPPORT_FRAGMENT_ACTIVITY, AndroidContainerType.SUPPORT_FRAGMENT_ACTIVITY, AndroidContainerType.VIEW, AndroidContainerType.DIALOG -> {
loadId()
iv.invokevirtual(containerType.internalClassName, "findViewById", "(I)Landroid/view/View;", false)
}
AndroidContainerType.FRAGMENT, AndroidContainerType.ANDROIDX_SUPPORT_FRAGMENT, AndroidContainerType.SUPPORT_FRAGMENT, LAYOUT_CONTAINER -> {
if (containerType == LAYOUT_CONTAINER) {
iv.invokeinterface(containerType.internalClassName, "getContainerView", "()Landroid/view/View;")
} else {
iv.invokevirtual(containerType.internalClassName, "getView", "()Landroid/view/View;", false)
}
iv.dup()
val lgetViewNotNull = Label()
iv.ifnonnull(lgetViewNotNull)
// Return if getView() is null
iv.pop()
iv.aconst(null)
iv.areturn(viewType)
// Else return getView().findViewById(id)
iv.visitLabel(lgetViewNotNull)
loadId()
iv.invokevirtual("android/view/View", "findViewById", "(I)Landroid/view/View;", false)
}
else -> throw IllegalStateException("Can't generate code for $containerType")
}
iv.store(2, viewType)
// Store resolved View in cache
cacheImpl.loadCache()
loadId()
cacheImpl.putViewToCache { iv.load(2, viewType) }
iv.visitLabel(lViewNonNull)
iv.load(2, viewType)
iv.areturn(viewType)
FunctionCodegen.endVisit(methodVisitor, CACHED_FIND_VIEW_BY_ID_METHOD_NAME, classOrObject)
}
複製代碼
這裏的代碼比較複雜,但能夠觀察到一個重要的地方,它會去判斷當前的類是 Activity 仍是 Fragment,再去執行對應的尋找控件方法。例如是 Activity 的話,則執行的是 findViewById 方法;而若是是 Fragment,則先執行 getView 方法獲取到對應的 rootView,再執行 findViewById。
還有一個點,最後的實現都會調用到 iv#invokevirtual() 方法,iv 是 InstructionAdapter 類的一個實例。InstructionAdapter 繼承於 MethodVisiter,用途是生成方法實現的字節碼,這裏再也不深究實現細節,有興趣的讀者能夠再去了解一下。
Kotlin-Android-Extensions 這個插件,經過自動生成尋找控件代碼的字節碼,對查找完的控件進行緩存以及 IDE 跳轉支持等方式,使得 Android 的業務開發更加地便捷高效,有效提升研發效率,提高研發體驗。