iOS Objective-C的骨架 -- Runtime
我们常说Objective-C
是一门动态语言, 或者运行时语言. 动态和静态是相对的. 相对于静态语言来说, Objective-C
不是在编译的时候就确定了所有东西. 可以在运行期间动态的添加类、变量、方法
等. 这意味着它不仅需要一个编译器,也需要一个运行时系统来动态得创建类和对象、进行消息传递和转发. Objective-C
是基于C
语言的超集,要想实现运行时
这一特性, 需要有一个桥梁, 而runtime
就是这个桥梁.
Objective-C
作为一门高级编程语言想, 要成为可执行文件需要先编译为汇编语言再汇编为机器语言,机器语言也是计算机能够识别的唯一语言,但是Objective-C
并不能直接编译为汇编语言,而是要先转写为纯C语言
再进行编译和汇编的操作,从Objective-C
到C语言
的过渡就是由runtime
来实现的。然而我们使用Objective-C
进行面向对象开发,而C语言
更多的是面向过程开发,这就需要将面向对象的类
转变为面向过程的结构体
.
Runtime
其实有两个版本: “modern
” 和 “legacy
”. 我们现在用的Objective-C 2.0
采用的是现行 (Modern
) 版的Runtime
系统, 只能运行在 iOS 和 macOS 10.5 之后的 64 位程序中. 而 macOS 较老的32位程序仍采用Objective-C 1
中的(早期)Legacy
版本的Runtime
系统. 这两个版本最大的区别在于当你更改一个类的实例变量的布局时, 在早期版本中你需要重新编译它的子类,而现行版就不需要. 苹果和GNU各自维护一个开源的 runtime 版本, 这两个版本之间都在努力的保持一致.
我将和大家一起从下边几个方面一起来深入认知Runtime
的实现:
- 一:
Runtime
的消息传递过程 - 二:
Runtime
的消息转发过程 - 三:
Category
的底层实现 - 四:
Runtime
在日常工作中的应用
一: Runtime
的消息传递过程:
Objective-C
是一门面向对象
的语言, 要想理解Runtime
在其中的作用, 首先我们先要了解我们口中的对象(object), 类(calss), 方法(method)
, 在Runtime
中是都是什么构造. 通过阅读objc/runtime.h
中源码, 我们可以找到:
//!< 对象
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
//!< 类
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
//!< 方法列表
struct objc_method_list {
struct objc_method_list *obsolete OBJC2_UNAVAILABLE;
int method_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_method method_list[1] OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
//!< 方法
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
}
下面讲解在消息传递过程中用到的概念:
- 类(类对象 objc_class);
- 对象(实例 objc_object);
- 元类(Meta Class);
- Method(objc_method);
- methodList(方法列表 objc_method_list);
- objc_cache (类缓存);
- SEL(objc_selector);
- IMP;
- Category;
类(类对象 objc_class):
在Objective-C
中, 类是由Class
来表示的, 由源码可知, 其实际上是一个指向objc_class 结构体
的指针
typedef struct objc_class *Class;
而 objc_class 结构体
的定义如下:
//!< 类
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
通过结构体中的变量
的命名, 我们不难明白每个变量
的含义及作用.
它保存了指向父类的指针
、类的名字
、版本
、实例大小
、实例变量列表
、方法列表
、缓存
、遵守的协议列表
等信息. 这个结构体中保存的数据称为元数据(metaData)
. 而结构体的第一个变量为isa
指针, 这说明类本身也是一个对象. 我们称之为类对象
. 类对象在编译期产生, 用于创造实例对象
, 是单利
.
对象(实例 objc_object):
/// Represents an instance of a class.
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
/// A pointer to an instance of a class.
typedef struct objc_object *id;
类对象中, 保存了创建一个实例
所需的数据, 那么创建类对象
所需要的数据保存在何处? 这就引出了一个新的概念元类(metaclass)
. 实际上, 实例对象
的isa
指针指向它的类
, 而类对象
的isa
指针, 指向保存了创建类对象
所需数据的类:元类
, 而元类
的isa
指针指向根元类
.
如下图所示:
元类(Meta Class):
在上一节中, 我们提出了metaclass
元类概念. 很显然, 元类
也是实例
, 那么它是谁的实例
呢? 通过上图我们不难看出, 元类
是根元类
的实例. 元类的isa
指针指向根元类
, 根元类
是其自身的实例
, 它的isa
指针指向自身, 这样就形成了一个巧妙的闭环. 元类
的super_class
指针指向它的父类的元类
, 任何NSObject
继承体系下的meta-class
都使用NSObject
的meta-class
作为自己的所属类,而基类的meta-class
(根元类
)的isa
指针是指向它自己.
Method(objc_method):
/// An opaque type that represents a method in a class definition.代表类定义中一个方法的不透明类型
typedef struct objc_method *Method;
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
}
从objc_method
结构体中, 可以看出一个Method
的构成: SEL method_name
方法名、char *method_types
方法类型、 IMP method_imp
方法实现.
methodList(方法列表 objc_method_list);
从命名可以很明显的得知, methodList
是用来存储一个类的所有method
.
//方法列表
struct objc_method_list {
struct objc_method_list *obsolete OBJC2_UNAVAILABLE;
int method_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_method method_list[1] OBJC2_UNAVAILABLE;
}
由源码可知, methodList
中存储的是一个个struct objc_method
. 那么我们每次需要进行objc_msgSend
时都要在methodList
进行遍历查找吗? 这样的效率未免也太低下了. 要知道, 一个类中很可能只有20%
的常用方法. 这就用到了struct objc_cache
来减少常用方法的查找时间.
objc_cache (类缓存):
当Objective-C
运行时通过跟踪它的isa
指针检查对象时,它可以找到一个实现许多方法的对象。然而,你可能只调用它们的一小部分,并且每次查找时,搜索所有选择器的类分派表没有意义。所以类实现一个缓存,每当你搜索一个类分派表,并找到相应的选择器(SEL
),它把它放入它的缓存。所以当objc_msgSend
查找一个类的selector
,它首先搜索类缓存。这是基于这样的理论:如果你在类上调用一个消息,你可能以后再次调用该消息。
为了加速消息分发, 系统会对方法和对应的地址进行缓存,就放在上述的objc_cache
,所以在实际运行中,大部分常用的方法都是会被缓存起来的,Runtime
系统实际上非常快,接近直接执行内存地址的程序速度。
SEL:
typedef struct objc_selector *SEL;
objc_msgSend
函数第二个参数类型为SEL
,它是selector
在Objective-
C中的表示类型. selector
是方法选择器,可以理解为区分方法的 Identifer
,而这个 Identifer
的数据结构是SEL
:
A method selector is a C string that has been registered (or “mapped“) with the Objective-C runtime. Selectors generated by the compiler are automatically mapped by the runtime when the class is loaded. 方法选择器是已使用Objective-C运行时注册(或“映射”)的C字符串。 加载类时,编译器生成的选择器将由运行时自动映射。
其实selector
就是个映射到方法的C
字符串,你可以用 Objective-C
编译器命令@selector()
或者 sel_registerName
函数来获得一个 SEL
类型的方法选择器。
selector
既然是用来区分方法的Identifer
, 那么猜想应该有以下规则:
- 同一个类中,
selector
不能重复. - 不同类中,
selector
可以重复.
实际上, 我们在日常工作中会发现, 同一个类中如果有相同的selector
, 那么在编译时就会报错. 然而在Category
中是可以有selector
和类中重复, 是Category
中的selector
将类中的selector
重写了吗? 我们会在后边详细讲解Category
的具体实现.
IMP:
看下IMP
的定义 :
/// A pointer to the function of a method implementation. 指向一个方法实现的指针
typedef id (*IMP)(id, SEL, ...);
#endif
就是指向最终实现方法的内存地址
的指针
.
Category(objc_category):
Category
是表示一个指向分类的结构体的指针,其定义如下:
struct category_t {
const char *name;
classref_t cls;
struct method_list_t *instanceMethods;
struct method_list_t *classMethods;
struct protocol_list_t *protocols;
struct property_list_t *instanceProperties;
};
name
:是指 class_name
而不是 category_name
.
cls
:要扩展的类对象,编译期间是不会定义的,而是在Runtime
阶段通过name
对应到对应的类对象.
instanceMethods
:category
中所有给类添加的实例方法的列表.
classMethods
:category
中所有添加的类方法的列表.
protocols
:category
实现的所有协议的列表.
instanceProperties
:表示Category
里所有的properties
,这就是我们可以通过objc_setAssociatedObject
和objc_getAssociatedObject
增加实例变量的原因,不过这个和一般的实例变量是不一样的.
从上面的category_t
的结构体中可以看出,分类中可以添加实例方法
,类方法
,甚至可以实现协议
,添加属性
,不可以添加成员变量(ivar)
. 具体原因以及底层实现我们在后边详细探讨.
经过上述概念解释, 我们可以总结出Runtime
消息传递流程:
实例方法:
一个实例方法像这样[obj foo]
,编译器转成消息发送objc_msgSend(obj, foo)
,Runtime
时执行的流程是这样的:
- 通过
obj
的isa
指针找到它的class
; - 在
class
结构体的objc_cache
中找foo
; - 如果缓存中没找到, 继续在
class
结构体的method list
中找foo
; - 如果
class
中没找到foo
,通过它的superclass
指针去class
的父类
中找, 一直查找到基类NSObject
; - 一旦找到
foo
这个函数, 通过objc_method
结构体中的IMP
指针找到最终实现方法的内存地址
, 去实现.
类方法:
一个实例方法像这样[class foo]
,编译器转成消息发送objc_msgSend(class, foo)
,Runtime
时执行的流程是这样的:
- 通过
class
的isa
指针, 找到class
的metaclass
元类, - 在
metaclass
中的objc_cache
中查找类方法foo
; - 若缓存中没找到, 继续在
methodlist
中查找foo
. - 若
metaclass
中没有找到foo
, 通过metaclass
的superclass
指针去class
的父类的metaclass
中查找, 一直到NSObject类
的metaclass
; - 一旦找到
foo
这个函数, 通过objc_method
结构体中的IMP
指针找到最终实现方法的内存地址
, 去实现.
这个时候又有新的问题产生: 在消息传递的过程中, 如果一直到最后都没有找到foo
呢? 接下来我们来看看当没有找到foo
时会发生什么.
二: Runtime
的消息转发过程:
上文在消息传递的过程中, 我们产生了新的问题: 如果一直到最后都没有找到foo
? 实际上, 如果还是找不到并且消息转发都失败了就回执行doesNotRecognizeSelector:
方法, 抛出unrecognized selector
异常. 那么消息转发到底是什么呢?接下来将会逐一介绍消息转发的三个步骤, 这也是在抛出unrecognized selector
异常前, 我们可以拯救程序的唯三
步骤.
- 动态方法解析:
+resolveInstanceMethod:
/+resolveClassMethod:
- 备用接收者:
-forwardingTargetForSelector:
- 完整消息转发:
-methodSignatureForSelector:
;-forwardInvocation:
动态方法解析: +resolveInstanceMethod:
/ +resolveClassMethod:
:
消息转发的第一步, Runtime
会调用 +resolveInstanceMethod:
(实例方法)或者 +resolveClassMethod:
(类方法), 让你有机会提供一个函数实现(IMP)。如果你添加了IMP
并返回YES
, 那Runtime
就会重新启动一次消息发送的过程.
实现一个动态方法解析的例子如下:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
//!< 执行foo函数, 这时消息发送过程肯定找不到IMP, 就会进入消息转发
[self performSelector:@selector(foo:)];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(foo:)) {
//!< 如果是执行foo函数,就动态解析,指定一个新的IMP
//!< "v@:" 方法类型
class_addMethod([self class], sel, (IMP)fooMethod, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
//!< 将要执行这个新的IMP
void fooMethod(id obj, SEL _cmd) {
NSLog(@"new foo has executed");
}
如果+resolveInstanceMethod:
或者+resolveClassMethod:
return NO
, 那么将会进行消息转发的第二个步骤: 备用接收者: -forwardingTargetForSelector:
.
备用接收者: -forwardingTargetForSelector:
:
如果目标对象实现了-forwardingTargetForSelector:
方法, 那么Runtime
允许我们指定一个对象, 将这个方法转发给指定的对象.
实现一个备用接收者的例子如下:
#import "ViewController.h"
#import "objc/runtime.h"
@interface Person: NSObject
@end
@implementation Person
- (void)foo {
NSLog(@" foo has executed"); //!< Person的foo函数
}
@end
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
//!< 执行foo函数, 这时消息发送过程肯定找不到IMP, 就会进入消息转发
[self performSelector:@selector(foo)];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return NO; //!< 返回NO,进入下一步转发
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(foo)) {
return [Person new];//!< 返回Person对象,让Person对象接收这个消息
}
return [super forwardingTargetForSelector:aSelector];
}
@end
可以看到我们通过-forwardingTargetForSelector:
把当前ViewController
的方法转发给了Person
去执行.
如果这个对象实现了foo
那么就会重新走一遍消息发送的过程. 如果return nil
或者这个对象没有实现foo
, 进行第三个步骤--完整消息转发.
完整消息转发: -methodSignatureForSelector:
; -forwardInvocation:
:
如果在上一步还不能处理未知消息, 则唯一能做的就是启用完整的消息转发机制了.
首先, 会发送-methodSignatureForSelector:
函数, 来获取函数的参数
和返回值类型
, 如果-methodSignatureForSelector:
return nil
, Runtime
会发送doesNotRecognizeSelector:
, 抛出unrecognized selector
, 这个时候程序就会崩溃. 如果-methodSignatureForSelector:
返回一个函数签名(NSMethodSignature)
, Runtime
就会创建一个NSInvocation
对象并发送 -forwardInvocation:
消息给目标对象.
实现一个完整消息转发的例子如下:
import "ViewController.h"
#import "objc/runtime.h"
@interface Person: NSObject
@end
@implementation Person
- (void)foo {
NSLog(@"foo has executed"); //!< Person的foo函数
}
@end
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
//!< 执行foo函数, 这时消息发送过程肯定找不到IMP, 就会进入消息转发
[self performSelector:@selector(foo)];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return NO; //!< 返回NO,进入下一步转发
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
return nil; //!< 返回nil,进入下一步转发
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if ([NSStringFromSelector(aSelector) isEqualToString:@"foo"]) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"]; //!< 签名,进入forwardInvocation
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL sel = anInvocation.selector;
Person *p = [Person new];
if([p respondsToSelector:sel]) {
[anInvocation invokeWithTarget:p];
}
else {
[self doesNotRecognizeSelector:sel];
}
}
@end
以上就是Runtime
完整的三次转发流程.
三: Category
的底层实现
在这里, 我们一起思考以下问题:
-
Category
的实现原理是什么? -
Category
可以添加属性
吗?可以添加实例变量
吗? -
Category
中添加的方法, 在消息发送的过程中是如何找到的? -
Category
中添加了一个与本类中方法相同名字的方法, 为什么本类中的方法不会再调用? 是重写
还是被覆盖
?
接下来我们通过一段代码来进行分析:
#import <Foundation/Foundation.h>
@interface Preson : NSObject
{
int _age;
}
- (void)run;
@end
//!< Presen.m
#import "Preson.h"
@implementation Preson
- (void)run
{
NSLog(@"Person - run");
}
@end
//!< Presen分类1
//!< Presen+Test.h
#import "Preson.h"
@interface Preson (Test) <NSCopying>
- (void)test;
+ (void)abc;
@property (assign, nonatomic) int age;
- (void)setAge:(int)age;
- (int)age;
@end
//!< Presen+Test.m
#import "Preson+Test.h"
@implementation Preson (Test)
- (void)test
{
}
+ (void)abc
{
}
- (void)setAge:(int)age
{
}
- (int)age
{
return 10;
}
@end
Presen分类2
//!< Preson+Test2.h
#import "Preson.h"
@interface Preson (Test2)
@end
//!< Preson+Test2.m
#import "Preson+Test2.h"
@implementation Preson (Test2)
- (void)run
{
NSLog(@"Person (Test2) - run");
}
@end
通过查看分类的源码我们可以找到category_t
结构体:
struct category_t {
const char *name;
classref_t cls;
struct method_list_t *instanceMethods; //!< 对象方法
struct method_list_t *classMethods; //!< 类方法
struct protocol_list_t *protocols; //!< 协议
struct property_list_t *instanceProperties; //!< 属性
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties;
method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};
从源码基本可以看出我们平时使用Categroy
的方式,对象方法,类方法,协议,和属性都可以找到对应的存储方式. 并且我们发现分类结构体中是不存在实例变量
的, 因此分类中是不允许添加实例变量
的. 在分类中添加property
, instanceProperties
中会添加一个property
, 不过分类中添加的property
并不会自动生成实例变量
, 也不会生成get
set
方法的声明, 需要我们自己去实现.
通过源码我们发现,分类的方法,协议,属性等好像确实是存放在Categroy
结构体里面的,那么他又是如何存储在类对象中的呢?
我们回到Runtime
的源码, 来分析分类的方法是如何添加到类的method list
中的:
所用到的runtime源码为objc4-680.tar.gz ,
runtime
的初始化函数在objc-os.mm
中.
void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;
// fixme defer initialization until an objc-using image is found?
environ_init();
tls_init();
static_init();
lock_init();
exception_init();
// Register for unmap first, in case some +load unmaps something
_dyld_register_func_for_remove_image(&unmap_image);
dyld_register_image_state_change_handler(dyld_image_state_bound,
1/*batch*/, &map_2_images);
dyld_register_image_state_change_handler(dyld_image_state_dependents_initialized, 0/*not batch*/, &load_images);
}
接着我们来到 &map_images
读取模块(images
这里代表资源模块),来到map_images_nolock
函数中找到_read_images
函数,在_read_images
函数中我们找到category
相关代码
在
objc-runtime-new.mm
中能找到相关源码
// Discover categories.
for (EACH_HEADER) {
category_t **catlist =
_getObjc2CategoryList(hi, &count);
for (i = 0; i < count; i++) {
category_t *cat = catlist[i];
Class cls = remapClass(cat->cls);
if (!cls) {
// Category's target class is missing (probably weak-linked).
// Disavow any knowledge of this category.
catlist[i] = nil;
if (PrintConnecting) {
_objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
"missing weak-linked target class",
cat->name, cat);
}
continue;
}
// Process this category.
// First, register the category with its target class.
// Then, rebuild the class's method lists (etc) if
// the class is realized.
bool classExists = NO;
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties)
{
addUnattachedCategoryForClass(cat, cls, hi);
if (cls->isRealized()) {
remethodizeClass(cls);
classExists = YES;
}
if (PrintConnecting) {
_objc_inform("CLASS: found category -%s(%s) %s",
cls->nameForLogging(), cat->name,
classExists ? "on existing class" : "");
}
}
if (cat->classMethods || cat->protocols
/* || cat->classProperties */)
{
addUnattachedCategoryForClass(cat, cls->ISA(), hi);
if (cls->ISA()->isRealized()) {
remethodizeClass(cls->ISA());
}
if (PrintConnecting) {
_objc_inform("CLASS: found category +%s(%s)",
cls->nameForLogging(), cat->name);
}
}
}
}
从上述代码中我们可以知道这段代码是用来查找有没有分类的. 通过_getObjc2CategoryLis
t函数获取到分类列表之后, 进行遍历,获取其中的方法,协议,属性等. 可以看到最终都调用了remethodizeClass(cls)
;函数. 我们来到remethodizeClass(cls)
函数内部查看.
在
objc-runtime-new.mm
中找到源码:
static void remethodizeClass(Class cls)
{
category_list *cats;
bool isMeta;
runtimeLock.assertWriting();
isMeta = cls->isMetaClass();
// Re-methodizing: check for more categories
if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
if (PrintConnecting) {
_objc_inform("CLASS: attaching categories to class '%s' %s",
cls->nameForLogging(), isMeta ? "(meta)" : "");
}
attachCategories(cls, cats, true /*flush caches*/);
free(cats);
}
}
通过上述代码我们发现attachCategories
函数最终接收了类对象cls
和分类数组cats
, 如我们一开始写的代码所示, 一个类可以有多个分类. 之前我们说到分类信息存储在category_t
结构体中, 那么多个分类则保存在category_list
中.
我们来看attachCategories
函数:
在
objc-runtime-new.mm
中能找到相关源码
// Attach method lists and properties and protocols from categories to a class.
//将类别中的方法列表,属性和协议附加到类。
// Assumes the categories in cats are all loaded and sorted by load order,
//假设cat中的类别都按加载顺序加载和排序,
// oldest categories first.
// 最老的类别最先进行
static void attachCategories(Class cls, category_list *cats, bool flush_caches)
{
if (!cats) return;
if (PrintReplacedMethods) printReplacements(cls, cats);
bool isMeta = cls->isMetaClass();
// fixme rearrange to remove these intermediate allocations
method_list_t **mlists = (method_list_t **)
malloc(cats->count * sizeof(*mlists));
property_list_t **proplists = (property_list_t **)
malloc(cats->count * sizeof(*proplists));
protocol_list_t **protolists = (protocol_list_t **)
malloc(cats->count * sizeof(*protolists));
// Count backwards through cats to get newest categories first
int mcount = 0;
int propcount = 0;
int protocount = 0;
int i = cats->count;
bool fromBundle = NO;
while (i--) {
auto& entry = cats->list[I];
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
mlists[mcount++] = mlist;
fromBundle |= entry.hi->isBundle();
}
property_list_t *proplist = entry.cat->propertiesForMeta(isMeta);
if (proplist) {
proplists[propcount++] = proplist;
}
protocol_list_t *protolist = entry.cat->protocols;
if (protolist) {
protolists[protocount++] = protolist;
}
}
auto rw = cls->data();
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
rw->methods.attachLists(mlists, mcount);
free(mlists);
if (flush_caches && mcount > 0) flushCaches(cls);
rw->properties.attachLists(proplists, propcount);
free(proplists);
rw->protocols.attachLists(protolists, protocount);
free(protolists);
}
从注释中我们可以看出这个函数的作用是用来将类别中的方法列表,属性和协议附加到类
. 以及它的顺序是最老的类别最先进行
.
我们继续看具体如何附加到类
,
来到attachLists()
函数:
在
objc-runtime-new.h
中能找到相关源码
void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;
if (hasArray()) {
// many lists -> many lists
uint32_t oldCount = array()->count;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
array()->count = newCount;
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
else if (!list && addedCount == 1) {
// 0 lists -> 1 list
list = addedLists[0];
}
else {
// 1 list -> many lists
List* oldList = list;
uint32_t oldCount = oldList ? 1 : 0;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)malloc(array_t::byteSize(newCount)));
array()->count = newCount;
if (oldList) array()->lists[addedCount] = oldList;
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
}
源码中有两个重要的数组:
array()->lists
: 类对象原来的方法列表,属性列表,协议列表.
addedLists
: 传入的所有分类的方法列表,属性列表,协议列表.
attachLists
函数中最重要的两个方法为memmove内存移动
和memcpy内存拷贝
. 我们先来分别看一下这两个函数:
// memmove :内存移动。
/* __dst : 移动内存的目的地
* __src : 被移动的内存首地址
* __len : 被移动的内存长度
* 将__src的内存移动__len块内存到__dst中
*/
void *memmove(void *__dst, const void *__src, size_t __len);
// memcpy :内存拷贝。
/* __dst : 拷贝内存的拷贝目的地
* __src : 被拷贝的内存首地址
* __n : 被移动的内存长度
* 将__src的内存移动__n块内存到__dst中
*/
void *memcpy(void *__dst, const void *__src, size_t __n);
下面我们图示经过memmove
和memcpy
方法过后的内存变化:
经过memmove
方法之后,内存变化为:
// array()->lists 原来方法、属性、协议列表数组
// addedCount 分类数组长度
// oldCount * sizeof(array()->lists[0]) 原来数组占据的空间
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
memmove后指针位置
经过memmove
方法之后,我们发现,虽然本类的方法,属性,协议列表会分别后移,但是本类的对应数组的指针依然指向原始位置. 这时候会进行memcopy
:
memcpy
方法之后,内存变化:
// array()->lists 原来方法、属性、协议列表数组
// addedLists 分类方法、属性、协议列表数组
// addedCount * sizeof(array()->lists[0]) 原来数组占据的空间
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
memcopy后指针位置
至此, 分类中的方法,属性,协议等被完全附加到本类上.
我们发现原来指针位置并没有改变, 至始至终指向开头的位置. 并且经过memmove
和memcpy
方法之后,分类的方法,属性,协议列表被放在了类对象中原本存储的方法,属性,协议列表前面. 这样做的目的, 是为了保证分类方法优先调用.
category总结:
Category
可以添加属性
. 但是runtime
不会生产对应的ivar
和setter
getter
方法. 不可以添加实例变量
?
Category
中添加的方法, 在runtime
初始化的时候, 会被附加到本类methodlist
中, 并且在本类方法前边, 所以消息发送过程和对象的消息发送过程相同.
Category
中添加了一个与本类中方法相同名字的方法, 为什么本类中的方法不会再调用? 是重写
还是被覆盖
? 既不是重写, 也不是覆盖, 而是在附加到本类methodlist
中的过程中被放在本类方法前边. 在消息发送的过程中, 会优先找到分类的方法,并且执行其IMP
, 所以本类方法不会再执行.
四: Runtime
在日常工作中的应用:
洋洋洒洒写了这么多, 接下来才是大家最关心的环节. Runtime
在日常工作中到底有什么应用呢?
- 关联对象(
Objective-C Associated Objects
)给分类增加属性
用来给
Category
添加属性.
- 方法魔法(Method Swizzling)方法添加和替换和KVO实现
class_addMethod()
method_exchangeImplementations
- 消息转发(热更新)解决Bug(JSPatch)
- 实现NSCoding的自动归档和自动解档
- 实现字典和模型的自动转换(MJExtension)