17 Jul 2014
TableView滚动有点卡的时候,在保证Core Animation帧数是60的情况下,有时还要再看一下Time Profile。比如哪个是当前比较耗时的方法,分析一下原因以及解决方法。
昨天解决Detail页面滚动评论卡的时候,Core Animation的帧数始终在59-60之间,貌似看不出什么问题。

再看一下Time Profile貌似发现有一个比较常见的[NSString stringWithFormat:]方法居然占到了13.7%的执行时间感觉有点不靠谱。

定位原因
Detail页面的滚动是用户评论,还好内容不多;用排除法最终定位在评论内容做表情替换这里,代码如下:

解释一下,因为表情有100多个,而且服务端返回的是类似/:026、/:-W之类的伪符号,所以循环把这些伪符号替换成一段类似html的代码,并且每次循环都用到[NSString stringWithFormat:]方法构建了这段类似html的代码。
解决方案
-
可以把上述字符串替换的过程放到global线程中执行,等替换成功了再切换回来执行真正的绘图相关代码。
-
既然[NSString stringWithFormat:]这么坑,我们就想是否有性能更好的方法做同样的事情。
int sprintf( char *restrict buffer, const char *restrict format, … ); (since C99)
所以上面这段表情替换的代码就可以改成
- (void)setText:(NSString *)text {
NSString *s = [text copy];
NSDictionary *dict = [FMCommon getEmojiToImageDict];
char emojiKey[250];
CGFloat pointSize = self.font.pointSize ;
for (NSString *key in [dict allKeys]) {
NSString *value = [dict objectForKey:key];
if (value) {
sprintf(emojiKey, EMOJI_DIRECTORY, [value UTF8String],pointSize,pointSize);
NSString *imageValue = [NSString stringWithCString:emojiKey encoding:NSASCIIStringEncoding];
s = [s stringByReplacingOccurrencesOfString:key
withString:imageValue];
}
}
[super setText:s];
}
现在我们Detail页面是上述两种方法一起应用,改成这样以后肉眼看滚动TableView似乎没那么卡了,再看一下TimeProfile终于也没有占用特别长时间的方法了。
性能对比
用[NSString stringWithFormat:]和sprint()两种方法先后进行一次总计1million的字符串拼装,分别看看两者的耗时情况
long calCount = 1000000;
for (int i=0; i<calCount; i++) {
testString = [NSString stringWithFormat:AlTStringTestStringFormat,sample,i];
}
sprintf()
char testchar[20];
const char* sampleChars = [sample UTF8String];
for (int i=0; i<calCount; i++) {
sprintf(testchar,sampleChars ,i);
}
同样循环1百万次拼装字符串的性能差距到底有大,我们通过计算程序执行的时间来看
mach_timebase_info_data_t info;
if (mach_timebase_info(&info) != KERN_SUCCESS) return -1.0;
uint64_t start = mach_absolute_time ();
// 这里写入需要统计时间的执行代码
uint64_t end = mach_absolute_time ();
uint64_t elapsed = end - start;
uint64_t nanos = elapsed * info.numer / info.denom;
return (CGFloat)nanos / NSEC_PER_SEC;
对比结果
在iPhone 5真机上面执行上述程序的结果:
但从对比结果上来看sprintf的效率应该是stringWithFormat的20倍以上,看来以后对于大量的字符串拼装最好还是用sprintf方法。
以上测试代码可以在https://github.com/allenzerg001/TestHelloWorld下载
至于为什么是这样,因为还没有找到stringWithFormat方法的真正源码,还为从得知,有知晓的还望指教。
28 May 2014
文字排版
通常我们使用UILabel、UITextField、UITextView在iOS上展示一些我们需要的文字。前者用于简单的展示,后两者可以用于接受用户的输入。通常情况下我们用上述3者展示简单的纯文本,如果我们需要展示图文混排或者稍微带一点排版样式的文字时,我们需要使用更底层的一些技术,比如Text Kit 或者 Core Text。

上图展示的iOS7的框架层次结构,可以看到基于Core Text的Text Kit为上述3个常用控件提供了技术支持。
首先我们需要准备一些关于字符的预备知识作为铺垫。
字符和字形(Character & Glyphs)
字符(Character)
字符从本质上讲是一个在特定字符集中定义的数字,比如我们在OS X中广泛使用的Unicode。Unicode标准为每一种我们可能会用到的字符,提供了唯一的数字标识。
字形(Glyphs)
字形是用于展示上述字符的一个图形形状。但是每个字符对应的字形,根据字体的不同、字体粗细/斜体定义的不一,并不是唯一的。
例如下图展示的字符“A”的多种字形

另外字体对应字形也不总是一对一的,在英文中经常就有连写的写法,如下图展示f+f和f+l连写情况下的字形

属性字符串(NSAttributedString)
属性字符串(NSAttributedString)在Core Text中使用广泛,接下来你可能会经常遇到他。他用于管理字符串和相关的属性集(例如:字体、字间距),这些属性可以被用于单个字符也可以是一段连续的字符串。
他使用一个NSDictionary来管理唯一标示的属性名称,你可以为任一范围的字符指定想要的字符属性。
在iOS6及以后,你可以通过attributedText属性为UILabel、UITextView、UITextField指定想要展示的字符串。通过这种方式我们就可以展示一些带格式的文本了。但是也许你会问如果要兼容iOS5或者更早的版本,要怎么做呢?这个我们在后面的Core Text部分会有解答。
以下是常见的4种类型,mutable和toll free bridge
NSAttributedString
NSMutableAttributedString
// Toll Free Bridge
CFAttributedString
CFMutableAttributedString
创建方法
initWithString:
initWithString:attributes:
initWithAttributedString:
同时还支持从RTF或者RTFD格式的rich text直接创建
initWithRTF:documentAttributes:
initWithRTFD:documentAttributes:
另外还支持从HTML数据直接创建
initWithHTML:documentAttributes:
initWithHTML:baseURL:documentAttributes:
读写属性
详细参考NSAttributedString 和 NSMutableAttributedString 的reference
Text kit
好了接下来重新回到我们之前说的Text Kit 和 Core Text中来。
Text Kit是UIKit framework中定义的一组用于提供高性能的排版、布局和展示文字的类和协议,比如展示特别的字间距、行间距、断行规则。
Text Kit中最主要的类之间的数据流图:

