APP性能优化之二 运行时优化

APP启动部分的优化做完,剩下的就是APP运行时相关的优化了,主要从下面几个问题入手

  • UIImage优化
  • UITableView优化
  • 卡顿优化
  • 网络请求优化

UIImage优化

1 imageNamed和imageWithContentsOfFile

imageNamed:本质上是通过由AsSet的键值对来实现对图片的管理的,所以会一直存在内存中,好处是多个同一图片的对象,其实在内存中调用维护的是一个对象,节省内存。
而imageWithContentsOfFile是一个对象,会随着所依附的对象的生命周期快速的释放。缺点是多个多个同一图片的对象,在内存中调用维护的也是多个对象,内存浪费。
所以这里有一个判定标准:对不常用的大图片,使用imageWithContentsOfFile代替imageNamed方法,避免内存缓存滞留不释放,经常多次出现的图片,采用imageNamed。

2 对大图片进行缩放

有很多时候UI给的图的分辨率是很大的,而其实我们的APP是用不到那么大的图片的,这个时候可以对图片进行缩放。再配合ImageIO,直接读取图像大小和数据信息,避免图片缩放过程中额外的内存开销。
代码示例:

- (UIImage *)resizeScaleImage:(CGFloat)scale {
    
    CGSize imgSize = self.size;
    CGSize targetSize = CGSizeMake(imgSize.width * scale, imgSize.height * scale);
    NSData *imageData = UIImageJPEGRepresentation(self, 1.0);
    CFDataRef data = (__bridge CFDataRef)imageData;
    
    CFStringRef optionKeys[1];
    CFTypeRef optionValues[4];
    optionKeys[0] = kCGImageSourceShouldCache;
    optionValues[0] = (CFTypeRef)kCFBooleanFalse;
    CFDictionaryRef sourceOption = CFDictionaryCreate(kCFAllocatorDefault, (const void **)optionKeys, (const void **)optionValues, 1, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
    CGImageSourceRef imageSource = CGImageSourceCreateWithData(data, sourceOption);
    CFRelease(sourceOption);
    if (!imageSource) {
        NSLog(@“imageSource is Null!”);
        return nil;
    }
    //获取原图片属性
    int imageSize = (int)MAX(targetSize.height, targetSize.width);
    CFStringRef keys[5];
    CFTypeRef values[5];
    //创建缩略图等比缩放大小,会根据长宽值比较大的作为imageSize进行缩放
    keys[0] = kCGImageSourceThumbnailMaxPixelSize;
    CFNumberRef thumbnailSize = CFNumberCreate(NULL, kCFNumberIntType, &imageSize);
    values[0] = (CFTypeRef)thumbnailSize;
    keys[1] = kCGImageSourceCreateThumbnailFromImageAlways;
    values[1] = (CFTypeRef)kCFBooleanTrue;
    keys[2] = kCGImageSourceCreateThumbnailWithTransform;
    values[2] = (CFTypeRef)kCFBooleanTrue;
    keys[3] = kCGImageSourceCreateThumbnailFromImageIfAbsent;
    values[3] = (CFTypeRef)kCFBooleanTrue;
    keys[4] = kCGImageSourceShouldCacheImmediately;
    values[4] = (CFTypeRef)kCFBooleanTrue;
    
    CFDictionaryRef options = CFDictionaryCreate(kCFAllocatorDefault, (const void **)keys, (const void **)values, 4, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
    CGImageRef thumbnailImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options);
    UIImage *resultImg = [UIImage imageWithCGImage:thumbnailImage];
    
    CFRelease(thumbnailSize);
    CFRelease(options);
    CFRelease(imageSource);
    CFRelease(thumbnailImage);
    
    return resultImg;
}
3 下采样

WWDC2018的时候苹果提出了使用 UIGraphicsImageRenderer 代替 UIGraphicsBeginImageContextWithOptions。使用 UIGraphicsBeginImageContextWithOptions 生成的图片,每个像素需要 4 个字节表示。使用 UIGraphicsImageRenderer系统会自动选择最佳的图像格式,可以减少很多内存。系统可以根据图片分辨率选择创建解码图片的格式,如选用SRGB format 格式,每个像素占用 4 字节,而Alpha 8 format,每像素只占用 1 字节,可以减少大量的解码内存占用。这个方法是从 iOS 10 引入,在 iOS 12 上做了更好的优化。

4 SDWebImage缓存优化

4.1 在使用SDWebImage的时候,会默认保存图片解码后的内存,以便提高页面的渲染速度,但是这会导致内存的急速增加,所以可以在不影响体验的情况下,选择机型和系统,进行优化,避免大量的内存占用,引起OOM问题。关闭解码内存缓存的方法如下:

[[SDImageCache sharedImageCache] setShouldDecompressImages:NO];
[[SDWebImageDownloader sharedDownloader] setShouldDecompressImages:NO];

4.2 SDWebImage还有一个可优化的点,是清理SDWebImage的缓存,和合适的时机,譬如tableView滚动的时候,或者页面返回的时候,或者收到内存警告的时候进行清理

[[SDWebImageManager sharedManager].imageCache clearMemory];
[[SDImageCache sharedImageCache] setValue:nil forKey:@“memCache”];//建议使用这句话,效果更好

4.3 在SDWebimage中修改下载的图片体积,SDWebimageManager.m文件中添加如下代码,会导致图片模糊,CPU使用上升,但是内存会下降。

-(UIImage *)compressImageWith:(UIImage *)image
{
    float imageWidth = image.size.width;
    float imageHeight = image.size.height;
    CGSize croppedSize;
    CGFloat offsetX = 0.0;
    CGFloat offsetY = 0.0;
    if (imageWidth > imageHeight) {
        offsetX = (imageWidth -imageHeight) / 2;
        croppedSize = CGSizeMake(imageHeight, imageHeight);
    } else {
        offsetY = (imageHeight-imageWidth) / 2;
        croppedSize = CGSizeMake(imageWidth, imageWidth);
    }
    CGRect clippedRect = CGRectMake(offsetX, offsetY, croppedSize.width, croppedSize.height);
    CGImageRef imageRef = CGImageCreateWithImageInRect([image CGImage], clippedRect);
    
    float ratio = croppedSize.width>120?120:croppedSize.width;
    CGRect rect = CGRectMake(0.0, 0.0, ratio, ratio);
    UIGraphicsBeginImageContext(rect.size);
    [[UIImage imageWithCGImage:imageRef] drawInRect:rect];
    UIImage *thumbnail = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    CGImageRelease(imageRef);
    
    return thumbnail;
}

//在获取到内存中的图片进行体积减少操作
else if (cachedImage) {
    cachedImage = [self compressImageWith:cachedImage];//修改图片大小
    [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
    [self safelyRemoveOperationFromRunning:strongOperation];
} else {

//在下载完成后对图片进行体积减少操作
if (downloadedImage && finished) {//修改图片大小
      [self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil];
    downloadedImage = [self compressImageWith:downloadedImage];
    downloadedData = [NSMutableData dataWithData:UIImageJPEGRepresentation(downloadedImage, 1)];
//                            if (self.cacheSerializer) {
//                                dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
//                                    NSData *cacheData = self.cacheSerializer(downloadedImage, downloadedData, url);
//                                    [self.imageCache storeImage:downloadedImage imageData:cacheData forKey:key toDisk:cacheOnDisk completion:nil];
//                                });
//                            } else {
//                                [self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil];
//                            }
}
[self callCompletionBlockForOperation:strongSubOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
图片推荐使用webp和BPG格式

这两种格式的压缩比例非常大,在使用的时候需要先解压缩,所以内存占用变小,但是CPU的使用率会上升。

UITableView优化

1 缓存自定义高度

很多人cell高度自适应的实现思路都是,在Cell中提供一个根据数据计算内含空间高度的方法,然后在heightForRowAtIndexPath中调用去实时计算。这里就会存在问题,一个是这个计算高度的操作必然会相对比较耗时,另外一个就是heightForRowAtIndexPath这个方法在UTableView的一次刷新中会被调用3-5次,那这个计算就相当于是重复计算了3-5次,显然是可以优化的。
优化的方法是在数据拿到之后直接根据数据计算出对应Cell的高度,UITableView刷新的时候就去数组对应的位置取对应的高度,这样Cell高度的确定就由计算操作改为了取值操作。

2 处理离屏渲染

off Screen Rendering意为离屏渲染,指的是GPU在当前屏幕缓冲区意外开辟一个缓冲区进行渲染操作。创建新缓冲区,上下文切换离屏渲染的整个过程,需要多次切换上下文环境,先从当前屏幕切换到离屏,等到离屏渲染结束后,将离屏缓冲区的渲染结果显示到屏幕上有需要将上下文环境从离屏切换到当前屏幕。从上下文环境的切换是要付出很大的代价的。

UITableView的离屏渲染主要存在于UIImageView圆角绘制上,目前主要有三种方式绘制圆角
1 layer

view.layer.cornerRadius = 20;
view.layer.masksToBounds = YES;

优点:简单易用
缺点:性能消耗大,会造成丽萍渲染

2 贝塞尔遮罩

CAShapeLayer *layer = [CAShapeLayer layer];  
UIBezierPath *aPath = [UIBezierPath bezierPathWithOvalInRect:aImageView.bounds];  
layer.path = aPath.CGPath;  
poImgView.layer.mask = layer;

优点:看起来技术点上更高级点
缺点:性能消耗大,比第一种都大, 离屏渲染

3 UIImage绘制

- (UIImage *)imageWithCornerRadius:(CGFloat)radius {
CGRect rect = (CGRect){0.f, 0.f, self.size};
 
UIGraphicsBeginImageContextWithOptions(self.size, NO, UIScreen.mainScreen.scale);
CGContextAddPath(UIGraphicsGetCurrentContext(),
 [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:radius].CGPath);
CGContextClip(UIGraphicsGetCurrentContext());
[self drawInRect:rect];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}

优点:性能高,不会离屏渲染
缺点:书写起来麻烦
推荐使用第三种方法

Cell的异步绘制

利用runloop的model特性,实现在滚动的时候不进行图片的加载。方法比较简单,这里不贴代码了。

卡顿优化

如何查找卡顿

KMCGeigerCounter是一个比较不错的屏幕卡顿监测的库,或者自己利用Runtime写一个专属的监测包也可以,参考资料:iOS屏幕卡顿监测Runtime实现
XCode调试过程中面板还有一个fps监测面板

如何解决卡顿

主要从以下几个点进行优化
1 UIImageView和View尽量设置为不透明。
当某一块图层的alpha和其superView的背景色alpha不一样的时候会触发alpha合成操作,这是一项看似很简单但却是非常消耗CPU性能的操作。UIView的背景色尽量不要设置为clearColor,这样也会触发alpha叠加,在UITableView滑动的时候是非常消耗性能的。子视图的背景色尽可能设置成其superView的背景色,这样图层合成的时候不会触发blend操作。
最好不使用带alpha通道的图片,如果有alpha尽量让UI设计人员取消alpha通道。
2 cell上layer尽量避免使用圆角
这个上面有解释,这里不多赘述。
3 图片尽量使用imageWithContentOfFile,这种方式当使用完图片的时候会立即丢弃释放资源,所以对性能不会带来负担。
4 尽量延迟加载图片
当我们在滑动页面的时候尤其对于那种布局特别复杂的cell,滑动的时候不要加载图片,当滑动停止得时候再进行图片的加载。
我们都知道不管是UITableView还是UIScrollView在滚动的时候需要显示东西都是通过runLoop去拿。
当滚动的时候runLoop会处于NSRunLoopTrackingMode的模式,我们可以通过一个主线程队列dispatch_after或者selfPerformSelector设置runLoop的模式为NSDefaultRunLoopMode模式,就可以做到停止滚动再加载图片。
注:其实严格意义上selfPerformSelector的事件就是在主线程队列中等待。
5 尽量不要使用xib和storyBoard
苹果推出storyboard确实为开发者节省了大量的时间,提高了开发效率,但是对于那种复杂的滑动界面,利用storyboard是非常消耗资源的,不信的可以试试用性能工具timeProfie看看CPU所占的性能百分比,其CPU的资源远远大于纯代码布局。

网络请求优化

网络请求这块优化的地方了解的比较少,我这里主要是通过二次封装实现下面几个方面的优化

1 取消重复网络请求

通过封装,利用Runtime,将网络请求对象的生命周期和传入对象的生命周期绑定(通常是对应的Controller),在Controller被释放的时候,同步释放网络请求对象 ,也就同步取消了网络请求。
代码示例

@implementation NSObject (NMNetWorkingAutoCancel)
-(NetManAutoCancelHandler *)netManAutoCancelRequests
{
    NetManAutoCancelHandler *requests = objc_getAssociatedObject(self, @selector(netManAutoCancelRequests));
    if (requests == nil) {
        requests = [[NetManAutoCancelHandler alloc]init];
        objc_setAssociatedObject(self, @selector(netManAutoCancelRequests), requests, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return requests;
}
@end
2 取消无用的网络请求

举个例子,在省市级信息筛选,一开始选择山东省,那么就会去请求山东省的市级信息,再点击河北,就会去请求河北省的市级信息。这个时候就会产生一个问题,第一次请求山东省的市级信息是误操作,在点击河北省的时候,请求山东省的市级信息这个操作就是无意义的了,那么应该设法将这个请求也取消掉,具体思路可参考上面的方法。

3 网络请求数据缓存

二次封装的网络框架应该需要提供一个网络缓存的功能。例如淘宝的商品详情页,信息在很长的一段时间内是不会变化的,那么框架就可以支持一种功能,即以url+请求参数为key,以请求的结果为value的一个网络请求缓存,若是本地的网络请求数据在有效期内,那么不进行网络请求,本地提取即可。

4 最优服务器选择

二次封装的另外一个优化就是提供一个接口,可以动态配置服务器的地址。APP在启动后,对一个服务器列表中的所有地址进行ping,然后取ping值最小的那个服务器的地址为主服务器地址,接受动态替换。

我曾执笔雕刻时光 奈何良辰难书过往

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

推荐阅读更多精彩内容