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

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

1. 何为 kotlin-android-extensions ?

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

 
   
  1.    android:id="@+id/logoutView"

  2.    ...

  3.    android:text="退出登录"/>

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

 
   
  1. logoutView.onClick {

  2.    AccountManager.logout()

  3.            .subscribe {

  4.               ...

  5.            }

  6. }

有人这时候难免会有疑问,我们既然从来没有定义过这个变量 logoutView,那它是从哪里来的呢?

关于这个问题,我在将近一年前的一篇文章当中提到过,就是一些编译期的黑魔法啦,不信我们来看下刚才那段 Kotlin 代码对应的字节码:

 
   
  1.   L5

  2.    LINENUMBER 43 L5

  3.    ALOAD 0

  4.    GETSTATIC com/bennyhuo/kae/R$id.logoutView : I

  5.    INVOKEVIRTUAL com/bennyhuo/kae/view/UserDetailActivity._$_findCachedViewById (I)Landroid/view/View;

  6.    CHECKCAST android/widget/Button

  7.    DUP

  8.    LDC "logoutView"

  9.    INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkExpressionValueIsNotNull (Ljava/lang/Object;Ljava/lang/String;)V

  10.    CHECKCAST android/view/View

  11.    ACONST_NULL

  12.    ...

发现了什么?

原来编译器为我们生成了一个叫做 _$_findCachedViewById 的方法,如果你深入查看这个方法的实现,你还会发现有个缓存来存储找到的 View,也就是说在我们使用 logoutView 的时候,第一次会最终调用到 findViewById,后面再使用它的话就直接从缓存中获取了。

这里也有个比较有意思的小尝试,你可以在你的 Activity 当中定义一个方法:

 
   
  1. fun `_$_findCachedViewById`(id: Int): View{

  2.    return RelativeLayout(this)

  3. }

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

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

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

 
   
  1. override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {

  2.    super.onViewCreated(view, savedInstanceState)

  3.    RESTfulService.user(id)

  4.            .subscribeOn(Schedulers.io())

  5.            .observeOn(AndroidSchedulers.mainThread())

  6.            .subscribe { user ->

  7.                userNameView.text = user.name

  8.                ...

  9.            }

  10. }

这段代码的问题在于,如果网络不太好,这个网络请求可能在 10s 甚至更久才返回,而这期间也许我已经离开了这个 Fragment 页面,那么结果会怎样呢?

当然是空指针。是的,你没看错,就是你熟悉的空指针。这次 Kotlin 让你毫无防备的给你一刀,其实它也不愿意的,且让我们来看看这空指针是哪里来的。

 
   
  1. ...

  2. userNameView.text = user.name

  3. ...

注意这一行,我们访问 userNameView ,本质上相当于调用前面提到的编译期为 Fragment 生成的一个方法,这个方法会先从缓存查找,接着再去 FragmentView 中查找,那么问题来了,我们退出这个 Fragment 以后,它的生命周期已经结束,这时候,编译期生成的缓存会被清空:

 
   
  1.  public _$_clearFindViewByIdCache()V

  2.    ALOAD 0

  3.    GETFIELD com/bennyhuo/kae/view/fragments/RepoFragment._$_findViewCache : Ljava/util/HashMap;

  4.    IFNULL L0

  5.    ALOAD 0

  6.    GETFIELD com/bennyhuo/kae/view/fragments/RepoFragment._$_findViewCache : Ljava/util/HashMap;

  7.    INVOKEVIRTUAL java/util/HashMap.clear ()V

  8.    ...

 
   
  1.  public synthetic onDestroyView()V

  2.    ALOAD 0

  3.    INVOKESPECIAL com/bennyhuo/kae/view/common/CommonViewPagerFragment.onDestroyView ()V

  4.    ALOAD 0

  5.    INVOKEVIRTUAL com/bennyhuo/kae/view/fragments/RepoFragment._$_clearFindViewByIdCache ()V

  6.    ...

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

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

 
   
  1.    ...

  2.    INVOKEVIRTUAL android/support/v4/app/Fragment.getView ()Landroid/view/View;

  3.    ...

  4.    INVOKEVIRTUAL android/view/View.findViewById (I)Landroid/view/View;

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

3. 我们该怎么办?

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

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

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


欢迎关注微信公众号 Kotlin

在 Fragment 当中使用 Kotlin-Android-Extensions 需要注意的_第1张图片


你可能感兴趣的:(在 Fragment 当中使用 Kotlin-Android-Extensions 需要注意的)