runtime
运行时语言,实现Object-C的C语言库,将OC转换成C进行编译的过渡者。
作为一门动态编程语言,Objective-C 会尽可能的将编译和链接时要做的事情推迟到运行时。只要有可能,Objective-C 总是使用动态 的方式来解决问题。这意味着 Objective-C 语言不仅需要一个编译环境,同时也需要一个运行时系统来执行编译好的代码,这就是runtime。
运行时系统(runtime)扮演的角色类似于 Objective-C 语言的操作系统,Objective-C 基于该系统来工作。因此,runtime好比Objective-C的灵魂,很多东西都是在这个基础上出现的。所以它是值的你花功夫去理解的。
实现
主要是用C语言实现,部分由汇编语言。实际上正是runtime将OC中面向对象的类转换成C语言中面向过程的结构体。无论是实例对象还是类对象,实际上对应的都是C中的结构体。
使用
iskindofclass和isMemberOfClass就是两个典型的运行时方法。
同时注意使用class_addMethod等方法时要引入#import <objc/runtime.h>
头文件。
关于32位与64位
runtime分为早期版本和现行版本,32位使用早期,64位使用现行。32位已被淘汰。
64位与32位的区别在于,操作系统上64位用于3D动画等先进功能32位用于日常简单使用;处理器上64位比32位更宽处理能力更强;软件上基于32位和64位开发的不同;内存容量不同。现在必须支持arm64,代码中主要注意类型长度上的区别。ios推荐使用的NSInteger区别于int的地方也就是在于前者可以根据系统位数使用较大的长度。
与传统静态语言C语言的区别
很常见的一个消息发送语句:
[receiver message]
会被编译器转化成
objc_msgSend(receiver, selector)
如果有参数则为
objc_msgSend(receiver, selector, arg1, arg2, …)
receiver:消息接受者(receiver),self,某个类
selector:方法名(message)
arg:参数
传统的静态语言,编译器会自上而下,在存在调用函数时会进入调用函数,逐句实现编译。
而OC在编译过程中并不会这样做,只是编辑成明确方法名调用者参数的形式,该函数被如何实现以及是否实现并不关心。只有在程序运行时才会根据方法名进入具体函数中,这也就是会在运行中找不到对应函数时会造成崩溃的原因。
OC实际上是使用消息传递机制代替传统C语言的函数调用。
_CMD,SEL类型,可以获取当前方法名的关键字。
- (void)message
{
self.name = @"James";//通过self关键字给当前对象的属性赋值
SEL currentSel = _cmd;//通过_cmd关键字取到当前函数对应的SEL
NSLog(@"currentSel is :%s",(char *)currentSel);
}
打印结果
ObjcRunTime[693:403] currentSel is :message
初步理解,其实objc_msgSend 所做的事情,就是通过我们传入的self 指针,找到class 的method_list 然后根据SEL 做比较,没有的话,就在super class 找,如此往复。直到找到匹配的SEL,然后,call imp。当然,runtime在其中又做了很多优化。
OC中的消息传递
OC中的消息传递与C中函数调用的最大区别在于OC中的消息传递在时运行时中进行的,而函数调用是在编译时就可以进行的。
id num = @123;
//输出123
NSLog(@"%@", num);
//程序崩溃,报错[__NSCFNumber appendString:]: unrecognized selector sent to instance 0x7b27
[num appendString:@"Hello World"];
上述代码没有任何问题,id可以使任何类型包括NSString,但是在运行过程中在NSNumber中找不到appendString这个方法,所以会报错。所以消息传递的强大之处在于可以在运行时添加新方法,缺点在于无法再编译时发现错误。
OC中的消息传递转化为C的函数调用
简单的创建Person对象
//为了方便查看转写后的C语言代码,将alloc和init分两步完成
Person *p = [Person alloc];
p = [p init];
p.name = @"Jiaming Chen";
[p showMyself];
使用clang -rewrite-objc main.m
可以转化成C
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"));
第一行代码简要表示为
Person *p = objc_msgSend(objc_getClass("Person"), sel_registerName("alloc"));
第一步,获取Person类,第二步注册alloc方法,然后通过objc_msgSend将alloc消息发送给消息接受者Person。
第二行代码简要表示为
p = objc_msgSend(p, sel_registerName("init"));
由此可见,对于OC而言都是以消息传递实现,而正是runtime通过objc_msgSend将一个面向对象的消息传递转换成了一个面向过程的的函数调用。
消息转发
我们知道消息传递实际上做的事就是利用objc_msgSend,将消息传递给接受者,在接受者的结构体中的objc_method_list中查找方法。如果找到就可以直接调用,如果找不到就会通过super_class指针去它的父类中查找,直至基类NSObject。如果还是没有找到就会调用NSObject的doesNotRecognizeSelector报出unrecognized的错误。也就是我们常出现的unrecognized selector sent to instance
错误(对象提前被释放或者为空导致无法识别其方法)。
但是在此之前,还有三次机会来处理这个消息来避免出现崩溃,这就是所谓的消息转发。详细见后文。
runtime源码学习
- selector
selector可以叫做选择器,其实指的就是对象的方法,也可以理解为C语言里面的函数指针,面向对象里面的对应概念。
@selector(xxxx)的作用是找到名字为xxxx的方法。一般用于[a performSelector:@selector(b)];就是说去调用a对象的b方法,和[a b];的意思一样,但是这样更加动态一些。@selector(xxxx)返回的类型是SEL,看方法说明的时候如果参数类型是SEL,那么就是要接受@selector(xxxx)返回的值。
在Objc中 SEL的定义是:
typedef struct objc_selector *SEL;
我们注意到对于SEL的定义,实际上就是使用typedef将结构体objc_selector重新命名为*SEL。而SEL则是指向这个结构体的指针,所以SEL的对象不需要再加*。
在Mac OS X中SEL其实被映射为一个C字符串,可以看作是方法的名字(编号),它并不一个指向具体方法实现(IMP类型才是)。对于所有的类,只要方法名是相同的,产生的selector都是一样的。
简而言之,你可以理解 @selector()就是取类方法的编号,他的行为基本可以等同C语言的中函数指针,只不过C语言中,可以把函数名直接赋给一个函数指针,而Object-C的类不能直接应用函数指针,这样只能做一个@selector语法来取。
它的结果是一个SEL类型。这个类型本质是类方法的名字(编号)。
注意1. @selector是查找当前类的方法,而[object @selector(方法名:方法参数..) ] ;是取object对应类的相应方法.
注意2.查找类方法时,除了方法名,方法参数也查询条件之一.
注意3. 可以用字符串来找方法 SEL 变量名 = NSSelectorFromString(方法名字的字符串);
注意4. 可以运行中用SEL变量反向查出方法名字字符串
NSString *变量名 = NSStringFromSelector(SEL参数);
runtime 在实现selector时,实现了一个很大的Set,简单的说就是一个经过了优化过的hash表。而Set的特点就是唯一,也就是SEL是唯一的。那么对于字符串的比较仅仅需要比较他们的地址就可以了。 所以OC中不允许使用方法名一样参数不一样的函数,编译器会根据每个方法的方法名生成唯一的SEL。所以,在速度上是无与伦比的。
selector主要用于两个对象之间进行松耦合的通讯.这种方法很多开发环境用到。比如GTK,Delphi.基本上整个Cocoa库之间对象,控制之间通讯都是在这个基础构建的。
用户行为统计中的经典运用:
+ (void)setUpAnalytics
{
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//读取配置文件,获取需要统计的事件列表
for (NSString *classNameString in analyticsData.allKeys) {
//使用运行时创建类对象
const char *className = [classNameString UTF8String];
//从一个字串返回一个类
Class newClass = objc_getClass(className);
NSArray *pageEventList = [[analyticsData objectForKey:classNameString] objectForKey:Event];
for (NSDictionary *eventDict in pageEventList) {
//事件方法名称
NSString *eventMethodName = eventDict[MethodName];
SEL seletor = NSSelectorFromString(eventMethodName);
NSString *eventId = eventDict[EventId];
[weakSelf trackEventWithClass:newClass selector:seletor event:eventId];
[weakSelf uAnalyticstrackEventWithEventdata:eventDict];
}
}
});
}
- id
id是通用类型指针,能够表示任何对象,换句话说,id 类型的变量可以存放任何数据类型的对象。在内部处理上,这种类型被定义为指向对象的指针,实际上是一个指向这种对象的实例变量的指针。
查看到id数据结构如下:
// 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;
实际上*id就是结构体objc_object的别名。而id则是一个指向objc_object结构体指针,它包含一个Class isa成员,根据isa指针就可以顺藤摸瓜找到对象所属的类。需要注意的是id 是一个指针,所以在使用id的时候不需要加星号。
NSString *str = [[NSString alloc] init];
这里我们定义的对象str,实际上同样是一个objc_object结构体指针。
- Class
isa指针的数据类型是Class,Class表示对象所属的类。
// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
可以查看到Class其实就是一个objc_class结构体指针。class类型和id类型一样,本身继承于指针类型,用于储存类对象的指针,故在声明class类型变量时和id一样不需要*号。
NSString *str = [[NSString alloc] init];
Class c = [str Class];
这里我们定义的c,实际上同样是一个objc_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;
/* Use `Class` instead of `struct objc_class *` */
OC的类其实也是一个对象,即类对象,意思就是你可以向一个类发送消息。为了可以调用类方法,这个类的isa指针必须指向一个包含这些类方法的类结构体,也就是元类(meta-class)的概念。meta-class之所以重要,是因为它保存着创建类对象以及类方法所需的所有信息。每个类都会有一个单独的meta-class,因为每个类的类方法基本不可能完全相同。
任何NSObject继承体系下的meta-class都使用NSObject的meta-class作为自己所属的类。所有的meta-class使用基类的meta-class作为它们的父类,而基类的meta-class也是属于它自己,也就是说基类的meta-class的isa指针指向它自己。
上图实线是super_class指针,虚线是isa指针。有几个关键点需要解释以下:
1.Root class (class)其实就是NSObject,NSObject是没有超类的,所以Root class(class)的superclass指向nil。
2.每个Class都有一个isa指针指向唯一的Meta class
3.Root class(meta)的superclass指向Root class(class),也就是NSObject,形成一个回路。
4.每个Meta class的isa指针都指向Root class (meta)。
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;
}
由此可见,类对象是单例。对于Person这个类对象来说,只有一个,所有类对象均是如此。
介绍两个函数:
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;
}
由上看出,实例的isa指针指向类,类的isa指针指向元类。
super_class表示实例对象对应的父类;
name表示类名,我们可以在运行期,通过这个名称查找到该类(通过:id objc_getClass(const char *aClassName))或该类的 metaclass(id objc_getMetaClass(const char *aClassName));
ivars表示多个成员变量,它指向objc_ivar_list结构体。在runtime.h可以看到它的定义:
struct objc_ivar_list {
int ivar_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_ivar ivar_list[1] OBJC2_UNAVAILABLE;
}
objc_ivar_list其实就是一个链表,存储多个objc_ivar,而objc_ivar结构体存储类的单个成员变量信息。
(* struct objc_ivar ivar_list[1] 这个称为结构体数组,顾名思义,结构数组是指能够存放多个结构体类型(objc_ivar)的一种数组(ivar_list)形式。)
methodLists表示方法列表,它指向objc_method_list结构体的二级指针,可以动态修改methodLists的值来添加成员方法,也是Category实现原理,同样也解释Category不能添加属性的原因。
Category只能在运行时中添加属性。代码如下:
///例如可能是这样的使用
static const void *propertyKey = &propertyKey;
/// 将value通过运行时绑定到self
objc_setAssociatedObject(self, propertyKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
/// 将value在运行时中通过propertyKey取出绑定的值
id value = objc_getAssociatedObject(self, propertyKey);
在runtime.h可以看到它的定义
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;
}
同理,objc_method_list也是一个链表,存储多个objc_method,而objc_method结构体存储类的某个方法的信息。
cache用来缓存经常访问的方法,它指向objc_cache结构体,后面会重点讲到。
protocols表示类遵循哪些协议。
- Method
Method表示类中的某个方法,在runtime.h文件中找到它的定义:
/// 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;
}
其实Method就是一个指向objc_method结构体指针,它存储了方法名(method_name)、方法类型(method_types)和方法实现(method_imp)等信息。而method_imp的数据类型是IMP,它是一个函数指针,后面会重点提及。
- Ivar
Ivar表示类中的实例变量,在runtime.h文件中找到它的定义:
/// An opaque type that represents an instance variable.
typedef struct objc_ivar *Ivar;
struct objc_ivar {
char *ivar_name OBJC2_UNAVAILABLE;
char *ivar_type OBJC2_UNAVAILABLE;
int ivar_offset OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
}
Ivar其实就是一个指向objc_ivar结构体指针,它包含了变量名(ivar_name)、变量类型(ivar_type)等信息。
- IMP
在上面讲Method时就说过,IMP本质上就是一个函数指针,指向方法的实现,在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 (*IMP)(id, SEL, ...);
#endif
当你向某个对象发送一条信息,可以由这个函数指针来指定方法的实现,它最终就会执行那段代码,这样可以绕开消息传递阶段而去执行另一个方法实现。
- cache
顾名思义,Cache主要用来缓存,那它缓存什么呢?我们先在runtime.h文件看看它的定义
typedef struct objc_cache *Cache OBJC2_UNAVAILABLE;
struct objc_cache {
unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method buckets[1] OBJC2_UNAVAILABLE;
};
Cache其实就是一个存储Method的链表,主要是为了优化方法调用的性能。当对象receiver调用方法message时,首先根据对象receiver的isa指针查找到它对应的类,然后在类的methodLists中搜索方法,如果没有找到,就使用super_class指针到父类中的methodLists查找,一旦找到就调用方法。如果没有找到,有可能消息转发,也可能忽略它。但这样查找方式效率太低,因为往往一个类大概只有20%的方法经常被调用,占总调用次数的80%。所以使用Cache来缓存经常调用的方法,当调用方法时,优先在Cache查找,如果没有找到,再到methodLists查找。
消息发送
- objc_msgSend函数
在前面已经提过,当某个对象使用语法[receiver message]来调用某个方法时,其实[receiver message]被编译器转化为:
id objc_msgSend ( id self, SEL op, ... );
现在让我们看一下objc_msgSend它具体是如何发送消息:
1.首先根据receiver对象的isa指针获取它对应的class;
2.优先在class的cache查找message方法,如果找不到,再到methodLists查找;
3.如果没有在class找到,再到super_class查找;
4.一旦找到message这个方法,就执行它实现的IMP。
- self与super
为了让大家更好地理解self和super,借用sunnyxx博客的iOS程序员6级考试一道题目:下面的代码分别输出什么?
@implementation Son : Father
-(id)init
{
self = [super init];
if (self)
{
NSLog(@"%@", NSStringFromClass([self class]));
NSLog(@"%@", NSStringFromClass([super class]));
}
return self;
}
@end
self表示当前这个类的对象,而super是一个编译器标示符,和self指向同一个消息接受者(son)。在本例中,无论是[self class]还是[super class],接受消息者都是Son对象,但super与self不同的是,self调用class方法时,是在子类Son中查找方法,而super调用class方法时,是在父类Father中查找方法。
当调用[self class]方法时,会转化为objc_msgSend函数,这个函数定义如下:
id objc_msgSend(id self, SEL op, ...)
这时会从当前Son类的方法列表中查找,如果没有,就到Father类查找,还是没有,最后在NSObject类查找到。我们可以从NSObject.mm文件中看到- (Class)class的实现:
-(Class)class {
return object_getClass(self);
}
所以NSLog(@"%@", NSStringFromClass([self class]));会输出Son。
当调用[super class]方法时,会转化为objc_msgSendSuper,这个函数定义如下:
id objc_msgSendSuper(struct objc_super *super, SEL op, ...)
objc_msgSendSuper函数第一个参数super的数据类型是一个指向objc_super的结构体,从message.h文件中查看它的定义:
/// Specifies the superclass of an instance.
struct objc_super {
/// Specifies an instance of a class.
__unsafe_unretained id receiver;
/// Specifies the particular superclass of the instance to message.
#if !defined(__cplusplus) && !__OBJC2__
/* For compatibility with old objc-runtime.h header */
__unsafe_unretained Class class;
#else
__unsafe_unretained Class super_class;
#endif
/* super_class is the first class to search */
};
#endif
结构体包含两个成员,第一个是receiver,表示某个类的实例。第二个是super_class表示当前类的父类。
这时首先会构造出objc_super结构体,这个结构体第一个成员是self,第二个成员是(id)class_getSuperclass(objc_getClass("Son")),实际上该函数会输出Father。然后在Father类查找class方法,查找不到,最后在NSObject查到。此时,内部使用objc_msgSend(objc_super->receiver, @selector(class))去调用,与[self class]调用相同,所以结果还是Son。
Method Resolution
Fast Forwarding
Normal Forwarding
我的理解:objc_super结构体中receiver是son,superclass是father,而objc_msgSend(objc_super->receiver, @selector(class))内部是使用objc_super中的receiver作为消息发送者,也就是son调用NSObject中的class方法,所以结果是son。
方法解析与消息转发
[receiver message]调用方法时,如果在message方法在receiver对象的类继承体系中没有找到方法,那怎么办?一般情况下,程序在运行时就会Crash掉,抛出unrecognized selector sent to…类似这样的异常信息。但在抛出异常之前,还有三次机会按以下顺序让你拯救程序。
1.Method Resolution 即所属类动态方法解析
2.Fast Forwarding 即备援接受者
3.Normal Forwarding
- Method Resolution首先Objective-C在运行时调用+ resolveInstanceMethod:或+ resolveClassMethod:方法,让你添加方法的实现。如果你添加方法并返回YES,那系统在运行时就会重新启动一次消息发送的过程。
举一个简单例子,定义一个类Message,它主要定义一个方法sendMessage,下面就是它的设计与实现:
@interface Message : NSObject
-(void)sendMessage:(NSString *)word;
@end
@implementation Message
-(void)sendMessage:(NSString *)word
{
NSLog(@"normal way : send message = %@", word);
}
@end
如果我在viewDidLoad方法中创建Message对象并调用sendMessage方法:
-(void)viewDidLoad {
[super viewDidLoad];
Message *message = [Message new];
[message sendMessage:@"Sam Lau"];
}
控制台会打印以下信息:
normal way : send message = Sam Lau
但现在我将原来sendMessage方法实现给注释掉,覆盖resolveInstanceMethod方法:
#pragma mark - Method Resolution
/// override resolveInstanceMethod or resolveClassMethod for changing sendMessage method implementation
+(BOOL)resolveInstanceMethod:(SEL)sel
{
if (sel == @selector(sendMessage:)) {
class_addMethod([self class], sel, imp_implementationWithBlock(^(id self, NSString *word) {
NSLog(@"method resolution way : send message = %@", word);
}), "v@*");
}
return YES;
}
控制台就会打印以下信息:
method resolution way : send message = Sam Lau
该方法主要原型为
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);
方法名为给class添加方法,返回一个Bool类型返回值。第一个参数为需要添加方法的类,第二个参数为实例方法的名字也就是+(BOOL)resolveInstanceMethod:(SEL)sel
方法中的参数sel,第三个参数为IMP类型的变量也就是函数的实现。需要传一个C函数,该函数至少要有两个变量,一个是id self,一个是SEL _cmd,也可以传字符串。
另一种写法。
void dynamicAdditionMethodIMP(id self, SEL _cmd) {
//实现
NSLog(@"dynamicAdditionMethodIMP");
}
class_addMethod([self class], name, (IMP)dynamicAdditionMethodIMP, "v@:");
第四个参数传入一个字符串"v@*,它表示方法的参数和返回值,详情请参考Type Encodings。
如果resolveInstanceMethod方法返回NO,运行时就跳转到下一步:消息转发(Message Forwarding)。
- Fast Forwarding
当对象所属类不能动态添加方法后,runtime就会询问当前的接受者是否有其他对象可以处理这个未知的selector。方法就是:
- (id)forwardingTargetForSelector:(SEL)aSelector;
如果目标对象实现- forwardingTargetForSelector:方法,系统就会在运行时调用这个方法,只要这个方法返回的不是nil或self,也会重启消息发送的过程,把这消息转发给其他对象来处理。否则,就会继续Normal Fowarding。
继续上面Message类的例子,将sendMessage和resolveInstanceMethod方法注释掉,然后添加forwardingTargetForSelector方法的实现:
#pragma mark - Fast Forwarding
-(id)forwardingTargetForSelector:(SEL)aSelector
{
if (aSelector == @selector(sendMessage:)) {
return [MessageForwarding new];
}
return nil;
}
此时还缺一个转发消息的类MessageForwarding,这个类的设计与实现如下:
@interface MessageForwarding : NSObject
-(void)sendMessage:(NSString *)word;
@end
@implementation MessageForwarding
-(void)sendMessage:(NSString *)word
{
NSLog(@"fast forwarding way : send message = %@", word);
}
@end
此时,控制台会打印以下信息:
fast forwarding way : send message = Sam Lau
这里叫Fast,是因为这一步不会创建NSInvocation对象,但Normal Forwarding会创建它,所以相对于更快点。
- Normal Forwarding
如果没有使用Fast Forwarding来消息转发,最后只有使用Normal Forwarding来进行消息转发。它首先调用methodSignatureForSelector:方法来获取函数的参数和返回值,如果返回为nil,程序会Crash掉,并抛出unrecognized selector sent to instance异常信息。如果返回一个函数签名,系统就会创建一个NSInvocation对象并调用-forwardInvocation:方法。
继续前面的例子,将forwardingTargetForSelector方法注释掉,添加methodSignatureForSelector和forwardInvocation方法的实现:
#pragma mark - Normal Forwarding
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
NSMethodSignature *methodSignature = [super methodSignatureForSelector:aSelector];
if (!methodSignature) {
methodSignature = [NSMethodSignature signatureWithObjCTypes:"v@:*"];
}
return methodSignature;
}
-(void)forwardInvocation:(NSInvocation *)anInvocation
{
MessageForwarding *messageForwarding = [MessageForwarding new];
if ([messageForwarding respondsToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:messageForwarding];
}
}
三种方法的选择
Runtime提供三种方式来将原来的方法实现代替掉,那该怎样选择它们呢?
- Method Resolution
由于Method Resolution不能像消息转发那样可以交给其他对象来处理,所以只适用于在原来的类中代替掉。 - Fast Forwarding
它可以将消息处理转发给其他对象,使用范围更广,不只是限于原来的对象。 - Normal Forwarding
它跟Fast Forwarding一样可以消息转发,但它能通过NSInvocation对象获取更多消息发送的信息,例如:target、selector、arguments和返回值等信息。
Associated Objects
如果我们想对系统的类添加方法时,可以使用扩展,但是添加属性时,只能使用继承。如果不想使用继承,则可以用runtime来关联对象,这与我们自定义类的添加属性不同。本质上是使用类别进行扩展,通过添加get方法和set方法从而在使用时可以使用点方法使用。与普通使用方法一致。
同时当使用Category对某个类进行扩展时,有时需要存储属性,Category是不支持的,这时需要使用Associated Objects来给已存在的类Category添加自定义的属性。Associated Objects提供三个C语言API来向对象添加、获取和删除关联值:
- void objc_setAssociatedObject (id object, const void *key, id value, objc_AssociationPolicy policy )
- id objc_getAssociatedObject (id object, const void *key )
- void objc_removeAssociatedObjects (id object )
其中objc_AssociationPolicy是个枚举类型,它可以指定Objc内存管理的引用计数机制。
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. */
};
下面有个关于NSObject+AssociatedObject Category添加属性associatedObject的示例代码:
NSObject+AssociatedObject.h
@interface NSObject (AssociatedObject)
@property (strong, nonatomic) id associatedObject;
@end
NSObject+AssociatedObject.m
@implementation NSObject (AssociatedObject)
- (void)setAssociatedObject:(id)associatedObject
{
objc_setAssociatedObject(self, @selector(associatedObject), associatedObject, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (id)associatedObject
{
return objc_getAssociatedObject(self, _cmd);
}
@end
Associated Objects的key要求是唯一并且是常量,而SEL是满足这个要求的,所以上面的采用隐藏参数_cmd作为key。
一个给scrollview添加refreshView的实际用例:
@interface UIScrollView (Refresh)
@property (nonatomic) RefreshView * refreshView;
@end
#import <objc/runtime.h>
static char kRefreshView;
@implementation UIScrollView (Refresh)
@dynamic refreshView;
- (void)setRefreshView:(RefreshView *)aView {
objc_setAssociatedObject(self, &kRefreshView, aView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (RefreshView *)refreshView {
return objc_getAssociatedObject(self, &kRefreshView);
}
@end
GetClass
得到一个实例的类。代码如下:
#import "ViewController.h"
#import <objc/runtime.h>
#import "Person.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
Person * p1 = [[Person alloc] init];
Class c1 = object_getClass(p1);
NSLog(@"%@", c1);
Person * p2 = [[[c1 class] alloc] init];
NSLog(@"%@", p2.name);
}
isKindOfClass和isMemberOfClass
先看看isKindOfClass和isMemberOfClass在Object.mm中的实现:
- (BOOL)isKindOf:aClass
{
Class cls;
for (cls = isa; cls; cls = cls->superclass)
if (cls == (Class)aClass)
return YES;
return NO;
}
- (BOOL)isMemberOf:aClass
{
return isa == (Class)aClass;
}
可以看到isKindOfClass是取自身指针isa与参数指针做对比,如果相等返回YES,如果不等则取其superClass指针,直到superClass为空为止,循环结束返回空。
int main(int argc, const char * argv[]) {
@autoreleasepool {
BOOL res1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
BOOL res2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
BOOL res3 = [(id)[Sark class] isKindOfClass:[Sark class]];
BOOL res4 = [(id)[Sark class] isMemberOfClass:[Sark class]];
NSLog(@"%d %d %d %d", res1, res2, res3, res4);
}
return 0;
}
//输出
2014-11-05 14:45:08.474 Test[9412:721945] 1 0 0 0
res1中NSObject的isa指针指向NSOject的meta-class,继续循环,meta-class的superClass是NSObject所以为ture;
res3中Sark的isa指针指向sark的meta-class,接下来指向NSObject的meta-class,再接下来指向NSObject;
res2中NSObject的isa指针指向NSOject的meta-class,与NSObject不一致;
res4中Sark的isa指针指向sark的meta-class,与Sark不一致。
注意上面是类与类相比。下面是类的实例与类相比:
Person *person = [[Person alloc] init];
Teacher *teacher = [[Teacher alloc] init];
//YES
if ([teacher isKindOfClass:[Teacher class]]) {
NSLog(@"teacher 是 Teacher类或Teacher的子类");
}
//YES
if ([teacher isKindOfClass:[Person class]]) {
NSLog(@"teacher 是 Person类或Person的子类");
}
//YES
if ([teacher isKindOfClass:[NSObject class]]) {
NSLog(@"teacher 是 NSObject类或NSObject的子类");
}
// YES
if ( [teacher respondsToSelector: @selector( setName: )] == YES ) {
NSLog(@"teacher responds to setSize: method" );
}
// NO
if ( [teacher respondsToSelector: @selector( abcde )] == YES ) {
NSLog(@"teacher responds to nonExistant method" );
}
// YES
if ( [Teacher respondsToSelector: @selector( alloc )] == YES ) {
NSLog(@"teacher class responds to alloc method\n" );
}
Method Swizzling
Method Swizzling就是在运行时将一个方法的实现代替为另一个方法的实现。由于Foundation等框架都是闭源的,我们没有办法直接修改代码,通常情况下只能通过继承,类别,关联属性等手段添加属性和实例方法,较为繁琐。而Method Swizzling可以通过将一个方法的实现代替另一个方法的实现,从而达到修改闭源代码的目的。
例如一个想要统计每个页面出现的次数,如果在所有页面的viewWillAppear中都添加统计代码,则太过繁琐。那我们使用一个类别,并自定义一个方法替代系统的viewWillAppear,在其中做统计,并在统计结束后调用系统的viewWillAppear即可。
@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 {
//这里实际上是调用系统的viewWillAppear,不会递归调用。
[self myViewWillAppear:animated];
NSLog(@"MyViewWillAppear %@", [self class]);
}
使用load实现预加载。使用GCD的dispatch_once_t保证只会交换一次不会重复交换。使用method_exchangeImplementations来实现交换。
将上述代码放入工程中就可以打印所有viewcontroller的加载情况。
Aspect-Oriented Programming(AOP)
类似记录日志、身份验证、缓存等事务非常琐碎,与业务逻辑无关,很多地方都有,又很难抽象出一个模块,这种程序设计问题,业界给它们起了一个名字叫横向关注点(Cross-cutting concern),AOP作用就是分离横向关注点(Cross-cutting concern)来提高模块复用性,它可以在既有的代码添加一些额外的行为(记录日志、身份验证、缓存)而无需修改代码。
runtime如何实现weak置为nil
runtime对注册的类会进行布局,对于weak修饰的对象会放入一个hash表中,用weak指向的对象内存地址作为key。当此对象的引用计数为0的时候会dealloc,假如weak指向的对象内存地址是a,那么就会以a为键在这个weak表中搜索,找到所有以a为键的weak对象,从而设置为nil。
NSString *name = [[NSString alloc] initWithString: @"Jiaming Chen"];
__weak NSString *weakStr = name;
当为weakStr这一weak类型的对象赋值时,编译器会根据name的地址为key去查找weak哈希表,该表项的值为一个数组,将weakStr对象的地址加入到数组中,当name变量超出变量作用域或引用计数为0时,会执行dealloc函数,在执行该函数时,编译器会以name变量的地址去查找weak哈希表的值,并将数组里所有 weak对象全部赋值为nil。
总结
虽然在平时项目不是经常用到Objective-C的Runtime特性,但当你阅读一些iOS开源项目时,你就会发现很多时候都会用到。所以深入理解Objective-C的Runtime数据结构、消息转发机制有助于你更容易地阅读和学习开源项目。