【Objective-c】 runtime_又是一黑魔法

如果不装X,跟咸鱼又有什么区别了。
听了一节关于runtime相关的课程,这里第一时间做个笔记,方便自己过后的复习。


一、 OC 消息运行时机制

场景1:创建一个继承于NSObject的Person类,添加一个对象方法-(void)eat,当每次调用这个对象方法时打印一行文字。如下图:

// .h文件
#import <Foundation/Foundation.h>
@interface Person : NSObject
- (void)eat;
@end
// .m文件
#import "Person.h"
@implementation Person
- (void)eat
{
    NSLog(@"我要食大柴饭");
}
@end

正常是导入Person的头文件,创建对象,直接调用对象方法.

#import "ViewController.h"
#import "Person.h"

@interface ViewController ()
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    Person *p = [[Person alloc]init];
    [p eat];
}

@end

上面的方法,会OC的人都不是什么问题,接下来慢慢的揭开消息机制的神秘面纱。

    调用对象方法
    /*
        方式一:
     */
    [p eat];
    
    /*
        方式二:
     */
    [p performSelector:@selector(eat)];
    
    /*
        方式三:
        消息发送(注意要先导入 <objc/message.h> 头文件)
        从Xcode 5.0 开始苹果就不建议使用低层方式
     */
    objc_msgSend(p, @selector(eat));

补充使用message.h头文件需要以下操作:

Paste_Image.png

前面的只是前菜,接下来是要剥析Person对象的实例另一种方法

//未剥析前
Person *p = [[Person alloc]init];

//1.0
Person *p = [Person alloc];
p = [p init];

//2.0 特别注明一点
//objc_msgSend(<#id self#>, <#SEL op, ...#>)
//第一个参数是id类型,如果是对象就直接传对象就可以,
//如果是类,则传类(好吧!忽略我说的)
Person *p = objc_msgSend([Person class], @selector(alloc));
p = objc_msgSend(p, @selector(init));

//3.0 这里还有一点不足就是还是引用了Person类
// 不急我们接下来会一步步的解决
//首先获取"类"的方法有如下三种
//第一种:[Person class]
//第二种:NSClassFromString(@"Person") 
//第三种:objc_getRequiredClass("Person")
//3.1
Person *p = objc_msgSend(objc_getRequiredClass("Person"), @selector(alloc));
//3.2 根据面向对象多态的特性:Person类继承于NSObject类
NSObject *p = objc_msgSend(objc_getRequiredClass("Person"), @selector(alloc));

//4.0 上面已经转变完成了,都是通过调用函数,没有用到OC语法
//  再将上面的两行代码写在一块了,这个给人的等级就是上天了
NSObject *p = objc_msgSend( objc_msgSend(objc_getRequiredClass("Person"), @selector(alloc)), @selector(init));

换个写法,是不是有点feel了,接下来验证我们的结论
创建一个新工程(只有main函数的工程)

Paste_Image.png
Paste_Image.png
Paste_Image.png
Paste_Image.png
Paste_Image.png

Paste_Image.png
二、 runtime
2.1 runtime简历

runtime 是苹果提供的一套低层的API

runtime的作用有下面三个:
a、动态的创建一个类
b、动态的修改一个类的属性/方法
c、遍历一个类的所有的成员变量

runtime使用前提
导入同文件<objc/message.h>或者<objc/runtime.h>
备注:<objc/message.h>头文件已经包括了<objc/runtime.h>

两个概念
Method : 代表成员方法
Ivar : 成员变量

2.2 runtime使用

runtime的作用:
1、获取类方法或对象方法
2、交互方法的实现

场景2: 通过URLWithString:方法创建一个NSURL对象,如"www.baidu.com",一般情况是无BUG的,但是如果此时将url改成"www.baidu.com/好好学习",则创建的NSURL为nil,就会导致以后的代码出现BUG,甚至崩溃,问题是就算是做了一个全局的断点也找不到这个BUG。
所以我们的需求是:NSURL 这个类的URLWithString:方法添加一个功能!在创建URL的同时也能判断是否为空

为一个类添加新的方法或者是修改原有的方法,就有创建类别和继承两种方式。

继承
现在这种场景,如果是使用继承创建一个新的NSURL子类的话是能修改这个隐藏的BUG,不过就要将项目中的NSURL替换成继承的子类,在一个已经持续开发几个月的项目,这种方案并不是最可行的。

类别
类别,就是一个补丁,为一个原有类的基础增加新功能。但是类别也是有缺点的
第一:不能添加属性(意思是添加的属性不会自动生成setter方法和getter方法);
第二:不建议重写类的方法(原因请看下面的例子)。
当然这两个问题还是有办法解决的,使用我们今天的主角"runtime",runtime先生有两个特殊的技能:1、动态添加成员变量(属性);2、交换方法的实现

//重写类方法1:会造成死循环
+ (instancetype)URLWithString:(NSString *)URLString
{
    NSURL *url = [NSURL URLWithString:URLString];
}

//重写类方法2:不能实现
+ (instancetype)URLWithString:(NSString *)URLString
{
  //因为NSURL的父类是NSObject,并没有URLWithString:方法
    NSURL *url = [super URLWithString:URLString];
}

现在的解决思路:创建NSURL分类(NSURL+url),并添加一个新的实现方法,使用runtime与原来的URLWithString:交互实现方法,详情看代码

//NSURL(url).h 文件
#import <Foundation/Foundation.h>
@interface NSURL (url)
+ (instancetype)MZ_URLWithString:(NSString *)URLString;
@end
//NSURL(url).m 文件
#import "NSURL+url.h"
#import <objc/runtime.h>
@implementation NSURL (url)