-
Text views是指实例化以后的UITextView
-
Text containers是指实例化的NSTextContainer。定义了字符串需要显示的区域,通常是一个矩形。通过继承后自定义他也可以是其他的任意形状。另外通过NSTextContainer.exclusionPath定义的UIBezierPath数组,可以定义在当前区域内不能用于text展示的例外区域。
-
Layout manager是指实例化的NSLayoutManager。映射了字符到字形的对应变换关系。
-
Text storage是指实例化的NSTextStorage。NSTextStorage是NSMutableAttributeString的子类,他保存了我们需要展示的字符串,以及字符串的各种样式
更形象化的展示,如下图所示

如果text在一个text container中展示不完全,那么他就会展示到另外一个text container中(如果有的话)。
代码demo:
NSTextStorage* textStorage = [[NSTextStorage alloc] initWithString:string];
NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
[textStorage addLayoutManager:layoutManager];
self.textContainer = [[NSTextContainer alloc] initWithSize:self.view.bounds.size];
[layoutManager addTextContainer:self.textContainer];
UITextView* textView = [[UITextView alloc] initWithFrame:self.view.bounds textContainer:self.textContainer];
[self.view addSubview:textView];
Core Text
位于Text Kit 更底层的 Core Text framework 是一种用于排版和展示文字的技术,他被设计得高效且易用,速度比已有的ATSUI要快2倍以上。自从Core Text随着OSX 10.5(Leopard)的推出以来,很快就取代了ATSUI的地位。
简单的绘制方法
-
OSX
可以通过drawAtPoint:和drawInRect:以及drawWithRect:options:方法绘制在NSView。但是这几个方法不推荐大范围使用,特别是针对频繁的绘制text,更推荐使用Text布局引擎(这个后面会提到)
-
iOS
由于没有NSView的支持,Apple特别提供了CATextLayer来帮助我们绘制。
CATextLayer有一个-(id)String 的属性,可以被设置为NSString或者NSAttrib
utedString两种类型。这样我们就可以把CATextLayer加在任何我们想要的view中,用于展示需要的text。
代码例子如下:
NSAttributedString *attributedString ; // 假设这里已经初始化好了
CATextLayer *textLayer = [[CATextLayer alloc ] init];
textLayer = attributedString;
[self.view.layer addSublayer:textLayer];
这里也就解答了前面提到的如何兼容iOS5下通过NSAttributeString来展示带格式的文本了。CATextLayer支持iOS 3.2及以后的版本,所以如果你要支持更早的版本,那么你可能要自己在stackoverflow一下了,呵呵。
Core Text布局引擎和Font api
接下来我们正式回到Core Text的部分。
Core Text主要由两个部分组成,text布局引擎和font API。

-
首先通过已经创建好的CFAttributedStringRef(toll bridge to NSAttributedString),使用下面方法创建一个CTFramesetterRef
CTFramesetterRef CTFramesetterCreateWithAttributedString( CFAttributedStringRef string );
-
通过CTFramesetterRef 和 指定的文字显示区域 CGPathRef 创建CTFrameRef。
根据上图所示,同一个CFAttributedStringRef 和 CGPathRef 可以产生一个或者多个的CTFrameRef,在此同时framesetter将段落的样式应用到CTFrameRef上,比如对齐方式,tab位置,行间距,缩进,断行方式等。
CTFrameRef CTFramesetterCreateFrame( CTFramesetterRef framesetter, CFRange stringRange, CGPathRef path, CFDictionaryRef frameAttributes );
-
在CTFramesetterRef 生成 CTFrameRef的同时,也唤起了一个typesetter(排字,通常是CTTypesetterRef)。她的作用是将CFAttributedStringRef中的字符转换成对应的字形。
-
每个段落中(可以理解CTFrameRef代表一个段落)中包含了多行(CTLineRef)。
-
每一行中又包含了多个CTRunRef,是指一行中连续的一段包含同样属性和方向的文字。
自定义图文混排
通常情况下图文混排,还是需要开发者自己做一些事情。至于怎么做,这个就需要用到CTRunDelegate了。
本身这是Core Text提供的一个用于自定义CTRun排版属性的回调方法,这里的排版属性包括字形的宽度,字形的向上高度,向下高度。
字形宽度可以理解,但是什么是向上高度和向下高度呢?这个请看下图:

其实这个也比较好理解,大家刚学英语的时候,一定用过那种英文练习簿吧。那么倒数第二根线就是原点所在的基线了,往上就是向上高度,往下就是向下高度。

