Allen Chiang

iOS程序猿的blog

© 2014. All rights reserved.

Stringwithformat和sprintf方法性能比较

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的代码。

解决方案

  1. 可以把上述字符串替换的过程放到global线程中执行,等替换成功了再切换回来执行真正的绘图相关代码。

  2. 既然[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;

stringWithFormat

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真机上面执行上述程序的结果:

  • stringWithFormat 执行这1million的时间是7.025311秒

  • sprintf执行同样这1million的时间是0.326543秒

但从对比结果上来看sprintf的效率应该是stringWithFormat的20倍以上,看来以后对于大量的字符串拼装最好还是用sprintf方法。

以上测试代码可以在https://github.com/allenzerg001/TestHelloWorld下载

至于为什么是这样,因为还没有找到stringWithFormat方法的真正源码,还为从得知,有知晓的还望指教。

Text Kit & Core Text 文字排版

文字排版

通常我们使用UILabel、UITextField、UITextView在iOS上展示一些我们需要的文字。前者用于简单的展示,后两者可以用于接受用户的输入。通常情况下我们用上述3者展示简单的纯文本,如果我们需要展示图文混排或者稍微带一点排版样式的文字时,我们需要使用更底层的一些技术,比如Text Kit 或者 Core Text。

上图展示的iOS7的框架层次结构,可以看到基于Core Text的Text Kit为上述3个常用控件提供了技术支持。 首先我们需要准备一些关于字符的预备知识作为铺垫。

字符和字形(Character & Glyphs)

字符(Character)

字符从本质上讲是一个在特定字符集中定义的数字,比如我们在OS X中广泛使用的Unicode。Unicode标准为每一种我们可能会用到的字符,提供了唯一的数字标识。

字形(Glyphs)

字形是用于展示上述字符的一个图形形状。但是每个字符对应的字形,根据字体的不同、字体粗细/斜体定义的不一,并不是唯一的。 例如下图展示的字符“A”的多种字形 字形A

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

属性字符串(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:

读写属性

详细参考NSAttributedStringNSMutableAttributedString 的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。

  • text布局引擎 text布局引擎将字符生成字形,同时将字形转换成包含多行的frames,lines以及run。而且他还提供了字形和布局相关的数据,例如字形的定位和lines和frames的尺寸。

  • font API 通过NSFont和NSFontDescriptor将iOS开发者喜闻乐见的方法带给了Mac OSX的开发者们。他提供了字体的引用、字体的描述以及访问font数据结构的快捷方法。

  1. 首先通过已经创建好的CFAttributedStringRef(toll bridge to NSAttributedString),使用下面方法创建一个CTFramesetterRef CTFramesetterRef CTFramesetterCreateWithAttributedString( CFAttributedStringRef string );

  2. 通过CTFramesetterRef 和 指定的文字显示区域 CGPathRef 创建CTFrameRef。 根据上图所示,同一个CFAttributedStringRef 和 CGPathRef 可以产生一个或者多个的CTFrameRef,在此同时framesetter将段落的样式应用到CTFrameRef上,比如对齐方式,tab位置,行间距,缩进,断行方式等。

    CTFrameRef CTFramesetterCreateFrame( CTFramesetterRef framesetter, CFRange stringRange, CGPathRef path, CFDictionaryRef frameAttributes );

  3. 在CTFramesetterRef 生成 CTFrameRef的同时,也唤起了一个typesetter(排字,通常是CTTypesetterRef)。她的作用是将CFAttributedStringRef中的字符转换成对应的字形。

  4. 每个段落中(可以理解CTFrameRef代表一个段落)中包含了多行(CTLineRef)。

  5. 每一行中又包含了多个CTRunRef,是指一行中连续的一段包含同样属性和方向的文字。

自定义图文混排

通常情况下图文混排,还是需要开发者自己做一些事情。至于怎么做,这个就需要用到CTRunDelegate了。

  • CTRunDelegate

本身这是Core Text提供的一个用于自定义CTRun排版属性的回调方法,这里的排版属性包括字形的宽度,字形的向上高度,向下高度。

字形宽度可以理解,但是什么是向上高度和向下高度呢?这个请看下图:

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

我们可以通过以下方法来创建一个CTRunDelegateRef

CTRunDelegateRef CTRunDelegateCreate(const CTRunDelegateCallbacks* callbacks, void* refCon ) 
  • CTRunDelegateCallbacks

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(如果我理解的不对欢迎指出)

  1. 首先我们定义好自己的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;
    
  2. 之后我们把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来做。

  1. 获取CTRun对应的坐标范围

通过创建CTFrameSetterRef,并且获取嵌套的CTFrameRef,以及包含的CTLineRef,从而最终循环到对应的CTRunRef,再通过以下函数获取当前CTRun坐标范围内的相关属性,比如宽度、向上高度和向下高度。

// 后面3个参数ascent/descent/leading都是输出项
double CTRunGetTypographicBounds (   
	CTRunRef run,    
	CFRange range,   
	CGFloat *ascent,    
	CGFloat *descent,    
	CGFloat *leading 
); 
  1. 自定义绘图

上面获取的只是当前run的属性,真正绘图需要的是相对origin的坐标值,所以在循环CTLine和CTRun的时候,要记录下line和run的origin,并累加起来才是真正相对于坐标原点的偏移量。 有了坐标值后面就是真正的Core Graphic绘图了,限于篇幅不再展开去,具体可以参考Apple的官方文档:

参考文献

iOS.Core.Animation笔记三.Performance

第三部分,主要讲动画的性能

12. Tuning for speed

通常说CPU执行的是软件加速,GPU执行的是硬件加速。大多数的性能优化就是在这两者之间做选择,以避免任意一方过度消耗。

在iOS5及之前,我们知道有一个进程叫做SpringBoard,他其实是用来实现动画以及合成layer的独立进程。到iOS6及以后,负责这个任务的进程叫做BackBoard。我们一般也叫他Render Server。

animation通常的4个步骤

  1. Layout
  2. Display
  3. Prepare
  4. Commit

animation被提交到render server以后,还有2个步骤

  1. 计算所有layer的属性值并且建立OpenGL geometry进行渲染
  2. 渲染纹理到屏幕上

所以总共有6个步骤,前面5个由CPU实现,最后1个才有GPU实现。但是我们仍然可以在前面2个可以由我们控制的步骤中,抉择好哪些是CPU做前期,并传递给GPU继续完成。

GPU

可能会导致GPU慢的因素

  1. Too much geometry 太多的层次 虽然现在iOS的GPU可以绘制百万级别的三角形,但是准备过程中通过IPC传递到render server的数据,由于太多的layer会引起CPU的瓶颈。
  2. Too much overdraw 透支太多 透支主要是由重叠的半透明层引起的。
  3. Offscreen drawing 离屏绘制 离屏绘制的创建额外内存空间以及切换context都会引起GPU的额外消耗。使用某些特地的layer效果也会强制系统开启离屏预渲染,比如圆角,遮罩,阴影,rasterization。
  4. Too large images 太大尺寸的图片

CPU

可能会导致CPU慢的因素

  1. Layout calculations 复杂的layer层次结构,特别是iOS6以后提供的autolayout
  2. Lazy view loading 为了节约内存并减少系统启动时间,iOS只在view controller第一次展示在屏幕上的时候才被加载。如果展现在屏幕前需要太多的IO work就会导致不好的响应。
  3. Core Graphics drawing 如果实现了-drawRect:方法,或者是CALayerDelegate的-drawLayer:inContext:方法,你都创造了一个额外的性能开销。因为系统会自动创建一个同当前view size大小的image作为背景。
  4. Image decompression 虽然PNG和JPEG会比未压缩的bitmap小很多,但是当他被绘制到屏幕上前,仍然需要解压成完整的bitmap大小,通常是图片高x宽x4的bytes。

即使在layer被打包好并传递到了render server以后,cpu仍然需要计算每个可见的layer并转换成纹理三角形以供Open GL使用,因为GPU对Core Animation的结构式一无所知。

13. Efficient Drawing

使用正确的layer做正确的事,不仅速度比Core Graphic快很多,而且避免创建背景图像。

  • CAShapeLayer 多边形、线条、曲线
  • CATextLayer 文字
  • CAGradientLayer 渐变效果

Dirty Rectangles

重绘部分区域

// 指定需要重绘的矩形区域	
- (void)setNeedsDisplayInRect:(CGRect)invalidRect

// 并且在绘制的方法中进行判断是否在重绘的范围内
- drawRect:(CGRect)rect

iOS.Core.Animation笔记二.Motion

第二部分主要是关于动画的

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的任意属性被修改时:

  1. 调用自己的-actionForKey:方法,并且传入当前属性的名称;返回对象(CAAction协议对象)并执行,传入参数分别是属性名、layer,dict;
  2. layer检查是否有delegate实现了CALayerDelegate协议中定义的-actionForLayer:forKey方法,有的话执行并返回;
  3. 如果上面没有,则检查layer自己的actions有没有属性对应的CAAction;
  4. 如果上面也没有,则检查layer自己的style有没有属性对应的CAAction;
  5. 如果上面也没有,layer会调用-defaultActionForKey:方法,找到那些默认定义的属性及其action;

而在UIView中,每个UIView都是他layer的delegate,并且提供了-actionForLayer:forKey方法的实现。不在动画block中时(应该是指+beginAnimations:context和+commitAnimations之间),该方法永远返回ni,而如果在动画block中时则返回非nil。

  • UIView的layer不支持隐式动画,有几种方法让UIView的属性也支持动画:
    1. 使用UIView的动画方法
    2. 子类化UIView并且重载-actionForLayer:forKey:方法
    3. 创建一个显式动画
  • 自主Layer控制动画的方式:
    1. layer的delegate实现-actionForLayer:forKey:
    2. 提供actions

展现层和模型层

iOS中,屏幕每秒钟绘制60次。如果一个动画的持续时间超过60分之1秒,那么Core Animation在新值展现到屏幕之前的过程中,需要合成计算好几次。这意味着CALayer必须要保持一个用于展示的当前值。

这个用于展示的当前值被保存在一个叫做展现层(presentation layer)的地方,我们可以通过-presentationLayer方法访问。注意这个属性只有在layer被第一次绘制到屏幕上以后才被创建。

以下两种情况你需要用到这个展现层

  1. 你在实现一个基于时间轴的动画
  2. 你需要动画中的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

CAMediaTiming

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

CAMediaTimingFunction

// 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

————————————————————————————————————————

iOS.Core.Animation笔记一.Layer

image

Nick Lockwood的《iOS Core Animation》全书分为3大部分

  1. The Layer Beneath
  2. Setting Things in Motion
  3. 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不能做的事情:

  1. 阴影、圆角、多彩的border

  2. 3D变化和定位

  3. 非矩形的边界

  4. alpha遮罩

  5. 多步非线性动画


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坐标表示法。

image

如上图所示就比较清晰了,定义了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: 不同的两点需要注意

  • 开发者需要手动调用CALayer的-display:方法来触发layer的绘制

  • 通过CALayerDelegate自定义绘图方法绘制的图像不会超出边界,类似UIView的masksToBounds=YES的效果


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。

image

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往右下角移动

image

坐标系统

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可能会出现下图这样的情况

image

这主要是由于helloword所在层也被设置了opacity(假设opacity=0.5),和后面的control的opacity叠加,所以你看到的中间部分其实是0.5*0.5+0.5=0.75。

通常这不是我们想要的效果,我们通常有2种方法来处理:

  1. 在app的Info.plist中添加UIViewGroupOpacity=YES。这会作用于当前app下得所有所有范围,可能还有点小的性能浪费。
  2. 通过CALayer的 shouldRasterize=YES,将layer及其子layer全部打散成一个平面的图形,再使Opacity生效就可以达到我们想要的效果了。由于rasterizationScale默认等于1.0,会使得高清屏幕下图片像素化,所以我们还要同时设置一下

    layer.rasterizationScale = [[UIScreen mainScreen] scale];

5. Transforms

Affine Transforms 仿射变换

UIView的transform属性类型是CGAffineTransform,他是用于对view进行二维空间下的旋转、拉伸或变形。CGAffineTransform其实是一个2列3行的矩阵,被用于乘以一个二维的行向量(当前情况下是CGPoint),来实现变换。但是实际上你会经常看到二维变换是一个3x3的矩阵。

image

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

image

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。

Combining Transforms

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;

Shear Transform

虽然官方没有提供这种变换的方法,但是我们仍然可以通过修改transform的a,b,c,d几个参数来实现

3D Transforms

跟仿射变换的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)

image

如上图所示,环绕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使用矢量图形代替了位图图像进行绘制,优点有:

  1. 速度快 - 硬件加速
  2. 高效使用内存 - 不需要像普通CALayer一样先创建一个背景图像,所以更节约内存
  3. 绘图不限于当前边界 - 默认可以绘制到bounds以外
  4. 无像素结构 - 拉伸或者做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

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;

CASCrollLayer

用于实现最基本的滚动视图效果,类似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实现,并提供了更底层对视频播放控制的方法。