iOS 2021 面试前的准备(总结各知识点方便面试前快速复习使用)(一)

4,544 阅读53分钟

 Apple 的五份源码 objc4-781libdispatch-1173.40.5CF-1151.16libmalloc-283.100.6libclosure-74

1. KVC 的工作原理。

iOS《Key-Value Coding Programming Guide》官方文档iOS《Key-Value Coding Programming Guide》官方文档 这两篇是 KVC 和 KVO 官方文档的翻译,如果需要的话可以进行详细阅读,下面对它们的原理进行简要总结。

 Key-Value Coding(键值编码)是由 NSKeyValueCoding 非正式协议启用的一种机制,对象采用这种机制来提供对其属性/成员变量的间接访问。当一个对象符合键值编码时,它的所有属性/成员变量可以通过一个简洁、统一的消息传递接口(setValue:forKey:)通过字符串参数寻址。这种间接访问机制补充了实例变量及其相关访问器方法(getter 方法)提供的直接访问

 KVC 在代码实现层面则是在 Foundation 框架下有一个 NSKeyValueCoding.h 文件,其内部定义了多组分类接口,其中包括:@interface NSObject(NSKeyValueCoding)、@interface NSArray(NSKeyValueCoding)、@interface NSDictionary<KeyType, ObjectType>(NSKeyValueCoding)、@interface NSMutableDictionary<KeyType, ObjectType>(NSKeyValueCoding)、@interface NSOrderedSet(NSKeyValueCoding)、@interface NSSet(NSKeyValueCoding),其中 NSObject 基类已经实现了 NSKeyValueCoding 机制的所有接口,所以我们自己创建的 NSObject 子类都是支持 KVC 的,然后 NSArray、NSDictionary、NSMutableDictionary、NSOrderedSet、NSSet 这些子类则是对 setValue:forKey:valueForKey: 函数进行重载。例如当对一个 NSArray 对象调用 setValue:forKey: 函数时,它内部是对数组中的每个元素调用 setValue:forKey: 函数。当对一个 NSArray 对象调用 valueForKey: 函数时,它返回一个数组,其中包含在数组的每个元素上调用 valueForKey: 的结果。返回的数组将包含 NSNull 元素,指代的是数组中某些元素调用 valueForKey: 函数返回 nil 的情况。

 集合运算符(@avg、@count、@max、@min、@sum)数组运算符(@distinctUnionOfObjects、@unionOfObjects、)嵌套运算符(@distinctUnionOfArrays、@unionOfArrays、@distinctUnionOfSets)。

 非对象类型的属性的包装和解包,如 int/float 包装成 NSNumber,struct(NSPoint、NSRange、NSRect、NSSize) 包装成 NSValue。

 在给定键参数作为输入的情况下,valueForKey: 的默认实现执行以下过程。(在接收 valueForKey: 调用的类对象内部进行操作)

  1. 在实例中搜索第一个名为 get<Key><key>is<Key>\_<key> 的访问器方法。如果找到了,则调用它并继续执行步骤 5 并返回结果。否则继续下一步。(如果想简单描述的话可以把步骤 2 和 3 省略,2 和 3 针对是一对多关系的搜索过程,如 NSArray 和 NSSet 类型属性的搜索过程)

  2. 如果找不到简单的访问器方法,在实例中搜索名称与 countOf<Key>objectIn<Key>AtIndex:(对应于 NSArray 类定义的原始方法) 和 <key>AtIndexes:(对应于 NSArray 的 objectsAtIndexes: 方法)模式匹配的方法。 如果找到其中的第一个以及其他两个中的至少一个,则创建一个响应所有 NSArray 方法的集合代理对象(collection proxy object),并返回该对象。否则,请继续执行步骤 3。 代理对象随后将接收到的任何 NSArray 消息转换为 countOf<Key>objectIn<Key>AtIndex:<Key>AtIndexes: 消息的组合,并将其转换为创建它的键值编码兼容对象。如果原始对象还实现了一个名为 get<Key>:range: 之类的可选方法,则代理对象也将在适当时使用该方法。实际上,代理对象与键值编码兼容的对象一起工作,允许底层属性的行为就像 NSArray 一样,即使它不是。

  3. 如果找不到简单的访问器方法或数组访问方法组,请查找名为 countOf<Key>enumeratorOf<Key>memberOf<Key> 的三重方法。(对应于 NSSet 类定义的原始方法) 如果找到所有三个方法,请创建一个响应所有 NSSet 方法的集合代理对象,并返回该对象。否则,继续执行步骤 4。 代理对象随后将接收到的任何 NSSet 消息转换为 countOf<Key>enumeratorOf<Key>memberOf<Key> 消息的某种组合,以创建它的对象。实际上,代理对象与键值编码兼容对象一起工作,使得基础属性的行为就像 NSSet 一样,即使它不是 NSSet。

  4. 如果找不到简单的访问器方法或集合访问方法组,并且如果 receiver 的类方法 accessInstanceVariablesDirectly 返回 YES,则按该顺序搜索名为 \_<key>\_is<Key><key>is<Key> 的实例变量。如果找到,则直接获取实例变量的值并继续执行步骤 5。否则,继续进行步骤 6。

  5. 如果检索到的属性值是对象指针,则只需返回结果。 如果该值是 NSNumber 支持的标量类型,则将其存储在 NSNumber 实例中并返回它。 如果结果是 NSNumber 不支持的标量类型,则转换为 NSValue 对象并返回该对象。

  6. 如果所有方法均失败,则调用 valueForUndefinedKey:。默认情况下,这会引发一个 NSUndefinedKeyException 异常,但是 NSObject 的子类可以提供特定于键的行为(子类重写 valueForUndefinedKey: 函数,那进一步我们自行添加一个 NSObject 分类重写 valueForUndefinedKey: 方法呢?)。