+ (instancetype)MZ_URLWithString:(NSString *)URLString
{
    NSURL *url = [NSURL URLWithString:URLString];
    if (url == nil) {
        NSLog(@"URL 为空");
    }
    return url;
}
@end

上面是创建的NSURL类别,新增的一个MZ_URLWithString:方法,但是离我们目标还有一步:交互方法实现

//NSURL(url).m 文件
#import "NSURL+url.h"
#import <objc/runtime.h>
@implementation NSURL (url)

+ (void)load
{
    NSLog(@"NSURL(url).m文件加载了");
    //交换方法实现
    //第一步:获取这两个方法
    //class_getClassMethod 获取类方法
    //class_getInstanceMethod 获取对象方法
    Method URLWithStr = class_getClassMethod(self, @selector(URLWithString:));
    Method MRURLWithStr = class_getClassMethod(self, @selector(MZ_URLWithString:));
    //交换方法
    method_exchangeImplementations(URLWithStr, MRURLWithStr);
}

+ (instancetype)MZ_URLWithString:(NSString *)URLString
{
    //注意:这里不能调用原来的URLWithString:方法,否则会造成死循环崩溃的
    NSURL *url = [NSURL MZ_URLWithString:URLString];
    if (url == nil) {
        NSLog(@"URL 为空");
    }
    return url;
}
@end

到这里就已经是完成了,在load方法中交互方法的实现有个好处,就是不用在是使用这个补丁的类中添加"NSURL+url.h"头文件,会直接生效。(备注:利跟弊都是相对的。既然能在不导入头文件的情况就能全局的替换原有方法的实现,容易产生混淆,并且不像继承那样可以直接跳转代码查看实现,所以推荐,不对,是必须添加注释,添加注释,添加注释)
你一定会疑惑为什么在load方法中交互方法的实现,就会全局有效?
小编也是一知半解的,如果想深入了解可以参考下面两篇文章:
TerryZhang:iOS的load方法与initialize方法
Draveness:你真的了解load方法么?

关于+(void)load方法至少要记住:
只要类的实现文件引入项目中(如下图),在程序启动时就会执行,并且是执行一次。

Paste_Image.png

runtime的作用 2
动态创建属性

场景3:

runtime的作用 3
动态创建方法

场景4:Person类声明了一个对象如-(void)eat,但该方法无实现方法。使用runtime为Person类动态创建一个对象方法的实现

viewController.m 文件

#import "ViewController.h"
#import "Person.h"

@interface ViewController ()
@property (nonatomic,strong) Person *p;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.p = [[Person alloc]init];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    //调用不带参数的方法
    [self.p performSelector:@selector(eat)];   
}
Person.m 文件

#import "Person.h"
#import <objc/runtime.h>

@implementation Person

//函数的名字就是函数的指针
//每一个函数内都默认有这两个参数,这是隐式参数,这是系统传过来的
void eat(id self,SEL _cmd){
    NSLog(@"调用了%@的%@方法",self,NSStringFromSelector(_cmd));
}


/*
    当调用没有实现的对象方法,会先调用此方法
    // 当调用没实现的类方法,会调用此方法 + (BOOL)resolveClassMethod:(SEL)sel
 */
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    //动态添加方法
    if (sel == @selector(eat)) {
        /**
            1.类类型
            2.方法编号 (即方法名)
            3.方法实现,函数指针
            4.返回值类型(c语言字符串)
         */
        class_addMethod([Person class], sel,(IMP)eat, nil);
    }
    
    return [super resolveInstanceMethod:sel];
}
@end

到这里动态创建一个方法是已经完成了,这个时候再调用Person类的eat方法就不会出现崩溃的情况了。

这里有3个知识点:

1、当调用没有实现的方法(对象方法或类方法),系统崩溃前会先调用+(BOOL)resolveInstanceMethod:或+(BOOL)resolveClassMethod:方法

2、函数的名字就是函数的指针,虽然这是C语言的基础,但也要提一下(小编自己也是忘了)。并且每个函数都有两个默认的隐式参数(id self,SEL _cmd),"self"代表当前对象,"_cmd"代表方法名。提醒:这两个参数是系统提供的值,系统提供的值,系统提供的值,重要的事说三遍。

3、runtime动态创建『方法』的方法:class_addMethod(<#__unsafe_unretained Class cls#>, <#SEL name#>, <#IMP imp#>, <#const char *types#>),这个方法有四个参数,前三个就不提了,代码块上都有标注,重点是第四个参数(c语言字符串),是关于这个方法返回值和入参类型的一个编码字符串。含义如下:

未标题-1.png

以void eat(id self,SEL _cmd)函数为例子,第四个参数字符串的值应该是"v@:"。
当然这里我们列举的是无参的案列,如果是有带参数该怎么办了?
比如吃东西这个方法一个食物的名称

//带参数
void eatOBJ(id self,SEL _cmd,id obj){
    NSLog(@"我吃了%@",obj);
}

/*
    当调用没有实现的方法,会先调用此方法
    //+ (BOOL)resolveClassMethod:(SEL)sel
 */
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    //动态添加方法
    if (sel == @selector(eat)) {
        /**
            1.类类型
            2.方法编号 (即方法名)
            3.方法实现,函数指针
            4.返回值类型(c语言字符串)
         */
        class_addMethod([Person class], sel,(IMP)eat, nil);
        
    } else if (sel == @selector(eat:)){
        class_addMethod([Person class], sel, (IMP)eatOBJ, "v@:@");
    }
    return [super resolveInstanceMethod:sel];
}

    //调用带参数的方法
    [self.p performSelector:@selector(eatOBJ:) withObject:@"红烧肉"];

这里就不解释了

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

推荐阅读更多精彩内容