ReactNative FlatList Carsh Memory Warning

问题描述

最近用户反馈iOS客户端进入餐厅首页后,过几秒钟后闪退了。

分析解决

原因内存泄漏导致。

运营上架了新菜品,配置了高清图片,列表同时展示多张高清图片内存不足。
首先要知道几点:
1.RN上的列表是没有复用机制的,这就导致列表上的所有图片对象都会同时被持有。
2.运营在后台配置了高清图片,因我司的餐厅模块依赖了哗啦啦平台(为了支持线下下单,购买了双屏机,餐品由双屏机操作录入),而哗啦啦提供的服务并不支持同一个菜品配置多张图片用于展示缩略图、高清图,同时也不支持动态获取指定尺寸的图片。
3.我司开发人员无法从技术上约束运营只能配置小尺寸图片。
解决方案:
iOS Native 添加图片裁剪接口给 js 使用。

对比前后

ReactNative FlatList Carsh Memory Warning_第1张图片
优化前
ReactNative FlatList Carsh Memory Warning_第2张图片
优化后

具体实现

思路:
1.先判断本地是否已经有裁剪好的图片可以使用,如果有就直接使用。
2.依赖SDWebImage下载图片。
3.根据指定尺寸裁剪图片。
4.保存图片到本地。
5.返回本地图片路径给js。

要注意的是需要控制图片处理的并发量,如果同时处理多张图片同样有内存问题。
一下为Native的代码,只要看 -getImage:resolve:rejecter 方法即可。

#define QD_LOCK(lock) dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
#define QD_UNLOCK(lock) dispatch_semaphore_signal(lock);

@interface ImageDownloadModule() 

/// 图片处理并发数
@property (strong, nonatomic, nonnull) dispatch_semaphore_t imageHandleLock;

@end

@implementation ImageDownloadModule

RCT_EXPORT_MODULE()

- (instancetype)init
{
  self = [super init];
  if (self) {
    self.imageHandleLock = dispatch_semaphore_create(3);
  }
  return self;
}

RCT_EXPORT_METHOD(getImage:(NSDictionary *)params
                  resolve:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)
{
  NSString *url = params[@"url"];
  CGFloat width = [params[@"width"] integerValue];
  CGFloat height = [params[@"height"] integerValue];
  NSString *qdResizeMode = params[@"qdResizeMode"];
  // 先查找本地是否存在
  NSString *fileName = [self fileNameWithUrl:url width:width height:height resizeMode:qdResizeMode];
  NSString *path = [[self diskCachePath] stringByAppendingPathComponent:fileName];
  if ([[NSFileManager defaultManager] fileExistsAtPath:path]) {
    resolve(@{ @"path": path });
    return;
  }
  // 控制并发数
  QD_LOCK(self.imageHandleLock)
  
  // 依赖SDWebImage下载图片
  __weak typeof(self) weakSelf = self;
  [[SDWebImageManager sharedManager] loadImageWithURL:[NSURL URLWithString:url]
                                              options:0
                                             progress:^(NSInteger receivedSize,
                                                        NSInteger expectedSize,
                                                        NSURL * _Nullable targetURL)
  {
    // 下载过程
  } completed:^(UIImage * _Nullable image,
                NSData * _Nullable data,
                NSError * _Nullable error,
                SDImageCacheType cacheType,
                BOOL finished,
                NSURL * _Nullable imageURL)
  {
    // 获取图片完成
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
      // 获取指定尺寸
      UIViewContentMode mode = [qdResizeMode isEqualToString:@"contain"] ? UIViewContentModeScaleAspectFit : UIViewContentModeScaleAspectFill;
      CGRect rect = [weakSelf aspectRectFromSize:image.size
                                      toSize:CGSizeMake(width, height)
                                 contentMode:mode];
      // 根据尺寸裁剪
      UIGraphicsBeginImageContextWithOptions(CGSizeMake(width, height), NO, 1.0f);
      [image drawInRect:rect];
      UIImage *imagez = UIGraphicsGetImageFromCurrentImageContext();
      UIGraphicsEndImageContext();
      // 内存优化
      NSString *cacheKey = [SDWebImageManager.sharedManager cacheKeyForURL:imageURL];
      [[SDImageCache sharedImageCache] removeImageFromMemoryForKey:cacheKey];
      // 保存到本地
      NSData *pngData = UIImagePNGRepresentation(imagez);
      BOOL result = [pngData writeToFile:path atomically:YES];
      if (result) {
        resolve(@{ @"path": path });
      } else {
        NSError *err = [NSError errorWithDomain:@"" code:1 userInfo:nil];
        reject(@"1", @"", err);
      }
      QD_UNLOCK(weakSelf.imageHandleLock)
    });
  }];
}

