聊一聊 Runtime

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:方法的实现。
细心的小伙伴可能发现了我这里有个细节,看上去不符合常规:


image.png

这里可以负责人的告诉小伙伴:没错!就该这样写!!哈哈……
因为我们已经交换了两个方法的IMP,因此调用createUrlWithString:方法实际上调用的是URLWithString:方法的实现,没毛病!如果函数里面调用的还是URLWithString:方法的话,会导致createUrlWithString:方法形成递归,即不断地调用自己,程序会卡住,一段时间后就会闪退!感兴趣的小伙伴可以试一下,下面附上我的运行结果:


image.png

四、动态地添加方法

创建一个类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() 从不返回数据,而是你的方法在运行时实现被调用后才会返回数据。下面详细叙述消息的发送步骤:

  1. 首先检测这个 selector 是不是要忽略。比如Mac OSX开发,有了垃圾回收就不理会 retain,release 这些函数;
  2. 检测这个 selector 的target 是不是 nil,Objc 允许我们对一个 nil 对象执行任何方法不会Crash,因为运行时会被忽略掉;
  3. 如果上面两步都通过了,那么就开始查找这个类的实现 IMP,先从 cache 里查找,如果找到了就运行对应的函数去执行相应的代码;
  4. 如果类的列表中找不到,就到父类的方法列表中查找,一直找到 NSObject 类为止;
  5. 如果还找不到,就要开始进入动态方法解析了。

开始了动态解析后,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@:@" 表示返回值和参数。运行结果为:


image.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,529评论 5 475
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,015评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,409评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,385评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,387评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,466评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,880评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,528评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,727评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,528评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,602评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,302评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,873评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,890评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,132评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,777评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,310评论 2 342

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,678评论 0 9
  • 对于从事 iOS 开发人员来说,所有的人都会答出【runtime 是运行时】什么情况下用runtime?大部分人能...
    梦夜繁星阅读 3,697评论 7 64
  • 前言 runtime其实在我们日常开发过程中很少使用到,尤其是像我现在比较初级的程序猿就更用不到了。但是去面试很多...
    WolfTin阅读 608评论 0 2
  • 重生之爱 (文/亦浓) 过了午夜 是另一天了 可是, 竟如此不舍 纪念这个日子 十月十五号,八月二十六日 虔诚叩拜...
    开在夜里的花儿阅读 237评论 8 10
  • JavaScript prototype每个函数都有一个prototype属性,这个属性是指向一个对象的引用,这个...
    MakingChoice阅读 448评论 0 1