背景
最近开发遇到一个问题,下面图片的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)
if (needNotifyTabChange) {
rv_tab?.smoothScrollToPosition(parentPosition)
onFilterTabClicked(parentPosition, false)
}
}
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
}
})
}
分析
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<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
|