源代码转换为可执行程序,需要经过三个步骤:编译、链接、运行。
在C语言中,到链接的时候,对象的类型、方法的实现就已经确定了。
而在OC中,编译和链接过程中的工作,放到了运行阶段。也就是在没运行的时候,并不知道调用一个方法会发生什么。因此称OC为动态语言。
实现动态特性的基础就是runtime。runtime是底层纯C语言的API库,OC代码会被编译器转换成运行时代码,通过消息机制决定函数调用方式。
runtime可以在运行时创建对象、检查对象,修改类和对象的方法。其中核心内容是消息传递。
一个对象的调用方法,例如[obj foo]。
在编译器阶段,编译器转换后会变成objc_msgSend(obj, foo)。然后到运行时,runtime执行的流程如下:
- 先通过实例obj的isa指针,找到obj的class类对象;
- 在class类对象中的cache中查找是否有缓存的方法
- 如果没有缓存的方法,则去查找objc_method_list中的方法
- 如果没有找到对应的方法,则会继续往它的父类superclass中查找
- 如果找到了方法,将sel的hash找到对应的下标,将sel、imp、cls作为bucket_t存到对应下标的value中,以便下次不用再次到方法列表中查找
- 有对应的IMP后,可以通过找到方法实现的函数。
- 如果没有找到则执行消息转发过程
- 如果消息转发也失败了,就会报出找不到方法的崩溃
根据上篇类和对象中,知道的isa指针的对应指向。
实例对象
其中,对象的源码中
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
/// A pointer to an instance of a class.
typedef struct objc_object *id;
对象的结构体只包含一个指向类的isa指针。
也就是调用方法时,通过isa指针,找到Class类。
类
然后接着看Class,class的定义是objc_class结构体
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
class_rw_t *data() const {
return bits.data();
}
......
}
通过对象的isa指针,找到了对应的类的结构体。
方法调用时,通过结构体中的class_rw_t中objc_method_list和cache进行方法的查找。该类结构体中的方法为实例方法。
元类
而在类中,也有一个isa指向Class,此处指向元类,也就是类对象所属的类,其中结构体中的信息和类的信息类似,方法列表中存储的是类方法。
比如,调用类方法时,流程是通过类的isa指针,找到元类,然后再通过元类的class_rw_t中objc_method_list和cache进行方法的查找。
Method
在上述描述中,在objc_method_list中存储了定义的方法,方法的结构体如下:
/// An opaque type that represents a method in a class definition.
/// 代表类定义中一个方法的不透明类型
typedef struct objc_method *Method;
struct objc_method {
SEL _Nonnull method_name; // 方法名
char * _Nullable method_types; // 方法类型
IMP _Nonnull method_imp; // 方法实现
};
包含了,方法名、方法类型、方法实现。
- SEL方法名
SEL方法名是指向obj_selector的指针,在源码中未能找到其结构体定义,不过通过代码可以看出,实际上SEL存的就是方法的字符串。
在OC中,同一个类是不可以方法名重复,即使传入的参数格式不一样也不行,实现不了函数重载的处理。因为SEL只存储了方法名,并不记录-/+或者参数类型等信息。
- IMP方法实现
IMP实际上是一个指针,指向方法的实现函数。通过IMP找到函数地址,然后执行函数。
- method_types 方法类型
方法类型是字符串,用来存储方法中的参数类型和返回值的类型。
在上述消息发送的过程中,先是在对应的类中查找方法,接着往上一层层找父类的方法。当在最后一层的父类也没找到方法时。runtime会执行消息转发的机制。
消息转发机制如下图所示:
从图中来看,主要分为3大步骤:
- Method resolution 方法解析阶段
- fast forwarding 快速转发阶段
- Normal forwarding 常规转发阶段
当类和父类都找不到对应SEL时,为了不发生unrecognized selector 的错误,需要使用上述三种方式进行消息发送的补救。
Method resolution 方法解析阶段
在进入消息转发阶段后,第一步是先调用+ (BOOL)resolveInstanceMethod:(SEL)sel或+ (BOOL)resolveClassMethod:(SEL)sel来询问是否实现了对应的处理,如果返回YES,则能接受对应的消息并进行处理,返回NO则不会进行处理,进入下一步。
在NSObject源码中,这两个类方法返回的NO。需要对其进行处理,需要进行重写方法和实现。
其中调用的实例方法对应的是resolveInstanceMethod。
代码如下:
TestObject *test = [[TestObject alloc] init];
//比如 调用了一个没有定义的方法
[test performSelector:@selector(logTest)];
在TestObject中的resolution的补救代码如下:
void dynamicMethodIMP(id self, SEL _cmd) {
NSLog(@" dynamicMethodIMP ");
}
@implementation TestObject
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if ([NSStringFromSelector(sel) isEqualToString:@"logTest"]) {
NSLog(@"添加resolveInstanceMethod转发方法");
//实例方法需要添加到类中
class_addMethod([self class], sel, (IMP)dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
类方法对应的是resolveClassMethod。
代码如下:
[TestObject performSelector:@selector(logTest)];
在TestObject中的resolution的补救代码如下:
+ (BOOL)resolveClassMethod:(SEL)sel {
if ([NSStringFromSelector(sel) isEqualToString:@"logTest"]) {
NSLog(@"添加resolveClassMethod转发方法");
//类方法需要插入到元类中
Class metaClass = objc_getMetaClass([NSStringFromClass([self class]) UTF8String]);
class_addMethod(metaClass, sel, (IMP)dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
Fast forwarding快速转发阶段
在消息转发的第一步Method resolution没有得到添加方法的YES返回。那么将进入第二步,需要问一下有没有别人帮忙处理这个方法。
调用的是- (id)forwardingTargetForSelector:(SEL)aSelector。
在NSObject的源码中快速转发的方法返回的都是nil。
创建一个别的类来实现响应对应的方法。
然后实现对应的方法复写,代码如下:
// 消息转发第二步 Fast forwarding 快速转发阶段
- (id)forwardingTargetForSelector:(SEL)aSelector {
if ([NSStringFromSelector(aSelector) isEqualToString:@"logTest"]) {
Class TestObjectTwo = NSClassFromString(@"TestObjectTwo");
//返回实例对象
return [[TestObjectTwo alloc] init];
}
return [super forwardingTargetForSelector:aSelector];
}
如果是类方法,代码如下:
+ (id)forwardingTargetForSelector:(SEL)aSelector {
if ([NSStringFromSelector(aSelector) isEqualToString:@"logTest"]) {
Class TestObjectTwo = NSClassFromString(@"TestObjectTwo");
//返回类对象
return TestObjectTwo;
}
return [super forwardingTargetForSelector:aSelector];
}
使用这个特性,可以做一些解耦或者循环引用的问题,例如,NSTimer导致循环引用的问题。
Normal Forwarding 常规转发阶段
如果第二步返回self或者nil,则找不到可以响应的对象去执行。接下来进入第三步。
第三步的消息转发机制,本质上跟第二步是一样的,都是切换接受消息的对象。但第三步切换响应目标更复杂一些,第二步只需要返回可以响应的对象即可,第三步还需要手动将响应方法切换给备用响应对象。
第三步有2个步骤
- methodSignatureForSelector
- forwardInvocation
methodSignatureForSelector中,返回SEL方法的签名,返回的签名是根据方法的参数来封装的。
手动创建签名,但是尽量少使用,因为容易创建错误。
代码如下,先实现methodSignatureForSelector
// 3.1 先创建签名标签
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
//写法例子
//例子"v@:@"
//v@:@ v 返回值类型void;@ id类型,执行sel的对象;: SEL;@ 参数
//例子"@@:"
//@ 返回值类型id;@ id类型,执行sel的对象;: SEL
if ([super methodSignatureForSelector:aSelector] == nil) {
NSMethodSignature *sign = [NSMethodSignature signatureWithObjCTypes:"v@:"];
return sign;
}
return [super methodSignatureForSelector:aSelector];
}
然后再实现forwardInvocation
// 执行消息转发调用
- (void)forwardInvocation:(NSInvocation *)anInvocation {
//创建备用对象
Class TestObjectTwo = NSClassFromString(@"TestObjectTwo");
id testObjectTwo = [[TestObjectTwo alloc] init];
SEL sel = anInvocation.selector;
if ([TestObjectTwo respondsToSelector:sel]) {
[anInvocation invokeWithTarget:testObjectTwo];
} else {
[self doesNotRecognizeSelector:sel];
}
}
类方法转发,只需要将-变成+,然后forwardInvocation中的target使用Class。
以上就是消息转发机制,转发过程越早越好,第一步实现添加后,runtime会缓存到方法缓存里面,下次再调用可以提高效率,而后面的步骤每次都得进行代码查找调用,最后一步还需要处理完整的NSInvocation。
消息转发可以动态添加部分方法。可以实现多重代理,不同的对象同时代理,然后再各自负责的区域进行相应的处理,降低代码耦合。以及简洁实现多继承。
使用方法交换,对业务逻辑进行分离,降低耦合度。
比如,在所有页面都添加统计功能,其中除了手动添加和继承一个基类或使用分类来解决之外,就是使用Method Swizzling来解决。或者是对一些不能改动源码的方法做hook处理。
交换的实际上是Selector和IMP的对应关系。从而达到通过selector去查找调用时,实际上执行的是交换后的IMP。
一般交换是在+load中完成,应该只在dispatch_once中交换。+load是在类加载的时候就被调用,启动时就会先加载所有的类,在main函数之前就会执行+load方法。
+initialize类似懒加载,是在类或者子类的第一个方法被调用时调用,默认只加载一次。+initialize调用发生在+init之前。
+load | +initialize | |
---|---|---|
执行时机 | 在main函数之前 | 在类的方法被第一次调用时执行 |
若自身未定义,是否沿用父类的方法 | 否 | 是 |
类别中的定义 | 全都执行,但在类中的方法后 | 覆盖类中的方法,只执行一次 |
交换的代码如下:
+ (void)load {
[super load];
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
// 原方法名 和 要替换的方法名
SEL originalSelector = @selector(viewDidAppear:);
SEL swizzledSelector = @selector(swizzle_viewDidAppear:);
//获取方法
Method oriMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
if (!class || !oriMethod || !swizzledMethod) {
return;
}
//多加一层判断,如果添加成功,表示该方法不存在于本类,而是在父类中,不能交换父类的方法,否则父类的对象调用该方法时会崩溃,如果添加失败说明存在于本类中
BOOL canAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if (canAddMethod) {
//添加成功后,将原有的实现替换到swizzledMethod上,实现方法的交换而不影响父类的方法实现
class_replaceMethod(class, swizzledSelector, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
} else {
// 说明在本类中,交换方法的实现
method_exchangeImplementations(oriMethod, swizzledMethod);
}
});
}
- (void)swizzle_viewDidAppear:(BOOL)animated {
NSLog(@"执行了交换的方法");
//此处调用swizzle_viewDidAppear不会造成递归调用,因为已经交换过,执行的是原来的方法
[self swizzle_viewDidAppear:animated];
}
虽然可以直接用KVC来实现字典转模型,方法是调用setValuesForKeysWithDictionary:,但这种方法要求,Key是一一对应,否则就会报错。这里可以使用Runtime配合KVC来实现字典转模型。
下面添加一个NSObject的分类来实现,代码如下:
- (instancetype)initWithDictionary:(NSDictionary *)dictionary {
self = [self init];
if (self) {
unsigned int count;
objc_property_t *propertyList = class_copyPropertyList([self class], &count);
for (int i = 0; i < count; i++) {
// 获取属性列表
const char *propertyName = property_getName(propertyList[i]);
NSString *name = [NSString stringWithUTF8String:propertyName];
id value = [dictionary objectForKey:name];
if (value) {
[self setValue:value forKey:name];
}
}
free(propertyList);
}
return self;
}