UIView和CALayer

1 UIView和CALayer关系

UIView是iOS系统中界面元素的基础,所有的界面元素都是继承自它。它真正的绘图部分,是由一个CALayer类来管理,UIView中对于图片的渲染,和一些动画操作都是layer完成的,UIView只是呈现CALayer的工作结果和处理交互响应事件。

1.1 UIView可以响应事件,CALayer不可以

  • UIView 继承于 UIResponder (UIResponder继承于NSObject)
  • CALayer继承于NSObject
    所以他们之间的差异来自于UIResponder定义的那一部分。

1.2 View和CALayer的Frame映射及View如何创建CALayer

一个Layer的frame是由它的anchorPoint、position、bounds和transform共同决定的。而一个view的frame则是返回Layer的frame,同样view的center和bounds也是Layer的一些相关属性。

// 要访问层,读取UIView实例的layer属性
CALayer *layer = myView.layer
//WYECustomView.swift
class WYECustomView: UIView {
        
    override init(frame: CGRect) {
        print("view init")
        super.init(frame: frame)
        self.backgroundColor = UIColor.purple
    }
    
    required init?(coder aDecoder: NSCoder) {
        print("view init coder")
        fatalError("init(coder:) has not been implemented")
    }
    
    override class var layerClass: AnyClass {
        get {
            print("view get layerClass invoke")
            return WYECustomLayer.self
        }
    }
    
    override var frame: CGRect {
        didSet {
            print("view set frame invoke")
            super.frame = frame
        }
    }
    
    override var center: CGPoint {
        didSet {
            print("view set center invoke")
            super.center = center
        }
    }
    
    override var bounds: CGRect {
        didSet {
            print("view set bounds invoke")
            super.bounds = bounds
        }
    }
}
override func draw(_ rect: CGRect) {
        print("view draw")
        super.draw(rect)
    }
//WYECustomLayer.swift
class WYECustomLayer: CALayer {
    
    override init() {
        print("layer init")
        super.init()
    }
    
    required init?(coder aDecoder: NSCoder) {
        print("layer init coder")
        fatalError("init(coder:) has not been implemented")
    }
    
    override  var frame: CGRect {
        didSet {
            print("layer set frame invoke")
            super.frame = frame
        }
    }
    
    override var position: CGPoint {
        didSet {
            print("layer set position invoke")
            super.position = position
        }
    }
    
    override var bounds: CGRect {
        didSet {
            print("layer set bounds invoke")
            super.bounds = bounds
        }
    }
}
override func display() {
        print("layer display")
        super.display()
    }
//testVC
var view = WYECustomView(frame: CGRect(x: 20, y: 600, width: 100, height: 50))
self.view.addSubview(wyeView)

方法调用顺序:
view init
view get layerClass invoke
layer init
layer set bounds invoke
layer set position invoke
layer set bounds invoke
layer set frame invoke
layer set position invoke
layer set bounds invoke
view set frame invoke
layer set position invoke
layer set bounds invoke
layer set frame invoke
layer set position invoke
layer set bounds invoke

从打印结果分析看出,view的set frame、bounds等方法均未调用,而都是调用的layer的相关方法。

1.3 UIView侧重内容管理,CALayer侧重内容绘制

在layer的display()方法里打上断点,查看调用栈如下:


image.png

UIView是CALayer的CALayerDelegate,代理方法内部[UIView drawLayer:inContext]调用UIView的drawRect方法,从而绘制了UIView的内容。
总结来说:在 View显示的时候,UIView 做为 Layer 的 CALayerDelegate,View 的显示内容由内部的 CALayer 的 display。

1.4 在做 iOS 动画的时候,修改非 RootLayer的属性(譬如位置、背景色等)会默认产生隐式动画,而修改UIView则不会

对于每一个 UIView 都有一个 layer,把这个 layer 且称作RootLayer,而不是 View 的根 Layer的叫做 非 RootLayer。我们对UIView的属性修改时时不会产生默认动画,而对单独 layer属性直接修改会,这个默认动画的时间缺省值是0.25s.

