From b675667a47f651e85d66e12daaeeec00371d1b23 Mon Sep 17 00:00:00 2001 From: hublot Date: Thu, 20 Jul 2023 10:55:54 -0700 Subject: [PATCH] Replace RCTLocalAssetImageLoader to RCTBundleAssetImageLoader (#37232) Summary: From the video below, we can see that the UI thread has dropped many frames, and it would become worse if there are multiple images. If an image is located in the sandbox of the disk, we cannot load it using `RCTLocalAssetImageLoader` because `RCTLocalAssetImageLoader.requiresScheduling` is set to true, which loads the data on the UI thread and causes main thread stuttering. This will affect libraries such as `react-native-code-push` and others that save images to the sandbox of the disk. Therefore, we should replace `RCTLocalAssetImageLoader.canLoadImageURL` from `RCTIsLocalAssetURL(url)` to `RCTIsBundleAssetURL(url)`. Similarly, we should rename the entire `RCTLocalAssetImageLoader` file with `RCTBundleAssetImageLoader`, which ignores images in the disk sandbox. And finally these images will be loaded from `NSURLRequest`, and our UI thread will run smoothly again. https://user-images.githubusercontent.com/20135674/236368418-8933a2c6-549c-40d3-a551-81b492fe41d5.mp4 ## Changelog: [IOS] [Breaking] - Replace `RCTLocalAssetImageLoader` to `RCTBundleAssetImageLoader` Pull Request resolved: https://github.com/facebook/react-native/pull/37232 Test Plan: Test Code: ```javascript constructor(props) { super(props) this.state = { bundle_image: require('./large_image.png'), sandbox_image: '', source: null, isLoading: false, } } render() { console.log('render', this.state) return ( { [{ title: 'Save Image To SandBox', onPress: () => { let image = Image.resolveAssetSource(this.state.bundle_image) console.log(image.uri) this.setState({ isLoading: true }) RNFetchBlob.config({ fileCache: true, appendExt: "png" }).fetch("GET", image.uri).then(response => { let path = response.path() path = /^file:\/\//.test(path) ? path : 'file://' + path console.log(path) this.state.sandbox_image = path }).finally(() => this.setState({ isLoading: false })) }}, { title: 'Load From SandBox', onPress: () => { this.setState({ source: { uri: this.state.sandbox_image } }) }}, { title: 'Clear', onPress: () => { this.setState({ source: null, isLoading: false }) }}, { title: 'Load From Bundle', onPress: () => { this.setState({ source: this.state.bundle_image }) }}].map((item, index) => { return ( 0 ? 15 : 0 }} onPress={item.onPress} > {item.title} ) }) } console.log(nativeEvent)} onLoadStart={() => this.setState({ isLoading: true })} onLoadEnd={() => this.setState({ isLoading: false })} /> ) } ``` It needs to be tested in three environments: [Simulator_Debug, RealDevice_Debug, RealDevice_Release] 1. Open `Perf Monitor` (RealDevice_Release can be skipped) 2. Click `Save Image to SandBox` 3. Wait for the loading to end and click `Load From SandBox` 4. Verify that the image can be loaded successfully 5. Verify that the `UI thread` keeps `60 FPS` (RealDevice_Release can be skipped) 6. Click `Clear` 7. Repeat steps [3, 4, 5, 6] several times 8. Click `Load From Bundle` to verify that the bundle image can be loaded successfully Simulator_Debug https://user-images.githubusercontent.com/20135674/236369344-ee1b8ff1-2d49-49f3-a322-d973f4adf3e7.mp4 RealDevice_Debug https://user-images.githubusercontent.com/20135674/236369356-fe440b2b-f72a-49be-b63c-b4bf709dac8c.mp4 RealDevice_Release https://user-images.githubusercontent.com/20135674/236369365-8a6a5c2f-09ad-4c90-b6bd-41e8a5e3aa7f.mp4 Reviewed By: rshest Differential Revision: D46441513 Pulled By: dmytrorykun fbshipit-source-id: 652febd4147dbff6c1ceef03d84ce125b8c66770 --- .../Libraries/AppDelegate/RCTAppSetupUtils.mm | 4 +- .../Image/RCTBundleAssetImageLoader.h | 12 +++ .../Image/RCTBundleAssetImageLoader.mm | 83 +++++++++++++++++++ .../Image/RCTLocalAssetImageLoader.h | 3 +- .../Image/RCTLocalAssetImageLoader.mm | 7 +- 5 files changed, 105 insertions(+), 4 deletions(-) create mode 100644 packages/react-native/Libraries/Image/RCTBundleAssetImageLoader.h create mode 100644 packages/react-native/Libraries/Image/RCTBundleAssetImageLoader.mm diff --git a/packages/react-native/Libraries/AppDelegate/RCTAppSetupUtils.mm b/packages/react-native/Libraries/AppDelegate/RCTAppSetupUtils.mm index 99484c42092..a416e90b907 100644 --- a/packages/react-native/Libraries/AppDelegate/RCTAppSetupUtils.mm +++ b/packages/react-native/Libraries/AppDelegate/RCTAppSetupUtils.mm @@ -10,13 +10,13 @@ #if RCT_NEW_ARCH_ENABLED // Turbo Module #import +#import #import #import #import #import #import #import -#import #import // Fabric @@ -83,7 +83,7 @@ id RCTAppSetupDefaultModuleFromClass(Class moduleClass) if (moduleClass == RCTImageLoader.class) { return [[moduleClass alloc] initWithRedirectDelegate:nil loadersProvider:^NSArray> *(RCTModuleRegistry *moduleRegistry) { - return @[ [RCTLocalAssetImageLoader new] ]; + return @[ [RCTBundleAssetImageLoader new] ]; } decodersProvider:^NSArray> *(RCTModuleRegistry *moduleRegistry) { return @[ [RCTGIFImageDecoder new] ]; diff --git a/packages/react-native/Libraries/Image/RCTBundleAssetImageLoader.h b/packages/react-native/Libraries/Image/RCTBundleAssetImageLoader.h new file mode 100644 index 00000000000..fd91a7b86d9 --- /dev/null +++ b/packages/react-native/Libraries/Image/RCTBundleAssetImageLoader.h @@ -0,0 +1,12 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +@interface RCTBundleAssetImageLoader : NSObject + +@end diff --git a/packages/react-native/Libraries/Image/RCTBundleAssetImageLoader.mm b/packages/react-native/Libraries/Image/RCTBundleAssetImageLoader.mm new file mode 100644 index 00000000000..f412e9b74c1 --- /dev/null +++ b/packages/react-native/Libraries/Image/RCTBundleAssetImageLoader.mm @@ -0,0 +1,83 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import +#import + +#import +#import + +#import "RCTImagePlugins.h" + +@interface RCTBundleAssetImageLoader () +@end + +@implementation RCTBundleAssetImageLoader + +RCT_EXPORT_MODULE() + +- (BOOL)canLoadImageURL:(NSURL *)requestURL +{ + return RCTIsBundleAssetURL(requestURL); +} + +- (BOOL)requiresScheduling +{ + // Don't schedule this loader on the URL queue so we can load the + // local assets synchronously to avoid flickers. + return NO; +} + +- (BOOL)shouldCacheLoadedImages +{ + // UIImage imageNamed handles the caching automatically so we don't want + // to add it to the image cache. + return NO; +} + +- (nullable RCTImageLoaderCancellationBlock)loadImageForURL:(NSURL *)imageURL + size:(CGSize)size + scale:(CGFloat)scale + resizeMode:(RCTResizeMode)resizeMode + progressHandler:(RCTImageLoaderProgressBlock)progressHandler + partialLoadHandler:(RCTImageLoaderPartialLoadBlock)partialLoadHandler + completionHandler:(RCTImageLoaderCompletionBlock)completionHandler +{ + UIImage *image = RCTImageFromLocalAssetURL(imageURL); + if (image) { + if (progressHandler) { + progressHandler(1, 1); + } + completionHandler(nil, image); + } else { + NSString *message = [NSString stringWithFormat:@"Could not find image %@", imageURL]; + RCTLogWarn(@"%@", message); + completionHandler(RCTErrorWithMessage(message), nil); + } + + return nil; +} + +- (std::shared_ptr)getTurboModule: + (const facebook::react::ObjCTurboModule::InitParams &)params +{ + return nullptr; +} + +- (float)loaderPriority +{ + return 1; +} + +@end + +Class RCTBundleAssetImageLoaderCls(void) +{ + return RCTBundleAssetImageLoader.class; +} diff --git a/packages/react-native/Libraries/Image/RCTLocalAssetImageLoader.h b/packages/react-native/Libraries/Image/RCTLocalAssetImageLoader.h index 453accef355..c7d3465b827 100644 --- a/packages/react-native/Libraries/Image/RCTLocalAssetImageLoader.h +++ b/packages/react-native/Libraries/Image/RCTLocalAssetImageLoader.h @@ -7,6 +7,7 @@ #import -@interface RCTLocalAssetImageLoader : NSObject +__deprecated_msg("Use RCTBundleAssetImageLoader instead") @interface RCTLocalAssetImageLoader + : NSObject @end diff --git a/packages/react-native/Libraries/Image/RCTLocalAssetImageLoader.mm b/packages/react-native/Libraries/Image/RCTLocalAssetImageLoader.mm index a79b4666b18..8f6e1b5c0d1 100644 --- a/packages/react-native/Libraries/Image/RCTLocalAssetImageLoader.mm +++ b/packages/react-native/Libraries/Image/RCTLocalAssetImageLoader.mm @@ -24,7 +24,7 @@ RCT_EXPORT_MODULE() - (BOOL)canLoadImageURL:(NSURL *)requestURL { - return RCTIsLocalAssetURL(requestURL); + return RCTIsBundleAssetURL(requestURL); } - (BOOL)requiresScheduling @@ -70,6 +70,11 @@ RCT_EXPORT_MODULE() return nullptr; } +- (float)loaderPriority +{ + return 0; +} + @end Class RCTLocalAssetImageLoaderCls(void)