iOS 札记2:Core Graphics小记

导语:Core Graphics是iOS和Mac OS X的2D绘图引擎,本文简单介绍Core Graphics绘图相关概念

一、概述

1、iOS的绘图系统

iOS主要的绘图系统有UIKit、Core Graphics、Core Animation、Core Image、OpenGL ES.结构图如下:

iOS的绘图系统结构.png
iOS的绘图系统 主要作用 API缩写前缀
UIKit 使用频率最高,高级别的OC图形接口,它能够访问绘图、动画、字体、图片等内容。 缩写前缀为UI
Core Animation 使用频率较高,提供了强大的2D和3D动画 缩写前缀为CA
Core Graphics 使用频率中,基于C实现的iOS和Mac OS X的2D绘图引擎, 缩写前缀为CG
Core Image 使用频率低,图片的滤镜处理,比如高斯模糊、锐化等 缩写前缀为CL
OpenGL ES 使用频率低,OpenGL针对嵌入式设备的简化版本,用以绘制高性能的2D和3D图形 缩写前缀为GL

说明:图像的滤镜效果建议使用优秀的第三方库 GPUImage,iOS支持两套图形API族:Core Graphics 和OpenGL ES,它们都是基于C的API框架。

2、视图绘制 VS 视图布局
  • 视图绘制:调用UIView中的drawRect方法。如果一个视图调用setNeedsDisplay(或setNeedsDisplayInRect:)方法,它就被标记为重新绘制,并且会在下一次绘图周期中调用drawRect方法重新绘制。

  • 调用setNeedsDisplay或者setNeedsDisplayInRect:方法,只是告诉系统该视图需要重新绘制了,真正的绘制发生在下一个绘制周期,所以可以一次性对多个视图调用setNeedsDisplay/setNeedsDisplayInRect,等到它们在下一个周期被调用drawRect方法更新视图。

  • 即时不使用setNeedsDisplay/setNeedsDisplayInRect:方法,如果当遮挡你的视图的其他视图被移动或删除操作的时,设置视图的hidden属性,改变视图的显示状态,视图滚出屏幕,然后再重新回到屏幕上。等等都会触发视图重新绘制。

  • 视图布局:调用UIView中的layoutSubviews方法。如果视图中的子视图布局发生变化,需要重新排列,UIKit会自动调用setNeedsLayout方法,也就是对于发生变化的视图逐层次调用layoutSubviews方法。比如frame发生变化、滚动视图等。

    说明:因为绘制的工作大部分是使用CPU完成的,处理速度不如GPU, 尽量避免绘制,多使用布局。视图在第一次创建的时候,绘制和布局的都会调用的。如果子视图因为一些条件的改变,造成布局的改变,这个时候,系统会自动调用layoutSubviews方法。

3、UIKit相关绘图API
  • 填充、 描边和路径

    UIRectFill(CGRect rect);   //填充矩形函数
    UIRectFrame(CGRect rect);  //矩形描边函数
    UIBezierPath //绘制常见路径类,但是对于线段、渐变、阴影、反锯齿等高级特性支持还需要使用Core Graphics
    
  • UIImage类中绘制图像主要的方法

    - (void) drawAtPoint:(CGPoint)point;   //设置绘制定点。
    - (void) drawInRect:(CGRect)rect;      //图片绘制在指定的矩形里。
    - (void) drawAsPatternInRect:(CGRect)rect;   //在指定的矩形里平铺绘制图片,如果图片大小超出了指定的矩形,形式上与-drawAtPoint:方法
                                                                            // 类似,如果图片大小小于指定的矩形,就会有平铺的效果。
    
  • NSString类中绘制文本主要的方法:

    - (void) drawAtPoint:(CGPoint)point withAttributes:(NSDictionary *)attrs;文本在指定点绘制;
    - (void) drawInRect:(CGRect)rect withAttributes:(NSDictionary *)attrs;文本在指定的矩形里绘制;
    

二、Core Graphics绘图

Core Graphics中的绘图操作都是在在一个上下文中进行,在绘图之前必须获取该上下文,这是绘图任务的第一步。

