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

2020-03-23  本文已影响0人  执笔时光er

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

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值最小的那个服务器的地址为主服务器地址,接受动态替换。

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

上一篇下一篇

猜你喜欢

热点阅读