NSTimmer
方法一
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface IDLTimerProxy : NSObject

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;

@end

NS_ASSUME_NONNULL_END
#import "IDLTimerProxy.h"

@interface IDLTimerProxy()

@property(nonatomic, assign, readwrite) SEL selector;
@property(nonatomic, weak, readwrite) id target;
@property(nonatomic, weak, readwrite) NSTimer *timer;

@end

@implementation IDLTimerProxy

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo {
IDLTimerProxy *proxy = [IDLTimerProxy new];
proxy.target = aTarget;
proxy.selector = aSelector;
proxy.timer = [NSTimer scheduledTimerWithTimeInterval:ti target:proxy selector:@selector(performSelectorWithTimmer:) userInfo:userInfo repeats:yesOrNo];
return proxy.timer;
}

- (void)performSelectorWithTimmer:(NSTimer *)timmer {
if(self.target && [self.target respondsToSelector:self.selector]) {
[self.target performSelector:self.selector withObject:timmer.userInfo];
} else {
[self.timer invalidate];
self.timer = nil;
}
}
@end

调用方式:

self.timmer = [IDLTimerProxy scheduledTimerWithTimeInterval:5 target:self selector:@selector(run:) userInfo:nil repeats:YES];
方法二
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface IDLWeakTimerProxy : NSProxy

+ (instancetype)proxyWithTarget:(id)targe;

@end

NS_ASSUME_NONNULL_END
#import "IDLWeakTimerProxy.h"

@interface IDLWeakTimerProxy ()

@property(nonatomic, strong, readwrite) id target;

@end

@implementation IDLWeakTimerProxy

+ (instancetype)proxyWithTarget:(id)target {
return [[IDLWeakTimerProxy alloc] initWithTarget:target];
}

- (instancetype)initWithTarget:(id)target {
_target = target;
return self;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [self.target methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation {
if(!invocation) return;
SEL selector = invocation.selector;
if([self.target respondsToSelector:selector]) {
[invocation invokeWithTarget:self.target];
}
}

- (BOOL)respondsToSelector:(SEL)aSelector {
return [self.target respondsToSelector:aSelector];
}

@end

调用方式:

self.weakTimmerProxy = [IDLWeakTimerProxy proxyWithTarget:self];
self.timmer = [NSTimer scheduledTimerWithTimeInterval:3 target:self.weakTimmerProxy selector:@selector(run:) userInfo:nil repeats:YES];
方法三
@implementation NSTimer (IDLBlockTimer)

+ (NSTimer *)idl_scheduledTimerWithTimeInterval:(NSTimeInterval)interval block:(void (^)(void))block repeats:(BOOL)repeats {

return [self scheduledTimerWithTimeInterval:interval target:self selector:@selector(idl_blockSelector:) userInfo:[block copy] repeats:repeats];
}

+ (void)idl_blockSelector:(NSTimer *)timer {

void(^block)(void) = timer.userInfo;
if (block) {
block();
}
}
@end
dispatch_source_t
self.gcdTimmer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));
dispatch_source_set_timer(self.gcdTimmer, dispatch_time(DISPATCH_TIME_NOW, 0), 3 * NSEC_PER_SEC, 0);
dispatch_source_set_event_handler(self.gcdTimmer, ^{

});
dispatch_resume(self.gcdTimmer);
  • CADisplayLink 与 NSTimer 的区别:
  • CADisplayLink是一个能让我们以和屏幕刷新率同步的频率将特定的内容绘制到屏幕上的定时器类。CADisplayLink以特定模式注册到runloop后,每当屏幕显示内容刷新结束的时候,runloop就会向CADisplayLink指定的target发送一次指定的selector消息,CADisplayLink类对应的selector就会被调用一次。

  • NSTimer以指定的模式注册到runloop后,每当设定的周期时间到达后,runloop会向指定的target发送一次指定的selector消息。

  • 对于精度而言

由于iOS设备的屏幕刷新频率是固定的,CADisplayLink在正常情况下精确度相当高。NSTimer的精确度就显得低了点,比如NSTimer的触发时间到的时候,runloop如果在忙于别的调用,触发时间就会推迟到下一个runloop周期。NSTimer新增了tolerance属性,让用户可以设置可以容忍的触发的时间范围。

  • 对于使用场景而言

