如何避免RN中的内存泄漏?常见的内存泄漏场景(定时器、事件监听、异步请求未取消)及解决方案?

15 阅读2分钟

一、定时器未清除(最常见 🚨)

❌ 问题场景

useEffect(() => {
  const timer = setInterval(() => {
    console.log('running');
  }, 1000);
}, []);

👉 组件卸载后,定时器还在执行


✅ 正确写法

useEffect(() => {
  const timer = setInterval(() => {
    console.log('running');
  }, 1000);

  return () => {
    clearInterval(timer);
  };
}, []);

⚠️ 延伸(容易忽略)

  • setTimeout
  • InteractionManager.runAfterInteractions
const task = InteractionManager.runAfterInteractions(() => {});
return () => task.cancel();

二、事件监听未移除

❌ 问题场景

useEffect(() => {
  Dimensions.addEventListener('change', handler);
}, []);

👉 handler 持续存在,组件销毁后仍触发


✅ 正确写法

useEffect(() => {
  const subscription = Dimensions.addEventListener('change', handler);

  return () => {
    subscription?.remove();
  };
}, []);

⚠️ 常见泄漏来源

  • Dimensions
  • Keyboard
  • AppState
  • BackHandler
  • DeviceEventEmitter

三、异步请求未取消(非常隐蔽 ⚠️)

❌ 问题场景

useEffect(() => {
  fetchData().then(res => {
    setData(res);
  });
}, []);

👉 组件已经卸载,但请求返回后仍然 setState


✅ 方案一:标记是否卸载(推荐简单方案)

useEffect(() => {
  let isMounted = true;

  fetchData().then(res => {
    if (isMounted) {
      setData(res);
    }
  });

  return () => {
    isMounted = false;
  };
}, []);

✅ 方案二:AbortController(更优雅)

useEffect(() => {
  const controller = new AbortController();

  fetch(url, { signal: controller.signal })
    .then(res => res.json())
    .then(data => setData(data))
    .catch(err => {
      if (err.name !== 'AbortError') {
        console.error(err);
      }
    });

  return () => controller.abort();
}, []);

⚠️ axios 方案

const source = axios.CancelToken.source();

axios.get(url, {
  cancelToken: source.token
});

return () => {
  source.cancel();
};

四、闭包导致的内存泄漏(高级但常见)

❌ 问题

函数持有旧 state 引用,导致内存无法释放

useEffect(() => {
  const interval = setInterval(() => {
    console.log(count); // 旧值
  }, 1000);
}, []);

✅ 解决方案

👉 使用 useRef

const countRef = useRef(count);

useEffect(() => {
  countRef.current = count;
}, [count]);

useEffect(() => {
  const interval = setInterval(() => {
    console.log(countRef.current);
  }, 1000);

  return () => clearInterval(interval);
}, []);

五、全局变量 / 单例引用

❌ 问题

global.cache = largeObject;

👉 永远不会释放


✅ 解决

  • 避免存储大对象
  • 使用弱引用(如果有)
  • 页面卸载时手动清理

六、动画未停止(RN特有)

❌ 问题

Animated.timing(value, {
  toValue: 1,
  duration: 1000,
  useNativeDriver: true
}).start();

👉 页面卸载仍执行动画


✅ 解决

const animation = Animated.timing(...);

animation.start();

return () => {
  animation.stop();
};

七、FlatList / 大列表引起的“伪内存泄漏”

不是严格泄漏,但表现类似:

❌ 问题

  • 渲染过多 item
  • key 不稳定
  • renderItem 频繁创建

✅ 优化

<FlatList
  data={data}
  renderItem={renderItem}
  keyExtractor={item => item.id}
  removeClippedSubviews
  windowSize={5}
  initialNumToRender={10}
  maxToRenderPerBatch={10}
/>

八、第三方库未正确释放

比如:

  • WebSocket
  • 地图 SDK
  • 视频播放器

✅ 标准做法

useEffect(() => {
  const socket = new WebSocket(url);

  return () => {
    socket.close();
  };
}, []);

🔥 总结一套“防泄漏原则”

你可以记住这 5 条核心原则:

✅ 1. 有副作用,就必须有清理

  • 定时器
  • 监听器
  • 动画
  • socket

👉 一律 return cleanup


✅ 2. 异步请求必须可控

  • 标记 isMounted
  • 或 AbortController

✅ 3. 不在卸载后 setState


✅ 4. 避免闭包持有旧引用


✅ 5. 大对象谨慎缓存


🚀 给你一个工程级最佳实践模板

useEffect(() => {
  let isMounted = true;
  const controller = new AbortController();
  const timer = setInterval(() => {}, 1000);

  const subscription = Dimensions.addEventListener('change', () => {});

  fetch(url, { signal: controller.signal })
    .then(res => res.json())
    .then(data => {
      if (isMounted) setData(data);
    });

  return () => {
    isMounted = false;
    controller.abort();
    clearInterval(timer);
    subscription?.remove();
  };
}, []);