1、概述
  • Core Graphics的图形上下文是CGContextRef对象,相当于一块画布,以堆栈形式存放,只有在栈顶的图形上下文上绘制才有效;图形上下文负责存储绘画状态(如画笔颜色、线条粗细等)和绘制内容所处的内存空间。获取上下文的方式有:

  • iOS有PDF图形上下文、图片处理的图形上下文等,而通过UIGraphicsBeginImageContextWithOptions函数或在UIView的drawRect:方法中,使用 UIGraphicsGetCurrentContext函数获取的是,当前图片处理的图形上下文。

  • Core Graphics中带有 Ref 后缀的类,其 实例对象 可能有指向其他 Core Graphics “对象”的强引用指针,但是不能被ARC管理,所以创建了这些对象,使用完之后记得手动释放,否则会有内存泄漏的问题。

  • 内存管理规则:凡使用名称中带有create 或者 copy 的函数创建了一个 Core Graphics “对象”,就必须调用对应的Release函数并传入该对象的指针,将其释放。

2、UIKit封装的图形上下文操作函数
1)UIGraphicsBeginImageContextWithOptions(CGSize size, BOOL opaque, CGFloat scale)
  • 功能:将旧的上下文入栈、为新上下文分配内存、创建新的上下文、翻转坐标系统,并将其设置为当前上下文。(简记:创建了一个图形处理的上下文,并将其设置为当前上下文)

  • 参数说明:size是新创建的位图的尺寸。opaque代表透明通道的开关,指定所生成图片的背景是否为不透明,YES表示不透明,图片背景是黑色,NO表示透明;scale表明缩放比例,传入0表示让图片的缩放因子等于屏幕的分辨率。

2)UIGraphicsBeginImageContext(CGSize size)
  • 创建一个基于位图的上下文,并将其设置为当前上下文。效果相当于UIGraphicsBeginImageContextWithOptions(size,NO,1.0)
3)UIGraphicsGetImageFromCurrentImageContext(void)
  • 从当前上下文中获取一个UIImage对象,一般用于获取最终绘制结果。
4)UIGraphicsEndImageContext(void)
  • 关闭图形上下文,一般在调用UIGraphicsGetImageFromCurrentImageContext之后调用。
5)UIGraphicsGetCurrentContext(void)
  • 获得当前的图形上下文,可以在UIGraphicsBeginImageContextWithOptions/UIGraphicsBeginImageContext或在UIView的drawRect:使用,以获取当前的图形上下文。
3、绘图状态切换 和 上下文切换

绘图中,有绘图状态切换上下文切换,对应两组不同函数,要注意区分使用。

1) 绘图状态切换
  • 在创建Core Graphics图形上下文时,图形上下文中持有一个绘图状态堆栈,这时的绘图状态堆栈是空的,通过CGContextSaveGStateCGContextRestoreGState可以推入和弹出绘图状态,实现了绘图状态切换,但是没有改变其图形上下文。

  • CGContextSaveGState当前图形上下文的之前绘图状态(Graphics State )压入绘图状态堆栈,而CGContextRestoreGState是将绘图状态堆栈顶部的状态弹出,返回到之前的图形状态。

  • CGContextSaveGState之前,绘图状态是A,在CGContextSaveGStateCGContextRestoreGState之间,可以改变绘制状态成为B,实现不同的效果,调用了CGContextRestoreGState之后,绘图状态又回到了A。

    //绘图状态A
    CGContextSaveGState(context)
    //绘图状态B
    CGContextRestoreGState(context)
    //绘图状态A
    
  • 这种推入和弹出的方式是回到之前绘图状态的快速方法,避免逐个撤消所有的状态修改;这也是将某些状态(比如裁剪路径)恢复到原有设置的唯一方式。