我们可以通过以下方法来创建一个CTRunDelegateRef
CTRunDelegateRef CTRunDelegateCreate(const CTRunDelegateCallbacks* callbacks, void* refCon )
CTRunDelegateCallbacks才是真正定义字形宽度、向上高度和向下高度的结构体,我们看一下他的定义:
typedef struct
{
CFIndex version;
CTRunDelegateDeallocateCallback dealloc;
CTRunDelegateGetAscentCallback getAscent;
CTRunDelegateGetDescentCallback getDescent;
CTRunDelegateGetWidthCallback getWidth;
} CTRunDelegateCallbacks;
其中的dealloc、getAscent、getDescent、getWidth几个属性的类型都差不多
typedef void (*CTRunDelegateDeallocateCallback) (
void* refCon );
typedef CGFloat (*CTRunDelegateGetAscentCallback) (
void* refCon );
typedef CGFloat (*CTRunDelegateGetDescentCallback) (
void* refCon );
typedef CGFloat (*CTRunDelegateGetWidthCallback) (
void* refCon );
c语言中的void* 含义是任意类型的指针,但是我理解这里的参数一般指的是NSAttributeString中当前CTRun对应的属性NSDictionary(如果我理解的不对欢迎指出)
-
首先我们定义好自己的CTRunDelegateCallbacks:
void ACRichTextRunDelegateDeallocCallback(void *refCon) {
}
CGFloat ACRichTextRunDelegateGetAscentCallback(void *refCon) {
CFDictionaryRef attributes = (CFDictionaryRef) refCon;
CFStringRef height = CFDictionaryGetValue(attributes, @ATTRIBUTE_IMG_HEIGHT);
if (height) {
double heightF = CFStringGetDoubleValue(height);
return (float) heightF;
}
return IMG_DEFAULT_HEIGHT;
}
CGFloat ACRichTextRunDelegateGetDescentCallback(void *refCon) {
return 0;
}
CGFloat ACRichTextRunDelegateGetWidthCallback(void *refCon) {
CFDictionaryRef attributes = (CFDictionaryRef) refCon;
CFStringRef height = CFDictionaryGetValue(attributes, @ATTRIBUTE_IMG_WIDTH);
if (height) {
double heightF = CFStringGetDoubleValue(height);
return (float) heightF;
}
return IMG_DEFAULT_HEIGHT;
}
// 创建我们自己的CTRunDelegateCallbacks 结构体
CTRunDelegateCallbacks imageCallbacks;
imageCallbacks.version = kCTRunDelegateVersion1;
imageCallbacks.dealloc = ACRichTextRunDelegateDeallocCallback;
imageCallbacks.getAscent = ACRichTextRunDelegateGetAscentCallback;
imageCallbacks.getDescent = ACRichTextRunDelegateGetDescentCallback;
imageCallbacks.getWidth = ACRichTextRunDelegateGetWidthCallback;
-
之后我们把CTRunDelegateCallbacks结构体设置到CFAttributedString中,并通过range指定作用的范围
CFMutableAttributedStringRef text ; // 假设这里已经初始化好
NSDictionary *imgAttributes = component.attributes;
CTRunDelegateRef runDelegate = CTRunDelegateCreate(&imageCallbacks, (__bridge void *) imgAttributes);
CFAttributedStringSetAttribute(text, CFRangeMake(position, 1), kCTRunDelegateAttributeName, runDelegate);
// 把属性设置到
CFAttributedStringSetAttributes(text, CFRangeMake(position, 1), (__bridge CFDictionaryRef) component.attributes, NO);
CFRelease(runDelegate);
经过上面这么多步骤,其实只是告诉Core Text 有一个地方需要占多大的位置,这样系统就会在指定的地方把空间腾出来,不绘制文字上去。真正的图像绘制其实还是需要我们自己通过Core Graphic来做。
- 获取CTRun对应的坐标范围
通过创建CTFrameSetterRef,并且获取嵌套的CTFrameRef,以及包含的CTLineRef,从而最终循环到对应的CTRunRef,再通过以下函数获取当前CTRun坐标范围内的相关属性,比如宽度、向上高度和向下高度。
// 后面3个参数ascent/descent/leading都是输出项
double CTRunGetTypographicBounds (
CTRunRef run,
CFRange range,
CGFloat *ascent,
CGFloat *descent,
CGFloat *leading
);
- 自定义绘图
上面获取的只是当前run的属性,真正绘图需要的是相对origin的坐标值,所以在循环CTLine和CTRun的时候,要记录下line和run的origin,并累加起来才是真正相对于坐标原点的偏移量。
有了坐标值后面就是真正的Core Graphic绘图了,限于篇幅不再展开去,具体可以参考Apple的官方文档:
参考文献
13 May 2014
第二部分主要是关于动画的
7. Implicit Animations 隐式动画
隐式动画就是,当你修改CALayer一个支持动画的属性时,这个变换的效果不是立刻体现在屏幕上,而是使用一个平滑的动画从原来值过度到新的值。这是一个默认的行为,根本不需要开发做任何事;
CATransaction类用于管理变换,你不能通过+alloc和-init方法来创建他,而是应该通过+begin和+commit来提交变换,或者获取当前正在执行的变换。通过+setAnimationDuration方法设置变换的持续时间,另外可以通过+animationDuration方法获取当前变换的持续时间。
Core Animation通过run loop的迭代循环来开始一个变换,默认的持续时间是0.25秒。通过+setAnimationDuration:方法设置持续时间,附带的也会影响到其他当前发生的变换动画。
CATransaction提供了+setCompletionBlock:用于设置动画结束后执行,并且他的默认持续时间也是0.25秒。
CALayer的动画属性被默认开启,但是UIView则不是。这需要了解一下隐式动画的实现方式,当有CALayer的任意属性被修改时:
- 调用自己的-actionForKey:方法,并且传入当前属性的名称;返回对象(CAAction协议对象)并执行,传入参数分别是属性名、layer,dict;
- layer检查是否有delegate实现了CALayerDelegate协议中定义的-actionForLayer:forKey方法,有的话执行并返回;
- 如果上面没有,则检查layer自己的actions有没有属性对应的CAAction;
- 如果上面也没有,则检查layer自己的style有没有属性对应的CAAction;
- 如果上面也没有,layer会调用-defaultActionForKey:方法,找到那些默认定义的属性及其action;
而在UIView中,每个UIView都是他layer的delegate,并且提供了-actionForLayer:forKey方法的实现。不在动画block中时(应该是指+beginAnimations:context和+commitAnimations之间),该方法永远返回ni,而如果在动画block中时则返回非nil。
- UIView的layer不支持隐式动画,有几种方法让UIView的属性也支持动画:
- 使用UIView的动画方法
- 子类化UIView并且重载-actionForLayer:forKey:方法
- 创建一个显式动画
- 自主Layer控制动画的方式:
- layer的delegate实现-actionForLayer:forKey:
- 提供actions
展现层和模型层
iOS中,屏幕每秒钟绘制60次。如果一个动画的持续时间超过60分之1秒,那么Core Animation在新值展现到屏幕之前的过程中,需要合成计算好几次。这意味着CALayer必须要保持一个用于展示的当前值。
这个用于展示的当前值被保存在一个叫做展现层(presentation layer)的地方,我们可以通过-presentationLayer方法访问。注意这个属性只有在layer被第一次绘制到屏幕上以后才被创建。
以下两种情况你需要用到这个展现层
- 你在实现一个基于时间轴的动画
- 你需要动画中的layer响应用户输入,比如-hitTest:判断layer是否被点击时,展现层才是layer当前的位置所在
————————————————————————————————————————
8. 显式动画
Basic Animations
-
CAAnimation 实现了CAAction和CAMediaTiming协议,另外有3个重要的属性
- removeOnCompletion 是否动画完成自动删除,default=YES
- timing fuction
- delegate 反馈动画的状态
-
CAPropertyAnimation 通过keyPath针对单个属性值,keyPath可以是含有.符号的多层属性,可以使layer的属性,也可以使layer子元素的属性或者虚拟属性,点击查看更多支持的键路径列表
-
CABasicAnimation 继承了CAPropertyAnimation并添加了3个属性:fromValue/toValue/byValue,他们同时不能超过2个有值,即不能3个同时有值:
- fromValue/toValue有值,fromValue ~ toValue
- fromValue/byValue有值,fromValue ~ byValue
- byValue/toValue有值,(toValue-byValue) ~ toValue
- fromValue有值,fromValue ~ (current presetation)
- toValue有值,(current presetation) ~ toValue
- byVallue有值,(current presetation) ~ (byValue + current presetation)
- all nil,(previous) ~ (current) in presetation layer
使用显示动画只会修改persetation layer的值,要想看到正常的效果需要同时修改model layer的属性值。
- CAAnimationDelegate CAAnimation的delegate实现了这个协议,他有2个方法-animationDidStop:finished:和-animationDidStart:
Keyframe Animations
CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
animation.keyPath = @"backgroundColor";
animation.duration = 2.0;
animation.values = @[
(__bridge id)[UIColor blueColor].CGColor,
(__bridge id)[UIColor redColor].CGColor,
(__bridge id)[UIColor greenColor].CGColor,
(__bridge id)[UIColor blueColor].CGColor
];
//apply animation to layer
[self.colorLayer addAnimation:animation forKey:nil];
通过path属性指定运动路径,rotationMode指定运动中物体朝向
//create the keyframe animation
CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
animation.keyPath = @"position";
animation.duration = 4.0;
animation.path = bezierPath.CGPath;
animation.rotationMode = kCAAnimationRotateAuto;
[shipLayer addAnimation:animation forKey:nil];
Virtual Properties
transform.rotation
transform.scale 不是真实存在的属性,但是可以在animation使用 不知道还有没有更多?
Animation Group 动画组
CAAnimationGroup *animationGroup = [CAAnimationGroup animation];
animationGroup.animations = @[animation1,animation2];
animationGroup.duration = 4.0;
// animation1和animation2可以任何的CAAnimation的子类型 更多动画组的示例可以参考[objc.io的issue-12-1](http://objccn.io/issue-12-1/)
Transition
CATransition有2个重要的属性type和subType,
type定义动画的方式,如下:
kCATransitionFade
kCATransitionMoveIn
kCATransitionPush
kCATransitionReveal
subType定义动画出现的方向,如下:
kCATransitionFromRight
kCATransitionFromLeft
kCATransitionFromTop
kCATransitionFromBottom
————————————————————————————————————————
9. Layer Time
CAMeidaTiming Protocol几个重要的属性:
duration // 每次重复的持续时间
repeatCount // 重复的次数 = HUGE_VALF 无限循环
repeatDuration // 总重复的时间
autoreverses // 是否自动倒序播放动画
beginTime // 动画开始的延迟时间
speed // 动画的播放速度,默认=1.0
timeOffset // 动画开始后xx时间才显示出来,也就是中途展示一个动画的偏移时间
fillMode
填充动画已经结束到没开始这段时间内展现层的效果,可选值如下:
kCAFillModeForwards
kCAFillModeBackwards
kCAFillModeBoth
kCAFillModeRemoved #### Local Time 每个Layer和Animation都有自己的local time,如果要同步多个不同speed的animation,我们需要使用以下方法做转换:
- (CFTimeInterval)convertTime:(CFTimeInterval)t fromLayer:(CALayer *)l;
- (CFTimeInterval)convertTime:(CFTimeInterval)t toLayer:(CALayer *)l;
Pause, Rewind, Fast-Forward
通过设置CALayer的speed(实际上是CAMediaTiming的speed,CALayer实现了该protocol)可以控制动画的暂停(speed=0),倒退(speed设置为负数)和快进(speed设置大于1);
可以通过类似方法控制main window的速度
self.window.layer.speed = 100;
Manual Animation
可以通过speed和timeOffset手动控制动画的播放。首先speed暂停动画的播放,然后再设置timeOffset设置动画前进或者后退。
————————————————————————————————————————
10. Easing
// 1. 系统提供的5种
+ (id)functionWithName:(NSString *)name
kCAMediaTimingFunctionDefault;
kCAMediaTimingFunctionEaseIn;
kCAMediaTimingFunctionEaseInEaseOut;
kCAMediaTimingFunctionEaseOut;
kCAMediaTimingFunctionLinear;
// 2. 通过自定义贝塞尔曲线的两个control point生成(在0-1范围内)
+ (id)functionWithControlPoints:(float)c1x :(float)c1y :(float)c2x :(float)c2y
CAKeyframeAnimation可以通过timingFunctions属性来支持每个关键帧之间的easing
————————————————————————————————————————
04 May 2014

