图片来自:unsplash.com
本文作者: lgq

背景

图片展现,在各大APP中不可或缺,众所周知云音乐是一款带有交际特点的音乐软件,那么在任何交际场景,都会有展现图片的诉求,而且常常会有重图片场景,比方一个云音乐中Mlog的Feed流场景全都是图片,或者便是Mlog中的图集,都需求展现很多的图片,要是图片无法及时的展现出来,不能及时的被用户消费,那么会造成用户浏览信息不顺畅,导致用户的丢失,因此优化图片下载火烧眉毛。

现有图片下载技能

这儿简略了解下云音乐APP中接入的图片资源服务,它能够经过拼接参数,在远端进行裁剪,质量压缩然后下载到不同的图片。更多信息参考

影响图片下载的因素

  1. 图片巨细
  2. 网络状况
  3. 本地缓存
  4. cdn缓存

综上所述,怎么进步图片的下载速度能够从上面几点开端优化。

优化方式

网络优化

  • 传统 HTTP1.0 的架构下没法多路复用,采用 HTTP2.0 的方式,恳求同一ip域名的资源能够从节约很多建连及传输时间。
  • 除此之外笔者在做音视频场景较重的页面时,发现音视频流媒体的数据有时分会抢占很多带宽,导致图片下载十分的慢,这时需求对音视频场景资源下载做恰当的操控,如限流等操作,详细看事务优先级。如音视频场景运用socket下载时能够恰当调中recv buffer 巨细。

图片巨细优化

  • 格式优化 这是最容易想的到的也是最有效的,假如正常运用jpg,png等惯例图片,那图片的巨细会是比较大的,目前咱们的nos服务支撑指定类型,将图片转成特定的格式,所以咱们这儿运用webp,然后减少图片的巨细。(只需求在恳求参数中拼接类型为webp即可)

那除此之外呢,咱们还能够做一些什么?

  • 按需裁剪 比方一个 100 * 100 的控件,3 倍屏的状况下,咱们只需求下载 300 * 300 的图就能够了,假如图片超过个尺度,咱们去下载那么大的也没有含义。所以依据控件巨细,能够决定咱们下的图片巨细,然后减小咱们所需下载的图片。

  • 压缩质量 比方要求没有那么高的场景咱们只需求质量为 80 的图就能够了。

思考 以上几项做完,咱们能够发现速度至少提高 30%,可是是不是能够做的更多,或者这个计划有什么纰漏?

取证 为此咱们简略的拉取了一下后台数据。发现有以下问题:

  1. URL拼接的参数不同,导致无法射中本地缓存,这样会有重复下载的问题,比方用户头像,用户头像再各个场景重复呈现,而且巨细纷歧,会下载屡次这样会导致一定的资源浪费。一起由于链接参数各异 cdn射中度不高
  2. 不同机型的UI尺度巨细或许不太共同,导致下载的片尺度会纷歧样,机型品种越多,拼接的尺度状况也越多,服务端需求重复裁剪。
  3. 质量参数由上层事务自行决定,会导致不同端没有约定好,下载到各式各样的图片

处理手法

  1. URL 参数标准化 所谓的标准化是规范大前端运用的参数拼接,分为次序标准化,参数值拟合。 咱们知道一个下载图片的URL链接http://path?imageView=1&enlarge=1&quality=80&thumbnail=80x80&type=webp
  • 其间参数咱们按首字母排序,这样在参数要求共同的状况下,不会呈现重复恳求。
  • thumbnail 参数其实对应的是需求下载的图片巨细,咱们做拟合(依据后端统计的到的数据),分红多档(档位能够配置),依照宽边对其等比例缩放,这样能够尽或许少的避免机型屏幕差一点点,呈现了其他size的case。
  • quality也同样分级,分红多档(档位能够配置)。
  • 去重,参数或许多拼接,对冗余参数去重复
  1. 本地巨细图片重用 简略了解是本地有大图,取小图的时分无需额定网络恳求,直接本地裁剪。 咱们优化了读取本地缓存的逻辑,在取缓存的时分,咱们会进行相关查找,找到可用的图片进行裁剪,直接返回。 详细规矩如下:
  • 不同裁剪参数能够转化,x,z裁剪参数能够转为y,y不能够转x,z。都能够转为相同的裁剪参数。其间x(内缩略),y(裁剪缩略),z(外缩略)的含义在本篇文档中有,代表着不同的填充形式。
  • 质量高的图片能够复用为质量低的图片,质量低的图片不能够复用为质量高的图片