在 Core Animation 编程指南的 “How to Animate Layer-Backed Views” 中,对为什么会这样做出了一个解释:UIView 默认情况下禁止了 layer 动画,但是在 animation block 中又重新启用了它们。

是因为任何可动画的 layer 属性改变时,layer 都会寻找并运行合适的 'action' 来实行这个改变。在 Core Animation 的专业术语中就把这样的动画统称为动作 (action,或者 CAAction)。

layer 通过向它的 delegate 发送 actionForLayer:forKey: 消息来询问提供一个对应属性变化的 action。delegate 可以通过返回以下三者之一来进行响应:

  1. 它可以返回一个动作对象,这种情况下 layer 将使用这个动作。
  2. 它可以返回一个 nil, 这样 layer 就会到其他地方继续寻找。
  3. 它可以返回一个 NSNull 对象,告诉 layer 这里不需要执行一个动作,搜索也会就此停止。

当 layer 在背后支持一个 view 的时候,view 就是它的 delegate。

总结来说:CALayer 是默认修改属性支持隐式动画的,在给 UIView 的 Layer 做动画的时候,View 作为 Layer 的代理,Layer 通过 actionForLayer:forKey:向 View请求相应的 action(动画行为)

2 UIView

layout机制相关方法

//传入的参数是receiver当前的size,返回一个适合的size
- (CGSize)sizeThatFits:(CGSize)size;
//可以被手动直接调用 ,不应该在子类中被重写,会自动调用sizeThatFits方法,应该重写sizeThatFits
- (CGSize)sizeToFit;
//注意:sizeToFit和sizeThatFits方法都没有递归,对subviews也不负责,只负责自己

//重写此方法,对receiver执行重绘任务,能获得context
- (void)drawRect:(CGRect)rect;
//标记为需要重绘,异步调用drawRect,即在receiver标上一个需要被重新绘图的标记,在下一个draw周期自动重绘,iphone device的刷新频率是60hz,也就是1/60秒后重绘
- (void)setNeedsDisplay;
//标记为需要局部重绘
- (void)setNeedsDisplayInRect:(CGRect)invalidRect;

//对subviews重新布局,这个方法,默认没有做任何事情,需要子类进行重写。该方法调用先于drawRect
- (void)layoutSubViews;
//UIKit会判断该receiver是否需要layout.根据Apple官方文档,layoutIfNeeded遍历的不是superview链,应该是subviews链
- (void)layoutIfNeeded;
//在receiver标上一个需要被重新布局的标记,在系统runloop的下一个周期自动调用layoutSubviews
- (void)setNeedsLayout;
//注意:如果要立即刷新,要先调用[view setNeedsLayout],把标记设为需要布局,然后马上调用[view layoutIfNeeded],实现布局

drawRect方法调用机制:

  1. drawRect一般调用时在Controller->loadView,Controller->viewDidLoad两个方法之后调用的。(这是系统自动的调用机制)
  2. 如果在UIView初始化时没有设置rect大小,将直接导致drawRect不被自动调用。
  3. 该方法在调用sizeThatFits后被调用,所以可以先调用sizeToFit计算出size。然后系统自动调用drawRect:方法。
  4. 通过设置contentMode属性值为UIViewContentModeRedraw。那么将在每次设置或更改frame的时候自动调用drawRect:。
  5. 直接调用setNeedsDisplay,或者setNeedsDisplayInRect:触发drawRect:,但是有个前提条件是rect不能为0. 以上1,2推荐;而3,4不提倡

layoutSubViews调用机制:

  1. init初始化不会触发layoutSubviews,但是用initWithFrame进行初始化时,当rect的值不为CGRectZero时,就会触发layoutSubviews方法;
  2. 当调用addSubview会触发layoutSubviews方法;
  3. 设置view的frame值,且设置前后frame的值发生了变化;
  4. 滚动UIScrollView时,会触发layoutSubviews方法;
  5. 旋转Screen会触发父UIView上的layoutSubviews方法;
  6. 改变UIView大小的时候,也会触发layoutSubviews方法;

