ReactNative FlatList Carsh Memory Warning

问题描述

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

分析解决

原因内存泄漏导致。

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

对比前后

优化前
优化后

具体实现

思路:
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() <RCTBridgeModule>

/// 图片处理并发数
@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相似的组件即可。

          // 具体使用
          <SizeImage
            style={styles.skuItemIcon}
            source={{ uri: imgePath || '' }}
            width={W(144) * 1.5}
            height={W(144) * 1.5}
            qdResizeMode='cover'
          />

<SizeImage>内部实现

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 <Image style={style} source={localPath ? { uri: localPath } : IMAGE.defaultPlaceholder} />
        } else {
            return <Image style={style} source={source} />
        }
    }
}

总结

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容

  • 2021.04.09 问题描述 最近用户反馈iOS客户端进入餐厅首页后,过几秒钟后闪退了。 分析解决 原因内存泄漏...
    一本大书阅读 345评论 0 0
  • 夜莺2517阅读 127,664评论 1 9
  • 版本:ios 1.2.1 亮点: 1.app角标可以实时更新天气温度或选择空气质量,建议处女座就不要选了,不然老想...
    我就是沉沉阅读 6,802评论 1 6
  • 我是黑夜里大雨纷飞的人啊 1 “又到一年六月,有人笑有人哭,有人欢乐有人忧愁,有人惊喜有人失落,有的觉得收获满满有...
    陌忘宇阅读 8,471评论 28 53
  • 兔子虽然是枚小硕 但学校的硕士四人寝不够 就被分到了博士楼里 两人一间 在学校的最西边 靠山 兔子的室友身体不好 ...
    待业的兔子阅读 2,538评论 2 9