美文网首页
页面优化

页面优化

作者: 三国韩信 | 来源:发表于2020-07-03 18:13 被阅读0次
    UIView和UILayer的关系

    self.view.backgroupColor = [UIColor redColor];
    这句话做了一个隐式的动画提交[CATransaction commit]
    那么 Commit Transaction做了什么,大概包括了以下4个步骤:

    • Layout,构建视图
    • Display,绘制视图
    • Prepare,额外的 Core Animation ⼯工作,⽐比如解码
    • Commit,打包图层并将它们发送到 Render Serve
    1. 在第一个步骤中Layout构建视图会循环的去遍历图层,计算frame。循环的调用[layer layoutSublayers] 。而 layoutSublayers会去调用它的delegate对象(UIView)的 layoutSublayersOfLayer方法,在layoutSublayersOfLayer方法中又会去调用layoutSubviews方法。大概的伪代码如下图:


      Layout操作
    2. 第二个步骤中,绘制视图的操作,是由[CALayer dispaly]->[CALayer drawInContext:]->[UIView drawLayer:inContext:]->[UIView drawRect]
      最终来到了我们熟悉的drawRect方法绘制出视图的样子。


      Display流程
    3. 第三和第四个步骤由底层的Core Animation提交个OpenGL ES或者Metal,再由Metal去操作硬件GPU来渲染出画面。

    那么UIView和CALayer的关系是啥呢?UIView是它对应layer的delegate,默认的layer是CALayer,当然也可以指定为开发者创建的某个CALayer的子类。


    view与layer

    view遵守了CALayerDelegate协议,layer的很多方法都是通过它的代理view来实现的。通过自定义一个CALayer的子类来绑定自定义的view,来看其中的关系。

    #import <UIKit/UIKit.h>
    @interface LGView : UIView
    - (CGContextRef)createContext;
    - (void)closeContext;
    @end
    
    #import "LGView.h"
    #import "LGLayer.h"
    @implementation LGView
    
    - (void)drawRect:(CGRect)rect {
        // Drawing code, 绘制的操作, BackingStore(额外的存储区域产于的) -- GPU
    }
    
    //子视图的布局
    - (void)layoutSubviews{
        [super layoutSubviews];
    }
    
    + (Class)layerClass{
        return [LGLayer class];
    }
    
    - (void)layoutSublayersOfLayer:(CALayer *)layer{
         [self layoutSubviews];
    }
    
    - (CGContextRef)createContext{
        
        UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, 0);
        CGContextRef context = UIGraphicsGetCurrentContext();
        return context;
    }
    
    - (void)layerWillDraw:(CALayer *)layer{
        //绘制的准备工作,do nontihing
    }
    
    //绘制的操作
    - (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx{
        [[UIColor redColor] set];
           //Core Graphics
        UIBezierPath *path = [UIBezierPath bezierPathWithRect:CGRectMake(self.bounds.size.width/2.0-20,self.bounds.size.width/2.0-20, 40, 40)];
        CGContextAddPath(ctx, path.CGPath);
        CGContextFillPath(ctx);
    }
    
    //layer.contents = (位图)
    - (void)displayLayer:(CALayer *)layer{
        UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
        dispatch_async(dispatch_get_main_queue(), ^{
            layer.contents = (__bridge id)(image.CGImage);
        });
    }
    
    - (void)closeContext{
        UIGraphicsEndImageContext();
    }
    @end
    
    #import <QuartzCore/QuartzCore.h>
    @interface LGLayer : CALayer
    @end
    
    #import "LGLayer.h"
    @implementation LGLayer
       //前面断点调用写下的代码
    - (void)layoutSublayers{
        if (self.delegate && [self.delegate respondsToSelector:@selector(layoutSublayersOfLayer:)]) {
            //UIView
            [self.delegate layoutSublayersOfLayer:self];
        }else{
            [super layoutSublayers];
        }
    }
    //绘制流程的发起函数
    - (void)display{
         // Graver 实现思路
        CGContextRef context = (__bridge CGContextRef)([self.delegate performSelector:@selector(createContext)]);
        
        [self.delegate layerWillDraw:self];
        
        [self drawInContext:context];  // 这里会自动回调到其delegate的drawLayer:inContext方法里。即自动调用LGView的对应方法
        
        [self.delegate displayLayer:self];
    
        [self.delegate performSelector:@selector(closeContext)];
    }
    @end
    

    在LGView中的+ (Class)layerClass可以看出,是吧LGView的layer绑定到了LGLayer上。那么LGView就成了LGLayer的delegate。在LGLayer的display方法和layoutSublayers方法中看到,有出现了很多self.delegate去调用的方法,它就会自动的回调到LGView里的对应方法里去。还有layer 的drawInContext:方法也会自动的调用其delegate的drawLayer:inContext 方法。

    通过这样把layer和view关联了起来,那么可能会有人问,为什么这么麻烦,直接把layer渲染显示的功能加到UIView上,或者把UIView的功能都加到layer,合成一个不就好么?

    这是一种设计思想——职责单一化。layer的作用渲染图片、动画等。UIView的功能有处理点击事件、管理子View等。各自的功能分开,而且layer单独抽出,他不止可以为iOS服务,还可以Mac,还可以iWatch等等。封装和设计的思想,细细品!

    页面卡顿检测
    1. 通过RunLoop来检测
    2. 通过FPS来检测 比如YYKit 框架来检测。
    3. 微信的卡顿检测方案——matrix框架
    页面卡顿优化
    1. 预排版,在子线程里提前计算好页面的布局。比如tableView中cell高度提前计算好,不要再heightForRowAtIndexPath的回调的里去计算。
    2. 页面中有出现圆角的情况,不要直接用view.layer.cornerRaudius去设置圆角。(会触发离屏渲染)能让UI切圆角图的,最好找UI去切图,不能的自己去画圆角。
    3. 预解码。把图片的解码的操作从主线程放到子线程去做。我们平时常用的SDImage或者YYImage框架其实已经做了这样的操作了。
    // 调用SDImage的这个方法,把解码成位图的操作放到子线程,然后通过block回调回来。然后在block里去自己画圆角的操作。
     [self.iconButton sd_setImageWithURL:[NSURL URLWithString:layout.timeLineModel.iconUrl] forState:UIControlStateNormal placeholderImage:nil options:SDWebImageAvoidAutoSetImage completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
            [self.iconButton setImage:[image cornerRadius:self.iconButton.bounds.size.width/ 2.0 size:self.iconButton.bounds.size] forState:UIControlStateNormal]; 
        }];
    
    // 自定画圆角
    #import "UIImage+cornerRadious.h"
    @implementation UIImage (cornerRadious)
    
    - (UIImage *)cornerRadius:(CGFloat)cornerRadius size:(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(cornerRadius, cornerRadius)];
        CGContextAddPath(ctx,path.CGPath);
        CGContextClip(ctx);
        [self drawInRect:rect];
        CGContextDrawPath(ctx, kCGPathFillStroke);
        UIImage * newImage = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
        return newImage;
    }
    @end
    
    1. 减少图层。 尽可能的减少图层的嵌套。图层越多,CPU的frame的计算、GPU的渲染也会耗时越多。如果不能避免很多图层的嵌套,可以考虑把整个图层合成一张位图(这个操作也很麻烦。。。)
    2. 异步渲染 这里推荐美团开源的Grave框架,不过它因为某些原因下架了。
    离屏渲染

    在上面的优化中,有说到设置圆角会触发离屏渲染,那么什么是离屏渲染?为什么圆角会触发离屏渲染?还有哪些会触发离屏渲染?出现了离屏渲的影响等也都是在面试的过程中会被问到的?

    • 什么是离屏渲染
      在说什么是离屏渲染之前,先来了解一下,我们代码上的view是如何被渲染到屏幕上的。
      简单的说,App上的一个view,经过一系列的操作后,会被放到帧缓冲区Frame Buffer里,然后等待被渲染的屏幕上。可是遇到一些稍微复杂的图像,当CPU和GPU不能够一次性的把图片渲染完放到Frame Buffer,那么此时就需要一个额外的存储空间来存放临时的图像信息,这个额外的存储空间就叫Offscreen Buffer(离屏缓冲区)。
    正常渲染简易流程 触发了离屏渲染的流程.png
    • 为什么设置圆角的时候会触发离屏渲染
      在知道了什么是离屏渲染之后,那么在平常开发的过程中总是听说设置了圆角会触发离屏渲染。这其中的原理是啥?是不是所有的设置圆角都会有离屏渲染的情况?(上代码)
        UIImageView *img1 = [[UIImageView alloc]init];
        img1.frame = CGRectMake(100, 420, 100, 100);
        img1.backgroundColor = [UIColor blueColor];
        img1.layer.cornerRadius = 50;
        img1.layer.masksToBounds = YES;
        img1.image = [UIImage imageNamed:@"btn.png"];
        [self.view addSubview:img1];
    

    众所周知,上面的代码会触发离屏渲染,因为设置了img1.layer.cornerRadius = 50; img1.layer.masksToBounds = YES; 这是为何? 苹果baba对cornerRadius 属性的解释是——当设置了cornerRadius的时候,只会设置layer 的background color 和border为圆角,而它的content不会默认被设置为圆角,只有同时设置了masksToBounds属性时,content才会被设置成圆角。


    cornerRadius的属性解释
    设置圆角的情况

    根据上面离屏渲染的定义可知UIImageView在被渲染到屏幕的过程中,它需要同时对imageView的自身的background color去设置圆角和其image图片去设置圆角,这2个操作它不是一次性把做完后放到frame buffer里去的。它需要offScreen buffer来存中间的temp。在offScreen buffer这2者都操作完成了再输入到frame buffer,等待渲染到屏幕上。

    设置圆角渲染的流程图

    那么是不是所有的设置圆角都会触发离屏渲染呢?通过上面的渲染流程可以看出,因为imageView本身background color和content都要设置圆角才需要offscreen buffer这个临时空间来存储。那么是不是当imageview的background color属性不赋值的时候,就不会离屏渲染呢?答案是肯定的,当只有imageView的content需要设置圆角的时候,是不需要offscreen存在的,一步到位的渲染好了放到帧缓冲区里等待上屏。showCode:

      // 当backgroundColor属性不赋值的时候不会触发离屏渲染
        UIImageView *img1 = [[UIImageView alloc]init];
        img1.frame = CGRectMake(100, 420, 100, 100);
        //img1.backgroundColor = [UIColor blueColor];
        img1.layer.cornerRadius = 50;
        img1.layer.masksToBounds = YES;
        img1.image = [UIImage imageNamed:@"btn.png"];
        [self.view addSubview:img1];
    
    • 还有哪些行为会触发离屏渲染
    1. 使用了 mask 的 layer (layer.mask)
    2. 需要进行裁剪的 layer (layer.masksToBounds / view.clipsToBounds)
    3. 设置了组透明度为 YES,并且透明度不为 1 的 layer (layer.allowsGroupOpacity/ layer.opacity)
    4. 添加了投影的 layer (layer.shadow*)
    5. 毛玻璃效果的图片
    6. 采用了光栅化的 layer (layer.shouldRasterize)
      7.绘制了文字的 layer (UILabel, CATextLayer, Core Text 等)
    • 离屏渲染的影响
      开发过程中最常见的就是在tableView或者collectionView中cell里有圆角的imageView或view。tableView在滚动的时候,每一帧变化都会触发每个cell的重新绘制,因此一旦存在离屏渲染,本来直接输出到frame buffer的操作就会被打断,要临时开辟一块空间(offscreen buffer)来进行“圆角”操作,然后在回到frame buffer。这一个切换在每秒会发生60次,并且很可能每一帧有几十张的图片要求这么做,对于GPU的性能冲击可想而知。故在tableView或者collectionView这种控件中尽可能的减少离屏渲染的情况出现。
    • 圆角代码解析
    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view, typically from a nib.
        
        //1.按钮存在背景图片(2个图层)
        UIButton *btn1 = [UIButton buttonWithType:UIButtonTypeCustom];
        btn1.frame = CGRectMake(100, 30, 100, 100);
        btn1.layer.cornerRadius = 50;
        [self.view addSubview:btn1];
        [btn1 setImage:[UIImage imageNamed:@"btn.png"] forState:UIControlStateNormal];
        btn1.layer.masksToBounds = YES;
        
        //2.按钮不存在背景图片
        UIButton *btn2 = [UIButton buttonWithType:UIButtonTypeCustom];
        btn2.frame = CGRectMake(100, 150, 100, 100);
        btn2.layer.cornerRadius = 50;
        btn2.backgroundColor = [UIColor blueColor];
        [self.view addSubview:btn2];
        btn2.clipsToBounds = YES;
        
        UIButton *btn3 = [UIButton buttonWithType:UIButtonTypeCustom];
        btn3.frame = CGRectMake(100, 280, 100, 100);
        [self.view addSubview:btn3];
        [btn3 setImage:[UIImage imageNamed:@"btn.png"] forState:UIControlStateNormal];
        
        //3.UIImageView 设置了图片+背景色;
        UIImageView *img1 = [[UIImageView alloc]init];
        img1.frame = CGRectMake(100, 420, 100, 100);
        img1.backgroundColor = [UIColor blueColor];
        [self.view addSubview:img1];
        img1.layer.cornerRadius = 50;
        img1.layer.masksToBounds = YES;
        img1.image = [UIImage imageNamed:@"btn.png"];
        
        //4.UIImageView 只设置了图片,无背景色;
        UIImageView *img2 = [[UIImageView alloc]init];
        img2.frame = CGRectMake(100, 560, 100, 100);
        //img2.backgroundColor = [UIColor blueColor];
        [self.view addSubview:img2];
        img2.layer.cornerRadius = 50;
        img2.layer.masksToBounds = YES;
        img2.image = [UIImage imageNamed:@"btn.png"];
        
    }
    

    上面的代码中,btn1、btn2、btn3、img1、img2五中情况是否都会触发离屏渲染呢?答案如图,只有btn1、和img1会,其他的不会。

    image.png

    解析1:在第一张图中,存在这2个图层,btn1和imageview,这2个layer都设置了圆角,那么肯定需要offscreen buffer来中间缓存着。

    解析2:在第二张图中,只是btn2自己本身设置了cornerRadius,没有其他图层的需要渲染,可以直接放到到frame buffer等到上屏,不需要offscreen buffer,不会触发离屏渲染。

    解析3:在第三张图中,虽然有btn3和imageView的存在,但他两只是单纯的图层的叠加,没有其他特殊的渲染操作,CUP会先把btn从帧缓冲区拿出来渲染到屏幕,再把imageview从帧缓冲区拿出来渲染到屏幕,这个过程不需要用到offscreen buffer,也不会触发离屏渲染。

    解析4:在第四张图的情况就是我们常常见到的会触发离屏渲染的情况,具体的流程如上面的设置圆角渲染流程图所示。

    解析5:第5张图和第4张图就差了一句设置backgroundColor的代码。结果第5中就不会出现离屏渲染。在img2中,没有设置backgroundColor,只有其位图需要设置圆角,这种情况不需要offscreen buffer,直接放到frame buffer等到上屏即可。

    相关文章

      网友评论

          本文标题:页面优化

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