图片来自:https://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
  2. 其中参数咱们按首字母排序,这样在参数要求统一的状况下,不会呈现反复申请。
  3. thumbnail 参数其实对应的是须要下载的图片大小,咱们做拟合(依据后端统计的到的数据),分成多档(档位能够配置),依照宽边对其等比例缩放,这样能够尽可能少的防止机型屏幕差一点点,呈现了其余size的case。
  4. quality也同样分级,分成多档(档位能够配置)。
  5. 去重,参数可能多拼接,对冗余参数去反复
  6. 本地大小图片重用
    简略了解是本地有大图,取小图的时候无需额定网络申请,间接本地裁剪。
    咱们优化了读取本地缓存的逻辑,在取缓存的时候,咱们会进行关联查找,找到可用的图片进行裁剪,间接返回。
    具体规定如下:
  7. 不同裁剪参数能够转化,x,z裁剪参数能够转为y,y不能够转x,z。都能够转为雷同的裁剪参数。其中x(内缩略),y(裁剪缩略),z(外缩略)的含意在本篇文档中有,代表着不同的填充模式。
  8. 品质高的图片能够复用为品质低的图片,品质低的图片不能够复用为品质高的图片

iOS 代码实现

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

首先咱们是基于SDWebImage进行肯定的封装,先简略理解下SDWebImage中大略的流程。

从图中咱们能够看出,下载图片次要是应用了imageLoader,查找缓存这里是用了imageCache,这两个都在manager中被治理

革新流程

咱们只须要在数据流转的最开始对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找到缓存的流程

从图中能够看出,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怎么找到本地的其余图片呢,如何关联上呢?

能够通过path,再查找关联的cachekey,而后找到对应的图片

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

咱们这里对缓存的图片信息封装了一个对象,留神 会用数据库长久化 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!