react native Android 真正回收复用 RecyclerView/ListView

原创
2016/09/23 08:14
阅读数 1.8W

    react native现在是一个热火朝天的框架。一个无原生开发经验的开发三两天即可搞出一个android/iso app,给做js的前端开发人员带来了狂欢。各种"ReactNative项目实战"的文章也遍布在互联网,比如qq 空间的<<ReactNative For Android 项目实战总结>>。然而,各种项目实战总结的背后却对react-native背后的问题一笔带过甚至只字不提。比如最常见的内存泄漏问题、Navigator巨慢内存泄漏、ListView巨吃内存。。。

    言归正传说rn android listview问题。在react-native的android源码中您可以见到RecyclerViewBackedScrollView,react-native编写js时ListView组建中指定属性renderScrollComponent为RecyclerViewBackedScrollView它就使用android的RecyclerView,不过别高兴的太早了,它跟我们想象中的recycle八竿子打不着,您有多少DataSource它就会在内存里帮您钉住多少行的视图毫不吝啬。

    知道了问题我们来找找解决方案(qq的开发员在祈祷facebook后续的版本解决)。国外的同行还是有不少人在提供解决方案的。比如react-native-sglistviewreact-native-enhanced-listview,不过android的react-native框架 facebook偷懒没有实现OnChangeVisibleRows这个东西跑不动,即使可行这个东西也还是会存在内存泄漏的情况。最后找到了一个iOS的解决方案《Recycling Rows for High Performance React Native List Views》。他的主要思想是:

  1. react-native js,端创建足够的滑动行;
  2. TableViewChildren.js里维护一个binging数组,关联视图和对应数据的行号;
  3. 原生发送onChange消息告诉TableViewChildren视图对应数据的行号有更改;
  4. ReboundRenderer.js通过判断行号更改刷新视图。

    这个方案非常巧妙,取巧地通过ReboundRenderer来重刷视图,原生主动更新js端视图。比起react-native-sglistview、enhanced-listview的思路更深入react-native原理,解决的思路值得借鉴。有了指导思想android照葫芦画瓢,一番码码android的RecyclerView可以复用item和刷新,不过它的样子却是错乱的。

    

    跟踪日志更新的视图行号和数据行号均没有问题,百思不得其解重新回到读react-native代码。在翻阅RecyclerViewBackedScrollView源码时,两个函数的注释引起了我的注意。
    

    即react-native自己接管了视图在屏幕中的measure和坐标计算。然而,我实现的RecyclerView item 的坐标在滑动、静止均是由RecyclerView包办的,那么很大可能是react-native在接管视图的坐标苗点计算有错误,所以导致RecyclerView错乱。

    为了验证猜测需要理清react-native更新视图的流程(啃了一堆js类库非常头疼,没有好的js ide真不方便),下面是我理出的更新流程
    
 

    简单归纳下:一个js render消息经过几层传递投递到ui消息队列,再由react-native原生代码绘制view。一个消息可以是createView、updateView等,而上文提到的坐标计算是updateLayout。updateLayout可以在createView、updateView、updateProperties等消息均可能触发。我们可以在UIViewOperationQueue或者NativeViewHierarchyManager中拦截updateLayout。

    现在来考虑我的修正方案。在《Recycling Rows for High Performance React Native List Views》中通过ReboundRenderer组件来实现重新绘制row ui,看似非常巧妙实则我觉的作者是掉进React Native组件的生命周期的陷阱,而且他的做法除了需要一个额外的binging数组还会导致TableViewChildren重新render一遍,也就是踢掉旧row view产生了一组新的。其实ReboundRenderer组件是一个多余的累赘,完全可以在row view里判断rowID更改再重新render。总结下我的方案:

  1. 实现一个RealRecyclerView,自定义一个row view(RealRecyclerItemView),当RecyclerView滑动时发消息给RealRecyclerItemView,告诉它刷新视图;
  2. 在UIViewOperationQueue拦截updateLayout消息,发现updateLayout RealRecyclerItemView中断执行消息。

   最后实现的方案见react-native-RealRecyclerView.
        

展开阅读全文
打赏
2
12 收藏
分享
加载中
你好,我测试了下内存,发现内存还是会一直上涨,是否真的回收了呢?
2017/08/05 16:29
回复
举报
listView 并没有拦截到?用了onMeasure 视图就不渲染出来 是什么情况
2017/05/02 14:34
回复
举报
rowHeight={100}
如果不定义高度,item的高度无法正常展示
这样定义每项高度都定死了,不能动态改变
2016/12/30 13:38
回复
举报

引用来自“obaniu”的评论

引用来自“oo2oo2oo”的评论

引用来自“obaniu”的评论

引用来自“oo2oo2oo”的评论

这个的确是真正的回收ListView。其他的procrank内存都在一直涨。
但是有一部分代码看不懂,特注册账号请教一下:

JS:
var rCount = Math.round(height / this.props.rowHeight * 1.6);
if (rCount < 9) rCount = 9;
这个1.6和9是代表什么意思呢