iOS 代码完结

说完了计划之后,咱们能够上代码了,这儿是 iOS的完结计划:

首先咱们是根据SDWebImage进行一定的封装,先简略了解下SDWebImage中大约的流程。

云音乐iOS端网络图片下载优化实践

从图中咱们能够看出,下载图片主要是运用了imageLoader,查找缓存这儿是用了imageCache,这两个都在manager中被管理

改造流程

云音乐iOS端网络图片下载优化实践

咱们只需求在数据流转的最开端对URL进行Fix,一起在查找缓存的时分对图片添加额定查找即可。

URL FIX

咱们给URL添加一个分类,对URL进行一个fix操作,计划便是用体系提供的 NSURLComponts对齐进行操作,提取出他的参数,进行去重,标准化,一起咱们有一些前史原因,一些老的参数将其转为正确的格式,最终一步进行排序,fix流程就完结了。

- (NSURL *)demo_fixImageURL {
    NSURLComponts *componts = [NSURLComponts compontsWithURL:self
                                             resolvingAgainstBaseURL:YES];
    NSMutableArray<NSURLQueryItem *> *queryItems = componts.queryItems.mutableCopy;
    ... 从URL取出 NSURLQueryItem 省掉一些代码
    if (qualityItem) {
        //quality 拟合, 将质量参数分为几档
        NSString *defaultQualityStr = @"39,69,89";
        //这儿是伪代码,便是为了获取配置信息
        NSArray<NSString *> *qualityLevel = CustomConfigQualityLevels;
        //固定 4档
        if (qualityLevel.count == 3) {
            NSInteger quality = [qualityItem.value intValue];
            NSString *fixQuality = @"";
            if (quality <= [[qualityLevel _objectAtIndex:0] intValue]) {
                fixQuality = [@(ImageQualityLevelLow) stringValue];
            } else if (quality <= [[qualityLevel _objectAtIndex:1] intValue]) {
                fixQuality = [@(ImageQualityLevelMed) stringValue];
            } else if (quality <= [[qualityLevel _objectAtIndex:2] intValue]) {
                fixQuality = [@(ImageQualityLevelHigh) stringValue];
            } else {
                fixQuality = [@(ImageQualityLevelOrigin) stringValue];
            }
            NSURLQueryItem *fixQualityItem = [[NSURLQueryItem alloc] initWithName:@"quality" value:fixQuality];
            [queryItems removeObject:qualityItem];
            [queryItems addObject:fixQualityItem];
        }
    }
    if (sizeItem) {
        //size 依照宽边拟合 分为几档且 等比缩放
        NSString *defaultSizeStr = @"30,60,90,120,180,256,315,512,720,1024";
        //这儿是伪代码 便是为了获取配置信息
        NSArray<NSString *> *sizeLevels = CustomConfigSizeLevels;
        NSString *originSizeStr = sizeItem.value;
        CGSize originSize = CGSizeZero;
        NSString *separatedStr = nil;
        for (NSString *separated in @[@"x", @"z", @"y"]) {
            NSArray *sizeList = [originSizeStr compontsSeparatedByString:separated];
            if (sizeList.count == 2) {
                originSize = CGSizeMake([sizeList[0] intValue], [sizeList[1] intValue]);
                separatedStr = separated;
                break;
            }
        }
        CGSize finalSize = CGSizeZero;
        if (!CGSizeEqualToSize(originSize, CGSizeZero)) {
            BOOL isW = originSize.width > originSize.height;
            NSInteger len = isW ? originSize.width : originSize.height;
            NSInteger requestSize = 0;
            for (NSString *sizeLevel in sizeLevels) {
                NSInteger sizeNumber = [sizeLevel integerValue];
                if (sizeNumber >= len) {
                    if (requestSize == 0) {
                        requestSize = sizeNumber;
                    } else {
                        requestSize = MIN(requestSize, sizeNumber);
                    }
                }
            }
            if (isW) {
                if (originSize.width != 0) {
                    NSInteger h = (requestSize / (originSize.width * 1.f)) * originSize.height;
                    finalSize = CGSizeMake(requestSize, floor(h));
                }
            } else {
                if (originSize.height != 0) {
                    NSInteger w = (requestSize / (originSize.height * 1.f)) * originSize.width;
                    finalSize = CGSizeMake(w, floor(requestSize));
                }
            }
        }
        if (!CGSizeEqualToSize(finalSize, CGSizeZero)) {
            NSString *fixSize = [NSString stringWithFormat:@"%ld%@%ld",(NSInteger)finalSize.width, separatedStr, (NSInteger)finalSize.height];
            NSURLQueryItem *fixSizeItem = [[NSURLQueryItem alloc] initWithName:@"thumbnail" value:fixSize];
            [queryItems removeObject:sizeItem];
            [queryItems addObject:fixSizeItem];
        }
    }
    //去重复
    NSMutableArray<NSString *> *keys = @[].mutableCopy;
    queryItems = [queryItems bk_select:^BOOL(NSURLQueryItem *obj) {
        BOOL containsObject = [keys containsObject:obj.name];
        [keys addObject:obj.name];
        return !containsObject;
    }].mutableCopy;
    //首字母排序
    queryItems = [queryItems sortedArrayUsingComparator:^NSComparisonResult(NSURLQueryItem *obj1, NSURLQueryItem *obj2) {
        return [obj1.name compare:obj2.name options:NSCaseInsensitiveSearch];
    }].mutableCopy;
    //最终组合
    componts.queryItems = queryItems.copy;
    NSURL *finalURL = componts.URL;
    return finalURL;
}

