在 Fragment 当中使用 Kotlin-Android-Extensions 需要注意的

2020-02-20 13:19:45 浏览数 (1)

自从有了 kotlin-android-extensions,小伙伴们的感觉就是一个字,爽!再也不用什么 findViewById 了,也不用什么反射和注解注入了,吾有奇招,黄油刀们速速退散!

1. 何为 kotlin-android-extensions ?

如果你不知道我在说什么,我简单提一句,我们在 xml 布局当中定义了一个 id 为 logoutView 的按钮:

代码语言:javascript复制
<Button
    android:id="@ id/logoutView"
    ...
    android:text="退出登录"/>

通常来讲,如果你想要在你的代码当中操作这个 View,例如给他设置一个点击事件,你需要先 findViewById 找到它的引用,然后 setOnClickListener,对吧。可是有了 kotlin-android-extensions 之后,我们可以直接在 ActivityFragmentView 当中使用这个 logoutView 了。

代码语言:javascript复制
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 当中定义一个方法:

代码语言:javascript复制
fun `_$_findCachedViewById`(id: Int): View{
    return RelativeLayout(this)
}

看看编译期会怎么报答你。

2. 在 Fragment 中使用 Kae 有什么毛病?

好啦,介绍到此,我们来说说问题。前面提到的实际上是 Activity 的实现, Activity 本身就有 findViewById ,所以这里面似乎不会有什么问题出现,而 Fragment 就会稍微麻烦一些,它需要用它的 ViewfindViewById,下面给大家看一段代码,看看有什么问题:

代码语言:javascript复制
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 生成的一个方法,这个方法会先从缓存查找,接着再去 FragmentView 中查找,那么问题来了,我们退出这个 Fragment 以后,它的生命周期已经结束,这时候,编译期生成的缓存会被清空:

代码语言:javascript复制
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
    ...

注意看到 FragmentonDestroyView 被调用时,缓存被清空了。

换句话说,这时候 userNameView 只能重新去 findViewById 了,然而 ——

代码语言:javascript复制
...
    INVOKEVIRTUAL android/support/v4/app/Fragment.getView ()Landroid/view/View;
    ...
    INVOKEVIRTUAL android/view/View.findViewById (I)Landroid/view/View;

这时候 Fragment.getView 必然返回 null,所以就会遇到空指针。

3. 我们该怎么办?

对于这个问题,如果我们强制要求 FragmentgetView 不返回 null,这样是不会出现空指针了,但长时间的持有 UI 引用,可能会导致内存泄露。换句话说, null 是不可避免的。

所以解决方法当然是离开页面就取消请求啊,这样刚刚那段操作 UI 的代码就不会在 Fragment 已经退出之后再执行了。

当然,还有一种思路,上文当中我用到了 RxJava,我可以通过自定义一个 UI 生命周期相关的 Scheduler,在生命周期发生变化时,一方面可以统一取消请求,另一方面,也可以控制在 UI 已经无效时,所有请求的回调都不会被执行。


0 人点赞