2) 上下文切换
  • 通过UIGraphicsPushContext与UIGraphicsPopContext函数可以实现图形上下文切换

  • UIGraphicsPushContext将图形上下文压入图形上下文堆栈;当需要新建一个上下文做一些绘制工作的话,使用UIGraphicsPushContext保存当前的上下文,在新的绘制图形上下文完成绘制工作后,使用UIGraphicsPopContext恢复会之前的图形上下文。

  • UIGraphicsPushContext之前,在绘制图形X,在UIGraphicsPushContextUIGraphicsPopContext之间绘制图形Y,在UIGraphicsPopContext之后,又回到了绘制图像X。

     //绘制图形X
     UIGraphicsPushContext(context)
     //绘制图形Y
     UIGraphicsPopContext(context)
     //绘制图形X
    

    说明:这种推入和弹出的方式是切换上下文比较好的方式。

三、Core Graphics在项目中的使用场景

在项目中,为了满足设计需要,利用Core Graphics完成一些绘制工作,如绘制圆角(Round Corner)、高斯模糊(Gaussian Blur) 和 阴影(Shadow)。这些绘制工作要考虑性能和内存消耗,尽可能将绘制内容缓存下来,避免重复绘制。

1、圆角(Round Corner)
  • 在硬件产品和 软件用户界面的设计上,圆角被大量采用;一是因为圆角在视觉过程中更易被认知,二是因为圆角也同时使得信息更易被处理,总之,大家认为“圆角看起来很漂亮”。

  • 在项目中常见圆角图片。虽然iOS 9之后,对使用加载png图片做了优化,不会触发离屏渲染 ,但网络图片未必都是png格式,使用Core Graphics来处理圆角,是个不错的选择。

  • 关于圆角处理,参考QSImageProcess项目,具体介绍可见iOS实录17:网络图片的优化显示

2、高斯模糊(Gaussian Blur)
  • 模糊(Blur)可以理解成每一个像素都取周边像素的平均值;如果简单地平均,显然不很合理,因为图像都是连续的,越靠近的点关系越密切,越远离的点关系越疏远。理想的想法是,利用加权平均,距离越近的点权重越大,距离越远的点权重越小。

  • 高斯模糊算法其实是利用高斯分布(正态分布)来处理周边像素权重的问题;高斯分布是一种钟形曲线,越接近中心,取值越大,越远离中心,取值越小。在计算平均值的时候,我们只需要将"中心点"作为原点,其他点按照其在正态曲线上的位置,分配权重,就可以得到一个加权平均值。

  • 模糊半径越大,图像就越模糊。从数值角度看,就是数值越平滑。

  • 虽然利用UIVisualEffectView和UIToolBar可以实现模糊效果,但是不够灵活。如果设计需要更加复杂的效果,使用Core Graphics处理也是不错的选择。

3、阴影(Shadow)
  • 大多数阴影效果设置,利用shadow****属性都可以完成。除非设计有特别的阴影需求,才使用Core Graphics(谨记)

  • 网上很多博客中说:通过shadow****设置阴影,会触发离屏渲染,这句话并不严谨。只有在不设置shadowPath(阴影路径)的情况下,设置阴影效果(如shadowColor、shadowOpacity、shadowOffset和shadowRadius等),才会触发。如下代码是不会触发离屏渲染的,

    imageView.layer.shadowOpacity = 0.8;  //阴影透明度
    imageView.layer.shadowOffset = CGSizeMake(2, 2);  ;// 阴影偏移量
    imageView.layer.shadowRadius = 10; //阴影的圆角
    imageView.layer.shadowColor = [UIColor grayColor].CGColor;  // 阴影的颜色
    UIBezierPath *path = [UIBezierPath bezierPathWithRect:imageView.bounds];
    imageView.layer.shadowPath = path.CGPath;      //阴影的路径(避开离屏渲染的关键)
    

    说明1:设置了shadowPath属性,图层在创建阴影的时候,不需要遍历sublayer的alpha通道,而是直接使用指定的路径作为阴影的轮廓,不发生离屏渲染。反之,CoreAnimation需要在每一帧绘制阴影的时候,遍历所有sublayer的alpha通道,以精确的计算出阴影的轮廓,发生离屏渲染,消耗了性能。

    说明2:设置shadowPath属性后,当图层的bounds产生变化后,需要更新shadowPath才能让其适配新的bounds。

End

推荐阅读更多精彩内容