Nick Lockwood的《iOS Core Animation》全书分为3大部分
- The Layer Beneath
- Setting Things in Motion
- The Performance of a Lifetime
本文章篇幅限定对第一部分Layer的摘记,后续部分可能会继续整理。
Core Animation并不只是包含动画,其实动画只是他的一部分,更合适的名称应该是Layer Kit。
1.The Layer Tree
凡是开发过iOS或者Mac OS应用的同学都应该知道view的概念。view是一个矩形的用于显示内容(比如图像,文字,视频等)的对象。
在iOS中,所有的view都继承自UIView。UIView可以控制用户事件并且支持Core Graphic绘图,仿射变换(例如旋转和拉伸),以及简单的动画比如滑动和渐隐。
同时你可能已经发现了UIView并不是他自己处理了这一切,其实绘图,布局和动画都是由CALayer来控制的。
CALayer
CALayer和UIView非常类似,最主要去区别就是CALayer不处理用户交互行为,也就是说CALayer并不能感知responder chain的存在所以他不能响应用户交互的事件,但他也用于对交互行为触发位置的落点判断。
我们为什么要了解CALayer的存在,因为他有一些UIView不能做的事情:
-
阴影、圆角、多彩的border
-
3D变化和定位
-
非矩形的边界
-
alpha遮罩
-
多步非线性动画
2. The Backing Image
contents
CALayer的contents属性的类型是id,主要是为了同时支持Mac OS和iOS,在Mac OS下contents可以被设置为NSImage,在iOS下则需要设置为CGImageRef。当然如果你设置其他类型的值,则CALayer会显示为空白。
contentsGravity
当图片大小不是适合当前view的时候,你可能会使用view.contentMode来设置缩放和位置,其实他是通过控制CALayer的contentGravity属性来实现的
kCAGravityCenter kCAGravityTop
kCAGravityBottom kCAGravityLeft
kCAGravityRight kCAGravityTopLeft
kCAGravityTopRight kCAGravityBottomLeft
kCAGravityBottomRight kCAGravityResize
kCAGravityResizeAspect kCAGravityResizeAspectFill
例如我们可以使用类似于UIViewContentModeScaleAspectFit的kCAGravityResizeAspect来设置我们的图片拉伸以适应我们layer的bounds
self.layerView.layer.contentsGravity = kCAGravityResizeAspect;
contentScale
contentScale用来定义像素尺寸到layer尺寸的拉伸比率,默认值是1。如果你设置了上面的contentsGravity属性,则contentScale很可能是没有效果的。
其实contentScale多用于设置高分辨率屏幕下的图片尺寸,如果=1则表示每个点代表一个像素,如果=2则表示每个点代表两个像素。因为layer的contents支持的CGImage并不知道当前分辨率的情况(UIImage支持),所以需要配合contentScale属性来支持合适的图片尺寸。以下两种方式设置contentScale都可以:
//1.set the contentsScale to match image
self.layerView.layer.contentsScale = image.scale;
//2.set the screen scale
self.layerView.layer.contentsScale = [UIScreen mainScreen].scale; ### contentsRect 允许我们定义content image的一部分矩形区域用于当前layer的显示,默认值是{0,0,1,1}表示显示全部。 不同于bounds、frame通过point来度量大小,contentsRect使用的是Unit坐标。在iOS中常见3各种坐标度量:
-
Points – 在非高清屏幕设备中,1point=1像素;高清屏幕设备中,1point=2像素。iOS用这种坐标来统一高清和非高清的设备坐标尺寸。
-
Pixels – CGImage使用Pixels坐标,所以要主要CGImage不适应高清屏幕显示。
-
Unit – 这种坐标更方便的用于指定相对大小,多用于OpenGL和Core Animation。
举例如{0,0,0.5,0.5}表示只显示contents的左上角四分之一,{0.5,0,0.5,0.5}表示只显示contents的右上角的四分之一,其他同理可得。
contentsCenter
contentsCenter定义了layer内部一个可以被拉伸的区域,外部则是一个固定的边界。同时他使用的也是跟上面contentsRect一样的Unit坐标表示法。

