栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

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

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

分析一次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_apply为xxx.apply的xxx也就是addOnScrollListener这个方法的调用者,即filterList这个控件(也就是gif中下面的那个recycleview)
也就是说等同于下面这样,很明显temp为空!

                        val temp = (this@apply).findViewById(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

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/458948.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号