关于drawRect

由于�一直没有好好学习UIView的绘制流程,关于UIView的drawRect一直以来都有两个疑问:
1 为什么只在drawRect方法里才能获取当前图层的上下文
2 drawRect不是号称自定义实现UIView吗,为什么我重写了drawRect原先设置的背景颜色和frame等等都没变,不是应该是我在drawRect写了什么就只显示什么吗?如:
代码:

// ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    self.roosterView = [[RoosterView alloc] initWithFrame:self.view.bounds];
    self.roosterView.backgroundColor = [UIColor whiteColor];
    [self.view addSubview:self.roosterView];
}

// RoosterView
- (void)drawRect:(CGRect)rect {
    CGContextRef context = UIGraphicsGetCurrentContext();
    UIImage *myImage = [UIImage imageNamed:@"rooster"];
    CGRect myRect = CGRectMake(0, 0, myImage.size.width, myImage.size.height);
    CGContextScaleCTM(context, 1.0, -1.0);
    CGContextTranslateCTM(context, 0, -(myRect.size.height-(myRect.size.height-2*myRect.origin.y-myRect.size.height)));//向上平移
    CGContextTranslateCTM (context, myRect.size.width/4, 0);
    CGContextScaleCTM (context, .25,  .5);
    CGContextRotateCTM (context, radians ( 22.));
    CGContextDrawImage(context, myRect, myImage.CGImage);
}
5F466471-C727-4C59-9DD5-18937678E2F7.png

不是只应该显示形变之后的图片吗?!为什么还是占满整个屏幕白色背景还在?不是说重写drawRect对UIView进行自定义嘛!!!

这里需要了解:真正被显示的是layer,每一个在 UIKit 中的 view 都有它自己的 CALayer。每一个layer都有个content,这个content指向的是一块缓存,叫做backing store(后备存储),backing store有点像一个图像。这个后备存储正是被渲染到显示器上的。
绘图流程大概是:

  • 每一个UIView都有一个layer,每一个layer都有个content,这个content指向的是一块缓存,叫做backing store。
  • UIView的绘制和渲染是两个过程,当UIView被绘制时,CPU执行drawRect,通过context将数据写入backing store。
  • 当backing store写完后,通过render server交给GPU去渲染,将backing store中的bitmap数据显示在屏幕上

CALayer被绘制时方法调用栈:


drawRect的调用栈.png

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

接着:当渲染系统准备好,它会调用视图图层的-display方法.此时,图层会装配它的后备存储。然后建立一个 Core Graphics 上下文(CGContextRef),将后备存储对应内存中的数据恢复出来,绘图会进入对应的内存区域,并使用 CGContextRef 绘制。
上图监听事件到来后出发一系列事件一直到-[CALayer display],根据微软开源WinObjc,display的主要工作有:

- (void)display {
        .......
       //  判断contents是否有值
      if (priv->contents == NULL || priv->ownsContents || [self isKindOfClass:[CAShapeLayer class]]) {
     
        .......

        // 创建当前图层上下文
        CGContextRef drawContext = CreateLayerContentsBitmapContext32(width, height);

        priv->ownsContents = TRUE;
        CGImageRef target = CGBitmapContextGetImage(drawContext);

        CGContextRetain(drawContext);
        CGImageRetain(target);
        priv->savedContext = drawContext;

        ......
        // 设备坐标和UIKit坐标之间的转换
        CGContextScaleCTM(drawContext, 1.0f, -1.0f);
        CGContextTranslateCTM(drawContext, -priv->bounds.origin.x, -priv->bounds.origin.y);
       
        CGContextSetDirty(drawContext, false);
        [self drawInContext:drawContext];

        if (priv->delegate != 0) {
            if ([priv->delegate respondsToSelector:@selector(displayLayer:)]) {
                [priv->delegate displayLayer:self];
            } else {
                [priv->delegate drawLayer:self inContext:drawContext];
            }
        }

        CGContextReleaseLock(drawContext);
        CGContextRelease(drawContext);

        // If we've drawn anything, set it as our contents
        if (!CGContextIsDirty(drawContext)) {
            CGImageRelease(target);
            CGContextRelease(drawContext);
            priv->savedContext = NULL;
            priv->contents = NULL;
        } else {
            priv->contents = target;
        }
    } else if (priv->contents) {
        priv->contentsSize.width = float(priv->contents->Backing()->Width());
        priv->contentsSize.height = float(priv->contents->Backing()->Height());
    }      
}

