美文网首页rn
RN图片加载和原生统一

RN图片加载和原生统一

作者: jayhe | 来源:发表于2020-08-10 21:39 被阅读0次

    RN图片加载和原生统一

    针对RN和原生混合开发的项目,由于图片的加载RN有自己的一套机制,跟原生的是分开的,就存在加载和缓存的差异性;我们可以做一些工作让图片的加载统一成一套,这对于维护和做一些优化都是有益处的

    1. RN图片加载框架

    在对RN的图片加载做优化之前,我们得先知道RN的图片加载框架流程;

    RN的图片的加载、缓存、解码等都是在RCTImageLoader中处理的,这里大概梳理了下其中的结构

    RCTImageLoader.jpg

    RCTImageLoader可以划分为以下几个模块:

    • 图片加载 RCTImageURLLoader
    • 图片缓存 RCTImageCache
    • 图片解码 RCTImageDataDecoder

    为了增强模块的可扩展性,RN将这三个核心模块都提供了外部可定制的能力,通过set方法、或者协议的方式;

    1.1 图片的缓存

    定义了RCTImageCache协议,同时提供了- (void)setImageCache:(id<RCTImageCache>)cache;来供外部去定义图片缓存模块

    /**
     * Provides an interface to use for providing a image caching strategy.
     */
    @protocol RCTImageCache <NSObject>
    
    - (UIImage *)imageForUrl:(NSString *)url
                        size:(CGSize)size
                       scale:(CGFloat)scale
                  resizeMode:(RCTResizeMode)resizeMode;
    
    - (void)addImageToCache:(UIImage *)image
                        URL:(NSString *)url
                       size:(CGSize)size
                      scale:(CGFloat)scale
                 resizeMode:(RCTResizeMode)resizeMode
                   response:(NSURLResponse *)response;
    
    @end
    

    如果我们设置了自定义的图片缓存,那么就使用自定义的,否则RN内部会使用默认的RCTImageCache;这个默认的实现只做了内存缓存,没有做磁盘缓存的

    // 提供set方法供外部设置图片缓存模块
    - (void)setImageCache:(id<RCTImageCache>)cache;
    
    - (void)setImageCache:(id<RCTImageCache>)cache
    {
        if (_imageCache) {
            RCTLogWarn(@"RCTImageCache was already set and has now been overriden.");
        }
        _imageCache = cache;
    }
    
    // 如果设置了图片缓存模块则用外部设置的,否则使用默认实现
    - (id<RCTImageCache>)imageCache
    {
        if (!_imageCache) {
            //set up with default cache
            _imageCache = [RCTImageCache new];
        }
        return _imageCache;
    }
    
    1.2 图片加载

    图片加载的统一入口函数:

    - (RCTImageLoaderCancellationBlock)loadImageWithURLRequest:(NSURLRequest *)imageURLRequest
                                                          size:(CGSize)size
                                                         scale:(CGFloat)scale
                                                       clipped:(BOOL)clipped
                                                    resizeMode:(RCTResizeMode)resizeMode
                                                 progressBlock:(RCTImageLoaderProgressBlock)progressBlock
                                              partialLoadBlock:(RCTImageLoaderPartialLoadBlock)partialLoadBlock
                                               completionBlock:(RCTImageLoaderCompletionBlock)completionBlock;
    

    同时定义了RCTImageURLLoader协议,将图片的加载能力进行抽象;协议中的canLoadImageURL:方法用来定义该Loader支持的URL资源的加载,这样不同的图片资源就可以使用不同的Loader去加载

    /**
     * Provides the interface needed to register an image loader. Image data
     * loaders are also bridge modules, so should be registered using
     * RCT_EXPORT_MODULE().
     */
    @protocol RCTImageURLLoader <RCTBridgeModule>
    
    // 是否是该loader支持加载的图片URL
    - (BOOL)canLoadImageURL:(NSURL *)requestURL;
    
    // 图片加载入口函数
    - (RCTImageLoaderCancellationBlock)loadImageForURL:(NSURL *)imageURL
                                                  size:(CGSize)size
                                                 scale:(CGFloat)scale
                                            resizeMode:(RCTResizeMode)resizeMode
                                       progressHandler:(RCTImageLoaderProgressBlock)progressHandler
                                    partialLoadHandler:(RCTImageLoaderPartialLoadBlock)partialLoadHandler
                                     completionHandler:(RCTImageLoaderCompletionBlock)completionHandler;
    
    @optional
    
    // loader 优先级
    - (float)loaderPriority;
    
    // 是否需要将任务放到内部的串行队列去执行,默认是YES在主线程执行
    - (BOOL)requiresScheduling;
    
    // 是否缓存图片,默认是YES
    - (BOOL)shouldCacheLoadedImages;
    
    @end
    

    RCTImageCache不同的是,图片加载loader不是通过提供set接口去定制,而是通过RCT_EXPORT_MODULE()的方式导出模块给RN,内部去获取实现了RCTImageURLLoader协议的loaders列表_loaders = [_bridge modulesConformingToProtocol:@protocol(RCTImageURLLoader)]

    - (id<RCTImageURLLoader>)imageURLLoaderForURL:(NSURL *)URL
    {
        if (!_maxConcurrentLoadingTasks) {
            [self setUp];
        }
    
        if (!_loaders) {
            // Get loaders, sorted in reverse priority order (highest priority first)
            RCTAssert(_bridge, @"Bridge not set");
            _loaders = [[_bridge modulesConformingToProtocol:@protocol(RCTImageURLLoader)] sortedArrayUsingComparator:^NSComparisonResult(id<RCTImageURLLoader> a, id<RCTImageURLLoader> b) {
                float priorityA = [a respondsToSelector:@selector(loaderPriority)] ? [a loaderPriority] : 0;
                float priorityB = [b respondsToSelector:@selector(loaderPriority)] ? [b loaderPriority] : 0;
                if (priorityA > priorityB) {
                    return NSOrderedAscending;
                } else if (priorityA < priorityB) {
                    return NSOrderedDescending;
                } else {
                    return NSOrderedSame;
                }
            }];
        }
        // ...
        // Normal code path
        for (id<RCTImageURLLoader> loader in _loaders) {
            if ([loader canLoadImageURL:URL]) {
                return loader;
            }
        }
        return nil;
    }
    

    RN图片加载模块默认实现了2类资源的Loader

    • RCTPhotoLibraryImageLoader 相册资源
    • RCTLocalAssetImageLoader 本地资源

    对于网络图片的加载,没有内置的Loader,假如外部没有定义该类型的loader则默认走的RCTNetworking模块去加载的

    我们如果想对于网络资源图片的加载走跟原生一样的模块(比如SDWebImage)则可以通过2种方式去实现:

    1. 可以通过定义一个Loader并通过RCT_EXPORT_MODULE()的方式导出模块给RN就能实现
    2. hook内部的默认网络资源加载函数loadImageWithURLRequest:
    1.3 图片解码

    针对请求返回的是data类型的数据,则需要去将data解码成图片;通过定义了RCTGIFImageDecoder协议来将解码的能力抽象,外部则可以实现对应的解码器来解码不同类型的数据,这里的设计跟上面介绍的Loader的设计是一样的

    /**
     * Provides the interface needed to register an image decoder. Image decoders
     * are also bridge modules, so should be registered using RCT_EXPORT_MODULE().
     */
    @protocol RCTImageDataDecoder <RCTBridgeModule>
    
    // 判断数据是否是该loader可以解码的
    - (BOOL)canDecodeImageData:(NSData *)imageData;
    
    // 解码函数,传入imageData解码得到image
    - (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)imageData
                                                  size:(CGSize)size
                                                 scale:(CGFloat)scale
                                            resizeMode:(RCTResizeMode)resizeMode
                                     completionHandler:(RCTImageLoaderCompletionBlock)completionHandler;
    
    @optional
    
    // 优先级,内部会根据这个来排序,使用优先级高的decoder
    - (float)decoderPriority;
    
    @end
    

    RN模块默认内置了一个RCTGIFImageDecodergif的解码模块,假如我们需要支持WebP,那么就可以定义一个WebP的Decoder来实现解码

    2. RN和原生的图片缓存统一

    由于RN和原生的图片加载是2个模块去实现的,这就存在一张图可能RN侧加载缓存了、原生侧也加载缓存了,这就造成了资源的重复加载以及无法复用缓存的问题;

    同时RN的缓存模块还只是做了内存缓存的,app杀掉下次打开则还是又会发起网络请求去加载,这就造成了不必要的请求浪费

    为了解决这些问题,将两端的图片缓存统一就显得有必要,同时统一了之后,后续需要做修改或者优化则不用2端都去修改,增强了可维护性

    2.1 自定义ImageCache

    RCTImageLoader也提供了缓存协议以及设置缓存的函数,自定制起来也很简单,定义一个Cache实现RCTImageCache协议

    @interface HCRNImageCache : NSObject <RCTImageCache>
    
    @end
    

    项目用的是SDWebImage做图片加载的,那么缓存的内部实现就是用SD那一套,只需要将缓存的key跟SD保持一致,那么读取和写入就统一了

    @implementation HCRNImageCache
    
    #pragma mark - RCTImageCache
    
    - (void)addImageToCache:(UIImage *)image URL:(NSString *)url size:(CGSize)size scale:(CGFloat)scale resizeMode:(RCTResizeMode)resizeMode response:(NSURLResponse *)response {
        NSString *cacheKey = [HCImageLoaderUtility cacheKeyWithUrlString:url size:size scale:scale];
        if ([self isURLInBlackList:url]) {
            [[SDImageCache sharedImageCache] storeImage:image forKey:cacheKey toDisk:NO completion:nil];
        } else {
            [[SDImageCache sharedImageCache] storeImage:image forKey:cacheKey toDisk:YES completion:nil];
        }
    }
    
    - (UIImage *)imageForUrl:(NSString *)url size:(CGSize)size scale:(CGFloat)scale resizeMode:(RCTResizeMode)resizeMode {
        NSString *cacheKey = [HCImageLoaderUtility cacheKeyWithUrlString:url size:size scale:scale];
        if ([self isURLInBlackList:url]) {
            return [[SDImageCache sharedImageCache] imageFromMemoryCacheForKey:cacheKey];
        } else {
            return [[SDImageCache sharedImageCache] imageFromCacheForKey:cacheKey];
        }
    }
    
    #pragma mark - Private Methods
    // 这里用来判断哪些资源不是网络资源:比如本地资源或者RN调试模式的本地资源
    - (BOOL)isURLInBlackList:(NSString *)url {
        return [HCImageLoaderUtility isLocalAssetURL:url];
    }
    
    @end
    
    

    这里我将用到的一些工具方法抽到一个工具类中

    // 由于有一些是工具方法,就抽出来一个工具类来
    @implementation HCImageLoaderUtility
    
    // 根据URL返回缓存的key,内部实现就是SD那一套
    + (NSString *)cacheKeyWithUrlString:(NSString *)urlString size:(CGSize)size scale:(CGFloat)scale {
        NSString *cacheKey = [self sdCacheKeyWithUrlString:urlString];
        
        return cacheKey;
    }
    
    + (NSString *)sdCacheKeyWithUrlString:(NSString *)urlString {
        NSString *cacheKey = [[SDWebImageManager sharedManager] cacheKeyForURL:[NSURL URLWithString:urlString]];
        
        return cacheKey;
    }
    
    // 判断URL是否是本地的资源文件
    + (BOOL)isLocalAssetURL:(NSString *)url {
        // 调试RN模式
        BOOL isDebugMode = [url rangeOfString:@"http"].location != NSNotFound && [url rangeOfString:@"8081/assets"].location != NSNotFound;
        // 加载本地图片文件
        BOOL isLocalAsset = [url rangeOfString:@"file://"].location != NSNotFound;
        
        return isDebugMode || isLocalAsset;
    }
    
    + (BOOL)isURLNotSupportFormat:(NSString *)url {
        // 非阿里云返回原地址; 动图返回原地址
        if (![url containsString:@"oss"] || ![url containsString:@"aliyuncs.com"] || [url containsString:@".gif"]) {
            return YES;
        }
        
        return NO;
    }
    
    + (BOOL)checkNeedSetSizeCompressFormatWithURLString:(NSString *)url size:(CGSize)size scale:(CGFloat)scale {
        CGFloat width = size.width * scale;
        CGFloat height = size.height * scale;
        BOOL willSetSize = YES;
        if (width <= 0 || width > 4096 || height <= 0 || height > 4096) {
            willSetSize = NO;
        }
        
        return willSetSize;
    }
    @end
    
    2.2 注入自定义ImageCache到RN模块

    注入则需要注意注入的时机,以及当bridge reload之后需要重新注入(reload之后会重新加载RN模块,RN模块load完成会有一个通知RCTJavaScriptDidLoadNotification);这里我们使用一个管理类来处理注入的逻辑,监听这个RN模块load完成的通知,然后去设置自定义的ImageCache即可

    @interface HCRNImageLoader ()
    
    @property (nonatomic, nullable, strong) HCRNImageCache *imageCache;
    
    @end
    
    @implementation HCRNImageLoader
    
    - (void)dealloc {
        [[NSNotificationCenter defaultCenter] removeObserver:self];
    }
    
    + (instancetype)sharedInstance {
        static HCRNImageLoader *_instance;
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            _instance = [self new];
        });
        
        return _instance;
    }
    
    - (void)startObserver {
        [self addNotifications];
    }
    
    - (void)registerHCImageLoader {
        // 你的项目的RCTbridge实例
        SomeBridge.imageLoader setImageCache:self.imageCache];
    }
    
    - (void)unregisterHCImageLoader {
        self.imageCache = nil;
        SomeBridge.imageLoader setImageCache:nil];
        [[NSNotificationCenter defaultCenter] removeObserver:self];
    }
    
    #pragma mark - Private Methods
    
    - (void)addNotifications {
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(jsBridgeReloaded) name:RCTJavaScriptDidLoadNotification object:nil];
    }
    
    - (void)jsBridgeReloaded {
        [self registerHCImageLoader];
    }
    
    #pragma mark - Getter && Setter
    
    - (HCRNImageCache *)imageCache {
        if (_imageCache == nil) {
            _imageCache = [HCRNImageCache new];
        }
        
        return _imageCache;
    }
    
    @end
    
    3. RN和原生的图片加载统一

    图片的加载统一上面也介绍了有2种方式可以实现,自定义RCTImageURLLoader或者hook RN的图片加载入口函数loadImageWithURLRequest:

    下面介绍定义loader的方式:

    @implementation HCRNURLImageLoader
    
    RCT_EXPORT_MODULE()
    
    - (BOOL)canLoadImageURL:(NSURL *)requestURL {
    // 这里的逻辑实现根据项目的图片资源的URL格式来实现即可
        return (!requestURL.isFileURL && [requestURL.absoluteString containsString:@"https://"]);
    }
    
    - (BOOL)shouldCacheLoadedImages {
        return YES;
    }
    
    - (RCTImageLoaderCancellationBlock)loadImageForURL:(NSURL *)imageURL size:(CGSize)size scale:(CGFloat)scale resizeMode:(RCTResizeMode)resizeMode progressHandler:(RCTImageLoaderProgressBlock)progressHandler partialLoadHandler:(RCTImageLoaderPartialLoadBlock)partialLoadHandler completionHandler:(RCTImageLoaderCompletionBlock)completionHandler {
        // 取缓存
        NSString *cacheKey = [HCRNImageLoaderUtility cacheKeyWithUrlString:imageURL.absoluteString size:size scale:scale];
        UIImage *cachedImage = [[SDImageCache sharedImageCache] imageFromCacheForKey:cacheKey];
        if (cachedImage) {
            if (completionHandler) {
                completionHandler(nil, cachedImage);
            }
        } else {
            // 取网络
            [[SDWebImageDownloader sharedDownloader] downloadImageWithURL:imageURL
                                                                  options:0
                                                                 progress:^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
                if (progressHandler) {
                    progressHandler(receivedSize, expectedSize);
                }
            } completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, BOOL finished) {
                if (image) {
                    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                        [[SDImageCache sharedImageCache] storeImage:image forKey:cacheKey completion:nil];
                    });
                }
                if (completionHandler) {
                    completionHandler(error, image);
                }
            }];
        }
    
        return ^{};
    }
    
    

    需要注意的是如果自己实现了Loader,则对应资源的加载直接托管给Loader去实现了,我们需要在loader中处理缓存、解码等这一套流程

    RCTImageLoader代码片段截取

    if (loadHandler) {
                cancelLoad = [loadHandler loadImageForURL:request.URL
                                                     size:size
                                                    scale:scale
                                               resizeMode:resizeMode
                                          progressHandler:progressHandler
                                       partialLoadHandler:partialLoadHandler
                                        completionHandler:^(NSError *error, UIImage *image) {
                                            completionHandler(error, image, nil);
                                        }];
            } else {
                UIImage *image;
                if (cacheResult) {
                    image = [[strongSelf imageCache] imageForUrl:request.URL.absoluteString
                                                            size:size
                                                           scale:scale
                                                      resizeMode:resizeMode];
                }
    
                if (image) {
                    completionHandler(nil, image, nil);
                } else {
                    // Use networking module to load image
                    cancelLoad = [strongSelf _loadURLRequest:request
                                               progressBlock:progressHandler
                                             completionBlock:completionHandler];
                }
            }
    

    你可以选择这种方式定义一个Loader去处理,也可以不做处理让走默认的图片加载流程

    4. 图片加载的一些优化

    图片加载优化除了缓存之外,还包括按需加载(按视图尺寸加载)、压缩(压缩参数、WebP格式等等)、裁剪(圆角)等等,现在主流的文件托管平台都支持通过配置参数来获取定制化的图片资源

    这些通过定制URL的图片格式化参数来加载图片带来的益处是值得去做的

    • 按视图尺寸加载 -- 减少了视图渲染的Color Misaligned Images
    • 压缩参数 -- 降低了图片资源的大小,提升了下载的效率
    • 裁剪等参数 -- 不需要代码去处理特殊的效果,通常一些效果还会触发离屏渲染

    项目中图片是放在阿里云OSS上,我们可以根据文档来设置这些参数图片缩放

    那么想让RN的图片加载也可以去设置图片处理的参数,当然也可以在自定义的Loader中去处理URL拼接format参数,或者不自定义Loader的话直接hook RN图片加载的入口函数loadImageWithURLRequest:

    @implementation RCTImageLoader (HCLoader)
    
    + (void)load {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            [self swizzleInstanceMethod:@selector(loadImageWithURLRequest:size:scale:clipped:resizeMode:progressBlock:partialLoadBlock:completionBlock:) with:@selector(hc_loadImageWithURLRequest:size:scale:clipped:resizeMode:progressBlock:partialLoadBlock:completionBlock:)];
        });
    }
    
    - (RCTImageLoaderCancellationBlock)hc_loadImageWithURLRequest:(NSURLRequest *)imageURLRequest
                                                               size:(CGSize)size
                                                              scale:(CGFloat)scale
                                                            clipped:(BOOL)clipped
                                                         resizeMode:(RCTResizeMode)resizeMode
                                                      progressBlock:(RCTImageLoaderProgressBlock)progressBlock
                                                   partialLoadBlock:(RCTImageLoaderPartialLoadBlock)partialLoadBlock
                                                    completionBlock:(RCTImageLoaderCompletionBlock)completionBlock {
        NSString *formattedURLString = [self formatURLWithURLString:imageURLRequest.URL.absoluteString size:size scale:scale mode:resizeMode];
        NSMutableURLRequest *tmpURLRequest = imageURLRequest.mutableCopy;
        tmpURLRequest.URL = [NSURL URLWithString:formattedURLString];
        return [self hc_loadImageWithURLRequest:tmpURLRequest
                                             size:size
                                            scale:scale
                                          clipped:clipped
                                       resizeMode:resizeMode
                                    progressBlock:progressBlock
                                 partialLoadBlock:partialLoadBlock
                                  completionBlock:completionBlock];
    }
    
    - (NSString *)formatURLWithURLString:(NSString *)urlString size:(CGSize)size scale:(CGFloat)scale mode:(RCTResizeMode)resizeMode {
        BOOL isInBlackList = [self isURLInBlackList:urlString];
        if (isInBlackList) { // 不需要拼接参数的直接返回原URL
            return urlString;
        }
        
        // 根据传入的width、height、scale、resizeMode来拼接url的format参数,格式类似这样 ?x-oss-process=image/resize,m_lfit,w_148,h_148/format,webp
        BOOL needSetSizeFormat = [HCImageLoaderUtility checkNeedSetSizeCompressFormatWithURLString:urlString size:size scale:scale];
        // ?x-oss-process=image/resize,m_lfit,w_148,h_148/format,webp
        NSString *resizeModeString = @"lfit"; // oss默认值
        switch (resizeMode) {
            case RCTResizeModeCover: // UIViewContentModeScaleAspectFill
                resizeModeString = @"fill";
                break;
            case RCTResizeModeContain: // UIViewContentModeScaleAspectFit
                resizeModeString = @"pad";
                break;
            case RCTResizeModeStretch: // UIViewContentModeScaleToFill
                resizeModeString = @"fixed";
                break;
            case RCTResizeModeCenter: // UIViewContentModeCenter
                resizeModeString = @"fill";
                break;
            default:
                resizeModeString = @"lfit";
                break;
        }
        NSMutableString *formattedString = urlString.mutableCopy;
        // 缩放配置
        [formattedString appendFormat:@"?x-oss-process=image/resize,m_%@", resizeModeString];
        if (needSetSizeFormat) {
            // 宽高设置
            [formattedString appendFormat:@",w_%.0f,h_%.0f", ceil(size.width * scale), ceil(size.height * scale)];
        }
        // WebP格式设置
        [formattedString appendFormat:@"/format,webp"];
        return formattedString;
    }
    
    // 这里根据需求,将不支持参数的URL、或者不需要拼接参数的URL过滤掉
    - (BOOL)isURLInBlackList:(NSString *)url {
        BOOL isLocalAsset = [HCImageLoaderUtility isLocalAssetURL:url];
        BOOL isNotSupportFormat = [HCImageLoaderUtility isURLNotSupportFormat:url];
        
        return isLocalAsset || isNotSupportFormat;
    }
    
    @end
    

    我们设置了图片格式WebP,那么默认的RCTImageLoader是么有WebP格式的Decoder,此时就需要实现一个供其使用,定义一个实现协议RCTImageDataDecoder的解码器

    
    @interface HCRNWebPImageDecoder : NSObject <RCTImageDataDecoder>
    
    @end
    
    @implementation HCRNWebPImageDecoder
    
    RCT_EXPORT_MODULE()
    
    - (BOOL)canDecodeImageData:(NSData *)imageData {
        return [[SDWebImageWebPCoder sharedCoder] canDecodeFromData:imageData];
    }
    
    - (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)imageData
                                                  size:(CGSize)size
                                                 scale:(CGFloat)scale
                                            resizeMode:(RCTResizeMode)resizeMode
                                     completionHandler:(RCTImageLoaderCompletionBlock)completionHandler {
        UIImage *image = [[SDWebImageWebPCoder sharedCoder] decodedImageWithData:imageData];
        if (completionHandler) {
            if (image) {
                completionHandler(nil, image);
            } else {
                completionHandler([NSError errorWithDomain:NSCocoaErrorDomain code:0 userInfo:@{NSLocalizedFailureReasonErrorKey : @"解码失败"}], nil);
            }
        }
        
        return ^{};
    }
    
    @end
    

    至此RN的图片加载就也支持设置format参数获取处理过的图片,同时也支持WebP格式的图片的加载了

    5. 总结

    在做RN的图片加载模块优化的时候,阅读了RCTImageLoader的源码,也学到了一些设计的理念,模块划分很清晰:Cache、Loader、Decoder;同时也提供了接口或者协议的方式将能力抽象,供外部可定制化。这样在做定制的时候就很清晰,而不用去各种hook方法去实现。

    相关文章

      网友评论

        本文标题:RN图片加载和原生统一

        本文链接:https://www.haomeiwen.com/subject/yydsdktx.html