简单-UIKit性能分析优化
本文是基于UIKit性能调优实战讲解 - 简书这篇文章的总结详细请看原文
实例讲解可以参考:小心别让圆角成了你列表的帧数杀手 - CocoaChina_让移动开发更简单
fps表示frames per second,也就是每秒钟显示多少帧画面。对于静止不变的内容,我们不需要考虑它的刷新率,但在执行动画或滑动时,fps的值直接反映出滑动的流畅程度
调试优化:
1.图层混合:
首先理解像素:像素就是屏幕上的每一个点由RGB三种颜色构成有时候会带有alpha,如果某一个区域覆盖了多个layer,最后的显示效果就会收到影响,显色混合会消耗一定的GPU,当把最上层的颜色透明度设置为100%时GPU就会忽略下面所有的layer节省不必要的运算
第一个调试选项"Color Blended Layers"正是用于检测哪里发生了图层混合,并用红色标记出来。因此我们需要尽可能减少看到的红色区域。一旦发现应该想法设法消除它
解决方案:a.将控件的opaque = true 原理是希望避免图层混合,但是作用不大,因为UIView的这个属性默认就是true只要不是人为的设置为透明都不会出现图层混合,但对于UIImageView他本身和图片都不能为透明的,图片自身的性质也可能会对结果产生影响,比opaque属性更重要的是backgroundColor属性,如果不设置这个属性,控件依然被认为是透明的,所以我们做的是将他设置为白色
PS:如果label文字有中文,依然会出现图层混合,这是因为此时label多了一个sublayer,如果有好的解决办法欢迎告诉我。
2.光栅化光栅化会导致离屏渲染
光栅化是将一个layer预先渲染成位图(bitmap),然后加入缓存中。如果对于阴影效果这样比较消耗资源的静态内容进行缓存,可以得到一定幅度的性能提升。demo中的这一行代码表示将label的layer光栅化:
label.layer.shouldRasterize = true
Instrument中,第二个调试选项是“Color Hits Green and Misses Red”,它表示如果命中缓存则显示为绿色,否则显示为红色,显然绿色越多越好,红色越少越好。
光栅化的缓存机制是一把双刃剑,先写入缓存再读取有可能消耗较多的时间。因此光栅化仅适用于较复杂的、静态的效果。通过Instrument的调试发现,这里使用光栅化经常出现未命中缓存的情况,如果没有特殊需要则可以关闭光栅化
3.颜色格式:
像素在内存中的布局和它在磁盘中的存储方式并不相同。考虑一种简单的情况:每个像素有R、G、B和alpha四个值,每个值占用1字节,因此每个像素占用4字节的内存空间。一张1920*1080的照片(iPhone6 Plus的分辨率)一共有2,073,600个像素,因此占用了超过8Mb的内存。但是一张同样分辨率的PNG格式或JPEG格式的图片一般情况下不会有这么大。这是因为JPEG将像素数据进行了一种非常复杂且可逆的转化。
比如应用中有一些从网络下载的图片,而GPU恰好不支持这个格式,这就需要CPU预先进行格式转化
第三个选项“Color Copied Images”就用来检测这种实时的格式转化,如果有则会将图片标记为蓝色。
4.图片大小
第五个选项“Color Misaligned Images”。它表示如果图片需要缩放则标记为黄色,如果没有像素对齐则标记为紫色
第三个优化是调整所有图片的像素大小以避免不必要的缩放。
5.离屏渲染
正常的渲染通道:OpenGL提交一个命令到Command Buffer,随后GPU开始渲染,渲染结果放到Render Buffer中,这是正常的渲染流程
有一些复杂的效果无法直接渲染出结果,它需要分步渲染最后再组合起来,比如添加一个蒙版(mask):GPU分别得到了纹理(texture,也就是那个相机图标)和layer(蓝色的蒙版)的渲染结果。但这两个渲染结果没有直接放入Render Buffer中,也就表示这是离屏渲染。直到第三个渲染通道,才把两者组合起来放入Render Buffer中。离屏渲染意味着把渲染结果临时保存,等用到时再取出,因此相对于普通渲染更占用资源。
第六个选项“Color Offscreen-Rendered Yellow”会把需要离屏渲染的地方标记为黄色,大部分情况下我们需要尽可能避免黄色的出现。离屏渲染可能会自动触发,也可以手动触发。以下情况可能会导致触发离屏渲染:
重写drawRect方法
有mask或者是阴影(layer.masksToBounds, layer.shadow*),模糊效果也是一种mask
layer.shouldRasterize = true
前两者会自动触发离屏渲染,第三种方法是手动开启离屏渲染。
第四个优化,在设置阴影效果的四行代码下面添加一行:
imgView.layer.shadowPath = UIBezierPath(rect: imgView.bounds).CGPath
这行代码制定了阴影路径,如果没有手动指定,Core Animation会去自动计算,这就会触发离屏渲染。如果人为指定了阴影路径,就可以免去计算,从而避免产生离屏渲染。
设置cornerRadius本身并不会导致离屏渲染,但很多时候它还需要配合layer.masksToBounds = true使用。根据之前的总结,设置masksToBounds会导致离屏渲染。解决方案是尽可能在滑动时避免设置圆角,如果必须设置圆角,可以使用光栅化技术将圆角缓存起来:
// 设置圆角 ps:用后用后无效果
label.layer.masksToBounds = true
label.layer.cornerRadius = 8
label.layer.shouldRasterize = true
label.layer.rasterizationScale = layer.contentsScale
6.快速路径:
之前将离屏渲染和渲染路径时的示意图么,离屏渲染的最后一步是把此前的多个路径组合起来。如果这个组合过程能由CPU完成,就会大量减少GPU的工作。这种技术在绘制地图中可能用到。
第七个选项“Color Compositing Fast-Path Blue”用于标记由硬件绘制的路径,蓝色越多越好。
7.变化区域
刷新视图时,我们应该把需要重绘的区域尽可能缩小。对于未发生变化的内容则不应该重绘,第八个选项“Flash updated Regions”用于标记发生重绘的区域。一个典型的例子是系统的时钟应用,绝大多数时候只有显示秒针的区域需要重绘:
总结
如果你一步一步做到了这里,我想一定会有不少收益。不过,学而不思则罔,思而不学则殆。动手实践后还是应该总结提炼,优化滑动性能主要涉及三个方面:
避免图层混合
确保控件的opaque属性设置为true,确保backgroundColor和父视图颜色一致且不透明
如无特殊需要,不要设置低于1的alpha值
确保UIImage没有alpha通道
避免临时转换
确保图片大小和frame一致,不要在滑动时缩放图片
确保图片颜色格式被GPU支持,避免劳烦CPU转换
慎用离屏渲染
绝大多数时候离屏渲染会影响性能
重写drawRect方法,设置圆角、阴影、模糊效果,光栅化都会导致离屏渲染
设置阴影效果是加上阴影路径
滑动时若需要圆角效果,开启光栅化
实战
本文的demo可以在我的Github上下载,然后一步一步自己体验优化过程。但demo毕竟是刻意搭建的一个环境,我会在我自己的仿写的简书app上不断进行实战优化,欢迎共同学习交流。
代码:
UIView圆角:
#import "UIView+AddCorner.h"
@implementation UIView (AddCorner)
-(double)roundbyuint:(double) num anUint:(double )unit{
double remain = modf(num, &unit);
if (remain > unit / 2.0) {
return [self ceilbyuint:num andUint:unit];
} else {
return [self floorbyuint:num andUint:unit];
}
}
-(double)ceilbyuint:(double)num andUint:(double)unit{
return num - modf(num, &unit) + unit;
//将浮点数num分解成整数部分和小数部分
}
-(double)floorbyuint:(double)num andUint:(double)unit{
return num - modf(num, &unit) ;
}
-(double)piexl:(double)num{
double unit;
switch ((int)[UIScreen mainScreen].scale) {
case 1: unit = 1.0/1.0; break;
case 2: unit = 1.0/2.0; break;
case 3: unit = 1.0/3.0; break;
default: unit = 0.0;
break;
}
return [self roundbyuint:num anUint:unit];
}
-(void)kt_viewAddCorner:(CGFloat)radius andBorderWidth:(CGFloat)borderWidth andBorderColor:(UIColor*)borderColor andBackgroundColor:(UIColor*)backgroundColor{
UIImageView * imageView = [[UIImageView alloc]initWithImage:[self ViewAddCorner:radius andBorderWidth:borderWidth andBorderColor:borderColor andBackgroundColor:backgroundColor]] ;
[self insertSubview:imageView atIndex:0];
}
-(UIImage *)ViewAddCorner:(CGFloat)radius andBorderWidth:(CGFloat)borderWidth andBorderColor:(UIColor*)borderColor andBackgroundColor:(UIColor*)backgroundColor {
CGSize sizeToFit = CGSizeMake((double)CGRectGetWidth(self.frame), (double)CGRectGetHeight(self.frame));
CGFloat halfBorderWidth = (CGFloat)borderWidth / 2.0;
UIGraphicsBeginImageContextWithOptions(sizeToFit, false, [UIScreen mainScreen].scale);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, borderWidth);
CGContextSetStrokeColorWithColor(context, borderColor.CGColor);
CGContextSetFillColorWithColor(context, backgroundColor.CGColor);
CGFloat width = sizeToFit.width, height = sizeToFit.height;
CGContextMoveToPoint(context, width - halfBorderWidth, radius + halfBorderWidth); // 开始坐标右边开始
CGContextAddArcToPoint(context, width - halfBorderWidth, height - halfBorderWidth, width - radius - halfBorderWidth, height - halfBorderWidth, radius); // 右下角角度
CGContextAddArcToPoint(context, halfBorderWidth, height - halfBorderWidth, halfBorderWidth, height - radius - halfBorderWidth, radius); // 左下角角度
CGContextAddArcToPoint(context, halfBorderWidth, halfBorderWidth, width - halfBorderWidth, halfBorderWidth, radius); // 左上角
CGContextAddArcToPoint(context, width - halfBorderWidth, halfBorderWidth, width - halfBorderWidth, radius + halfBorderWidth, radius) ;// 右上角
CGContextDrawPath(UIGraphicsGetCurrentContext(), kCGPathFillStroke);
UIImage *output = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return output;
}
@end
UIImageView圆角:
#import "UIImageView+AddCorner.h"
@implementation UIImageView (AddCorner)
-(void)kt_ImageViewAddCornerWithRadius:(CGFloat)radius{
// self.image = self.image?:[self.image imageAddCornerWithRadius:radius andSize:self.bounds.size];
//注意第三个选项的设置
UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, [UIScreen mainScreen].scale);
//在绘制之前先裁剪出一个圆形
[[UIBezierPath bezierPathWithRoundedRect:self.bounds
cornerRadius:radius] addClip];
//图片在设置的圆形里面进行绘制
[self.image drawInRect:self.bounds];
//获取图片
self.image = UIGraphicsGetImageFromCurrentImageContext();
//结束绘制
UIGraphicsEndImageContext();
}
@end
UIImage圆角:
#import "UIImage+ImageRoundedCorner.h"
@implementation UIImage (ImageRoundedCorner)
- (UIImage*)imageAddCornerWithRadius:(CGFloat)radius andSize:(CGSize)size{
CGRect rect = CGRectMake(0, 0, size.width, size.height);
UIGraphicsBeginImageContextWithOptions(size, NO, [UIScreen mainScreen].scale);
CGContextRef ctx = UIGraphicsGetCurrentContext();
UIBezierPath * path = [UIBezierPath bezierPathWithRoundedRect:rect byRoundingCorners:UIRectCornerAllCorners cornerRadii:CGSizeMake(radius, radius)];
CGContextAddPath(ctx,path.CGPath);
CGContextClip(ctx);
[self drawInRect:rect];
CGContextDrawPath(ctx, kCGPathFillStroke);
UIImage * newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return newImage;
}
@end