从调用栈截图看出layer是在drawInContext:方法里调用了layer代理实现的
drawLayer:inContext:方法和以上代码关于drawInContext:和代理函数[priv->delegate displayLayer:self];和[priv->delegate drawLayer:self inContext:drawContext];的调用时机有出入(待详查)不过整体流程操作还是可以明白的。

代码先判断contents属性是否有值,如果没有就开始创建自己的图层关联上下文,从上下文创建CGImageRef,最后赋值给contents属性,这与文档关于contents属性的描述一致。
文档:

If you are using the layer to display a static image, you can set this property to the CGImageRef containing the image you want to display. (In macOS 10.6 and later, you can also set the property to an NSImage object.) Assigning a value to this property causes the layer to use your image rather than create a separate backing store.
If the layer object is tied to a view object, you should avoid setting the contents of this property directly. The interplay between views and layers usually results in the view replacing the contents of this property during a subsequent update.

那么这些和只在drawRect方法里才能获取当前图层的上下文有什么关系呢,依然看源码:

- (void)drawLayer:(CALayer*)layer inContext:(CGContextRef)context {
    UIGraphicsPushContext(context);

    CGRect bounds;
    bounds = CGContextGetClipBoundingBox(context);
    [self drawRect:bounds];

    UIGraphicsPopContext();
}

drawRect方法在drawLayer:inContext:里被调用,并且被调用前有个UIGraphicsPushContext(context);方法将视图图层对应上下文压入栈顶,然后drawRect执行完后,将视图图层对应上下文执行出栈操作。
系统会维护一个CGContextRef的栈,而UIGraphicsGetCurrentContext()会取栈顶的CGContextRef,当前视图图层的上下文的入栈和出栈操作恰好将drawRect的执行包裹在其中,所以说只在drawRect方法里才能获取当前图层的上下文。

第一个问题知道了答案,那么是时候总结下第二道个问题的答案了:对于view的frame,backgroundColor各种设置是通过view间接操作了layer,继而存储到backing store,view给暴露出drawRect接口只是一个询问补充的目的,layer自己会装配它的后备存储,生成了上下文,已经玩的红红火火了,为了表示对你的尊重,再问你一句:大爷还有要补充的吗?你重写了drawRect说有。大家都说drawRect自定义view说白了其实只是一个补充的作用。

把drawRect说的这么不堪,其实不是没有凭据的,因为苹果说:

这听起来貌似有点低俗,但是最快的绘制就是你不要做任何绘制。
大多数时间,你可以不要合成你在其他视图(图层)上定制的视图(图层),这正是我们推荐的,因为 UIKit 的视图类是非常优化的 (就是让我们不要闲着没事做,自己去合并视图或图层) 。

最后:图层的后备存储将会被不断的渲染到屏幕上。直到下次再次调用视图的 -setNeedsDisplay ,将会依次将图层的后备存储更新到视图上。

在调用中drawRect之前的都在cpu中执行,然后GPU将bitmap从RAM移动到VRAM将按像素计算将一层层图层合成成一张图然后显示:

屏幕快照 2016-11-28 下午7.46.21.png

// 以下待进一步验证:
drawRect调是在Controller->loadView, Controller->viewDidLoad 两方法之后掉用的.所以不用担心在控制器中,这些View的drawRect就开始画了.这样可以在控制器中设置一些值给View(如果这些View draw的时候需要用到某些变量值).
1.如果在UIView初始化时没有设置rect大小,将直接导致drawRect不被自动调用。
2.该方法在调用sizeThatFits后被调用,所以可以先调用sizeToFit计算出size。然后系统自动调用drawRect:方法。
3.通过设置contentMode属性值为UIViewContentModeRedraw。那么将在每次设置或更改frame的时候自动调用drawRect:。
4.直接调用setNeedsDisplay,或者setNeedsDisplayInRect:触发drawRect:,但是有个前提条件是rect不能为0.
以上1,2推荐;而3,4不提倡

参考:
ObjC中国
iOS开发之图形渲染分析、离屏渲染、当前屏幕渲染、On-Screen Rendering、Off-Screen Rendering
iOS开发笔记--iOS 事件处理机制与图像渲染过程

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

推荐阅读更多精彩内容