在useEffect取消订阅后,Android向JS发送事件仍引发内存泄漏
问题
按照React Native官方文档实现了Android原生向RN发送事件的功能:定义RecyclerViewScrollListener,在滚动时将当前可见项索引发送至RN(类似FlatList的onViewableItemsChanged);监听器设置在TikTokScreenFragment的onViewCreated方法中,事件发送逻辑在自定义ViewManager的EventEmitter实现里。
RN侧通过useEffect使用NativeEventEmitter订阅事件,且在清理函数中移除了监听器,但滚动页面时Android内存分析器显示内存暴涨,注释addListener后问题消失,求排查解决建议。
相关代码:
RecyclerViewScrollListener 代码
scrollListener = object : RecyclerViewScrollListener() { override fun onItemIsFirstVisibleItem(index: Int) { Log.d("visible item index", index.toString()) // play just visible item if (index != -1) { PlayerViewAdapter.playIndexThenPausePreviousPlayer(index) eventEmitter.emitEvent("onVisibleItemChanged", index) } } } recyclerView!!.addOnScrollListener(scrollListener)
TikTokScreenFragment 的 onViewCreated 代码
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setAdapter() // this is where the scroll listener is set up // load data val model: MediaViewModel by activityViewModels() model.getMedia().observe(requireActivity(), Observer { mAdapter?.updateList(arrayListOf(*it.toTypedArray())) }) }
MyViewManager 代码
class MyViewManager( private val reactContext: ReactApplicationContext ) : ViewGroupManager<FrameLayout>() { override fun receiveCommand( root: FrameLayout, commandId: String, args: ReadableArray? ) { super.receiveCommand(root, commandId, args) val reactNativeViewId = requireNotNull(args).getInt(0) when (commandId.toInt()) { COMMAND_CREATE -> createFragment(root, reactNativeViewId) } } fun createFragment(root: FrameLayout, reactNativeViewId: Int) { val parentView = root.findViewById<ViewGroup>(reactNativeViewId) setupLayout(parentView) val eventEmitter = object : EventEmitter { override fun emitEvent(eventName: String) { reactContext .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) .emit(eventName, null) } override fun emitEvent(eventName: String, eventData: Int) { reactContext .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) .emit(eventName, eventData) } } val myFragment = TikTokScreenFragment(eventEmitter) val activity = reactContext.currentActivity as FragmentActivity activity.supportFragmentManager .beginTransaction() .replace(reactNativeViewId, myFragment, reactNativeViewId.toString()) .commit() } }
RN 侧 useEffect 代码
useEffect(() => { const eventEmitter = new NativeEventEmitter(); const onVisibleItemChanged = eventEmitter.addListener( "onVisibleItemChanged", function (e: Event) { // handle event. setVisibleIndex(e as unknown as number); } ); console.log("listening to onVisibleItemChanged"); return () => { console.log("removing onVisibleItemChanged listener"); onVisibleItemChanged.remove(); }; }, []);
排查解决建议
- 控制事件发送频率:RecyclerView滚动时
onItemIsFirstVisibleItem可能被频繁触发(快速滚动时每秒可达几十次),大量事件会导致JS线程处理压力剧增,同时Native侧积累大量未处理的事件对象。添加防抖逻辑,比如设置200ms间隔,同一时间段内仅发送最后一次的索引值。 - 清理Fragment与监听器的引用:自定义
EventEmitter匿名类持有reactContext强引用,若Fragment销毁时未移除滚动监听器,会导致ReactApplicationContext无法被回收。在TikTokScreenFragment的onDestroyView方法中调用recyclerView?.removeOnScrollListener(scrollListener),同时确保Fragment销毁时清理eventEmitter相关引用。 - 验证RN侧监听器的移除逻辑:确认组件卸载时是否打印了
removing onVisibleItemChanged listener日志,确保监听器确实被移除。同时检查useEffect的依赖项,避免因依赖变化导致监听器被重复添加。 - 优化跨线程数据传输:频繁发送int类型索引时,Native到JS的跨线程数据传输会产生大量临时对象。可合并事件或采用批量发送方式,减少传输次数。
- 用Android Profiler定位泄漏点:打开Android Studio Profiler进行内存分析,滚动后生成Heap Dump,重点排查:
- React相关对象(如
RCTDeviceEventEmitter事件队列、JSBridge相关实例) - 自定义
EventEmitter、TikTokScreenFragment是否被意外持有引用 - RecyclerView的Adapter相关对象是否存在泄漏
- React相关对象(如
- 替换事件传递方式:对于自定义组件,推荐使用
RCTEventEmitter(继承BaseJavaModule或SimpleViewManager)发送事件,而非直接获取RCTDeviceEventEmitter,这种方式更贴合RN生命周期管理,能降低内存泄漏风险。
内容的提问来源于stack exchange,提问作者quyentho
相关产品推荐
相关产品推荐