【注意】

  1. setNeedsLayout方法: 标记为需要重新布局,异步调用layoutIfNeeded刷新布局,不立即刷新,但layoutSubviews一定会被调用
  2. layoutIfNeeded方法:如果有需要刷新的标记,立即调用layoutSubviews进行布局(如果没有标记,不会调用layoutSubviews)

2 CALayer

2.1 CALayer绘制机制

CALayer以及CALayerDelegate相关重要API:

//CALayer
//这一组api与UIView的类似
- (void)display;
- (void)setNeedsDisplay;
- (void)setNeedsDisplayInRect:(CGRect)r;
- (void)displayIfNeeded;
- (void)drawInContext:(CGContextRef)ctx;

//CALayerDelegate <NSObject>
- (void)displayLayer:(CALayer *)layer;
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;

CALayer机制有两种绘制方法:
【方法一】:直接分配

UIImage *image = ...;
CALayer *layer = ...;
layer.contents = (id)image.CGImage;

【方法二】:core Animation会按照以下顺序通过CALayer和委托方法来创建它。
【1】 [CALayer setNeedsDisplay] : 代码调用setNeedsDisplay将图层标记为需要重绘的,要求通过以下步骤来更新contents。除非调用了setNeedsDisplay方法,否则contents属性永远不会更新。
【2】 [CALayer displayIfNeeded] :绘制系统会在需要时自动调用,如果图层通过被调用setNeedsDisplay标记为需要重绘的,绘制系统就会接着执行后续步骤。
【3】 [CALayer display]: displayIfNeeded方法会在合适的时候调用它,该方法由系统调用而不能手动调用。如果实现了委托方法,默认实现会调用displayeLayer:委托方法。否则,display方法会调用drawInContext:方法。可以在子类中覆盖display方法以直接设置contents属性。
【4】[delegate displayLayer: ] :默认的[CALayer display]会在方法实现时调用它。它的任务是设置contents。如果实现了这个方法(即使没有什么操作),后面就不会运行自定义的绘制代码。
【5】[CALayer drawInContext:] :默认的display方法会创建一个视图图形上下文并将其传给drawInContext:方法。它与UIView drawRect:方法相似,但不会自动设置UIKit上下文。为了使用UIKit来绘图,你需要调用UIGraphicsPushContext()方法指定接收到的上下文为当前上下文。否则,他只会使用Core Graphics在接收到的上下文中绘图。默认的display 方法获取最终的上下文,创建一个CGImage并将其分配给contents。默认的CALayer drawInContext:] 会在方法已实现时调用【delegate drawLayer: inContext】。否则就不执行任何操作。可以手动调用这个方法。
【6】[delegate drawLayer: inContext:]: 如果实现了这个方法,默认的drawInContext:会调用这个方法实现更新上下文,从而是display方法可以创建CGImage。

【总结】:

  1. 为什么系统要有基本稳定的刷新频率?如果每个layer更新都要系统刷新一遍,那么会导致紊乱的帧率,有时特别卡有时又很闲。因此在layer内容改变的时候,把这个layer做个需要刷新的标记,这就是setNeedsDisplay,每次刷新时,把上次刷新之后被标记的layer一次性全部提交给图形系统,所以这里还有一个东西,就是事务(CATransaction)。

  2. layer刷新就是被调用display,但这个我们不主动调用,让系统调用,它可以把我更好的时机。我们只需要setNeedsDisplay做标记。如果你真的非常急需,就用displayIfNeeded,对于已被标记为Needed的layer就立马刷新。

  3. 内容提供方法,以下四个方法,只要有一个已经实现了就不会继续往下执行,因为认为你已经提供了内容。

    • 前面两个方法没有构建内容缓冲区(Backing Store),需要直接提供contents,一种方法就是直接赋值一个CAImageRef;
    • 后两种方法,会给layer开辟一块内存用来存储绘制的内容,在这两个方法里,可以使用CoreGraphics的那套api来绘制需要的内容。