CADisplayLink使用场合相对专一,适合做界面的不停重绘。而NSTimer的使用范围要广泛的多,各种需要单次或者循环定时处理的任务都可以使用。

  • 例子:

    self.weakTimmerProxy = [IDLWeakTimerProxy proxyWithTarget:self];
    self.cadisplaylink = [CADisplayLink displayLinkWithTarget:self.weakTimmerProxy selector:@selector(run:)];
    [self.cadisplaylink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
    self.cadisplaylink.paused = YES;
  • CADisplayLink 接口:

@interface CADisplayLink : NSObject
{
@private
void *_impl;
}

//每秒多少帧
@property(nonatomic) NSInteger preferredFramesPerSecond
API_AVAILABLE(ios(10.0), watchos(3.0), tvos(10.0));

// 创建 display link
+ (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel;

//将display link 添加到 runloop 除非停止,否则在每个vsync信号到来的时候定时调用selector
- (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode;

//将display link 从runloop中移除
- (void)removeFromRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode;

// 将display link 从所有的runloop modes中移除,并且释放target 对象。
- (void)invalidate;

// 暂停display link
@property(getter=isPaused, nonatomic) BOOL paused;

@property(readonly, nonatomic) CFTimeInterval timestamp; //表示屏幕显示的上一帧的时间戳
@property(readonly, nonatomic) CFTimeInterval duration; //两帧间隔时间 默认为16.6ms
@property(readonly, nonatomic) CFTimeInterval targetTimestamp
API_AVAILABLE(ios(10.0), watchos(3.0), tvos(10.0));
@end

1. KeyChain 简要概述

KeyChain的实质是一个安全的数据库(数据库位于/private/var/Keychains/keychain-2.db),里面大部分数据都是加密的, 我们可以用KeyChain保存一些私密信息,比如密码、证书、设备唯一码(UDID)等等,总的来说KeyChain有如下特点:

  1. 安全
  2. Keychain的信息是存在于每个应用沙盒之外由系统负责管理,因此不会因App删除而丢失,在重装App后,Keychain里的数据还能使用,除非将系统恢复出厂设置。
  3. 一般而言不同App之间Keychain不能相互访问,但是从iOS 3 开始可以通过设置共有钥匙串部分信息。可以利用这一特性在自家的应用上共享KeyChain信息。
2. KeyChain的结构

每一个KeyChain由多个KeyChain item组成,KeyChain item的结构类似字典,同时每条KeyChain Item还包含一条data和多个attributes组成。
其中苹果提供了下面几种类型的keychain item,并且对不同类型的item做了不同的处理,比如password和key类的item就会做加密,而certificates类的就不会。

  • kSecClass

KeyChain Item 的类别,可以是下面几项:

extern CFTypeRef kSecClassGenericPassword   //通用密码项
extern CFTypeRef kSecClassInternetPassword //互联网密码项
extern CFTypeRef kSecClassCertificate //证书项
extern CFTypeRef kSecClassKey //key项
extern CFTypeRef kSecClassIdentity //认证项

系统指定的这些item都有特定需要配置的属性,这些属性是可选的不一定都需要给定,详细的可以查看SecItem.h文件,下面仅仅列出kSecClassGenericPassword的Attribute.

kSecClassGenericPassword item attributes:
kSecAttrAccess (OS X only)
kSecAttrAccessControl
kSecAttrAccessGroup (iOS; also OS X if kSecAttrSynchronizable specified)
kSecAttrAccessible (iOS; also OS X if kSecAttrSynchronizable specified)
kSecAttrCreationDate
kSecAttrModificationDate
kSecAttrDescription
kSecAttrComment
kSecAttrCreator
kSecAttrType
kSecAttrLabel
kSecAttrIsInvisible
kSecAttrIsNegative
kSecAttrAccount
kSecAttrService
kSecAttrGeneric
kSecAttrSynchronizable

需要注意的kSecClassIdentity item 由于是私有key和证书的结合,因此它的Attribute是二者的合集。

其中还有个比较重要的Attribute是kSecAttrAccessible,在我们调用SecItemCopyMatching方法返回item数据的时候,如果权限不够就会抛出errSecInteractionNotAllowed的错误。

kSecAttrAccessibleWhenUnlocked     当前的Item只有设备处于解锁状态才能被访问,这个适用于在前台访问的Item.
kSecAttrAccessibleAfterFirstUnlock 当前的Item只有设备重启并解锁后才能被访问,这个适用于需要在后台访问的Item.
kSecAttrAccessibleAlways 不论是否解锁库,都能访问改item,这种类型极为不安全,不推荐使用。

上面这些类型在加密备份的时候都会同步到新的机器上。

kSecAttrAccessibleWhenUnlockedThisDeviceOnly
kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
kSecAttrAccessibleAlwaysThisDeviceOnly

上面这些多出了ThisDeviceOnly,这个表明这些Item在加密备份后,会被以硬件相关的密钥(key)加密。并且不会随着备份移动至其他设备。

一般而言最好不要将Item设置为kSecAttrAccessibleAlways,并且钥匙串可以通过iTunes或iCloud同步的方式同步到其他设备,如果你保存的数据高度敏感,则需要使用后缀为ThisDeviceOnly的选项。

Keychain 从 iOS 7.0 开始也支持iCloud备份。把kSecAttrSynchronizable属性设置为@YES,这样后Keychain就能被iCloud备份并且跨设备分享。

3. 在项目中引入KeyChain的方案:

不论使用下面哪种方式在使用之前都必须往项目中导入Security.framework框架

对于KeyChain 的封装较少目前网上用得较多的有如下三种方式:

  1. 使用官方推出的 KeychainItemWrapper

使用KeychainItemWrapper保存数据:

KeychainItemWrapper *wrapper = [[KeychainItemWrapper alloc] initWithIdentifier:@"idealists" accessGroup:nil];
NSString *username = @"linxiaohai";
NSString *password = @"123";
[wrapper setObject:username forKey:(id)kSecAttrAccount];
[wrapper setObject:password forKey:(id)kSecValueData];

在初始化KeychainItemWrapper会用到两个参数:

  • Identifier: 我们从keychain中取数据的时候会用到
  • accessGroup: 如果想要在应用之间共享信息,那么需要指定访问组KeyChain Access Group.如果不需要共享则传nil
  • (void)setObject:(id)inObject forKey:(id)key;
    这里的key必须是Security.framework 里头文件“SecItem.h”里定义好的key,用其他字符串做key程序会崩溃

使用KeychainItemWrapper获取数据:

KeychainItemWrapper *wrapper = [[KeychainItemWrapper alloc] initWithIdentifier:@"idealists" accessGroup:nil];
username = [wrapper objectForKey:(id)kSecAttrAccount];
password = [wrapper objectForKey:(id)kSecValueData];

这里需要注意的是identifier和accessGroup必须要对应上。

  1. 第三方封装SAMKeychain,SSKeychain
    这两个三方库其实是同一个实现,只不过在iOS 10中有个私有系统类也叫SSKeychain,所以如果在iOS 10中使用会有不兼容的问题,可以查看对应的issue

    这个是star 比较多的一个开源项目,接口也十分简单。

  2. 通过Security.framework框架使用
    不论是KeychainItemWrapper还是SAMKeychain都是对Security的封装,Security.framework提供了如下的API供我们使用:

    SecItemAdd 添加一个keychain item
    SecItemUpdate 修改一个keychain item
    SecItemCopyMatching 搜索一个keychain item
    SecItemDelete 删除一个keychain item
4. 使用KeyChain Access Group 实现不同APP共享Keychain中的数据

从iOS 3.0 之后,不同应用之间可以共享KeyChain 数据了。如果我们的产品线有一系列的应用并且这些应用之间需要共享一些公共的账号信息,就可以通过共享KeyChain来实现。但是这是有严格限制的,只有拥有相同 App ID 前缀的应用才有可能共享 keychain。并且各应用存储的 keychain item 都需要标记了相同的 kSecAccessGroup 字段值。

App ID 是由两个部分组成:

<Bundle Seed ID> . <Bundle  Identifier>

Bundle Seed ID(Team ID) 是由苹果公司在第一次创建一个App ID生成的,是一个唯一的10个字符组成的字符串。Bundle Identifier这个就是我们应用的bundle id。
比如:

659823F3DC53.com.example.amazingApp

659823F3DC53 是我们的Team ID。一个开发者账号可以有几个不同的Team ID。也就是说要共享数据必须要求使用同一个Team ID。

比如我们的两个应用App ID如下:

ABC1234DEF.com.useyourloaf.amazingApp1
ABC1234DEF.com.useyourloaf.amazingApp2

我们可以定义一个共享的KeyChain Access Group

ABC1234DEF.amazingAppFamily
  1. Project-> Capebilities-> Keychain Sharing ,将Keychain Sharing打开,在新版的Xcode中,将Keychain Sharing打开后,会在项目对应的目录下自动生成对应的Entitlements文件,在Entitlements文件的KeyChain Access Group节点中添加KeyChain Access Group名字。ABC1234DEF.amazingAppFamily

  1. 在 Project-> Build Setting -> Code Signing Entitlements 中添加上一个步骤生成的Entitlements文件。

  2. 在存储数据的时候指定kSecAttrAccessGroup 为 ABC1234DEF.amazingAppFamily

5. 使用keychain需要注意的问题
  • 当我们没有打开Keychain Access Group,并且没有entitlement文件时,KeyChain默认以bundle id为Group。如果我们在版本更新的时候改变了bundle id,那么新版本就访问不了旧版本的KeyChain信息了。解决办法是从一开始我们就打开KeychainSharing,添加Keychain Access Group,并且指定每条keychain Item的group,私有的信息就指定app的bundle id为它的Group。
  • 代码内Access group名称一定要有AppIdentifierPrefix前缀。
  • Keychain是基于数据库存储,不允许添加重复的条目。所以每条item都必须指定对应的唯一标识符也就是那些主要的key,如果Key指定不正确,可能会出现添加后查找不到的问题。
  • kSecAttrSynchronizable也会作为主要的key之一。它的value值默认为No,如果之前添加的item此条属性为YES,在搜索,更新,删除的时候必须添加此条属性才能查找到之前添加的item。
  • KeyChain item字典内添加自定义key时会出现参数不合法的错误,所以要注意对传入的参数进行校验。
6. Keychain的安全性

Keychain内部的数据会自动加密。如果设备没有越狱并且不暴力破解,Keychain确实很安全。但是越狱后的设备,Keychain就很危险了,结合Keychain Dumper等工具,很容易拿到Keychain数据,网上也有较多现成攻略,

6. KeyChain的进一步封装

SAMKeychain 是一个很不错的项目,但是它只提供密码形式的存储,这里向借鉴SAMKeychain对KeyChain的核心功能的封装以及Masory配置属性的方式(主要是克服使用字典方式传递参数的时候,如果key不是指定的会崩溃的问题)

主要分成四层:

  1. KeyChainAttibuteMaker:将kSecClass传递进去KeyChainAttibuteMaker根据kSecClass,新建出对应的KeyChainItemAttribute的子项,比如传入kSecClassGenericPassword会新建一个GenericPasswordAttribute,并通过Block传出,供外面对属性进行配置。

  2. KeyChainAttibuteMaker会对配置完的KeyChainItemAttribute对象进行校验,对于非nil类型的属性,根据属性名,获取Keychain Attribute 的key字符串,构建出NSDirectionary对象传递出来。

  3. NMKeyChainTool 提供顶层的封装供外部开发者调用,它主要完成两项任务: KeyChainItemAttribute 的构建,KeyChainItemAttribute转NSDirectionary 调用NMKeyChainItemQuery操作KeyChain

  4. NMKeyChainItemQuery 是比较纯粹的增删改查的操作。调用的是Security.framework的SecItemAdd,SecItemUpdate,SecItemCopyMatching,SecItemDelete对KeyChain进行操作。

开篇叨叨

iOS动画简单说就是在一段时间内CALayer的Animatable Property发生了变化,一个完整动画包括时间成份(时长,时间变化曲线,动画速度),动画内容(哪些属性发生变化),属性变化范围:(fromValue,toValue),iOS的动画是基于Core Animation的,Core Animation将大部分实际的绘图任务交给了图形硬件GPU来处理,GPU会加速图形渲染的速度。这种加速技术让动画拥有更高的帧率并且显示效果更加平滑,不会加重CPU的负担而影响程序的运行速度。一般我们在项目中除了十分复杂或者比较简单的动画使用CAAnimation外,一般都是使用三方的动画库,比如JHChainableAnimationsLSAnimator,这两种都是支持Objective C 和 Swift, 并且是通过链式调用,用起来还是十分方便的,对了还有Facebook 的 POP,但是对于复杂的动画还是部分需要自己来封装。还有一类帧动画我们一般用lottie-ios,只要设计提供个json文件和资源文件就可以完成十分酷炫的动画。

这篇博客将从如下几个方面对iOS的动画做个总结:

  • CAAnimation的继承结构
  • 动画相关的CALayer
  • 时间系统
  • 动画事务管理
CAAnimation的继承结构

要了解iOS动画最重要的是对整个动画的继承结构,下图是CAAnimation的继承结构图:

CAAnimation

CAAnimation 这个是基类,它不是为了创建对象而存在的,它主要用于存放一些通用性的属性和方法,它有两个很重要的属性:timingFunction,delegate。

  • timingFunction 表示的是时间曲线,我们知道并不是所有的动画都是线性变化的,它们可能先快后慢,或者先慢后快,这就是由timingFunction决定的。timingFunction 是 CAMediaTimingFunction类型,它的可能值如下:
kCAMediaTimingFunctionLinear
kCAMediaTimingFunctionEaseIn
kCAMediaTimingFunctionEaseOut
kCAMediaTimingFunctionEaseInEaseOut
kCAMediaTimingFunctionDefault
  • delegate 这个是CAAnimationDelegate类型的代理。CAAnimationDelegate很简单,它只有两个方法:
- (void)animationDidStart:(CAAnimation *)anim;  
//动画开始
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag;
//动画停止,这种停止可以是因为动画结束,也有可能是因为动画从它的载体上移除,这里可以通过flag来区分。

同时CAAnimation遵循CAMediaTiming协议,这里包含了动画的很多基本属性:

duration:动画播放一次所用的时间
beginTime:动画起始时间
可以使用它来达到延迟执行动画的效果 beginTime = CACurrentMediaTime()+1;
speed:
如果把动画的duration设置为3秒,而speed设置为2,动画将会在1.5秒结束,并且动画速度是有层级关系的:一个动画的speed1.5,它同时是一个speed2的动画组的一个动画成员,则它将以3倍速度被执行。
timeOffset:
这个属性往往会结合其他属性类来控制动画的“当前时间”,下面是暂停动画的代码,在开发中十分实用,因为动画要么开始要么移除,但是如果只是暂停,后续还要继续就可以通过下面方法来实现。

-(void)pauseLayer:(CALayer*)layer {
CFTimeInterval pausedTime = [layer convertTime:CACurrentMediaTime() fromLayer:nil];
layer.speed = 0.0;
layer.timeOffset = pausedTime;
}

-(void)resumeLayer:(CALayer*)layer {
CFTimeInterval pausedTime = [layer timeOffset];
layer.speed = 1.0;
layer.timeOffset = 0.0;
layer.beginTime = 0.0;
CFTimeInterval timeSincePause = [layer convertTime:CACurrentMediaTime() fromLayer:nil] - pausedTime;
layer.beginTime = timeSincePause;
}

这里简单得对上面的动画暂停恢复方法进行解释下:

在CAMediaTiming协议的timeOffset的注释上,官方给出以下公式:

t = (tp - begin) * speed + offset

tp是父layer的时间点,为了方便理解,可以认为是绝对时间。
暂停的时候speed等于0,t = offset.要让t停在此刻,也就是让t = [layer convertTime:CACurrentMediaTime() fromLayer:nil]。这时候offset就必须等于[layer convertTime:CACurrentMediaTime() fromLayer:nil]

我们再来看下恢复,恢复的时候speed = 1,offset = 0, t = 上一次停留时间也就是t = (tp - begin) = pausedTime;
所以begin = [layer convertTime:CACurrentMediaTime() fromLayer:nil] - pauseTime;

repeatCount/repeatDuration:动画的重复执行,二者不可同时使用
repeatCount指定重复的次数
repeatDuration指定重复执行持续的时间,一到时间就停止重复执行。
autoreverses:是否自动翻转动画
将使动画先正常走,完了以后反着从结束值回到起始值,如果指定了autoreverse = YES 那么完成一次autoreverse就需要 2*duration
fillMode : 动画填充模式

fillMode的作用就是决定当前对象过了非Active时间段的行为. 比如动画开始之前,动画结束之后,如果要让动画在开始之前显示fromValue的状态,设置fillMode为kCAFillModeBackwards。如果想让动画结束后停留在toValue的状态,就应该设置为kCAFillModeForwards。如果两种都要有,就设置kCAFillModeBoth。注意必须配合animation.removeOnCompletion = NO才能达到以上效果

CALayer遵循CAMediaTiming协议,每个CALayer都有个时间系统,可以通过CACurrentMediaTime() 来获得当前时间,可以通过CALayer的

- (CFTimeInterval)convertTime:(CFTimeInterval)t fromLayer:(CALayer *)l;
- (CFTimeInterval)convertTime:(CFTimeInterval)t toLayer:(CALayer *)l;

在两个不同CALayer中进行转换。

下面是介绍这些属性的一个效果图,可以看这些参数来熟悉这些概念。

CAPropertyAnimation

CAPropertyAnimation 是属性动画,它其实也还是一个基类,不能用于创建对象,它有一个很重要的属性keyPath,用于指定哪些属性可以用于动画控制,那么我们怎么知道有哪些属性有属性动画呢?可以打开CALayer.h 搜索 “Animatable” 关键字,这些属性都是具有属性动画:将其罗列如下所示:

transform.scale     
transform.scale.x
transform.scale.y
transform.rotation
transform.rotation.x
transform.rotation.y
transform.rotation.z
transform.translation
transform.translation.x
transform.translation.y
transform.translation.z
opacity
margin
zPosition
backgroundColor
cornerRadius
borderWidth
bounds
contents
contentsRect
cornerRadius
frame
hidden
mask
masksToBounds
opacity
position
shadowColor
shadowOffset
shadowOpacity
shadowRadius

它还有两个很关键的属性:additive/cumulative

  • additive为YES时,变化值整体加上layer的当前值,也即是它其实是加性属性。
  • cumulative 为YES时,每次的值要加上上一次循环的的结束值。需要repeatCount>1的时候才能看出效果。
CAAnimationGroup

CAAnimationGroup 可以保存一组动画对象,将CAAnimationGroup对象加入层后,组中所有动画对象可以同时并发运行,可以通过设置动画对象的beginTime属性来更改动画的开始时间

CATransition

CATransition用于做转场动画,也就是layer的两种状态之间的过渡。能够为层提供移出屏幕和移入屏幕的动画效果:
CATransition有如下关键属性:

type:动画过渡类型
subtype:动画过渡方向
startProgress:动画起点(在整体动画的百分比)
endProgress:动画终点(在整体动画的百分比)

type的值可以是如下枚举值:

kCATransitionFade
kCATransitionMoveIn
kCATransitionPush
kCATransitionReveal

subtypes的值可以是如下枚举值:

kCATransitionFromRight
kCATransitionFromLeft
kCATransitionFromTop
kCATransitionFromBottom

各个属性的说明如下图所示:

下面是一个简单的例子:

CATransition *animation = [CATransition animation];
animation.type = kCATransitionPush;//设置动画的类型
animation.subtype = kCATransitionFromRight; //设置动画的方向
animation.duration = 1.0f;
[testView.layer addAnimation:animation forKey:@"pushAnimation"];
CABasicAnimation

CABasicAnimation:基础动画,通过属性修改进行动画参数控制,它有三个关键属性fromValue和toValue,还有byValue,当然不要忘记了它也是CAPropertyAnimation,在CAPropertyAnimation中可以指定要变化的keyPath.到这里为止动画内容(keyPath)有了,动画时间(duration和timingFunction)有了,开始和结束状态(fromValue和toValue)有了。通过插值就可以得到任意一个时间点的状态,然后渲染绘制形成一系列关联的图像,形成动画,也就是说到CABasicAnimation为止已经可以创建出一个动画了。

CAKeyframeAnimation

如果说CABasicAnimation是一帧动画的话,CAKeyframeAnimation就是多个CABasicAnimation组成的帧动画,我们知道动画其实就是一帧帧画面连续变化得到的,我们不可能提供无限的连续的动画帧,我们只需要提供必要的关键帧就可以通过插值来完成了,再加上人眼的视觉暂留效应就可以在大脑中留下连续运动的动画。CAKeyframeAnimation有两个最为关键的属性values和keyTimes,values就是各个关键帧的数据,keyTimes是各个关键帧的时间点。keyTimes这个可选参数,当keyTimes没有设置的时候,各个关键帧的时间是均分的。

除了values和keyTimes属性 CAKeyframeAnimation 还有个path属性也很关键,为什么需要这个属性,大家试想下,如果我们要实现一个沿着心型❤️轨迹运动的动画,那么我们要怎么获得keyTimes和values?为了高度拟合轨迹我们要做很多计算才能得到,这显然不是正确的做法,API应该是以简洁易用为目的,所以这时候就可以通过path来描述运动轨迹。这个值默认是nil当其被设定的时候values属性就会被覆盖.

还有比较重要的属性就是calculationMode和rotationMode

calculationMode 影响着关键帧之间的数据如何进行推算:

kCAAnimationLinear          通过线性插值
kCAAnimationDiscrete 不进行插值,只显示关键帧的画面,看到的动画会是跳跃的
kCAAnimationPaced 这个也是线性插值,但跟第一个的区别是它是整体考虑的。它会忽略掉keyTimes属性,重新计算keyTimes以达到全局匀速的效果。注意这时候keyTimes和timingFunctions是不起作用的;
kCAAnimationCubic 效果就是把转折点变得圆滑
kCAAnimationCubicPaced kCAAnimationPaced和kCAAnimationCubic两种效果叠加

rotationMode只有帧动画使用path路径的时候才有效果的,当值为kCAAnimationRotateAuto是,会把layer旋转,使得layer自身的x轴是跟路径相切的,并且x轴方向跟运动方向一致,使用kCAAnimationRotateAutoReverse也是相切,但x轴方向跟运动方向相反。

CASpringAnimation

CASpringAnimation是iOS7.0后新增的,它提供了像弹簧一样的变化规律,它有如下关键的属性:

mass            弹簧质量,影响弹簧的惯性,质量越大,弹簧惯性越大,运动的幅度越大
stiffness 弹簧弹性系数,弹性系数越大,弹簧的运动越快
damping 弹簧阻尼系数,阻尼系数越大,弹簧的停止越快
initialVelocity 初始速率,弹簧动画的初始速度大小,弹簧运动的初始方向与初始速率的正负一致
CoreAnimation的使用步骤
  • 创建CAAnmation子对象
  • 设置CAAnmation的属性
  • 调用CALayer的addAnimation:forKey:将CAAnimation对象添加到CALayer上,就能执行动画
  • 调用CALayer的removeAnimationForKey方法可以停止CALayer中的动画。
CGAffineTransform 仿射变换

在最初开发Android 的时候接触到仿射变换,一直不理解什么是仿射,其实的仿射变换就是将视图的每个点乘以一个仿射矩阵,得到一个变换后的视图,具体变换过程这里不做展开,这里会涉及到数学的矩阵运算,但是要记住一点:图层中平行的两条线在变换之后任然会保持平行。

可以通过如下方法创建CGAffineTransform:

CGAffineTransformMakeRotation(CGFloat angle)             //旋转变换
CGAffineTransformMakeScale(CGFloat sx, CGFloat sy) //缩放变换
CGAffineTransformMakeTranslation(CGFloat tx, CGFloat ty) //平移变换

下面是在一个仿射变换基础上叠加另一个仿射变换的方法。


CGAffineTransformRotate(CGAffineTransform t, CGFloat angle)
CGAffineTransformScale(CGAffineTransform t, CGFloat sx, CGFloat sy)
CGAffineTransformTranslate(CGAffineTransform t, CGFloat tx, CGFloat ty)

如果已经存在了两个仿射变换,现在要将它们合在一起的时候可以调用:

CGAffineTransformConcat(CGAffineTransform t1, CGAffineTransform t2);

如果需要一个什么都不变的变换,就可以实用

CGAffineTransformIdentity

下面是一个简单的例子:

CGAffineTransform transform = CGAffineTransformIdentity; 
transform = CGAffineTransformScale(transform, 2, 2);
transform = CGAffineTransformRotate(transform, M_PI / 180.0 * 45.0);
transform = CGAffineTransformTranslate(transform, 250, 0);
self.layerView.layer.affineTransform = transform;

UIView可以通过设置transform属性做变换。需要注意的是CALayer同样也有一个transform属性,但它的类型是CATransform3D,不要被误导了,真正用于仿射变换的是affineTransform属性。

CATransform3D 3D变换

和2D仿射变换类似CATransform3D也提供了对应的旋转,缩放,平移的方法,如下所示:

CATransform3DMakeRotation(CGFloat angle, CGFloat x, CGFloat y, CGFloat z)
CATransform3DMakeScale(CGFloat sx, CGFloat sy, CGFloat sz)
CATransform3DMakeTranslation(Gloat tx, CGFloat ty, CGFloat tz)

透视投影

上面的变换是没有透视效果的,原先平行的还是保持平行,因此我们需要做透视投影处理,要达到透视效果可以通过设置CATransform3D的m34值来实现,那么m34要怎么设置呢?


一般而言 m34 = -1.0 / distance (distance视角相机和屏幕之间的距离,以像素为单位,一般不需要仔细计算,根据实际效果500-1000中选择一个就好)

CATransform3D transform = CATransform3DIdentity;
transform.m34 = - 1.0 / 500.0;
transform = CATransform3DRotate(transform, M_PI_4, 0, 1, 0);
self.layerView.layer.transform = transform;

对于一个View,Core Animation定义消失点位于View的anchorPoint,但是这里会有个问题:一个界面可能有多个View要做3D变换,但是一个界面通常只能有一个消失点,所以不能通过设置position来移动,因为当改变一个图层的position,你也改变了它的消失点,因此在做3D变换的时候需要记住:当视图通过调整m34来让它更加有3D效果,应该首先把它放置于屏幕中央,然后通过平移来把它移动到指定位置,而不是直接改变它的position,这样所有的3D图层都共享一个消失点。还有一种方式就是通过sublayerTransform,sublayerTransform和对一个图层的变换不同,它影响到所有的子图层。这意味着你可以一次性对包含这些图层的容器做变换,因此我们可以把消失点设置在容器图层的中点,这样就不需要再对子图层分别设置了。这意味着你可以随意使用position和frame来放置子图层,而不需要把它们放置在屏幕中点,然后为了保证统一的消失点用变换来做平移。

CATransform3D perspective = CATransform3DIdentity;
perspective.m34 = - 1.0 / 500.0;
self.containerView.layer.sublayerTransform = perspective;
CATransform3D transform1 = CATransform3DMakeRotation(M_PI_4, 0, 1, 0);
self.layerView1.layer.transform = transform1;
CATransform3D transform2 = CATransform3DMakeRotation(-M_PI_4, 0, 1, 0);
self.layerView2.layer.transform = transform2;
动画的事务管理

事务这个概念在很多地方都会遇到,比如数据库操作等,在iOS动画中事务实际上是Core Animation用来包含一系列属性动画集合的机制,任何CALayer的Animatable Properties设置都应该属于某个CATransaction(在修改CALayer的Animatable Properties时如果发现当前没有事务,则会自动创建一个事务),在一个CATransaction中可以同时对多个layer的属性进行修改,CATransaction负责对layer的修改的捕获和提交,在事务中的变化并不会立刻生效,而是在事务提交的时候将这些图层树的变化成批包装起来,一次性发送到渲染服务进程,在我们看来就是图层的各个属性会在同一时刻由一个动画过渡到新值。Core Animation在每个Runloop周期中自动开始一次新的事务,事务的提交发生在RunLoop进入休眠或者退出期间,即使不显式的用[CATransaction begin]开始一次事务,任何在一次Runloop循环中属性的改变都会被集中起来,然后做一次0.25秒的动画。事务可以嵌套,当事务嵌套时候,只有当最外层的事务commit了之后,整个动画才开始.在没有RunLoop的地方设置CALayer的Animatable Properties,则必须使用显式的事务,有RunLoop的情况下会自动创建CATransation,CATransaction没有属性或者实例方法,并且也不能用+alloc和-init方法创建它。但是可以用+begin和+commit分别来入栈或者出栈,如下所示:

[CATransaction begin];
//动画内容
[CATransaction commit];

UIView动画中:+beginAnimations:context:和+commitAnimations ,以及UIView基于Block的动画方法:+animateWithDuration:animations:也都是基于CATransaction的封装。

动画相关的CALayer

在介绍iOS渲染的时候已经对iOS UIView以及CALayer做了较为详细的介绍,UIView 和 CALayer职责十分明确,一个是负责事件响应,一个是负责界面呈现,在介绍iOS渲染的时候主要关注的是CALayer内容显示的部分,而这里将会重点介绍CALayer的动画特性,其实给View加上动画,本质上是对其CALayer进行操作,CALayer有很多Animatable Property,我们可以基于这些属性做动画效果。

在介绍完CALayer后我们还需要了解一些特殊的CALayer,和CALayer基于Core Graphic的CPU渲染方式不同,它们大多数是基于GPU渲染的,下面是这些Layer的特点和作用:

  • CAShapeLayer
    CAShapeLayer是一个通过矢量图形而不是Bitmap来绘制的CALayer子类.
    CAShapeLayer相对于一般的CALayer有如下特点:

    1. CAShapeLayer使用了硬件加速,而CALayer是基于Core Graphics使用的是CPU绘图,因此渲染速度会快很多。
    2. CAShapeLayer不需要像普通CALayer一样创建一个寄宿图形,所以无论有多大,都不会占用太多的内存.
  • CATextLayer
    UILabel最早其实是通过WebKit来实现绘制的,这样就造成了当有很多文字的时候就会有极大的性能压力。而CATextLayer使用了Core Text,所以渲染性能十分快速。

  • CAGradientLayer
    CAGradientLayer是用来生成两种或更多颜色平滑渐变的图层,CAGradientLayer也是基于硬件加速对因此渲染效率也比基于Core Graphic 快很多。

  • CAGradientLayer
    CAGradientLayer 用于高效生成许多相似的图层。它会绘制一个或多个图层的子图层,并在每个复制体上应用不同的变换。

  • CAScrollLayer
    CAScrollLayer用于显示的是可滚动的图层的一部分,可以指定滑动方向和可视区域面积,限制不滑出区域外。

  • CATiledLayer
    有时候我们需要呈现一个很大的,质量很高的图片,这种情况下如果将整个图片加载到内存是不大现实的,一来会占用很大的空间,二来图片加载会很耗时,导致动画卡顿。还有个比较棘手的问题就是,OpenGL对纹理对大小是有限制的,如果超过最大纹理大小,Core Animation将会强制用CPU处理图片而不是GPU。为了解决这些问题,CATiledLayer将大图分解成小片然后将他们单独按需载入。从而减小内存占用和加载耗时。

  • CAEmitterLayer
    CAEmitterLayer是一个高性能的粒子引擎,被用来创建实时粒子动画如:烟雾,火,雨等等效果。

  • CAEAGLLayer
    CAEAGLLayer是CALayer的一个子类,用来显示任意的OpenGL图形。

  • AVPlayerLayer
    AVPlayerLayer是用来在iOS上播放视频的。它是高级接口例如MPMoivePlayer的底层实现,提供了显示视频的底层控制。

隐式动画

iOS中的动画有显式动画和隐式动画两种类型,上面介绍的动画都属于显式动画,接下来要介绍的是隐式动画,我们上面介绍过每一个View都有其对应的layer,这个layer是Root Layer,而其他通过CALayer或其子类直接创建的CALayer是非Root Layer。所有非Root Layer在我们设置Animatable Properties的时候都会存在duration为0.25s的隐式动画,而对Root Layer则没有这个过渡,这是为什么呢?实际上无论什么时候修改Animatable Properties。CALayer都会去查找并运行合适的action,什么是action呢?action实际上是一些遵循了CAAction协议的对象,用于定义一个动画需要做的事情,不论是否在block里面修改view的属性,都会触发CALayer查找合适的CAAction.

@protocol CAAction

- (void)runActionForKey:(NSString *)event object:(id)anObject
arguments:(nullable NSDictionary *)dict;

@end

CAAction协议中有个runActionForKey方法,我们可以在这个方法中对layer自定义某些动画效果。下面是一个简单的例子:

@interface CustomAction : NSObject<CAAction>
@property (nonatomic) CGColorRef currentColor;
@end
@implementation CustomAction
- (void)runActionForKey:(NSString *)key object:(id)anObject arguments:(NSDictionary *)dict {
CustomLayer *layer = anObject;
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"backgroundColor"];
animation.fromValue = (id)[UIColor greenColor].CGColor;
animation.toValue = (id)[UIColor redColor].CGColor;
animation.duration = 5;
[layer addAnimation:animation forKey:@"backgroundColor"];
}
@end

除了修改layer属性外,触发搜索action事件的触发点包括如下几个:

layer的属性被修改。包括layer的任何属性,不仅仅只是会产生动画的部分。
layer被添加到layer阶层。标识符key是kCAOnOrder。
layer被移除layer阶层。标示符key是kCAOnOrderOut。

action的搜索的过程:

layer调用actionForKey:方法搜索需要执行的action对象

如果layer设置了代理,layer会向它的delegate发送actionForLayer:forKey:消息来要求返回对应当前属性变化的CAAction。
actionForLayer:forKey:有三种返回情况:
1. 返回CAAction的对象,这时候将会使用这个CAAction来实现这个动画
2. 返回NSNull,这时候就会停止搜索,并且告诉layer不需要执行任何动画
3. 返回nil,这时候layer就会继续往下找
4. 查找layer的action属性,看可以是否有对应的值
5. 查找layer的style属性。
6. 调用defaultActionForKey返回对应key的默认action,一般是CABasicAnimation。

找到action对象后,调用action对象的runActionForLayer:object:arguments:方法执行相关操作。

- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event{
if ([event isEqualToString:@"backgroundColor"]) {
MyAction *action = [MyAction new];
return action;
}
return nil;
}

@interface MyAction : NSObject<CAAction>
@end

@implementation MyAction
- (void)runActionForKey:(NSString *)event object:(id)anObject arguments:(NSDictionary *)dict{
CustomLayer *layer = anObject;
CABasicAnimation *animation = [CABasicAnimation animation];
animation.duration = 3.0f;
[layer addAnimation:animation forKey:@"backgroundColor"];
}
@end

有了上面的介绍大家应该会对从修改属性到动画执行的整个流程有了比较详细的了解了吧,这里就很好理解为什么Root Layer没有隐式动画而非Root Layer会有隐式动画了,其实最大的玄机在于CALayer的delegate对象,我们知道Root Layer的delegte是对应的UIView,因此可以推测之所以Root Layer没有隐式动画就是因为UIView在一般情况下actionForLayer:forKey返回一个 NSNull,只有当属性改变发生在动画block 中时,view 才会返回实际的动作。而非Root Layer,delegate在不设置的情况下为空,所以返回的是通过defaultActionForKey返回的对应key的默认Action.

但是有时候我们又需要关闭这些隐式动画,这种情况就可以通过如下方式来关闭:

[CATransaction begin];
// 关闭隐式动画
[CATransaction setDisableActions:YES];
//原本会产生隐式动画的部分
[CATransaction commit];

设置setDisableActions:为YES后,layer的actionForKey:方法将不会被调用,隐式动画也不会生成。

UIView 过渡动画

视图过渡动画一般用在比如删除或增加子视图的时候。

+ (void)transitionWithView:(UIView *)view 
duration:(NSTimeInterval)duration
options:(UIViewAnimationOptions)options
animations:(void (^)(void))animations
completion:(void (^)(BOOL finished))completion;

view 就是指定的需要做动画过渡的视图,或者要做动画视图的容器视图。
animations 中可以执行比如添加、删除、显示或隐藏指定view 的子视图
其他的和UIView的block动画类似。

+ (void)transitionFromView:(UIView *)fromView 
toView:(UIView *)toView
duration:(NSTimeInterval)duration
options:(UIViewAnimationOptions)options
completion:(void (^ __nullable)(BOOL finished))completion

这个动画用于从一个view转变到另一个view过程的动画,在动画过程中,首先将 fromView 从父视图中删除,然后将 toView 添加,就是做了一个替换操作

UIImageView 帧动画
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSArray *imageArray = [self getImageArrayWithGIFNameWit:@"aisi"];
self.imageView.animationImages = imageArray;
self.imageView.animationDuration = 3;
self.imageView.animationRepeatCount = MAXFLOAT;
[self.imageView startAnimating];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[_imageView stopAnimating];
});
}

- (NSArray<UIImage *> *)getImageArrayWithGIFNameWit:(NSString *)imageName {
NSMutableArray *imageArray = [NSMutableArray array];
NSString *path = [[NSBundle mainBundle] pathForResource:imageName ofType:@"gif"];
NSData *data = [NSData dataWithContentsOfFile:path];
if (!data) {
return nil;
}
CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
size_t count = CGImageSourceGetCount(source);
if (count <= 1) {
[imageArray addObject:[[UIImage alloc] initWithData:data]];
} else {
for (size_t i = 0; i < count; i++) {
CGImageRef image = CGImageSourceCreateImageAtIndex(source, i, NULL);

[imageArray addObject:[UIImage imageWithCGImage:image scale:[UIScreen mainScreen].scale orientation:UIImageOrientationUp]];

CGImageRelease(image);
}
}
CFRelease(source);
return imageArray;
}
转场动画
1. UIViewController 容器转场动画

