Runtime
前言
从字面意思看,就是运行时.但是这个运行时究竟什么意思?可以把它理解成:不是在编译期也不是在链接期,而是在运行时.那究竟在运行期间做了什么呢?按照苹果官方的说法,就是把一些决策(方法的调用,类的添加等)推迟,推迟到运行期间.只要有可能,程序就可以动态的完成任务,而不是我们在编译期已经决定它要完成什么任务.这就意味了 OC 不仅仅需要编译器,还需要一个运行时的系统来支撑.
目录
接下来就对 Runtime 做一个系统的介绍,主要内容包括:
简介
涉及到的数据结构
runtime.h 解析
如何可以触及到 RunTime?
消息
动态消息解析
消息转发
Runtime 的使用场景
1. 简介
根据前言,你已经了解了 Runtime 大概是个什么鬼,在 OC 发展历程中,它主要有两个版本:Legacy 和 Modern.Legacy 版本采用的是 OC1.0 版本;Modern 版本采用的 OC2.0 版本,而且相比 Legacy 也添加了一些新特性.最明显的区别在于:
在 legacy 版本,如果你改变了类的布局,那么你必须重新编译继承自它的类.
在 modern 版本,如果你改变了类的布局,你不必重新编辑继承自它的类.
平台
iPhone 的应用程序以及 OS X v10.5 版本的 64 位机器使用的是 modern 版本的 runtime.
其他(OS X 桌面应用 32 位程序)使用的是 legacy 版本的 runtime.
2. 涉及到的数据结构
这里主要介绍一下在 runtime.h 里面涉及到的一些数据结构.
Ivar
Ivar 从字面意思来讲,它就是代表的实例变量,它也是一个结构体指针,包含了变量的名称,类型,偏移量以及所占空间.
SEL
选择器,每个方法都有自己的选择器,其实就是方法的名字,但是不仅仅是方法的名字,在 objc.h 中,我们可以看到它的定义:
由定义可知它是一个 objc_selector 的结构体指针,尴尬的是在 runtime 源码中并没有找到该结构体.猜想它内部应该就是一个 char 的字符串.
/// An opaque type that represents a method selector.一个不透明类型,用来代表一个方法选择器
typedef struct objc_selector * SEL;
你可以使用:
NSLog(@"%s",@selector(description)); //%s用来输出一个字符串
打印出来 description.
在这里你可以把它理解成一个选择器,可以标识某个方法.
IMP
它是一个函数指针,指向方法的实现,在 objc.h 里面它的定义是这样的:
id 是一个我们经常使用的类型,可用于作为类型转换的中介者.它类似于 Java 里面的 Object,可以转换为任何的数据类型.它在 objc.h 里面是这样定义的:
/// A pointer to the function of a method implementation.
#
if ! OBJC_OLD_DISPATCH_PROTOTYPES typedef void( * IMP)(void
/* id, SEL, ... */
);#
else typedef id _Nullable( * IMP)(id _Nonnull, SEL _Nonnull, ...);#endif
id
它其实是一个 objc _ object 的结构体指针,而在后面将要提到的 Class 其实是个 objc _ class 的指针,而 objc _ class 是继承自 objc _o bject 的,因此可以相互转换,这也是为什么 id 可以转换为其他任何的数据类型的原因.
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
/// A pointer to an instance of a class.
typedef struct objc_object * id;
Method
方法,它其实是一个 objc_method 的结构体指针, 其定义如下:
这个就比较好理解了,该结构体包含了方法的名称(SEL),方法的类型以及方法的 IMP.
/// An opaque type that represents a method in a class definition.
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;
}
Class
它是一个 objc_class 的结构体指针,在 runtime.h 中的定义如下:
该结构体中各部分介绍如下:
/// An opaque type that represents an Objective-C class.一个不透明类型,代表OC的类
typedef struct objc_class * Class;
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;#
if ! __OBJC2__ Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;#endif
}
OBJC2_UNAVAILABLE;
isa:是一个 Class 类型的指针,每个对象的实例都有 isa 指针,他指向对象的类.而 Class 里面也有个 isa 指针,它指向 meteClass(元类),元类保存了类方法的列表.
name:对象的名字
version:类的版本号,必须是 0
info:供运行期间使用的位标识
instance_size:该类的实例大小
ivars:成员变量数组, 包含了该类包含的成员变量
methodLists:包含方法的数组列表,也是一个结构体,该结构体里面还包含了一个 obsolete 的指针,表示废弃的方法的列表
cache:缓存.这个比较复杂,在后面会提到,这里先忽略.
protocols:协议列表,也是一个数组
而在 objc-runtime-new.h 中,你会发现这样的定义(在 runtime 中并没有完全暴露 objc_class 的实现):
其实 objc _ class 继承自 objc _ object.所以这也说明了为什么 id 能够转换为其他的类型.
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
class_rw_t *data() {
return bits.data();
}
//其他的省略
3.runtime.h 解析
我们先看一下在 usr/include/objc/runtime.h,这个是任何一个工程都可以直接找到的,它是 SDK 的一部分.主要定义了以下内容:
定义了一些类型,例如 Method/Ivar/Category 等,还有一些结构体.
函数.函数里面有分了几大类:
关于对象实例的方法,例如 object _ getClass,object _ setClass 以及 object _ getIvar 等.这些函数大多以 object 开头. 用来获取属性或者对对象进行操作.
获得类定义的方法,例如 objc _ getClass/objc _ getMetaClass 等,这些方法更多的是获取 Class 或者在 Class 级别上进行操作. 多以 objc 开头
和类相关的方法.例如 class _ getName/class _ isMetaClass 等,这些更多的是获取 Class 的一些属性.比如该类的属性列表,方法列表,协议列表等.传参大多为 Class. 多以 class 开头
实例化类的一些方法.例如 class _ createInstance 方法,就是相当于平时的 alloc init.
添加类的方法.例如你可以使用这些方法冬天的注册一个类.使用 objc _ allocateClassPair 创建一个新类,使用 objc _ registerClassPair 对类进行注册
等等...
另外就是一些废弃的方法和类型.
4. 如何可以触及到 RunTime?
有三种不同的方式可以让 OC 编程和 runtime 系统交互.
OC 源代码
大多数情况下,我们写的 OC 代码,其实它底层的实现就是 runtime.runtime 系统在背后自动帮我们处理了操作.例如我们编译一个类,编译器器会创建一个结构体,然后这个结构体会从类中捕获信息,包括方法,属性,Protocol 等.
NSObject 的一些方法
在 Foundation 框架里面有个 NSObject.h,在 usr/include/objc 里面也有一个 NSObject.h.而我们平时用到的类的基类是 / usr/include/objc 里面的这个 NSObject.h,Foundation 里面的 NSObject.h 只是 NSObject 的一个 Category.所以这里我们更关注一下 / usr/include/objc 里面的 NSObject.h.
由于大多数对象都是 NSObject 的子类,所以在 NSObject.h 里面定义的方法都可以使用.
在这些方法里面,有一些方法能够查询 runtime 系统的信息,例如:
这里用代码对 isKindOfClass 和 isMemberOfClass 做个简单介绍:
- (BOOL)isKindOfClass:(Class)aClass; //用来检测一个对象是否是某各类的实例对象,aClass也有可能是父类,同样可以检测出来.
- (BOOL)isMemberOfClass:(Class)aClass; //而该方法只能检测一个对象是否是某各类的实例对象.但如果aClass不能为该类的父类,如果是父类则该方法返回NO
- (BOOL)respondsToSelector:(SEL)aSelector;
- (BOOL)conformsToProtocol:(Protocol *)aProtocol;
- (IMP)methodForSelector:(SEL)aSelector;
我们可以在 objc 源代码中的 NSObject.mm 中看到相应的实现:
//stu是Student的实例对象,Student的父类为Person,Person的父类为NSObject.
[stu isKindOfClass:[Student class]]; //YES
[stu isKindOfClass:[Person class]]; //YES
[stu isKindOfClass:[NSObject class]]; //YES
[stu isMemberOfClass:[Student class]]; //YES
[stu isMemberOfClass:[Person class]]; //NO
[stu isMemberOfClass:[NSObject class]]; //NO
从具体实现可知,为什么 isKindOfClass 能够检测出 superclass.另外,在 NSObject.h 中,并没有看到两个方法的类方法声明,但是在实现里面却包含了类方法的实现.这里有个疑问:为什么没有对外声明的两个类方法依然可以在外部调用呢?(比如我可以直接使用 [Student isMemberOfClass:[NSObject class]]).
+ (BOOL)isMemberOfClass:(Class)cls {
return object_getClass((id)self) == cls;
}
- (BOOL)isMemberOfClass:(Class)cls {
return [self class] == cls;
}
+ (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
- (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
这里还用到了 class 方法,这个方法声明如下:
这里重要的是理解 self 究竟代表着什么:
+ (Class)class OBJC_SWIFT_UNAVAILABLE("use 'aClass.self' instead");
- (Class)class OBJC_SWIFT_UNAVAILABLE("use 'type(of: anObject)' instead");
+ (Class)class { //返回当前的self
return self;
}
- (Class)class {
return object_getClass(self);
}
当 self 为实例对象的时候,[self class] 和 object_getClass(self) 是等价的.object_getClass([self class]) 得到的是元类.
当 self 为类对象的时候,[self class] 返回的是自身,还是 self.object_getClass(self) 与 object_getClass([self class]) 等价.拿到的是元类.
Runtime 函数
runtime 系统其实就是一个动态共享的 Library,它是由在 / usr/include/objc 目录的公共接口中的函数和数据结构组成.
5. 消息
在 Objective-C 中,消息直到运行时才将其与消息的实现绑定,编译器会将
[receiver message];
转换成
如果包含参数,那么就会执行 2 方法.其实除了该方法,还有以下几个方法:
objc_msgSend(receiver,selector); //1
objc_msgSend(receiver,selector,arg1,arg2,...); //2
当想一个对象的父类发送 message 时,会使用
objc_msgSend_stret
objc_msgSendSuper
objc_msgSendSuper_stret
objc_msgSendSuper
如果方法的返回值是一个结构体,那么就会使用
这里我们可以打开 objc 源码 ,然后你会发现里面有多个. s 文件:
objc_msgSend_stret
objc_msgSendSuper_stret
这里之所以有 objc-msg - 类的不同文件,我猜想应该是对不同的 CPU 指令集 (指令不一样) 做了分别处理.因为这些. s 文件名称中包含的是不同的 arm 指令集.而且打开. s 文件你会发现里面的实现是汇编语言,所以苹果为了效率还是蛮拼的,直接用汇编语言实现.
其中就能找到 objc _ msgSend 的实现(objc-msg-i386.s 中):
虽然对汇编了解不是太多,但是这个文件中的注释很详细,从注释可以看出 objc_msgSend 方法的执行过程:
先加载 receiver 和 selector 到寄存器,然后判断 receiver 是否为空, 如果为空,则函数执行结束;
如果 receiver 不为空,开始搜索缓存,查看方法缓存列表里面是否有改 selector,如果有则执行;
如果没有缓存,则搜索方法列表,如果在方法列表中找到,则跳转到具体的 imp 实现.没有则执行结束.
使用了隐藏参数
在发送一个消息的时候,会被编译成 objc_msgSend,此时该消息的参数将会传入 objc_msgSend 方法里面.除此之外,还会包含两个隐藏的参数:
receiver
method 的 selector
这两个参数在上面也有提到.其中的 receiver 就是消息的发送方,而 selector 就是选择器,也可以直接用 _ cmd 来指代 (_ cmd 用来代表当前所在方法的 SEL).之所以隐蔽是因为在方法声明中并没有被明确声明,在源代码中我们仍然可以引用它们.
获取方法地址
我们每次发送消息都会走 objc_msgSend() 方法,那么有没有办法避开消息绑定直接获取方法的地址并调用方法呢?答案当然是有的.我们上面简单介绍了 IMP,其实我们可以使用 NSObject 的
- (IMP) methodForSelector: (SEL) aSelector;
方法,通过该方法获得 IMP,然后调用该方法.但是避开消息绑定而直接调用的使用并不常见,但是如果你要多次循环调用的话,直接获取方法地址并调用不失为一个省时操作.看下面的代码:
你可以自行跑一下,看一下时间差异.你会发现:获取方法地址直接调用更省时间,但请注意使用场景.
void( * setter)(id, SEL, BOOL);
setter = (void( * )(id, SEL, BOOL))[stu2 methodForSelector: @selector(learning)];
NSDate * startDate = [NSDate date];
for (int i = 0; i < 100000; i++) {
setter(stu2, @selector(learning), YES);
}
double deltaTime = [[NSDate date] timeIntervalSinceDate: startDate];
NSLog(@"----%f", deltaTime);
NSDate * startDate1 = [NSDate date];
for (int i = 0; i < 100000; i++) { [stu2 learning];
}
double deltaTime1 = [[NSDate date] timeIntervalSinceDate: startDate1];
NSLog(@"----%f", deltaTime1);
6. 动态消息解析
这里介绍一下如果动态地提供方法的实现.
动态方法解析
在开发过程中,你可能想动态地提供一个方法的实现.比如我们对一个对象声明了一个属性,然后我们使用了 @dynamic 标识符:
@dynamic propertyName;
该标识符的目的就是告诉编译器:和这个属性相关的 getter 和 setter 方法会动态地提供(当然你也可以直接手动在代码里面实现).这个时候你就会用到 NSObject.h 里面的两个方法
来提供方法的实现.
+ (BOOL)resolveClassMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
+ (BOOL)resolveInstanceMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
其实 OC 方法就是一个简单的 C 函数,它至少包含了两个参数 self 和 _ cmd,你可以自己声明一个方法:
此时我们可以在声明属性的类中实现上面提到的两个方法(一个是解析类方法,一个是解析实例方法),例如我在 Person 里面这样写:
void dynamicMethodIMP(id self, SEL _cmd) {
//这里是方法的具体实现
}
@dynamic address; //也就意味着我们需要手动/动态实现该属性的getter和setter方法.
你会发现当我们运行下面的代码时,程序会 crash:
这里简单的做一个动态方法解析:
Person *zhangsan = [[Person alloc] init];
zhangsan.address = @"he nan xinxiang ";
NSLog(@"%@",zhangsan.address);
// crash reason
// -[Person setAddress:]: unrecognized selector sent to instance 0x1d4449630
所以我们需要自己去实现 setAddress: 方法.(这里判断用 hasPrefix 不太准确,开发者可以自行根据需求调整).转发消息(下面会讲到)和动态解析是正交的.也就是说一个 class 有机会再消息转发机制前去动态解析此方法,也可以将动态解析方法返回 NO,然后将操作转发给消息转发.
void setter(id self,SEL _cmd) {
NSLog(@"set address");
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSString *selStr = NSStringFromSelector(sel);
if ([selStr hasPrefix:@"set"]) {
class_addMethod([self class], sel, (IMP)setter, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
动态加载
OC 编程也允许我们在程序运行的时候动态去创建和链接一个类或者分类.这些创建的类或者分类将会和运行 app 前创建的类一样,没有差别.
动态加载在开发的过程中可以做好多事情,例如系统设置中的不同模块就是动态加载的.
在 Cocoa 环境中,最经典的就是 Xcode,它可以安装不同的插件,这个也是动态加载的方式实现的.
7. 消息转发
发送一个消息给对象,如果对象不能处理,那么就会产生错误.然而,在产生错误之前,runtime 系统会给对象第二次机会去处理该消息.这里详细已经在 深入浅出理解消息的传递和转发 文章中做了介绍,这里就不再介绍了.
8. Runtime 的使用场景
Runtime 的使用几乎无处不在,OC 本身就是一门运行时语言,Class 的生成,方法的调用等等,都是 Runtime.另外,我们可以用 Runtime 做一些其他的事情.
字典转换 Model
平时我们从服务端拿到的数据是 json 字符串,我们可以将其转换成成 NSDictionary,然后通过 runtime 中的一些方法做一个转换:
先拿到 model 的所有属性或者成员变量,然后将其和字典中的 key 做映射,然后通过 KVC 对属性赋值即可.更多可参见 class_copyIvarList 方法获取实例变量问题引发的思考 中的例子.
热更新(JSPatch 的实现)
JSPatch 能做到 JS 调用和改写 OC 方法的根本原因就是 OC 是动态语言,OC 上的所有方法的调用 / 类的生成都通过 OC Runtime 在运行时进行,我们可以根据名称 / 方法名反射得到相应的类和方法.例如
也正是鉴于此,才实现了热更新.
Class class = NSClassFromString("UIViewController");
id viewController = [[class alloc] init];
SEL selector = NSSelectorFromString("viewDidLoad");
[viewController performSelector:selector];
给 Category 添加属性
我们可以使用 runtime 在 Category 中给类添加属性,这个主要使用了两个 runtime 钟的方法:
具体使用可参见: 给分类(Category)添加属性 .
OBJC_EXPORT void
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,id _Nullable value, objc_AssociationPolicy policy);
OBJC_EXPORT id _Nullable
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key);
Method Swizzling
它是改变一个已存在的 selector 的实现的技术,比如你想将 viewDidload 方法替换为我们自定义的方法,给系统的方法添加一些需要的功能,来实现某些需求.比如你想跟踪每个 ViewController 展示的次数,你可以使用该技术重写 ViewDidAppear 方法,然后做一些自己的处理.可以参见 Method Swizzling 里面的讲解.
总结
Objective-c 本身就是一门冬天语言,所以了解 runtime 有助于我们更加深入地了解其内部的实现原理.也会把一些看似很难的问题通过 runtime 很快解决.
参考链接:
4. 深入浅出理解消息的传递和转发
. Objective-C Runtime Programming Guide
. Objective-C Runtime
. objc4
5. class_copyIvarList 方法获取实例变量问题引发的思考
6. JSPatch 实现原理详解
7. 给分类(Category)添加属性
8. Method Swizzling
来源: https://www.cnblogs.com/zhanggui/p/8243316.html