Skip to content

Commit beb14f7

Browse files
feat: optimize memory usage across components with downsampling and pagination
1 parent e89f6bd commit beb14f7

File tree

6 files changed

+251
-56
lines changed

6 files changed

+251
-56
lines changed

android/app/build.gradle

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,30 @@ android {
8787
targetSdkVersion rootProject.ext.targetSdkVersion
8888
versionCode 22
8989
versionName "1.3.3"
90+
91+
// Increase memory allocated to Gradle
92+
dexOptions {
93+
javaMaxHeapSize "4g"
94+
}
95+
}
96+
97+
// Add large heap support to the application
98+
aaptOptions {
99+
cruncherEnabled = false
90100
}
101+
102+
packagingOptions {
103+
// Exclude unnecessary files to reduce APK size
104+
exclude 'META-INF/DEPENDENCIES'
105+
exclude 'META-INF/LICENSE'
106+
exclude 'META-INF/LICENSE.txt'
107+
exclude 'META-INF/license.txt'
108+
exclude 'META-INF/NOTICE'
109+
exclude 'META-INF/NOTICE.txt'
110+
exclude 'META-INF/notice.txt'
111+
exclude 'META-INF/ASL2.0'
112+
}
113+
91114
signingConfigs {
92115
debug {
93116
storeFile file('debug.keystore')

android/app/src/main/AndroidManifest.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
android:allowBackup="false"
1212
android:theme="@style/AppTheme"
1313
android:supportsRtl="true"
14-
android:gwpAsanMode="always">
14+
android:gwpAsanMode="always"
15+
android:largeHeap="true">
1516
<activity
1617
android:name=".MainActivity"
1718
android:label="@string/app_name"

src/Components/Gallery/src/index.tsx

Lines changed: 65 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
useWindowDimensions,
1212
View,
1313
ViewStyle,
14+
InteractionManager,
1415
} from 'react-native';
1516
import Animated, {
1617
useAnimatedStyle,
@@ -41,8 +42,8 @@ import {BannerAdSize} from 'react-native-google-mobile-ads';
4142

4243
const rtl = I18nManager.isRTL;
4344

44-
const DOUBLE_TAP_SCALE = 3;
45-
const MAX_SCALE = 6;
45+
const DOUBLE_TAP_SCALE = 2.5; // Reduced from 3
46+
const MAX_SCALE = 4; // Reduced from 6
4647
const SPACE_BETWEEN_IMAGES = 40;
4748

4849
type Dimensions = {
@@ -88,6 +89,11 @@ const defaultRenderImage = ({
8889
url: item,
8990
progressiveLoadingEnabled: true,
9091
allowHardware: true,
92+
// Add downsampling to reduce memory usage
93+
downsampling: {
94+
width: 1500, // Reasonable limit for comic images
95+
height: 1800,
96+
},
9197
headers: {
9298
Referer: item,
9399
},
@@ -247,6 +253,15 @@ const ResizableImage = React.memo(
247253
offset.y.value = animated ? withTiming(0) : 0;
248254
translation.x.value = animated ? withTiming(0) : 0;
249255
translation.y.value = animated ? withTiming(0) : 0;
256+
257+
// Request garbage collection when scaling back to normal size
258+
if (scale.value > 1.5) {
259+
runOnJS(InteractionManager.runAfterInteractions)(() => {
260+
setTimeout(() => {
261+
global.gc && global.gc();
262+
}, 1000);
263+
});
264+
}
250265
};
251266

252267
const getEdgeX = () => {
@@ -262,28 +277,6 @@ const ResizableImage = React.memo(
262277
return [-point, point];
263278
};
264279

265-
const clampY = (value: number, newScale: number) => {
266-
'worklet';
267-
const newHeight = newScale * layout.y.value;
268-
const point = (newHeight - height) / 2;
269-
270-
if (newHeight < height) {
271-
return 0;
272-
}
273-
return clamp(value, -point, point);
274-
};
275-
276-
const clampX = (value: number, newScale: number) => {
277-
'worklet';
278-
const newWidth = newScale * layout.x.value;
279-
const point = (newWidth - width) / 2;
280-
281-
if (newWidth < width) {
282-
return 0;
283-
}
284-
return clamp(value, -point, point);
285-
};
286-
287280
const getEdgeY = () => {
288281
'worklet';
289282

@@ -575,11 +568,11 @@ const ResizableImage = React.memo(
575568
.onUpdate(({translationX, translationY, velocityY}) => {
576569
'worklet';
577570
if (!isActive.value) return;
571+
const x = getEdgeX();
572+
578573
if (disableVerticalSwipe && scale.value === 1 && isVertical.value)
579574
return;
580575

581-
const x = getEdgeX();
582-
583576
if (!isVertical.value || scale.value > 1) {
584577
const clampedX = clamp(
585578
translationX,
@@ -674,7 +667,6 @@ const ResizableImage = React.memo(
674667
let snapPoints = [index - 1, index, index + 1]
675668
.filter((_, y) => {
676669
if (loop) return true;
677-
678670
if (y === 0) {
679671
return !isFirst;
680672
}
@@ -848,6 +840,42 @@ const ResizableImage = React.memo(
848840
}
849841
});
850842

843+
// Cleanup on unmount to prevent memory leaks
844+
useEffect(() => {
845+
return () => {
846+
InteractionManager.runAfterInteractions(() => {
847+
offset.x.value = 0;
848+
offset.y.value = 0;
849+
scale.value = 1;
850+
translation.x.value = 0;
851+
translation.y.value = 0;
852+
});
853+
};
854+
}, []);
855+
856+
// Clamp functions
857+
const clampY = (value: number, newScale: number) => {
858+
'worklet';
859+
const newHeight = newScale * layout.y.value;
860+
const point = (newHeight - height) / 2;
861+
862+
if (newHeight < height) {
863+
return 0;
864+
}
865+
return clamp(value, -point, point);
866+
};
867+
868+
const clampX = (value: number, newScale: number) => {
869+
'worklet';
870+
const newWidth = newScale * layout.x.value;
871+
const point = (newWidth - width) / 2;
872+
873+
if (newWidth < width) {
874+
return 0;
875+
}
876+
return clamp(value, -point, point);
877+
};
878+
851879
return (
852880
<GestureDetector
853881
gesture={Gesture.Race(
@@ -929,7 +957,6 @@ const GalleryComponent = <T extends any>(
929957
) => {
930958
const windowDimensions = useWindowDimensions();
931959
const dimensions = containerDimensions || windowDimensions;
932-
933960
const isLoop = loop && data?.length > 1;
934961

935962
const [index, setIndex] = useState(initialIndex);
@@ -998,6 +1025,16 @@ const GalleryComponent = <T extends any>(
9981025
// eslint-disable-next-line react-hooks/exhaustive-deps
9991026
}, [data?.length, dimensions.width]);
10001027

1028+
// Release resources when Gallery is unmounted
1029+
useEffect(() => {
1030+
return () => {
1031+
InteractionManager.runAfterInteractions(() => {
1032+
refs.current = [];
1033+
global.gc && global.gc();
1034+
});
1035+
};
1036+
}, []);
1037+
10011038
return (
10021039
<GestureHandlerRootView style={[styles.container, style]}>
10031040
<Animated.View style={[styles.rowContainer, animatedStyle]}>

src/Components/UIComp/Image.js

Lines changed: 76 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import React, {useState} from 'react';
1+
import React, {useState, useEffect} from 'react';
22
import {
33
ActivityIndicator,
44
Image as Img,
55
Platform,
66
Text,
77
View,
8+
Dimensions,
89
} from 'react-native';
910
import {FasterImageView} from '@candlefinance/faster-image';
1011

@@ -14,8 +15,73 @@ const Image = ({
1415
onFailer,
1516
resizeMode = null,
1617
onSuccess = null,
18+
downsample = true, // Enable downsampling by default
1719
...rest
1820
}) => {
21+
const [imageError, setImageError] = useState(false);
22+
const {width: screenWidth, height: screenHeight} = Dimensions.get('window');
23+
24+
// Calculate optimal resize dimensions based on screen size and style
25+
const getOptimalDimensions = () => {
26+
// If style has specific dimensions, use those as a base
27+
let targetWidth = style?.width || screenWidth;
28+
let targetHeight = style?.height || screenHeight;
29+
30+
// Convert percentage values to actual dimensions
31+
if (typeof targetWidth === 'string' && targetWidth.endsWith('%')) {
32+
const percentage = parseFloat(targetWidth) / 100;
33+
targetWidth = screenWidth * percentage;
34+
}
35+
if (typeof targetHeight === 'string' && targetHeight.endsWith('%')) {
36+
const percentage = parseFloat(targetHeight) / 100;
37+
targetHeight = screenHeight * percentage;
38+
}
39+
40+
// For flex layouts, use screen dimensions
41+
if (targetWidth === undefined || targetHeight === undefined) {
42+
targetWidth = screenWidth;
43+
targetHeight = screenHeight;
44+
}
45+
46+
// Calculate a reasonable downsampling value to save memory
47+
// The 2 multiplier accounts for high-density screens
48+
return {
49+
width: Math.round(targetWidth * 2),
50+
height: Math.round(targetHeight * 2),
51+
};
52+
};
53+
54+
const dimensions = getOptimalDimensions();
55+
56+
// Clean up error state when source changes
57+
useEffect(() => {
58+
setImageError(false);
59+
}, [source]);
60+
61+
// If image loading fails
62+
const handleError = (event) => {
63+
setImageError(true);
64+
if (onFailer) {
65+
onFailer(event);
66+
}
67+
};
68+
69+
// Handle successful image load
70+
const handleSuccess = (event) => {
71+
if (onSuccess) {
72+
onSuccess(event);
73+
}
74+
};
75+
76+
// If there's an error, show a fallback
77+
if (imageError) {
78+
return (
79+
<View style={[style, { justifyContent: 'center', alignItems: 'center' }]}>
80+
<Text style={{ color: '#999' }}>Image Error</Text>
81+
</View>
82+
);
83+
}
84+
1985
return (
2086
<View style={[style]}>
2187
<FasterImageView
@@ -33,15 +99,16 @@ const Image = ({
3399
headers: {
34100
Referer: source.uri,
35101
},
102+
// Add downsampling configuration if enabled
103+
...(downsample ? {
104+
downsampling: {
105+
width: dimensions.width,
106+
height: dimensions.height,
107+
}
108+
} : {}),
36109
}}
37-
onSuccess={event => {
38-
if (onSuccess) onSuccess(event);
39-
}}
40-
onError={event => {
41-
if (onFailer) {
42-
onFailer(event);
43-
}
44-
}}
110+
onSuccess={handleSuccess}
111+
onError={handleError}
45112
/>
46113
</View>
47114
);

src/Components/VerticalGallery/index.js

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ const VerticalGallery = ({
7272
source={{ uri: item }}
7373
style={styles.image}
7474
resizeMode="contain"
75+
// Enable downsampling to reduce memory usage
76+
downsample={true}
7577
/>
7678
</View>
7779
</TouchableWithoutFeedback>
@@ -84,23 +86,24 @@ const VerticalGallery = ({
8486
index,
8587
});
8688

87-
// Debug the data prop
88-
console.log('VerticalGallery data:', data && data.length ? 'Has items' : 'Empty or invalid',
89-
Array.isArray(data) ? `(${data.length} items)` : '(Not an array)');
89+
// Memory cleanup for images that are no longer visible
90+
const keyExtractor = useCallback((_, index) => `page-${index}`, []);
9091

9192
return (
9293
<FlatList
9394
ref={flatListRef}
9495
data={data}
9596
renderItem={renderItem}
96-
keyExtractor={(_, index) => index.toString()}
97+
keyExtractor={keyExtractor}
9798
pagingEnabled
9899
showsVerticalScrollIndicator={false}
99100
viewabilityConfigCallbackPairs={viewabilityConfigCallbackPairs.current}
100101
getItemLayout={getItemLayout}
101-
initialNumToRender={3}
102-
maxToRenderPerBatch={3}
103-
windowSize={5}
102+
initialNumToRender={2} // Reduced from 3 to 2
103+
maxToRenderPerBatch={2} // Reduced to save memory
104+
windowSize={3} // Reduced from 5 to 3
105+
removeClippedSubviews={true} // Important for memory management
106+
updateCellsBatchingPeriod={100} // Delay updates to improve performance
104107
style={styles.container}
105108
onScrollToIndexFailed={info => {
106109
const wait = new Promise(resolve => setTimeout(resolve, 100));

0 commit comments

Comments
 (0)