这个适用于多个子UIViewController在一个容器UIViewController中进行切换的动画,它的好处在没有转换到toViewController的时候toViewController没有显示也不会load,这样减少内存的使用。

 UIViewController *firstViewController = [UIViewController new];
firstViewController.view.backgroundColor = [UIColor redColor];
[self addChildrenController:firstViewController
locateSubViewBlock:^(UIView * _Nonnull parentControllerRootView, UIView * _Nonnull childControllerRootView) {
childControllerRootView.frame = parentControllerRootView.frame;
}];

UIViewController *secondViewController = [UIViewController new];
secondViewController.view.backgroundColor = [UIColor yellowColor];
secondViewController.view.frame = self.view.frame;
self.currentViewController = firstViewController;

要切换的时候调用:

[self changeControllerFromOldController:self.currentViewController toNewController:secondViewController];

- (void)changeControllerFromOldController:(UIViewController *)oldController toNewController:(UIViewController *)newController {
[self addChildViewController:newController];

[self transitionFromViewController:oldController toViewController:newController duration:8 options:UIViewAnimationOptionTransitionCurlUp animations:^{

} completion:^(BOOL finished) {

if (finished) {
//移除oldController,但在removeFromParentViewController:方法前不会调用willMoveToParentViewController:nil 方法,所以需要显示调用
[newController didMoveToParentViewController:self];
[oldController willMoveToParentViewController:nil];
[oldController removeFromParentViewController];
self.currentViewController = newController;

} else {
self.currentViewController = oldController;
}
}];
}

如果不是非常理解,大家还可以查看这篇博客:iOS addChildViewController方法

2. UIViewController之间跳转的转场动画

在iOS中一个ViewController又被称为一个场景,转场动画就是两个场景之间切换时候的动画,iOS已经默认为我们提供了四种转场动画,可以通过UIViewController的modalTransitionStyle属性来指定:

typedef NS_ENUM(NSInteger, UIModalTransitionStyle) {
UIModalTransitionStyleCoverVertical = 0, //从下向上弹起
UIModalTransitionStyleFlipHorizontal, //水平翻转
UIModalTransitionStyleCrossDissolve, //渐隐渐现
UIModalTransitionStylePartialCurl, //翻页
};

有些情况下这些转场动画并不能满足我们产品的需求这时候就需要我们自定义转场动画来满足了。iOS中的场景切换是一个蛮强大的一个模块,下面是整个模块的关系图,后面会针对这个图进行梳理:

2.1 Present / Dismiss 动画

首先我们以最常见的一个ViewController present 另外一个ViewController 为例子:

1. 代码触发一个presentViewController。
2. UIKit询问要过渡到的target ViewController 是否有自定义的过渡动画代理。如果没有,则UIKit将使用iOS自带的过渡动画
3. 如果有过渡动画代理,UIKit则会通过过渡动画代理transitioningDelegate,获取到动画控制器。比如通过 animationControllerForPresentedController(_:presentingController:sourceController:)方法获取到动画控制器,如果返回空,则使用默认的动画控制器。
4. 一旦找到了动画控制器,UIKit构建上下文对象UIViewControllerContextTransitioning。
5. 接着,UIKit通过动画控制器UIViewControllerContextTransitioning 的 transitionDuration(_:)方法获取动画执行时长。
6. 再接着调用动画控制器的animateTransition(_:)完成过渡动画。
7. 最后动画控制器调用上下文对象的completeTransition(_:)方法指示动画完成。

这里也介绍下一个最初困惑我比较久的概念presentingViewController/presentedViewController

假如我们有两个 VC A/B,我们要从A转换到B,我们称A为presentingViewController,称B presentedViewController,当从 B 结束转换回到 A 时,我们仍然称呼 A 为 presentingViewController,B 为 presentedViewController。这是让我每次比较懵逼的地方。

再说得详细点:

  • presentingViewController[负责呈现的ViewController]

关于presentingViewController 官方的说明如下:

The view controller that presented this view controller.
When you present a view controller modally (either explicitly or implicitly) using the presentViewController:animated:completion: method, the view controller that was presented has this property set to the view controller that presented it. If the view controller was not presented modally, but one of its ancestors was, this property contains the view controller that presented the ancestor. If neither the current view controller or any of its ancestors were presented modally, the value in this property is nil.

也就是说如果我们沿着viewController堆栈,但凡有一个是通过 presentViewController:animated:completion: 方法推出的,那么presentingViewController 的值就是推出的堆栈的起点viewController.也就是说只有在调用presentViewController:animated:completion: 的时候才会更改这个值,如果通过push的话,就会继承操作的发起者的presentingViewController值。

举个简单的例子:

有五个控制器 ABCDEF,应用启动首先显示RootViewController A,之后A通过present方式推出带导航栏的B,B再通过push的方式推出C,C再通过push的方式推出D,D再通过push的方式推出E,那么E的 presentingViewController 就是 B。上面例子中只有B是通过present方式推出的,且B是CDE的父级,那么 D 的presentingViewController也将是B。

  • presentedViewController[被呈现的ViewController]

关于presentedViewController 官方的说明如下:

The view controller that is presented by this view controller, or one of its ancestors in the view controller hierarchy.
When you present a view controller modally (either explicitly or implicitly) using the presentViewController:animated:completion: method, the view controller that called the method has this property set to the view controller that it presented. If the current view controller did not present another view controller modally, the value in this property is nil.

也就是你通过present模态推出了谁,你的presentedViewController就是谁.

OK 回到转场动画的解释上来:

下面以一个例子来说明如何自定义一个转场动画:

正常情况下我们跳转页面代码如下:

IDLTargetViewController * targetViewController = [IDLTargetViewController new];
[self presentViewController:targetViewController animated:YES completion:nil];

但是上面由于没有自定transitioningDelegate所以用的是系统的默认转场动画也就是从下往上弹出,我们现在要自定义转场动画,就需要指定一个transitioningDelegate,这样系统在跳转的时候就会从transitioningDelegate获取对应的遵循UIViewControllerAnimatedTransitioning协议的对象。

targetViewController.transitioningDelegate = [[IDLTransitionDelegate alloc] init];

我们先来看UIViewControllerTransitioningDelegate协议中的几个函数:

//这个函数用来设置当执行present方法时 进行的转场动画
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source;
//这个函数用来设置当执行dismiss方法时 进行的转场动画
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed;

//这个函数用来设置当执行present方法时 进行可交互的转场动画
- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:(id <UIViewControllerAnimatedTransitioning>)animator;
//这个函数用来设置当执行dismiss方法时 进行可交互的转场动画
- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator;

//返回UIPresentationController处理转场
- (nullable UIPresentationController *)presentationControllerForPresentedViewController:(UIViewController *)presented presentingViewController:(nullable UIViewController *)presenting sourceViewController:(UIViewController *)source NS_AVAILABLE_IOS(8_0);

这里我们只实现present动画那么只需要在IDLTransitionDelegate中实现animationControllerForPresentedController方法。

- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source{
return [IDLPresenteContentController new];
}

IDLPresenteContentController是一个实现了UIViewControllerAnimatedTransitioning协议的对象。它用于提供动画事件,动画上下文的对象。

//这个函数用来设置动画执行的时长
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext{
return 2;
}
//这个函数用来处理具体的动画
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext {

//1.获取动画的源控制器和目标控制器
ViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
DetailViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIView *container = [transitionContext containerView];

//2.创建一个imageView 的截图,并把 imageView 隐藏,造成使用户以为移动的就是 imageView 的假象
UIView *snapshotView = [fromVC.ImageView snapshotViewAfterScreenUpdates:NO];
//计算fromVC.view上的fromVC.ImageView.frame相对于container的坐标
snapshotView.frame = [container convertRect:fromVC.ImageView.frame fromView:fromVC.view];

//3.设置目标控制器的位置,并把透明度设为0,在后面的动画中慢慢显示出来变为1
toVC.view.frame = [transitionContext finalFrameForViewController:toVC];
toVC.view.alpha = 0;
toVC.bgImageView.hidden = YES;

//4.都添加到 container 中。注意顺序不能错了
[container addSubview:toVC.view];
[container addSubview:snapshotView];

[UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
snapshotView.frame = [container convertRect:toVC.bgImageView.frame fromView:toVC.view];
fromVC.view.alpha = 0;
toVC.view.alpha = 1;
} completion:^(BOOL finished) {
fromVC.ImageView.hidden = NO;
toVC.bgImageView.hidden = NO;
[snapshotView removeFromSuperview];

//一定要记得动画完成后执行此方法,让系统管理 navigation 如果设置为no。可以自己试试
[transitionContext completeTransition:YES];
}];
}

这里最重要的就是animateTransition方法,这个方法的参数中transitionContext也是关键中的关键,可以通过viewControllerForKey在transitionContext中取出跳转的ViewController,可以通过viewForKey获取到对应的view,还可以通过它来获得动画事件等。在animateTransition方法中一般是如下步骤实现整个动画的:

  1. 通过viewControllerForKey/viewForKey获取所有需要的 view 以及 VC
  2. 设定fromView, toView的初始状态
  3. 将toView通过addSubview 添加到 containerView
  4. 获取动画时间
  5. 设定动画
  6. 在动画结束的时候调用[transitionContext completeTransition:YES]来结束动画。

从上面可以看出transitionContext是一个十分关键的地方,它用于提供动画过程中所需要的各种数据,我们来看下它的声明:

//容器视图 用来表现动画
@property(nonatomic, readonly) UIView *containerView;
//下面是几个只读属性
//是否应该执行动画
@property(nonatomic, readonly, getter=isAnimated) BOOL animated;
//是否是可交互的
@property(nonatomic, readonly, getter=isInteractive) BOOL interactive; // This indicates whether the transition is currently interactive.
//是否被取消了
@property(nonatomic, readonly) BOOL transitionWasCancelled;
//转场风格
@property(nonatomic, readonly) UIModalPresentationStyle presentationStyle;
//调用这个函数来更新转场过程的百分比 用于可交互动画的阈值
- (void)updateInteractiveTransition:(CGFloat)percentComplete;
//完成可交互的转场交互动作时调用
- (void)finishInteractiveTransition;
//取消可交互的转场交互动作时调用
- (void)cancelInteractiveTransition;
//转场动画被中断 暂停时调用
- (void)pauseInteractiveTransition;
//转场动画完成时调用
- (void)completeTransition:(BOOL)didComplete;
//获取转场中的两个视图控制器
/*
UITransitionContextViewControllerKey的定义
UITransitionContextFromViewControllerKey //原视图控制器
UITransitionContextToViewControllerKey //跳转的视图控制器
*/
- (nullable __kindof UIViewController *)viewControllerForKey:(UITransitionContextViewControllerKey)key;
//直接获取转场中的视图
/*
UITransitionContextFromViewKey //原视图
UITransitionContextToViewKey //转场的视图
*/
- (nullable __kindof UIView *)viewForKey:(UITransitionContextViewKey)key;
//获取视图控制器的初识位置
- (CGRect)initialFrameForViewController:(UIViewController *)vc;
//获取视图控制器转场后的位置
- (CGRect)finalFrameForViewController:(UIViewController *)vc;
2.2 Interactive 交互动画

上面仅仅介绍的是present动画,但是iOS还支持交互动画,也就是通过手势等界面交互来触发动画。

有了上面的介绍接下来的介绍会稍稍简单点:

首先我们还是先设置transitioningDelegate,并在上面IDLTransitionDelegate中添加如下方法实现:

- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator{
return [IDLInteractiveTransition new];
}

接下来实现 IDLInteractiveTransition

- (void)startInteractiveTransition:(id<UIViewControllerContextTransitioning>)transitionContext {
// 把 context 对象保存起来
self.transitionContext = transitionContext;
[super startInteractiveTransition:transitionContext];
}


// 根据手势的偏移来计算当前动画应该有的完成度
- (CGFloat)percentForGesture:(UIScreenEdgePanGestureRecognizer *)gesture {
// 根据 container view 以及 gesture recognizer 计算偏移量
UIView *transitionContainerView = self.transitionContext.containerView;
CGPoint locationInSourceView = [gesture locationInView:transitionContainerView];

// 根据偏移量得出百分比
CGFloat width = CGRectGetWidth(transitionContainerView.bounds);
return (width - locationInSourceView.x) / width;
}

// gesture recognizer 的回调
- (IBAction)gestureRecognizeDidUpdate:(UIScreenEdgePanGestureRecognizer *)gestureRecognizer {
switch (gestureRecognizer.state)
{
case UIGestureRecognizerStateBegan:
break;
case UIGestureRecognizerStateChanged:
// 计算百分比,并返回
[self.transitionContext updateInteractiveTransition:[self percentForGesture:gestureRecognizer]];
break;
case UIGestureRecognizerStateEnded:
// 根据预先设定的阈值决定是结束还是取消,这里我们设定 view 中间是分界线
if ([self percentForGesture:gestureRecognizer] >= 0.5f)
[self.transitionContext finishInteractiveTransition];
else
[self.transitionContext cancelInteractiveTransition];
break;
default:
// 其他情况,取消转场
[self.transitionContext cancelInteractiveTransition];
break;
}
}

上面是通过自己实现一个遵循UIViewControllerInteractiveTransitioning协议的对象,当然也可以通过系统为我们提供的UIPercentDrivenInteractiveTransition来简化代码

2.3 UIPresentationController 实现弹窗效果

UIPresentationController 有很多属性下面将比较重要的给抠出来给大家介绍:

@interface UIPresentationController : NSObject <UIAppearanceContainer, UITraitEnvironment, UIContentContainer, UIFocusEnvironment>

//这个概念见文章上面介绍
@property(nonatomic, strong, readonly) UIViewController *presentingViewController;
@property(nonatomic, strong, readonly) UIViewController *presentedViewController;
//弹窗的模态形式
@property(nonatomic, readonly) UIModalPresentationStyle presentationStyle;
// 转场发生的容器视图
@property(nullable, nonatomic, readonly, strong) UIView *containerView;
// 初始化方法
- (instancetype)initWithPresentedViewController:(UIViewController *)presentedViewController presentingViewController:(nullable UIViewController *)presentingViewController NS_DESIGNATED_INITIALIZER;
// 动画容器布局,可以在这里布局子元素
- (void)containerViewWillLayoutSubviews;
- (void)containerViewDidLayoutSubviews;
//presentView的位置参数
@property(nonatomic, readonly) CGRect frameOfPresentedViewInContainerView;
//呈现动画时机回调
- (void)presentationTransitionWillBegin;
- (void)presentationTransitionDidEnd:(BOOL)completed;
- (void)dismissalTransitionWillBegin;
- (void)dismissalTransitionDidEnd:(BOOL)completed;

@end

下面是一个使用例子:

@interface IDLPopPresentationController : UIPresentationController<UIViewControllerTransitioningDelegate,UIViewControllerAnimatedTransitioning>

@property (nonatomic, strong, readonly) RACSubject *dissmissSignal;

@end
@implementation IDLPopPresentationController

#pragma mark overide UIPresentationController
- (instancetype)initWithPresentedViewController:(UIViewController *)presentedViewController presentingViewController:(UIViewController *)presentingViewController {
if (self = [super initWithPresentedViewController:presentedViewController presentingViewController:presentingViewController]) {
presentedViewController.modalPresentationStyle = UIModalPresentationCustom;
}
return self;
}

- (void)dealloc {
[self.dissmissSignal sendCompleted];
self.dissmissSignal = nil;
}

- (void)presentationTransitionWillBegin {

[self.containerView addSubview:self.dimmingView];
[self.dimmingView addSubview:self.closeBtn];
[self.closeBtn nm_makeFrame:^(NMFrameMaker *make) {
make.right.equalTo(self.dimmingView).margin(25);
make.top.equalTo(self.dimmingView).margin(40);
make.size.nm_equalTo(CGSizeMake(20, 20));
}];
@weakify(self);
[self.closeBtn addTapBlock:^(id obj) {
@strongify(self);
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
[self.dissmissSignal sendNext:nil];
[self.dissmissSignal sendCompleted];
}];

//背景 self.dimmingView 的淡入效果与过渡效果一起执
id<UIViewControllerTransitionCoordinator> transitionCoordinator = self.presentingViewController.transitionCoordinator;
[transitionCoordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
@strongify(self);
self.dimmingView.alpha = 0.65f;
} completion:nil];
}

- (void)presentationTransitionDidEnd:(BOOL)completed {
//如果呈现没有完成,那就移除背景 View
if(!completed){
[self.dimmingView removeFromSuperview];
self.dimmingView = nil;
}
}

- (void)dismissalTransitionWillBegin {
id<UIViewControllerTransitionCoordinator> coordinator = self.presentingViewController.transitionCoordinator;
@weakify(self);
[coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
@strongify(self);
self.dimmingView.alpha = 0.0;
} completion:nil];
}

- (void)dismissalTransitionDidEnd:(BOOL)completed {
if(completed) {
[self.dimmingView removeFromSuperview];
}
}

//Notifies an interested controller that the preferred content size of one of its children changed.
- (void)preferredContentSizeDidChangeForChildContentContainer:(id<UIContentContainer>)container {
[super preferredContentSizeDidChangeForChildContentContainer:container];
if (container == self.presentedViewController) {
[self.containerView setNeedsLayout];
}
}

- (void)containerViewWillLayoutSubviews {
[super containerViewWillLayoutSubviews];
self.dimmingView.frame = self.containerView.bounds;
}

- (CGRect)frameOfPresentedViewInContainerView {
//要呈现的ViewController区域
return CGRectMake(15.f, 73.f, SCREEN_MIN_LENGTH - 30.f , SCREEN_MAX_LENGTH - 103.f);
}

#pragma mark UIViewControllerTransitioningDelegate

- (UIPresentationController* )presentationControllerForPresentedViewController:(UIViewController *)presented
presentingViewController:(UIViewController *)presenting
sourceViewController:(UIViewController *)source {
return self;
}

- (id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented
presentingController:(UIViewController *)presenting
sourceController:(UIViewController *)source {
return self;
}

- (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed {
return self;
}

#pragma mark - UIViewControllerAnimatedTransitioning
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext {
return [transitionContext isAnimated] ? 0.3 : 0;
}

- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext {

UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];

__block UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];
__block UIView *fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];

UIView *containerView = transitionContext.containerView;
[containerView addSubview:toView];

BOOL isPresenting = (fromViewController == self.presentingViewController);
if (isPresenting) {
toView.frame = [self frameOfPresentedViewInContainerView];
toView.alpha = self.closeBtn.alpha = 0.0f;
toView.layer.masksToBounds = YES;
toView.layer.cornerRadius = 3;
}

NSTimeInterval duration = [self transitionDuration:transitionContext];
[UIView animateWithDuration:duration animations:^{
if (isPresenting) {
toView.alpha = self.closeBtn.alpha = 1.0f;
} else {
fromView.alpha = self.closeBtn.alpha = 0.0f;
}
} completion:^(BOOL finished) {
BOOL wasCancelled = [transitionContext transitionWasCancelled];
[transitionContext completeTransition:!wasCancelled];
}];
}

- (void)animationEnded:(BOOL) transitionCompleted {

}


#pragma mark Getter/Setters

- (UIView *)dimmingView {
if (!_dimmingView) {
_dimmingView = [[UIView alloc] initWithFrame:self.containerView.bounds];
_dimmingView.alpha = 0.0f;
_dimmingView.backgroundColor = [UIColor colorWithWhite:0.0f alpha:0.65f];
_dimmingView.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight;
}
return _dimmingView;
}

- (NMFocusExpandView *)closeBtn {
if (!_closeBtn) {
_closeBtn = [[NMFocusExpandView alloc] initWithFrame:CGRectZero];
[_closeBtn setImage:NMImage.login_instagram_close_icon];
}
return _closeBtn;
}

- (RACSubject *)dissmissSignal {
if (!_dissmissSignal) {
_dissmissSignal = [RACSubject subject];
}
return _dissmissSignal;
}

使用方法

IDLPopPresentationController *presentationController = [[IDLPopPresentationController alloc] initWithPresentedViewController:instagramLoginViewController
presentingViewController:self];
instagramLoginViewController.transitioningDelegate = presentationController;
[self presentViewController:instagramLoginViewController animated:YES completion:nil];
3 转场动画深入文章推荐

关于转场动画是一个十分大的一个话题,该博客只是帮大家理清楚转场动画的整个过程,对于细节大家还需要深入去挖掘,这里推荐几个比较好的文章大家可以在后续学习中供大家深入学习:

  1. iOS 视图控制器转场详解 Github地址 iOS 视图控制器转场详解 简书地址

  2. 玩转iOS转场动画

开篇叨叨

在iOS 事件模型中,由iOS事件源产生事件,而后顺着布局树进行Hit-Testing测试,判断哪些View可以响应这个事件,这些View组成一个事件响应链,产生的事件将沿着响应链一级一级传递,最终传递到最终的事件响应者中,由最终事件响应者提供的响应方法处理当前的事件。

这篇博客将主要针对如下问题进行展开:

  • iOS中有哪些事件源类型
  • 哪些对象会消费这些事件(哪些对象可以组成事件响应链上的节点)
  • 事件是怎么传递的
  • 如何判断谁是事件的最佳响应者
  • 事件的处理
事件源

为满足用户需求,iOS 提供了例如点击、长按、摇晃、3D Touch 等多种事件,这些事件大体可以分成触摸事件,运动事件,远程控制事件,按压事件四类。

  • 触摸事件:

    长按手势 (UILongPressGestureRecognizer)
    拖动手势 (UIPanGestureRecognizer)
    捏合手势 (UIPinchGestureRecognizer)
    响应屏幕边缘手势 (UIScreenEdgePanGestureRecognizer)
    轻扫手势 (UISwipeGestureRecognizer)
    旋转手势 (UIRotationGestureRecognizer)
    点击手势 (UITapGestureRecognizer)
  • 运动事件
    iPhone 内置陀螺仪、加速器和磁力仪,可以感知手机的运动情况。iOS 提供了 Core Motion 框架来处理这些运动事件。
    其中陀螺仪主要用于测量设备绕 X-Y-Z 轴的自转速率,倾斜角度等,加速器主要用于测量设备在 X-Y-Z 轴速度的改变,磁力仪可以测量当前设备的磁极、方向、经纬度等数据。

  • 远程控制事件
    远程控制事件指通过耳机去控制手机上的一些操作,比如上一曲/下一曲/播放/停止等。

  • 按压事件
    iOS 9 提供了 3D Touch 事件,可以通过压力的不同来区分不同的操作。

事件响应者

在iOS中不是任何对象都能处理事件,只有继承了UIResponder的对象才能接受并处理事件,下面是iOS中UIResponder的子类,也就是说只有这些类以及这些类的子类才能响应并处理事件,UIResponder对象之间的联系靠nextResponder指针,组成一个响应链:

UIApplication
UIViewController
UIView

UIResponder 中定义了一系列的触摸事件响应函数,我们可以通过覆写这些方法来提供自定义的响应:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;

- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event;

- (void)remoteControlReceivedWithEvent:(UIEvent *)event;

我们以最常见的触摸事件进行介绍:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;//一根或者多根手指开始触摸view
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;//一根或者多根手指在view上移动
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;//一根或者多根手指离开view
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;//触摸结束前,某个系统事件(例如电话呼入)打断触摸过程时候调用

这里还需注意的是:

  • 不论多少根手指同时触摸View,都只会调用一次touchesBegan,但是touches里面会包裹着多个UITouch,每个手指对应一个UITouch对象。
  • 如果多个手指一前一后触摸同一个View,那么会调用多次touchesBegan,每次只包含一个UITouch对象。
  • 如果是处理UIView触摸事件,需要在其子View的中覆写对应的touch方法。如果是处理UIViewController的触摸事件,可以直接在UIViewController的文件中覆写对应的touch方法。
  • 当我们手指按下后在屏幕上移动的时候会不断触发touchesMoved,一旦抬起来就会触发touchesEnded。
事件对象

了解了事件源,事件响应对象,我们还需要了解下事件对象,事件对象会携带者一系列的事件信息到事件响应对象,一个触摸事件可能是由多个手指同时触摸产生的。触摸对象集合通过 allTouches 属性获取。

  • UIEvent
@interface UIEvent : NSObject

@property(nonatomic,readonly) UIEventType type; //事件类型
@property(nonatomic,readonly) UIEventSubtype subtype; //事件子类型
@property(nonatomic,readonly) NSTimeInterval timestamp; //事件产生的时间戳
@property(nonatomic, readonly, nullable) NSSet <UITouch *> *allTouches; //一个事件包含的所有触摸事件
- (nullable NSSet <UITouch *> *)touchesForWindow:(UIWindow *)window;
- (nullable NSSet <UITouch *> *)touchesForView:(UIView *)view;
- (nullable NSSet <UITouch *> *)touchesForGestureRecognizer:(UIGestureRecognizer *)gesture;
@end

这里的UIEventType是事件大的类别:

typedef NS_ENUM(NSInteger, UIEventType) {
UIEventTypeTouches,
UIEventTypeMotion,
UIEventTypeRemoteControl,
UIEventTypePresses API_AVAILABLE(ios(9.0)),
};

UIEventSubtype 为事件小的类别,比如远程控制事件会使用这个字段来区分具体是哪个子事件:

typedef NS_ENUM(NSInteger, UIEventSubtype) {
// available in iPhone OS 3.0
UIEventSubtypeNone = 0,

// for UIEventTypeMotion, available in iPhone OS 3.0
UIEventSubtypeMotionShake = 1,

// for UIEventTypeRemoteControl, available in iOS 4.0
UIEventSubtypeRemoteControlPlay = 100,
UIEventSubtypeRemoteControlPause = 101,
UIEventSubtypeRemoteControlStop = 102,
UIEventSubtypeRemoteControlTogglePlayPause = 103,
UIEventSubtypeRemoteControlNextTrack = 104,
UIEventSubtypeRemoteControlPreviousTrack = 105,
UIEventSubtypeRemoteControlBeginSeekingBackward = 106,
UIEventSubtypeRemoteControlEndSeekingBackward = 107,
UIEventSubtypeRemoteControlBeginSeekingForward = 108,
UIEventSubtypeRemoteControlEndSeekingForward = 109,
};

里面包含了一系列的UITouch,可以通过touchesForWindowtouchesForViewtouchesForGestureRecognizer来判断属于某个window,view,以及手势下的UITouch.

  • UITouch
@interface UITouch : NSObject

//时间戳: 记录了触摸事件产生或变化时的时间。单位是秒。
@property(nonatomic,readonly) NSTimeInterval      timestamp;

//触摸事件在屏幕上有一个周期,即触摸开始、触摸点移动、触摸结束,还有中途取消。通过phase可以查看当前触摸事件在一个周期中所处的状态。
@property(nonatomic,readonly) UITouchPhase        phase;

//轻击(Tap)操作和鼠标的单击操作类似,tapCount表示短时间内轻击屏幕的次数。因此可以根据tapCount判断单击、双击或更多的轻击。
@property(nonatomic,readonly) NSUInteger          tapCount;

//触摸的类型
@property(nonatomic,readonly) UITouchType         type;

//触摸的半径
@property(nonatomic,readonly) CGFloat majorRadius;

//触摸的力度
@property(nonatomic,readonly) CGFloat force;

//触摸产生时所处的窗口。由于窗口可能发生变化,当前所在的窗口不一定是最开始的窗口。
@property(nullable,nonatomic,readonly,strong) UIWindow *window;

//触摸产生时所处的视图。由于视图可能发生变化,当前视图也不一定是最初的视图。
@property(nullable,nonatomic,readonly,strong) UIView *view;

//现在触摸的坐标
/函数返回一个CGPoint类型的值,表示触摸在view这个视图上的位置,这里返回的位置是针对view的坐标系的。调用时传入的view参数为空的话,返回的时触摸点在整个窗口的位置。
-(CGPoint)locationInView:(nullable UIView *)view;

//上一次触摸的坐标
//该方法记录了前一个坐标值,函数返回也是一个CGPoint类型的值,表示触摸在view这个视图上的位置,这里返回的位置是针对view的坐标系的。调用时传入的view参数为空的话,返回的时触摸点在整个窗口的位置。
-(CGPoint)previousLocationInView:(nullable UIView *)view;

UITouchPhase 用于表示某个触摸是处于哪个阶段

typedef NS_ENUM(NSInteger, UITouchPhase) {
UITouchPhaseBegan, // whenever a finger touches the surface.
UITouchPhaseMoved, // whenever a finger moves on the surface.
UITouchPhaseStationary, // whenever a finger is touching the surface but hasn't moved since the previous event.
UITouchPhaseEnded, // whenever a finger leaves the surface.
UITouchPhaseCancelled, // whenever a touch doesn't end but we need to stop tracking (e.g. putting device to face)
};
事件的传递流程
阶段1 将事件传递到UIWindow

当一个事件产生的时候,事件会在底层由IOKit.framework 封装成IOHIDEvent对象。然后系统通过mach port将IOHIDEvent对象转发给SpringBoard.app。 SpringBoard.app 它有点像Android中的lancher,只接收按钮,触摸,加速等事件,SpringBoard会根据当前桌面的状态,判断应该由谁处理此次触摸事件.可能在事件产生的时候你在桌面翻页并没有应用在前台运行,这时候触发SpringBoard本身主线程runloop的source0事件源的回调,将事件交由桌面系统去消耗,如果有应用在前台运行那么会通过mach port 将IOHIDEvent 转发给对应的App.App主线程的RunLoop收到SpringBoard转发的消息后,触发Source1回调__IOHIDEventSystemClientQueueCallback。在这个方法中会触发Source0回调__UIApplicationHandleEventQueue,将IOHIDEvent转换为UIEvent.并通过UIApplication的sendEvent:方法将UIEvent传递给UIWindow.

阶段2 Hit-Testing

上一阶段UIWindow已经拿到了事件,但是面对着整个复杂的视图层级树,要先判断最先将事件最先传给谁(也就是判断first responder,当然first responder也可以我们直接指定),这就需要靠Hit-Testing来完成了,注意这里并未涉及到事件的处理,只是确定由哪个视图来首先处理 UITouch 事件。

上图中左边是界面结构,右边是对应的视图层级树,其中View B的子View,View B.1遮住了View A的子View,View A2.我们假设点击了View B.1区域。
下面是整个Hit-Testing 的流程。

首先UIWindow 会顺着层级树,到RootView,然后RootView有三个子View .由于三个子View的添加顺序为: A –> B –> C, 所以先从C开始,也就是从subViews的最后一个开始,从后往前进行遍历,为什么从后往前而不是从前往后是因为,在数组中所处的位置越后,在界面上所处的位置越上层,这是为了考虑视图遮挡的情况下先判断最上层的。View C调用hit test 判断不在它上面,所以转向B,hit-test测试在它上面,就继续遍历它的子view,子view的遍历过程也是从子view数组的最后一项,往前测试。在测试到View B.1的时候发现触点在它上面,并且它没有子View所以,到此位置终止测试。View B.1 作为Hit-testing最终的结果。

上面只是展示了正常的Hit-Testing 的流程,实际上,View的一些属性还会影响到Hit-Testing的结果,比如如下的情况:

视图的hidden等于 YES
视图的alpha小于等于 0.01
视图的userInteractionEnabled为 NO

也就是当前待测试的视图不可见或者不处理交互,这些视图将会被忽略。因此整个hitTest的代码如下所示。

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
//如果当前视图不可见或者不接受事件,将传递给上层
if (self.alpha <= 0.01 || self.userInteractionEnabled == NO || self.hidden) {
return nil;
}
//判断触点是否在当前视图内部
BOOL inside = [self pointInside:point withEvent:event];
if (inside) {
//如果在的话判断子view
NSArray *subViews = self.subviews;
for (NSInteger i = subViews.count - 1; i >= 0; i--) {
UIView *subView = subViews[i];
//将point转换到subView视图上的坐标递归调用子view的hitTest
CGPoint insidePoint = [self convertPoint:point toView:subView];
UIView *hitView = [subView hitTest:insidePoint withEvent:event];
if (hitView) {
return hitView;
}
}
return self;
}
return nil;
}

下面需要重点看下这个阶段的两个重要方法:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;

hitTest 返回的是包含触点的最适合的子view,如果hitTest:withEvent:方法中返回nil,那么调用该方法的控件本身和其子控件都不是最合适的view,也就是在自己身上没有找到更合适的view。如果同级的兄弟控件也没有合适的view,那么最合适的view就是该控件的父控件。

想让谁成为最合适的view就重写谁自己的父控件的hitTest:withEvent:方法返回指定的子控件,或者重写自己的hitTest:withEvent:方法 return self。但是,建议在父控件的hitTest:withEvent:中返回子控件作为最合适的view!因为有可能在还没遍历到返回self的那个view的时候就已经拿到触点的的view。

pointInside很好理解就是判断点是否在当前view上面,如果是返回YES,否则返回NO.这里需要注意的是,point 必须先转换为相对当前view的坐标系坐标。

  • Hit-Testing 的应用
  1. 增加视图的touch区域

这里可以通过两种方式都可以实现:

一种是通过重写hitTest,一种是通过重写pointInside

重写hitTest

 - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) return nil;

CGFloat inset = 45.0f - 78.0f;
CGRect touchRect = CGRectInset(self.bounds, inset, inset);

if (CGRectContainsPoint(touchRect, point)) {
for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
CGPoint convertedPoint = [subview convertPoint:point fromView:self];
UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event];
if (hitTestView) {
return hitTestView;
}
}
return self;
}
return nil;
}

重写pointInside

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event {
CGRect bounds = self.bounds;
CGFloat widthDelta = MAX(self.focusSize - bounds.size.width, 0);
CGFloat heightDelta = MAX(self.focusSize - bounds.size.height, 0);
bounds = CGRectInset(bounds, -0.5 * widthDelta, -0.5 * heightDelta);
return CGRectContainsPoint(bounds, point);
}
  1. 透传事件

当时想在当前 view 处理事件,不想在对 subview 进行遍历,可以直接重写 [hitTest:withEvent:] 方法并 return self 即可。

  1. 指定某个视图处理事件

重写父视图的[hitTest:withEvent:],指定响应 View。

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *hitTestView = [super hitTest:point withEvent:event];
if (hitTestView) {
hitTestView = self.scrollView;
}
return hitTestView;
}
阶段3 事件的响应

Touch 事件处理的传递过程与 Hit-Testing 过程正好相反。Hit-Tesing 过程是从父视图到子视图遍历;Touch 事件处理传递是从子视图到父视图传递。
首先Touch事件会被发送到first responder,first responder便拥有了对事件的绝对控制权:它可以选择独吞这个事件,也可以将这个事件往下传递给其他响应者.

整个过程如下所示:

* 如果当前view是控制器的RootView,那么控制器就是nextResponder,事件就传递给控制器.
* 如果当前view不是控制器的RootView,那么父视图就是当前view的nextResponder,事件就传递给它的父视图.
* 在视图层次结构的最顶级视图,如果也不能处理收到的事件或消息,则其将事件或消息传递给window对象进行处理.
* 如果window对象也不处理,则其将事件或消息传递给UIApplication对象.
* 如果UIApplication也不能处理该事件或消息,则将其丢弃

响应者对于接收到的事件有3种操作:

  • 不拦截 :事件会自动沿着默认的响应链往下传递
  • 拦截,不再往下分发事件 :重写 touchesBegan:withEvent: 进行事件处理,不调用父类的 touchesBegan:withEvent:
  • 拦截,继续往下分发事件 :重写 touchesBegan:withEvent: 进行事件处理,同时调用父类的 touchesBegan:withEvent: 将事件往下传递

这里还需要注意的一点是如果我们的View有关联的手势识别器,那么在将touches发送给发生触摸的视图本身前,会先将touches发送给发生触摸的视图所关联的手势识别器,这部分内容将在下面讲手势的时候介绍。

触摸事件经历上面各个环节后要么被某个响应对象捕获后释放,要么没能找到能够响应的对象被丢弃。整个触摸事件就结束了。Runloop若没有其他事件需要处理,也将重新进入休眠,等待新的事件到来后唤醒。

手势

上面介绍了iOS的事件体系,紧接着讲下和事件相关的手势交互,iOS中的手势都是继承自UIGestureRecognizer,系统为了方便大家使用也内置了一系列的手势,下面是目前支持的几种手势,如果不够使用还可以通过继承UIGestureRecognizer来自定义手势。

手势 说明
UITapGestureRecognizer 轻拍手势
UISwipeGestureRecognizer 轻扫手势
UILongPressGestureRecognizer 长按手势
UIPanGestureRecognizer 平移手势
UIPinchGestureRecognizer 捏合(缩放)手势
UIRotationGestureRecognizer 旋转手势
UIScreenEdgePanGestureRecognizer 屏幕边缘平移
常用属性和方法
//设置代理,具体的协议后面会说
@property(nullable,nonatomic,weak) id <UIGestureRecognizerDelegate> delegate;
//设置手势是否有效
@property(nonatomic, getter=isEnabled) BOOL enabled;
//获取手势所在的view
@property(nullable, nonatomic,readonly) UIView *view;
//获取触发触摸的点
- (CGPoint)locationInView:(nullable UIView*)view;
//设置触摸点数
- (NSUInteger)numberOfTouches;
//获取某一个触摸点的触摸位置
- (CGPoint)locationOfTouch:(NSUInteger)touchIndex inView:(nullable UIView*)view;
手势的初始化

各种手势都是通过initWithTarget:action进行初始化的

- (instancetype)initWithTarget:(nullable id)target action:(nullable SEL)action;

还可以通过removeTarget:action将一个selector从手势对象上移除

- (void)removeTarget:(nullable id)target action:(nullable SEL)action;

iOS系统允许一个手势对象可以添加多个selector触发方法,并且触发的时候,所有添加的selector都会被执行

- (void)addTarget:(id)target action:(SEL)action;

这里需要注意的是UIGestureRecognizerSubclass.h头文件中定义了一个UIGestureRecognizer分类UIGestureRecognizerProtected,它里面也定义了一系列和UIResponder一样的方法,
可以供我们覆写以实现自定义效果。

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesEstimatedPropertiesUpdated:(NSSet<UITouch *> *)touches NS_AVAILABLE_IOS(9_1);

- (void)pressesBegan:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)pressesChanged:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)pressesEnded:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)pressesCancelled:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
手势代理

手势有其对应的代理UIGestureRecognizerDelegate 通过它可以指定很多特性,下面将对这些特性进行一一介绍:

@protocol UIGestureRecognizerDelegate <NSObject>
@optional
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer;
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch;
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceivePress:(UIPress *)press;
@end
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer;

开始进行手势识别时调用的方法,当手势识别器识别到手势,准备从UIGestureRecognizerStatePossible状态开始转换时.调用此代理,如果返回YES,那么就继续识别,如果返回NO,那么手势识别器将会将状态置为UIGestureRecognizerStateFailed.

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch

手指触摸屏幕后回调的方法,返回NO则不再进行手势识别,方法触发此方法在window对象在有触摸事件发生时,调用gesture recognizer的touchesBegan:withEvent:方法之前调用,如果返回NO,则gesture recognizer不会看到此触摸事件。(默认情况下为YES)

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceivePress:(UIPress *)press;

手指按压屏幕后回调的方法,返回NO则不再进行手势识别,方法触发等

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;

是否支持多手势触发,返回YES,则可以多个手势一起触发方法,返回NO则为互斥.

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;

这个方法返回YES,第一个手势和第二个互斥时,第一个会失效

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;

这个方法返回YES,第一个和第二个互斥时,第二个会失效

手势状态

和其他事件一样手势也是有个状态机,它用一个state属性描述。

@property(nonatomic,readonly) UIGestureRecognizerState state;

它的描述值如下:

typedef NS_ENUM(NSInteger, UIGestureRecognizerState) {
UIGestureRecognizerStatePossible, // 默认的状态,这个时候的手势并没有具体的情形状态
UIGestureRecognizerStateBegan, // 手势开始被识别的状态
UIGestureRecognizerStateChanged, // 手势识别发生改变的状态
UIGestureRecognizerStateEnded, // 手势识别结束,将会执行触发的方法
UIGestureRecognizerStateCancelled, // 手势识别取消
UIGestureRecognizerStateFailed, // 识别失败,方法将不会被调用
UIGestureRecognizerStateRecognized = UIGestureRecognizerStateEnded
};

左侧是非连续手势(比如单击)的状态机,右侧是连续手势(比如滑动)的状态机。所有的手势的开始状态都是UIGestureRecognizerStatePossible。
非连续的手势要么识别成功(UIGestureRecognizerStateRecognized),要么识别失败(UIGestureRecognizerStateFailed)。
连续的手势识别到第一个手势时,变成UIGestureRecognizerStateBegan,然后变成UIGestureRecognizerStateChanged,并且不断地在这个状态下循环,当用户最后一个手指离开view时,变成UIGestureRecognizerStateEnded,当然如果手势不再符合它的模式的时候,状态也可能变成UIGestureRecognizerStateCancelled。

手势与事件处理

我们前面讲到了当一个view有关联手势的时候,手势的优先级会比事件来的高,因此在我们触摸带手势的view的时候,如果先被手势识别器识别了,事件就不会传递给view 的UIResponder进行处理,但是我们知道手势是有状态的,也就是说手势识别是有一个过程的,因此在手势未被完全识别之前,事件是会被同时发送到UIResponder,以及UIGestureRecognizer,一旦手势识别器识别了某个手势之后,UIResponder就会的touchesCancelled:withEvent就会被调用,此后的事件都被UIGestureRecognizer独占,也就是只发往UIGestureRecognizer。如果识别失败手势状态将会被识别为UIGestureRecognizerStateFailed,这时候事件会继续发送给UIResponder,直到结束。

简单得说就是,在手势识别器未判定手势识别成功之前,事件会发给手势识别器和UIResponder,一旦手势识别器判定为识别成功就拦截了整个事件,UIResponder会收到cancel的信号,后续就不会继续收到对应的事件了,所有的后续事件都交给手势识别器进行处理。
同样在手势识别器未判定手势识别成功之前,事件会发给手势识别器和UIResponder,如果手势识别器识别失败,那么就会被标记为UIGestureRecognizerStateFailed,然后后续的事件都交给UIResponder进行处理。

  • cancelsTouchesInView

默认为YES。表示当手势识别器成功识别了手势之后,会通知Application取消响应链对事件的响应,并不再传递事件给UIResponder。若设置成NO,表示手势识别成功后不取消响应链对事件的响应,事件依旧会传递给UIResponder。

手势其他用法

我们添加的两个手势都是单击手势,会产生冲突,触发是很随机的,如果我们想设置一下当手势互斥时要优先触发的手势,可以使用如下的方法:

- (void)requireGestureRecognizerToFail:(UIGestureRecognizer *)otherGestureRecognizer;
比如我们在秀场直播间,双击屏幕会产生关注,如果单击屏幕会有点赞效果,就可以通过这种方式解决。

// 单击的 Recognizer UITapGestureRecognizer* singleRecognizer; singleRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:selfaction:@selector(handleSingleTapFrom)]; singleTapRecognizer.numberOfTapsRequired = 1; // 单击 [self.view addGestureRecognizer:singleRecognizer]; // 双击的 Recognizer UITapGestureRecognizer* double; doubleRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:selfaction:@selector(handleDoubleTapFrom)]; doubleTapRecognizer.numberOfTapsRequired = 2; // 双击 [self.view addGestureRecognizer:doubleRecognizer]; // 如果双击确定检测失败才会触发单击 [singleRecognizer requireGestureRecognizerToFail:doubleRecognizer]; [singleRecognizer release]; [doubleRecognizer release]; ``` 老规矩上图 ![](./iOS-事件及手势处理流程/000007.png)

1. 开篇叨叨

能不叨叨就不叨叨。

2. 什么时候会触发视图绘制
* 调用setNeedsDisplay/setNeedsDisplayInRect 
* 遮挡当前视图的其他视图被移动或删除时
* 设置视图的hidden属性,改变视图的显示状态
* 视图滚出屏幕,然后再重新回到屏幕上

其实可以推断后面的几项实际上也是通过调用setNeedsDisplay/setNeedsDisplayInRect来标记当前控件需要重新绘制。
这部分工作是在CPU中完成的,因此速度不如GPU绘制的效率高,因此尽可能避免使用绘制,多使用现有的控件组合来达到需求。

2. Core Graphic 简介

iOS支持两套图形API族:Core Graphics和OpenGL ES,而Core Graphics 是一套基于C的API框架,使用了Quartz作为绘图引擎, 和Open GL不同的是Core Graphics使用的是CPU进行绘制Open GL使用GPU进行绘制,使用Core Graphics 绘制一个图形一般是遵循以下步骤:

1. 创建/获取上下文
2. 创建路径并添加到上下文中。
3. 进行绘图内容的设置(画笔颜色、粗细、填充区域颜色、阴影、连接点形状等)
4. 开始绘图
5. 释放路径

这里需要注意的是Core Graphics中带有Ref后缀的类,其实例对象可能有指向其他 Core Graphics “对象”的强引用指针,但是不能被ARC管理,所以创建了这些对象,使用完之后记得手动释放,否则会有内存泄漏的问题。并且但凡名字中带有create 或者 copy 的函数创建了一个 Core Graphics “对象”,就必须调用对应的Release函数并传入该对象的指针将其释放,这是刚开始的时候很容易犯的错误。

从上面的过程来看,这部分总共有三个部分需要重点掌握:

一个是绘制上下文,也就是绘制的内容绘制到哪里?如何屏蔽多个环境之间的差异。
一个是绘制坐标系统,也就是我们怎么定义绘制的位置和长度的概念,
另一个就是Core Graphic API,也就是Core Graphic能够支持哪些绘制方法。

3. 绘图上下文

我们绘图的时候是需要一个载体或者说输出目标用来显示绘图信息,并且决定绘制的东西输出到哪个地方,Core Graphics框架就使用图形上下文来描述这个载体,这些上下文以堆栈形式存放,我们绘制的时候都是往栈顶的图形上下文上绘制,每个图形上下文包括画笔颜色、文本颜色、当前字体、变形,以及绘制内容所存储的位置等。之所以需要使用Context是因为Core Graphics可以在多种设备上绘制,比如在手机屏幕上,这也是最为常见的,再必须还可以在PDF上绘制,也可以再图片上进行绘制,每种设备上都存在很大的差异,Core Graphics 使用Context将这部分差异给隔离开来。让绘制内容与绘制步骤与设备无关。

Core Graphics 目前支持如下几种绘图上下文:

1. Bitmap Graphics Context:RGB图像或者黑白图像绘制到一个位图对象中.
2. PDF Graphics Context: PDF图形上下文可以帮助开发者创建PDF文件,将内容绘制进PDF文件中.
3. Window Graphics Context: 用于将内容绘制到OS系统中的窗口上
4. Layer Context: 用于将内容绘制在Layer图层上
5. Printer Graphics Context: 用于将内容绘制在打印输出源上

* CGContextSaveGState/CGContextRestoreGState 与 UIGraphicsPushContext/UIGraphicsPopContext区别

想象一个场景,比如我们现在需要修改上下文并使其恢复原样。举个例子,我们现在有一个使用特定颜色绘制特定形状的函数。由于只能有一只画笔,因此在更改颜色后,就会影响调用函数的结果。为了避免这个副作用,你可以使用CGContextSaveGState和CGContextRestoreGState将上下文入栈和出栈。下面是一个很有说服力的例子:

[[UIColor redColor] setStroke];                         //将线条颜色设置为红色
CGContextSaveGState(UIGraphicsGetCurrentContext()); //将带有红色线条颜色的上下文保存到上下文的状态堆栈
[[UIColor blackColor] setStroke]; //将当前上下文堆栈中的线条颜色设置为黑色
CGContextRestoreGState(UIGraphicsGetCurrentContext()); //恢复上一次保留的堆栈节点
UIRectFill(CGRectMake(10, 10, 100, 100)); //这时候绘制的是红色的线条

UIGraphicsPushContext并不能保存上下文的当前状态,而是完全切换上下文。假设你正在当前视图上下文中绘制什么东西,这时想要在位图上下文中绘制完全不同的东西。如果要使用UIKit来进行任意绘图,你会希望保存当前的UIKit上下文,包括所有已经绘制的内容,接着切换到一个全新的绘图上下文中。这就是UIGraphicsPushContext的功能。创建完位图后,再调用UIGraphicsPopContext将你的旧上下文出栈。这种情况很少见只会在要使用UIKit在新的位图上下文中绘图时才会发生。只要你使用的是Core Graphics函数,就不需要去执行上下文入栈和出栈,因为Core Graphics函数将上下文视作参数。
这是极其有用的常见操作,因为其常用性,苹果公司为我们创建了一个叫做UIGraphicsBeginImageContext的快捷方式。它负责将旧的上下文入栈、为新上下文分配内存、创建新的上下文、翻转坐标系统,并使其作为当前上下文使用。它替你完成了大部分的工作。

下图是Graphics Context与Graphics state的大致结构

4. 具体绘图方法
4.1 位图图片上下文

位图上下文的绘制不需要在drawRect:方法中进行,在一个普通的OC方法中就可以绘制。

关于UIGraphicsPushContext/UIGraphicsPopContext 与 UIGraphicsBeginImageContextWithOptions的区别用两个场景再说明:
当前正在使用CoreGraphics绘制图形A,想要使用UIKit绘制完全不同的图形B,此时希望保存当前绘图context及已绘制内容这时候需要用到UIGraphicsPushContext/UIGraphicsPopContext。
如果想在切换绘图context后,继续使用CoreGraphics绘图(而非UIKit),则不需要使用UIGraphicsPushContext/UIGraphicsPopContext。

位图上下文可以通过如下两种方式创建:

UIGraphicsBeginImageContextWithOptions(CGSize size, BOOL opaque, CGFloat scale);
这里需要注意的是scale可以用[UIScreen mainScreen].scale来获取,但实际上设为0后,系统就会自动设置正确的比例了。
UIGraphicsBeginImageContext(CGSize size);
相当于UIGraphicsBeginImageContextWithOptions的opaque参数为NO,scale因子为1.0

一般推荐使用第一种方式来创建,第一种方式除了可以指定图片的大小外,还可以指定图片是否透明,以及图片的scale。需要注意的是上面两种方式都会在创建一个基于位图的上下文,并将其设置为当前上下文,所以后续操作如果需要使用到上下文对象就可以通过UIGraphicsGetCurrentContext来获取。

一般使用步骤如下:

// 获取图片
UIImage *image = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:name ofType:nil]];
// 1.开启图形上下文,并将ImageContext放置到栈顶
UIGraphicsBeginImageContext(image.size);
// 2.获取到当前栈顶的图形上下文
CGContextRef context = UIGraphicsGetCurrentContext();
// 3. 使用Core Graphics API 在当前上下文中绘制

// ........

// 4.从上下文中获取图片
UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
// 5.关闭图形上下文
UIGraphicsEndImageContext();
//返回图片
return newImage;

下面分别使用UIKit和CoreGraphics实现的一个例子:

使用UIKit实现

// 获取图片上下文
UIGraphicsBeginImageContextWithOptions(CGSizeMake(100,100), NO, 0);
// 绘图
UIBezierPath* p = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0,0,100,100)];
[[UIColor blueColor] setFill];
[p fill];
// 从图片上下文中获取绘制的图片
UIImage* im = UIGraphicsGetImageFromCurrentImageContext();
// 关闭图片上下文
UIGraphicsEndImageContext();

使用CoreGraphics实现

// 获取图片上下文
UIGraphicsBeginImageContextWithOptions(CGSizeMake(100,100), NO, 0);
// 绘图
CGContextRef con = UIGraphicsGetCurrentContext();
CGContextAddEllipseInRect(con, CGRectMake(0,0,100,100));
CGContextSetFillColorWithColor(con, [UIColor blueColor].CGColor);
CGContextFillPath(con);
// 从图片上下文中获取绘制的图片
UIImage* im = UIGraphicsGetImageFromCurrentImageContext();
// 关闭图片上下文
UIGraphicsEndImageContext();
4.2 drawInRect

在系统调用drawInRect方法之前会创建一个新的Context,因此在drawInRect一般而言不需要创建新的Context,只需要通过UIGraphicsGetCurrentContext来获取即可。

使用UIKit实现

- (void) drawRect: (CGRect) rect {
UIBezierPath* p = [UIBezierPathbezierPathWithOvalInRect:CGRectMake(0,0,100,100)];
[[UIColor blueColor] setFill];
[p fill];
}

使用CoreGraphics实现

- (void) drawRect: (CGRect) rect {
CGContextRef con = UIGraphicsGetCurrentContext();//当前视图的上下文
CGContextAddEllipseInRect(con, CGRectMake(0,0,100,100));
CGContextSetFillColorWithColor(con, [UIColor blueColor].CGColor);
CGContextFillPath(con);
}
4.2 drawLayer:inContext

讲到这里得复习下Core Graphic的绘图方法的调用流程:

上图在介绍iOS渲染的时候已经讲解过了,我们这里再重新提一下第二个分支的过程,第二个分支开始的时候会创建一个新的backing store,然后开始走drawInContext,这时候会先看delegate是否实现了drawRect如果有则用drawRect,否则调用drawLayer:inContext:并将管理新建backing store的context传递出来,因此我们在绘制的时候只要使用传递出来的context就可以直接绘制到指定到layer

- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx {
CGContextAddEllipseInRect(ctx, CGRectMake(100,100,100,100));
CGContextSetFillColorWithColor(ctx, [UIColor blueColor].CGColor);
CGContextFillPath(ctx);
}
@interface ViewController () 

@property (nonatomic, weak) id myLayerDelegate;

@end

@implementation ViewController
- (void)viewDidLoad {
MyView *myView = [[MyView alloc] initWithFrame:self.view.bounds];
self.view addSubview:myView];
CALayer *layer = [CALayer layer];
layer.backgroundColor = [UIColor magentaColor].CGColor;
layer.bounds = CGRectMake(0, 0, 300, 500);
layer.anchorPoint = CGPointZero;
layer.delegate = [[MyLayerDelegate alloc] init];
[layer setNeedsDisplay];
[myView.layer addSublayer:layer];
}
4. 绘制坐标系统

UIView 有几个比较重要的位置坐标属性:frame, bounds, position,center,anchorPoint

* frame 表示视图,图层的外部坐标,也就是当前UIView相对于父视图的坐标
* bounds 表示视图,图层的内部坐标。原点位于左上角,它的作用主要是用于存放宽高尺寸。
* center 和 position 代表相对于父图层anchorPoint所在的位置,center 是视图的称呼,postion是图层里面的称呼,二者是同一个值。
* anchorPoint 可以看成是一个UIView的移动支点。我们设置center或者position的时候就是移动anchorPoint坐标,anchorPoint用单位坐标来描述,也就是图层的相对坐标,图层左上角是{0, 0},右下角是{1, 1},因此默认坐标是{0.5, 0.5}。anchorPoint可以通过指定x和y值小于0或者大于1,使它放置在图层范围之外。

frame是一个关联属性,是根据bounds,center和transform计算出来的,后面的任何一个值变化都会影响到frame的值。而frame值一旦改变也会对bounds,center和transform产生影响。同时也需要注意的是当图层做变换后,frame实际上代表了覆盖在图层旋转之后的整个轴对齐的矩形区域。这时候frame宽高和bounds有可能不一样了。

4.1 UIView坐标系

而在iOS的UIView中,统一使用左手坐标系,也就是坐标原点在左上角.

4.2 Quartz坐标系

Quartz(Core Graphics)坐标系使用的是右手坐标系,原点在左下角, 所以所有使用Core Graphics画图的坐标系都是右手坐标系,当使用CG的相关函数画图到UIView上的时候,需要注意CTM的Flip变换,要不然会出现界面上图形倒过来的现象。由于UIKit的提供的高层方法会自动处理CTM(比如UIImage的drawInRect方法),所以无需自己在CG的上下文中做处理。

当通过CGContextDrawImage绘制图片到一个context中时,如果传入的是UIImage的CGImageRef,因为UIKit和CG坐标系y轴相反,所以图片绘制将会上下颠倒。可以用下面几种方式来解决:

  1. 在绘制到context前通过矩阵垂直翻转坐标系
CGContextTranslateCTM(context, 0, height);
CGContextScaleCTM(context, 1.0, -1.0);
CGContextDrawImage(context, CGRectMake(0, 0, width, height), uiImage.CGImage);
  1. 使用UIImage的drawInRect函数,该函数内部能自动处理图片的正确方向

UIGraphicsPushContext(context);
[uiImage drawInRect:CGRectMake(0, 0, width, height)];
UIGraphicsPopContext();

4.3 坐标转换
// 将像素point由point所在视图(方法调用者)转换到目标视图view中,返回在目标视图view中的像素值
- [(CGPoint)convertPoint:(CGPoint)point] toView:(UIView *)view;
// 将像素point从view中转换到当前视图(方法调用者)中,返回在当前视图中的像素值
- (CGPoint)[convertPoint:(CGPoint)point fromView:(UIView *)view;]

// 将rect由rect所在视图转换到目标视图view中,返回在目标视图view中的rect
- [(CGRect)convertRect:(CGRect)rect] toView:(UIView *)view;
// 将rect从view中转换到当前视图中,返回在当前视图中的rect
- [(CGRect)convertRect:(CGRect)rect fromView:(UIView *)view];

  • 使用convertPoint:toView:时,调用者应为covertPoint的父视图。即调用者应为point的父控件。toView即为需要转换到的视图坐标系,以此视图的左上角为(0,0)点。
  • 使用convertPoint:fromView:时正好相反,调用者为需要转换到的视图坐标系。fromView为point所在的父控件。
  • toView可以为nil。此时相当于toView传入self.view.window

这里特地将同个坐标的用方括号扩起来,方便理解,也就是说括号内的是在同一个坐标系上。

4.4 坐标关系
  • 点是否在范围内的判断

需要判断点是否在某个范围内可以使用如下方法:

- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;

使用注意:
point必须为调用者的坐标系,即调用者的左上角为(0,0)的坐标系。
比如确定redView的中心点是否在blueView上:

//转换为blueView坐标系点
CGPoint redCenterInBlueView = [self.grayView convertPoint:self.redView.center toView:self.blueView];
BOOL isInside = [self.blueView pointInside:redCenterInBlueView withEvent:nil];
5. Core Graphic API

整个Core Graphic API 十分丰富,这里只是列举一部分比较核心的来介绍:
完整的可以查看官方文档:Core Graphic API

这里比较重要的是画圆弧的时候弧度的起始角度和结束角度,以及顺时针逆时针方向,我在网上找了一张图解释得很到位大家可以参照下。

深入理解UIBezierPath画圆弧addArcWithCenter

6. CGPath 和 UIBezierPath() 区别

CGPath是CoreGraphics库的类,而UIBezierPath是UIKit中的类. UIBezierPath是对CGPath的一种封装,可以很方便在二者之间进行转换,CGPath相对而言更底层,在速度性能上较UIBezierPath高,并且它具备UIBezierPath不具备的更高级的功能,但是UIBezierPath在使用上十分易用,所以除非在万不得己的情况下一般推荐使用UIBezierPath。

0. 较好的相关网站

各种设备的屏幕尺寸查询
Paint Code屏幕尺寸查询

1. 目录
  • iOS界面开发中常用的概念
  • iOS坐标系统及坐标转换
  • iOS布局相关函数
  • AutoresizingMask,AutoLayout,frame 布局
  • 三方开源布局引擎对比
  • iOS界面适配
2. 开篇叨叨

iPhone刚推出的时候尺寸比较单一,只有3.5英寸,这时候用frame就可以轻松搞定布局,后面推出了4.0英寸,4.7英寸,5.5英寸,5.8英寸,6.1英寸,6.5英寸,各种尺寸的屏幕,这给界面适配带来了很大困难,后面引入的刘海屏等更加剧了界面适配的难度。下图是iPhone和iPad不同机型的尺寸数据:



为了解决这个问题苹果公司推出了 AutoresizingMask,用来协调子视图与父视图之间的关系。而后又相继推出了AutoLayout供开发者进行屏幕适配,目前用得比较多的就是AutoLayout和frame布局,但是AutoLayout其实质是用一个个约束条件来约束布局参数,这些约束条件会被转换成一个不等式组,布局视图的阶段,其实就是解方程组的阶段,约束越多性能越低,对于十分复杂的界面是十分不适用的,因此一般大型项目都会回归到基于frame的布局,当然不会是手动计算,有很多开源的布局引擎比如YogaKit,比如LayoutKit都将针对frame的布局形式简化成十分易用,并且性能优于AutoLayout的布局形式,这篇博客将从界面开发的基础概念出发,介绍界面的组成,坐标系统,布局相关函数,原生三大布局方式,三方开源布局引擎以及iOS适配的方方面面。

后续还会针对个人正在使用的基于frame的开源布局引擎YogaKit以及基于AutoLayout的Masonry布局引擎进行源码分析。

3. 正文
3.1 iOS界面开发中常用的概念
3.2 点(Point) 屏幕像素 (Pixel) 对角线长度 ,屏幕密度,屏幕模式,宽高比

说到点和屏幕像素就会扯到逻辑像素和物理像素这两个概念,逻辑像素的单位是pt,它是按照内容的尺寸计算的单位,也就是这里所说的点,而物理像素就是这里的屏幕像素(Pixel) 。如果使用Sketch或Adobe XD设计界面,那么只要使用pt作为单位设计出一套就可以了,我们在开发的时候也只需要以pt为单位的进行开发。而屏幕像素一般是用来结合对角线长度来描述整个屏幕的ppi(每英寸的像素数),用于描述屏幕的细腻程度。计算方式如下

V(长像素数)*(长像素数) + (宽像素数) * (宽像素数) = 对角线像素数  (V表示开根号)

PPI = 对角线像素数/对角线长度

屏幕模式用于匹配App素材。为适配不同的屏幕,iOS App 的同一个图标,往往会准备 1x、2x、3x 等几个图片素材。假如屏幕 scale = 3,就会优先选取 3x 素材;假如屏幕 scale = 2, 就优先选择 2x 素材。scale也决定着点和像素之间的关系。当屏幕模式为 1x, 一个点就等于 1 个像素,当屏幕模式为 2x,一个点就等于 2 个像素。也就是说:

pixel = point * scale

宽高比则是在视频图片适配的时候比较重要,比如在手机上显示同一个视频,视频等比例铺满高度,在 iPhone X 左右两边就被裁掉更多内容。而视频等比例适应宽度,iPhone X 上下就被留更多的黑边。

3.3 布局坐标系及关键的属性

在iOS中有两大比较典型的坐标系,一个是UIKit的坐标系,零点位于左上角,一个是Core Graphic坐标系,零点位于左下角。
在布局使用的坐标系为UIKit坐标系,也就是说原点位于左上角。

UIView 有几个比较重要的布局属性:frame, bounds, position,center,anchorPoint

* frame 表示视图,图层的外部坐标,也就是当前UIView相对于父视图的坐标
* bounds 表示视图,图层的内部坐标。原点位于左上角,它的作用主要是用于存放宽高尺寸。
* center 和 position 代表相对于父图层anchorPoint所在的位置,center 是视图的称呼,postion是图层里面的称呼,二者是同一个值。
* anchorPoint 可以看成是一个UIView的移动支点。我们设置center或者position的时候就是移动anchorPoint坐标,anchorPoint用单位坐标来描述,也就是图层的相对坐标,图层左上角是{0, 0},右下角是{1, 1},因此默认坐标是{0.5, 0.5}。anchorPoint可以通过指定x和y值小于0或者大于1,使它放置在图层范围之外。

frame是一个关联属性,是根据bounds,center和transform计算出来的,后面的任何一个值变化都会影响到frame的值。而frame值一旦改变也会对bounds,center和transform产生影响。同时也需要注意的是当图层做变换后,frame实际上代表了覆盖在图层旋转之后的整个轴对齐的矩形区域。这时候frame宽高和bounds有可能不一样了。

3.4 iOS布局相关函数

为什么需要理解这块,因为刚接触iOS开发的时候最耗时的还是调试界面布局上面,这主要是对view 的 frame何时真正更新不是很清楚。该小结争取解开这个环节中的各个疑惑。

首先我们在触摸屏幕的时候会产生一系列的事件,这些事件由系统进行分发并传递到应用中,并通过一定的回调调用我们相应事件的响应方法,在响应方法中我们可能会修改当前界面上某个View,这时候系统并不会直接对view进行修改,重绘,因为有可能还会有其他事件需要相应。这时候只是将这些view标记为脏视图,当这些方法调用返回后,控制流回到主Runloop中,在Runloop进入休眠之前,会先进入Update cycle,在 Update cycle 中完成布局并重新渲染整个界面,由于iOS设备会以每秒60帧的速度刷新界面,这个时间很短用户在和应用交互时几乎感觉不到刷新UI时候带来的更新延迟,在整个过程系统会调用如下几个关键函数,这里会先介绍各个函数的功能,然后看下这些函数是怎么结合起来的。

3.4.1 frame布局
3.4.1.1 自动触发布局

在某些节点系统会帮我们自动给视图打上需要布局的标记,在下一个Update cycle的时候layoutSubviews会被调用,而不需要手动触发:


* 通过addSubView 新增子view的时候
* 设置self.view及子视图的frame.size会触发layoutSubviews,当然前提是frame.size的值设置前后发生了变化,注意,此处不是origin
* 滚动UIScrollView的时候会触发
* 横竖屏幕切换会触发
* 更新视图的 constraints

3.4.1.2 手动触发布局
  1. setNeedsLayout

在事件响应函数中我们可能会提交不是需要非常实时呈现在界面上的事件,这种情况下就可以调用这个方法,告诉系统该视图需要重新进行布局计算,也就是将当前视图标记为脏数据。这个方法会立刻执行并返回,但是在返回后并不会立即更新View.它会在下一个Update cycle中调用layoutSubviews集中对脏数据进行更新,这就导致了setNeedsLayout返回后到视图被重新绘制并布局之间有一段任意时间的间隔,但是这个延迟不会对用户造成影响,反而能将关键的时间留给其他事件的响应,这是一个比较正常的情景。

  1. layoutIfNeeded

layoutIfNeeded和setNeedsLayout一样也会触发layoutSubviews,但是这两个方法是有区别的:调用setNeedsLayout()并不会立刻触发layoutSubviews函数而是会延迟到Upate cycle中触发。而layoutIfNeeded会立刻触发,但是这里需要注意的是如果调用了layoutIfNeeded之前,并没有调用setNeedsLayout将视图标记为脏视图,那么就不会调用 layoutsubview。如果你在同一个 run loop 内调用两次 layoutIfNeeded,并且两次之间没有更新视图,第二个调用同样不会触发 layoutSubviews 方法,也就是说layoutIfNeeded必须结合setNeedsLayout使用。

3.4.1.3 布局回调节点

1. layoutSubviews

这个方法会在需要对界面进行重新定位和大小调整的时候被调用,它负责给出当前view和每个子view的位置和大小。这个方法十分耗时因为它会在每个子视图上起作用并且调用它们相应的layoutSubviews方法,所以我们可以在需要重新定位或者更改大小的情形下重载它,但是这个方法不能直接调用。但是可以通过上面介绍的setNeedsLayout方法以及layoutIfNeeded方法进行触发,如果不是非常必要可以通过调用setNeedsLayout后将这个方法的调用时机放在Update Cycle 中。这样会比调用setNeedsLayout立刻调用layoutIfNeeded这种方案消耗的资源要小得多。
这里需要注意的是layoutSubviews会先对父视图进行layoutSubviews,完成后再调用子视图的layoutSubviews

总结下layoutSubviews的触发点:

1、init初始化不会触发layoutSubviews,但是用initWithFrame进行初始化的时候,当rect的值不是CGRectZero时也会触发
2、addSubview会触发layoutSubviews。
3、设置view的frame会触发layoutSubviews,当然前提是frame的值设置前后发生了变化。
4、滚动一个UIScrollView会触发layoutSubviews。
5、旋转屏幕会触发父UIView上的layoutSubviews事件。
6、改变一个UIView大小的时候也会触发父UIView上的layoutSubviews事件。
7、直接调用setLayoutSubviews。

2. viewWillLayoutSubviews

viewWillLayoutSubviews/viewDidLayoutSubviews 是UIViewController中的方法,它会在ViewController的根view的layoutSubviews被调用之前被触发。

3. viewDidLayoutSubviews

viewDidLayoutSubviews会在ViewController的根view的layoutSubviews被调用之后被触发,在这个节点上viewController中的根view以及子view的尺寸都已经确定了,所以我们可以把所有依赖于布局或者大小的代码放在 viewDidLayoutSubviews 中,而不是放在 viewDidLoad 或者 viewDidAppear 中。

  • sizeThatFits

sizeThatFits有两种功能:

  1. 给一个限定的宽度和高度让view在这个范围内进行自适应size
  2. 在某个View中重写sizeThatFits,并返回需要的尺寸
  • sizeToFit

当调用UIView的sizeToFit后会调用sizeThatFits方法来计算UIView的bounds.size然后改变frame.size,注意 Autolayout 约束过的 view 该方法失效

- (CGSize)sizeThatFits:(CGSize)size {

CGFloat w = size.width;
w -= 2 * _margin;
_cacheSize1 = [_label1 sizeThatFits:CGSizeMake(w, MAXFLOAT)];
_cacheSize2 = [_label2 sizeThatFits:CGSizeMake(w, MAXFLOAT)];
CGFloat h = 3 * _margin + _cacheSize1.height + _cacheSize2.height;
return CGSizeMake(size.width, h);
}

IDLCustomView *customerView = [[IDLCustomView alloc] init];
......

[customerView sizeToFit];
[self.view addSubview:customerView];

3.4.2 自动布局

自动布局相对于frame布局来说多出了一步–约束计算,整个自动布局包括如下三个阶段:

1. 约束计算: 从子View->父View,系统会计算并给视图设置所有要求的约束,更新约束或者手动调用setNeedsUpdateConstraints都会触发这个过程。
2. 布局阶段: 从父View->子View,布局引擎计算并设置视图和子视图的frame,这个阶段也可以通过手动和自动方式触发,上面已经做过详细的介绍了。
3. 显示阶段: 从父View->子View,重绘视图的内容,如果实现了draw 方法则会被调用。

1. updateConstraints

这个方法绝不能显式调用,而应该被重载,需要注意的是在updateConstraints方法中只做必须要更新的约束。约束的初始化工作一般在视图的初始化方法或者 viewDidLoad() 方法中指定,通常情况下,开启或者关闭 constrains、更改 constrain 的属性或者从视图层级中移除一个视图时都会设置一个内部的标记,这个标记会在下一个更新周期中触发updateConstrains。当然和布局一样也可以手动触发updateConstrains,比如setNeedsUpdateConstraints,updateConstraintsIfNeeded。

2. setNeedsUpdateConstraints

setNeedsUpdateConstraints和setNeedsLayout一样会保证在下一次更新周期中更新约束。

3. updateConstraintsIfNeeded

updateConstraintsIfNeeded 和 layoutIfNeeded一样会立即触发updateConstraints(),而不会等到主线程Runloop进入休眠前调用。

4. invalidateIntrinsicContentSize

自动布局中某些视图拥有intrinsicContentSize属性,这是视图根据它的内容得到的自然尺寸,它是由所包含的元素的约束决定,但也可以通过重载提供自定义行为。我们可以调用invalidateIntrinsicContentSize()方法设置一个标记表示这个视图的intrinsicContentSize 已经过期,需要在下一个布局阶段重新计算。

3.4.3 显示

1. setNeedsDisplay

类似于setNeedsLayout,setNeedsUpdateConstraints,它会将需要重绘的视图标记为脏视图,但是不会立刻调用而是在下一个Update Cycle 中遍历所有已标标记的视图,并调用它们的draw 方法,如果只想重绘部分视图,可以调用 setNeedsDisplayInRect,并把需要重绘的矩形部分传进去。

2. drawRect

UIView的drawRect方法类似于视图布局的layoutSubviews是重绘视图的节点,但是不同于layoutSubviews,drawRect方法不会触发后续对视图的子视图方法的调用,视图的显示方法里没有类似布局中的layoutIfNeeded 这样可以触发立即更新的方法。

还需要注意的是layoutSubviews方法会先于drawRect调用。

3.5AutoresizingMask

AutoresizingMask 仅适用于约束父子控件之间的关系,我们知道UIView有个autoresizingMask属性,当UIView的autoresizesSubviews是YES时, 那么一旦bounds发生了变化,当前view的子view会根据它自身的autoresizingMask属性来自动适应其与superView之间的位置和大小。

需要注意的是Autoresizing只能设置父子视图之间的关系,不能设置兄弟视图之间的关系,当然也不能设置完全不相关的两个视图之间的关系。
autoresizing是约束子控件和父控件之间的位置关系的,UIViewController的根view并没有父控件,所以不能通过autoresizing来约束UIViewController的根view。

autoresizingMask是一个枚举类型,默认值为UIViewAutoresizingNone

typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) {
UIViewAutoresizingNone = 0,
UIViewAutoresizingFlexibleLeftMargin = 1 << 0,
UIViewAutoresizingFlexibleWidth = 1 << 1,
UIViewAutoresizingFlexibleRightMargin = 1 << 2,
UIViewAutoresizingFlexibleTopMargin = 1 << 3,
UIViewAutoresizingFlexibleHeight = 1 << 4,
UIViewAutoresizingFlexibleBottomMargin = 1 << 5
};

UIViewAutoresizingNone view的frame不会随superview的改变而改变
UIViewAutoresizingFlexibleLeftMargin 自动调整view与superview左边的距离保证右边距离不变
UIViewAutoresizingFlexibleRightMargin 自动调整view与superview右边的距离保证左边距不变
UIViewAutoresizingFlexibleWidth 自动调整view的宽,保证与superView的左右边距不变
UIViewAutoresizingFlexibleHeight 自动调整view的高,保证与superView的顶部和底部距离不变
UIViewAutoresizingFlexibleTopMargin 自动调整view与superview顶部的距离保证底部距离不变
UIViewAutoresizingFlexibleBottomMargin 自动调整view与superview底部部的距离保证顶部距离不变
3.6 AutoLayout
3.6.1 AutoLayout 工作原理

iOS 6 之后推出了AutoLayout,它主要是为了替代Autoresizing,AutoLayout和Autoresizing是不能在同一个项目中共存。
AutoLayout主要由基于Cassary线性方程解析引擎,约束规则组成的,我们在开发的时候会以比较直观的方式对视图控件添加约束,比如某个视图的左边距距离另一个视图的右边边距20pt的距离,每条约束会被转换成一个多元一次方程,一个视图往往有多条约束,这样每个视图都会形成一个多元一次方程组,这些方程组作为布局引擎的输入,经过布局引擎计算后得到视图的frame数据。再进入布局流程,完成整个页面的布局。

下图是整个Auto Layout的流程图:

为一个View添加约束需要如下步骤:

1. 设置View的translatesAutoresizingMaskIntoConstraints属性为NO
2. 根据实际需要的效果创建约束
3. 把约束添加到对应位置,iOS 8+直接通过active激活某条约束;
3.6.2 AutoLayout 约束的组成

下图是一个约束条件组成部分:

  • Item1,Item2:表示该约束关系对应的两个视图,当约束等式表示尺寸时,其中一个Item为nil。
  • Attribute1,Attribute2:NSLayoutAttribute类型,表示约束属性。当约束等式表示尺寸时,其中一个Attribute为NSLayoutAttributeNotAnAttribute,表示占位,无任何意义。

目前可用的约束属性有如下几种类型:

约束属性 意义
NSLayoutAttributeWidth NSLayoutAttributeHeight 视图的尺寸:宽、高
NSLayoutAttributeLeft NSLayoutAttributeRight 视图的X轴方向的位置:左、右
NSLayoutAttributeLeading NSLayoutAttributeTrailing 视图的X轴方向的位置:前、后
NSLayoutAttributeTop NSLayoutAttributeBottom 视图Y轴方向的位置:顶、底
NSLayoutAttributeBaseline 视图Y轴方向的位置:底部基准线
NSLayoutAttributeCenterX NSLayoutAttributeCenterY 视图的中心点:视图在X轴的中心点、视图在Y轴的中心点
  • Relationship:NSLayoutRelation类型,表示约束关系,可以是如下几种关系:
约束关系 意义
NSLayoutRelationLessThanOrEqual 小于等于
NSLayoutRelationGreaterThanOrEqual 大于等于
NSLayoutRelationLessThanOrEqual 等于
  • Multiplier:CGFloat类型,表示倍数关系,一般用于尺寸
  • Constant:CGFloat类型,表示常数。
  • UILayoutPriority: 无论是我们创建的约束,还是系统创建的约束(IntrinsicContentSize相关的约束)都必须指定一个约束优先级
    UILayoutPriority,布局引擎按照线性方程的优先级从高到底对线性方程组进行解析,当设置的约束欠缺会导致线性方程组有多个解,可能导致视图丢失,错位等问题。当设置的约束过多,会导致线程方程组无解。这会产生约束冲突,同样可能造成布局错误,这时候往往有错误Log输出。
    默认创建出来的约束优先级为UILayoutPriorityRequired(1000),其他优先级小于1000的约束称为可选约束。
    Auto Layout Engine会按优先级从高到低满足约束集合中的每一个约束,如果无法满足某个可选约束,则忽略;当优先级不同的两个约束描述的是同一个布局关系,Auto Layout会跳过优先级较低的约束。
3.6.3 AutoLayout 约束的安装与移除
约束安装

在早期的版本中往哪里添加约束需要我们自己判断,这种情况下一般有三种情况:

  1. 约束为对视图本身宽高尺寸进行约束的时候,则约束直接添加到该视图本身
  2. 约束表示两个视图的布局关系的时候,则将约束添加到两个视图所在的视图树的第一个公共祖先
  3. 约束表示两个有层次关系的view之间的约束关系的时候,则将约束添加到层次较高的view上

约束安装需要调用addConstraints方法来进行

约束移除

移除约束需要使用removeConstraint/removeConstraints进行移除,当视图通过removeFromSuperView被整个移除的时候,与该视图相关的全部约束都会自动移除

iOS 版本之后,Auto Layout推出新的接口。NSLayoutConstraint多了一个active属性,用于激活、失效一个约束。不需要再考虑约束安装位置。原本用于添加、移除约束的接口addConstraint/addConstraints、removeConstraint/removeConstraints,接口文档表示在后续的版本升级将会过期,建议避免使用。

UIView *grayView = [[UIView alloc] init];
grayView.backgroundColor = [UIColor lightGrayColor];
[self.view addSubview:grayView];
grayView.translatesAutoresizingMaskIntoConstraints = NO;

NSLayoutConstraint *left = [NSLayoutConstraint constraintWithItem:grayView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeft multiplier:1.0 constant:50];
NSLayoutConstraint *top = [NSLayoutConstraint constraintWithItem:grayView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTop multiplier:1.0 constant:100];
NSLayoutConstraint *width = [NSLayoutConstraint constraintWithItem:grayView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:100];
NSLayoutConstraint *height = [NSLayoutConstraint constraintWithItem:grayView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:100];

[self.view addConstraints:@[left, top]];
[grayView addConstraints:@[width, height]];

// iOS8+
// left.active = YES;
// top.active = YES;
// width.active = YES;
// height.active = YES;

3.6.4 Alignment Rect

在AutoLayout中约束使用的是Alignment Rect 而不是frame,这两者有什么区别呢?一般情况下是没有区别的,只有在一个图片包含阴影、外边框、角标等修饰元素的时候,frame和Alignment Rect才存在差别,结合下面图片可以很明显看出二者的差异。

(a) 是设计给出的切图 ,(c) 是切图的frame,它会将阴影和角标的外边距包括进去,但是这会导致一个问题就是center和right会偏移我们想要的位置,这在居中对齐的时候是很麻烦的一件事情,这种情况下就可以使用Alignment Rect来指定(b)中的框框为自动布局的外框。

如果视图是UIImageView,可以通过UIImage的方法imageWithAlignmentRectInsets来调整对齐矩形,插入内边距。

举个例子,设计给出一个右下角分别有20间距的阴影,导致其中心位置稍稍偏高和偏左,这时候我们要使用imageWithAlignmentRectInsets取出指定矩形区域内的图像。这时候就需要定义一个inset表示距离矩形的顶边、左边、底边和右边的间隙,用来描述从矩形的边移进(使用正值)或移出(使用负值)多远。

UIImage *image = [[UIImage imageNamed:@"Shadowed.png"]  
imageWithAlignmentRectInsets:UIEdgeInsetsMake(0, 0, 20, 20)];
UIImageView *imageView = [[UIImageView alloc] initWithImage:image];

对于UIView它提供了方法对应的方法我们在子类中重写它就可以达到效果,由 frame 得到 alignment rect 可以使用:


// The alignment rectangle for the specified frame.
- (CGRect)alignmentRectForFrame:(CGRect)frame;

当然也可以从 alignment rect 反过来得到 frame:

// The frame for the specified alignment rectangle.
- (CGRect)frameForAlignmentRect:(CGRect)alignmentRect;

3.6.5 IntrinsicContentSize

一般情况下,要确定一个View的布局需要确定位置及尺寸,但是对于那些拥有固有内容大小的View,只需要指定它的位置约束就可以了,首先需要明确下IntrinsicContentSize的情况:

  • UIView 没有 IntrinsicContentSize,但是在自定义的View中,可以覆盖intrinsicContentSize方法来返回Intrinsic Content Size,并可以通过调用invalidateIntrinsicContentSize来通知布局系统在下一个布局过程采用新的Intrinsic Content Size。
  • UISlider 在iOS下只定义了width
  • UILabel、UIButton、UISwitch、UITextField的IntrinsicContentSize同时存在width、height
    其中UILabel、UIButton与视图文字数量、字体大小相关,即使没有设置内容,也有IntrinsicContentSize;
  • UITextView、UIImageView的IntrinsicContentSize是动态变化的
    UITextView 的 IntrinsicContentSize 与内容、是否可滚动、约束相关。
    UIImageView 当没有设置image就没有IntrinsicContentSize,当设置了image,IntrinsicContentSize就是设置的image对应的Size;
3.6.6 Compression Resistance && Content Hugging

Compression Resistance 压缩阻力,在有外界力量向内压缩的时候这个值用于表示抗压缩的能力
Content Hugging 内容凝聚力,在有外力将Uiview向外面拉的时候,这个值用于表示拉伸的阻力

对于同一个View,Content Hugging和Compression Resistance不会同时起作用。当一个Label有文字的时候,label会存在一个内容的Size。
如果有外力让其size扩张,Content Hugging会起作用,外力大于Content Hugging的力量,label的size由外力决定,反之,label的Size由内容决定。
如果有外力让其size压缩,Compression Resistance会起作用,外力大于Compression Resistance的力量,label的size由外力决定,反之,label的Size由内容决定。

[label1 setContentCompressionResistancePriority:UILayoutPriorityRequired-1 forAxis:UILayoutConstraintAxisHorizontal];
[label1 setContentHuggingPriority:UILayoutPriorityRequired-1 forAxis:UILayoutConstraintAxisHorizontal];
3.7 NSLayoutAnchor

NSLayoutAnchor 是iOS 9之后推出的一种比NSLayoutConstraint更方便的方案,它将NSLayoutAnchor作为UIView的属性:

布局锚点类型 对应的子类 布局属性
X轴方向 NSLayoutXAxisAnchor leadingAnchor、trailingAnchor、leftAnchor、rightAnchor、centerXAnchor
Y轴方向 NSLayoutYAxisAnchor topAnchor、bottomAnchor、centerYAnchor、firstBaselineAnchor、lastBaselineAnchor
尺寸 NSLayoutDimension widthAnchor、heightAnchor

这里有一篇关于NSLayoutAnchor写得比较好的文章可以供大家深入学习。

3.8 其他比较少用的布局方式
3.8.1 VFL

使用字符串来描述约束关系,这种没有校验的方式,一般项目都不会采用。

3.8.2 sizeClass

sizeClass 只能用于StoryBoard,个人比较偏向在项目中使用纯代码实现布局,StoryBoard编译速度慢,并且团队合作的情况下如果出现冲突很难解决。

3.8.3 Interface Builder

同上原因:不喜欢用storyboard,喜欢纯代码方式实现布局。

3.8.4 UIStackView

UIStackView 是iOS 9 推出的布局方案,如果需要兼容之前的版本可以使用FDStackView这个开源库,UIStackView在某些成行成列的布局中比较好用,比如九宫格布局之类的,如果是比较复杂的布局,就会有比较深的嵌套,维护起来很蛋疼,建议谨慎使用。

3.8 三方开源布局引擎对比
3.8.1 Masonry 17438 Stars

Masonry是我接触iOS开发时候使用的第一个布局引擎,它也是目前遇到的Stars数量最多的一个布局引擎,如果没有Masonry我会觉得AutoLayout是一个蛋疼的设计,有了Masonry有种从地狱到天堂的感觉,但是它底层是基于AutoLayout,所以在复杂界面性能上较差,因此在项目中期切换到基于frame封装的布局引擎了。但是整个封装还是十分优雅的,在中小型项目或者简单布局完全可以胜任。

3.8.2 YogaKit 12141 Stars

这是我个人项目中用的一个布局引擎,它是一个跨平台的底层基于C++实现的,使用的是目前前端比较火的Flex Box,之所以用它有如下理由:

  1. 它是跨平台的Android,iOS,Reactive Native都可以使用,在iOS平台上对应的库为YogaKit,在Android 平台上对应的库为Litho
  2. 基于FlexBox的这个是前端必须掌握的布局模式,使用它可以缩小下技术栈。
  3. 轻量,易用,基于frame布局性能上面有保证

其实最重要的原因还是想统一下技术栈。

3.8.3 ComponentKit 5053 Stars

ComponentKit 是Facebook推出的,它最初是为Facebook News Feed开发的,目前它被应用于Facebook的全部iOS应用。它可以在后台对UI提前测量和布局,因此不会阻碍UI线程。在 ComponentKit中我们处理的对象是一个个Component,它是一个NSObject子类,而不是UIView,我们在描述布局的时候只需要描述component,然后提交给ComponentKit渲染,如果UI有变化,只需要重新生成component然后全部刷新,ComponentKit负责处理这些变化。总结来说ComponentKit有如下优点:

  1. 将对UI的变化转换成状态的变化
  2. 可以在后台进行测量和布局
  3. 可以对布局进行缓存

它使用起来有点像Flutter的UI布局,对于描述复杂界面,整个代码会显得很庞大,不过结合懒加载写法这一点可以克服,后续有机会会在自己的业余项目中体验下它,这里就不再做介绍了。

3.8.4 LayoutKit 2872 Stars

说实话在选型的时候LayoutKit有吸引到我,如果不是因为Yoga可以跨平台我可能会选择它,下面是LayoutKit文档中对它的描述:

LayoutKit has many benefits over using Auto Layout:

  • Fast: LayoutKit is as fast as manual layout code and is significantly faster than Auto Layout.
  • Asynchronous: Layouts can be computed in a background thread so user interactions are not interrupted.
  • Declarative: Layouts are declared with immutable data structures. This makes layout code easier to develop, document, code review, test, debug, profile, and maintain.
  • Cacheable: Layout results are immutable data structures so they can be precomputed in the background and cached to increase user perceived performance.

LayoutKit also provides benefits that make it as easy to use as Auto Layout:

UIKit friendly: LayoutKit produces UIViews and also provides an adapter that makes it easy to use with UITableView and UICollectionView.
Internationalization: LayoutKit automatically adjusts view frames for right-to-left languages.
Swift: LayoutKit can be used in Swift applications and playgrounds.
Tested and production ready: LayoutKit is covered by unit tests and is being used inside of recent versions of the LinkedIn and LinkedIn Job Search iOS apps.
Open-source: Not a black box like Auto Layout.
Apache License (v2): Your lawyers will be happy that there are no patent shenanigans.

总结起来就是: 速度块, 异步,对结果进行缓存,可以很方便得支持从右往左的语言,可以在Swift语言中使用。

3.8.5 MyLinearLayout 3503 Stars

MyLinearLayout是国内开发者开发的它是基于frame布局的方式,TangramKit是它的Swift版本,MyLayout功能强大而且简单易用,不论在功能,用法还是性能上都是一个不错的布局引擎。也是个十分值得推荐的布局引擎。

3.8.6 Texture 5737 Stars

Texture 内部支持ASLayoutSpect 和 Yoga 两种布局引擎,但是因为如果整个引入会显得太过庞大,目前还没考虑切换到Texture 不过后续会在列表页面使用它Texture + IGListKit应该是一个不错的组合。

0. 开篇叨叨

这篇博客主要从原理的角度来向大家介绍下一个界面是如何在CPU和GPU的配合下显示到屏幕上的,后续博客还会对Core Graphic,OpenGL, Core Animation的使用一一介绍,优化部分也会单独抽到后续的博客中进行介绍。下面是该博客的内容目录:

  • iOS 渲染框架组成概述
  • 界面渲染的整体流程
  • 离屏渲染
1. iOS 渲染框架组成概述

上图是iOS 渲染框架组成部分,其中UIKit位于最上层,组成界面的各个元素基本上都来自UIKit,我们可以给它设置布局,可以通过绘制改变它的显示内容,除此之外还负责事件的接收,其实界面的显示是由它的一个被称为图层的属性CALayer来完成的。这个放在后面详细介绍。UIKit的下一层是Core Animation,最开始接触iOS的时候,我一直以为Core Animation只是用于生成动画的,实际上动画的生成只是Core Animation的冰山一角,可以说在iOS上绝大多数的原生控件都是通过Core Animation绘制出来的,Core Animation在这里最重要的任务是尽可能快地合成图层送到下一级。位于Core Animation之下是Open GL 以及 Core Graphic,其中Open GL 使用GPU进行渲染,而Core Graphic则是使用Qurtaz 2D引擎使用CPU进行渲染,这里个人理解Core Graphic不单单只有CPU参与,最终渲染到屏幕上还是需要GPU参与,这部分在下个小结中将会进行详细介绍,各个GPU厂商的实现是不同的,为了隔离这个不同,在GPU的上层添加了GPU驱动层,经过GPU处理后的数据会放到帧缓冲区中,最终显示到显示器上。

2.界面渲染的整体流程

2.1 UIView && CALayer

对于UIView和CALayer大家都比较熟悉,UIView内部包含着一个CALayer属性,它继承自NSObject,负责界面上的内容显示,而UIView继承自UIResponder它作为CALayer的CALayerDelegate负责事件的响应,以及创建并管理它的图层,以确保当子视图在层级关系中添加或者被移除的时候,它们关联的图层也同样对应在层级关系树当中有相同的操作。每个View被创建的时候都会自动创建一个CALayer,同时还可以在后续的操作中添加多个layer。

我们先来看下UIView 和 CALayer的结构:
上面我们介绍了二者的分工,UIView 负责事件的响应,CALayer负责内容的显示,但是为什么需要有这样的分工?归根到底是因为Mac上和iPhone上的事件存在很大的区别?iPhone 上的事件绝大多数是屏幕触摸事件,而Mac上还有鼠标,键盘等事件,但是显示上却是高度一致的,因此就可以将这部分显示的给拎出来,作为CALayer单独存在。
CALayer有个id类型的contents属性,它指向内存中的一个成为backing storage的存储空间。往contents上赋值的时候就会将图片存储到这个backing storage中,这里虽然是id类型,但是如果传递其他类型进去会不显示,这里为什么使用id类型而不是明确的CGImageRef,也还是为了兼容,因为图像类型在Mac OS中是NSImage类型而在iOS上却是CGImageRef类型。

下面是整个UIView 和 CALayer的结构图:

接下来看下最重要的一点:我们怎么将要显示的内容绘制到CALayer上,下图是整个流程,总共分成两大分支:

  1. 第一个分支是通过给layer.contents赋值,将内容绘制到CALayer 默认的backing store上,在我们调用[UIView setNeedsDisplay]的时候,会间接触发[view.layer setNeedsDisplay],紧接着调用[view.layer display] 在这个方法中会判断delegate 是否实现了displaylayer如果有则将layer传递出去,在这里可以对contents进行赋值,也就是说可以选择覆写CALayer的display方法为content赋值,或者直接对CALayer的content赋值,或者作为代理为在displaylayer方法中对content赋值。

比如SDAnimatedImageView中有如下代码:

- (void)displayLayer:(CALayer *)layer {
UIImage *currentFrame = self.currentFrame;
if (currentFrame) {
layer.contentsScale = currentFrame.scale;
layer.contents = (__bridge id)currentFrame.CGImage;
}
}

YYText中的*YYTextAsyncLayer**覆写了display方法

- (void)display {
super.contents = super.contents;
[self _displayAsync:_displaysAsynchronously];
}
  1. 第二个分支开始的时候会创建一个新的backing store,然后开始走drawInContext,这时候会先看delegate是否实现了drawRect如果有则用drawRect,否则调用drawLayer:inContext:并将管理新建backing store的context传递出来。
    这里需要注意的是drawRect: 方法没有默认的实现,因为对UIView来说,寄宿图并不是必须的,但是如果UIView检测到drawRect: 方法被调用了,它就会为视图分配一个寄宿图。因此如果没有自定义绘制的任务就不要在子类中写一个空的-drawRect:方法。

因此只有在创建单独的图层外,我们很少会用到CALayerDelegate,因为在UIView创建了它的Root Layer时,它就会自动地把Root Layer的delegate设置为它自己,并内部提供了-displayLayer:的实现。如果我们需要重新对控件进行自定义也不必实现-displayLayer:和-drawLayer:inContext:方法。通常做法是实现UIView的-drawRect:方法,UIView就会帮你做完剩下的工作,包括在需要重绘的时候调用-display方法。

2.2 Core Graphics && 图片加载

从上面的流程可以看出,可以通过两种方式给一个控件显示需要展示的内容,一种是通过设置layer的contents,一种是通过覆写对应的代理方法,在代理方法中会传出新建backing store的CGContextRef,我们可以使用它向backing store中绘制内容。这里需要注意两点:

  1. 使用第一种方式的时候,由于从磁盘文件中加载出来的图片文件往往是经过压缩的,因此在将它设置到contents之前需要对图片进行解压,这一步是可以在后台线程完成的,这往往是一个性能优化点。
  2. 使用第二种方式的时候需要注意的是这种方式会新建一个backing store所以是十分耗费资源的。这也是尽量不要使用重写drawRect来绘制界面的原因。

最后需要明确一点经过这个步骤我们得到的是Bitmap数据,这个Bitmap将会被输送到后续流程。

2.3 在提交到Render Server前都做了什么工作

在一个界面从开始到提交到Render Server前一共可以分成三个步骤:

* Layout
* Prepare && Display
* Commit
2.3.1 Layout

在一个控件被加到界面上的时候,首先会触发控件的布局,从而确定出整个层级树中每个控件的frame。这部分大家可以看下我之前写的iOS布局总结,这里就不做重复介绍了。

2.3.2 Prepare && Display

这部分主要是由CPU参与,在2.1 和 2.2已经描述得相对比较详细了,这部分会涉及到图片的解码,文本绘制,或者通过CALayer暴露出来的CGContextRef在backing store中进行绘制。图片解码一般发生在Prepare阶段。存储在backing store的 bitmap后续就会被打包送到Render Server中。

2.3.3 Commit

当RunLoop即将进入休眠期间或者即将退出的时候,会通过已经注册的通知回调执行_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv函数,在这个函数会递归将待处理的图层进行打包压缩,并通过IPC方式发送到Render Server,这里还需要提到一点:这时候的Core Animation会创建一个OpenGL ES纹理并将2.3.2 中backing store中的位图上传到对应的纹理中。

2.4 在送到GPU之前Core Animation 都做了什处理

Render Server在拿到压缩后的数据的时候,首先对这些数据进行解压,从而拿到图层树,然后根据图层树的层次结构,每个层的alpha值opeue值,RGBA值、以及图层的frame值等对被遮挡的图层进行过滤,最终得到渲染树,渲染树就是指将图层树对应每个图层的信息,比如顶点坐标、顶点颜色这些信息,抽离出来,形成的树状结构。渲染树就是下一步送往GPU进行渲染的数据。

2.5 在GPU中做了哪些处理

这步骤输入的是两类数据,一个是渲染指令,一个是2.4 生成的顶点,以及对应的纹理数据,输出的是像素数据

整个管线从整体来讲可以分成两大阶段:

  1. 将3D 坐标转换到2D 的屏幕坐标系
  2. 把2D 坐标系转换为有颜色的像素值。

往细得分可以分成六个阶段:

  • 顶点着色器(Vertex Shader)
  • 图元装配(Shape Assembly)
  • 几何着色器(Geometry Shader)
  • 光栅化(Rasterization)
  • 片段着色器(Fragment Shader)
  • 测试与混合(Tests and Blending)

下面是整个流程的示意图:

其中蓝色部分是代表着色器,着色器是运行在GPU上的非常独立的可编程小程序,可以通过这些小程序来控制整个管线的各个部分。

2.5.1 顶点着色器

在Render Server 拿到顶点数据并输入到渲染管线的时候,顶点着色器会对每个顶点数据进行一次运算,每个顶点都对应一组顶点数组,这些数组可以用于存储:顶点坐标,表面法线,RGBA颜色,辅助颜色,颜色索引,雾坐标,纹理坐标以及多边形边界标志等。

  • Step 1 模型坐标系 –> 世界坐标系

模型坐标系是为了方便建立模型而设立的坐标,在模型坐标系中我们不用考虑模型显示在屏幕的哪个位置,它是模型的自身坐标系,描述的是模型的各个部分相对于模型原点的坐标值。
要理解世界坐标系就需要先理解世界这个概念,在一个世界中可以存在很多模型,打个比方,整个银河系是一个世界,这个世界上存在很多的行星,这里的行星可以看成是一个个模型,模型本身也有它的坐标系,在Step 1 阶段就是将模型安放到制定的世界中,并将模型上的坐标转换为在这个世界中的坐标值。


上图第一张为三个茶壶各自的模型坐标系,第二张表示三个茶壶被放置到同一个世界坐标系的时候各个茶壶的坐标情况。

  • Step 2 世界坐标系 –> 相机坐标系

在将多个模型放到同一个世界上并拥有同一个世界坐标系后,就需要考虑另一个问题,从哪个视角捕获我们想要的场景,相机坐标系中的坐标,就是从相机的角度来解释世界坐标系中位置,OpenGL中相机始终位于原点,指向-Z轴,而以相反的方式来调整场景中物体,从而达到相同的观察效果。

  • Step 3 相机坐标系 –> 裁剪坐标系

投影是顺着相机的视角,将物体投射到屏幕上,投影方式有很多种,OpenGL中主要使用两种方式,即透视投影和正交投影,经过投影后我们获得的是二维的图像。

  • Step 4 规范化设备坐标系 –> 屏幕坐标系

这个步骤最终决定生成的二维图像到底显示在屏幕的什么位置和显示窗口的大小。

2.5.2 图元装配

该阶段将顶点着色器输出的所有顶点作为输入,并将所有的点装配成指定图元的形状。图元是渲染的单位,用于表示如何渲染顶点数据,OpenGL ES 支持三种图元—– 点、线、三角形。也就是说图元装配的过程就是将顶点连接起来,形成一个个所支持的图元元素。

2.5.3 几何着色器

几何着色器把图元形式的一系列顶点的集合作为输入,它可以通过产生新顶点构造出新的图元来生成其他形状,它是一个可选的阶段。

2.5.4 光栅化

光栅化会把图元映射为最终屏幕上相应的像素,生成供片段着色器使用的片段。在片段着色器运行之前会执行裁切。裁切会丢弃超出你的视图以外的所有像素,用来提升执行效率。

2.5.5 片段着色器

在经过光栅化后得到的是一个一个片段,在介绍片段着色器之前,先了解下什么是片段,OpenGL中的一个片段是OpenGL渲染一个像素所需的所有数据,它包含位置,颜色,纹理坐标等信息。这些值是由图元的顶点信息进行插值计算得到的。片段着色器的主要目的是计算一个像素的最终颜色,这也是所有OpenGL高级效果产生的地方。通常,片段着色器包含3D场景的数据(比如光照、阴影、光的颜色等等),这些数据可以被用来计算最终像素的颜色。

其中贴图是最重要的部分,我们的 Resources资源中,可以包含纹理等数据,片段着色器可以根据顶点着色器输出的顶点纹理坐标对纹理进行采样,以计算该片段的颜色值。从而调整成各种各样不同的效果图。除了纹理贴图外另一个很重要的功能就是光照特效:我们可以传给片段着色器一个光源位置和光源颜色,就可以根据一定的公式计算出一个新的颜色值,这样就可以实现光照特效。

2.5.6 测试和混合阶段

这个阶段检测片段的对应的深度值,用它们来判断这个像素是其它物体的前面还是后面,决定是否应该丢弃。这个阶段也会检查alpha值并对物体进行混合(Blend)。所以,即使在片段着色器中计算出来了一个像素输出的颜色,在渲染多个三角形的时候最后的像素颜色也可能完全不同。总得来说这个阶段主要决定同一个位置的物体到底哪一个可以显示在屏幕上以及颜色的混合。

到目前位置我们拿到各个点的最终像素值,最后我们要做的就是将这些像素值写到帧缓存器中,等待VSync信号到来。

2.6 显示的原理

在介绍显示原理前需要先了解下CRT显示器的原理。CRT 的电子枪会从屏幕的左上角从上到下一行行扫描,扫描完成后显示器就呈现一帧画面,随后电子枪回到初始位置继续下一次扫描。但是这里存在生产者消费者现象,对于屏幕而言它是消费者,而GPU是生产者,为了同步二者的节奏显示器通过用硬件时钟产生一系列的定时信号,当电子枪换到新的一行,准备进行扫描时,显示器会发出一个水平同步信号,简称 HSync,而当一帧画面绘制完成后,电子枪回复到原位,准备画下一帧前,显示器会发出一个垂直同步信号,简称 VSync,并且在显示器和GPU之间使用了双缓存机制,在显示器显示某帧数据的时候,GPU可以往另一个缓存中提交渲染好的数据,在VSync信号到来的时候,视频控制器切换到另一个缓存用于显示,也就是说在Vsync信号到来的时候另一个缓存必须填满渲染数据,也就是之前的步骤必须完成,在iOS设备中,每秒60帧,每帧16.7ms。那么如果在16.7ms还没渲染完呢?这时候视频控制器就不会将缓存切换到未完成的帧,而是继续显示当前的内容。这就给人们带来视觉上的卡顿。因此在UI线程尽量少处理耗时操作。


2.7 动画渲染

iOS 动画的渲染也是基于上述 Core Animation 流水线完成的,所以大致的流程也是类似的,但是它需要CAdisplayLink 定时器协助下完成整个动画。

1. 调用 animationWithDuration:animations: 方法
2. 在 Animation Block 中进行 Layout,Display,Prepare,Commit 等步骤。
3. Render Server 根据 Animation 逐帧进行渲染。
3.离屏渲染
3.1 什么是离屏渲染

上面介绍的渲染为当前屏幕渲染(On-Screen Rendering),也就是GPU的操作是在当前用于显示的屏幕缓冲区中进行的,但是还有一种渲染模式为离屏渲染,它发生在某些图层元素未预先合成之前不能直接在当前屏幕上绘制的情况下,这种情况下系统会新开一个缓冲区,在这里进行渲染操作。

3.2 触发离屏渲染的因素

离屏渲染一般发生在如下几种情况:

* shouldRasterize = YES(开启光栅化)
* edge antialiasing(抗锯齿)
* group opacity(不透明)

* circleRadius (圆角)
* masks(遮罩)
* shadows(阴影)
3.3 为什么离屏渲染会比较耗时

之所以耗时是因为离屏渲染涉及到两个开销较大的操作:

  1. 创建新缓冲区

要想进行离屏渲染,首先要创建一个新的缓冲区。

  1. 上下文切换

离屏渲染的整个过程,需要多次切换上下文环境:先是从当前屏幕切换到离屏;等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上有需要将上下文环境从离屏切换到当前屏幕。而上下文环境的切换是要付出很大代价的。

所以能够避免离屏渲染尽量避免。

下图为Marks离屏渲染过程图,可以看到它其实是分两大部分,一部分用于渲染图层纹理,一部分用于生成遮罩纹理。最后再将这两部分合成。

App 生命周期
应用程序的五种状态

应用程序的五种状态:

  1. 未运行状态:程序尚未启动,或者应用正在运行但是中途被系统停止,当设备内存紧张的时候,也会将挂起的应用当前状态写入到内存,然后退出应用并释放内存,这时候我们虽然能够在任务栏看到图标,但是它已经退出,我们称之为应用墓碑。

  2. 未激活状态:当前应用正在前台运行,但是焦点被其他抢去。比较典型的是用户锁屏或者离开应用去响应来电,信息等事件等时候。还有一种是比较常见的就是在不同状态切换的时候会短暂处于该状态,这时候App会停止运行,但是依然占用内存空间,用于保存当前状态。

  3. 激活状态:当前应用正常运行,应用焦点在当前应用上,所有的事件都会被分发到当前应用,这时应用占用内存和CPU时间。

  4. 后台状态:当前应用还是存活的,并且能够执行代码,但是默认处于这种情况的时间不长(最多十分钟),当我们按下Home键的时候会进入后台状态,如果不继续申请在后台运行的时间会快速进入挂起状态。

  5. 挂起状态: 应用处在后台,并且已经停止执行代码。这时候应用还驻留在内存中,并没有被系统完全回收,只有在系统发出低内存告警的时候,系统才会把处于挂起状态的应用清除出内存给前台正在运行的应用。这时候不占用CPU资源,但是内存依然占用。

AppDelegate

App 生命周期的切换会通过AppDelegate来通知开发者,因此我们可以在AppDelegate中的关键方法中注入我们需要的逻辑从而完成任务,下面是一些常见方法:

//告诉代理进程启动但还没进入状态保存
- (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions
// 启动基本完成,程序准备开始运行
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
// 当应用程序将要进入非活动状态执行,在此期间,应用程序不接收消息或事件,比如打来电话
- (void)applicationWillResignActive:(UIApplication *)application
// 当应用程序进入活动状态执行,此方法跟上面那个方法相反
- (void)applicationDidBecomeActive:(UIApplication *)application
// 当程序被推送到后台的时候调用。所以要设置后台继续运行,则在这个函数里面设置即可
- (void)applicationDidEnterBackground:(UIApplication *)application
// 当程序从后台将要重新回到前台时候调用,此方法跟上面的那个方法相反
- (void)applicationWillEnterForeground:(UIApplication *)application
// 当程序将要退出是被调用,通常是用来保存数据和一些退出前的清理工作
- (void)applicationWillTerminate:(UIApplication *)application

一些关键场景下的状态及回调
  • 应用冷启动:


相关方法:

-[AppDelegate application:didFinishLaunchingWithOptions:]
-[AppDelegate applicationDidBecomeActive:]
  • 应用热启动 - 点击图标再次打开程序:


-[AppDelegate applicationWillEnterForeground:]
-[AppDelegate applicationDidBecomeActive:]
  • 解锁:
-[AppDelegate applicationWillEnterForeground:]
-[AppDelegate applicationDidBecomeActive:]
  • 按下home键,应用进入后台:


-[AppDelegate applicationWillResignActive:]
-[AppDelegate applicationDidEnterBackground:]
  • 锁屏:

-[AppDelegate applicationWillResignActive:]
-[AppDelegate applicationDidEnterBackground:]
  • 其他应用中断了当前应用:


-[AppDelegate applicationWillResignActive:]

返回

-[AppDelegate applicationDidBecomeActive:]
如何优化性能

主要是如下几个地方:

  1. applicationWillResignActive: 在应用失去焦点的时候:
    可以停止视频播放,游戏播放,减少帧率,挂起不必要的操作队列。
  2. applicationDidEnterBackground:在应用进入后台的时候:
    这时候需要保存用户数据或状态信息,在进入后台时,写到磁盘去,因为程序可能在后台被杀死。再有就是释放尽可能释放的内存。
    可以通过去掉对大图片,大视频,大文件的强引用,在回收的时候就可以先回收这部分资源。
  3. applicationWillTerminate:在系统内存中,必须清除无用数据,但是这个方法有时间限制,默认是5s。
    如果超过时间还有未完成的任务,我们的程序就会被终止而且从内存中清除
App后台限制

iOS设备为了节省电量一般会使用假后台机制

我们开发的应用在用户按下Home之后,App会转入后台运行,系统默认会给应用短暂的后台时间(iOS7 180s,iOS6 600s),来让用户做一些必要的处理,如果这段时间过去之后,还是可以告诉系统未完成任务,还需要点时间,通过向系统发出申请来争取更多的时间,系统批准申请后可以继续运行,但是不会超过10分钟。这10分钟过去之后,无论怎么向系统申请,系统都会挂起应用,直到用户再次点击后才会继续运行。但是这对于一些应用是完全不够的,比如一款即时通讯软件,一般在后台都需要保持长链接不断,才能保证应用在后台还能收到消息,再比如你的音乐播放器软件,即使处于后台你也希望它能够继续播放音乐。
当然iOS也并非全部任务都不能在后台执行,某些任务比如播放音频,获取位置更新,或者从服务器获取最新内容这些任务是可以在后台长时间运行的,只要我们在应用的配置中,指定应用包含这些服务,一旦审核通过,就可以让我们在后台长时间执行任务,而不会被挂起,具体有哪些模式可以在Capabilities 下的Background Mode中选定指定的类型。

下面有个较好的关于Background Mode的翻译文章可以看下
中文翻译:iOS后台模式开发指南

英文原著:Background Modes Tutorial: Getting Started

但是我们最常见的是通过backgroundTaskIdentifier来申请后台运行的时间,下面是关键代码:

- (void)applicationDidEnterBackground:(UIApplication *)application {
self.backgroundTaskIdentifier = [application beginBackgroundTaskWithExpirationHandler:^{
//后台任务到期,取消任务
[application endBackgroundTask:self.backgroundTaskIdentifier];
self.backgroundTaskIdentifier = UIBackgroundTaskInvalid;
}];
}

- (void)applicationWillEnterForeground:(UIApplication *)application {
//进入前台的时候取消任务
[application endBackgroundTask:self.backgroundTaskIdentifier];
self.backgroundTaskIdentifier = UIBackgroundTaskInvalid;
}
UIViewController生命周期

生命周期方法介绍
1. load、initialize、init
  • 在main函数执行前,会初始化objc运行时环境,这时会加载所有类并调用类的load方法,一般在这个方法中实现方法交换(Method Swizzle);
  • initialize方法会在类第一次收到消息之前被调用,可以用来初始化静态变量;
  • init用来在类的内存被设置好后初始化成员变量.
2. initWithCoder、initWithNibName、awakeFromNib
  • initWithCoder:反归档,如果对象是从文件解析来的就会调用。
  • awakeFromNib:从xib或者storyboard加载完毕会调用。
  • initWithNibName:使用代码创建对象的时候会调用这个方法。

用storyboard创建界面的顺序:

initialize -> initWithCoder -> awakeFromNib -> loadView

用Xib或者纯代码创建界面的顺序:

如果用init函数来初始化:

initialize -> init -> initWithNibName -> loadView

如果用initWithNibName函数来初始化:

initialize -> initWithNibName -> loadView
3. loadView
  • 当你alloc并init了一个ViewController时,这个ViewController是还没有创建view的.

  • loadView用于加载控制器管理的 view,不能直接手动调用该方法

  • UIViewController的self.view是通过懒加载方式创建的,每次调用控制器的view属性时并且view为nil时,loadView函数就会被调用.加载成功后接着调用viewDidLoad函数,如果self.view已经是非空的情况下会直接调用viewDidLoad函数。

  • 如果在loadView函数中自定义了view,那么xib、storyboard中对页面的设置会失效,因为它是在加载之后调用的,所以如果使用 Interface Builder创建view,则务必不要重写该方法

  • [super loadView]默认的逻辑:如果控制器由xib或storyboard初始化,那么会根据xib或storyboard的传入的名字来初始化view;如果没有显示的指定名称,就默认加载和控制器同名的文件;如果没有找到文件,就会创建一个空白的UIView,这个view的frame为屏幕的大小。所以在覆写该方法的时候不应该再调用父类的该方法。

4. viewDidLoad
  • 在loadView执行完成后,view将会被加载到内存这时候调用viewDidLoad主要完成界面的初始化,例如添加子控件,以及约束的初始化
5. viewWillAppear
  • 每次进入页面都会执行viewWillAppear,该方法中可以进行操作即将显示的 view。
6. updateViewConstraints
  • 在该函数中用于更新视图的约束.在控制器的view更新视图布局时,会调用updateViewConstraints函数,可以重写这个函数来更新当前视图的布局.这个函数只有在Autolayout布局的时候才会被调用。初始化约束时,最好写到init或viewDidLoad类似的函数中,updateViewConstraints适合于更新约束
7. viewWillLayoutSubviews
  • 该方法在通知控制器将要布局 view 的子控件时调用。

  • 在这个函数中布局子视图,如果用了Autolayout,那么会在viewWillLayoutSubviews和viewDidLayoutSubviews之间用Autolayout机制布局,但是需要注意的是该方法调用时,AutoLayout 未起作用。

8. viewDidLayoutSubviews
  • 控制器的子视图的布局已完成,这里获取的frame才是最正确的frame。如果用约束来布局,在该函数去设置视图的frame 是无效的。如果用frame来布局的,在该函数中去设置视图的frame是有效的。self.view在该函数中去设置frame是有效的。该方法调用时,AutoLayout 已经完成。
9. viewDidAppear
  • 该方法在控制器 view 已经添加到视图层次时被调用
10. viewWillDisappear
  • view即将消失的时候调用
11. viewDidDisappear
  • 该方法在控制器 view 已经从视图层次移除时被调用
12. dealloc
  • 用于释放自身持有的资源.
13. didReceiveMemoryWarning

当系统内存不足时,当前控制器以及所在的Navigation堆栈上的控制器都会调用didReceiveMemoryWarning函数.该函数会判断当前控制器的view是否显示在window上,如果没有会将view以及子view全部销毁.

14 UI相关代码规范
  1. ViewController init里不要调用self.view,一般在init里应该只有关键数据的初始化。
  2. 如果确实需要重写loadView,在重写的代码中只初始化view,其他的工作放在viewDidLoad方法中完成。
  3. viewDidLoad 这时候view已经有了,可以创建并添加界面上的其他子视图,以及设置这些视图的属性。
  4. viewWillAppear 这个一般在view被添加到superview之前,切换动画之前调用,一般可以用于弹出键盘,status bar和navigationbar颜色设置等。
  5. viewWillLayoutSubViews/viewDidLayoutSubviews viewDidLayoutSubviews的时候frame值已经确定了,可以在这里做一些依赖frame的操作
  6. viewDidAppear 这时候view已经被加入到superview上了。

下面是View加载的流程:

下面是View卸载的流程:

UIView 的关键函数
  • didAddSubview

    Tells the view that a subview was added.

当前View添加子View的时候会被调用

The default implementation of this method does nothing. Subclasses can override it to perform additional actions when subviews are added. This method is called in response to adding a subview using any of the relevant view methods.

- (void)didAddSubview:(UIView *)subview;
  • willMoveToSuperview

Tells the view that its superview is about to change to the specified superview.

在某个View的SuperView改变的时候会调用这个方法

The default implementation of this method does nothing. Subclasses can override it to perform additional actions whenever the superview changes.

- (void)willMoveToSuperview:(UIView *)newSuperview;
  • didMoveToSuperview

Tells the view that its superview changed.

在view已经添加到superview上的时候被调用

The default implementation of this method does nothing. Subclasses can override it to perform additional actions whenever the superview changes.

- (void)didMoveToSuperview;
  • willMoveToWindow

Tells the view that its window object is about to change.

在view的window将要发生改变的时候调用,这个在willDisapear之后的阶段

The default implementation of this method does nothing. Subclasses can override it to perform additional actions whenever the window changes.

- (void)willMoveToWindow:(UIWindow *)newWindow;
  • didMoveToWindow

Tells the view that its window object changed.

在view的window发生改变的时候调用,这个在willDisapear之后的阶段

The default implementation of this method does nothing. Subclasses can override it to perform additional actions whenever the window changes.

- (void)didMoveToWindow;
  • willRemoveSubview

Tells the view that a subview is about to be removed.

在子view将要被移除的时候调用

The default implementation of this method does nothing. Subclasses can override it to perform additional actions whenever subviews are removed. This method is called when the subview’s superview changes or when the subview is removed from the view hierarchy completely.

- (void)willRemoveSubview:(UIView *)subview;
  • removeFromSuperview

Unlinks the view from its superview and its window, and removes it from the responder chain.

将当前View从父View和它的window,以及响应链移除

If the view’s superview is not nil, the superview releases the view.Calling this method removes any constraints that refer to the view you are removing, or that refer to any view in the subtree of the view you are removing.

- (void)removeFromSuperview;

下面是在界面上添加一个view 然后在view的各个关键节点上加上log输出的结果:

-[IDLViewController viewDidLoad] :@@@==viewDidLoad
-[IDLQuarzCoreSampleView willMoveToSuperview:] :@@@======================================willMoveToSuperview:
-[IDLQuarzCoreSampleView didMoveToSuperview] :@@@======================================didMoveToSuperview
-[IDLViewController onViewWillAppear:] :@@@==onViewWillAppear
-[IDLQuarzCoreSampleView willMoveToWindow:] :@@@======================================willMoveToWindow:
-[IDLQuarzCoreSampleView didMoveToWindow] :@@@======================================didMoveToWindow
-[IDLViewController updateViewConstraints] :@@@==updateViewConstraints
-[IDLViewController viewWillLayoutSubviews] :@@@==viewWillLayoutSubviews
-[IDLViewController viewDidLayoutSubviews] :@@@==viewDidLayoutSubviews
-[IDLViewController viewWillLayoutSubviews] :@@@==viewWillLayoutSubviews
-[IDLViewController viewDidLayoutSubviews] :@@@==viewDidLayoutSubviews
-[IDLQuarzCoreSampleView layoutSubviews] :@@@======================================layoutSubviews
-[IDLQuarzCoreSampleView drawRect:] :@@@======================================drawRect:
-[IDLViewController onViewDidAppear:] :@@@==onViewDidAppear

下面是让viewcontroller不可见时候的调用log输出:

-[IDLViewController onViewWillDisappear:] :@@@==onViewWillDisappear
-[IDLQuarzCoreSampleView willMoveToWindow:] :@@@======================================willMoveToWindow:
-[IDLQuarzCoreSampleView didMoveToWindow] :@@@======================================didMoveToWindow
-[IDLViewController onViewDidDisappear:] :@@@==onViewDidDisappear

从上面结果我们做个总结:

  1. 在ViewController viewDidLoad 之后,我们会在父view调用addSubView,将当前view添加到superView上面,这时候会调用当前view的willMoveToSuperview,didMoveToSuperview方法,这之后再去寻找自己的subView并依次添加。在添加子view的过程中didAddSubview会被反复调用。等到所有的subView都在内存层面加载完成了,会调用一次viewWillAppear,这时候会调用当前view的willMoveToWindow,didMoveToWindow然后会把加载好的一层层view,分别绘制到window上面。然后layoutSubview,drawRect,最后onViewDidAppear。

  2. UIView生命周期移除阶段。会先调用willMoveToWindow表示当前view将会从window上移除,继而调用didMoveToWindow,这里需要注意的是这时候传入的window为nil,然后再removeFromSuperView,dealloc,dealloc之后再removeSubview。

  3. 如果没有子视图,则不会接收到didAddSubview和willRemoveSubview消息。

  4. 和superView,window相关的方法,可以通过参数判断是创建还是销毁,如果指针不为空,是创建,如果为空,就是销毁。

  5. removeFromSuperview和dealloc在view的生命周期中,调且只调用一次,可以用来removeObserver,移除计时器等操作。

UIView约束,布局,绘图介绍iOS渲染的那篇博客已经介绍过了,但是这里还是把这块再拎出来介绍下,毕竟不是很大块。

  • 约束相关

  • 监控约束变化 : APP启动后,随着RunLoop的运行,系统在其内部监听着约束变化(Constraints Change):如激活或失效约束、修改优先级、修改常量值,添加,删除视图等操作,都可以导致约束发生变化。

  • 重新计算布局 : 在接收到布局变化后,布局引擎会根据变化的约束重新计算布局,并通过对其父视图调用setNeedsLayout方法将需要更新布局的视图进行标记,之后便进入延迟布局阶段(Deffered Layout Pass)。这里需要注意的是在进入延迟布局阶段之前,Layout Engine已经将更新的约束计算完毕并将视图的新frame求出。但并不在此时更新视图。

  • 延迟布局阶段 :此阶段的主要作用是将错误位置的视图重新定位。其在视图层级中执行,分为两步:

  • 更新约束:从下往上(子视图到父视图),依次遍历视图层级,调用View的updateConstraints方法(或ViewController的updateViewConstraints方法)来更新约束,我们可以在此覆盖本方法来设置自定义约束,且在此设置时,执行效率最高。记得最后调用父类实现。

  • 给视图及子视图重新设定位置(给view的frame赋值):从上到下依次调用View的layoutSubViews方法(或ViewController的viewLayoutSubViews方法),从Layout Engine中取出预算好的frame进行赋值。

* setNeedsUpdateConstraints:标记需要updateConstraints。
* updateConstraintsIfNeeded:若需要,马上调用updateConstraints应用约束更新。
* needsUpdateConstraints:返回是否需要updateConstraints。
* updateConstraints:更新约束,在这里一般只针对某个view的现有约束进行更新,约束的初始化一般放在viewDidLoad方法中,注意:要在最后调用[super updateConstraints]
  • 布局相关
* layoutSubviews:在需要进行布局的时候会调用这个方法,我们可以通过覆写这个方法来确定每个view的frame。

手动触发layoutSubviews:

* setNeedsLayout:此方法会将view当前的layout设置为无效的,并在下一个upadte cycle里去触发layout更新。 * layoutIfNeeded:使用此方法强制立即进行layout,从当前view开始,此方法会遍历整个view层次(包括superviews)请求layout。因此,调用此方法会强制整个view层次布局。

哪些条件下会自动触发layoutSubviews:

1、init初始化不会触发layoutSubviews,但是用initWithFrame进行初始化的时候,当rect的值不是CGRectZero时也会触发 2、addSubview会触发layoutSubviews。 3、设置view的frame会触发layoutSubviews,当然前提是frame的值设置前后发生了变化。 4、改变一个UIView大小的时候也会触发父UIView上的layoutSubviews事件。 5、滚动一个UIScrollView会触发layoutSubviews。 6、旋转屏幕会触发父UIView上的layoutSubviews事件。

* ****绘制相关****

* setNeedsDisplay:标记整个视图的边界矩形需要重绘. * drawRect:如果你的View画自定义的内容,就要实现该方法。若使用UIView绘图,只能在drawRect:方法中获取相应的ContextRef并绘图,其他方法中获取将获取到一个invalidate 的ContextRef不能用于绘制。 * 若使用CAlayer绘图,只能在drawInContext: 中绘制,或者在Delegate中的相应方法绘制.

哪些条件下会自动触发drawRect:

* 如果在UIView初始化时没有设置rect大小将直接导致drawRect不被自动调用。drawRect 调用是在loadView,viewDidLoad 两方法之后掉用的.所以可以在控制器中设置一些在draw阶段需要的值。 * 该方法在调用sizeToFit后被调用,所以可以先调用sizeToFit计算出size。然后系统自动调用drawRect:方法。 * 通过设置contentMode属性值为UIViewContentModeRedraw。那么将在每次设置或更改frame的时候自动调用drawRect:。 * 直接调用setNeedsDisplay,或者setNeedsDisplayInRect:触发drawRect:但是有个前提条件是rect不能为0。

* 这些方法在UIViewController生命周期的位置:
在一个UIView init初始化后会把三个标记都置为YES。然后在VC的布局方式中,viewWillLayoutSubviews中会调用updateConstraints,在viewDidLayoutSubviews会调用layoutSubviews,drawRect。

其中updateConstraints是子 -> 父。layoutSubviews和drawRect是父 -> 子。

* sizeToFit

* sizeToFit会自动调用sizeThatFits方法; * sizeToFit不应该在子类中被重写,应该重写sizeThatFits * sizeThatFits传入的参数是receiver当前的size,返回一个适合的size * sizeToFit可以被手动直接调用 * sizeToFit和sizeThatFits方法都没有递归,只负责自己 ``` 最后来一张总结的图,来自之前的一篇博客《图解生命周期》 ![](/iOS-生命周期/000015.png) ###### TODO App 保活方案