SDWebImageManager

批改了URL之后,下一步要做什么,怎么将批改后的URL传递下去呢?也能够从上面的SDWebImage流程中看出,所有的图片下载流程,离不开SDWebImageManager,所以咱们承继 SDWebImageManager,重写以下办法

- (SDWebImageCombidOperation *)loadImageWithURL:(nullable NSURL *)url
                                          options:(SDWebImageOptions)options
                                          context:(nullable SDWebImageContext *)context
                                         progress:(nullable SDImageLoaderProgressBlock)progressBlock
                                        completed:(nonnull SDInternalCompletionBlock)completedBlock

后续假如要走批改流程的只需求用咱们封装好的manager即可,完结假如下

- (SDWebImageCombidOperation *)loadImageWithURL:(nullable NSURL *)url
                                          options:(SDWebImageOptions)options
                                          context:(nullable SDWebImageContext *)context
                                         progress:(nullable SDImageLoaderProgressBlock)progressBlock
                                        completed:(nonnull SDInternalCompletionBlock)completedBlock
                                         corp:(BOOL)corp {
    NSURL *fixURL = [self.class fixURLWithUrl:url];
    SDInternalCompletionBlock fixBlock = completedBlock;
    if (![fixURL.absoluteString isEqualToString:url.absoluteString] && corp) {
        fixBlock = [self.class fixcompletedBlockWithOriginCompletedBlock:completedBlock url:url];
    }
    return [super loadImageWithURL:fixURL options:options context:context progress:progressBlock completed:^void(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) {
        if (fixBlock) {
            fixBlock(image,data,error,cacheType,finished,imageURL);
        }
    }];
}