如上图所示就比较清晰了,定义了contentsCenter={0.25,0.25,0.5,0.5},则表示中间绿色区域都是可以被拉伸的,蓝色区域可以被横向拉伸纵向则固定宽度,红色区域反之是纵向可以被拉伸横向宽度固定。
自定义绘图方法-drawRect: & -displayLayer:
你可能已经知道在UIView的子类中可以通过实现-drawRect:方法来自定义绘图。根据apple的建议如果你不需要自定义绘图请不要实现一个空的该方法,以浪费不必要的系统开销。因为UIView一旦发现有-drawRect:方法实现,系统就会自动为他创建一个像素背景尺寸乘以contentScale大小的图像。
CALayer的自定义绘图则通过layer的delegate对象(CALayerDelegate 协议)的以下两个方法来实现
// 方法1
- (void)displayLayer:(CALayerCALayer *)layer;
// 方法2. 如果方法1没有实现,则调方法2
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;
和UIView的-drawRect: 不同的两点需要注意
3. Layer Geometry
UIView有3个最主要的布局属性:frame、bounds和center,CALayer也有相对应的属性:frame、bounds和position。
frame其实是layer在父layer中的坐标,bounds是自己内部的坐标(左上角为{0,0}原点),frame反映了layer在父layer中轴向坐标下占用的矩形区域。下图显示了一个经过旋转以后的矩形view或者layer的frame、bounds以及center和position。

