IOS知识积累动画图像

UIView之drawRect: & layoutSub

2017-12-24  本文已影响453人  好雨知时节浩宇

重绘机制

iOS的绘图操作是在UIView的drawRect中完成的,我们想要在UIView中完成绘图(或者自定义控件),需要在UIView的拓展类(或者子类)中重写drawRect函数,在这里进行绘图的操作,系统会自动调用该函数进行绘图。
重绘也是在drawRect:中完成的,但是Apple并不建议我们直接调用drawRect:方法,如果直接调用没有效果,Apple建议我们调用setNeedDiplay方法,调用该方法后,系统会自动调用drawRect:方法。
我们重写drawRect:方法可以画自定义的图案,或者我们需要自定义View控件时也需要重写该方法,通常该函数只会调用一次,当需要手动触发是,只需要调用setNeedDiplay方法即可。

不知道大家是否有想过下面的问题:为什么苹果会提供drawRect机制,为什么不建议直接调用drawRect函数,而是建议我们调用setNeedDisplay ?
这里允许我通俗的描述下:我们可以认为,在在创建视图时,设置frame等参数后,可以理解成只有一个点,然后晚些系统查看所有需要绘制的东西,并按顺序排列,因为有些内容是重叠的,最后高效的将视图绘制出来。这样系统根据层的情况优化性能。
另外:再说一下setNeedDisplay函数,加入有A、B两个VC,如果我们在当前显示的VC A中调用[B.view drawRect]函数,这时B回去绘制页面,但是B并未显示在window上,这就造成了一种资源的浪费。所以Apple建议我们调用setNeedDisplay,这样当B展示在Window上时再去绘制渲染视图,充分减少资源浪费。

视图绘制相关方法

①、- (void)drawRect:(CGRect)rect;
重写此方法,执行重绘任务
②、- (void)setNeedsDisplay;
将视图标记为需要重绘,异步调用drawRect
③、- (void)setNeedsDisplayInRect:(CGRect)rect;
将视图标记为需要局部重绘

drawRect调用机制

1、调用时机:loadView ->ViewDidload ->drawRect:
2、如果在UIView初始化时没有设置rect大小,将直接导致drawRect:不被自动调用。
3、通过设置contentMode属性值为UIViewContentModeRedraw。那么将在每次设置或更改frame的时候自动调用drawRect:
4、直接调用setNeedsDisplay,或者setNeedsDisplayInRect:触发drawRect:,但是有个前提条件是:view当前的rect不能为nil

5、该方法在调用sizeThatFits后被调用,所以可以先调用sizeToFit计算出size。然后系统自动调用drawRect:方法。
这里简单说一下sizeToFit和sizeThatFit:
sizeToFit:会计算出最优的 size 而且会改变自己的size
sizeThatFits:会计算出最优的 size 但是不会改变 自己的 size

注意事项:

1、若使用UIView绘图,只能在drawRect:方法中获取相应的contextRef并绘图。如果在其他方法中获取到一个invalidate的ref保存下来,在drawRect中并不能用于画图。等到在这里调用时,可能当前上下文环境已经变化。
2、若使用CALayer绘图,只能在drawInContext: 中(类似于drawRect)绘制,或者在delegate中的相应方法绘制。同样也是调用setNeedDisplay等间接调用以上方法。
3、若要实时画图,不能使用gestureRecognizer,只能使用touchbegan等方法来掉用setNeedsDisplay实时刷新屏幕。
4、UIImageView继承自UIView,但是UIImageView能不重写drawRect方法用于实现自定义绘图。具体原因如下:
Apple在文档中指出:UIImageView是专门为显示图片做的控件,用了最优显示技术,是不让调用darwrect方法, 要调用这个方法,只能从uiview里重写。

layoutSubviews

这个方法是用来对subviews重新布局,默认没有做任何事情,需要子类进行重写。
当我们在某个类的内部调整子视图位置时,需要调用。
反过来的意思就是说:如果你想要在外部设置subviews的位置,就不要重写。

视图布局相关方法:

①、- (void)layoutSubviews;
对subview重新布局
②、- (void)setNeedsLayout;
将视图标记为需要重新布局, 这个方法会在系统runloop的下一个周期自动调用layoutSubviews。
③、- (void)layoutIfNeeded;
如果有需要刷新的标记立即调用layoutSubviews进行布局(如果没有标记,不会调用layoutSubviews)这里注意一个点:标记,没有标记,即使我们掉了该函数也不起作用
如果要立即刷新,要先调用[view setNeedsLayout],把标记设为需要布局,然后马上调用[view layoutIfNeeded],实现布局.
在视图第一次显示之前,标记总是“需要刷新”的,可以直接调用[view layoutIfNeeded]