细心的同学能够发现咱们添加了一个参数 corp,假如上层事务便是需求依照他传入的巨细来的话,咱们做一层裁剪缩放操作。详细操作放在了fixBlock中。默许是不进行fix的,由于本身nos服务器下发的图片也纷歧定是事务传入希望的尺度。

fixblock 中心的代码是用了sd_webimage自带的裁剪

cutImage = [image sd_resizedImageWithSize:requestSize scaleMode:[urlInfo.cropStr isEqualToString:@"x"] ? SDImageScaleModeAspectFit : SDImageScaleModeAspectFill];

代码到这儿根本fixURL操作根本完结,可是假如需求兼容老的缓存(本地现已有的,而且永久缓存(特别case),可是线上现已下架的资源图片的),在fixblock中,咱们在加载失利的状况下,用老的URL捞了一次本地缓存。

 [[self sharedManager] loadImageWithURL:url options:options | SDWebImageFromCacheOnly context:mutContext.copy progress:nil completed:completedBlock];

留意:现已fix过的URL不会再fix,是否是永久缓存是经过imageCache区分的

SDWebImageFromCacheOnly 表示只从缓存了读取,避免了重复发恳求的问题。

imageCache

上面说到要完结复用,需求批改imageCache,这儿不得不提以下SDWebImage找到缓存的流程

云音乐iOS端网络图片下载优化实践

从图中能够看出,URL需求转为cacheKey,然后再从内存或者磁盘中捞出缓存。那么咱们怎么改造呢,由于咱们需求经过URL找到本地能够重用的图片

cacheKey需求保存一定规矩,经过cache能够看到原始URL的一些东西。所以咱们cachekey是这么生成的

+ (NSString *)cacheKeyForURL:(NSURL *)url  {
    NSURL *wUrl = url;
    NSString *host = wUrl.host;
    NSString *absoluteString = wUrl.absoluteString;
    if (!host)
    {
       return absoluteString;
    }
    NSRange hostRange = [absoluteString rangeOfString:host];
    if (hostRange.location + hostRange.length < absoluteString.length)
    {
       NSString *subString = [absoluteString substringFromIndex:hostRange.location + hostRange.length];
       if (subString.length != 0)
       {
           return subString;
       }
    }
    return absoluteString;
}

简而言之,便是去掉host,保存剩下的参数。ps:由于fixURL去过恳求参数重复,所以cacheKey也能同一张图片保证仅有。

那经过URL怎么找到本地的其他图片呢,怎么相关上呢?

云音乐iOS端网络图片下载优化实践

能够经过path,再查找相关的cachekey,然后找到对应的图片

找到图片后,选择出一张能够运用的,对其进行裁剪操作,流程如下:

云音乐iOS端网络图片下载优化实践

咱们这儿对缓存的图片信息封装了一个目标,留意 会用数据库耐久化 ImageCacheKeyAndURLObject数组,他的key是恳求URL链接中的 path,留意数据库有上限巨细,一起会在恰当的机遇清理(如图片缓存过期等)

下面是封装耐久化的目标

@interface WebImageCacheImageInfo : NSObject
@property (nonatomic) BOOL isAnimation;
@property (nonatomic) CGFloat sizeW;
@property (nonatomic) CGFloat sizeH;
- (CGSize)size;
@end
@interface WebImageURLInfo : NSObject
@property (nonatomic) CGSize requestSize;
@property (nonatomic) NSString *cropStr;
@property (nonatomic) NSInteger quality;
@property (nonatomic) NSInteger enlarge;
@end
@interface WebImageCacheKeyAndURLObject : NSObject<NMModel>
@property (nonatomic, readonly) NSString *path;
@property (nonatomic) NSString *cacheKey;
@property (nonatomic, nullable) NSURL *url;
@property (nonatomic, nullable) WebImageCacheImageInfo *imageInfo;
- (NSArray<WebImageCacheKeyAndURLObject *> *)relationObjects;
- (nullable WebImageCacheKeyAndURLObject *)canReuseObject;
- (WebImageURLInfo *)urlInfo;
- (void)storeImage:(UIImage *)image;
- (void)remove;
@end

怎么存储图片信息呢

- (void)storeImage:(UIImage *)image {
    if (self.path.length == 0) {
        return;
    }
    BOOL isAniamtion = image.sd_isAnimated;
    CGSize size = image.size;
    if (image) {
        _imageInfo = [WebImageCacheImageInfo new];
        _imageInfo.sizeH = size.height;
        _imageInfo.sizeW = size.width;
        _imageInfo.isAnimation = isAniamtion;
    }
    NSMutableArray<WebImageCacheKeyAndURLObject *> *items = [[self searchfromDBUsePath:self.path] mutableCopy];
    if (items.count == 0) {
        items = @[].mutableCopy;
    }
    if ([items containsObject:self]) {
        [items removeObject:self];
    }
    [items addObject:self];
    [self saveDBForPath:self.path item:items];
}

怎么判断图片是否能够复用呢?

- (WebImageCacheKeyAndURLObject *)canReuseObject {
    WebImageURLInfo *info = self.urlInfo;
    if (CGSizeEqualToSize(CGSizeZero, info.requestSize)) {
        return nil;
    }
    NSArray<WebImageCacheKeyAndURLObject *> *relationObjects = [self relationObjects];
    // 非动图 尺度非0
    relationObjects = [relationObjects bk_select:^BOOL(WebImageCacheKeyAndURLObject *obj) {
        return !obj.imageInfo.isAnimation && obj.imageInfo.size.width > 0 && obj.imageInfo.size.height > 0;
    }];
    @weakify(self)
    relationObjects = [relationObjects bk_select:^BOOL(WebImageCacheKeyAndURLObject *obj) {
        @strongify(self)
        return ![obj.cacheKey isEqualToString:self.cacheKey];
    }];
    // 质量大于恳求的图
    relationObjects = [relationObjects bk_select:^BOOL(WebImageCacheKeyAndURLObject *obj) {
        WebImageURLInfo *objInfo = obj.urlInfo;
        NSInteger quality = objInfo.quality == 0 ? 75 : objInfo.quality;
        NSInteger requestQuality = info.quality == 0 ? 75 : info.quality;
        return quality >= requestQuality;
    }];
    //缩放能支撑的
    NSArray<WebImageCacheKeyAndURLObject *> *canUses = nil;
    if ([info.cropStr isEqualToString:@"x"] || [info.cropStr isEqualToString:@"z"]) {
        canUses = [relationObjects bk_select:^BOOL(WebImageCacheKeyAndURLObject *obj) {
            WebImageURLInfo *objInfo = obj.urlInfo;
            if ([objInfo.cropStr isEqualToString:@"x"] || [objInfo.cropStr isEqualToString:@"z"]) {
                CGSize displaySize = WebImageDisplaySizeForImageSizeContentSizeContentMode(obj.imageInfo.size, info.requestSize, [info.cropStr isEqualToString:@"x"] ? UIViewContentModeScaleAspectFit :  UIViewContentModeScaleAspectFill);
                CGFloat p = 0;
                if (info.requestSize.width > 0) {
                    p = displaySize.width / obj.imageInfo.size.width;
                } else {
                    p = displaySize.height / obj.imageInfo.size.height;
                }
                return p <= 1;
            } else {
                // y 不能够转z/x
                return NO;
            }
        }];
    } else if ([info.cropStr isEqualToString:@"y"]) {
        canUses = [relationObjects bk_select:^BOOL(WebImageCacheKeyAndURLObject *obj) {
            WebImageURLInfo *objInfo = obj.urlInfo;
            if ([objInfo.cropStr isEqualToString:@"x"] || [objInfo.cropStr isEqualToString:@"z"]) {
                CGSize displaySize = WebImageDisplaySizeForImageSizeContentSizeContentMode(obj.imageInfo.size, info.requestSize, UIViewContentModeScaleAspectFill);
                CGFloat p = 0;
                if (info.requestSize.width > 0) {
                    p = displaySize.width / obj.imageInfo.size.width;
                } else {
                    p = displaySize.height / obj.imageInfo.size.height;
                }
                return p <= 1;
            } else  if ([objInfo.cropStr isEqualToString:@"y"]) {
                return (obj.imageInfo.size.width >= info.requestSize.width && obj.imageInfo.size.height >= info.requestSize.height);
            }
            return NO;
        }];
    }
    return canUses.firstObject;
}

要过滤动图,由于动图本地裁剪比较难处理,而且占比不高,所以这儿先忽略他,WebImageCacheKeyAndURLObject记录了cacheKey等一些相关信息,中心还记录了实践缓存的图片尺度。方便查询。WebImageDisplaySizeForImageSizeContentSizeContentMode便是传入图片巨细,容器巨细,填充形式计算出缩放后的图片巨细。

相关关系有了,再什么机遇去查找呢? 咱们承继SDImageCache,重写了他

- (nullable NSData *)diskImageDataBySearchingAllPathsForKey:(nullable NSString *)key;

这个办法,在找不到data的状况下更进一步查找。找到的相关图片进行裁剪,运用和上面相同的批改办法

if ([objInfo.cropStr isEqualToString:@"x"] || [objInfo.cropStr isEqualToString:@"z"]) {
                result = [result fixResizedImageWithSize:requestSize scaleMode:[objInfo.cropStr isEqualToString:@"x"] ? UIViewContentModeScaleAspectFit : UIViewContentModeScaleAspectFill needCorp:NO];
            } else if ([objInfo.cropStr isEqualToString:@"y"]) {
                result = [result fixResizedImageWithSize:requestSize scaleMode:UIViewContentModeScaleAspectFill needCorp:YES];
            }

这儿补充下fixsize办法

- (UIImage *)fixResizedImageWithSize:(CGSize)size scaleMode:(UIViewContentMode)scaleMode needCorp:(BOOL)needCorp {
    if (scaleMode != UIViewContentModeScaleAspectFit && scaleMode!= UIViewContentModeScaleAspectFill) {
        return self;
    }
    // 假如是fill形式,实践size会大于容器size 假如需求裁剪为容器巨细就不走这一步了
    if (scaleMode == UIViewContentModeScaleAspectFill && !needCorp) {
        size = WebImageDisplaySizeForImageSizeContentSizeContentMode(self.size, size, scaleMode);
    }
    UIImage *fixImage = [self sd_resizedImageWithSize:size scaleMode:scaleMode == UIViewContentModeScaleAspectFit ? SDImageScaleModeAspectFit : SDImageScaleModeAspectFill];
    return fixImage;
}

这样咱们就能够得到批改后的图片,流程完结。

UIImageView 及 UIButton 等分类

咱们包装一层自己的下载,然后传入咱们的manager即可。

context = @{
           SDWebImageContextCustomManager:[WebImageManager sharedManager]
       };

额定说一点

CDN射中率和这个资源是否曾经被恳求过有关,射中CDN的key又是恳求的URL,所以大前端恳求都保持共同的规矩很重要!这样每一端都能够蹭到其他端预热过的图片资源。

总结

咱们中心点就批改了URL改造了SDWebImageManager,SDImageCache,而且建立了CacheKey相关关系,而且兼容一些老逻辑这样本地流程就都算走通了。本文除了惯例优化图片的思路外提供了一种新的思路,本地利用现已下载过的巨细图做文章,然后起到加速及节省的作用,并获得一定的收益,假如读者也是采用相似拼接url下载图片的方式的话,这种优化方式能够一试。悉数做完获得效果详细数值不便展现,大约为提高下载速度 50%,一起能节约一定的 CDN带宽,日均节约至少 10% 。

本文发布自网易云音乐技能团队,文章未经授权禁止任何形式的转载。咱们常年招收各类技能岗位,假如你准备换工作,又恰好喜爱云音乐,那就加入咱们 grp.music-fe(at)corp.netease.com!