center和position都是anchorPoint(锚点)相对于父layer的坐标。默认情况下,anchorPoint总是位于矩形区域的中间点,在view中不出现anchorPoint,意味着不能移动anchorPoint,所以view中名称就是center。而layer中anchorPoint可以移动,比如你可以把anchorPoint移动到当前layer的左上角{0,0}(anchorPoint用的也是unit坐标,默认中间点{0.5,0.5}),这时由于position保持不变,layer会相对父layer往右下角移动

坐标系统
CALayer提供若干工具方法用于转换相对于不同layer的坐标
- (CGPoint)convertPoint:(CGPoint)point fromLayer:(CALayer *)layer;
- (CGPoint)convertPoint:(CGPoint)point toLayer:(CALayer *)layer;
- (CGRect)convertRect:(CGRect)rect fromLayer:(CALayer *)layer;
- (CGRect)convertRect:(CGRect)rect toLayer:(CALayer *)layer;
Flipped Geometry
在iOS中采用的是左上角为原点的坐标,而Mac OS中采用的坐下家为原点的坐标。Core Animation通过geometryFlipped属性来支持上述两种坐标体系的转换。如果geometryFlipped属性为YES,则表示当前layer相对于父layer坐标进行几何垂直翻转。
The Z Axis
不同于UIView的二维坐标,CALayer通过zPosition和anchorPointZ两个float属性提供三维坐标空间。zPosition值越大越显示在上面。
Hit Testing
虽然CALayer不感知responder chain,但是他通过提供-containPoint:和-hitTest:两个方法来帮助你定位事件所在layer。
另外CALayer对自动布局缺乏支持,建议使用UIView。
4. Visual EFfects
Rounded Corners 圆角
CALayer有一个cornerRadius属性用来制作layer的圆角,值表示圆弧的半径,单位是上面提到过3种单位中的point,他可以被设置为任何的float数字(default=0)。默认情况下该属性只作用于backgroundColor,而对于sublayer和背景图都没有效果,但是如果配合masksToBounds=YES的设置,可以对layer内所有元素生效。
Layer Border 边框
CALayer通过borderWidth和borderColor两个属性的组合来定义border的宽度和颜色。
borderWidth用float的point度量定义了boder笔画的粗细。
borderColor的类型是CGColorRef,而不是UIColor;他同时会retain传给他的这个CGColorRef。
border位于bounds范围内,而且位于所有的子layer之上。不管内容是什么形状或者是否超出当前layer的bounds,border总是沿着bounds。
Drop Shadows 阴影
通过设置shadowOpacity属性为一个大于0的值,可以给任意layer添加一个背景阴影。取值范围是0.0到1.0,表示全透明到非透明。同时还可以通过shadowColor、shadowOffset和shadowRadius三个属性来修改阴影的外观。
shadowColor同borderColor和backgroundColor类似用于修改阴影的颜色,默认是黑色。
shadowOffset是一个CGSize类型,控制了阴影的距离和方向。默认值是{0,-3},是的默认值下阴影位于layer的上部,因为这个属性最早用于Mac OS,而Mac OS的坐标系和iOS是相反的。
shadowRadius控制阴影的模糊度,值越大越模糊。通常我们使用较大的值来制造一种比较大的视觉深度。
Shadow Clipping 阴影裁切
不同于border围绕在layer的bounds,shadow是围绕在真正的内容外。正是由于他围绕在真正内容的外面,如果他超过bounds时,会被masksToBounds给裁切掉,通常这不是我们想要的效果。
如果你既想要裁切多余的内容,同时又显示阴影,这时候你就需要两个重叠的layer,一个masksToBounds=YES,另外一个则=NO同时设置想要的阴影外观
shadowPath 阴影路径
由于需要结合所有子layer画出边界外的阴影,所以shadow方法其实会有性能上的隐患;shadowPath正是为了解决性能问题而推出的,可以给开发者手动设置阴影的路径。
shadowPath可以接受CGPathRef类型的值,你可以通过CGPath开头的很多CoreGraphic方法绘制自己想要的阴影图形。
Layer Masking 遮罩
mask属性接受另外一个CALayer值,可以被用作将当前layer裁切成给定layer的轮廓。
Scaling Filters 缩放方式
minificationFilter 用于指定缩小的方式
magnificationFilter 用于指定放大的方式
可用的值均是
kCAFilterLinear
kCAFilterNearest
kCAFilterTrilinear
Group Opacity
我们通常使用UIView的alpha属性来设置view的透明度,如果在CALayer中我们同样会使用opacity来设置,这2个属性同样会作用于他得所有subView和subLayer。
但是如果是一个有sublayer的control可能会出现下图这样的情况