JAVA:
void setRowHeight(int rowHeight) {
mRowHeight = (int) PixelUtil.toPixelFromDIP(rowHeight);
final int height = Math.max(DisplayMetricsHolder.getScreenDisplayMetrics().heightPixels, DisplayMetricsHolder.getScreenDisplayMetrics().widthPixels);
mHoldItems = Math.round(1.6f * height / this.mRowHeight);
if (mHoldItems < 6) mHoldItems = 6;
}
这里面也有1.6,而且为什么height要取宽高的Math.max呢

回复@oo2oo2oo : 由于RecyclerView需要一定数量的view来复用,所以我们需要通过设置item的高度来计算recyclerview回收复用的试图数量。1.6的意思是填充1.6个屏幕高度的视图。
OK,感谢!那这个可以自己随便定义一个合适的值了。

Java部分的呢,为什么要取长宽的最大值呢,应该直接拿高度来计算才对吧,另外mHoldItems最小设置为6,这个6也是个经验值吗?

回复@oo2oo2oo : 你说可以随便定义就随便定义咯,just try it.
OK,感谢!
2016/12/27 14:25
回复
举报
obaniu博主

引用来自“oo2oo2oo”的评论

引用来自“obaniu”的评论

引用来自“oo2oo2oo”的评论

这个的确是真正的回收ListView。其他的procrank内存都在一直涨。
但是有一部分代码看不懂,特注册账号请教一下:

JS:
var rCount = Math.round(height / this.props.rowHeight * 1.6);
if (rCount < 9) rCount = 9;
这个1.6和9是代表什么意思呢

JAVA:
void setRowHeight(int rowHeight) {
mRowHeight = (int) PixelUtil.toPixelFromDIP(rowHeight);
final int height = Math.max(DisplayMetricsHolder.getScreenDisplayMetrics().heightPixels, DisplayMetricsHolder.getScreenDisplayMetrics().widthPixels);
mHoldItems = Math.round(1.6f * height / this.mRowHeight);
if (mHoldItems < 6) mHoldItems = 6;
}
这里面也有1.6,而且为什么height要取宽高的Math.max呢

回复@oo2oo2oo : 由于RecyclerView需要一定数量的view来复用,所以我们需要通过设置item的高度来计算recyclerview回收复用的试图数量。1.6的意思是填充1.6个屏幕高度的视图。
OK,感谢!那这个可以自己随便定义一个合适的值了。

Java部分的呢,为什么要取长宽的最大值呢,应该直接拿高度来计算才对吧,另外mHoldItems最小设置为6,这个6也是个经验值吗?

回复@oo2oo2oo : 你说可以随便定义就随便定义咯,just try it.
2016/12/27 14:13
回复
举报

引用来自“obaniu”的评论

引用来自“oo2oo2oo”的评论

这个的确是真正的回收ListView。其他的procrank内存都在一直涨。
但是有一部分代码看不懂,特注册账号请教一下:

JS:
var rCount = Math.round(height / this.props.rowHeight * 1.6);
if (rCount < 9) rCount = 9;
这个1.6和9是代表什么意思呢

JAVA:
void setRowHeight(int rowHeight) {
mRowHeight = (int) PixelUtil.toPixelFromDIP(rowHeight);
final int height = Math.max(DisplayMetricsHolder.getScreenDisplayMetrics().heightPixels, DisplayMetricsHolder.getScreenDisplayMetrics().widthPixels);
mHoldItems = Math.round(1.6f * height / this.mRowHeight);
if (mHoldItems < 6) mHoldItems = 6;
}
这里面也有1.6,而且为什么height要取宽高的Math.max呢

回复@oo2oo2oo : 由于RecyclerView需要一定数量的view来复用,所以我们需要通过设置item的高度来计算recyclerview回收复用的试图数量。1.6的意思是填充1.6个屏幕高度的视图。
OK,感谢!那这个可以自己随便定义一个合适的值了。

Java部分的呢,为什么要取长宽的最大值呢,应该直接拿高度来计算才对吧,另外mHoldItems最小设置为6,这个6也是个经验值吗?
2016/12/27 14:00
回复
举报
这个的确是真正的回收ListView。其他的procrank内存都在一直涨。
但是有一部分代码看不懂,特注册账号请教一下:

JS:
var rCount = Math.round(height / this.props.rowHeight * 1.6);
if (rCount < 9) rCount = 9;
这个1.6和9是代表什么意思呢

JAVA:
void setRowHeight(int rowHeight) {
mRowHeight = (int) PixelUtil.toPixelFromDIP(rowHeight);
final int height = Math.max(DisplayMetricsHolder.getScreenDisplayMetrics().heightPixels, DisplayMetricsHolder.getScreenDisplayMetrics().widthPixels);
mHoldItems = Math.round(1.6f * height / this.mRowHeight);
if (mHoldItems < 6) mHoldItems = 6;
}
这里面也有1.6,而且为什么height要取宽高的Math.max呢
2016/12/27 13:09
回复
举报
更多评论
打赏
7 评论
12 收藏
2
分享
返回顶部
顶部