自从有了 kotlin-android-extensions,小伙伴们的感觉就是一个字,爽!再也不用什么 findViewById
了,也不用什么反射和注解注入了,吾有奇招,黄油刀们速速退散!
1. 何为 kotlin-android-extensions ?
如果你不知道我在说什么,我简单提一句,我们在 xml 布局当中定义了一个 id 为 logoutView
的按钮:
<Button
android:id="@ id/logoutView"
...
android:text="退出登录"/>
通常来讲,如果你想要在你的代码当中操作这个 View
,例如给他设置一个点击事件,你需要先 findViewById
找到它的引用,然后 setOnClickListener
,对吧。可是有了 kotlin-android-extensions 之后,我们可以直接在 Activity
、 Fragment
、 View
当中使用这个 logoutView
了。
logoutView.onClick {
AccountManager.logout()
.subscribe {
...
}
}
有人这时候难免会有疑问,我们既然从来没有定义过这个变量 logoutView
,那它是从哪里来的呢?
关于这个问题,我在将近一年前的一篇文章当中提到过,就是一些编译期的黑魔法啦,不信我们来看下刚才那段 Kotlin 代码对应的字节码:
代码语言:javascript复制L5
LINENUMBER 43 L5
ALOAD 0
GETSTATIC com/bennyhuo/kae/R$id.logoutView : I
INVOKEVIRTUAL com/bennyhuo/kae/view/UserDetailActivity._$_findCachedViewById (I)Landroid/view/View;
CHECKCAST android/widget/Button
DUP
LDC "logoutView"
INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkExpressionValueIsNotNull (Ljava/lang/Object;Ljava/lang/String;)V
CHECKCAST android/view/View
ACONST_NULL
...
发现了什么?
原来编译器为我们生成了一个叫做 _$_findCachedViewById
的方法,如果你深入查看这个方法的实现,你还会发现有个缓存来存储找到的 View
,也就是说在我们使用 logoutView
的时候,第一次会最终调用到 findViewById
,后面再使用它的话就直接从缓存中获取了。
这里也有个比较有意思的小尝试,你可以在你的 Activity
当中定义一个方法:
fun `_$_findCachedViewById`(id: Int): View{
return RelativeLayout(this)
}
看看编译期会怎么报答你。
2. 在 Fragment 中使用 Kae 有什么毛病?
好啦,介绍到此,我们来说说问题。前面提到的实际上是 Activity
的实现, Activity
本身就有 findViewById
,所以这里面似乎不会有什么问题出现,而 Fragment
就会稍微麻烦一些,它需要用它的 View
来 findViewById
,下面给大家看一段代码,看看有什么问题:
override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
RESTfulService.user(id)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { user ->
userNameView.text = user.name
...
}
}
这段代码的问题在于,如果网络不太好,这个网络请求可能在 10s 甚至更久才返回,而这期间也许我已经离开了这个 Fragment
页面,那么结果会怎样呢?
当然是空指针。是的,你没看错,就是你熟悉的空指针。这次 Kotlin 让你毫无防备的给你一刀,其实它也不愿意的,且让我们来看看这空指针是哪里来的。
代码语言:javascript复制...
userNameView.text = user.name
...
注意这一行,我们访问 userNameView
,本质上相当于调用前面提到的编译期为 Fragment
生成的一个方法,这个方法会先从缓存查找,接着再去 Fragment
的 View
中查找,那么问题来了,我们退出这个 Fragment
以后,它的生命周期已经结束,这时候,编译期生成的缓存会被清空:
public _$_clearFindViewByIdCache()V
ALOAD 0
GETFIELD com/bennyhuo/kae/view/fragments/RepoFragment._$_findViewCache : Ljava/util/HashMap;
IFNULL L0
ALOAD 0
GETFIELD com/bennyhuo/kae/view/fragments/RepoFragment._$_findViewCache : Ljava/util/HashMap;
INVOKEVIRTUAL java/util/HashMap.clear ()V
...
代码语言:javascript复制public synthetic onDestroyView()V
ALOAD 0
INVOKESPECIAL com/bennyhuo/kae/view/common/CommonViewPagerFragment.onDestroyView ()V
ALOAD 0
INVOKEVIRTUAL com/bennyhuo/kae/view/fragments/RepoFragment._$_clearFindViewByIdCache ()V
...
注意看到 Fragment
的 onDestroyView
被调用时,缓存被清空了。
换句话说,这时候 userNameView
只能重新去 findViewById
了,然而 ——
...
INVOKEVIRTUAL android/support/v4/app/Fragment.getView ()Landroid/view/View;
...
INVOKEVIRTUAL android/view/View.findViewById (I)Landroid/view/View;
这时候 Fragment.getView
必然返回 null
,所以就会遇到空指针。
3. 我们该怎么办?
对于这个问题,如果我们强制要求 Fragment
的 getView
不返回 null
,这样是不会出现空指针了,但长时间的持有 UI 引用,可能会导致内存泄露。换句话说, null
是不可避免的。
所以解决方法当然是离开页面就取消请求啊,这样刚刚那段操作 UI 的代码就不会在 Fragment
已经退出之后再执行了。
当然,还有一种思路,上文当中我用到了 RxJava,我可以通过自定义一个 UI 生命周期相关的 Scheduler,在生命周期发生变化时,一方面可以统一取消请求,另一方面,也可以控制在 UI 已经无效时,所有请求的回调都不会被执行。