约会 Runtime

阅读笔记

神经病院 Objective-C Runtime 住院第二天——消息发送与转发
神经病院 Objective-C Runtime 出院第三天——如何正确使用 Runtime

objc_msgSend

[receiver message]会被编译器转化为:

1
2
3
4
5
// self: A pointer that points to the instance of the class that is to receive the message.
// op: The selector of the method that handles the message.
id objc_msgSend ( id self, SEL op, ... );

typedef struct objc_selector *SEL;

objc_selector 是一个映射到方法的C字符串。

receiver拿到对应的selector之后,如果自己无法执行这个方法,那么该条消息要被转发。或者临时动态的添加方法实现。如果转发到最后依旧没法处理,程序就会崩溃。

编译期仅仅是确定了要发送消息,而消息如何处理是要运行期需要解决的事情。

objc_msgSend会做一下几件事情:
1.检测这个 selector 是不是要忽略的。
2.检查target是不是为nil。

如果这里有相应的nil的处理函数,就跳转到相应的函数中。
如果没有处理nil的函数,就自动清理现场并返回。这一点就是为何在OC中给nil发送消息不会崩溃的原因。

3.确定不是给nil发消息之后,在该class的缓存中查找方法对应的IMP实现。

如果找到,就跳转进去执行。
如果没有找到,就在方法分发表里面继续查找,一直找到NSObject为止。

4.如果还没有找到,那就需要开始消息转发阶段了。至此,发送消息Messaging阶段完成。这一阶段主要完成的是通过select()快速查找IMP的过程。

消息发送Messaging阶段
lookUpImpOrForward函数逻辑

  • 尝试从类的cache中获取IMP
  • 如果在cache缓存中获取失败,则再去类方法列表里面进行查找
  • 如果以上尝试都失败了,接下来就会循环尝试父类的缓存和方法列表。一直找到NSObject为止。
  • 如果在父类中找到了该方法methodIMP,接下来就应该把这个方法cache回自己的缓存中。
  • 如果没有在父类的cache中找到IMP,继续在父类的方法列表里面查找。
  • 如果父类还没有找到,那么就会开始尝试_class_resolveMethod方法。
  • 回到lookUpImpOrForward方法中,如果也没有找到IMP的实现,那么method resolver也没用了,只能进入消息转发阶段。

消息转发Message Forwarding阶段

  • 到了转发阶段,会调用id _objc_msgForward(id self, SEL _cmd,...)方法
  • 在执行_objc_msgForward之后会调用__objc_forward_handler函数。
  • 这一步是替消息找备援接收者,如果这一步返回的是nil,那么补救措施就完全的失效了,Runtime系统会向对象发送methodSignatureForSelector:消息,

消息发送和转发的全过程:

Category

OC在初始化的时候,会去加载map_imagesmap_images最终会调用objc-runtime-new.mm里面的_read_images方法。_read_images方法里面会去初始化内存中的map, 这个时候将会load所有的类,协议,CategoryNSOBject+load方法就是这个时候调用的。
加载完所有的category之后,就开始处理这些类别。
处理完之后的结果
1)、把category的实例方法、协议以及属性添加到类上
2)、把category的类方法和协议添加到类的metaclass
先去调用addUnattachedCategoryForClass函数,申请内存,分配空间。remethodizeClass这个方法里面会调用attachCategories方法。remethodizeClass方法里面会用头插法,把新加的方法从头插入方法链表中。我们可以在Category里面覆盖原有的方法,因为头插法,新的方法在链表的前面,会优先被遍历到。

优点

实现多继承Multiple Inheritance

在OC程序中可以借用消息转发机制来实现多继承的功能。

通过forwardingTargetForSelector:方法,一个类可以做到继承多个类的效果,只需要在这一步将消息转发给正确的类对象就可以模拟多继承的效果。利用转发消息来实现的是“假”继承,respondsToSelector:isKindOfClass:不会考虑转发链。

Method Swizzling

Method Swizzling理论上可以在运行时通过类名/方法名hook到任何 OC 方法,替换任何类的实现以及新增任意类。Method Swizzling本质上就是对IMPSEL进行交换。

  • Swizzling应该总在+load中执行。+load会在类初始加载时调用, +initialize方法是以懒加载的方式被调用的。
  • Swizzling应该总是在dispatch_once中执行
  • Swizzling+load中执行时,不要调用[super load]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#import <objc/runtime.h>
@implementation UIViewController (Swizzling)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
// When swizzling a class method, use the following:
// Class class = object_getClass((id)self);
SEL originalSelector = @selector(viewWillAppear:);
SEL swizzledSelector = @selector(xxx_viewWillAppear:);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

// 判断原有类中是否有要替换的方法的实现
BOOL didAddMethod = class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
// 当前类中没有要替换方法的实现,需要在父类中去寻找
// 用method_getImplementation去获取class_getInstanceMethod里面的方法实现。然后再进行class_replaceMethod来实现Swizzling。
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
// 当前类中有要替换方法的实现,所以可以直接进行替换
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}

#pragma mark - Method Swizzling
- (void)xxx_viewWillAppear:(BOOL)animated {
// 不会造成死循环
[self xxx_viewWillAppear:animated];
NSLog(@"viewWillAppear: %@", self);
}
@end

Aspect Oriented Programming

AOP作用就是分离横向关注点(Cross-cutting concern)来提高模块复用性,它可以在既有的代码添加一些额外的行为(记录日志、身份验证、缓存)而无需修改代码。

Isa Swizzling

本质上也是交换,不过交换的是Isa。官方库里面有一个很有名的技术就用到了Isa Swizzling,那就是KVO——Key-Value Observing

KVO是为了监听一个对象的某个属性值是否发生变化。在属性值发生变化的时候,肯定会调用其setter方法。所以KVO的本质就是监听对象有没有调用被监听属性对应的setter方法。在执行完addObserver: forKeyPath: options: context: 方法之后,把isa指向到另外一个类去。在这个新类里面重写被观察的对象四个方法。classsetterdealloc_isKVOA

重写class方法是为了我们调用它的时候返回跟重写继承类之前同样的内容。

在新的类中会重写对应的set方法,是为了在set方法中增加另外两个方法的调用:

1
2
- (void)willChangeValueForKey:(NSString *)key
- (void)didChangeValueForKey:(NSString *)key

重写dealloc方法,销毁新生成的NSKVONotifying_类。

所以,永远不要用用isa来判断一个类的继承关系,而是应该用class方法来判断类的实例。

Associated Object关联对象

我们可以通过Associated Object来实现在 Category 中添加属性的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// NSObject+AssociatedObject.h
@interface NSObject (AssociatedObject)
@property (nonatomic, strong) id associatedObject;
@end

// NSObject+AssociatedObject.m
@implementation NSObject (AssociatedObject)
@dynamic associatedObject;

- (void)setAssociatedObject:(id)object {
objc_setAssociatedObject(self, @selector(associatedObject), object, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (id)associatedObject {
return objc_getAssociatedObject(self, @selector(associatedObject));
}

动态的增加方法

在消息发送阶段,如果在父类中也没有找到相应的IMP,就会执行resolveInstanceMethod方法。在这个方法里面,可以动态的给类对象或者实例对象动态的增加方法。

1
2
3
4
5
6
7
8
9
+ (BOOL)resolveInstanceMethod:(SEL)sel {

NSString *selectorString = NSStringFromSelector(sel);
if ([selectorString isEqualToString:@"method1"]) {
class_addMethod(self.class, @selector(method1), (IMP)functionForMethod1, "@:");
}

return [super resolveInstanceMethod:sel];
}

NSCoding的自动归档和自动解档

runtime实现的思路就比较简单,我们循环依次找到每个成员变量的名称,然后利用KVC读取和赋值就可以完成encodeWithCoderinitWithCoder了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#import "Student.h"
#import <objc/runtime.h>
#import <objc/message.h>

@implementation Student

- (void)encodeWithCoder:(NSCoder *)aCoder{
unsigned int outCount = 0;
Ivar *vars = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i ++) {
Ivar var = vars[i];
const char *name = ivar_getName(var);
NSString *key = [NSString stringWithUTF8String:name];

id value = [self valueForKey:key];
[aCoder encodeObject:value forKey:key];
}
}

- (nullable __kindof)initWithCoder:(NSCoder *)aDecoder{
if (self = [super init]) {
unsigned int outCount = 0;
Ivar *vars = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i ++) {
Ivar var = vars[i];
const char *name = ivar_getName(var);
NSString *key = [NSString stringWithUTF8String:name];
id value = [aDecoder decodeObjectForKey:key];
[self setValue:value forKey:key];
}
}
return self;
}
@end

字典和模型互相转换

字典转模型

  1. 调用 class_getProperty 方法获取当前 Model 的所有属性。
  2. 调用 property_copyAttributeList 获取属性列表。
  3. 根据属性名称生成 setter 方法。
  4. 使用 objc_msgSend 调用 setter 方法为 Model 的属性赋值(或者 KVC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
+(id)objectWithKeyValues:(NSDictionary *)aDictionary{
id objc = [[self alloc] init];
for (NSString *key in aDictionary.allKeys) {
id value = aDictionary[key];

/*判断当前属性是不是Model*/
objc_property_t property = class_getProperty(self, key.UTF8String);
unsigned int outCount = 0;
objc_property_attribute_t *attributeList = property_copyAttributeList(property, &outCount);
objc_property_attribute_t attribute = attributeList[0];
NSString *typeString = [NSString stringWithUTF8String:attribute.value];

if ([typeString isEqualToString:@"@\"Student\""]) {
value = [self objectWithKeyValues:value];
}

//生成setter方法,并用objc_msgSend调用
NSString *methodName = [NSString stringWithFormat:@"set%@%@:",[key substringToIndex:1].uppercaseString,[key substringFromIndex:1]];
SEL setter = sel_registerName(methodName.UTF8String);
if ([objc respondsToSelector:setter]) {
((void (*) (id,SEL,id)) objc_msgSend) (objc,setter,value);
}
free(attributeList);
}
return objc;
}

模型转字典

  1. 调用 class_copyPropertyList 方法获取当前 Model 的所有属性。
  2. 调用 property_getName 获取属性名称。
  3. 根据属性名称生成 getter 方法。
  4. 使用 objc_msgSend 调用 getter 方法获取属性值(或者 KVC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//模型转字典
-(NSDictionary *)keyValuesWithObject{
unsigned int outCount = 0;
objc_property_t *propertyList = class_copyPropertyList([self class], &outCount);
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
for (int i = 0; i < outCount; i ++) {
objc_property_t property = propertyList[i];

//生成getter方法,并用objc_msgSend调用
const char *propertyName = property_getName(property);
SEL getter = sel_registerName(propertyName);
if ([self respondsToSelector:getter]) {
id value = ((id (*) (id,SEL)) objc_msgSend) (self,getter);

/*判断当前属性是不是Model*/
if ([value isKindOfClass:[self class]] && value) {
value = [value keyValuesWithObject];
}

if (value) {
NSString *key = [NSString stringWithUTF8String:propertyName];
[dict setObject:value forKey:key];
}
}

}
free(propertyList);
return dict;
}

缺点

Method swizzling不是原子性操作。如果在+load方法里面写,是没有问题的,但是如果写在+initialize方法中就会出现一些奇怪的问题。

可能导致命名冲突。

调用顺序对于Swizzling来说,很重要。

难以理解。

难以调试。