本文主要讲解runtime
相关知识,从原理到实践,由于包含内容过多分为以下五篇文章详细讲解,可自行选择需要了解的方向:
什么是runtime
runtime
就是运行时,在实际开发中使用runtime
的场景并不多,但是了解runtime
有助于我们更好的理解OC的原理,从而提高开发水平。runtime
很强大,是OC最重要的一部分也是OC最大的特色,可以不夸张的说runtime
成就了OC,尽管runtime
是OC的一个模块而已。
我们都知道高级编程语言想要成为可执行文件需要先编译为汇编语言再汇编为机器语言,机器语言也是计算机能够识别的唯一语言,但是OC并不能直接编译为汇编语言,而是要先转写为纯C语言再进行编译和汇编的操作,从OC到C语言的过渡就是由runtime
来实现的。然而我们使用OC进行面向对象开发,而C语言更多的是面向过程开发,这就需要将面向对象的类转变为面向过程的结构体,本文正是通过runtime
源码分析来讲解runtime
是如何将面向对象的类转变为面向过程的结构体。
深入代码理解instance、class object、metaclass
面向对象编程中,最重要的概念就是类,下面我们就从代码入手,看看OC是如何实现类的。
前文一直在说runtime
将面向对象的类转变为面向过程的结构体,那这个结构体到底是什么样子的?打开#import<objc/objc.h>
文件,可以发现以下几行代码
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
/// 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;
通过注释和代码不难发现,我们创建的一个对象或实例其实就是一个struct objc_object
结构体,而我们常用的id
也就是这个结构体的指针。有如下代码:
//以下两种写法都成立
id str = [[NSString alloc] init];
NSString *str = [[NSString alloc] init];
通过上述代码可以看出,我们创建的NSString类
的实例str
其实就是一个struct objc_object
结构体指针,所以不管是Foundation
框架中的类或是自定义的类,我们创建的类的实例最终获取的都是一个结构体指针,这个结构体只有一个成员变量就是Class
类型的isa
指针,Class
是结构体指针,指向struct objc_class
,那这个结构体又是什么呢?这里先透露一句话str is a NSString
,再加上Class
这个指针的名字,我们不难猜测,Class
就是代表NSString
这个类。
接下来会详细讲解这个结构体,现在再看另一个例子,有时我们也会通过下述方法来创建一个实例:
NSString *str = [[NSString alloc] initWithString: @"Hello World"];
Class c = [str class];
NSString *str2 = [[c alloc] initWithString: @"Hello World"];
可能你已经发现了,通过实例对象调用的class
方法,我们能够获取到一个Class
类型的变量,我们可以通过这个Class
来创建相应的实例对象。
实际上,OC中的类也是一个对象,称为类对象
,上述方法中通过[str class]
方法获取到的就是NSString类
的类对象
,接着我们就可以通过这个类对象
来创建实例对象,那这个类对象
又是什么东西呢?打开#import<objc/runtime.h>
文件,我们可以找到结构体struct objc_class
的定义,该结构体定义如下:
文件objc/runtime.h中有如下定义:
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
Class super_class
const char *name
long version
long info
long instance_size
struct objc_ivar_list *ivars
struct objc_method_list **methodLists
struct objc_cache *cache
struct objc_protocol_list *protocols
}
/* Use `Class` instead of `struct objc_class *` */
文件objc/objc.h文件中有如下定义
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
/// 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;
struct objc_class
结构体定义了很多变量,通过命名不难发现,结构体里保存了指向父类的指针、类的名字、版本、实例大小、实例变量列表、方法列表、缓存、遵守的协议列表等,一个类包含的信息也不就正是这些吗?没错,类对象
就是一个结构体struct objc_class
,这个结构体存放的数据称为元数据(metadata)
,该结构体的第一个成员变量也是isa
指针,这就说明了Class
本身其实也是一个对象,因此我们称之为类对象
,类对象
在编译期产生用于创建实例对象,是单例。
类对象
中的元数据
存储的都是如何创建一个实例的相关信息,那么类对象
和类方法
应该从哪里创建呢?就是从isa
指针指向的结构体创建,类对象
的isa
指针指向的我们称之为元类(metaclass)
,元类
中保存了创建类对象
以及类方法
所需的所有信息,因此整个结构应该如下图所示:
![](http://upload-images.jianshu.io/upload_images/3132379-113ee36be069d16b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/614)
通过上图我们可以清晰的看出来一个实例对象也就是struct objc_object
结构体它的isa
指针指向类对象
,类对象
的isa
指针指向了元类,super_class
指针指向了父类的类对象
,而元类
的super_class
指针指向了父类的元类
,那元类
的isa
指针又指向了什么?为了更清晰的表达直接使用一个大神画的图。
![](http://upload-images.jianshu.io/upload_images/3132379-1805a41a9b43a8c7.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/522)
通过上图我们可以看出整个体系构成了一个自闭环,如果是从NSObject
中继承而来的上图中的Root class
就是NSObject
。至此,整个实例
、类对象
、元类
的概念也就讲清了,接下来我们在代码中看看这些概念该怎么应用。
@interface Person : NSObject
@property (nonatomic, copy) NSString* name;
@property (nonatomic, assign) NSUInteger age;
@end
@implementation Person
@synthesize name = _name;
@synthesize age = _age;
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [[Person alloc] init];
Class c1 = [p class];
Class c2 = [Person class];
//输出 1
NSLog(@"%d", c1 == c2);
}
return 0;
}
c1
是通过一个实例对象获取的Class
,实例对象可以获取到其类对象
,类名作为消息的接受者时代表的是类对象
,因此类对象获取Class
得到的是其本身,同时也印证了类对象
是一个单例的想法。
那么如果我们想获取isa
指针的指向对象呢?
介绍两个函数
OBJC_EXPORT BOOL class_isMetaClass(Class cls)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
OBJC_EXPORT Class object_getClass(id obj)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
class_isMetaClass
用于判断Class
对象是否为元类
,object_getClass
用于获取对象的isa
指针指向的对象。
再看如下代码:
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [[Person alloc] init];
//输出1
NSLog(@"%d", [p class] == object_getClass(p));
//输出0
NSLog(@"%d", class_isMetaClass(object_getClass(p)));
//输出1
NSLog(@"%d", class_isMetaClass(object_getClass([Person class])));
//输出0
NSLog(@"%d", object_getClass(p) == object_getClass([Person class]));
}
return 0;
}
通过代码可以看出,一个实例对象通过class
方法获取的Class
就是它的isa
指针指向的类对象
,而类对象
不是元类
,类对象
的isa
指针指向的对象是元类
。
总结
通过上文的代码分析,我们已经了解了OC中的类和实例是如何映射到C语言结构体的,实例对象是一个结构体,这个结构体只有一个成员变量,指向构造它的那个类对象,这个类对象中存储了一切实例对象需要的信息包括实例变量、实例方法等,而类对象是通过元类创建的,元类中保存了类变量和类方法,这样就完美解释了整个类和实例是如何映射到结构体的
你不知道的msg_send
我们知道在OC中的实例对象调用一个方法称作消息传递
,比如有如下代码:
NSMutableString *str = [[NSMutableString alloc] initWithString: @"Jiaming Chen"];
[str appendString:@" is a good guy."];
上述代码中的第二句str
称为消息的接受者,appendString:
称作选择子
也就是我们常用的selector
,selector
和参数
共同构成了消息
,所以第二句话可以理解为将消息:"增加一个字符串: is a good guy"
发送给消息的接受者str
。
OC中里的消息传递
采用动态绑定机制来决定具体调用哪个方法,OC的实例方法在转写为C语言后实际就是一个函数,但是OC并不是在编译期决定调用哪个函数,而是在运行期决定,因为编译期根本不能确定最终会调用哪个函数,这是由于运行期可以修改方法的实现,在后文会有讲解。举个栗子,有如下代码:
id num = @123;
//输出123
NSLog(@"%@", num);
//程序崩溃,报错[__NSCFNumber appendString:]: unrecognized selector sent to instance 0x7b27
[num appendString:@"Hello World"];
上述代码在编译期没有任何问题,因为id
类型可以指向任何类型的实例对象,NSString
有一个方法appendString:
,在编译期不确定这个num
到底具体指代什么类型的实例对象,并且在运行期还可以给NSNumber
类型添加新的方法,因此编译期发现有appendString:
的函数声明就不会报错,但在运行时找不到在NSNumber
类中找不到appendString:
方法,就会报错。这也就是消息传递的强大之处和弊端,编译期无法检查到未定义的方法,运行期可以添加新的方法。
讲了这么多OC究竟是怎么将实例方法转换为C语言的函数,又是如何调用这些函数的呢?这些都依靠强大的runtime
。
在深入代码之前介绍一个clang
编译器的命令:
clang -rewrite-objc main.m
该命令可以将.m的OC文件转写为.cpp文件
有如下代码:
@interface Person : NSObject
@property (nonatomic, copy) NSString* name;
@property (nonatomic, assign) NSUInteger age;
- (void)showMyself;
@end
@implementation Person
@synthesize name = _name;
@synthesize age = _age;
- (void)showMyself {
NSLog(@"My name is %@ I am %ld years old.", self.name, self.age);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
//为了方便查看转写后的C语言代码,将alloc和init分两步完成
Person *p = [Person alloc];
p = [p init];
p.name = @"Jiaming Chen";
[p showMyself];
}
return 0;
}
通过上述clang
命令可以转写代码,然后找到如下定义:
static NSString * _I_Person_name(Person * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_Person$_name)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
static void _I_Person_setName_(Person * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Person, _name), (id)name, 0, 1); }
// @synthesize age = _age;
static NSUInteger _I_Person_age(Person * self, SEL _cmd) { return (*(NSUInteger *)((char *)self + OBJC_IVAR_$_Person$_age)); }
static void _I_Person_setAge_(Person * self, SEL _cmd, NSUInteger age) { (*(NSUInteger *)((char *)self + OBJC_IVAR_$_Person$_age)) = age; }
static void _I_Person_showMyself(Person * self, SEL _cmd) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_1f_dz4kq57d4b19s4tfmds1mysh0000gn_T_main_f5b408_mi_0, ((NSString *(*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("name")), ((NSUInteger (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("age")));
}
// @end
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
Person *p = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc"));
p = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("init"));
((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)p, sel_registerName("setName:"), (NSString *)&__NSConstantStringImpl__var_folders_1f_dz4kq57d4b19s4tfmds1mysh0000gn_T_main_f5b408_mi_1);
((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("showMyself"));
}
return 0;
}
关于属性property
生成的getter
、setter
和实例变量相关代码在另一篇博客iOS @property探究(二): 深入理解中有详细介绍,本文不再赘述,本文仅针对自定义的方法来讲解。
可以发现转写后的C语言代码将实例方法转写为了一个静态函数。接下来一行一行的分析上述代码,第一行代码可以简要表示为如下代码:
Person *p = objc_msgSend(objc_getClass("Person"), sel_registerName("alloc"));
这一行代码做了三件事情,第一获取Person
类,第二注册alloc
方法,第三发送消息,将消息alloc
发送给类对象,可以简单的将注册方法理解为,通过方法名获取到转写后C语言函数的函数指针。
第二行代码就可以简写为如下代码:
p = objc_msgSend(p, sel_registerName("init"));
这一行代码与上一行类似,注册了init
方法,然后通过objc_msgSend
函数将消息init
发送给消息的接受者p
。
第三行是一个对setter
的调用,同样的也可以简写为如下代码:
//这一行是用来查找参数的地址,取名为name
(NSString *)&__NSConstantStringImpl__var_folders_1f_dz4kq57d4b19s4tfmds1mysh0000gn_T_main_f5b408_mi_1)
objc_msgSend(p, sel_registerName("setName:"), name);
这一行代码同样是先注册方法setName:
然后通过objc_msgSend
函数将消息setName:
发送给消息的接收者,只是多了一个参数的传递。
同理,最后一行代码也可以简写为如下:
objc_msgSend(p, sel_registerName("showMyself"));
解释与上述相同,不再赘述。
到这里,我们应该就可以看出OC的runtime
通过objc_msgSend
函数将一个面向对象的消息传递转为了面向过程的函数调用。objc_msgSend
函数根据消息的接受者和selector
选择适当的方法来调用,那它又是如何选择的呢?这就涉及到前一篇博客讲解的内容iOS runtime探究(一): 从runtime开始: 理解面向对象的类到面向过程的结构体,这一篇博客中详细讲解了OC的runtime
是如何将面向对象的类映射为面向过程的结构体的,再来回顾一下几个主要的结构体:
文件objc/runtime.h中有如下定义:
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
Class super_class
const char *name
long version
long info
long instance_size
struct objc_ivar_list *ivars
struct objc_method_list **methodLists
struct objc_cache *cache
struct objc_protocol_list *protocols
}
/* Use `Class` instead of `struct objc_class *` */
文件objc/objc.h文件中有如下定义
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
/// 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;
注意结构体struct objc_class
中包含一个成员变量struct objc_method_list **methodLists
,通过名称我们分析出这个成员变量保存了实例方法列表,继续查找结构体struct objc_method_list
的定义如下:
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[5];
} _OBJC_$_INSTANCE_METHODS_Person __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
5,
{{(struct objc_selector *)"showMyself", "v16@0:8", (void *)_I_Person_showMyself},
{(struct objc_selector *)"name", "@16@0:8", (void *)_I_Person_name},
{(struct objc_selector *)"setName:", "v24@0:8@16", (void *)_I_Person_setName_},
{(struct objc_selector *)"age", "Q16@0:8", (void *)_I_Person_age},
{(struct objc_selector *)"setAge:", "v24@0:8Q16", (void *)_I_Person_setAge_}}
};
struct _objc_method {
struct objc_selector * _cmd;
const char *method_type;
void *_imp;
};
我们发现struct objc_method_list
中还包含了一个未知的结构体struct _objc_method
同时也找到它的定义,为了方便查看将两者写在一起。
结构体struct objc_method_list
里面包含以下几个成员变量:结构体struct _objc_method
的大小、方法个数以及最重要的方法列表,方法列表存储的是方法描述结构体struct _objc_method
,该结构体里保存了选择子、方法类型以及方法的具体实现。可以看出方法的具体实现就是一个函数指针,也就是我们自定义的实例方法,选择子也就是selector
可以理解为是一个字符串类型的名称,用于查找对应的函数实现(由于苹果没有开源selector的相关代码,但是可以查到GNU OC中关于selector的定义,也是一个结构体但是结构体里存储的就是一个字符串类型的名称)。
这样就能解释objc_msgSend
的工作原理的,为了匹配消息的接收者和选择子,需要在消息的接收者所在的类中去搜索这个struct objc_method_list
方法列表,如果能找到就可以直接跳转到相关的具体实现中去调用,如果找不到,那就会通过super_class
指针沿着继承树向上去搜索,如果找到就跳转,如果到了继承树的根部(通常为NSObject)还没有找到,那就会调用NSObjec
的一个方法doesNotRecognizeSelector:
,这个方法就会报unrecognized selector
错误(其实在调用这个方法之前还会进行消息转发,还有三次机会来处理,消息转发在后文会有介绍)。
这样一看,要发送消息真的好复杂,需要经过这么多步骤,难道不会影响性能吗?当然了,这样一次次搜索和静态绑定那样直接跳转到函数指针指向的位置去执行来比肯定是耗时很多的,因此,类对象也就是结构体struct objc_class
中有一个成员变量struct objc_cache
,这个缓存里缓存的正是搜索方法的匹配结果,这样在第二次及以后再访问时就可以采用映射的方式找到相关实现的具体位置。
到这里我们就已经弄清楚了整个发送消息的过程,但是当对象无法接收相关消息时又会发生什么?以及前文说的三次机会又是什么?下文将会介绍消息转发。
消息转发: unrecognized selector的最后三次机会
还是那个栗子:
id num = @123;
//输出123
NSLog(@"%@", num);
//程序崩溃,报错[__NSCFNumber appendString:]: unrecognized selector sent to instance 0x7b27
[num appendString:@"Hello World"];
前文介绍了进行一次发送消息会在相关的类对象中搜索方法列表,如果找不到则会沿着继承树向上一直搜索知道继承树根部(通常为NSObject),如果还是找不到并且消息转发都失败了就回执行doesNotRecognizeSelector:
方法报unrecognized selector
错。那么消息转发到底是什么呢?接下来将会逐一介绍最后的三次机会。
第一次机会: 所属类动态方法解析
首先,如果沿继承树没有搜索到相关方法则会向接收者所属的类进行一次请求,看是否能够动态的添加一个方法,注意这是一个类方法,因为是向接收者所属的类进行请求。
+(BOOL)resolveInstanceMethod:(SEL)name
举个栗子吧:
@interface Person : NSObject
@property (nonatomic, copy) NSString* name;
@property (nonatomic, assign) NSUInteger age;
@end
@implementation Person
@synthesize name = _name;
@synthesize age = _age;
//如果需要传参直接在参数列表后面添加就好了
void dynamicAdditionMethodIMP(id self, SEL _cmd) {
NSLog(@"dynamicAdditionMethodIMP");
}
+ (BOOL)resolveInstanceMethod:(SEL)name {
NSLog(@"resolveInstanceMethod: %@", NSStringFromSelector(name));
if (name == @selector(appendString:)) {
class_addMethod([self class], name, (IMP)dynamicAdditionMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:name];
}
+ (BOOL)resolveClassMethod:(SEL)name {
NSLog(@"resolveClassMethod %@", NSStringFromSelector(name));
return [super resolveClassMethod:name];
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
id p = [[Person alloc] init];
[p appendString:@""];
}
return 0;
}
先看一下最后的输出结果吧:
2017-03-24 19:05:25.092404 OCTest[5142:1185077] resolveInstanceMethod: appendString:
2017-03-24 19:05:25.092810 OCTest[5142:1185077] dynamicAdditionMethodIMP
先看一下main函数,首先创建了一个Person
的实例对象,一定要用id
类型来声明,否则会在编译期就报错,因为找不到相关函数的声明,id
类型由于可以指向任何类型的对象,因此编译时能够找到NSString
类的相关方法声明就不会报错。
由于Person
类没有声明和定义appendString:
方法,所以运行时应该会报unrecognized selector
错误,但是并没有,因为我们重写了类方法+ (BOOL)resolveInstanceMethod:(SEL)name
,当找不到相关实例方法的时候就会调用该类方法去询问是否可以动态添加,如果返回True
就会再次执行相关方法,接下来看一下如何给一个类动态添加一个方法,那就是调用runtime
库中的class_addMethod
方法,该方法的原型是
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);
通过参数名可以看出第一个参数是需要添加方法的类,第二个参数是一个selector
,也就是实例方法的名字,第三个参数是一个IMP
类型的变量也就是函数实现,需要传入一个C函数,这个函数至少有两个参数,一个是id self
一个是SEL _cmd
,第四个参数是函数类型。具体设置方法可以看注释。
第二次机会: 备援接收者
当对象所属类不能动态添加方法后,runtime
就会询问当前的接受者是否有其他对象可以处理这个未知的selector
,相关方法声明如下:
- (id)forwardingTargetForSelector:(SEL)aSelector;
该方法的参数就是那个未知的selector
,这是一个实例方法,因为是询问该实例对象是否有其他实例对象可以接收这个未知的selector
,如果没有就返回nil
,可以自行实验。
第三次机会: 消息重定向
当没有备援接收者时,就只剩下最后一次机会,那就是消息重定向。这个时候runtime
会将未知消息的所有细节都封装为NSInvocation
对象,然后调用下述方法:
- (void)forwardInvocation: (NSInvocation*)invocation;
调用这个方法如果不能处理就会调用父类的相关方法,一直到NSObject
的这个方法,如果NSObject
都无法处理就会调用doesNotRecognizeSelector:
方法抛出异常。
整个消息转发流程如下图所示:
![](http://upload-images.jianshu.io/upload_images/3132379-658e5bfa9f93dc9d.jpeg?imageMogr2/auto-orient/strip%7CimageView2/2/w/700)
总结
本文通过对runtime
的分析,详细解释了整个发送消息和消息转发的流程,对OC的runtime
能有一个更清晰的掌握。
关联对象 Associated Object
如果我们想为系统的类添加一个方法可以采用类别的方式进行扩展,相对来说比较简单,但如果要添加一个属性或称为成员变量,通常采用的方法就是继承,这样就比较繁琐了,如果不想去继承那就可以通过runtime
来进行关联对象操作。
使用runtime
的关联对象
添加属性与我们自定义类时定义的属性其实是两个不同的概念,通过关联对象
添加属性本质上是使用类别
进行扩展,通过添加setter
和getter
方法从而在访问时可以使用点语法进行方法,在使用上与自定义类定义的属性没有区别。
具体需要使用的C函数如下:
//为一个实例对象添加一个关联对象,由于是C函数只能使用C字符串,这个key就是关联对象的名称,value为具体的关联对象的值,policy为关联对象策略,与我们自定义属性时设置的修饰符类似
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
//通过key和实例对象获取关联对象的值
id objc_getAssociatedObject(id object, const void *key);
//删除实例对象的关联对象
void objc_removeAssociatedObjects(id object);
通过注释和函数名不难发现上诉三个方法分别是设置关联对象、获取关联对象和删除关联对象。
需要说明一下objc_AssociationPolicy
,具体的定义如下:
/**
* Policies related to associative references.
* These are options to objc_setAssociatedObject()
*/
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0, /**< Specifies a weak reference to the associated object. */
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object.
* The association is not made atomically. */
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, /**< Specifies that the associated object is copied.
* The association is not made atomically. */
OBJC_ASSOCIATION_RETAIN = 01401, /**< Specifies a strong reference to the associated object.
* The association is made atomically. */
OBJC_ASSOCIATION_COPY = 01403 /**< Specifies that the associated object is copied.
* The association is made atomically. */
};
这些关键词很眼熟,没错,就是property
使用的修饰符,具体含义也与property修饰符
相同,如果对property
或property修饰符
等有疑问可以查阅本系列教程第三篇文章从runtime开始: 理解OC的属性property或本博客另外两篇关于property
的讲解文章:iOS @property探究(一): 基础详解、iOS @property探究(二): 深入理解。
说了这么多,接下来举个具体的栗子,为一个已有类添加一个关联对象。
@interface Person : NSObject
@property (nonatomic, copy) NSString* cjmName;
@property (nonatomic, assign) NSUInteger cjmAge;
@end
@implementation Person
@synthesize cjmName = _cjmName;
@synthesize cjmAge = _cjmAge;
@end
@interface NSArray (MyPerson)
- (void)setPerson:(Person*)person;
- (Person*)person;
@end
@implementation NSArray (MyPerson)
- (void)setPerson:(Person *)person {
objc_setAssociatedObject(self, "_person", person, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (Person*)person {
return objc_getAssociatedObject(self, "_person");
}
@end
这个栗子设置的关联对象其实没有任何实际意义,通过代码可以看出,使用runtime
为一个已有类添加属性就是通过类别扩展getter
和setter
方法。
实例方法
在本系列文章的第二篇iOS runtime探究(二): 从runtime开始深入理解OC消息转发机制,我们详细介绍了runtime
对方法的底层处理,以及发送消息和消息转发机制,这里就不再赘述了,如有需要可以查看相关文章,本文会介绍OC层面对方法的相关操作,同时会介绍method swizzling
的方法。
先来回顾一下实例方法相关的结构体和底层实现,有如下代码:
@interface Person : NSObject
@property (nonatomic, copy) NSString* name;
@property (nonatomic, assign) NSUInteger age;
- (instancetype)initWithName:(NSString*)name age:(NSUInteger)age;
- (void)showMyself;
- (void)helloWorld;
@end
@implementation Person
@synthesize name = _name;
@synthesize age = _age;
- (instancetype)initWithName:(NSString*)name age:(NSUInteger)age {
if (self = [super init]) {
self.name = name;
self.age = age;
}
return self;
}
- (void)showMyself {
NSLog(@"Hello World, My name is %@ I\'m %ld years old.", self.name, self.age);
}
- (void)helloWorld {
NSLog(@"Hello World");
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [[Person alloc] initWithName:@"Jiaming Chen" age:22];
[p showMyself];
unsigned int count = 0;
Method *methodList = class_copyMethodList([p class], &count);
for (int i = 0; i < count; i++) {
SEL s = method_getName(methodList[i]);
NSLog(@"%@", NSStringFromSelector(s));
if ([NSStringFromSelector(s) isEqualToString:@"helloWorld"]) {
IMP imp = method_getImplementation(methodList[i]);
imp();
}
}
}
return 0;
}
通过clang
转写后可以找到如下与实例方法相关的定义:
struct _objc_method {
struct objc_selector * _cmd;
const char *method_type;
void *_imp;
};
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[7];
} _OBJC_$_INSTANCE_METHODS_Person __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
7,
{{(struct objc_selector *)"initWithName:age:", "@32@0:8@16Q24", (void *)_I_Person_initWithName_age_},
{(struct objc_selector *)"showMyself", "v16@0:8", (void *)_I_Person_showMyself},
{(struct objc_selector *)"helloWorld", "v16@0:8", (void *)_I_Person_helloWorld},
{(struct objc_selector *)"name", "@16@0:8", (void *)_I_Person_name},
{(struct objc_selector *)"setName:", "v24@0:8@16", (void *)_I_Person_setName_},
{(struct objc_selector *)"age", "Q16@0:8", (void *)_I_Person_age},
{(struct objc_selector *)"setAge:", "v24@0:8Q16", (void *)_I_Person_setAge_}}
};
上一篇文章iOS runtime探究(二): 从runtime开始深入理解OC消息转发机制已经详细介绍了上述结构体,这里不再赘述了。
通过上述代码可以看出,一个实例方法在底层就是一个方法描述和一个C函数的具体实现,我们可以通过如下代码获取这个方法描述结构体:
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [[Person alloc] initWithName:@"Jiaming Chen" age:22];
unsigned int count = 0;
Method *methodList = class_copyMethodList([p class], &count);
for (int i = 0; i < count; i++) {
SEL s = method_getName(methodList[i]);
NSLog(@"%@ %s", NSStringFromSelector(s), method_getTypeEncoding(methodList[i]));
if ([NSStringFromSelector(s) isEqualToString:@"helloWorld"]) {
IMP imp = method_getImplementation(methodList[i]);
imp();
}
}
}
return 0;
}
首先看一下Method
是什么,在objc/runtime.h
中可以找到相关定义:
typedef struct objc_method *Method;
它是一个指向结构体struct objc_method
的指针,这里的结构体struct objc_method
其实就是前文中.cpp
文件中的struct _objc_method
结构体,通过class_copyMethodList
方法就可以获取到相关类的所有实例方法,具体函数声明如下:
/**
* Describes the instance methods implemented by a class.
*
* @param cls The class you want to inspect.
* @param outCount On return, contains the length of the returned array.
* If outCount is NULL, the length is not returned.
*
* @return An array of pointers of type Method describing the instance methods
* implemented by the class—any instance methods implemented by superclasses are not included.
* The array contains *outCount pointers followed by a NULL terminator. You must free the array with free().
*
* If cls implements no instance methods, or cls is Nil, returns NULL and *outCount is 0.
*
* @note To get the class methods of a class, use \c class_copyMethodList(object_getClass(cls), &count).
* @note To get the implementations of methods that may be implemented by superclasses,
* use \c class_getInstanceMethod or \c class_getClassMethod.
*/
OBJC_EXPORT Method *class_copyMethodList(Class cls, unsigned int *outCount)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
通过注释可以看出,第一个参数是相关类的类对象(如有疑问可以查阅本系列文章的前两篇文章),第二个参数是一个指向unsigned int
的指针,用于指明Method
的数量,通过该方法就能够获取到所有的实例方法,接下来可以通过method_getName
方法获取成员变量_cmd
,这是一个选择子selector
可以通过方法NSStringFromSelector
获取到实例方法的名称。通过方法method_getTypeEncoding
就可以获得函数类型method_type
。通过方法method_getImplementation
就可以获取到实例方法的具体实现imp
,这个具体实现就是我们自定义的实例方法的一个C函数,因此,如果该方法内不访问任何其他实例变量并且没有任何参数就可以直接执行该函数。
上述代码的输出结果如下:
2017-03-27 12:36:12.342715 OCTest[4135:952839] initWithName:age: @32@0:8@16Q24
2017-03-27 12:36:12.342795 OCTest[4135:952839] showMyself v16@0:8
2017-03-27 12:36:12.342843 OCTest[4135:952839] helloWorld v16@0:8
2017-03-27 12:36:12.342866 OCTest[4135:952839] Hello World
2017-03-27 12:36:12.342884 OCTest[4135:952839] .cxx_destruct v16@0:8
2017-03-27 12:36:12.342911 OCTest[4135:952839] name @16@0:8
2017-03-27 12:36:12.342929 OCTest[4135:952839] setName: v24@0:8@16
2017-03-27 12:36:12.342951 OCTest[4135:952839] age Q16@0:8
2017-03-27 12:36:12.342966 OCTest[4135:952839] setAge: v24@0:8Q16
我们也可以通过class_addMethod
函数动态的为一个类添加实例方法,具体的栗子可以查看前文从runtime开始: 深入理解OC消息转发机制这里不再赘述。
Method Swizzling
通过前面的介绍,我们知道一个实例方法在底层就是一个方法描述加上方法类型和具体的C函数实现,Foundation
等框架都是闭源的,我们没有办法直接修改代码,通常情况下可以通过继承、类别、关联属性等手段添加属性或实例方法,在某些情况下通过上述方法实现的代码还是比较复杂或繁琐。接下来本文将介绍一种方法用于交换两个实例方法的实现,从而达到修改闭源代码的效果,这个方法就是Method Swizzling
。
Method Swizzling
方法的本质就是修改前文介绍的方法描述结构体,方法描述结构体struct _objc_method
中有一个struct objc_selector
类型的成员变量_cmd
,这就是我们常用的selector
选择子,同时也有一个函数指针_imp
,这个函数指针就指向实例方法的具体实现。了解了这些我们就可以手动修改selector
对应的_imp
,也就是修改实例方法的具体实现,下面举个栗子:
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [[Person alloc] initWithName:@"Jiaming Chen" age:22];
Method method1 = class_getInstanceMethod([p class], @selector(helloWorld));
Method method2 = class_getInstanceMethod([p class], @selector(showMyself));
method_exchangeImplementations(method1, method2);
[p showMyself];
[p helloWorld];
}
return 0;
}
上述代码使用了一个C函数:
/**
* Exchanges the implementations of two methods.
*
* @param m1 Method to exchange with second method.
* @param m2 Method to exchange with first method.
*
* @note This is an atomic version of the following:
* \code
* IMP imp1 = method_getImplementation(m1);
* IMP imp2 = method_getImplementation(m2);
* method_setImplementation(m1, imp2);
* method_setImplementation(m2, imp1);
* \endcode
*/
OBJC_EXPORT void method_exchangeImplementations(Method m1, Method m2)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
通过注释和函数名称不难发现,该函数用于交换两个方法的实现,也就是说前文讲述的结构体struct _objc_method
中的函数指针_imp
被交换了,原来的选择子@selector(helloWorld)
对应着方法helloWorld
的实现,原来的选择子@selector(showMyself)
对应着方法showMyself
的实现。如下图所示:
通过上述方法将两个结构体的
_imp
成员变量进行了一次交换操作,也就是说选择子@selector(helloWorld)
对应着方法showMyself
的实现,而选择子@selector(showMyself)
对应着方法helloWorld
的实现,如下图所示:
![](http://upload-images.jianshu.io/upload_images/3132379-87cc45427fb0858e.jpeg?imageMogr2/auto-orient/strip%7CimageView2/2/w/517)
因此上述代码的输出结果如下:
2017-03-27 15:35:54.077598 OCTest[6061:1472928] Hello World
2017-03-27 15:35:54.077853 OCTest[6061:1472928] Hello World, My name is Jiaming Chen I'm 22 years old.
runtime
强大到可以改变一个实例方法的具体实现,但是上面的例子好像并没有什么用,没有人会闲的没事去交换两个实例方法的实现。
考虑一个需求,现在需要为每一个页面添加一个手势用于执行某项固定操作,比如添加一个长按收拾,用户可以在任意界面长按后弹出一个视图或是执行某项操作,又比如需要统计每个视图打开的次数,你可能会想到在每一个的视图控制器的viewDidLoad
方法中添加这个手势或在viewDidAppear
方法中进行统计操作,但是这样太繁琐了。你也可能想到通过继承来实现上述方法,但是你就需要继承UIViewController
、UITableViewController
、UINavigationController
等,你在代码中使用过的任意视图控制器,这样一看似乎也挺麻烦的而且代码也不统一。
通过前面的学习我们可以通过使用类别加上Method Swizzling
来实现在不修改使用方式的前提下执行自定义操作了。
具体栗子如下:
@interface UIViewController (MyUIViewController)
@end
@implementation UIViewController(MyUIViewController)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL originalSelector = @selector(viewWillAppear:);
Method originalMethod = class_getInstanceMethod([self class], originalSelector);
SEL exchangeSelector = @selector(myViewWillAppear:);
Method exchangeMethod = class_getInstanceMethod([self class], exchangeSelector);
method_exchangeImplementations(originalMethod, exchangeMethod);
});
}
- (void)myViewWillAppear:(BOOL)animated {
[self myViewWillAppear:animated];
NSLog(@"MyViewWillAppear %@", self);
}
@end
首先需要使用类方法load
来进行实例方法实现的交换操作,因为load
方法会保证在类第一次被加载的时候调用,这样可以保证一定会执行方法交换操作。其次使用GCD
的dispatch_once
来保证交换两个实例方法的实现只进行一次。接下来通过前文介绍的方法来获取自定义的myViewWillAppear:
以及UIViewController
的选择子和具体的方法描述结构体,最后调用前文介绍的method_exchangeImplementations
函数将两个实例方法的实现进行交换就可以了。
可能你看到myViewWillAppear:
方法会有疑问,这样不就会导致递归调用吗?需要注意的是,交换两个方法的实现是在运行时进行的,当你调用myViewWillAppear:
方法时,实际会执行viewWillAppear:
的方法实现,因此不会导致递归调用。
weak
weak
不论是用作property
修饰符还是用来修饰一个变量的声明其作用是一样的,就是不增加新对象的引用计数,被释放时也不会减少新对象的引用计数,同时在新对象被销毁时,weak
修饰的属性或变量均会被设置为nil
,这样可以防止野指针错误,本文要讲解的也正是这个特性,runtime
如何将weak
修饰的变量的对象在销毁时自动置为nil
。
那么runtime
是如何实现在weak
修饰的变量的对象在被销毁时自动置为nil
的呢?一个普遍的解释是:runtime
对注册的类会进行布局,对于weak
修饰的对象会放入一个hash表
中。用weak
指向的对象内存地址作为key
,当此对象的引用计数为0的时候会dealloc
,假如weak
指向的对象内存地址是a
,那么就会以a
为键在这个weak表
中搜索,找到所有以a
为键的weak
对象,从而设置为nil
。
了解了以上知识后就可以深入runtiem
代码来看看具体实现细节,有兴趣的读者可以继续阅读。
深入runtime理解weak
这部分内容参考《Objective-C高级编程:iOS与OS X多线程和内存管理》,可以看出具体的实现方式就是使用了一个HashTable
。
NSString *name = [[NSString alloc] initWithString: @"Jiaming Chen"];
__weak NSString *weakStr = name;
当为weakStr
这一weak
类型的对象赋值时,编译器会根据name
的地址为key去查找weak哈希表
,该表项的值为一个数组,将weakStr
对象的地址加入到数组中,当name
变量超出变量作用域或引用计数为0时,会执行dealloc
函数,在执行该函数时,编译器会以name
变量的地址去查找weak哈希表
的值,并将数组里所有 weak
对象全部赋值为nil。