layer.contents = [UIImage imageNamed:@"xxx"].CGImage;
//CALayer
- (void)display;
//CALayerDelegate <NSObject>
- (void)displayLayer:(CALayer *)layer;

//CALayer
- (void)drawInContext:(CGContextRef)ctx;
//CALayerDelegate <NSObject>
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;

2.2 delegate的作用

从上面还可以搞清楚一个问题,就是layer的delegate的作用。delegate控制layer的内容,这也是为什么UIView自带的layer的delegate是默认指定到view自身的,而也因为这样,绝大多数时候我们直接修改view的属性(颜色位置透明度等等),layer的呈现就自动发生变化了。

2.3 CALayer和动画的关系

在使用CoreAnimation的动画的时候,是把创建的动画放到layer上,而简单的使用动画,很多时候是使用[UIView animation...],那么后者其实本质是内部建了一个动画放到了layer上吗?是的,动画的载体就是layer,这就是它们的基本关系。但为了更高效的动画,还有更多的细节。

如果你做过位移的动画,并且试着在动画的过程里去输出view的位置,你会惊讶的发现:在动画开始后,view的frame就已经是结束位置的值了!

看段文档:

Instead, a layer captures the content your app provides and caches it in a bitmap, which is sometimes referred to as the backing store. ... When a change triggers an animation, Core Animation passes the layer’s bitmap and state information to the graphics hardware, which does the work of rendering the bitmap using the new information. Manipulating the bitmap in hardware yields much faster animations than could be done in software.

这段话的含义是:layer的内容生成一个位图(bitmap),触发动画的时候,是把这个动画和状态信息传递给图形硬件,图形硬件使用这两个数据就可以构造动画了。处理位图对于图形硬件更快。

模拟一下动画处理过程就是:一个很复杂的view的动画,是把它的layer的内容合成一张图片,然后要旋转,就是把这张图旋转一下显示出来。实际上图形系统在渲染的过程里,对于旋转、缩放、位移等,只需要加一个矩阵就可以了(对应就是transform),对于图形系统而言这些工作就是最基本的操作,非常高效。
所以动画的呈现和view本身的的数据时分离的,也就出现了动画时看到的都是结束时的数据。

如果按照常识理解去实现动画,是怎么做?
view移动,在界面刷新的方法里,不断的更新view的位置,每次更新完,把数据提供给图形系统,重新绘制。对于有复杂子视图的view,要把整个子视图树都全部重绘。
对比两者,基于layer的欺骗性的动画节省了什么?

  • 不用不断的更新view的数据
  • 不用不断的和图形硬件交互数据
  • 对于复杂的view,不用重绘整个图层树
  • 处理这些对图形硬件更擅长

能这么做的本质原因我觉得还是因为我们需要的动画是程式化的,有模板、有套路的。哪怕是稍微复杂的动画,也可以用关键帧动画来简化,最后还是变成一个个离散独立的数据,按照既定的路线去呈现。如果动画是即时计算出来的,就没法这么干了,比如一个球扔到地上后怎么弹,是根据球的材料重量大小地面坡度等来计算的。

2.4 三层layer

上面的动画系统,也就催生了layer3种不同的图层树:
模型树(model layer tree):存储了动画的结束值,最终展示在界面上的就是modelLayer
表现树(presentation tree):包含了动画正在进行中的值,这是一个中间层,系统正在这一层上更改属性,进行各种渲染操作。
渲染层(render tree):用来表现实际动画的数据,文档无更多说明,应该是跟图形系统相关的数据,比如提供给GPU的bitmap等。

如果要拿到动画过程中view的数据,可以通过presentation tree来获取。
我们修改动画的属性,在动画的其实是 Layer 的 presentLayer的属性值,而最终展示在界面上的其实是提供 View的modelLayer

3 参考文章

详解 CALayer 和 UIView 的区别和联系
【重读iOS】认识CALayer

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