这主要是由于helloword所在层也被设置了opacity(假设opacity=0.5),和后面的control的opacity叠加,所以你看到的中间部分其实是0.5*0.5+0.5=0.75。
通常这不是我们想要的效果,我们通常有2种方法来处理:
- 在app的Info.plist中添加UIViewGroupOpacity=YES。这会作用于当前app下得所有所有范围,可能还有点小的性能浪费。
-
通过CALayer的 shouldRasterize=YES,将layer及其子layer全部打散成一个平面的图形,再使Opacity生效就可以达到我们想要的效果了。由于rasterizationScale默认等于1.0,会使得高清屏幕下图片像素化,所以我们还要同时设置一下
layer.rasterizationScale = [[UIScreen mainScreen] scale];
UIView的transform属性类型是CGAffineTransform,他是用于对view进行二维空间下的旋转、拉伸或变形。CGAffineTransform其实是一个2列3行的矩阵,被用于乘以一个二维的行向量(当前情况下是CGPoint),来实现变换。但是实际上你会经常看到二维变换是一个3x3的矩阵。

仿射变换的限制是无论如何变换,变换之前平行的两条线,变换后仍然平行,比如下图前3个变换都是仿射变换,而最后一个就不是仿射变化,明显竖线上述条件。

Core Graphics提供创建仿射变化的快捷方法
// 这里的angle是弧度,通常使用的如:M_PI_4/M_PI
CGAffineTransformMakeRotation(CGFloat angle)
CGAffineTransformMakeScale(CGFloat sx, CGFloat sy)
CGAffineTransformMakeTranslation(CGFloat tx, CGFloat ty)
// 角度和弧度相互转换的宏
#define RADIANS_TO_DEGREES(x) ((x)/M_PI*180.0)
#define DEGREES_TO_RADIANS(x) ((x)/180.0*M_PI)
UIView可以通过transform属性来实现变换,其实他就是对CALayer的封装。
CALayer同时也有一个transform属性,不过他的类型不是CAAffineTransform,而是CATransform3D。CALayer实现仿射变化的属性是affineTransform。
Core Graphics同时提供了在一个transform上合并另外一个的方法。
CGAffineTransformRotate(CGAffineTransform t, CGFloat angle)
CGAffineTransformScale(CGAffineTransform t, CGFloat sx, CGFloat sy)
CGAffineTransformTranslate(CGAffineTransform t, CGFloat tx, CGFloat ty)
首先你需要创建一个恒等变换:
CGAffineTransformIdentity
使用下面方法组合两个不同的变换
CGAffineTransformConcat(CGAffineTransform t1, CGAffineTransform t2);
另外以顺序组合变换,前一个变换会影响到下一个变换,比如如下代码:
//create a new transform
CGAffineTransform transform = CGAffineTransformIdentity; //scale by 50%
transform = CGAffineTransformScale(transform, 0.5, 0.5); //rotate by 30 degrees
//translate by 200 points
transform = CGAffineTransformRotate(transform, M_PI / 180.0 * 30.0);
transform = CGAffineTransformTranslate(transform, 200, 0);
//apply transform to layer
self.layerView.layer.affineTransform = transform;
虽然官方没有提供这种变换的方法,但是我们仍然可以通过修改transform的a,b,c,d几个参数来实现
跟仿射变换的3x3矩阵不同,CATransform3D是一个4x4的矩阵,从而支持了对于垂直屏幕的z轴的变换效果。
CATransform3DMakeRotation(CGFloat angle, CGFloat x, CGFloat y, CGFloat z)
CATransform3DMakeScale(CGFloat sx, CGFloat sy, CGFloat sz)
CATransform3DMakeTranslation(Gloat tx, CGFloat ty, CGFloat tz)
和上面2D的方法有点类似,只是多了一个z的参数,另外旋转方法多了3个x,y,z的参数来表示旋转针对哪个轴。(大于0即可,如果同时有多个值大于0会怎么旋转,回头试试看 lol)