- (NSString *)fileNameWithUrl:(NSString *)url
                    width:(NSInteger)width
                   height:(NSInteger)height
                   resizeMode:(NSString *)resizeMode
{
  @try {
    NSString *fileName = [NSString stringWithFormat:@"%@_%ld_%ld_%@", resizeMode, (long)width, (long)height, url.lastPathComponent];
    return fileName;
  } @catch (NSException *exception) {
    return @"";
  } @finally {
  }
}

- (NSString *)diskCachePath {
  NSString *folderName = @"tmp";
  NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
  NSString *path = [paths[0] stringByAppendingPathComponent:folderName];
  NSFileManager *manager = [NSFileManager defaultManager];
  BOOL isExist = [manager fileExistsAtPath:path];
  if (!isExist) {
      [manager createDirectoryAtPath:path
         withIntermediateDirectories:YES
                          attributes:nil
                               error:nil];
  }
  return path;
}

- (CGRect)aspectRectFromSize:(CGSize)fromSize
                      toSize:(CGSize)toSize
                 contentMode:(UIViewContentMode)contentMode
{
    CGRect rect = CGRectZero;
    
    // tosize 200x200
    CGFloat scaleW = fromSize.width / toSize.width;
    CGFloat scaleH = fromSize.height / toSize.height;
    CGFloat scale;
    if (contentMode == UIViewContentModeScaleAspectFit) {
        scale = MAX(scaleW, scaleH);
    } else if (contentMode == UIViewContentModeScaleAspectFill) {
        scale = MIN(scaleW, scaleH);
    } else {
        scale = MIN(scaleW, scaleH);
    }
    
    CGFloat width = fromSize.width / scale;
    CGFloat height = fromSize.height / scale;
    
    CGFloat x = (toSize.width - width) * 0.5;
    CGFloat y = (toSize.height - height) * 0.5;
    rect = CGRectMake(x, y, width, height);
    
    return rect;
}

@end

对于js端,包装一个使用方法与Image相似的组件即可。

          // 具体使用
          

内部实现

export class SizeImage extends Component {
    static defaultProps = {
        width: 100,
        height: 100,
        /** 只支持 cover 、 contain */
        qdResizeMode: 'cover',
    };

    constructor(props) {
        super(props);
        const { source, width, height, qdResizeMode } = props || {}
        const { uri } = source || {}

        this.state = {
            localPath: ''
        }

        const { ImageDownloadModule } = NativeModules
        if (Platform.OS === 'ios' && ImageDownloadModule && uri) {
            // 调用Native,获取缩略图localPath
            ImageDownloadModule.getImage({ 
                url: uri, 
                width,
                height,
                qdResizeMode,
            }).then(res => {
                const { path } = res || {}
                this.setState({ localPath: path })
            })
        }
    }

    render() {
        const { localPath } = this.state
        const { style, source } = this.props || {}
        const { uri } = source || {}
        const { ImageDownloadModule } = NativeModules
        if (Platform.OS === 'ios' && ImageDownloadModule && uri) {
            return 
        } else {
            return 
        }
    }
}

总结

这种场景很少遇到,但就是这种场景需要去思考解决成长,给平淡的工作加点料。

你可能感兴趣的:(ReactNative FlatList Carsh Memory Warning)