这里有必要描述下三者之间的关系:
在没有外界干预的情况下,一个view的frame或者bounds发生变化时,系统会先去标记flag这个view,等下一次渲染时机到来时(也就是runloop的下一次循环),会去按照最新的布局去重新布局视图。
setNeedLayout就是给这个view添加一个标记,告诉系统下一次渲染时机需要重新布局这个视图。
layoutIfNeed就是告诉系统,如果已经设置了flag,那不用等待下个渲染时机到来,立即重新渲染。前提是设置了flag。
layoutSubviews则是由系统去调用,不需要我们主动调用,我们只需要调用layoutIfNeed,告诉系统是否立即执行重新布局的操作。

layoutSubviews调用时机

结论是经过搜索得到的,基于此笔者进行了验证,并得到了些结果:
1、init初始化不会触发layoutSubviews。
2、addSubview会触发layoutSubviews。(当然这里frame为0,是不会调用的,同上面的drawrect:一样)
3、设置view的Frame会触发layoutSubviews,(当然前提是frame的值设置前后发生了变化。)
4、滚动一个UIScrollView会触发layoutSubviews。
5、旋转屏幕会触发父UIView上的layoutSubviews事件。(这个我们开发中会经常遇到,比如屏幕旋转时,为了界面美观我们需要修改子view的frame,那就会在layoutSubview中做相应的操作)
6、改变一个UIView大小的时候也会触发父UIView上的layoutSubviews事件。
7、直接调用setLayoutSubviews。(Apple是不建议这么做的)

这里需要补充一点:layoutSubview是布局相关,而drawRect则是负责绘制。因此从调用时序上来讲,layoutSubviews要早于drawRect:函数。

关于LayoutSubView我们再来看一个例子:
1、另同时用上一套的场景举个例,当想知道tableView reloadData后的contentSize的话可以在reloadData后用这两个方法,然后就可以直接提取contentSize了。
2、demo完善中,稍后奉上

渲染的时机

了解了drawRect:和layoutSubviews:的原理后,我们是否会想跟进一步的去了解:我在使用setNeedDisplay和setNeedLayout分别标记了需要重绘和需要重新布局后,那到底什么时间去执行的渲染操作呢?我们接下里详细拆分讲解

iOS显示系统:

1、如何让App渲染的代码定时执行(例如:每秒执行60次)?
iOS 的显示系统是由 VSync 信号驱动的,VSync 信号由硬件时钟生成,每秒钟发出 60 次(这个值取决设备硬件,比如 iPhone 真机上通常是 59.97)。iOS 图形服务接收到 VSync 信号后,会通过 IPC 通知到 App 内。App 的 Runloop 在启动后会注册基于端口的源也就是source1,Vsync信号则通过 mach_port 端口传递过来,同时唤醒runloop,随后 Source1 的回调会驱动整个 App 的动画与显示。
tips:图形服务同APP Process是两个进程,他们之间通信的方式是IPC,了解WKWebview实现机制的同学会发现,WebContent process 同App process进行通信的方式也是通过IPC来实现的。有兴趣的同学可以参考我的另一篇博客:关于wkwebview讲解。
2、通过mach_port端口发送消息,唤醒Runloop后,做了一些修改view和layer的工作,并提交到全局容器,等待渲染时机到来。
Core Animation 在 RunLoop 中注册了一个 Observer,监听了 BeforeWaiting 和 Exit 事件。当一个触摸事件到来时(也可以理解成Vsync信号唤起),RunLoop 被唤醒,App 中的代码会执行一些操作,比如创建和调整视图层级、设置 UIView 的 frame、修改 CALayer 的透明度、为视图添加一个动画;这些操作最终都会被 CALayer 标记,并通过 CATransaction 提交到一个中间状态去。当上面所有操作结束后,RunLoop 即将进入休眠(或者退出)时,关注该事件的 Observer 都会得到通知。这时 Core Animation 注册的那个 Observer 就会在回调中,把所有的中间状态合并提交到 GPU 去显示;
如果此处有动画,通过 DisplayLink 稳定的刷新机制会不断的唤醒runloop,使得不断的有机会触发observer回调,从而根据时间来不断更新这个动画的属性值并 绘制出来。
注:动画由CADisplayLink来不断唤醒runloop。
3、具体逻辑图:(来源于网络)

iOS显示系统.png

渲染时机

1、Core Animation 在 RunLoop 中注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件 。
2、当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。当Oberver监听的事件到来时,回调执行函数中会遍历所有待处理的UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。
3、回调函数内部调用栈大致如下:

_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
   QuartzCore:CA::Transaction::observer_callback:
       CA::Transaction::commit();
           CA::Context::commit_transaction();
               CA::Layer::layout_and_display_if_needed();
                   CA::Layer::layout_if_needed();
                         [CALayer layoutSublayers];
                         [UIView layoutSubviews];
                   CA::Layer::display_if_needed();
                         [CALayer display];
                         [UIView drawRect];

简单解释下:

1、首先是通过CATransaction提交到全局的容器中
2、检查是否有标记为需要重新绘制和布局的Layer
3、如果有则执行layout和redraw操作。
另外从这上面我们也可以看到:一定是先有布局,再去绘制图形。即:layout调用一定是在drawRect:之前。
上一篇下一篇

猜你喜欢

热点阅读