如上图所示,环绕z轴的旋转其实就是我们上面提到过的2D旋转,而环绕x,y轴的旋转则使得layer离开了屏幕所在的2维屏幕。
如果只是y轴的旋转,可能会让你看起来似乎只是对layer做了横向的压缩。其实他看起来不对的原因是没有perspective(透视)。
Perspective Projection
透视投影的意思是在真实世界中,我们看到的东西越远则越小。所以我们期望在靠近我们的边界应该比远离我们的边界更大一些。
虽然框架没有提供我们透视变换的方法,但是我们可以使用CATransform3D的m34属性来实现类似效果。m34属性通常被用来计算x和y值的缩放比例,默认值是0。我们一般设置属性m34的值等于 -1.0/d(d的含义是我们设想的镜头和屏幕的距离)。d的取值范围一般在500-1000之间会看起来比较自然,值越小透视效果越明显甚至可能会失真,越大透视效果越小甚至可能没有。
Layer Flatterning
在2维环境下对一个父Layer和子Layer分别做变换没有任何问题,但是在3维环境下就不是了。这里指的3维其实是指对z轴进行变化,而不是指用3d的变换方法。如果用3d的变换方法进行2d的变换,比如只对z做旋转也是没有问题的,但是对x和y轴做旋转可能就不是我们想要的那种效果了。
虽然Core Animation的layer是在3D空间中的,但其实他是一个伪3D。每个layer的3D视觉效果是由他得子layer创建出来的(视觉差的效果),比如你倾斜屏幕并不能真正绕过屏幕上面对你的那个layer。所以说每个layer的3D场景其实是扁平化的。
其实你很难通过Core Animation创建一个非常复杂真实的3D场景,但是有另外CALayer的子类提供了类似的解决方案,后面会提到他CATransformLayer。
6. Specialized Layers
CAShapeLayer
CAShapeLayer使用矢量图形代替了位图图像进行绘制,优点有:
- 速度快 - 硬件加速
- 高效使用内存 - 不需要像普通CALayer一样先创建一个背景图像,所以更节约内存
- 绘图不限于当前边界 - 默认可以绘制到bounds以外
- 无像素结构 - 拉伸或者做3D透视变换不会导致layer的像素化
终极版的圆角
使用UIBezierPath下面两个创建圆角的方法,再配合CAShapeLayer可以制作出符合各种需要的圆角layer。再配合layer的mask属性对layer做我们想要的裁切。
+ (UIBezierPath *)bezierPathWithRoundedRect:(CGRect)rect byRoundingCorners:(UIRectCorner)corners cornerRadii:(CGSize)cornerRadii
+ (UIBezierPath *)bezierPathWithRoundedRect:(CGRect)rect cornerRadius:(CGFloat)cornerRadius
CATextLayer
CATextLayer保留了UILabel的大部分显示文字用到的方法,并且还额外添加了一些更好的,同时渲染速度也优于UILabel。一个鲜为人知的事实是在iOS6及更早的版本,UILabel使用WebKit来渲染,这有显著的性能开销。而CATextLayer使用的是Core Text所以明显更快。
CATextLayer *textLayer = [CATextLayer layer];
textLayer.font = CGFontCreateWithFontName([[UIFont systemFontOfSize:15] fontName]);
textLayer.contentScale = [UIScreen mainScreen].scale;
textLayer.string = @"something to show";
string属性的类型是id,因为他同时接受NSString和NSAttributedString类型的值。
通过+(Class)layerClass方法来指定当前view生成的layer类型。
CATransformLayer不同于其他的layer,他不显示他自己的任何内容,而是用于承载子layer的变换内容,同时他不会对子layer进行扁平化,从而使得他可以对不同的子Layer运用两个完全独立的3D变换。
CAGradientLayer
两种颜色的渐变通过接受数组的colors属性,以及startPoint和endPoint定义方向。
//create gradient layer and add it to our container view
CAGradientLayer *gradientLayer = [CAGradientLayer layer];
gradientLayer.frame = self.containerView.bounds; [self.containerView.layer addSublayer:gradientLayer];
//set gradient colors
gradientLayer.colors = @[(__bridge id)[UIColor redColor].CGColor, (__bridge id)[UIColor blueColor].CGColor];
//set gradient start and end points
gradientLayer.startPoint = CGPointMake(0, 0); gradientLayer.endPoint = CGPointMake(1, 1);
多种颜色的变换,增加一个locations属性定义每个颜色的位置。
locations、startPoint、endPoint属性都采用Unit Point为单位。
CAReplicatorLayer
通过instanceCount设置复制的数量,传递CATransform3D给instanceTransform设置每个拷贝的变换(相对上一个拷贝的变换,而不是原始拷贝)。
CAReplicatorLayer *replicator = [CAReplicatorLayer layer];
replicator.instanceCount = 10;
//apply a transform for each instance
CATransform3D transform = CATransform3DIdentity;
transform = CATransform3DTranslate(transform, 0, 200, 0);
transform = CATransform3DRotate(transform, M_PI / 5.0, 0, 0, 1);
transform = CATransform3DTranslate(transform, 0, -200, 0);
replicator.instanceTransform = transform;
//apply a color shift for each instance
replicator.instanceBlueOffset = -0.1;
replicator.instanceGreenOffset = -0.1;
可以使用CAReplicatorLayer很方便的实现镜像的效果,例如:
//move reflection instance below original and flip vertically
CATransform3D transform = CATransform3DIdentity;
CGFloat verticalOffset = self.bounds.size.height + 2;
transform = CATransform3DTranslate(transform, 0, verticalOffset, 0);
transform = CATransform3DScale(transform, 1, -1, 0);
layer.instanceTransform = transform;
//reduce alpha of reflection layer
layer.instanceAlphaOffset = -0.6;
用于实现最基本的滚动视图效果,类似UIScrollView和UITableView。因为他比较简单所以没有类似的滚动条,不传递touch事件,以及其他许多特别的效果。
CATiledLayer
CATiledLayer用于解决加载太大的图片时的性能问题。
CAEmitterLayer
CAEmitterLayer诞生自iOS 5,他是一个展示高性能的例子效果引擎,例如烟雾、火、雨等等。
CAEmitterLayer是一个CAEmitterCell的容器,CAEmitterCell跟CALayer类似
- color会混合contents图像
- emissionRange属性代表粒子扩散效果的范围,如果是2π就是360°全方向。
- alphaSpeed=-0.4表示alpha值每秒衰减0.4,以及一个远离淡化的粒子效果。
还有其他很多属性有待研究
CAEAGLLayer
使用Open GL的方法来实现绘图功能
AVPlayerLayer
AVPlayerLayer其实不是Core Animation框架的,而是来自AVFoundation,用于在iOS中播放视频。他基于例如MPMoviePlayer的高级API实现,并提供了更底层对视频播放控制的方法。