关于runtime,网上的资料都很全了,这里是根据自己的理解写一个学习总结报告。主要借鉴文章如下:
https://www.jianshu.com/p/db6dc23834e3 神经病院Objective-C Runtime出院第三天——如何正确使用Runtime
https://www.cnblogs.com/ioshe/p/5489086.html [iOS开发-Runtime详解]
1.runtime是用来做什么的?
1.1 与runtime相关最大的,就是OC语言的动态绑定机制。
动态绑定是指一个对象发送消息后,该消息的实现(实际执行的函数)根据运行环境的不同而不同(此处只针对OC,Swift中已经不是运行时加载方法,而是和C语言类似,在编译阶段就确定了)。实现该机制,常用的就是分类(categor)、类扩展(extension)、子类(subclass)继承等我们每个人都会使用的设计模式。
正常情况下,我们使用OC的这些特性就能够解决大部分问题。但是有些情况下,为了优雅、高效的解决问题,我们有时候希望从更底层的层面进行操纵。
1.2. 一个经典案例
有一个业务需求,我们希望统计某个页面viewController被点击的次数,或者在进入某些页面的时候添加引导图。常规的做法是在这些对应的页面的viewDidLoad中进行对应的需求作业。但是,如果项目比较大,页面非常多,或者层级很复杂,这样操作就效率很低,需要到不同的界面去进行分散的操作,日后新增、修改、维护或者调整也很麻烦。一个比较高效、优雅的做法是在基类UIViewContoller的viewDidLoad中实现该方法,因为所有的页面都会继承基类的viewDidLoad方法,在该基类中实现之后我们只需要在此处维护和新增就够了。
所以我们需要给UIViewContoller基类添加一个category分类,在分类中重写viewDidLoad方法。但是如果直接在分类中重写,会导致基类代码中的viewDidLoad不执行。此时,我们就需要使用runtime相关的方法来解决该问题(具体方式见第4节,第2条方法交换,如果需要深入了解,可以看看动态埋点统计的实例)。
1.3 常用runtime实现的强大功能
OC本质上是C的扩展和封装。我们的OC代码运行时,底层调用的实际上是c语言的代码。runtime(翻译过来即运行时)就是苹果暴露给用户的一个偏底层的可以操作底层代码的API接口,是对常用的设计模式的一个必要补充。通过该接口的一些函数,我们可以直接干预消息发送过程,从而实现很多强大的功能。比如
- (1) 实现多继承Multiple Inheritance (利用消息转发机制)
- (2) 在分类中重写原类方法而又不失去原类方法中的功能 (利用class的Method Swizzling)
- (3) Aspect Oriented Programming (切片编程)
- (4) 重写class方法(Isa Swizzling)
- (5) 给分类添加属性变量( 利用Associated Object给分类添加关联对象)
- (6) 动态的增加方法 (利用消息转发机制,在运行时实现方法)
- (7) NSCoding的自动归档和自动解档(利用类底层的结构查询函数,批量给所有属性自动添加相同的解档、归档方法)
2. runtime API中主要内容
2.1 对象、类的定义
从下表可以看到,本质上类是一个指向类结构体的指针,而对象是一个指向对象结构体的指针,对象结构体中存储有一个isa类,它动态的指向该对象的类。类结构体中存储有类的名字,父类名字,类的成员变量(无论是通过@property还是直接定义的成员变量都存储在这里),类的实例变量大小(我们定义实例变量的时候变量空间大小就已经确定了),类的方法链表(普通类里面存储着该类的实例方法,元类中存储中该类的类方法),协议链表,方法缓存表(我们发送消息时第一个查询的结构体)等。
//对象结构体中存储有一个isa类
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;//Isa标识对象的类;它指向一个结构的类定义编译。
};
//所有的对象本质上都是一个id,而id是一个指向对象结构体的指针
typedef struct objc_object *id;
//类是一个指向类结构体的指针
typedef struct objc_class *Class;`
`//类结构体中存储有该类定义的所有相关数据;`
`struct objc_class {`
`Class isa OBJC_ISA_AVAILABILITY;//Isa标识对象的类;它指向一个结构的类定义编译。`
`#if !__OBJC2__`
`Class super_class;``//父类`
`const char *name;``//类名`
`long version;``//类的版本信息,默认为0`
`long info;``//类信息,供运行期使用的一些位标识`
`long instance_size;``//类的实例变量大小`
`struct objc_ivar_list *ivars;``// 类的成员变量链表`
`struct objc_method_list **methodLists;``// 方法链表`
`struct objc_cache *cache;``//方法缓存`
`struct objc_protocol_list *protocols;``//协议链表#`
`endif} `
`OBJC2_UNAVAILABLE;`
因为类也有一个isa 指针,所以类本质上也是一个对象,称为类对象。类对象Isa指针标识的类为该类的元类(meta class),每一个类都是这个元类的唯一实例对象。元类对象Isa指针标识的类为根元类,根元类(root meta Class)在整个系统中只有一个,所有的元类的isa指针都指向根元类,根元类的Isa指针标识的类为自己。具体如下所示,图中虚线代表类的isa指针指向,实线代表类的父类。根元类的父类是根类,同时根元类的实例对象也是根类(root class),这里形成了一个闭环。
isa、superclass指针.png
isa指针指向:实例对象->类->元类->(不经过父元类)直接到根元类(NSObject的元类),根元类的isa指向自己;
2.2 Method、IMP、SEL的定义
把他们拿出来说,是因为容易他们之间存在相关性和差异,非常容易产生误解,而且他们对我们理解消息机制很有帮助,我们可以看一下方法Method的定义如下:
typedef struct objc_method *Method;
struct objc_method {
SEL _Nonnull method_name OBJC2_UNAVAILABLE;
char * _Nullable method_types OBJC2_UNAVAILABLE;
IMP _Nonnull method_imp OBJC2_UNAVAILABLE;
}
Method 是一个指向结构体的指针,它包含了IMP和SEL,还包含了方法类型定义、方法的参数等。
SEL是方法的指针,但不同于C语言中的函数指针,函数指针直接保存了方法的地址,但SEL只是方法编号;
IMP是方法的具体实现函数指针,在runtime里,我们可以使用函数改变或者设置IMP来更改一个函数的具体实现,例如:
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2) //交换两个方法的实现
method_setImplementation(Method _Nonnull m, IMP _Nonnull imp) // 给一个方法设置实现
2.3 runtime中常用的的一些函数
runtime中的函数,一般按照结构体的层级结构来操纵。对类中成员进行操作的,以class开头,对方法中成员进行操作的以method开头,其他的以此类推。常见的函数如下:
class_getProperty(Class _Nullable cls, const char * _Nonnull name) //获取类的所有属性列表
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp,
const char * _Nullable types) //给类添加方法
class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp,
const char * _Nullable types) //替换类方法
class_addIvar(Class _Nullable cls, const char * _Nonnull name, size_t size,
uint8_t alignment, const char * _Nullable types) //增加类变量
method_getImplementation(Method _Nonnull m) //获取方法的实现
method_setImplementation(Method _Nonnull m, IMP _Nonnull imp) //设置方法的实现
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2) //交换方法的实现
imp_implementationWithBlock(id _Nonnull block) //使用一个block创建一个实现
sel_getName(SEL _Nonnull sel) //获取方法的名称
sel_registerName(const char * _Nonnull str) //注册一个方法
3.消息传递、转发机制
想要合理的利用runtime中相关API接口,必须理解runtime中的消息传递、转发机制。
(1)当一个对象发送消息时,首先,底层会执行一个消息发送函数,函数长这样
objc_msgSend(void /* id self, SEL op, ... */
如果是使用super发送消息,函数长这样:
objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */
(2)底层会从该对象所属的类中(isa指针所指的类)的方法缓存列表查找对应的实现
(3)如果2找不到,会从该类的方法链表中继续查找
(4)如果3找不到,会跳转到该类的父类查找,父类步骤和子类一样,具体如下图所示。
isa方法传递.gif
(5)一直向上到根类,如果根类仍然找不到,就开始准备进行消息转发。转发第一步:动态消息解析。查看当前类是否实现了resolveInstanceMethod方法(如果是类方法,会看是否实现了resolveClassMethod方法)。如果该方法返回了YES,消息转发终止。我们可以在这个方法中动态添加方法实现,不实现也不要紧,只要返回YES消息发送就不会报错。
+(BOOL)resolveClassMethod:(SEL)sel
{
NSString * selStr = NSStringFromSelector(sel);
if ([selStr isEqualToString:@"runTest"]) {
//注意,想要给类添加方法,必须添加到它的metaClass上,所以在class_addMethod中添加的类都要是原类!!!
// 确定metaClass的方法是objc_getMetaClass(object_getClassName(self));
if (class_addMethod(objc_getMetaClass(object_getClassName(self)), sel,class_getMethodImplementation(objc_getMetaClass(object_getClassName(self)), @selector(runTestFunction)), "s@:")) {
return YES;
}
return [super resolveClassMethod:sel];
}
return [super resolveClassMethod:sel];
}
(6)如果第5步返回NO,就开始消息重定向。查看是否指定了其他对象来执行该方法。具体是查看当前类是否实现了forwardingTargetForSelector方法;如果该方法返回了一个对象,就在该对象上执行该selctor方法(该对象上执行该方法时步骤与本对象一致);
-(id)forwardingTargetForSelector:(SEL)aSelector
(7)如果第6步返回nil,就需要进行真正的消息转发机制。具体是查看当前类是否实现了methodSignatureForSelector方法,如果该方法返回不为nil,就执行forwardInvocation方法。如果forwardInvocation实现了,消息转发终止(但不见得消息转发完成,forwardInvocation只是一个消息的分发中心,将这些不能识别的消息转发给不同的接收对象,或者转发给同一个对象,再或者将消息翻译成另外的消息,亦或者简单的“吃掉”某些消息,因此没有响应也不会报错。)。
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
if ([someOtherObject respondsToSelector:
[anInvocation selector]])
[anInvocation invokeWithTarget:someOtherObject];
else
[super forwardInvocation:anInvocation];
}
(8)上述步骤,如果到第7部都没有实现,系统就会报错,提示unrecognized selector sent to instance
注意:上述任何一步,都要在前一步骤没有完成的基础上 。
4.如何使用runtime
基于runtime提供的数据结构,以及上述消息传递、转发机制,runtime提供了丰富的函数让我们来实现我们第1节中提到的强大的功能,我们这里简单梳理下实现方式:
-
(1) 实现多继承Multiple Inheritance (利用消息转发机制),如下图所示。
1330553-c7ef6392ecc9ee9d.gif
我们在Warrior中头文件中定义一个方法negotiate,但是不实现它,而在forwardingTargetForSelector方法中,针对该selecotr,指定一个Diplomat对象,就可以将该方法实现交给diplomat类来实现。看起来就像是Warrior也继承了了Diplomat的方法一样(注意,像respondsToSelector:
和 isKindOfClass:
这类方法只会考虑继承体系,不会考虑转发链。也就是说如果[Warrior respondsToSelector:negotiate]会返回NO)。
-
(2) 在分类中重写原类方法而又不失去原类方法中的功能 (利用class的Method Swizzling)
实现方式是通过runtime中的实现交换函数method_exchangeImplementations。首先,在本类中定义另一个待交换的方法exchage_ViewDidLoad;待交换的方法中需要调用原方法,然后添加需要额外实现的功能(例如第1节中提到的数据统计方法)。在恰当的时机(一般是在load方法中),交换该两个方法的实现。实际执行代码的使用,调用原类方法会执行待交换的方法的实现,待交换的方法实现中又会调用原来的方法实现,从而保留了原来的方法的实现。
+(void)exchangeOriginMethodWithMethodExchangeMethod
{
// 防止方法被多次调用后交换失效;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL originSEL = @selector(viewDidLoad);
SEL swizzSEL = @selector(exchage_ViewDidLoad);
Method viewDidLoad = class_getInstanceMethod([self class], originSEL);
Method exchang_viewDidLoad = class_getInstanceMethod([self class], swizzSEL);
// 测试原来的选择子是否已经添加了方法(是否已经交换了方法);
Boolean didAddMethod = class_addMethod([self class], originSEL,method_getImplementation(exchang_viewDidLoad),method_getTypeEncoding(exchang_viewDidLoad));
if (!didAddMethod) {
//
// 如果没有添加方法,就直接交换
method_exchangeImplementations(viewDidLoad, exchang_viewDidLoad);
}else{
// 如果已经添加了,就同时更换交换后的方法实现;
class_replaceMethod([self class], swizzSEL, method_getImplementation(viewDidLoad), method_getTypeEncoding(viewDidLoad));
}
});
}
-(void)exchage_ViewDidLoad
{
NSLog(@"%@ did load",self);
[self exchage_ViewDidLoad];//注意,exchage_ViewDidLoad的实现现在是viewDidLoad了,所以没有循环调用
}
-
(3) Aspect Oriented Programming (切片编程,内容太多,暂不展开,可以看这里)
-
(4) 重写class方法(Isa Swizzling)
苹果著名的KVO技术和NSNotificationCenter就使用的该方法,在我们给一个对象添加了KVO键值观察方法后
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)contex。
后台会重新创建一个NSKVONotifying_Object类,然后偷偷将原来的类的isa指针指向该类。该类中会在属性变量修改时候,调用
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
方法,并发出相应的通知
-
(5) 给分类添加属性变量( 利用Associated Object给分类添加关联对象)
我们可以给分类添加属性,但是分类不会自动给我们生成成员变量。因为类的成员变量在编译器已经决定了(写入了类的结构体中,具体见前面结构体的定义),但是category是在运行期才决议的。所以如果要给分类添加成员变量,需要用runtime里面函数在运行期实现。一般使用objc_setAssociatedObject和objc_getAssociatedObject函数来实现。这两个函数都是成对的出现,一个给对象添加关联对象,一个获取关联对象。具体代码如下。
@property(nonatomic,strong)id associatedObjcet;
-(void)setAssociatedObjcet:(id)associatedObjcet{
objc_setAssociatedObject(self, @selector(associatedObjcet), associatedObjcet, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
-(id)associatedObjcet{
return objc_getAssociatedObject(self, @selector(associatedObjcet));
}
-
(6) 动态的增加方法 (利用消息转发机制,在运行时实现方法)
动态的增加方法和多重继承有些类似,都是调用的方法在类中并没有实现代码,而是在消息转发机制的某一步才动态的添加实现代码。消息转发机制本身有多步骤,所以根据需要,可以在不同的步骤实现动态添加,常见的一般在方法动态解析resolveInstanceMethod或者在消息转发forwardInvocation的时候进行。
-
(7) NSCoding的自动归档和自动解档(利用类底层的结构查询函数,批量给所有属性自动添加相同的解档、归档方法)
NSCoding其实就是对所有的属性调用encode和decode方法。使用手动操作有一个缺陷,如果属性多起来,要写好多行相似的代码,虽然功能是可以完美实现,但是看上去不是很优雅。用runtime实现的思路就比较简单,我们循环依次找到每个成员变量的名称,然后利用KVC读取和赋值就可以完成encodeWithCoder和initWithCoder了,部分代码如下:
Ivar *vars = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i ++) {
Ivar var = vars[i];
const char *name = ivar_getName(var);
NSString *key = [NSString stringWithUTF8String:name];
id value = [aDecoder decodeObjectForKey:key];
[self setValue:value forKey:key];
}
网友评论