分析一次kotlin-android-extensions引起的空指针问题

背景

最近开发遇到一个问题,下面图片的recycleview在滚动的时候需要动态的去滚动上面的分类recycleview,如下图,结果是代码里虽然写了在底部rv滚动的时候已计算出对应的分类rv_tab的position,并调用了 rv_tab?.smoothScrollToPosition(parentPosition),为何没有生效?
在这里插入图片描述
代码逻辑也很清晰:

        //初始化滤镜浮层下面的分类
        filterList.apply {
            layoutManager = CenterLayoutManager(context, RecyclerView.HORIZONTAL, false)
            adapter = filterItemAdapter
            filterItemAdapter.also {
                it.setExposureHelper(filterExposureHelper)
                it.setOnItemClickListener { holder, position, item ->
                    applyFilter(position, item)
                }
            }
            addOnScrollListener(object : RecyclerView.OnScrollListener() {
                override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
                    super.onScrolled(recyclerView, dx, dy)
                    // xxxx忽略无关代码
                    if (needNotifyTabChange) {
                    //问题代码是下面这句
                        rv_tab?.smoothScrollToPosition(parentPosition)
                        onFilterTabClicked(parentPosition, false)
                    }
                }

                override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
                    super.onScrollStateChanged(recyclerView, newState)
                    //xxxx忽略无关代码
                }
            })
        }

分析

rv_tab?.smoothScrollToPosition(parentPosition)代码有问题,因为在onScrolled回调里面,所以打log看,结果发现rv_tab为空,那自然是无法滚动到我们想要的位置,那问题来了,通过kotlin-android-extensions(以下简称KAE)。大家都知道,在fragment或者activity里面本质上是有一个HashMap/SparseArray用来缓存当前页面的控件,在onDestroy/onDestroyView中clear。于是

猜想1

那会不会是因为这个fragment走了onDestroyView造成的?打log,答案NO

猜想2

那换种思路,我直接用findViewById(),不用id来直接用行不行?答案YES
继续分析,为啥?
在这里插入图片描述

还原成原来的KAE方式id直接去调用方法,然后Tools------>Kotlin------>Show Kotlin Bytecode------>Decompile 再反编译成java
在这里插入图片描述
继续跟下去
在这里插入图片描述
这里就出现了比较奇怪的现象。正常情况下KAE生成的代码是这样的:
kotlin代码

        rv_tab.adapter = xxxxAdapter()

对应的java代码

      RecyclerView var1 = (RecyclerView)this._$_findCachedViewById(id.rv_tab);
      xxxx无关代码

这个很好理解,this是当前的fragment,_$_findCachedViewById是一个方法

   public View _$_findCachedViewById(int var1) {
      if (this._$_findViewCache == null) {
         this._$_findViewCache = new HashMap();
      }

      View var2 = (View)this._$_findViewCache.get(var1);
      if (var2 == null) {
         View var10000 = this.getView();
         if (var10000 == null) {
            return null;
         }

         var2 = var10000.findViewById(var1);
         this._$_findViewCache.put(var1, var2);
      }

      return var2;
   }

根据上面代码可以看到正常情况下,通过控件id直接去调用方法,会先去从HashMap中取,取不到的话再去判断fragment.getView方法,如果返回的也是空,则此时控件为空,不加?的话则直接崩溃(根源就是 public View _$_findCachedViewById(int var1) 这个方法应该标注@Nullable)。如果fragment.getView不为空,则从fragment的根布局findViewById(),并放到map中,供下次直接取来用。

再回过头来看我们的

                  var10000 = (RecyclerView)((View)this.$this_apply).findViewById(id.rv_tab);
               if (var10000 != null) {
                  var10000.smoothScrollToPosition(parentPosition);
               }

是不是发现了不一样,竟然不是通过_findCachedViewById方法取,会不会跟我前面的嵌套有关系?

        //初始化滤镜浮层下面的分类
      filterList.apply {
      	xxxx无关代码
          addOnScrollListener(object : RecyclerView.OnScrollListener() {
              override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
                  super.onScrolled(recyclerView, dx, dy)
                  val parentPosition = findFirstVisiblePosition(firstVisiblePosition)
                  rv_tab?.smoothScrollToPosition(parentPosition)
                  onFilterTabClicked(parentPosition, false)
              }
          })
      }
                  var10000 = (RecyclerView)((View)this.$this_apply).findViewById(id.rv_tab);

根据上面的截图也可以看出来,这里的this_applyxxx.applyxxx也就是addOnScrollListener这个方法的调用者,即filterList这个控件(也就是gif中下面的那个recycleview)
也就是说等同于下面这样,很明显temp为空!

                        val temp = (this@apply).findViewById<RecyclerView>(R.id.rv_tab)
                        temp?.smoothScrollToPosition(parentPosition)

为了继续验证,于是我把相关代码提到apply外面
果然一切就舒服了
在这里插入图片描述
和同事讨论后,同事发现

import kotlinx.android.synthetic.main.clip_fragment_cv_filter_layout.*
import kotlinx.android.synthetic.main.clip_fragment_cv_filter_layout.view.*

如果把上面的第二行干掉,反编译后发现也是正常的 通过_$_findCachedViewById()来取控件,所以也是正常的,那问题来了,我格式化、并且把无用的包去掉之后,这个xxxx.view.*的导入包还是在的。删掉后也可以正常编译,这就有点诡异。
于是我去看了KAE源码,想弄明白这里面的原因,但是没有找到,如果有大神遇到过,并且知道原因,求赐教。

总结

KAE引起的NPE在我们项目中出现概率极高,有时候可能是内存或者配置更改之类的引起了生命周期的变化,存有控件ID的map被清掉并且mView也为空了,此时再去引用,这就要求我们在耗时操作比如属性动画、handler/view postDelay、异步回调等场景中至少要aaa?.call(),而在apply种调用匿名函数的情景应该要被避免,因为我们不能依赖于xxxxx.view.不被导入来判断,毕竟有些自定义view的导包就只有xxxxx.view.。目前还有一个可以探讨的策略,即通过lint规则来匹配。

import kotlinx.android.synthetic.main.xxxxxx_layout.*
import kotlinx.android.synthetic.main.xxxxxx_layout.view.*

参考

https://juejin.cn/post/6844904057815957517
https://www.kotlincn.net/docs/reference/android-overview.html
https://github.com/JetBrains/kotlin/tree/master/plugins/android-extensions

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Alex_ChuTT

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值