1. UIView和CALayer的区别
- CALayer无法响应用户事件
NSObject -> CALayer
NSObject -> UIResponder -> UIView -> UIWindow - 分工不同
UIView侧重于对显示内容的管理和整体布局
CALayer侧重于显示内容的绘制、显示和动画。 - 所属框架不同
UIView属于UIKit框架,UIKit框架主要用来构建用户界面的。
CALayer属于QuartzCore框架,而且CALayer是作为一个低级的,可以承载绘制内容的底层对象出现在该框架的。
2.什么是Layer层对象
用来显示绘制内容的一种数据对象,常见的几个自身具有绘制功能的专用Layer有:CATextLayer、CAShapeLayer、CAGradientLayer
2.1 CATextLayer
用来实现更加灵活的文字布局和渲染的,几乎包含了UILabel的所有特性并在此基础上增加了更强大的功能,包括字体、尺寸、颜色和下划线等文字效果,同时CATextLayer的渲染效果明显高于UILabel。
通过CATextLayer实现一个UILabel的示例代码如下:
// 创建一个字符承载视图
UIView *textView = [[UIView alloc] initWithFrame:CGRectMake(50, 50, 280, 50)];
CATextLayer *textLayer = [CATextLayer layer];
textLayer.frame = textView.bounds;
textLayer.string = @"CATextLayer";
// 文字前景色和背景色
textLayer.foregroundColor = [UIColor whiteColor].CGColor;
textLayer.backgroundColor = [UIColor grayColor].CGColor;
// 文字超出视图边界裁剪
textLayer.wrapped = YES;
textLayer.font = (__bridge CFTypeRef)[UIFont systemFontOfSize:18].fontName;
textLayer.alignmentMode = kCAAlignmentCenter;
// 适应屏幕retain分辨率,防止像素化导致模糊
textLayer.contentsScale = [[UIScreen mainScreen] scale];
[textView.layer addSublayer:textLayer];
[self.view addSubview:textView];
显示效果如下:
截屏2021-01-0120.55.29.png
2.2 CAShapeLayer
专门用来绘制矢量图形的图形子类,例如可以指定线宽和颜色等利用CGPath绘制图形路径,可以实现图形的3D变换效果,渲染效率比Core Graphics高得多,而且可以在超出视图边界之外绘制,即不会被边界裁减掉。
圆形的绘制代码如下:
// 创建圆形路径
UIBezierPath *path = [[UIBezierPath alloc] init];
// 起点要在圆心水平右侧半径长度处
[path moveToPoint:CGPointMake(200, 100)];
// 添加圆弧路径
[path addArcWithCenter:CGPointMake(150, 100) radius:50 startAngle:0 endAngle:2 * M_PI clockwise:YES];
// 创建图形层
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
// 路径线的颜色
shapeLayer.strokeColor = [UIColor cyanColor].CGColor;
// 闭合图形填充色,此处设置透明
shapeLayer.fillColor = [UIColor clearColor].CGColor;
// 线宽
shapeLayer.lineWidth = 10;
// 线的样式,端点、叫点
shapeLayer.lineCap = kCALineCapRound;
shapeLayer.lineJoin = kCALineJoinRound;
// 设置图形路径
shapeLayer.path = path.CGPath;
[self.view.layer addSublayer:shapeLayer];
效果如下:
截屏2021-01-0121.09.58.png
2.3 CAGradientLayer
是一个硬件加速的高性能绘制图层,主要用来实现多种颜色的平滑渐变效果。以下是一个3种颜色从正方形左上角到右下角的渐变效果示例代码:
// 创建leyer承载视图
UIView *containView = [[UIView alloc] initWithFrame:CGRectMake(50, 50, 200, 200)];
CAGradientLayer *gradientLayer = [CAGradientLayer layer];
gradientLayer.frame = containView.bounds;
// 依次设置渐变颜色数组
gradientLayer.colors = @[(__bridge id)[UIColor greenColor].CGColor,(__bridge id)[UIColor yellowColor].CGColor,(__bridge id)[UIColor orangeColor].CGColor];
// 颜色从起点到终点按比例分段位置
gradientLayer.locations = @[@0.0, @0.3, @0.5];
// 颜色渐变的起点和终点,(0, 0)~(1, 1)表示左上角到右下角
gradientLayer.startPoint = CGPointMake(0, 0);
gradientLayer.endPoint = CGPointMake(1, 1);
[containView.layer addSublayer:gradientLayer];
[self.view addSubview:containView];
效果如下:
截屏2021-01-0121.22.44.png
3.如何使用CAShapeLayer绘制图层
CAShapeLayer是一个通过矢量图形来进行绘制的图层子类。开发者通过指定诸如颜色和线宽等属性,用CGPath来定义指定形状的图形,最后CAShapeLayer对象就会自动渲染出来了。与直接使用Core Graphics在原始的CAL ayer对象中绘制的方式相比,使用CAShapeLayer有以下优点:
1.渲染速度更快。CAShapeLayer使用了硬件加速的方式绘制图形,绘制速度比Core Graphics快很多。
2.内存使用更高效。普通的CALayer对象需要创建一个寄宿图,而CAShapeLayer不需要,这样就节约了内存。
3.不会被边界涂层裁减掉。一个CAShapeLayer可以在图层边界绘制。而CAShapeLayer的图层路径不会像在使用Core Graphics的普通CALayer一样被裁减掉。
通常通过指定CAShapeLayer对象的path属性来绘制图层。示例代码如下:
@property (nullable) CGPathRef path;
此外还可以通过一些属性来描绘图形的线条,但在图层层面只有一次机会设置这些属性,如果想用不同的颜色或者风格来绘制多个形状,那么就不得不为每一个形状准备一个图层。下面示例用一个CAShapeLayer渲染一个简单的多边形,由于CAShapeLayer对象的path属性是CGPathRef类型,所以很容易使用UIBezierPath类帮助创建图层路径,这样就不需要人工释放CGPath了。
代码如下:
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
// 设置线宽
shapeLayer.lineWidth = 3;
shapeLayer.lineCap = kCALineJoinRound;
shapeLayer.lineJoin = kCALineJoinRound;
// 描边颜色
shapeLayer.strokeColor = [UIColor redColor].CGColor;
// 填充颜色
shapeLayer.fillColor = [UIColor whiteColor].CGColor;
// 创建UIBezierPath对象
UIBezierPath *bezierPath = [UIBezierPath bezierPath];
[bezierPath moveToPoint:CGPointMake(160, 100)];
[bezierPath addLineToPoint:CGPointMake(100, 160)];
[bezierPath addLineToPoint:CGPointMake(100, 220)];
[bezierPath addLineToPoint:CGPointMake(160, 280)];
[bezierPath addLineToPoint:CGPointMake(220, 220)];
[bezierPath addLineToPoint:CGPointMake(220, 160)];
[bezierPath closePath];
shapeLayer.path = bezierPath.CGPath;
[self.view.layer addSublayer:shapeLayer];
效果如下:
截屏2021-01-0419.53.08.png
4.iOS中如何为UIImageView添加圆角
按照渲染方式,实现UIView及其子类的圆角效果有2种方法:一种是直接设置layer圆角属性,为离屏渲染,另一种是自定义圆角绘制方法,实现当前屏幕渲染。
当前屏幕渲染:指GPU直接在当前屏幕缓冲区进行图形渲染,不需要提前另开缓冲区也就不需要缓冲区的切换,因此性能高。
离屏渲染:提前另开一个缓冲区进行图形渲染,由于需要和当前屏幕缓冲区进行切换,所以很耗性能。通常圆角、遮罩、不透明度、阴影、渐变、光栅化和抗锯齿等设置都会触发离屏渲染。
1.离屏渲染实现圆角
view.layer.masksToBounds = YES;
view.layer.cornerRadius = 5;
2.当前屏幕渲染实现圆角
直接在当前屏幕绘制,提高性能。
为UIImage类扩展一个实例方法:
- (UIImage *)imageWithCornerRadius:(CGFloat)radius ofSize:(CGSize)size {
// 当前UIImage可见绘制区域
CGRect rect = (CGRect){0.f,0.f,size};
// 创建基于位图的上下文
UIGraphicsBeginImageContextWithOptions(size, NO, UIScreen.mainScreen.scale);
// 在当前位图上下文添加圆角绘制路径
CGContextAddPath(UIGraphicsGetCurrentContext(), [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:radius].CGPath);
// 当前绘制路径和原绘制路径相交得到最终裁剪绘制路径
CGContextClip(UIGraphicsGetCurrentContext());
// 绘制
[view drawRect:rect];
// 取得裁剪后的image
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
// 关闭当前位图上下文
UIGraphicsEndImageContext();
return image;
}
5.contentsScale属性有什么作用
图层的contentsScale属性属于支持高分辨率屏幕(如Retina屏幕)机制的一部分,它定义了图层content中图像的像素尺寸与视图大小的比例,它也被用来判断在绘制图层时允许为content属性创建的空间大小,以及需要显示的图片的拉伸度。
默认情况下,contentsScale值为1.0,也就是说图层的绘制系统将会以每一个点对应一个像素来绘制图片,如果将其设置为2.0,那么会以每个点对应2个像素绘制图片,即所谓的Retina屏幕。
在开发中,可设置contentsScale为合适的值,如下:
layer.contentsScale = [UIScreen mainScreen].scale;
问:像素和点有什么关系
1.点是iOS中标准的坐标体系,他是IOS中的虚拟像素,也就是逻辑像素。标准设备中1个点就是1个像素,Retina中1个点是2个像素。iOS中用点作为屏幕的坐标测算就是为了在Retina设备和普通设备上有一致的效果。
2.像素是屏幕分辨率的尺寸单位,物理像素坐标并不适用于屏幕布局,但是仍然和图片有相对关系。UIImage是一个屏幕分辨率解决办法,它是用来衡量点大小的,但是,一些底层的CGImage类型的图片会使用像素,所以必须清楚在Retina设备和普通设备上,点和像素代表不同的大小。
6.如何理解anchorPoint和position作用
在UIView中有3个重要的属性:frame、bounds、center,分别对应CALayer中的frame、bounds、position。虽然图层使用了position而视图使用了center,但这2个属性代表了同样的值。也就是说,当前图层的anchorPoint相当于父图层的位置。
anchorPoint被称为“锚点”,图层的anchorPoint属性通过影响position的值来控制它的frame,可以将anchorPoint比作控制图层移动的“支点”。anchorPoint是使用单位坐标来描述的,也就是图层的相对坐标。左上角是{0,0},右下角是{1,1}。anchorPoint默认的坐标是{0.5,0.5},即图层的中心点。
position等价于视图中的center,代表了anchorPoint点在父图层(superLayer)的位置,因此可以说,position点是相对于父图层坐标系的,而anchorPoint是相对于当前图层的,2者是相对于不同坐标系的一个重合点。
事实上,position和anchorPoint是相互不影响的,修改其中任何一个值,另一个不会改变,改变的只是当前图层的frame。对于视图或者图层来说,frame并不是非常清晰的一个属性,当其中任何一个值发生改变,frame都会变化,相反,改变frame的值,同样也会影响他们的值。
frame、position和anchorPoint关系如下:
frame.origin.x = position.x - anchorPoint.x * bounds.size.width;
frame.origin.y = position.y - anchorPoint.y * bounds.size.height;
在制作动画时,视图默认是围绕中心点进行旋转或者平移的,这是因为默认UIView的rootLayer的anchorPoint就是图层的中心。在实际开发中,可以改变anchorPoint以达到其他的效果。
7.如何理解drawRect方法
iOS的绘图操作是发生在UIView的drawRect方法中的。如果想在UIView中绘图,那么可以通过继承UIView类并实现drawRect方法,在drawRect方法中获取当前环境的上下文进行绘制。drawRect方法被定义在UIView的UIViewRendering类别中:
- (void)drawRect:(CGRect)rect;
drawRect:里面的代码利用Core Graphics在指定的rect中绘制图形,然后内容就会被缓存起来直到它需要被更新。事实上,苹果公司不建议开发者主动调用drawRect,当然如果直接强制调用,也是没有效果的,因为系统此时不会自动创建View相关联的上下文。而且当没有自定义绘制任务时,就不要在子类中写一个空的drawRect,否则会造成CPU资源和内存的浪费。
当视图在屏幕中出现的时候,drawRect会被自动调用。具体是,drawRect方法的第一次调用是在控制器中loadView和viewDidLoad两方法之后。所以不必担心控制器初始化之前,drawRect会被执行。虽然drawRect是UIView中的方法,但事实上都是底层的CALayer对象安排了重绘工作并保存了绘制好的内容。
另外,可以调用setNeedsDisplay方法将绘制好的图形更新到视图上。setNeedsDisplay就是在receiver上设置一个需要被重绘的标记,在下一个绘制周期自动进行重绘,一般iPhone的刷新频率是60HZ,也就是说1/60秒后重绘。示例代码如下:
- (void)drawRect:(CGRect)rect {
// 获取上下文
CGContextRef ctx = UIGraphicsGetCurrentContext();
// 绘制一个圆形
CGContextAddEllipseInRect(ctx, CGRectMake(0, 0, 200, 200));
// 设置颜色
[[UIColor redColor] set];
// 填充
CGContextFillPath(ctx);
}
8.如何使用mask属性实现图层蒙版功能
在实际开发中,通常使用mask属性来实现图层蒙版的功能,这个属性本身就是CALayer类型,和其他图层一样有绘制和布局的属性,不同于那些绘制在父图层中的子图层,mask定义了父图层的部分可见区域。
mask图层中最重要的是图层的轮廓。在父图层中,与mask图层相重叠的部分会被保留,其他部分将会被遮盖,也就是说,父图层提供内容,mask图层提供形状。mask不仅仅局限于静态图,还可以用代码或者动画实时生成。
CGSize size = [UIScreen mainScreen].bounds.size;
// 设置寄宿图
self.view.layer.contents = (id)[UIImage imageNamed:@"海水"].CGImage;
// 设置寄宿图的显示方式,等价于UIView的contentMode
self.view.layer.contentsGravity = @"resizeAspect";
// 设置mask层
UIImageView *searchImageView = [[UIImageView alloc] initWithFrame:CGRectMake((size.width - 150) * 0.5, (size.height - 150) * 0.5, 150, 150)];
searchImageView.image = [UIImage imageNamed:@"马"];
self.view.layer.mask = searchImageView.layer;
9.如何解决maskToBounds离屏渲染带来的性能损耗
在开发中常用CALayer的cornerRadius属性来设置图层的圆角曲率,默认情况下,cornerRadius只影响背景颜色而不影响背景图片或者子图层。但是将CALayer的masksToBounds设置为YES时,图层内容就会被截取:
imageView.layer.masksToBounds = YES;
imageView.layer.cornerRadius = 5;
这样的渲染机制是GPU在当前屏幕缓冲区外新开辟一个缓冲区进行工作,也就是所谓的离屏渲染,所以会有额外的性能损耗。如果在某一时刻大量使用这种方式设置圆角,那么就会触发缓冲区频繁合并和上下文之间频繁切换,应用程序就可能出现掉帧和卡顿。具体可以用Instruments检测。
为了防止离屏渲染,可以不使用上面方法,而是将图片处理的权利交给CPU,虽然CPU对图形的处理能力不如GPU,但是设置圆角的难度不大,且代价远小于上下文切换,以下是常用的2种方法:
1.使用CALayer的shouldRasterize属性
shouldRasterize属性是设置光栅化,可以使离屏渲染的结果缓存到内存中存为位图,当下次使用的时候可以直接使用内存缓存,这样就节省了一直离屏渲染的性能损耗。为了使用shouldRasterize属性,还需要设置rasterizationScale属性去适配屏幕,以防止Retina屏幕像素化的问题。
imageView.layer.shouldRasterize = YES;
imageView.layer.rasterizationScale = [UIScreen mainScreen].scale;
这种方法虽然在一定程度上优化性能,但是如果layer及sublayers经常改变,那么就会不停地渲染及设置缓存,这种情况下也是很耗性能的。
2.使用Core Graphics绘制出圆角图片
可以使用UIKit中对Core Graphics有一定封装的应用层类UIBezierPath,对图片进行重新剪切。代码如下:
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
imageView.image = [UIImage imageNamed:@"1"];
// 开启上下文,开始对imageView画图
UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, NO, 1.0);
// 使用贝塞尔曲线画出一个圆形图
[[UIBezierPath bezierPathWithRoundedRect:imageView.bounds cornerRadius:imageView.frame.size.width] addClip];
[imageView drawRect:imageView.bounds];
imageView.image = UIGraphicsGetImageFromCurrentImageContext();
// 关闭上下文,结束画图
UIGraphicsEndImageContext();
[self.view addSubview:imageView];
10. QuartzCore和Core Graphics有什么区别
Core Graphics是iOS系统中的底层绘图框架,平时使用最频繁的point、size、rect等视图属性都被定义在这个框架中,包含的API都是以CG开头,提供的都是C语言的函数接口。
QuartzCore框架从其头文件可以发现,其实就是CoreAnimation。也就是说,QuartzCore专指CoreAnimation用到的动画相关的库、API和类。
以下是QuartzCore头文件:
#ifndef QUARTZCORE_H
#define QUARTZCORE_H
#include <QuartzCore/CoreAnimation.h>
#endif /* QUARTZCORE_H */
Core Graphics和QuartzCore都是跨iOS和Mac OS平台的,这点区别于UIKit(只适用于iOS平台)。
QuartzCore大量使用了Core Graphics中的类,因为动画的产生必然要用到图形库中的东西。
网友评论