setValue:forKey: 的默认实现,给定 key 和 value 参数作为输入,尝试将名为 key 的属性设置为 value,使用以下过程在接收到调用的对象内部:

  1. 按此顺序查找名为 set<Key>:\_set<Key> 的第一个访问器。如果找到了,则使用 value(或根据需要解包 value 的值)调用它并完成。

  2. 如果未找到简单的访问器,并且类方法 accessInstanceVariablesDirectly 返回 YES,按该顺序查找名称类似于 \_<key>\_is<Key><key>is<Key> 的实例变量。如果找到,则直接使用 value(或根据需要解包 value 的值)设置实例变量并完成操作。

  3. 在找不到访问器或实例变量时,调用 setValue:forUndefinedKey:。这在默认情况下会引发 NSUndefinedKeyException 异常,但 NSObject 的子类可能会提供键特定的行为。(由子类重写 setValue:forUndefinedKey:

 NOTE: 当你使用非对象属性的 nil 值调用其中一个键值编码协议 setter 时,setter 没有明显的常规操作过程可采取。因此,它向接收 setter 调用的对象发送 setNilValueForKey: 消息。此方法的默认实现会引发 NSInvalidArgumentException 异常,但子类可能会重写此行为,如处理非对象值中所述,例如设置标记值或提供有意义的默认值。

 键值编码是高效的,尤其是当你依靠默认实现来完成大部分工作时,但是它确实添加了一个间接级别,该级别比直接的方法调用稍慢。只有当你可以从它提供的灵活性中获益或者允许你的对象参与依赖于它的 Cocoa 技术时,才使用键值编码。


2. KVO 的工作原理。(追问 KVO 动态生成的新类重写了属性的 Setter 函数后,那原始手动实现的 Setter 函数会被覆盖吗?对对象的某个属性添加观察者后那对象的 isa 指向和 class 函数会发生什么变化?移除观察者后呢?)

 Key-Value Observing(键值观察)是一种机制,它允许将其他对象的指定属性的更改通知给对象。

 KVO (自动键值观察)是通过 isa-swizzling (交换)实现的。基本的流程就是编译器自动为被观察者对象创造一个派生类(此派生类的父类是被观察者对象所属的类),并将被观察者对象的 isa 指向这个派生类(类名是 NSKVONotifying_XXX)。如果用户注册了对此目标对象的某一个属性的观察,那么此派生类会重写这个属性的 setter 方法,并在其中添加进行通知的代码。Objective-C 在发送消息的时候,会通过 isa 指针找到当前对象所属的类对象。而类对象中保存着当前对象可调用的实例方法,因此在向此对象发送消息时候,实际上是发送到了派生类对象的方法。由于编译器对派生类的方法进行了重写,并添加了通知代码,因此会向注册的观察者对象发送通知。注意派生类只重写注册了观察者的属性方法。

-(void)setValue:(id)obj {
    [self willChangeValueForKey:@"keyPath"];
    
    // 这里内部使用 super 调用,由于当前派生类的 super 正是指向原类,所以不影响原类中自己手动实现的 setter 函数调用(去 58 面试时遇到了这个问题) 
    [super setValue:obj];
    
    [self didChangeValueForKey:@"keyPath"];
}

 如下示例代码中定义的 Student 类,当对其 name 属性注册了观察者后,打印其 class 和 isa 如下:

@interface Student : NSObject
@property (nonatomic, copy) NSString *name;
@end

// 对 self.student 添加观察者后 class 函数返回的依然是 Student
NSLog(@"🤍🤍 %@", [self.student class]);

// object_getClass 方法返回 isa 指向却是 NSKVONotifying_Student
NSLog(@"🤍🤍 %@", object_getClass(self.student));

// 控制台打印:
🤍🤍 Student
🤍🤍 NSKVONotifying_Student
 
// 然后移除 self.student 的观察者后,object_getClass(self.student) 返回的则是 Student。

 简而言之,Apple 使用了一种 isa 交换的技术,当 student 被观察后,student 对象的 isa 指针被指向了一个新建的 Student 的子类 NSKVONotifying_Student,且这个子类重写了被观察属性的 setter 方法、class 方法、dealloc 和 _isKVO 方法,然后使 student 对象的 isa 指针指向这个新建的类,然后事实上 student 变为了NSKVONotifying_Student 的实例对象,执行方法要从这个类的方法列表里找。dealloc 方法:观察者移除后使 class 变回去 Student(通过 isa 指向), _isKVO 方法判断被观察者自己是否同时也观察了其他对象。(同时苹果警告我们,通过 isa 获取类的类型是不可靠的,通过 class 方法才能得到正确的类)用代码探讨 KVC/KVO 的实现原理

  • 验证观察者提前释放了且被观察者没有主动移除该观察者,那被观察者的 isa 会不会回归为原类? 不会回归原类,还是 NSKVONotifying_xxx 类,且此时再向观察者发送通知(observeValueForKeyPath:ofObject:change:context:)会发生野指针访问 crash。
  • 验证 KVO 中编译器派生的新类的父类是不是原类? 是原类,例如打印 class_getSuperclass(object_getClass(self.vcTestObjc)) 时可看到 NSKVONotifying_TestObjc 的父类是 TestObjc。
  • 如何手动触发 KVO?
[self willChangeValueForKey:@"vcTestObjcTWO"];

// 这里我们直接给实例变量赋值,不执行 vcTestObjcTWO 属性的 Setter 函数则会手动触发一次监听通知,
// 如果是使用 self.vcTestObjcTWO 赋值,则会调用 Setter 函数,此时手动加自动便会执行两次监听通知。 
_vcTestObjcTWO = [[TestObjc alloc] init];

[self didChangeValueForKey:@"vcTestObjcTWO"];

3. iOS 中的方法缓存、快速查找、慢速查找流程。

 首先是 cache_t cache 的位置,它是 objc_class 结构体的第三个成员变量(起始地址偏移 16 个字节),类型是 cache_t 结构体,从数据结构角度及使用方法来看 cache_t 的话,它是一个 SEL 作为 KeySEL + IMP(bucket_t) 作为 Value 的哈希表。

struct objc_class : objc_object {
// Class ISA; // objc_class 继承自 objc_object,所以其第一个成员变量其实是 isa_t isa 
Class superclass; // 父类指针
cache_t cache; // formerly cache pointer and vtable 以前缓存指针和虚函数表
...
};

方法缓存插入的执行过程: 把指定的 selimp 插入到 cache_t 中,如果开始是空状态,则首先会初始一个容量为 4 散列数组再进行插入,其它情况插入之前会计算已用的容量占比是否到了临界值 3/4,如果是则首先进行扩容扩大为 2 倍,然后再进行插入操作(哈希函数是直接 sel 和 mask 与操作),如果还没有达到则直接插入,插入操作如果发生了哈希冲突则以开放寻址法进行哈希探测。cache 扩容时为了性能考虑不会把旧的 buckets 数据重新哈希放入新内存中,会把旧的 buckets 放进一个等待释放的数组中,但是也不会立即就释放旧的 bukckts,而是将旧的 buckets 存放到全局的 static bucket_t **garbage_refs 数组中,以便稍后释放,注意这里是稍后释放。因为此时可能其他线程正在进行方法缓存查找。当 garbage_refs 数组的内存容量达到阈值 32*1024 字节时会进行释放旧的 buckets 数据。(_collecting_in_critical 函数内部会判断当前是否有别的线程在查找旧 buckets 数据,如果没有到话才会进行释放旧 buckets 数据)

objc_msgSend 函数 那么 objc_msgSend 是怎么实现的呢?乍看它以为是一个 C/C++ 函数,但它其实是汇编实现的。 使用汇编的原因,除了 快速,方法的查找操作是很频繁的,汇编是相对底层的语言更容易被机器识别,节省中间的一些编译过程 还有一些重要的原因,用汇编实现,是为了应对不同的 “Calling convention”,把函数调用前的栈和寄存器的参数、状态设置,交给编译器去处理

 objc_msgSend 函数查找 cache 的过程,x0 保存 self,x1 保存 _cmd,后续的 x2-x7 依次是存放其他参数(超过 7 个参数时存在栈中)。 在 arm64 下首先判断 self 是否小于等于 0,如果是的话判断是 TaggedPointer (TaggedPointer 在 arm64 下,最高位为 1,作为有符号数 < 0)还是 nil,如果是 nil 则跳转到 LReturnZero 把寄存器清 0,如果是 TaggedPointer(从类表中取 Class) 或正常的对象(从 isa 中取 Class,3~36 bit),则首先取得它们所属的类,然后是 CacheLookup 在缓存中查找或进行完整方法查找。缓存查找首先找到从 Class 起始地址偏移 16 字节取得 cache_t cache 成员变量,然后根据传入的 _cmd 在 buckets 哈希表中进行哈希查找(_cmd & mask)(开放寻址法),缓存命中时 TailCallCachedImp 内部 br 指令调用 _cmd 对应的 imp(其他参数都已经在寄存器中存放好)(在缓存表进行哈希查找的过程中有一个细节可展开可不展开,当到达缓存表头后,继续从缓存表尾开始全缓存表扫描,直至重新回到缓存表头。是为了处理缓存被破坏时的情况。)如果缓存未命中的情况下,则都会调用 __objc_msgSend_uncached 内部则是调用 MethodTableLookup(即查询方法列表) 进行方法查找。(即大家常说的慢速查找)

 缓存未命中时,都会走到 __objc_msgSend_uncached 去处理。__objc_msgSend_uncached 的实现很简单,调用 MethodTableLookup 进行方法查找。MethodTableLookup 的核心则是进行一个 lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER) 的查找,找到 imp 后同样是 TailCallFunctionPointer 调用,下面我们看 lookUpImpOrForward 函数,它是一个 C 函数定义在 objc-runtime-new.mm 文件中。

  • C/C++ 中调用 汇编 ,去查找汇编时,C/C++ 调用的方法需要多加一个下划线
  • 汇编 中调用 C/C++ 方法时,去查找 C/C++ 方法,需要将汇编调用的方法去掉一个下划线

 当缓存未命中时首先进行的是在当前类中进行慢速查找,如果还是未找到的话,会去父类的缓存中查找,依然未命中的话则是在父类中进行慢速查找。沿着继承链一直重复缓存查找和慢速查找直到根类。在父类中找到的方法会被缓存到当前类的 cache 中。

 lookUpImpOrForward 函数开始先进行 是否是已知类、类是否实现、是否初始化三个判断,然后是 for 循环沿着类继承链或者元类继承链进行顺序查找,在 curClass 的方法列表(从类的第四个成员变量 class_data_bits_t bits 中取得方法列表)中使用二分查找算法查找方法(findMethodInSortedMethodList),如果找到的话写入 cls 的 cache 中并返回 imp。如果 for 循环结束都没有找到则判断是否进行动态方法解析,即我们熟悉的 resolveInstanceMethod 和 resolveClassMethod。

 类的方法列表是 class list_array_tt,它有三种形态:

  • empty
  • a pointer to a single list
  • an array of pointers to lists(当类存在分类时,它的方法列表是这种二维数组的形式。且分类的方法列表排在原类的方法列表前面)

 最后如果在快速查找、慢速查找、方法解析流程中,均没有找到实现,则使用消息转发,调用汇编函数 _objc_msgForward_impcache 其内部是跳转至 __objc_msgForward 其内部调用 _objc_forward_handler,它是一个函数指针,默认指向:void *_objc_forward_handler = (void*)objc_defaultForwardHandler;,objc_defaultForwardHandler 内部则是抛出我们见过很多次的 unrecognized selector 错误。

// Default forward handler halts the process.
__attribute__((noreturn, cold)) void
objc_defaultForwardHandler(id self, SEL sel)
{
    _objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
                "(no message forward handler is installed)", 
                class_isMetaClass(object_getClass(self)) ? '+' : '-', 
                object_getClassName(self), sel_getName(sel), self);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

 参考链接🔗🔗:


4. 解释 Dynamic Method Resolution 与 Message Forwarding。

 在 OC 中每个方法调用都是一个发送消息的过程,这是一种在运行时选择方法实现的方式,用面向对象编程的术语来说,方法是动态绑定到消息的。(动态绑定就是沿着对象的 isa 在类的继承体系中查找具体的函数实现的过程)。

 绕过动态绑定的唯一方法是获取方法的地址并直接调用它,就好像它是一个函数一样。当一个特定的方法将连续执行很多次,并且你希望避免每次执行该方法时的消息传递开销时,这种方法可能非常合适。使用 NSObject 类中定义的方法 methodForSelector: 你可以取得指定类下 selector 对应的方法的指针,然后使用该指针来调用该方法。 methodForSelector: 返回的指针必须仔细转换为适当的函数类型。返回类型和参数类型都应包含在强制类型转换中。如下示例代码调用 setFilled: 方法的过程:

void (*setter)(id, SEL, BOOL);
int i;
 
setter = (void (*)(id, SEL, BOOL))[target methodForSelector:@selector(setFilled:)];
for (i = 0 ; i < 1000 ; i++ )
    setter(targetList[i], @selector(setFilled:), YES);

 使用 methodForSelector: 规避动态绑定可以节省消息传递所需的大部分时间。但是,仅在重复多次发送特定消息的情况下,这种节省才是可观的,如上面的 for 循环所示。注意 methodForSelector: 由 Cocoa 运行时系统提供;这不是 Objective-C 语言本身的功能。

 Selector 选择器是用于选择要为对象执行的方法的名称,或者是在编译源代码时替换该名称的唯一标识符。选择器本身不起任何作用。它只是标识一个方法。

 NSMethodSignature 方法签名,表示方法的返回值和参数的类型信息。可以使用 NSObject 的 methodSignatureForSelector: 实例方法创建 NSMethodSignature 对象。NSMethodSignature 对象是用一个字符数组初始化的 + (nullable NSMethodSignature *)signatureWithObjCTypes:(const char *)types;,该数组表示方法的返回和参数类型的字符串编码。如表示 NSString 实例方法 containsString: 方法签名的字符串是:c@:@。(c 是返回类型 BOOL,@ 是 self,: 是 _cmd,@ 是第一个显示参数 NSString *)

 NSInvocation 描绘为对象的 Objective-C 消息,即以 NSInvocation 对象描述 Objective-C 消息,用于在对象之间和应用程序之间存储和转发消息。NSInvocation 对象包含 Objective-C 消息的所有元素:目标(target)、选择器(selector)、参数(arguments)和返回值(return value),这些元素中的每一个都可以直接设置,并且在 invoke NSInvocation 对象时会自动设置返回值(- (void)invoke; 将 NSInvocation 对象的消息(带有参数)发送到其 target 并设置返回值(setReturnValue:),必须先设置 NSInvocation 对象的 targetselector 和参数值(setArgument:atIndex:),然后才能调用此方法)。

 NSInvocation 不支持使用可变数量的参数或 union 参数调用方法,应该使用 invocationWithMethodSignature: 类方法创建 NSInvocation 对象,不应该使用 alloc 和 init 创建 NSInvocation 对象。为了提高效率,新创建的 NSInvocation 对象不保留或复制其参数,也不保留其 target(target 是 assign 修饰的 id 类型的属性:@property (nullable, assign) id target;)、复制 C 字符串或复制任何关联的 blocks。如果要缓存 NSInvocation 对象,则应该指示该对象保留其参数(调用 retainArguments 函数),因为这些参数可能会在调用之前释放。NSTimer 对象总是指示它们的调用保留它们的参数,因为在计时器触发之前通常有一个延迟。

BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types) 向具有给定名称和实现的类中添加新方法。Return Value: 如果成功添加了方法,则为 YES,否则为 NO(例如,该类已经包含具有该名称的方法实现时也会返回 NO)。要更改现有的实现,请使用 IMP method_setImplementation(Method m, IMP imp); 设置方法的新实现,返回值是该方法的旧实现。如下示例:

// Objective-C 方法只是一个 C 函数,它至少接受两个参数 self 和 \_cmd。
void myMethodIMP(id self, SEL _cmd) {
    // implementation ....
}

// 可以像这样将方法动态添加到类中
class_addMethod([self class], @selector(resolveThisMethodDynamically), (IMP)myMethodIMP, "v@:");

 resolveInstanceMethod: 动态的为实例方法的给定选择器(sel)提供实现。resolveClassMethod: 动态的为类方法的给定选择器(sel)提供实现。如下示例:

+ (BOOL)resolveInstanceMethod:(SEL)aSEL {
    if (aSEL == @selector(resolveThisMethodDynamically)) {
          class_addMethod([self class], aSEL, (IMP)dynamicMethodIMP, "v@:");
          return YES;
    }
    
    return [super resolveInstanceMethod:aSel];
}

 forwardingTargetForSelector: 返回未识别消息应首先指向的对象。

- (id)forwardingTargetForSelector:(SEL)aSelector OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

 objc-781 下 NSObject 类的 forwardingTargetForSelector 函数的默认实现,是直接返回 nil。

+ (id)forwardingTargetForSelector:(SEL)sel {
    return nil;
}

- (id)forwardingTargetForSelector:(SEL)sel {
    return nil;
}

 如果你在非根类(非 NSObject)中实现此方法,如果你的类对于给定的选择器没有要返回的内容,那么你应该返回调用 super 实现的结果(return [super forwardingTargetForSelector:aSelector];)。

 这种方法使对象有机会在更昂贵的 forwardInvocation: 机制接管之前重定向发送给它的未知消息。当你只想将消息重定向到另一个对象时,这非常有用,并且可以比常规转发快一个数量级。如果转发的目标是捕获 NSInvocation,或者在转发过程中操纵参数或返回值,那么它就没有用了。

 methodSignatureForSelector: 返回一个 NSMethodSignature 对象,该对象包含由给定选择器标识的方法的描述。(也可以根据自己的需要返回有别与 aSelector 之外的 NSMethodSignature)

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector OBJC_SWIFT_UNAVAILABLE("");

 forwardInvocation: 被子类重写以将消息转发到其他对象。

- (void)forwardInvocation:(NSInvocation *)anInvocation OBJC_SWIFT_UNAVAILABLE("");

 当一个对象被发送一条没有相应方法(实现)的消息时,运行时系统给接收者一个机会将消息委托给另一个接收者。它通过创建表示消息的 NSInvocation 对象并向接收方发送 forwardInvocation: 包含此 NSInvocation 对象作为参数的消息来代理消息。然后,接收方的 forwardInvocation: 方法可以选择将消息转发到另一个对象。(如果该对象也不能响应消息,它也将有机会转发消息。)

 Important: 要响应对象本身无法识别的方法,除了 forwardInvocation: 之外,还必须重写 methodSignatureForSelector:。转发消息的机制使用从 methodSignatureForSelector: 获取的信息(NSMethodSignature 对象)来创建要转发的 NSInvocation 对象。重写方法必须为给定的选择器提供适当的方法签名,方法可以是预先制定一个方法签名,也可以是向另一个对象请求一个方法签名。

 forwardInvocation: 方法的实现有两个任务:

  • 定位一个对象,该对象可以响应在 anInvocation 中编码的消息。对于所有消息,此对象不必相同。
  • 使用 anInvocation 将消息发送到该对象。调用将保存结果,运行时系统将提取该结果并将其传递给原始发送者。

 在一个简单的情况下,对象只将消息转发到一个目的地(如下面示例中假设的 friend 实例变量),forwardInvocation: 方法可以如下所示:

- (void)forwardInvocation:(NSInvocation *)invocation {
    SEL aSelector = [invocation selector];
 
    if ([friend respondsToSelector:aSelector])
        [invocation invokeWithTarget:friend];
    else
        [super forwardInvocation:invocation];
}

 forwardInvocation: 方法的实现可以做的不仅仅是转发消息。forwardInvocation: 例如,可以用于合并响应各种不同消息的代码,从而避免了为每个选择器编写单独方法的必要性。forwardInvocation: 方法可能还会在对给定消息的响应中包含其他几个对象,而不是只将其转发给一个对象。

 NSObject 的 forwardInvocation: 实现只是调用 doesNotRecognizeSelector: 方法;它不转发任何消息。因此,如果选择不实现 forwardInvocation:,则向对象发送无法识别的消息将引发异常。

 objc-781 下 NSObject 类的 forwardInvocation: 函数的默认实现:

+ (void)forwardInvocation:(NSInvocation *)invocation {
    [self doesNotRecognizeSelector:(invocation ? [invocation selector] : 0)];
}

- (void)forwardInvocation:(NSInvocation *)invocation {
    [self doesNotRecognizeSelector:(invocation ? [invocation selector] : 0)];
}

 Dynamic Method Resolution(动态方法解析): 即实现 resolveInstanceMethod:resolveClassMethod: 方法,分别为实例和类方法的给定选择器动态提供实现。Message Forwarding(消息转发)和 Dynamic Method Resolution(动态方法解析)在很大程度上是正交的,类有机会在转发机制启动之前动态解析方法。如果调用了 respondsToSelector:instancesRespondToSelector:,则动态方法解析器将有机会首先为选择器提供 IMP。如果你实现 resolveInstanceMethod: 但希望特定的选择器实际通过转发机制转发,那么你需要为这些选择器返回 NO。

 如果 Dynamic Method Resolution(动态方法解析)无法补救时,继续走下一个流程:Fast forwarding 快速转发阶段(如果可以的话返回一个备用响应对象 forwardingTargetForSelector: )和 Normal forwarding 常规转发阶段(完整的消息转发 methodSignatureForSelector: 和 forwardInvocation:)。

 forwardInvocation: 它是动态的而不是静态的。它的工作方式如下:当对象由于没有与消息中的选择器匹配的方法而无法响应消息时,运行时系统会通过向其发送 forwardInvocation: 消息来通知对象,其中 NSInvocation 对象作为其唯一参数,NSInvocation 对象将封装原始消息及其传递的参数。每个对象都从 NSObject 类继承了 forwardInvocation: 方法。但是,该方法的 NSObject 版本仅调用 dosNotRecognizeSelector:。通过覆盖 NSObject 的版本并实现自己的版本,你可以利用 forwardInvocation: 消息提供的机会将消息转发给其他对象。

 forwardInvocation: 方法可以充当未识别消息的分发中心,将它们分发给不同的接收者。或者它可以是一个中转站,将所有消息发送到同一个目的地。它可以将一条消息转换成另一条消息,或者简单地 “吞下(swallow)” 一些消息,这样就没有响应也没有错误。forwardInvocation: 方法还可以将多个消息合并到单个响应中。forwardInvocation: 做什么取决于实现者。然而,它提供了在转发链中链接对象的机会,为程序设计开辟了可能性。

 转发提供了你通常希望从多重继承中获得的大部分功能。然而,两者之间有一个重要的区别:多重继承在单个对象中结合了不同的功能。它趋向于大的、多方面的对象。另一方面,转发将不同的责任分配给不同的对象。它将问题分解为更小的对象,但以对消息发送者透明的方式关联这些对象。尽管转发模仿继承,但 NSObject 类从不混淆两者。respondsToSelector:isKindOfClass: 等方法只查看继承层次结构,而不查看转发链。

 如果使用转发来设置代理对象或扩展类的功能,则转发机制应该与继承一样透明。如果你希望你的对象像他们真正继承了转发消息的对象的行为一样工作,则需要重新实现 responsToSelector:isKindOfClass: 方法以包括你的转发算法:

- (BOOL)respondsToSelector:(SEL)aSelector {
    if ( [super respondsToSelector:aSelector] )
        return YES;
    else {
        /* Here, test whether the aSelector message can     *
         * be forwarded to another object and whether that  *
         * object can respond to it. Return YES if it can.  */
         
        // 在这里,测试 aSelector 消息是否可以转发到另一个对象以及该对象是否可以响应。如果可以,则返回 YES。
        
    }
    return NO;
}

 如果对象转发其收到的任何远程消息,则它应具有 methodSignatureForSelector: 的版本, 该函数可以返回对最终响应所转发消息的方法的准确描述。例如,如果对象能够将消息转发到其代理,则可以实现 methodSignatureForSelector: 如下:

- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector{
    NSMethodSignature* signature = [super methodSignatureForSelector:selector];
    if (!signature) {
       signature = [surrogate methodSignatureForSelector:selector];
    }
    return signature;
}

 参考链接🔗🔗:


5. 解释 iOS 响应者链(Responder Chain)与触摸事件处理过程。

  UIView 是 UIResponder 的子类,UIResponder 则是 NSObject 的子类,UIButton 则是继承自 UIControl,而 UIControl 则是继承自 UIView,UIView 等一众子类正是因为继承自 UIResponder 所以才可以被作为响应者使用,而之所以能被称为响应者,就是因为它们可以重写 UIResponder 的 touches...(响应触摸事件)、presses...(响应按键事件)、motion...(响应运动事件) 系列函数而已。UIControl 系列则是依据 Target-Action 机制来响应用户事件。

 当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。这个过程的详细情况可以参考 IOHIDFamily。SpringBoard 只接收按键(锁屏/静音等)、触摸、加速、距离传感器(UIEventTypeMotion)等几种 Event,SpringBoard 判断桌面是否存在前台应用,若无(如处于桌面翻页),则触发 SpringBoard 应用内部主线程 run loop 的 source0 事件回调,由桌面应用内部消耗;若有则通过 mach port 转发给需要的前台 App 进程。

 App 启动后会创建一条名为 com.apple.uikit.eventfetch-thread 的线程,并直接启动此线程的 run loop,且在其 kCFRunLoopDefaultMode 运行模式下添加了一个回调函数是 __IOHIDEventSystemClientQueueCallback 的 source1,用于接收上面提到的 SpringBoard 通过 mach port 发来的消息。

 前台 App 进程的 com.apple.uikit.eventfetch-thread 线程被 SpringBoard 根据指定的 mach port 唤醒后,执行其 source1 对应的回调函数 __IOHIDEventSystemClientQueueCallback,并将 main run loop 中的回调函数是 __handleEventQueue 的 source0 的 signalled 设置为 YES 标记其为待处理状态,同时唤醒 main run loop,主线程则调用 __handleEventQueue 来进行事件(IOHIDEvent)的处理。

 UIApplication 对象调用 sendEvent: 将事件(UIEvent)调度到 window。window 对象将触摸事件调度到发生触摸的 view,并将其他类型的事件调度到最合适的目标对象。你可以根据需要在应用程序中调用 sendEvent: 方法以调度你创建的自定义事件。例如,你可以调用此方法将自定义事件调度到 window 的响应者链。

 UIEvent 是描述用户与应用交互的对象。应用程序可以接收许多不同类型的事件,包括触摸事件(touch events)、运动事件(motion events)、远程控制事件(remote-control events)和按键事件(press events)。

  • 触摸事件是最常见的,并且被传递到最初发生触摸的 view 中。
  • 运动事件是 UIKit 触发的,与 Core Motion 框架报告的运动事件是分开的。
  • 远程控制事件允许响应者对象从外部附件或耳机接收命令,以便它可以管理音频和视频的管理,例如,播放视频或跳至下一个音轨。
  • 按键事件表示与游戏控制器、AppleTV 遥控器或其他具有物理按钮的设备的交互。  可以使用类型(type)和子类型(subtype)属性确定事件的类型。(如远程控制事件包括播放、暂停、停止等子类型)

 触摸事件对象包含与事件有某种关系的 touches。触摸事件对象可以包含一个或多个 touch,并且每个 touch 都由 UITouch 对象表示。当触摸事件发生时,系统将其路由到相应的响应者并调用相应的方法,如 touchesBegan:withEvent:。然后,响应者使用 touches 来确定适当的行动方案。

 UITouch 表示屏幕上发生的触摸的位置(location)、大小(size)、移动(movement)和力度(force,针对 3D Touch 和 Apple Pencil)的类。UITouch 对象包括用于以下内容的访问:

  • 发生触摸的 view 或 window
  • 触摸在 view 或 window 中的位置
  • 触摸的近似半径(approximate radius)
  • 触摸的力度(force)(在支持 3D Touch 或 Apple Pencil 的设备上)  UITouch 对象还包含一个时间戳,该时间戳指示何时发生触摸;代表用户 tapped 屏幕的次数的整数;以及触摸的阶段,其形式为常数,描述了触摸是开始,移动还是结束,或系统是否取消触摸;gestureRecognizers 属性包含当前正在处理 touch 的 gesture recognizers。

 UITouch 在不同的阶段时响应者会调用不同的响应函数。(touchesBegan:withEvent:、touchesMoved:withEvent:、touchesEnded:withEvent:、touchesCancelled:withEvent:)

 hitTest:withEvent: 返回包含指定点(point)的视图层次结构中 view 的最远子视图(最远子视图,也可能是其自身)。此方法通过调用每个子视图的 pointInside:withEvent: 方法来遍历视图层次结构,以确定哪个子视图应接收 touch 事件。如果 pointInside:withEvent: 返回 YES,然后类似地遍历其子视图的层次结构,直到找到包含 point 的最前面的视图。如果视图不包含该 point,则将忽略其视图层次结构的分支。你很少需要自己调用此方法,但可以重写它以从子视图中隐藏 touch 事件,或者扩大 view 响应范围。此方法将忽略 hidden 设置为 YES 的、禁用用户交互(userInteractionEnabled 设置为 NO)或 alpha 小于 0.01 的 view 对象。

 默认情况下超出 view 的 bounds 的 point 永远不会被报告为命中,即使它们实际上位于 view 的一个子视图中。如果当前 view 的 clipsToBounds 属性设置为 NO,并且受影响的子视图超出了 view 的边界,则会发生这种情况。(例如一个 button 按钮超出了其父视图的 bounds,此时点击 button 未超出父视图的 bounds 的区域的话可以响应点击事件,如果点击 button 超出父视图的 bounds 的区域的话则不能响应点击事件)

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

 hitTest:withEvent: 寻找一个包含 point 的 view 的过程可以理解为如下:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
    // 3 种状态无法响应事件
    // 1): userInteractionEnabled 为 NO,禁止了用户交互。
    // 2): hidden 为 YES,被隐藏了。
    // 3): alpha 小于等于 0.01,透明度小于 0.01。
    if (self.userInteractionEnabled == NO || self.hidden == YES ||  self.alpha <= 0.01) return nil;
    
    // 触摸点若不在当前视图上则无法响应事件
    if ([self pointInside:point withEvent:event] == NO) return nil;
    
    // ⬇️⬇️⬇️ 从后往前遍历子视图数组(倒序)
    int count = (int)self.subviews.count;
    for (int i = count - 1; i >= 0; i--) {
        // 获取子视图
        UIView *childView = self.subviews[i];
        
        // 坐标系的转换,把触摸点在当前视图上坐标转换为在子视图上的坐标
        CGPoint childP = [self convertPoint:point toView:childView];
        
        // 询问子视图层级中的最佳响应视图(递归)
        UIView *fitView = [childView hitTest:childP withEvent:event];
        
        if (fitView) {
            // 如果子视图中有更合适的就返回
            return fitView;
        }
    }
    
    // 没有在子视图中找到更合适的响应视图,那么自身就是最合适的
    return self;
}

 pointInside:withEvent: 返回一个布尔值,该值指示 view 是否包含该 point。如果 point 包含在 view 的 bounds 中,则返回 YES,否则返回 NO。

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

 convertPoint:toView: 将 point 从 view 的坐标系转换为指定 view 的点(CGPoint)。

- (CGPoint)convertPoint:(CGPoint)point toView:(UIView *)view;

 UIResponder 响应和处理事件的 abstract interface。(UIResponder 是一个极重要的继承自 NSObject 的抽象接口,它几乎是 UIKit 框架下所有 UI 类的父类(也有特殊,如 UIImage 类则是直接继承自 NSObject),正是从它建立了 NSObject 和 UI 层之间的连接。)响应者对象(即 UIResponder 实例)构成 UIKit 应用程序的事件处理主干(event-handling backbone)。许多关键对象也是响应者,包括 UIApplication 对象、UIViewController 对象和所有 UIView 对象(包括 UIWindow)。当事件发生时,UIKit 会将它们调度给应用程序的响应者对象进行处理。

 有几种事件,包括触摸事件(touch events)、运动事件(motion events)、遥控事件(remote-control events)和按键事件(press events)。要处理特定类型的事件,响应者必须重写相应的方法。例如,为了处理触摸事件,响应者实现 touchesBegan:withEvent:、touchesMoved:withEvent:、touchesEnded:withEvent: 和 touchesCancelled:withEvent: 方法。在触摸的情况下,响应者使用 UIKit 提供的事件信息(UIEvent)来跟踪这些触摸的变化,并适当地更新应用程序的界面。

 除了处理事件外,UIKit 响应者还管理将未处理的事件转发到应用程序的其他部分。如果给定的响应者不能处理事件,它会将该事件转发给响应者链中的下一个响应者(文档应该错了,文档写的是 "next event")。UIKit 动态管理响应者链,使用预定义的规则来确定下一个接收事件的响应者对象。例如,view 将事件转发到其 superview,层次结构的 root view 将事件转发到其 view controller。

 The path of an event。事件在响应者链上的一般路径从第一个响应者的视图或鼠标指针或手指下的视图开始。从那里开始,它向上进入视图层次结构,进入 window 对象,然后进入全局应用程序对象。但是,iOS 中事件的响应者链为该路径添加了一个变体:如果 view 由 view controller 管理,并且 view 无法处理事件,则 view controller 将成为下一个响应者。

 应用程序使用响应者对象(responder objects)接收和处理事件。responder 对象是 UIResponder 类的任何实例,常见的子类包括 UIView、UIViewController 和 UIApplication。响应者接收原始事件数据,并且必须处理该事件或将其转发给另一个响应者对象。当应用程序接收到事件时,UIKit 会自动将该事件定向到最合适的响应者对象(称为第一响应者)。

 Figure 1 Responder chains in an app(应用中的响应者链) responder_chains_in_an_app

 如果 text field 不处理事件,UIKit 会将事件发送到 text field 的父 view 对象,后跟 window 的根视图。在将事件定向到 window 之前,响应者链从根视图转移到拥有的视图控制器。如果 window 无法处理事件,UIKit 会将事件传递给 UIApplication 对象,如果该对象是 UIResponder 的实例而不是响应程序链的一部分,则可能会传递给 app delegate。

 当触摸发生时,UIKit 创建一个 UITouch 对象并将其与 view 相关联。当触摸位置或其他参数改变时,UIKit 用新信息更新同一 UITouch 对象。唯一不变的属性是 view。(即使触摸位置移动到原始 view 之外,触摸 view 属性中的值也不会更改。(如我们常见的页面上有一个小的滚动区域时,我们手指先摸到它并滑动该小区域开始滚动,当我们的手指超出此块滚动区域并不离开屏幕时,此小滚动区域也一直响应我们手指的滑动))当触摸结束时,UIKit 释放 UITouch 对象。

 你可以通过重写响应者对象的 nextResponder 属性来更改响应者链。当你这样做时,下一个响应者就是你返回的对象。

 许多 UIKit 类已经重写此属性并返回特定的对象,包括:

  • UIView 对象。如果 view 是 view controller 的 root view,则下一个响应者是 view controller;否则,下一个响应者是 view 的 superview。
  • UIViewController 对象。
    • 如果 view controller 的 view 是 window 的 root view,则它的下一个响应者是 window 对象。
    • 如果 view controller 由另一个 view controller 呈现,则它的下一个响应者是呈现 view controller。(parent view controller)
  • UIWindow 对象。window 的下一个响应者是 UIApplication 对象。
  • UIApplication 对象。下一个响应者是 app delegate,但仅当该 app delegate 是 UIResponder 的实例并且不是 view、view controller 或 app 对象本身时,才是下一个响应者。

 我们再对 gesture recognizers(继承自 NSObject,响应事件的方式同 UIControl,也是 target-action 机制) 和 target-action 进行一个拓展学习,它们还挺重要的。(当一个 view 同时实现了 touches... 系列函数和添加了手势时,我们去触摸该 view 首先会调用 touchesBegan:withEvent: 函数,而当 gesture recognizers 识别出手势后会调用 view 的 touchesCancelled:withEvent: 打断 touches... 系列函数的执行,然后去执行手势的 action 函数。)

 Gesture recognizers 有两种类型:离散(discrete)和连续(continuous)。一个离散的手势识别器(discrete gesture recognizer)会在手势被识别后准确地调用你的动作方法(action method)一次。满足初始识别条件后,连续手势识别器(continuous gesture recognizer)会多次执行对动作方法的调用,并在手势事件中的信息发生更改时通知你。例如,每次触摸位置更改时,UIPanGestureRecognizer 对象都会调用动作方法。

 Target-Action 机制:当用户以指定的方式触摸控件时,控件通过 sendAction:to:from:forEvent: 消息将 action 消息转发到全局 UIApplication 对象。与在 AppKit 中一样,全局 application 对象是 action 消息的集中调度点。如果控件为 action 消息指定了nil target,应用程序将查询响应程序链中的对象,直到找到一个愿意处理 action 消息的对象,即实现与 action 选择器对应的方法的对象。

 看完响应者的定义大概就可以尽量详细的解释 UIView 和 CALayer 关系中 UIView 担负起响应用户交互的职责。

 参考链接🔗🔗:


6. UIView 的一些重要知识点。

 UIView 负责绘制内容、处理多点触控事件以及管理任何 subviews 的布局。绘图涉及使用诸如 Core Graphics、OpenGL ES 或 UIKit 之类的图形技术在 view 的矩形区域内绘制形状、图像和文本。view 通过使用手势识别器或直接处理触摸事件来响应其矩形区域中的触摸事件。

 UIView 的许多属性也可以直接设置动画。例如,通过动画,你可以更改 view 的透明度、它在屏幕上的位置、它的大小、它的背景颜色或其他属性。而且,如果你直接使用 view 的基础 Core Animation 图层对象(CALayer),则还可以执行许多其他动画。view 是应用程序中手势和触摸事件的主要接收者。自定义 view 必须使用可用的绘图技术来呈现其内容。在标准 view 动画不足的地方,可以使用 Core Animation。

 UIView 与 Core Animation layers 结合使用,以处理 view 内容的渲染和动画处理。 UIKit 中的每个 view 都由一个 layer 对象(通常是 CALayer 类的实例)支持,该对象管理该 view 的 backing store 并处理与 view 相关的动画。大多数操作都应该通过 UIView 接口执行。但是,在需要更好地控制 view 的渲染或动画行为的情况下,可以改为通过其 layer 执行操作。

 Core Animation layer 对象的使用对性能有重要影响。view 对象的实际绘图代码被尽可能少地调用,当调用代码时,结果被 Core Animation 缓存,并在以后尽可能多地重用。重用已经呈现的内容消除了更新 view 通常需要的昂贵的绘图周期。在可以操纵现有内容的动画中,重用此内容尤其重要。这种重用比创建新内容要节省的多。

 当 view 首次出现在屏幕上时,系统会要求它绘制其内容。系统捕获该内容的 snapshot,并将该 snapshot 用作 view 的视觉表示。如果你从不更改 view 的内容,则可能永远不会再次调用 view 的绘图代码。snapshot 图像可用于涉及 view 的大多数操作。如果确实更改了内容,则会通知系统 view 已更改。然后,views 重复绘制 view 的过程并捕获绘制结果的 snapshot。

 当 view 的 contents 更改时,你不会直接重绘这些更改。而是使用 setNeedsDisplay 或 setNeedsDisplayInRect: 方法使 view 被标记为无效。这些方法告诉系统, view 的内容已更改,需要在下一个时机重新绘制。在启动任何重新绘制操作之前,系统将一直等到当前 run loop 结束。此延迟使你有机会一次使多个 view 无效,从层次结构中添加或删除 views、隐藏 views、调整 views 大小以及重新放置 views。你所做的所有更改都将同时在下一次绘制结果中反映出来。(即我们可以把一组对 views 的不同操作放在一起,然后在下次绘制时统一进行绘制,而不是说对 views 进行一步操作就绘制一次)

Note: 更改 view 的几何形状不会自动导致系统重新绘制 view 的内容。view 的 contentMode 属性确定如何解释对 view 几何的更改。大多数 content modes 会在 view 范围内拉伸或重新定位现有 snapshot,而不创建新 snapshot。(UIViewContentModeRedraw 的话则是强制系统调用 view 的 drawRect: 方法进行重绘)

 对于自定义 UIView 子类,通常会重写 view 的 drawRect: 方法,并使用该方法绘制 view 的内容。还有其他提供 view 内容的方法,例如直接设置 underlying layer 的 contents,但是重写 drawRect: 方法是最常见的技术。

 在每个 view 后面都有一个 layer 对象的好处之一是,你可以轻松地对许多与 view 相关的更改进行动画处理。动画是一种向用户传达信息的有用方法,在设计应用程序时应始终考虑动画。 UIView 类的许多属性都是可设置动画的,也就是说,存在从一个值到另一个值进行动画制作的半自动支持。

 你可以在 UIView 对象上设置动画的属性包括:

  • frame - 使用此动画为 view 设置位置和大小变化。(CALayer 类的 frame 不支持动画,可使用 bounds 和 position 属性来达到同样的效果)
  • bounds - 使用此动画可对 view 大小进行动画处理。
  • center - 使用它可以动画化 view 的位置。
  • transform - 使用它旋转或缩放 view。
  • alpha - 使用它可以更改 view 的透明度。
  • backgroundColor - 使用它可以更改 view 的背景色。
  • contentStretch - 使用它来更改 view 内容的拉伸方式。

 已经证实了图层不能像视图那样处理触摸事件,那么它能做哪些视图不能做的呢?这里有一些 UIView 没有暴露出来的 CALayer 的功能:

  • 阴影、圆角、带颜色的边框
  • 3D变换
  • 非矩形范围
  • 透明遮罩
  • 多级非线性动画

 如果使用手势识别器来处理事件,则不需要重写任何事件处理方法(touches...系列函数)。类似地,如果 view 不包含 subviews 或其大小没有更改,则没有理由重写 layoutSubviews 方法。最后,只有当 view 的内容在运行时可以更改,并且你正在使用 UIKit 或 Core Graphics 等 native 技术进行绘制时,才需要 drawRect: 方法。

 虽然 drawRect: 方法是一个 UIView 方法,事实上都是底层 的 CALayer 安排了重绘工作和保存了因此产生的图片。

 虽然自定义绘图有时是必要的,但也应尽可能避免。只有当现有的系统 view 类不提供所需的外观或功能时,才应该真正执行任何 custom drawing。每当你的内容可以与现有 view 的组合进行组合时,你最好将这些 view 对象组合到自定义 view 层次结构中。

 UIKit 使用每个 view 的 opaque 属性来确定该 view 是否可以优化合成操作。对于 custom view,将此属性的值设置为 YES 可以告诉 UIKit,它不需要在 view 背后呈现任何内容。较少的渲染可以提高绘图代码的性能,因此通常会鼓励这样做。当然,如果将 opaque 属性设置为 YES,则 view 必须使用完全不透明的内容完全填充其 bounds 矩形。(如尽量使用 alpha 为 NO 的图片)

 在你想要执行更复杂的动画或 UIView 类不支持的动画的地方,可以使用 Core Animation 和视图的基础 layer 来创建动画。由于 view 和 layer 对象错综复杂地链接在一起,因此对 view layer 的更改会影响 view 本身。使用 Core Animation,可以为 view 的 layer 设置以下类型的动画:

  • layer 的大小和位置
  • 执行 transformations 时使用的 center 点
  • Transformations 为 3D 空间中的 layer 或其 sublayers
  • 在 layer 层次结构中添加或删除 layer
  • layer 相对于其他同级图层(sibling layers)的 Z 顺序(Z-order )
  • layer 的阴影(shadow)
  • layer 的 border(包括 layer 的 corners 是否圆角)
  • 在调整大小操作期间拉伸的 layer 部分
  • layer 的不透明度(opacity)
  • 超出 layer bounds 的 sublayers 的裁剪行为
  • layer 的当前内容(contents)
  • layer 的栅格化行为(rasterization behavior)

 应用程序可以根据需要自由混合 view-based 和 layer-based 的动画代码,但配置动画参数的过程取决于 layer 的所有者。更改 view 拥有的 layer 与更改 view 本身是相同的,并且应用于 layer 属性的任何动画都会考虑当前 view-based 的 animation block 的动画参数。对于你自己创建的 layer,情况并非如此。自定义 layer 对象忽略 view-based 的 animation block 参数,而使用默认的 Core Animation 参数。

 因为 view 对象是应用程序与用户交互的主要方式,所以它们有许多职责。以下是一些:

  • 布局和 subview 管理
    • view 相对于其 superview 定义了自己的默认大小调整行为。
    • view 可以管理 subviews 列表。
    • view 可以根据需要覆盖其 subviews 的大小和位置。
    • view 可以将其坐标系中的点转换为其他 views 或 window 的坐标系。
  • 绘图和动画
    • view 在其矩形区域中绘制内容。
    • 某些 view 属性可以设置为新值时附加动画。
  • 事件处理
    • view 可以接收触摸事件。
    • view 参与 responder chain。

 在调用视图的 drawRect: 方法之前,UIKit 会为视图配置基本的绘图环境。具体来说,它创建一个图形上下文,并调整坐标系和剪裁区域以匹配视图的坐标系和可见边界。因此,在调用 drawRect: 方法时,可以开始使用 native 绘图技术(如 UIKit 和 Core Graphics)绘制内容。可以使用 UIGraphicsGetCurrentContext 函数获取指向当前图形上下文的指针。

 参考链接🔗🔗:


7. UIView 的 layoutSubviews/setNeedsLayout/layoutIfNeeded/drawRect:/setNeedsDisplay/setNeedsDisplayInRect: 函数。

 layoutSubviews 由 layoutIfNeeded 自动调用,子类可以根据需要重写此方法,以更精确地布局其子视图。仅当子视图的自动调整大小(autoresizing)和基于约束(constraint-based)的行为没有提供所需的行为时,才应重写此方法。你不应该直接调用此方法。如果要强制更新布局,请在下次 drawing update 之前调用 setNeedsLayout 方法。如果要立即更新视图的布局,请调用 layoutIfNeeded 方法。

 setNeedsLayout 使 UIView 的当前布局无效,并在下一个更新周期内触发布局更新。如果要调整视图子视图的布局,请在应用程序的主线程上调用此方法。此方法记录请求并立即返回。因为此方法不强制立即更新,而是等待下一个更新周期,所以可以使用它在更新任何视图之前使多个视图的布局无效。此行为允许你将所有布局更新合并到一个更新周期,这通常会提高性能。

 layoutIfNeeded 如果布局更新正在等待中,请立即布置子视图。使用此方法可强制视图立即更新其布局。使用 Auto Layout 时,布局引擎会根据需要更新视图的位置,以满足约束的更改。使用接收消息的视图作为根视图,此方法从根视图开始布局子视图。如果根视图没有被标记为需要更新布局,则此方法将退出,而不修改布局或调用任何与布局相关的回调。

 drawRect: 在传入的矩形内绘制 UIView 的图像。此方法的默认实现不执行任何操作。使用 Core Graphics 和 UIKit 等技术绘制视图内容的子类应重写此方法并在那里实现其绘制代码。如果视图以其他方式设置其内容,则不需要重写此方法。例如,如果视图仅显示背景色,或者视图直接使用底层对象(CALayer)设置其内容,则不需要重写此方法。类似地,如果使用 OpenGL ES 和 GLKView 类进行绘制,那么 GLKit 在调用此方法(或 glkView:drawInRect: GLKView 委托的方法),因此你只需发出渲染内容所需的任何 OpenGL ES 命令。当第一次显示视图或发生使视图的可见部分无效的事件时,将调用此方法。你不应该自己直接调用这个方法。若要使视图的一部分无效,从而导致该部分被重新绘制,请改为调用 setNeedsDisplay 或 setNeedsDisplayInRect: 方法。

 setNeedsDisplay/setNeedsDisplayInRect: 将 UIView 的整个 bounds 矩形标记为需要重绘。你可以使用此方法或 setNeedsDisplayInRect: 通知系统你的视图内容需要重绘。此方法记录请求并立即返回。该视图实际上直到下一个绘图周期才被重绘,此时所有无效的视图都将更新。应该使用此方法请求仅当视图的内容或外观更改时才重新绘制视图。如果仅更改视图的几何图形,则通常不会重新绘制视图。而是根据视图的 contentMode 属性中的值调整其现有内容。重新显示现有内容可以避免重新绘制未更改的内容,从而提高性能。

 参考链接🔗🔗:


8. CALayer 的一些重要知识点。

 CALayerDelegate 协议则是提供给 CALayer 的 delegate 必须遵守的协议(在 iOS 中 View 的 layer 属性的 delegate 默认是 View 本身),实现三个作用:提供 CALayer 的内容、布局 CALayer 子图层(- layoutSublayersOfLayer:)、提供图层的操作(- actionForLayer:forKey:)。 但是它的所有协议方法默认都是可选的(@optional)。其中 - displayLayer:- drawLayer:inContext: 以两种不同的方式为 CALayer 提供内容,不过 - displayLayer: 执行级别高于 - drawLayer:inContext:,当 CALayer 的 delegate 实现了 - displayLayer: 方法后则不再调用 - drawLayer:inContext: 方法。 - displayLayer: 委托方法通常在 CALayer 调用其 setNeedsDisplay 方法标记 CALayer 需要重新加载其内容时被调用,且 CALayer 的 - display 方法的默认实现会调用 - displayLayer: 委托方法。 同样,当 - displayLayer: 委托方法未实现时,- drawLayer:inContext: 委托方法通常在 CALayer 调用其 setNeedsDisplay 方法标记 CALayer 需要重新加载其内容时被调用,不同的是 CALayer 的 - drawInContext: 方法的默认实现会调用 - drawLayer:inContext: 委托方法。 而 - layerWillDraw: 委托方法则是在 - drawLayer:inContext: 之前调用。你可以使用此方法在 - drawLayer:inContext: 之前配置影响 contents 的任何 CALayer 状态,例如 contentsFormat 和 opaque。

 Layers 通常用于为 view 提供 backing store,但也可以在没有 view 的情况下使用以显示内容。layer 的主要工作是管理你提供的视觉内容(visual content),但 layer 本身也具有可以设置的视觉属性(visual attributes),例如背景色(background color)、边框(border)和阴影(shadow)。除了管理视觉内容外,layer 还维护有关其内容的几何(geometry)(例如其位置(position)、大小(size)和变换(transform))的信息,这些信息用于在屏幕上显示该内容。修改 layer 的属性是在 layer 的内容或几何(geometry)上启动动画的方式。layer 对象通过 CAMediaTiming 协议封装 layer 及其动画的持续时间(duration)和步调(pacing),该协议定义了 layer 的时间信息(timing information)。

- (nullable instancetype)presentationLayer; 方法返回 presentation layer 对象的副本,该对象表示当前在屏幕上显示的 layer 的状态,通过此方法返回的 layer 对象提供了当前在屏幕上显示的 layer 的近似值。在动画制作过程中,你可以检索该对象并使用它来获取那些动画的当前值。

- (instancetype)modelLayer; 返回与 receiver 关联的 model layer 对象(如果有)。表示基础模型层(underlying model layer)的 CALayer 实例。

@property(nullable, strong) id contents; 提供 CALayer 内容的对象,可动画的。如果使用 CALayer 显示静态图像,则可以将此属性设置为 CGImageRef,其中包含要显示的图像。

- (void)display; 重新加载该层的内容。不要直接调用此方法。CALayer 会在适当的时候调用此方法以更新 CALayer 的内容。如果 CALayer 具有 delegate 对象,则此方法尝试调用 delegate 的 displayLayer: 方法,delegate 可使用该方法来更新 CALayer 的内容。如果 delegate 未实现 displayLayer: 方法,则此方法将创建 backing store 并调用 CALayer 的 drawInContext: 方法以将内容填充到该 backing store 中。新的 backing store 将替换该 CALayer 的先前内容。

 子类可以重写此方法,并使用它直接设置 CALayer 的 contents 属性。如果你的自定义 CALayer 子类对图层更新的处理方式不同,则可以执行此操作。

 重新加载 CALayer 的内容,调用 CALayer 的 drawInContext: 方法,然后更新 CALayer 的 contents 属性。通常,不直接调用它。

- (void)drawInContext:(CGContextRef)ctx; 使用指定的图形上下文绘制 CALayer 的内容,图形上下文可以被裁剪以保护有效的 CALayer 内容,希望找到要绘制的实际区域的子类可以调用 CGContextGetClipBoundingBox。此方法的默认实现本身不会进行任何绘制。如果 CALayer 的 delegate 实现了 - drawLayer:inContext: 方法,则会调用该方法进行实际绘制。子类可以重写此方法,并使用它来绘制 CALayer 的内容,绘制时,应在逻辑坐标空间中的点指定所有坐标。

 当 contents 属性被更新时,通过 - display 方法调用。默认实现不执行任何操作。上下文可以被裁剪以保护有效的 CALayer 内容。希望找到要绘制的实际区域的子类可以调用 CGContextGetClipBoundingBox()

 shadowOpacity/shadowRadius/shadowOffset/shadowColor/shadowPath

@property(nullable, copy) NSDictionary *style; 可选 NSDictionary,用于存储未由 CALayer 明确定义的属性值。

@property BOOL allowsEdgeAntialiasing; 指示是否允许该 CALayer 执行边缘抗锯齿。值为 YES 时,允许 CALayer 按照其 edgeAntialiasingMask 属性中的值要求对其边缘进行抗锯齿。默认值是从 main bundle 的 Info.plist 文件中的 boolean UIViewEdgeAntialiasing 属性读取的。如果未找到任何值,则默认值为 NO。

@property BOOL allowsGroupOpacity; 指示是否允许该 CALayer 将自身与其父级分开组合为一个组。当值为 YES 且 CALayer 的 opacity 属性值小于 1.0 时,允许 CALayer 将其自身组合为与其父级分开的组。当 CALayer 包含多个不透明组件时,这会给出正确的结果,但可能会降低性能。

@property(nullable, copy) NSArray *filters; 一组 Core Image 过滤器,可应用于 CALayer 及其 sublayers 的内容,可动画的。你添加到此属性的过滤器会影响 CALayer 的内容,包括其 border、填充的背景和 sublayers。此属性的默认值为 nil。

 在 CIFilter 对象附加到 CALayer 之后直接更改其输入会导致未定义的行为。可以在将过滤器附着到 CALayer 后修改过滤器参数,但必须使用 CALayer 的 setValue:forKeyPath: 方法执行此操作。此外,必须为过滤器指定一个名称,以便在数组中标识它。例如,要更改过滤器的 inputRadius 参数,可以使用类似以下代码:

CIFilter *filter = ...;
CALayer *layer = ...;
 
filter.name = @"myFilter";
layer.filters = [NSArray arrayWithObject:filter];
[layer setValue:[NSNumber numberWithInt:1] forKeyPath:@"filters.myFilter.inputRadius"];

 iOS 中的图层不支持此属性。

@property(nullable, strong) id compositingFilter; 一个 Core Image 滤镜,用于合成 CALayer 及其背后的内容。

@property(nullable, copy) NSArray *backgroundFilters; 一组 Core Image 过滤器,可应用于紧靠该图层后面的内容。

@property(copy) CALayerContentsFilter minificationFilter; 减小内容大小时使用的过滤器。此属性的默认值为 kCAFilterLinear。(magnificationFilter 增大)

 呈现 CALayer 的 contents 属性时要使用的过滤器类型。缩小滤镜用于减小图像数据的大小,放大滤镜用于增大图像数据的大小。当前允许的值为 "nearest" 和 "linear"。这两个属性默认为 "linear"。

 图 1 显示了当将一个10 x 10 点的圆图像放大 10 倍时,线性 filtering 和最近 filtering 之间的差异。

 Figure 1 Circle with different magnification filters

Circle_with_different_magnification_filters

 左侧的圆圈使用 kCAFilterLinear,右侧的圆圈使用 kCAFilterNearest。

@property CAEdgeAntialiasingMask edgeAntialiasingMask; 此属性指定层的哪些边缘被消除锯齿,并且是 CAEdgeAntialiasingMask 中定义的常量的组合。你可以分别为每个边缘(顶部,左侧,底部,右侧)启用或禁用抗锯齿。默认情况下,所有边缘均启用抗锯齿。通常,你将使用此属性为与其他层的边缘邻接的边缘禁用抗锯齿,以消除否则会发生的接缝。

@property BOOL drawsAsynchronously; 指示是否在后台线程中延迟和异步处理绘制命令。默认值为 NO。当此属性设置为 YES 时,用于绘制图层内容的图形上下文将对绘制命令进行排队,并在后台线程上执行这些命令,而不是同步执行这些命令。异步执行这些命令可以提高某些应用程序的性能。但是,在启用此功能之前,你应该始终衡量实际的性能优势。

 如果为 YES,则传递给 drawInContext: 方法的 CGContext 对象可以将提交给它的绘图命令排入队列,以便稍后执行它们(即,与 - drawInContext: 方法的执行异步)。这可以允许该层比同步执行时更快地完成其绘制操作。默认值为 NO。

@property BOOL shouldRasterize; 指示在合成之前是否将 CALayer 渲染为位图。默认值为 NO。可动画的。当此属性的值为 YES 时,层将在其局部坐标空间中渲染为位图,然后与任何其他内容合成到目标。阴影效果和 filters 属性中的任何过滤器都将光栅化并包含在位图中。但是,层的当前不透明度未光栅化。如果光栅化位图在合成过程中需要缩放,则会根据需要应用 minificationFilter 和 magnificationFilter 属性中的过滤器。如果此属性的值为 NO,则在可能的情况下将图层直接复合到目标中。如果合成模型的某些功能(例如包含滤镜)需要,则在合成之前仍可以对图层进行栅格化。

@property CGFloat rasterizationScale; 相对于图层的坐标空间栅格化内容的比例。可动画的。当 shouldRasterize 属性中的值为 YES 时,图层将使用此属性来确定是否缩放栅格化的内容(以及缩放多少)。此属性的默认值为 1.0,这表示应以当前大小对其进行栅格化。较大的值将放大内容,较小的值将缩小内容。

- (void)renderInContext:(CGContextRef)ctx; 将图层及其子图层渲染​​到指定的上下文中。此方法直接从图层树进行渲染,而忽略添加到渲染树的所有动画。在图层的坐标空间中渲染。

 参考链接🔗🔗:


9. CALayer 的 setNeedsDisplay/setNeedsDisplayInRect:/displayIfNeeded/needsDisplay/needsDisplayForKey: 函数。

- (void)setNeedsDisplay; 将图层的内容标记为需要更新。调用此方法将导致图层重新缓存其内容。这导致该层可能调用其 delegate 的 displayLayer:drawLayer:inContext: 方法。图层的 contents 属性中的现有内容将被删除,以便为新内容腾出空间。

- (void)setNeedsDisplayInRect:(CGRect)r; 将指定矩形内的区域标记为需要更新。r: 标记为无效的图层的矩形区域。你必须在图层自己的坐标系中指定此矩形。

- (void)displayIfNeeded; 如果图层当前被标记为需要更新,则启动该图层的更新过程。你可以根据需要调用此方法,以在正常更新周期之外强制对图层内容进行更新。但是,通常不需要这样做。更新图层的首选方法是调用 setNeedsDisplay,并让系统在下一个周期更新图层。

 如果接收方被标记为需要重绘,则调用 - display

- (BOOL)needsDisplay; 返回一个布尔值,指示该图层是否已标记为需要更新。

+ (BOOL)needsDisplayForKey:(NSString *)key; 返回一个布尔值,指示对指定 key 的更改是否需要重新显示该图层。key: 一个字符串,它指定图层的属性。子类可以重写此方法,如果在指定属性的值更改时应重新显示该图层,则返回 YES。更改属性值的动画也会触发重新显示。此方法的默认实现返回 NO。

 子类重写的方法。对于给定的属性,返回 YES 会导致更改属性时(包括通过附加到该图层的动画进行更改时)重绘该图层的内容。默认实现返回 NO。子类应为超类定义的属性调用超类。(例如,不要尝试对 CALayer 实现的属性返回 YES,这样做会产生不确定的结果。)


10. CALayer 的 addAnimation:forKey:/animationForKey:/removeAllAnimations/removeAnimationForKey:/animationKeys 等函数。

- (void)addAnimation:(CAAnimation *)anim forKey:(nullable NSString *)key; 将指定的动画对象添加到图层的渲染树(render tree)。(目前为止已经见到过 "表示树"、"模型树"、"渲染树"、"层级树",在 Core Animation 文档里面都能得到解释)anim: 要添加到渲染树的动画。该对象由渲染树复制,不引用(not referenced)。因此,对动画对象的后续修改不会传播到渲染树中。key: 标识动画的字符串。每个唯一键仅将一个动画添加到该层。特殊键 kCATransition 自动用于过渡动画。你可以为此参数指定 nil。

 如果动画的 duration 属性为零或负,则将 duration 更改为 kCATransactionAnimationDuration 事务属性的当前值(如果已设置)或默认值为 0.25 秒。

 将动画对象附加到图层。通常,这是通过作为 CAAnimation 对象的 action 隐式调用的。(CAAnimation 遵循 CAAction 协议)

 key 可以是任何字符串,因此每个唯一 key 每个图层仅添加一个动画。特殊键 transition 会自动用于过渡动画(transition animations)。 nil 指针也是有效的键。

 如果动画的 duration 属性为零或负数,则指定默认持续时间,否则为 animationDuration transaction 属性的值,否则为 0.25 秒。

 在将动画添加到图层之前先对其进行复制,因此,除非对动画进行任何后续修改,否则将其添加到另一层都不会产生影响。

- (nullable __kindof CAAnimation *)animationForKey:(NSString *)key; 返回具有指定标识符的动画对象。你可以使用此字符串来检索已经与图层关联的动画对象。但是,你不得修改返回对象的任何属性。这样做将导致不确定的行为。

- (void)removeAllAnimations; 删除所有附加到该图层的动画。

- (void)removeAnimationForKey:(NSString *)key; 使用指定的 key 删除动画对象。

- (nullable NSArray<NSString *> *)animationKeys; 返回一个字符串数组,这些字符串标识当前附加到该图层的动画。数组的顺序与将动画应用于图层的顺序匹配。

 可看到 layout 和 display 的一组方法的使用方式和命名方式基本相同。- setNeedsDisplay/- setNeedsLayout 标记在下一个周期需要进行 display/layout,- displayIfNeeded/- layoutIfNeeded 如果需要则立即执行 display/layout,- needsDisplay/- needsLayout 返回是否需要 display/layout,- display/- layoutSublayers 更新执行 display/layout。

@property(copy) NSArray<CAConstraint *> *constraints; 用于定位当前图层的子图层的约束。macOS 应用程序可以使用此属性来访问其 layer-based 的约束。在应用约束之前,还必须将 CAConstraintLayoutManager 对象分配给图层的 layoutManager 属性。iOS 应用程序不支持基于图层的约束。

- (CGPoint)convertPoint:(CGPoint)p fromLayer:(nullable CALayer *)l; 将点从指定图层的坐标系转换为接收者的坐标系。

- (CGRect)convertRect:(CGRect)r fromLayer:(nullable CALayer *)l; 将矩形从指定图层的坐标系转换为接收者的坐标系。

- (nullable __kindof CALayer *)hitTest:(CGPoint)p; 返回包含指定点的图层层次结构中接收者的最远子图层(包括自身)。

🎉🎉🎉 未完待续...