在标题中提出了两个问题,第二个问题中带出了第一个问题的答案。我相信有很多人看到后会说
KAE —— Kotlin-Android-Extensions
(下文使用KAE
简称)这个插件已经被废弃了,不过切换过渡总还需要时间,这篇文章就是写给仍在使用的同学们,希望你们能注意关于Adapter中可能出现的一个坑
1. 被废弃的Kotlin-Android-Extensions
关于KAE
的介绍、废弃及替代方案,以及标题中的第二个问题,在郭神的这篇文章解答得很详细了:
kotlin-android-extensions插件也被废弃了?扶我起来
废弃的主要原因: 每一个View都需要使用一个额外的HashMap存储所有控件实例,无形中增加了一些内存的开支。
每次从HashMap中找出对应控件实例,也降低了程序的运行效率。
最重要的是,这些内容对于绝大部分开发者来说都是黑盒,使用kotlin-android-extensions插件的人可能并不知道这些隐藏的“坑”。
2.KAE
插件在Adapter
中使用的坑
此处摘录郭神文章中关于Adapter
使用KAE
的部分:
刚才我们已经看到过了使用kotlin-android-extensions插件后的代码,非常简单:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewToShowText.text = "Hello"
}
}
复制代码
那么这段代码为什么可以工作呢?
我们可以通过点击Android Studio顶部导航栏的Tools -> Kotlin -> Show Kotlin Bytecode来查看这段代码对应的Kotlin字节码,然后在弹出窗口中点击Decompile按钮将字节码反编译成Java代码。
为了方便阅读,我将反编译后的代码又做了些整理,大致如下所示:
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;
}
}
复制代码
可以看到,实际上kotlin-android-extensions插件会帮我们生成一个_$_findCachedViewById()函数(使用这种奇怪的命名方式是为了防止和开发者定义的函数名冲突)。在这个函数中首先会尝试从一个HashMap中获取传入的资源id参数所对应的控件实例缓存,如果还没有缓存的话,就调用findViewById()函数来查找控件实例,并写入HashMap缓存当中。这样当下次再获取相同控件实例的话,就可以直接从HashMap缓存中获取了。
这就是kotlin-android-extensions插件的实现原理,其实还是非常简单的。
然而有些读者朋友跟我反馈,说这种写法还要在ViewHolder当中声明控件变量,
还要编写findViewById(),实在是太复杂了。
自己找到了一种更简单的写法,只需要借助kotlin-android-extensions插件,就可以这样写:
class FruitAdapter(val fruitList: List<Fruit>) : RecyclerView.Adapter<FruitAdapter.ViewHolder>() {
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.fruit_item, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val fruit = fruitList[position]
holder.itemView.fruitImage.setImageResource(fruit.imageId)
holder.itemView.fruitName.text = fruit.name
}
override fun getItemCount() = fruitList.size
}
可以看到,这里ViewHolder中没有进行任何控件声明,相当于只是定义了一个空的ViewHolder。
然后在onBindViewHolder()函数当中,直接调用holder.itemView再接上控件id的名称就可以使用它了。
这种写法确实简化了不少代码,但是这种写法对吗?
如果你的评判标准只是这段代码能不能正常工作,那么答案是肯定的,这样写确实可以正常工作。
但是这种写法我可以说是完全不正确的,为什么呢?我们只需要使用刚才的手法把这段代码反编译一下,
看看它对应的Java代码是什么样的就知道了。
同样为了方便阅读,我还是对代码进行了简化,只保留了关键部分,如下所示:
public final class FruitAdapter extends Adapter {
...
public final class ViewHolder extends androidx.recyclerview.widget.RecyclerView.ViewHolder {
public ViewHolder(@NotNull View view) {
super(view);
}
}
public void onBindViewHolder(@NotNull FruitAdapter.ViewHolder holder, int position) {
Fruit fruit = (Fruit)this.fruitList.get(position);
View var10000 = holder.itemView;
((ImageView)var10000.findViewById(id.fruitImage)).setImageResource(fruit.getImageId());
var10000 = holder.itemView;
TextView var4 = (TextView)var10000.findViewById(id.fruitName);
var4.setText((CharSequence)fruit.getName());
}
}
不知道你有没有发现问题,现在onBindViewHolder()函数当中,
每次都是调用了findViewById()来获取控件实例,这样就导致ViewHolder的作用完全失效了。
所以,上面这种写法就是kotlin-android-extensions插件在Adapter当中一种比较典型的误用方式。
同时也算是一个隐藏的“坑”,因为如果你不去将Kotlin代码进行反编译,
可能都不知道自己的ViewHolder其实根本就没有起到任何作用。
文章中描述的很清楚,直接在ViewHolder
中使用KAE
的方式是通过holder.itemView.xxx
,转换成java
代码的话,就是每次都在onBindViewHolder()
中调用findViewById()
,显然会徒增性能消耗,违背了ViewHolder
的用法初衷,这也就是KAE
使用在Adapter
中可能出现的坑了。
3. 如何正确的在Adapter
中使用KAE
将KAE
在Activity
、Fragment
中的使用转换为Java
代码,可以看到实质上是创建了一个HashMap
来存储和提取已经实例化的控件,避免多次调用findViewById()
。简化的源码如下:
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;
}
}
那么在ViewHolder
中,能不能也使用HashMap
来缓存控件实例呢,答案是可以的。
KAE
包含一些仍在实验性的功能,比如Kotlin1.1.4之后出现的LayoutContainer
接口,就可以让KAE
除了支持Activity
,Fragment
和View
以外,还能把任何类转换为Android Extensions
容器,从而实现控件的直接使用及缓存。
这些实验性功能需要在build.gradle
中启用实验模式:
android {
compileSdkVersion 30
buildToolsVersion "30.0.3"
defaultConfig {
...
}
...
//打开实验性功能 (主要是为了启用LayoutContainer)
androidExtensions {
experimental = true
}
}
然后将ViewHolder
实现LayoutContainer
接口,即可以直接使用控件Id
了:
class Adapter() : RecyclerView.Adapter<Adapter.Holder>() {
//实现LayoutContainer接口
inner class Holder(itemView: View) : RecyclerView.ViewHolder(itemView), LayoutContainer {
override val containerView: View = this.itemView
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.view_index_title, parent, false)
return Holder(view)
}
override fun onBindViewHolder(holder: Holder, position: Int) {
//每次都会调用itemView.findViewById()
holder.itemView.titleTextView.text = "wrong way"
//初次调用findViewById()之后会缓存进HashMap里
holder.titleTextView.text = "right way"
}
override fun getItemCount(): Int = 0
}
在上面的实现示例里,onBindViewHolder()
中使用了两种调用方式,下面将代码转成Java
并简化:
public void onBindViewHolder(@NotNull Adapter.Holder holder, int position) {
View var10000 = holder.itemView;
AppCompatTextView var3 = (AppCompatTextView)var10000.findViewById(id.titleTextView);
var3.setText((CharSequence)"");
var3 = (AppCompatTextView)holder._$_findCachedViewById(id.titleTextView);
var3.setText((CharSequence)"");
}
可以看到,确实是使用缓存与否的区别,所以可以下出结论: