美文网首页
界面卡顿

界面卡顿

作者: Carden | 来源:发表于2017-05-22 18:57 被阅读426次

    记单词

    • FPS:Frames Per Second每秒传输帧数。60满帧。每16ms刷新一次屏幕。

    • VSync:vertical synchronization垂直同步信号,当一帧画面绘制完成后,电子枪回复到原位,准备画下一帧前,显示器会发出一个垂直同步信号

    • CPU:操作对象(分内存、调属性、读数据、layer层属性巨耗性能);计算布局(本质调整frame/bounds/center属性);文本计算(NSAttributedString、CoreText);文本渲染(创建CoreText对象、内容异步绘制成Bitmap、默认都是主线程);解码图片(UIImage-> Bitmap->显示。变主线程为异步);绘制图像(异步将内容填充到上下文)。经常的CPU计算量过大就例如当你正在执行一个for循环的时候去滚动TableView这样必须卡顿呀!

    • GPU:CPU将计算好的内容移交给GPU --> 图片纹理+矢量图形 -> 变换+合成+渲染 --> 渲染结果提交到帧缓冲区 --> 显示器发出VSync信号 --> 逐行读取帧缓存区数据 --> 数模传输给显示器显示

    离屏渲染负担过重

    • Cell视图大量使用view.layer.cornerRadius + masksToBounds的设置,造成卡顿
    • View圆角遮罩->GPU离屏渲染->拖慢了满帧60fp/s绘制->CPU圆角路径贝塞尔曲线UIBezierPath->UIImage天然圆角->直接填充到方的View

    • 性能 = GPU效率+CPU效率。

    • 离屏渲染真正的消耗 ≠ GPU的图形计算 = 屏幕内外缓冲区来回切换

    • 渲染本质 = 每1/60秒从屏幕缓冲区获取一次渲染结果 = CPU计算和GPU渲染之间及时交换数据 = 顺畅绘制(否则掉帧卡顿)

    • CPU圆角路径 -> 主线程计算耗时卡顿UI -> GPU图形遮罩切换缓冲区又耗内存 -> CPU异步计算UIImage圆角路径 -> 后台线程计算主线程统一绘制 -> 完善的线程管理机制+辅助Cache缓存机制->于是facebook开源的 AsyncDisplayKit诞生了

    离屏渲染

    • 原理:OPENGL单独创建内存->当前屏幕缓冲区以外新开辟一个缓冲区->GPU预合成图层混合体->CPU切换到当前屏幕缓冲区上下文->耗性能

    • 触发:shouldRasterize(光栅化保存到bitmap)、masks(遮罩)、shadows(阴影)、edge antialiasing(抗锯齿)、group opacity(不透明)

    • 特例:CPU渲染。使用Core Graphics技术对UIImage进行绘制操作。渲染得到的bitmap最后再交由GPU用于显示。

    • 工具:Instruments的Core Animation工具

    负载均衡

    • GPU天生图形处理,擅长浮点运算矩阵运算,但是屏幕渲染缓冲区切换相当耗性能

    • GPU离屏渲染负担过重,一部分运算交给CPU,减少卡顿

    • 离屏渲染的数量比较少,CPU闲着,把运算交给不擅长图形计算的CPU处理弄巧成拙

    /**
     *  返回一个图片圆形裁剪
     */
    + (UIImage *)imageWithClipImage:(UIImage *)image borderWidth:(CGFloat)borderWidth borderColor:(UIColor *)color{
        // 图片的宽度和高度
        CGFloat imageWH = image.size.width;
        // 设置圆环的宽度
        CGFloat border = borderWidth;
        // 圆形的宽度和高度
        CGFloat ovalWH = imageWH + 2 * border;
        // 1.开启上下文
        UIGraphicsBeginImageContextWithOptions(CGSizeMake(ovalWH, ovalWH), NO, 0);
        // 2.画大圆
        UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0, 0, ovalWH, ovalWH)];
        [color set];
        [path fill];
        // 3.设置裁剪区域
        UIBezierPath *clipPath = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(border, border, imageWH, imageWH)];
        [clipPath addClip];
        // 4.绘制图片
        [image drawAtPoint:CGPointMake(border, border)];
        // 5.获取图片
        UIImage *clipImage = UIGraphicsGetImageFromCurrentImageContext();
        // 6.关闭上下文
        UIGraphicsEndImageContext();
        
        return clipImage;
    }
    

    图片过大过多

    • 过大:tableView滚动 -> Cell子视图内容刷新 - > 网络请求图片 - > Image和View大小不一致 - > 设置.contentMode属性图片自由压缩 -> 图片开始transform计算乘以一个变换矩阵 - > CPU计算陡增 -> 解决方法是网络图片先异步处理后显示Or服务器返回预处理过的大小图

    • 过多:图片网络库下载图片 -> 异步把图片绘制到CGBitmapContext -> 绑定Bitmap到GPU Texture -> GPU调整渲染Texture纹理 - > GPU将渲染好的Bitmap从内存移植到显存 - > 短时间显示大量图片 - > GPU压力陡增 - > 掉帧

    动态高度计算

    • 尽量避免临时根据Model数据计算cell高度

    • 尽量不使用Cell高度返回函数,Cell高度委托里尽量从缓存里获取高度

    • 当然Cell定高优先考虑使用tableView.rowHeight属性

    • 缓存一切可缓存!空间换时间

    我们获取动态高度的时候,常见逻辑获取Cell高度-->创建Cell-->显示,那么我们通常都需要在计算Cell高度回调方法里先把Cell创建出来,必须先要知道Cell的内容才能决定Cell的高度,通常的做法都是专门谓词创建一个工具Cell,不为别的,就为了把内容复制到Cell的子视图上,如此一来适配Cell的高度,问题在于,在计算Cell高度成功,又会进入创建Cell的回调方法里,正式创建Cell显示出来,那么问题来了,这样本质上我们其实创建了两次Cell,赋值也赋值了两次!我们就会想了,为什么必须先调用Cell高度后调创建Cell的方法呢,如果先创建Cell后获取Cell高度,直接就可以通过Cell的内容布局来显示Cell呀。于是苹果想到了一个方法就是预估Cell高度的方法,就可以实现先创建Cell后返回Cell的真实高度了,如此一来,直接通过创建完成的Cell去获取Cell高度,直接就可以避免多次创建Cell赋值Cell内容的性能消耗了,这对于数量巨大的Cell和内容巨大的性能节省尤其明显。增加Cell盖度估算方法之后的逻辑:获取Cell数量-->数量*估算高度=contentSize--->创建Cell --->布局Cell的内容视图存储真实高度---->直接在返回真实高度里面把数组里面存储的高度返回。原本就是有多少个Cell就调用多少次真实高度计算方法,而且每一次计算高度都相当于创建了一个Cell,多耗性能,现在估算高度,直接相乘,然后真实高度直接从数组里面取。后来想了一下,还是觉得把创建Cell会布局得到的Cell高度存进模型里面,灵活性更高。直接以Model属性的形式存进模型里面。

    SubView过多

    • 减少subviews的数量

    • UIView很重,创建和销毁都比CALayer要耗费资源,CALayer代替UIView,典型分割线

    • 不需要响应事件的视图,优先考虑CALayer

    Cell 样式变化控制

    • addSubview、addSublayer、viewWithTag、removeFromSuperview大量的UI绘制会对内存和CPU产生负荷

    • 样式变化不大,使用hidden属性进行样式控制

    • Cell 样式变化很大,直接创建新的自定义Cell

    优化图片加载方式

    1、开辟很多内存将使用过的图片存储在缓存之中
    UIImageView *image = [UIImageView imageView:@"1.png"];
    
    2、使用完图片会立即丢弃释放资源。(工程中的大图&&使用次数很少 = 优先考虑)
    UIImageView *image = [UIImageView imageWithContentOfFile:@"1.png"];
    
    3、图片的解码:后台线程先把图片绘制到 CGBitmapContext中,然后从Bitmap直接创建图片
    
    
    一张png格式的图片赋值给UIImageView或CGImageSource之后,依然是png格式,只有在CPU将要把所有视图转变成bitmap位图的时候,才会解码图片,这一步实在主线程完成的,一张图片的数据量极大,如果是一张高清大图,CPU的压力可想而知,因此通常所用的三方库都会先把图片异步从png格式变成位图二进制模式并保存在图片的位图上下文之中,这样GPU在图层合成的时候直接就实现了数据的合成。
    

    图片集中加载卡顿

    • 拖拽滚动tableView -> NSRunLoopTrackingMode模式 - > 图片加载添加runLoop为NSDefaultRunLoopMode模式这个前提 -> 实现视图停止滚动才加载图片
    // 只在NSDefaultRunLoopMode模式下显示图片
    [self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"placeholder"] afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]];
    
    
    所谓的图片异步绘制其实就是说,下载完图片之后异步将图片从png格式变成bitmap保存在图片位图上下文之中,然后获取imageView的上下文,把图片位图上下文中提取到的bitmap绘制在layer层上。即使异步渲染,图片掉帧依然严重,这就需要从CPU和GPU的配合来看了,GPU专门用来处理bitmap的textTure纹理的地方,通过纹理对输入的bitmap进行变换混合渲染等操作,无论是CPU把bitmap从内存提到显存进而绑定到GPU的纹理上,还是GPU正式处理bitmap的过程,CPU都是轻松的,GPU压力很大,所以一旦大量图片需要加载,GPU就会力不从心,必然就会出现在16ms内不能完成渲染任务的情况,于是掉帧不可避免的出现了,最好的解决办法就是按需加载,只是加载屏幕附近的Cell的图片。
    
    
    为什么说减少视图的层次也能够提升性能,刚才说过GPU的textture纹理负责bitmap的混合,这个混合就相当于一个压缩的过程,把很多个图层的每一特性都要考虑到,然后决定最后的整体的样子,势必消耗性能巨大,假如说很多图层一开始就合成一张图片,就相当于原本10个图层,现在一个图层。alphe通道颜色的叠加通通不需要再考虑了。GPU 避免了多张 texture 合成和渲染的消耗,更少的 bitmap 也意味着更少的内存占用。
    

    透明度卡顿

    • 图层半透明Or背景色clearColor -> 图层blend操作即alpha合成操作 -> GPU渲染 -> 极耗性能

    • 设置非透明视图的layer.opaque属性为YES,以避免无用的 Alpha 通道合成

    • 尽量使所有的view opaque,包括Cell自身

    多个图片异步请求集中到达UI线程

    • 集中处理图片加载造成UI卡顿严重
    • performSelectorAfterDelay堆积图片加载操作到UI线程空闲的时候执行,副作用:滑动过程中不会加载任何图片
    • NSOperationQueue:队列中逐个执行

    内容太多未缓存

    主线程阻塞

    同步变异步

    • 布局计算

    • 文本排版

    • 图片/文本/图形渲染

    • 图片解码

    • 绘制

    性能优化

    • UIKit操作

    • Core Animation操作

    按需加载Cell

    本质需要明白的的及方法,第一获取当前屏幕的第一个Cell的indexPath,第二获取tableView的content最大y偏移对应的cell的indexPath,然后比较按需加载的前提,是否屏幕当前cell与目标Cell的行差大于设定值,如果大于,则执行按需加载的逻辑,很简单,本质上就是拒绝所有数据的reloadData,只刷新特定行的内容,首要就是找到特定行的indexPath,所以很容易,想获取当前屏幕上所有显示的Cell的indexPath集合,然后判断firstObject第一个indexpath.row是否大于三,然后把屏幕上方的三个indexPath也加入到数组之中。同理依然,一旦判断即使lastObject的indexpath加上3个屏幕下方的indexPath依然不会数组越界,就相当于直接就实现了tableView的数据按需加载。

    复用Cell

    • 不仅仅节省内存,更可以节省创建Cell的时间

    卡顿现象:

    1、cell大量subview修改了layer的属性,例如mask、shadow、corRodius

    2、cell视图中图片过大过多集中展示的时候,即使使用了三方库的图片异步加载

    3、cell中大量的文本宽高结算boundingRectWithSize、CoreText排版绘制成bitmap显示

    4、cell界面复杂,子视图巨多

    优化性能的举措:

    Cell创建:能用layer绝不用View、尽量避免layer属性的修改、少用clearView和alpha通道、drawRect异步绘制化繁为简、CoreText自定义文本控件

    Cell使用:固定高预估高行高函数赋值和布局计算分离滚动条跳跃、cell复用内存开销、按需加载Cell本质reload指定的indexPath数组

    Cell赋值:布局计算一步到位、图片解码加载、 避免UI绘制操作、图片过多的时候调整runloop

    1、layer

    2、Image(解码,加载)

    3、cellHeight

    4、绘制()

    5、渲染(Alpha、)

    6、解码(过大、过多)

    7、布局计算(Cell高度、Cell创建)

    界面卡顿的原因:

    CPU:内存、属性、CoreText、Bitmap

    GPU:纹理(位图)合成、OpenGL、

    FPS:

    VSync:交换数据、

    相关文章

      网友评论

          本文标题:界面卡顿

          本文链接:https://www.haomeiwen.com/subject/kcjixxtx.html