美文网首页ios进阶
iOS 图片加载解码造成内存峰值的一些思考

iOS 图片加载解码造成内存峰值的一些思考

作者: 果哥爸 | 来源:发表于2021-11-03 00:02 被阅读0次

    我们都知道一些图文列表,加载网络图片的时候,由于一些图片比较高清,加载下来解码渲染到视图上的时候,会造成内存飙升问题。

    现在通用的方法是: 图片存储服务器将存储的图片生成各种不同规格的图片,来支持客户端通过拼接想要的图片尺寸,来获取对应的小图,这样就能避免图文列表造成的内存飙升问题,比如七牛云存储。

    但如果存储后台不提供这种服务,是否还有更好的方式来解决该问题。

    这里能想到的比较好的方法就是下载完图片之后,不进行解码,然后对下载的图片,进行降采样,得到想要的小图,然后再去解码渲染到图文列表控件上。

    但这里对高清大图进行降采样的时候,也是需要对高清大图进行解码,拿到解码后的颜色数据,才能绘制生成小图,这时候还是不可避免的造成瞬间的内存飙升。有没有好的方法来避免?

    苹果在wwdc2018的课程Image and Graphics Best Practices提出了利用ImageIO里面的CGImageSourceCreateThumbnailAtIndex生成缩略图来进行降采样,这里可以通过设置属性kCGImageSourceShouldCache,来告诉CGImageSource读取图片数据的时候,边读取边解码,代码如下:

    OC版本

    + (UIImage *)downSamplingWithScale:(CGFloat)scale
                                imgUrl:(NSURL *)imgURL
                            targetSize:(CGSize)targetSize {
    
        //避免下次产生缩略图时大小不同,但被缓存了,取出来是缓存图片
        //所以要把kCGImageSourceShouldCache设为false
        CFStringRef key[1];
        key[0] = kCGImageSourceShouldCache;
        CFTypeRef value[1];
        value[0] = (CFTypeRef)kCFBooleanFalse;
    
        CFDictionaryRef imageSourceOption = CFDictionaryCreate(NULL,
                                                               (const void **) key,
                                                               (const void **) value,
                                                               1,
                                                               &kCFTypeDictionaryKeyCallBacks,
                                                               &kCFTypeDictionaryValueCallBacks);
        CGImageSourceRef imageSource = CGImageSourceCreateWithURL((__bridge CFURLRef)imgURL, imageSourceOption);
    
        CFMutableDictionaryRef mutOption = CFDictionaryCreateMutable(NULL,
                                                                     4,
                                                                     &kCFTypeDictionaryKeyCallBacks,
                                                                     &kCFTypeDictionaryValueCallBacks);
    
    
        CGFloat maxDimension = MAX(targetSize.width, targetSize.height) * scale;
        NSNumber *maxDimensionNum = [NSNumber numberWithFloat:maxDimension];
    
        // · kCGImageSourceCreateThumbnailFromImageAlways
        //这个选项控制是否生成缩略图(没有设为true的话 kCGImageSourceThumbnailMaxPixelSize 以及 CGImageSourceCreateThumbnailAtIndex不会起作用)默认为false,所以需要设置为true
        CFDictionaryAddValue(mutOption, kCGImageSourceCreateThumbnailFromImageAlways, kCFBooleanTrue);//
    
        // · kCGImageSourceShouldCacheImmediately
        // 是否在创建图片时就进行解码(当然要这么做,避免在渲染时解码占用cpu)并缓存,
        /* Specifies whether image decoding and caching should happen at image creation time.
        * The value of this key must be a CFBooleanRef. The default value is kCFBooleanFalse (image decoding will
        * happen at rendering time). //默认为不缓存,在图片渲染时进行图片解码
        */
        CFDictionaryAddValue(mutOption, kCGImageSourceShouldCacheImmediately, kCFBooleanTrue);
    
        // · kCGImageSourceCreateThumbnailWithTransform
        //指定是否应根据完整图像的方向和像素纵横比旋转和缩放缩略图
        /* Specifies whether the thumbnail should be rotated and scaled according
         * to the orientation and pixel aspect ratio of the full image.(默认为false
         */
        //要设为true,因为我们要缩小他!
        CFDictionaryAddValue(mutOption, kCGImageSourceCreateThumbnailWithTransform, kCFBooleanTrue);
    
    
        // · kCGImageSourceThumbnailMaxPixelSize
        /* Specifies the maximum width and height in pixels of a thumbnail.  If
         * this this key is not specified, the width and height of a thumbnail is
         * not limited and thumbnails may be as big as the image itself.  If
         * present, this value of this key must be a CFNumberRef. */
        //指定缩略图的宽
    
        CFDictionaryAddValue(mutOption, kCGImageSourceThumbnailMaxPixelSize, (__bridge CFNumberRef)maxDimensionNum);
    
        CFDictionaryRef dowsamplingOption = CFDictionaryCreateCopy(NULL, mutOption);
    
    
        //生成缩略图
        CGImageRef rf = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, dowsamplingOption);
        //用UIImage把他装起来,返回
        UIImage *img = [UIImage imageWithCGImage:rf];
        return img;
    }
    
    

    swift版本

    func downsample(imageAt imageURL: URL, to pointSize: CGSize, scale: CGFloat) -> UIImage {
        let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
        let imageSource = CGImageSourceCreateWithURL(imageURL as CFURL, imageSourceOptions)!
        let maxDimensionInPixels = max(pointSize.width, pointSize.height) * scale
        let downsampleOptions = [kCGImageSourceCreateThumbnailFromImageAlways: true,
        kCGImageSourceShouldCacheImmediately: true,
        kCGImageSourceCreateThumbnailWithTransform: true,
        kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels] as CFDictionary
        let downsampledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downsampleOptions)!
        return UIImage(cgImage: downsampledImage)
    }
    

    这里根据图片缩小的原理:
    比如有一张像素大小为2000*2000的高清大图,要将其缩小为100*100像素小图,这时候就相当于对原来的图片缩小400倍,因此将原来图片划分为400个互不相交的小块,然后计算小块的颜色平均值,该值作为缩小图像对应的颜色值。

    因此整改降采样的过程如下:

    • CGImageSource对象边读取图像数据边解压的过程中,只需要开辟这一小块大小的内存空间来存储解压后的颜色数据,然后通过颜色数据就可以得出该小块区域的颜色平均值。

    • 然后通过开辟一个缩略图大小的内存空间用来存储缩略图的颜色数据,将之前算出来颜色平均值填充到缩略图对应的内存空间上,然后将小块的内存空间清空,继续存放下一个小块的颜色数据。

    • 就这样对高清大图边读取边解码,然后计算出对应的像素平均值,填充到对应的缩率图像素内存空间上。

    这个过程中内存只用到了缩略图的占用的内存空间和临时存储小块数据的内存空间,与之前高清大图解码所需的内存空间相比,会小很多。

    image.png

    引用自:iOS性能优化——图片加载和处理

    因此这种方式可以很好的解决刚才提到的降采样时对高清大图进行解码造成的内存飙升问题。

    既然这个方法可以解决图片加载过程中内存飙升问题,那能否跟图片加载框架结合起来?

    二.与图片加载框架结合

    1.思路

    • 首先,利用图片加载框架去加载图片,图片加载成功之后,先不进行解码,拿到图片的原始数据

    • 然后,依据显示视图控件的尺寸,利用原始图片数据,去生成对应的缩略图

    • 在原来图片的url后面添加上对应的尺寸大小,生成新的url链接,然后利用该链接生成对应的MD5值,来作为缩略图的存储名称,将缩略图存储到磁盘和内存中,并显示到视图控件上。

    • 下次再加载的时候,先去根据当前的url链接依据想要获取的尺寸,生成对应的缩略图url,然后获取缩略图urlMD5值,去判断本地是否有缓存,如果有直接从磁盘缓存或者内存缓存返回,没有重走之前的加载逻辑。

    具体实现如下:

    - (void)sd_setImageWithURL:(nullable NSURL *)url
              placeholderImage:(nullable UIImage *)placeholder
                    targetSize:(CGSize)targetSize
                       options:(SDWebImageOptions)options
                      progress:(nullable SDImageLoaderProgressBlock)progressBlock
                     completed:(nullable SDExternalCompletionBlock)completedBlock {
        
        NSString *imageUrl = url.absoluteString;
        if (!imageUrl.length) {
            return;
        }
        
        NSString *targetImageUrl = [UIImageView sd_targetImageUrlWithImageUrl:imageUrl targetSize:targetSize];
        BOOL isCachedTargetImage = [UIImageView sd_isCachedImageWithImageUrl:imageUrl targetSize:targetSize];
        if (isCachedTargetImage) {
            [self sd_setImageWithURL:[NSURL URLWithString:targetImageUrl] placeholderImage:placeholder options:options progress:progressBlock completed:completedBlock];
        } else {
            [self sd_setImageWithURL:url placeholderImage:placeholder options:options | SDWebImageAvoidAutoSetImage | SDWebImageAvoidDecodeImage progress:nil completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
                
                if ([UIImageView sd_isNormalImageWithImageUrl:imageUrl]) {
                    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                        NSString *imagePath = [[SDImageCache sharedImageCache].diskCache cachePathForKey:url.absoluteString];
                        CGSize imageSize = [UIImage imageSizeWidthTargetWidth:targetSize.width originalSize:image.size];
                        UIImage *targetImage = [UIImage downSamplingWithScale:[UIScreen mainScreen].scale imgUrl:[NSURL fileURLWithPath:imagePath] targetSize:imageSize];
                        [[SDImageCache sharedImageCache] storeImage:targetImage forKey:targetImageUrl completion:^{
                            [self sd_setImageWithURL:[NSURL URLWithString:targetImageUrl] placeholderImage:placeholder options:options progress:progressBlock completed:completedBlock];
                        }];
                    });
                    
                } else if([UIImageView sd_isGifImageWithImageUrl:imageUrl]) {
                    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                        NSArray<SDImageFrame *> *animatedImageFrameArray = [SDImageCoderHelper framesFromAnimatedImage:image];
                        NSMutableArray<SDImageFrame *> *tmpThumbImageFrameMarray = [NSMutableArray array];
                        [animatedImageFrameArray enumerateObjectsUsingBlock:^(SDImageFrame * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
                            CGSize imageSize = [UIImage imageSizeWidthTargetWidth:targetSize.width originalSize:obj.image.size];
                            NSData *imgData = UIImageJPEGRepresentation(obj.image , 0.75);
                            UIImage *targetImage = [UIImage downSamplingWithScale:[UIScreen mainScreen].scale imgData:imgData targetSize:imageSize];
                            SDImageFrame *thumbFrame = [SDImageFrame frameWithImage:targetImage duration:obj.duration];
                            [tmpThumbImageFrameMarray addObject:thumbFrame];
                        }];
                        
                       UIImage *thumbAnimatedImage = [SDImageCoderHelper animatedImageWithFrames:tmpThumbImageFrameMarray];
                        [[SDImageCache sharedImageCache] storeImage:thumbAnimatedImage forKey:targetImageUrl completion:^{
                            [self sd_setImageWithURL:[NSURL URLWithString:targetImageUrl] placeholderImage:placeholder options:options progress:progressBlock completed:completedBlock];
                        }];
                    });
                } else {
                    [self sd_setImageWithURL:url placeholderImage:placeholder options:options progress:progressBlock completed:completedBlock];
                }
            }];
        }
    }
    

    三.如何解决gif图导致的内存问题

    因为gif图是由多张图片拼接而成,如果对gif图里面的所有图片进行解码,然后持有去渲染显示,这样无疑会导致,内存的飙升。

    常用的方法是,对gif图进行解析,将解析后的图片数据放到图片数组里面,这时候先不进行解码,然后启动定时器,定时去数组里面获取单张图片数据去解码,渲染显示到视图上,这样就可以避免一次性对图片数组解码导致的内存飙升问题。

    虽然这样解决了内存飙升问题,但也会引入新的问题,就是通过定时器每次去取出对应的图片,再去解码渲染到视图上,无疑也会加大CPU的损耗,

    而这里我们也可以利用降采样,先对gif图进行解析,获取所有的图片和时间间隔,然后对每一张图片进行降采样,将降低采样的图片放到数组里面,然后对降低采样后的图片进行合成,合成为对应的gif图,接着将该降低采样后的缩略的gif图按照上面普通的缩略图一样进行存储和加载。

    + (BOOL)sd_isGifImageWithImageUrl:(NSString *)imageUrl {
        NSString *imagePath = [[SDImageCache sharedImageCache].diskCache cachePathForKey:imageUrl];
        NSData *data = [NSData dataWithContentsOfFile:imagePath];
        SDImageFormat imageFormat = [NSData sd_imageFormatForImageData:data];
        if (imageFormat == SDImageFormatGIF) {
            return YES;
        }
        return NO;
    }
    

    Demo详见

    四.阅读引申

    iOS中ImageIO框架详解与应用分析
    iOS性能优化——图片加载和处理

    相关文章

      网友评论

        本文标题:iOS 图片加载解码造成内存峰值的一些思考

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