Runtime 是一套 C 语言的 API,属于OC 底层的实现。接下来将从消息机制、归档、Hook(下钩子)、动态的添加方法四个方面来简单地聊一下。
一、消息机制
OC 是一门动态语言,所有的方法调用都会在底层转化成消息发送。为了证明这个观点,做如下实验。
打开Xcode,新建一个CommandLine 程序,新建一个继承自NSObject 的Person 类,如下所示
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [Person alloc];//堆区分配空间 malloc
p = [p init];//初始化对象
}
return 0;
}
接下来,看一下这段代码的底层的实现情况。打开终端,cd 到工程的根目录,运行命令 :
clang -rewrite-objc main.m
会得到一个 main.cpp的文件。利用Xcode 打开,会发现其内容比较长(有近10万行代码)。选取最后面的十几行代码:
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
Person *p = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc"));
p = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("init"));
objc_msgSend(p, sel_registerName("init"));
}
return 0;
}
由上可以得知:
[Person alloc] 被转化为了 objc_msgSend(objc_getClass("Person"), sel_registerName("alloc"));
[p init] 被转化为了 objc_msgSend(p, sel_registerName("init"))。
其中,objc_msgSend( )函数的作用是向对象发送消息,objc_getClass( )作用是获取类对象,sel_registerName( )作用是注册消息。
假设Person对象含有一个对象方法:eat,则可以有如下几种方式实现调用:
1. [p eat];
2. [p performSelector:@selector(eat)];
3. objc_msgSend(p, @selector(eat));
4. objc_msgSend(p, sel_registerName("eat"));
二、归档
还是以刚才的Person类为例,假设其有两个属性:name 和 age
@interface Person : NSObject<NSCoding>
@property(nonatomic,strong) NSString *name;
@property(nonatomic,assign) NSInteger age;
@end
其实现归档的方式为:
- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super init];
if (self) {
_name = [coder decodeObjectForKey:@"name"];
_age = [coder decodeIntegerForKey:@"age"];
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)coder
{
[coder encodeObject:_name forKey:@"name"];
[coder encodeInteger:_age forKey:@"age"];
}
当类的属性比较多的情况下,再使用这种方式归档的话,就不免有些麻烦,尤其是增加或者删除某些属性后都需要修改这两个方法的内容。而通过Runtime实现归档就可以避免这些麻烦的出现:
- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super init];
if (self) {
unsigned int count = 0;
Ivar * ivars = class_copyIvarList([self class], &count);
for(int i =0 ;i<count;i++)
{
NSString *key =[NSString stringWithUTF8String:ivar_getName(ivars[i])];
id value = [coder decodeObjectForKey:key];
[self setValue:value forKey:key];
}
free(ivars);//释放对应的区域
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)coder
{
unsigned int count = 0;
//在C里面,但凡让传递一个基本数据类型的指针,它内部就是想要改变外面的值
Ivar * ivars = class_copyIvarList([self class], &count);
for(int i =0;i<count;i++)
{
NSString * key = [NSString stringWithUTF8String:ivar_getName(ivars[i])];
[coder encodeObject:[self valueForKey:key] forKey:key];
}
free(ivars);
}
Ivar 是表示成员变量的类型,class_copyIvarList( )作用是获取成员变量的列表。注:记得引入头文件 #import <objc/runtime.h>
三、Hook(下钩子)
设想如下的场景:
- (void)viewDidLoad {
[super viewDidLoad];
NSURL *url = [NSURL URLWithString:@"www.baidu.com/中文"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSLog(@"%@",request);
}
我们知道,利用含有中文的字符串生成的URL对象会为 nil,而系统的URLWithString方法并没有对出现nil 时的情况进行提示。但我们可以自己加上,而且很容易想到通过类别的方式添加:
@interface NSURL (url)
+ (instancetype) createUrlWithString:(NSString *)str;
@end
+ (instancetype)createUrlWithString:(NSString *)str
{
NSURL *url = [NSURL URLWithString:str];
if(url == nil)
{
NSLog(@"URL 为空!!");
}
return url;
}
这样,调用刚才写好的createUrlWithString:方法生成URL对象就能及时察觉空的URL对象的出现。新的问题又出现了:如果这个类别是后期创建的,那么程序中但凡用到URLWithString:方法的地方就必须都改成createUrlWithString:方法。不但麻烦,而且容易遗漏。所以我们不禁会想:能不能不改变调用的方法,即还是调用URLWithString:方法,同时能够达到调用createUrlWithString:的效果呢?
答案是肯定的!!
@interface NSURL (url)
+ (instancetype) createUrlWithString:(NSString *)str;
@end
+ (void)load
{
//下钩子
Method UrlWithStr = class_getClassMethod(self, @selector(URLWithString:));
Method CreateUrlwithStr = class_getClassMethod(self, @selector(createUrlWithString:));
//交换方法实现!!
method_exchangeImplementations(UrlWithStr, CreateUrlwithStr);
}
//高级用法,记得写上备注
+ (instancetype)createUrlWithString:(NSString *)str
{
NSURL *url = [NSURL createUrlWithString:str];
if(url == nil)
{
NSLog(@"URL 为空!!");
}
return url;
}
load 方法是在APP被装载进内存的时候调用,可以说比 main.m 中的 main 函数执行的还要早。所以我们可以在 load 方法里做文章。
通过 class_getClassMethod( )方法分别获取到 URLWithString:方法和createUrlWithString:方法的IMP( 函数指针)。这里有个形象的比喻帮助大家理解方法跟IMP的关系:方法如同书中的目录,IMP如同书中的页码。方法的最终实现情况取决于IMP。
由此,我们可以想到,在方法不变的情况下,可以修改其相应的IMP达到修改方法实际调用情况的目的。method_exchangeImplementations( , )函数恰好帮我们实现了这个目标。即完成了URLWithString:方法和createUrlWithString:方法的IMP的交换。所以,接下来调用URLWithString:方法实际上最终调用的是我们之前已经写好的createUrlWithString:方法的实现。
细心的小伙伴可能发现了我这里有个细节,看上去不符合常规:
这里可以负责人的告诉小伙伴:没错!就该这样写!!哈哈……
因为我们已经交换了两个方法的IMP,因此调用createUrlWithString:方法实际上调用的是URLWithString:方法的实现,没毛病!如果函数里面调用的还是URLWithString:方法的话,会导致createUrlWithString:方法形成递归,即不断地调用自己,程序会卡住,一段时间后就会闪退!感兴趣的小伙伴可以试一下,下面附上我的运行结果:
四、动态地添加方法
创建一个类Person_add (为了区分之前的Person)继承自NSObject,不添加任何成员方法和变量。作如下处理:
- (void)viewDidLoad {
[super viewDidLoad];
Person_add *p = [[Person_add alloc]init];
objc_msgSend(p, @selector(eat:),@"汉堡",@"水果");
}
程序运行后,大家很容易想到程序会闪退。因为Person_add 对象既没有声明更没有实现eat:方法。
前面已经提到,OC中所有的方法调用最终都会在底层转化为消息发送。这里要清楚一点,objc_msgSend ( )方法看起来好像返回了数据,其实objc_msgSend() 从不返回数据,而是你的方法在运行时实现被调用后才会返回数据。下面详细叙述消息的发送步骤:
- 首先检测这个 selector 是不是要忽略。比如Mac OSX开发,有了垃圾回收就不理会 retain,release 这些函数;
- 检测这个 selector 的target 是不是 nil,Objc 允许我们对一个 nil 对象执行任何方法不会Crash,因为运行时会被忽略掉;
- 如果上面两步都通过了,那么就开始查找这个类的实现 IMP,先从 cache 里查找,如果找到了就运行对应的函数去执行相应的代码;
- 如果类的列表中找不到,就到父类的方法列表中查找,一直找到 NSObject 类为止;
- 如果还找不到,就要开始进入动态方法解析了。
开始了动态解析后,Runtime 会调用 resolveInstanceMethod:或者 resolveClassMethod:来给我们一次动态添加方法实现的机会:
#import "Person_add.h"
#import <objc/runtime.h>
#import <objc/message.h>
@implementation Person_add
//对象方法
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
NSLog(@"%@",NSStringFromSelector(sel));
//添加一个
if(sel == @selector(eat:)){
/*
1.cls 目标类
2.SEL 方法编号
3.IMP 方法的实现
4.返回值类型
*/
class_addMethod(self, sel, (IMP)haha, "V@:@");
}
return [super resolveInstanceMethod:sel];
}
/*
1.方法的调用者
2.方法的编号
*/
void haha(id self,SEL _cmd,NSString *str,NSString *str2)
{
NSLog(@"吃%@",str2);
}
@end
上面的例子为 eat:方法添加了实现内容,就是 haha 方法中的代码。其中 "V@:@" 表示返回值和参数。运行结果为: