013-Runtime
一、概述
1.多态
1)含义
计算机程序运行时,相同的消息可能会送给多个不同的类别之对象,而系统可依据对象所属类别,引发对应类别的方法,而触发不同的行为,这个就是多态;简单来说,所谓多态指的是相同的消息给予不同的对象会引发不同的动作
2)分类
静态多态:允许将不同的特殊行为和单个泛化记号相关联,由于这种关联处理于编译期而非运行期,因此被称为“静态”,比如函数重载
动态多态:即运行时多态,对于C++来说,通过类继承机制和虚函数机制生效于运行期,可以优雅地处理异质对象集合,只要其共同的基类定义了虚函数的接口;对于OC来说,通过借鉴于smallTalk的消息传递机制实现动态多态。一般来说,动态多态才是真正的多态
2.OC的多态实现
1 #import <Foundation/Foundation.h> 2 3 @interface Base: NSObject 4 @end 5 6 @implementation Base 7 8 - (void)f { 9 NSLog(@"Base f"); 10 } 11 12 @end 13 14 @interface Derived: Base 15 @end 16 17 @implementation Derived 18 19 - (void)f { 20 NSLog(@"Derived f"); 21 } 22 23 @end 24 25 int main(int argc, char *argv[]) { 26 Derived *d = [[Derived alloc] init]; 27 Base *pb = d; 28 Derived *pd = d; 29 [pb f]; // 输出:Derived f 30 [pd f]; // 输出:Derived f 31 return 0; 32 }
分析:上述代码创建了Derived对象d,将Base类型指针pb和Derived类型指针pd指向d,可知pb和pd的编译时刻的类型分别是Base和Derived,而运行时刻类型均为Derived。从两个输出来看,可以发现,OC并没有使用虚函数,也没有虚函数的概念,但是OC自动完成了动态多态的实现
3.运行时机制
C语言是一种静态语言,而OC语言是一个依托于C的语言。为了保持C编译时的功能(如:类型检查),并且增加灵活性,OC在C的基础上,借鉴了smalltalk的消息传递机制为其添加了运行时机制,这也就是runtime机制
换种方式来说,runtime机制是一个由C和汇编编写的Library,而这个Library给C增加了面向对象的特性,从而形成了OC语言
基于runtime,OC中对象的类型和对象所执行的方法都是在运行时阶段进行查找并确认的,这种机制被称为动态绑定
OC中与运行时机制进行交互的方面大致以下三种:
1)Objective-C源代码
大部分情况下你就只管写你的Objective-C代码就行,runtime系统自动地在幕后辛勤地劳作着
1 [pb f];
经过编译转换为:
1 ((void (*)(id, SEL))(void *)objc_msgSend)((id)pb, sel_registerName("f"));
2)NSObject的方法
Cocoa中大多数的类都是继承自NSObject类,自然也就继承了它的方法。
有的NSObject中的方法起到了抽象接口的作用:如description方法需要你重载它并为你定义的类提供描述内容
有的NSObject中的方法能在运行时获得类的信息,并检查一些特性:如class返回对象的类,isKindOfClass:和isMemberOfClass:则检查对象是否在指定的类继承体系中,respondsToSelector:检查对象能否响应指定的消息,conformsToProtocol:检查对象是否实现了指定协议类的方法,methodForSelector:则返回指定方法实现的地址
3)Runtime的函数
4.静态语言和动态语言
C语言是静态语言,在经过编译之后,就知道代码要干嘛;而OC语言是一门面向对象的动态语言,它在依托C语言的情况下,还自行添加了动态特性,即在编译之后只是知道给对象发送消息,只有到运行时才了解要干嘛。
5.Runtime的应用
1)方法实现的替换:替换两个方法的实现,可以做一些埋点、容错等工作
2)关联属性:在运行时刻为某个class增加属性
3)Json映射:通过runtime的方法将Json映射到model
二、Runtime代码实例
1.OC的消息机制
Person.h文件
1 #import <Foundation/Foundation.h> 2 3 @interface Person : NSObject 4 5 - (void)eatWithFood:(NSString *)food; 6 7 @end
Person.m文件
1 #import "Person.h" 2 #import <objc/runtime.h> 3 4 @implementation Person 5 6 - (void)eatWithFood:(NSString *)food 7 { 8 NSLog(@"吃%@", food); 9 } 10 11 @end
MsgSendController.m文件
1 #import "MsgSendController.h" 2 #import <objc/message.h> 3 #import "Person.h" 4 5 @interface MsgSendController () 6 7 @end 8 9 @implementation MsgSendController 10 11 - (void)viewDidLoad 12 { 13 [super viewDidLoad]; 14 15 [self initUI]; // 界面 16 } 17 18 #pragma mark - 界面 19 - (void)initUI 20 { 21 self.view.backgroundColor = [UIColor whiteColor]; 22 self.title = @"消息发送机制"; 23 24 [self sendMsgC]; 25 } 26 27 #pragma mark - 给对象发送消息 28 - (void)sendMsg 29 { 30 Person *p = [[Person alloc] init]; 31 32 // 方法调用1 33 [p eatWithFood:@"汉堡包"]; 34 35 // 方法调用2 36 [p performSelector:@selector(eatWithFood:) withObject:@"汉堡包"]; 37 38 // 方法调用3 39 /* 消息机制:给对象发送消息 */ 40 ((void(*)(id,SEL,NSString *))objc_msgSend)((id)p, @selector(eatWithFood:), @"汉堡包"); 41 } 42 43 #pragma mark - 纯C代码 44 - (void)sendMsgC 45 { 46 // 1.拆分类Person的对象初始化方法 47 // Person *p = [Person alloc]; 48 // 换成C的消息机制 49 // 类也是一个特殊的对象:类对象 50 // Person *p = objc_msgSend([Person class], @selector(alloc)); 51 52 // p = [p init]; 53 // 换成C的消息机制 54 // p = objc_msgSend(p, @selector(init)); 55 56 // 2.合并类Person的对象初始化方法 57 Person *p = objc_msgSend(objc_msgSend([Person class], @selector(alloc)), @selector(init)); 58 59 // 3.调用类Person的对象方法eatWithFood: 60 ((void(*)(id,SEL,NSString *))objc_msgSend)((id)p, @selector(eatWithFood:), @"汉堡包"); 61 } 62 63 @end
2.交换方法实现
NSURL+URL.h文件
1 #import <Foundation/Foundation.h> 2 3 @interface NSURL (URL) 4 5 #pragma mark - 由于Objective-C中NSURL没有错误处理,所以我们只能重新写一个方法 6 + (instancetype)LD_URLWithStr:(NSString *)str; 7 8 @end
NSURL+URL.m文件
1 #import "NSURL+URL.h" 2 #import <objc/message.h> 3 4 @implementation NSURL (URL) 5 6 #pragma mark - 文件加载时调用,在main函数调用之前就会调用 7 + (void)load 8 { 9 NSLog(@"NSURL+URL这个分类文件被加载"); 10 11 // 方法 12 // method_exchangeImplementations 交换两个方法的实现 13 // class_getClassMethod 获取类方法 14 // class_getInstanceMethod 获取实例方法 15 16 // 交换方法的实现(其实就是交换的方法指针) 17 // 参数1:类对象 18 // 参数2:方法名 19 Method URLMethod = class_getClassMethod([NSURL class], @selector(URLWithString:)); 20 Method LDURLMethod = class_getClassMethod([NSURL class], @selector(LD_URLWithStr:)); 21 method_exchangeImplementations(URLMethod, LDURLMethod); 22 } 23 24 #pragma mark - 由于Objective-C中NSURL没有错误处理,所以我们只能重新写一个方法 25 + (instancetype)LD_URLWithStr:(NSString *)str 26 { 27 // 这里面调用LD_URLWithStr:这个方法,根据runtime的方法交换其实在调用系统的URLWithString: 28 NSURL *url = [NSURL LD_URLWithStr:str]; 29 if (!url) { 30 NSLog(@"url为空,错误处理"); 31 } 32 return url; 33 } 34 35 @end
RunTimeController.m文件
1 #import "RunTimeController.h" 2 #import <objc/runtime.h> 3 #import "NSURL+URL.h" // 如果是使用runtime的话,就不必引入这个头文件 4 5 @interface RunTimeController () 6 7 @end 8 9 @implementation RunTimeController 10 11 - (void)viewDidLoad 12 { 13 [super viewDidLoad]; 14 15 [self initUI]; // 界面 16 } 17 18 #pragma mark - 界面 19 - (void)initUI 20 { 21 self.view.backgroundColor = [UIColor whiteColor]; 22 self.title = @"交换方法实现"; 23 24 [self runtime]; 25 } 26 27 #pragma mark - 分类来控制NSURL的错误处理 28 // 这样处理起来比较麻烦,只要项目中有用到NSURL,就必须引入分类头文件,且换成我们分类中的方法 29 // 所以说还是必须使用runtime 30 - (void)catgory 31 { 32 NSURL *url = [NSURL LD_URLWithStr:@"http://哈哈"]; 33 NSURLRequest *request = [NSURLRequest requestWithURL:url]; 34 NSLog(@"网络请求1%@", request); 35 } 36 37 #pragma mark - RunTime 38 // 通过在分类NSURL+URL中去通过runtime交换方法实现来处理 39 // 具体的代码请看分类NSURL+URL 40 - (void)runtime 41 { 42 // runtime:运行时 43 // 相关头文件:<objc/runtime.h> 44 // runtime是OC的底层实现,帮我们完成OC无法完成的事情 45 // 1.利用runtime在程序运行的过程中,动态地创建一个类 46 // 2.利用runtime在程序运行的过程中,动态地修改一个类(属性/方法) 47 // 3.利用runtime在程序执行的过程中,动态地交换方法实现 48 49 // 这里我们就可以大大方方的调用系统的 50 // 由于runtime的方法交换,系统的URLWithString:方法会找到分类中LD_URLWithStr:方法的实现 51 NSURL *URL = [NSURL URLWithString:@"http://哈哈"]; 52 NSURLRequest *request = [NSURLRequest requestWithURL:URL]; 53 NSLog(@"网络请求2%@", request); 54 } 55 56 @end
3.方法的懒加载
Boy.h文件
1 #import <Foundation/Foundation.h> 2 3 @interface Boy : NSObject 4 5 #pragma mark - 实例方法 6 - (void)eat; 7 - (void)drink; 8 9 #pragma mark - 类方法 10 + (void)play:(NSString *)what; 11 12 @end
Boy.m文件
1 #import "Boy.h" 2 #import <objc/runtime.h> 3 4 @implementation Boy 5 6 #pragma mark - 当这个类被调用一个没有实现的对象方法时就会来到这里 7 + (BOOL)resolveInstanceMethod:(SEL)sel 8 { 9 // 方法 10 // class_addMethod // 添加方法 11 // class_addIvar // 添加属性 12 13 // 在这里添加该类没有实现的实例方法的实现 14 // 参数1:类对象 15 // 参数2:方法名 16 // 参数3:方法实现,就是一个函数指针 17 // 参数4:返回值类型(C字符串) 18 class_addMethod([Boy class], @selector(eat), (IMP)eatfood, nil); 19 // 参数4举例:v@:中的v代表返回值是void类型,@:代表两个参数 20 class_addMethod([Boy class], @selector(drink), (IMP)drinkwater, "v@:"); 21 22 return [super resolveInstanceMethod:sel]; 23 } 24 25 #pragma mark - 当这个类被调用一个没有实现的类方法时就会来到这里 26 + (BOOL)resolveClassMethod:(SEL)sel 27 { 28 // 获取当前类的class元类 29 Class aMeta = objc_getMetaClass(class_getName([self class])); 30 31 // 在这里添加该类没有实现的类方法的实现 32 // 参数1:类对象 33 // 参数2:方法名 34 // 参数3:方法实现,就是一个函数指针 35 // 参数4:返回值类型(C字符串) 36 // 参数4举例:v@:*中的v代表返回值是void类型,@:*代表三个参数 37 class_addMethod([aMeta class], @selector(play:), (IMP)play, "v@:*"); 38 39 return [super resolveClassMethod:sel]; 40 } 41 42 #pragma mark - 函数实现 43 void eatfood() { 44 printf("吃饭饭\n"); 45 } 46 47 #pragma mark - 函数实现 48 // id obj, SEL _cmd这是两个隐形参数,所有函数都默认有的 49 void drinkwater(id obj, SEL _cmd) { 50 NSLog(@"调用了%@的%@方法", obj, NSStringFromSelector(_cmd)); 51 printf("喝水\n"); 52 } 53 54 #pragma mark - 函数实现 55 void play(id obj, SEL _cmd, char * what) { 56 NSLog(@"调用了%@的%@方法", obj, NSStringFromSelector(_cmd)); 57 printf("玩%s\n", what); 58 } 59 60 @end
RunTimeMethodController.m文件
1 #import "RunTimeMethodController.h" 2 #import "Boy.h" 3 4 @interface RunTimeMethodController () 5 6 @end 7 8 @implementation RunTimeMethodController 9 10 - (void)viewDidLoad 11 { 12 [super viewDidLoad]; 13 14 [self initUI]; // 界面 15 } 16 17 #pragma mark - 界面 18 - (void)initUI 19 { 20 self.view.backgroundColor = [UIColor whiteColor]; 21 self.title = @"方法懒加载"; 22 23 [self test]; 24 } 25 26 #pragma mark - 调用Boy类中没有实现的方法,一般情况下,会出现崩溃,但是这里使用了runtime懒加载方法,所以不会 27 // 具体实现部分请看Boy类中的代码 28 - (void)test 29 { 30 Boy *b = [[Boy alloc] init]; 31 [b eat]; 32 [b drink]; 33 [Boy play:@"足球"]; 34 } 35 36 @end
4.KVO的底层实现
1)系统的KVO的常规用法
Dog.h文件
1 #import <Foundation/Foundation.h> 2 3 @interface Dog : NSObject 4 5 @property (nonatomic, assign) int age; // 年龄 6 7 @end
Dog.m文件
1 #import "Dog.h" 2 3 @implementation Dog 4 5 @end
Man.h文件
1 #import <Foundation/Foundation.h> 2 3 @interface Man : NSObject 4 5 @end
Man.m文件
1 #import "Man.h" 2 3 @implementation Man 4 5 #pragma mark - KVO监听事件 6 // 在RunTimeRespondController中注册了让Man对象监听Dog对象的age属性的变化 7 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context 8 { 9 NSLog(@"监听到%@的%@属性变化为:%@", object, keyPath, change); 10 } 11 12 @end
RunTimeRespondController.m文件
1 #import "RunTimeRespondController.h" 2 #import "Man.h" 3 #import "Dog.h" 4 5 @interface RunTimeRespondController () 6 7 @property (nonatomic, strong) Man *m; 8 @property (nonatomic, strong) Dog *d; 9 10 @end 11 12 @implementation RunTimeRespondController 13 14 - (void)viewDidLoad 15 { 16 [super viewDidLoad]; 17 18 [self initUI]; // 界面 19 } 20 21 #pragma mark - 界面 22 - (void)initUI 23 { 24 self.view.backgroundColor = [UIColor whiteColor]; 25 self.title = @"KVO响应式编程"; 26 27 [self test]; 28 } 29 30 #pragma mark - 注册KVO监听者 31 - (void)test 32 { 33 // KVO底层实现原理 34 // KVO的监听,其实就是重写被监听者的被监听属性的set方法 35 // 底层找方法其实也就是通过isa指针 36 // KVO在注册监听的时候,其实就是在注册完监听的时候,底层会自动创建一个继承自被监听对象的子类,并在这个子类中去重写这个被监听属性的set方法,并且这个被监听对象的isa指针指向的是它的子类,后面通过属性方法调用改变该被监听对象的值的时候,就回去找该属性的set方法,但由于这个被监听对象的isa指针指向它的子类,所以调用的set方法自然也就是子类中重写的set方法 37 38 _m = [[Man alloc] init]; 39 _d = [[Dog alloc] init]; 40 41 // 注册监听 42 // 让m对象监听d对象的age属性的变化 43 [self.d addObserver:self.m forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil]; 44 } 45 46 #pragma mark - 点击屏幕 47 - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event 48 { 49 _d.age ++; 50 } 51 52 #pragma mark - 销毁KVO观察者 53 - (void)dealloc 54 { 55 [self.d removeObserver:self.m forKeyPath:@"age"]; 56 } 57 58 @end
2)自定义KVO的实现
Girl.h文件
1 #import <Foundation/Foundation.h> 2 3 @interface Girl : NSObject 4 5 @property (nonatomic, copy) NSString *name; 6 @property (nonatomic, assign) int age; 7 8 @end
Girl.m文件
1 #import "Girl.h" 2 3 @implementation Girl 4 5 @end
NSObject+LDKVO.h文件
1 #import <Foundation/Foundation.h> 2 3 @interface NSObject (LDKVO) 4 5 #pragma mark - 扩展一个方法 6 // 仿照KVO的注册监听方法 7 - (void)LD_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context; 8 9 @end
NSObject+LDKVO.m文件
1 #import "NSObject+LDKVO.h" 2 #import <objc/message.h> 3 4 @implementation NSObject (LDKVO) 5 6 #pragma mark - 扩展一个方法 7 // 仿照KVO的注册监听方法 8 - (void)LD_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context 9 { 10 // 方法 11 // objc_allocateClassPair // 动态添加一个类 12 // objc_registerClassPair // 动态注册一个类 13 // class_addMethod // 动态地给某一个类添加方法 14 // class_addIvar // 动态地给某一个类添加属性 15 // object_setClass // 修改某一个类的isa指针 16 // objc_setAssociatedObject // 关联属性 17 // objc_getAssociatedObject // 取得关联属性 18 // objc_registerClassPair // 注册类 19 20 // 1.自定义一个类,继承[self class] 21 // self:谁调用这个方法,谁就是self 22 23 // 2.重写被监听属性keyPath的set方法 24 25 // 3.通知观察者observer 26 // 其实就是调用observer的observeValueForKeyPath方法 27 28 // 动态添加一个继承[self class]的类 29 NSString *oldClassName = NSStringFromClass([self class]); // 拿到父类名称字符串 30 NSString *newClassName = [NSString stringWithFormat:@"LDKVO_%@", oldClassName]; // 拼接继承父类的子类名称字符串 31 const char * newName = [newClassName UTF8String]; // OC字符串转化为C的字符串 32 Class myClass = objc_allocateClassPair([self class], newName, 0); 33 34 // 动态地给子类添加被监听属性的set方法 -- 相当于给子类重写被监听属性的set方法 35 class_addMethod(myClass, @selector(setAge:), (IMP)setAge, "v@:i"); 36 37 // 注册这个子类 38 objc_registerClassPair(myClass); 39 40 // 修改被观察对象的isa指针 41 // 其实就是self的isa指针 42 object_setClass(self, myClass); 43 44 // 将观察者属性observer保存到当前类myClass里面去 45 // 这里的self指的是myClass,因为上一步已经改变了self的isa指针 46 objc_setAssociatedObject(self, (__bridge const void *)@"objc", observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 47 } 48 49 #pragma mark - 相当于重写被监听属性的set方法 50 void setAge(id self, SEL _cmd, int age) { 51 // 保存当前类 52 Class myClass = [self class]; 53 // 调用父类的监听属性的set方法,去设置新值 54 // 这里将self的isa指针改成指向父类的 55 object_setClass(self, class_getSuperclass([self class])); 56 // 调用父类 57 ((void(*)(id,SEL,int))objc_msgSend)((id)self, @selector(setAge:), age); 58 // 拿出观察者属性 59 id objc = objc_getAssociatedObject(self, (__bridge const void *)@"objc"); 60 // 通知观察者 61 ((void(*)(id,SEL,id,NSString *,id,id))objc_msgSend)(objc, @selector(observeValueForKeyPath:ofObject:change:context:), self, @"age", nil, nil); 62 // 改回子类 63 object_setClass(self, myClass); 64 } 65 66 @end
CustomKVOController.h文件
1 #import "CustomKVOController.h" 2 #import "Girl.h" 3 #import "NSObject+LDKVO.h" 4 5 @interface CustomKVOController () 6 7 @property (nonatomic, strong) Girl *g; 8 9 @end 10 11 @implementation CustomKVOController 12 13 - (void)viewDidLoad 14 { 15 [super viewDidLoad]; 16 17 [self initUI]; // 界面 18 } 19 20 #pragma mark - 界面 21 - (void)initUI 22 { 23 self.view.backgroundColor = [UIColor whiteColor]; 24 self.title = @"KVO底层-自定义KVO"; 25 26 [self test]; 27 } 28 29 #pragma mark - 自定义KVO 30 - (void)test 31 { 32 _g = [[Girl alloc] init]; 33 _g.name = @"Frank"; 34 _g.age = 18; 35 36 // 注册监听 37 // 用我们分类中的扩展方法 38 [self.g LD_addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil]; 39 } 40 41 #pragma mark - KVO监听事件 42 // 注册了让self监听Girl对象的age属性的变化 43 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context 44 { 45 NSLog(@"监听到%@的%@属性变化为:%@", object, keyPath, change); 46 } 47 48 #pragma mark - 点击屏幕 49 - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event 50 { 51 _g.age ++; 52 } 53 54 @end