callstack / react-native-pager-view

React Native wrapper for the Android ViewPager and iOS UIPageViewController.
MIT License
2.68k stars 411 forks source link

Memory Leaks when component is disposed #702

Open Shaw-Signaturize opened 1 year ago

Shaw-Signaturize commented 1 year ago

Environment

System: OS: macOS 13.2.1 CPU: (8) x64 Intel(R) Core(TM) i7-7820HQ CPU @ 2.90GHz Memory: 16.34 MB / 16.00 GB Shell: 3.2.57 - /bin/bash Binaries: Node: 16.17.1 - ~/.nvm/versions/node/v16.17.1/bin/node Yarn: 1.17.3 - /usr/local/bin/yarn npm: 8.15.0 - ~/.nvm/versions/node/v16.17.1/bin/npm Watchman: 4.9.0 - /usr/local/bin/watchman Managers: CocoaPods: 1.11.2 - /usr/local/bin/pod SDKs: iOS SDK: Platforms: DriverKit 22.2, iOS 16.2, macOS 13.1, tvOS 16.1, watchOS 9.1 Android SDK: Not Found IDEs: Android Studio: 3.4 AI-183.6156.11.34.5522156 Xcode: 14.2/14C18 - /usr/bin/xcodebuild Languages: Java: Not Found npmPackages: @react-native-community/cli: Not Found react: 18.2.0 => 18.2.0 react-native: 0.71.3 => 0.71.3 react-native-macos: Not Found npmGlobalPackages: react-native: Not Found

Description

When using pager view with larger resources, memory seems to leak by the amount contained in the active page. Originally discovered in a project using a routing library but for this example the toggle component simulates a page disposal. As resources are proportionally small for the example to reproduce press the toggle button multiple times Although the leaking memory from this example is small It has been able to reach over 8gb where images have client side processing applied such as grayscale.

Memory Profile with pager view included

image

Memory Profile with pager view excluded

image

Reproducible Demo

import React, { useMemo, useState, useCallback } from 'react';
import { 
  View, 
  Button,
  StyleSheet, 
  SafeAreaView, 
} from 'react-native';

import FastImage from 'react-native-fast-image'

import PagerView from 'react-native-pager-view';

const data = new Array(100).fill(1)

function App() {
  const [visible, setVisible] = useState(true)

  const onPress = useCallback(() => {
    setVisible((x) => !x)
  }, [])

  return (
    <SafeAreaView style={{ flex: 1}}>
      <View style={{ flex: 0 }}>
        <Button title={'Toggle'} onPress={onPress} />
      </View>
      <Toggle visible={visible}>
        <Pager />
      </Toggle>
    </SafeAreaView>
  );
};

function Toggle({ visible, children }) {
  return visible
    ? children
    : null
}

function Pager() {  
  const content = useMemo(() => {
    return data.map((x, i) => {
      return <Image value={i} key={i} />
    })
  }, [data])

  return (
    // Apply comment to the below line to demo memory is stable without this component
    <PagerView style={styles.pagerView} initialPage={0}>
      <View key="1">
        {content}
      </View>
    </PagerView>
  )
}

function Image({ value }) {
  const source = useMemo(() => {
    const i = value % 40
    return { uri: `https://unsplash.it/1200/1200?image=${i}` }
  }, [value])

  return (
    <FastImage
      style={styles.image}
      source={source}
      resizeMode={FastImage.resizeMode.cover}
    />
  )
}

const styles = StyleSheet.create({
  pagerView: {
    flex: 1,
  },
  image: {
    margin: 8,
    aspectRatio: 1,
    width: 400,
    height: 400
  }
});

export default App
okwasniewski commented 1 year ago

Hello, which version of pager-view did you test this on?

Shaw-Signaturize commented 1 year ago

@okwasniewski

Originally discovered with 5.4.9 but persisted after upgrading to 6.1.4

Also seems to persist in fresh projects with 6.1.0-rc.2

zyestin commented 3 months ago

Just change 'NSHashTableStrongMemory' to 'NSHashTableWeakMemory' in file ReactNativePageView.m. The item view outside the window can then be freed. And memory can be easily stabilized.

Use this patch react-native-pager-view+5.4.25.patch as below

diff --git a/node_modules/react-native-pager-view/ios/ReactNativePageView.m b/node_modules/react-native-pager-view/ios/ReactNativePageView.m
index eacfbe8..c61745f 100644
--- a/node_modules/react-native-pager-view/ios/ReactNativePageView.m
+++ b/node_modules/react-native-pager-view/ios/ReactNativePageView.m
@@ -44,7 +44,7 @@ - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher {
         _dismissKeyboard = UIScrollViewKeyboardDismissModeNone;
         _coalescingKey = 0;
         _eventDispatcher = eventDispatcher;
-        _cachedControllers = [NSHashTable hashTableWithOptions:NSHashTableStrongMemory];
+        _cachedControllers = [NSHashTable hashTableWithOptions:NSHashTableWeakMemory];
         _overdrag = NO;
         _layoutDirection = @"ltr";
     }