探究react-native 源码的图片缓存

作者: 请叫我啊亮 | 来源:发表于2017-08-24 13:55 被阅读325次

    本文为xcode模拟器测试,rn版本0.44.3
    突然想学习下RN是如何封装ios中的UIImage的,看着看着发现图片的缓存问题是个坑。。。
    先看js端图片使用的三种方式,依次排序1、2、3

     <Image source={{uri:url}} style={{width:200,height:200}}/>    1、 加载远程图片
     <Image source={{uri:'1.png'}} style={{width:50,height:50}}/>  2、加载xcode中图片
     <Image source={require('../../../Resources/Images/Contact/conact_searchIcon@3x.png')}/>  3、加载js中图片
    

    1、2必须设置图片宽高,3不需设置。

    对应的ios原生端文件是RCTImageViewManager,暴露的属性

        RCT_REMAP_VIEW_PROPERTY(source, imageSources, NSArray<RCTImageSource *>);
    

    就是js中Image组件的属性source,在js中设置source会触发该属性的setter方法。进入RCTImageView的

    - (void)setImageSources:(NSArray<RCTImageSource *> *)imageSources
     {
             if (![imageSources isEqual:_imageSources]) {
                   _imageSources = [imageSources copy];
                   [self reloadImage];
             }
     }
    

    通过此方法中断点打印imageSources,依次得到下面结果:

    可见,Image组件加载图片都是采用URL的形式,将图片当作网络资源。不同的是URL的类型:

       加载网络上图片         :  http://
       加载xcode资源         :  file://
       加载js中图片           :   http://localhost:8081
    

    追踪setter方法,到RCTImageLoader.m中的如下方法

    - (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
    {
        __block volatile uint32_t cancelled = 0;
      __block dispatch_block_t cancelLoad = nil;
      dispatch_block_t cancellationBlock = ^{
        dispatch_block_t cancelLoadLocal = cancelLoad;
        if (cancelLoadLocal && !cancelled) {
          cancelLoadLocal();
        }
        OSAtomicOr32Barrier(1, &cancelled);
      };
      // 下载图片完成后回调
      __weak RCTImageLoader *weakSelf = self;
      void (^completionHandler)(NSError *, id, BOOL, NSString *) = ^(NSError *error, id imageOrData, BOOL cacheResult, NSString *fetchDate) {
        __typeof(self) strongSelf = weakSelf;
        if (cancelled || !strongSelf) {
          return;
        }
         //  如果imageOrData是图片类型,则直接回调
         //  此处,如果是第二种情况,则会满足,其他情况继续走下面方法
        if (!imageOrData || [imageOrData isKindOfClass:[UIImage class]]) {
          cancelLoad = nil;
          completionBlock(error, imageOrData);
          return;
        }
      
        // 在内存中查看是否存在该url对应的字节码图片
        if (cacheResult) {
          UIImage *image = [[strongSelf imageCache] imageForUrl:imageURLRequest.URL.absoluteString
                                                           size:size
                                                          scale:scale
                                                     resizeMode:resizeMode
                                                   responseDate:fetchDate];
          if (image) {
            cancelLoad = nil;
            completionBlock(nil, image);
            return;
          }
        }
    
          // 若没有缓存,则将图片解压,再将解压后图片缓存block
        RCTImageLoaderCompletionBlock decodeCompletionHandler = ^(NSError *error_, UIImage *image) {
          if (cacheResult && image) {
            // Store decoded image in cache
            [[strongSelf imageCache] addImageToCache:image
                                                 URL:imageURLRequest.URL.absoluteString
                                                size:size
                                               scale:scale
                                          resizeMode:resizeMode
                                        responseDate:fetchDate];
          }
    
          cancelLoad = nil;
          completionBlock(error_, image);
        };
         // 具体的解压过程
        cancelLoad = [strongSelf decodeImageData:imageOrData
                                          size:size
                                         scale:scale
                                       clipped:clipped
                                    resizeMode:resizeMode
                               completionBlock:decodeCompletionHandler];
      };
       //  走具体的方法加载图片,1、3种情况用网络请求下载,2情况加载本地文件
      cancelLoad = [self _loadImageOrDataWithURLRequest:imageURLRequest
                                                   size:size
                                                  scale:scale
                                             resizeMode:resizeMode
                                          progressBlock:progressBlock
                                       partialLoadBlock:partialLoadBlock
                                        completionBlock:completionHandler];
      return cancellationBlock;
    }
    

    具体的缓存类是RCTImageCache,采用NSCache缓存,方法

    - (void)addImageToCache:(UIImage *)image
                     forKey:(NSString *)cacheKey
    {
      if (!image) {
        return;
      }
      CGFloat bytes = image.size.width * image.size.height * image.scale * image.scale * 4;
      if (bytes <= RCTMaxCachableDecodedImageSizeInBytes) {
        [self->_decodedImageCache setObject:image
                                     forKey:cacheKey
                                       cost:bytes];
      }
    }
    

    RCTMaxCachableDecodedImageSizeInBytes是个常量,为1048576,也就是只缓存小于1MB的图片。
    问题出在cacheKey,查看缓存key的方法

    static NSString *RCTCacheKeyForImage(NSString *imageTag, CGSize size, CGFloat scale,
                                         RCTResizeMode resizeMode, NSString *responseDate)
    {
        return [NSString stringWithFormat:@"%@|%g|%g|%g|%zd|%@",
                imageTag, size.width, size.height, scale, resizeMode, responseDate];
    }
    

    缓存key的生成方法中包含了responseDate,responseDate是网络请求时返回来的

    responseDate = ((NSHTTPURLResponse *)response).allHeaderFields[@"Date"];
    

    1、3方式每次加载都是一个网络请求,那么网络请求的时间总是变化的,于是responseDate是变化的,cacheKey不唯一,所以虽然系统做了图片的缓存,但是每次取出的都为nil,缓存无效。

    2方式加载具体方法在RCTLocalAssetImageLoader.m中,其调用的是RCTUtils的RCTImageFromLocalAssetURL方法

    UIImage *__nullable RCTImageFromLocalAssetURL(NSURL *imageURL)
    {
    // .....省略各种处理
      UIImage *image = nil;
      if (bundle) {
        image = [UIImage imageNamed:imageName inBundle:bundle compatibleWithTraitCollection:nil];
      } else {
        image = [UIImage imageNamed:imageName];
      }
    // .....省略各种处理
      return image;
    }
    

    可见是采用[UIImage imageNamed:imageName]的方式加载xcode自带的图片,这个是有内存缓存的。

    综上,对react-native图片加载
    1、3情况,没有内存缓存
    2情况有系统默认的内存缓存
    所有情况都没有磁盘缓存

    想让内存缓存生效,只需要改变cacheKey的生成规则即可。

    补充:沙盒下面的Library/Caches/项目bunderId号/fsCachedData文件夹里面会磁盘缓存大于一定值(测试约为5kb)的图片和文件,这个是NSURLSession网络请求系统默认的缓存类NSURLCache自动生成的,非图片的磁盘缓存。

    相关文章

      网友评论

      本文标题:探究react-native 源码的图片缓存

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