iOS - Objective-C Runtime
Objective-C 的 Runtime 是一个运行时库(Runtime Library),为 C 添加了面向对象的能力并创造了 Objective-C。这就是说它在类信息中被加载,完成所有的方法分发,方法转发等等。Objective-C runtime 创建了所有需要的结构体,让 Objective-C 的面相对象编程变为可能
参考链接
一、 Introduction
动态 & 静态语言
-
Objective-C 是面向运行时的语言,就是说它会尽可能的把编译和链接时要执行的逻辑延迟到运行时
-
在 C 里,你从 main() 方法开始写然后就是从上到下的写逻辑了并按你写代码的顺序执行程序
NSObject 类如何与 Runtime 系统进行交互?Runtim 如何动态加载新类?如何转发消息到其他对象?
Legacy And Modern Version
- modern -> Objective-C 2.0
- legacy -> Objective-C 1.0
二、 Messaging
在 Objective-C 中直到运行时才绑定方法实现,编译器将消息表达式转换为 objc_msgSend
方法调用
[receiver message]
有参方法 & 无参方法
objc_msgSend(receiver, selector)
objc_msgSend(receiver, selector, arg1, arg2, ...)
编译器通过编译将类和对象转换为结构体,每个结构体都有两个基本要素
- 一个父类的指针
- dispatch table,该实体包括方法选择器(SEL)与对应的调用地址(IMP)
SEL 与 IMP 的关系非常类似于 HashTable 中 key 与 value 的关系。OC 中不支持函数重载的原因就是因为一个类的方法列表中不能存在两个相同的 SEL 。但是多个方法却可以在不同的类中有一个相同的 SEL,不同类的实例对象执行相同的 SEL 时,会在各自的方法列表中去根据 SEL 去寻找自己对应的 IMP,这使得 OC 可以支持函数重写
每个对象的第一个变量是 isa 指向它的类结构体,isa 提供对象访问的类和所有它继承的类
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
image.png
>> 消息发送过程
- 当一个消息发送给一个对象时,通过对象的 isa 在类结构体的 dispatch table 中寻找方法的方法选择器(SEL)
- 如果找不到,则尝试在父类的 dispatch table 中寻找,一直沿着继承链进行寻找,直到 NSObject 类
- 当找到 selector 时,调用对应 dispatch table 中的函数,并将接收对象的数据结构传递给函数
如果用实例对象调用实例方法,会到实例的 isa 指针指向的对象(也就是类对象)操作
如果调用的是类方法,就会到类对象的 isa 指针指向的对象(也就是元类对象)中操作
_cmd 引用当前 selector,self 为当前接收消息的对象
- strange
{
id target = getTheReceiver();
SEL method = getTheMethod();
if ( target == self || method == _cmd )
return nil;
return [target performSelector:method];
}
Getting a Method Address
methodForSelector:
可以直接获取函数地址,规避消息传递带来的多余开销
void (*setter)(id, SEL, BOOL);
int i;
setter = (void (*)(id, SEL, BOOL))[target
methodForSelector:@selector(setFilled:)];
for ( i = 0 ; i < 1000 ; i++ )
setter(targetList[i], @selector(setFilled:), YES);
>> 消息缓存
- 在 dispatch table 寻找 Selector 之前,会在 Runtime 系统缓存中查找已使用过的函数地址 和 Selector
- 每一个类都有一个单独的缓存,包括继承的方法以及自身定义的 Selector
- 消息的方式的调用时间会长于函数调用,通过 Runtime 系统缓存会加快调用时间;随着程序的不断运行,会不断对每个类方法进行缓存,并且会动态扩展系统缓存大小
三、 Dynamic Method Resolution
>> 动态属性方法实现使用 @dynamic
修饰;@dynamic propertyName;
>> 动态解析方法
动态提供实例方法和类方法的方法实现
resolveInstanceMethod
resolveClassMethod
eg: 动态添加方法
@implementation MyClass
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
if (aSEL == @selector(resolveThisMethodDynamically)) {
class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:aSEL];
}
@end
>> Dynamic Loading
Objective-C 在运行的时候会连接新的类和扩展
四、 Message Forwarding
当接收消息对象不能处理,在系统报错之前,会进行消息转发
- (void)forwardInvocation:(NSInvocation *)anInvocation {
if ([someOtherObject respondsToSelector:
[anInvocation selector]]) {
[anInvocation invokeWithTarget:someOtherObject];
} else {
[super forwardInvocation:anInvocation];
}
}
通过 forwardInvocation:
消息转发的消息返回值会被转发到被转发对象
如果被转发对象没有相应方法才会调用 forwardInvocation:
;
关于消息转发可以查看 NSInvocation 类详细说明
>> Forwarding and Multiple Inheritance
Message Forwarding 提供了多继承的大部分功能
>> Forwarding and Inheritance
respondsToSelector:
和 isKindOfClass:
只会走继承树,不会走转发流程
Warrior 转发消息给 Diplomat,但是 [aWarrior respondsToSelector:@selector(negotiate)]
将返回 NO
如果希望通过转发来实现继承的行为,需要重写 respondsToSelector:
和 isKindOfClass:
- (BOOL)respondsToSelector:(SEL)aSelector
{
if ( [super respondsToSelector:aSelector] )
return YES;
else {
/* Here, test whether the aSelector message can *
* be forwarded to another object and whether that *
* object can respond to it. Return YES if it can. */
}
return NO;
}
如果对象接收到远程消息,需要重写 methodSignatureForSelector:
方法,返回最终响应转发消息方法的准确描述
- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector
{
NSMethodSignature* signature = [super methodSignatureForSelector:selector];
if (!signature) {
signature = [surrogate methodSignatureForSelector:selector];
}
return signature;
}
五、Runtime tutorial
bulidSetting -> 搜 objc 设置为 NO // 用于使用 objc_msgSend(receiver, selector);
Runtime 库函数在 usr/include/objc
目录下,我们主要关注是这两个头文件:
#import <objc/runtime.h>
#import <objc/objc.h>
>> 1. 获取类的一些信息(包括属性列表,方法列表,成员变量列表,和遵循的协议列表)
- 获取成员变量,利用 KVO 修改值
unsigned int count;
//获取属性列表
objc_property_t *propertyList = class_copyPropertyList([self class], &count);
for (unsigned int i = 0; i < count; i++) {
const char *propertyname = property_getName(propertyList[i]);
NSLog(@"property----="">%@", [NSString stringWithUTF8String:propertyname]);
}
//获取方法列表
Method *methodList = class_copyMethodList([self class], &count);
for (unsigned int i; i<count; i++) {
Method method = methodList[i];
NSLog(@"method----="">%@", NSStringFromSelector(method_getName(method)));
}
//获取成员变量列表
Ivar *ivarList = class_copyIvarList([self class], &count);
for (unsigned int i; i<count; i++) {
Ivar myivar = ivarList[i];
const char *ivarname = ivar_getName(myivar);
NSLog(@"ivar----="">%@", [NSString stringWithUTF8String:ivarname]);
}
//获取协议列表
__unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
for (unsigned int i; i<count; i++) {
Protocol *myprotocal = protocolList[i];
const char *protocolname = protocol_getName(myprotocal);
NSLog(@"protocol----="">%@", [NSString stringWithUTF8String:protocolname]);
}
>> 2. 遍历对象的属性
比如,看看 zhangsan 的有哪些属性(身高:180、年龄:18)
>> 3. 动态添加/修改属性,动态添加/修改/替换方法
比如,修改 zhangsan 的身高为 190、年龄为 20,替换 walkTheDog 方法(变成 walkTheBigDog),给他添加一个新方法 walkTheCat 等等
>> 4. 动态创建类/对象/协议等等
比如,创建一个新的对象:lisi
>> 5. 方法拦截调用
比如,给 zhangsan 发送一个 walkTheDog 消息,但是 zhangsan 不知道怎么 walk 啊(没实现该方法),那我们可以拦截下,给该方法动态添加一个实现,甚至可以讲该方法定向或者打包给 lisi(其他对象),让 lisi 来 walk
六、Message Forwarding Process
- 首先,在相应操作的对象中的缓存方法列表中找调用的方法,如果找到,转向相应实现并执行
- 如果没找到,在相应操作的对象中的方法列表中找调用的方法,如果找到,转向相应实现执行
- 如果没找到,去父类指针所指向的对象中执行 1,2
- 以此类推,如果一直到根类还没找到,转向拦截调用
- 如果没有重写拦截调用的方法,程序报错
>> 拦截调用
在找不到调用的方法程序崩溃之前,你有机会通过重写 NSObject 的四个方法来处理
-
动态方法解析 -
resolveInstanceMethod
-
动态方法解析 -
resolveClassMethod
-
消息转发 -
forwardInvocation:
-
消息转发 -
methodSignatureForSelector:
>> 关于父类
- 重写父类的方法,并没有覆盖掉父类的方法,只是在当前类对象中找到了这个方法后就不会再去父类中找了
- 如果想调用已经重写过的方法的父类的实现,只需使用 super 这个编译器标识,它会在运行时父类对象中寻找方法
七、Associated
- 现在你准备用一个系统的类,但是系统的类并不能满足你的需求,你需要额外添加一个属性。 这种情况的一般解决办法就是继承
- 但是只增加一个属性,就去继承一个类,总是觉得太麻烦了
- 这个时候 Runtime 的关联属性就发挥它的作用了
// objc_AssociationPolicy 关联策略,有以下几种策略:
enum {
OBJC_ASSOCIATION_ASSIGN = 0,
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
OBJC_ASSOCIATION_RETAIN = 01401,
OBJC_ASSOCIATION_COPY = 01403
};
>> 使用 Selector 作为关联对象唯一的 key
//设置关联对象
- (void)setGifDelgeate:(id<GifImageViewDelegate>)gifDelgeate {
objc_setAssociatedObject(self, @selector(gifDelgeate), gifDelgeate, OBJC_ASSOCIATION_ASSIGN); //获取关联对象
}
//获取关联对象
- (id<GifImageViewDelegate>)gifDelgeate {
// 这里面我们把 getAssociatedObject 方法的地址作为唯一的 key,_cmd 代表当前调用方法的地址
// return objc_getAssociatedObject(self, _cmd);
return objc_getAssociatedObject(self, @selector(gifDelgeate));
}
>> 定义一个全局变量用它的地址,作为关联对象唯一的 key
static char associatedObjectKey;
objc_setAssociatedObject(target, &associatedObjectKey, @"添加的字符串属性", OBJC_ASSOCIATION_RETAIN_NONATOMIC);
NSString *string = objc_getAssociatedObject(target, &associatedObjectKey);
NSLog(@"AssociatedObject = %@", string);
八、Structure
Structure | Description |
---|---|
Method | 成员方法 |
Ivar | 成员属性 |
Category | 分类 |
objc_property_t | 属性 |
class_copyIvarList | 拷贝出一个对象的所有成员列表 |
class_copyMethodLIst | 拷贝出一个对象的所有成员方法列表 |
message: | 两个函数 |
objc_msgSend | 给某个对象发送消息 |
objc_msgSendSuper | 给某个对象的父类发送消息 |