Objective-C 特性:Runtime
Objective-C是基于C语言加入了 面向对象特性 和 消息转发机制 的动态语言,这意味着它不仅需要一个编译器,还需要 Runtime系统 来动态创建类和对象,进行消息发送和转发。下面通过分析Apple开源的 Runtime代码 (我使用的版本是objc4-646.tar)来深入理解Objective-C的Runtime机制。
Runtime数据结构
在Objective-C中,使用[receiver message]语法并不会马上执行receiver对象的message方法的代码,而是向receiver发送一条message消息,这条消息可能由receiver来处理,也可能由转发给其他对象来处理,也有可能假装没有接收到这条消息而没有处理。其实[receiver message]被编译器转化为:
id objc_msgSend ( id self, SEL op, ... );
下面从两个数据结构id和SEL来逐步分析和理解Runtime有哪些重要的数据结构。
SEL
SEL是函数objc_msgSend第二个参数的数据类型,表示 方法选择器 ,按下面路径打开objc.h文件
SEL Data Structure
查看到SEL数据结构如下:
typedef struct objc_selector *SEL;
其实它就是映射到方法的C字符串,你可以通过Objc编译器命令@selector()或者Runtime系统的sel_registerName函数来获取一个SEL类型的方法选择器。
如果你知道selector对应的方法名是什么,可以通过NSString* NSStringFromSelector(SEL aSelector)方法将SEL转化为字符串,再用NSLog打印。
id
接下来看objc_msgSend第一个参数的数据类型id,id是通用类型指针,能够表示任何对象。按下面路径打开objc.h文件
id Data Structure.png
查看到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结构体指针,它包含一个Class isa成员,根据isa指针就可以顺藤摸瓜找到 对象所属的类 。
注意:根据Apple的官方文档 Key-Value Observing Implementation Details 提及,key-value observing是使用isa-swizzling的技术实现的,isa指针在运行时被修改,指向一个中间类而不是真正的类。所以,你不应该使用isa指针来确定类的关系,而是使用 class 方法来确定实例对象的类。
Class
isa指针的数据类型是Class,Class表示对象所属的类,按下面路径打开objc.h文件
Class Data Structure
/// An opaque type that represents an Objective-C class. typedef struct objc_class *Class;
可以查看到Class其实就是一个objc_class结构体指针,但这个头文件找不到它的定义,需要在runtime.h才能找到objc_class结构体的定义。
按下面路径打开runtime.h文件
objc_class Data Structure
查看到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 *` */
注意:OBJC2_UNAVAILABLE是一个Apple对Objc系统运行版本进行约束的宏定义,主要为了兼容非Objective-C 2.0的遗留版本,但我们仍能从中获取一些有用信息。
让我们分析一些重要的成员变量表示什么意思和对应使用哪些数据结构。
-
isa表示一个Class对象的Class,也就是 Meta Class 。在面向对象设计中,一切都是对象,Class在设计中本身也是一个对象。我们会在objc-runtime-new.h文件找到证据,发现objc_class有以下定义:
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 ...... }
由此可见,结构体objc_class也是继承objc_object,说明 Class在设计中本身也是一个对象 。
其实Meta Class也是一个Class,那么它也跟其他Class一样有自己的isa和super_class指针,关系如下:
Class isa and superclass relationship from Google
上图 实线 是super_class指针, 虚线 是isa指针。有几个关键点需要解释以下:
- Root class (class)其实就是NSObject,NSObject是没有超类的,所以Root class(class)的superclass指向nil。
- 每个Class都有一个isa指针指向唯一的Meta class
- Root class(meta)的superclass指向Root class(class),也就是NSObject,形成一个回路。
- 每个Meta class的isa指针都指向Root class (meta)。
- super_class表示实例对象对应的父类
- name表示类名
-
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结构体存储类的单个成员变量信息。
-
methodLists表示方法列表,它指向objc_method_list结构体的二级指针,可以动态修改*methodLists的值来添加成员方法,也是Category实现原理,同样也解释Category不能添加属性的原因。在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作为入口,逐步深入分析Runtime的数据结构,了解每个数据结构的作用和它们之间关系后,我们正式转入 消息发送 这个正题。
objc_msgSend函数
在前面已经提过,当某个对象使用语法[receiver message]来调用某个方法时,其实[receiver message]被编译器转化为:
id objc_msgSend ( id self, SEL op, ... );
现在让我们看一下objc_msgSend它具体是如何发送消息:
- 首先根据receiver对象的isa指针获取它对应的class
- 优先在class的cache查找message方法,如果找不到,再到methodLists查找
- 如果没有在class找到,再到super_class查找
- 一旦找到message这个方法,就执行它实现的IMP。
Objc Message.gif
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指向同一个消息接受者。在本例中,无论是[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 。
隐藏参数self和_cmd
当[receiver message]调用方法时,系统会在运行时偷偷地动态传入两个隐藏参数self和_cmd,之所以称它们为隐藏参数,是因为在源代码中没有声明和定义这两个参数。至于对于self的描述,上面已经解释非常清楚了,下面我们重点讲解_cmd。
_cmd表示当前调用方法,其实它就是一个方法选择器SEL。一般用于判断方法名或在Associated Objects中唯一标识键名,后面在Associated Objects会讲到。
方法解析与消息转发
[receiver message]调用方法时,如果在message方法在receiver对象的类继承体系中没有找到方法,那怎么办?一般情况下,程序在运行时就会Crash掉,抛出 unrecognized selector sent to … 类似这样的异常信息。但在抛出异常之前,还有 三次机会 按以下顺序让你拯救程序。
- Method Resolution
- Fast Forwarding
- Normal Forwarding
Message Forward from Google
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
注意到上面代码有这样一个字符串"v@*,它表示方法的参数和返回值,详情请参考 Type Encodings
如果resolveInstanceMethod方法返回NO,运行时就跳转到下一步: 消息转发(Message Forwarding)
Fast Forwarding
如果目标对象实现- 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]; } }
关于这个例子的示例代码请到 github 下载。
三种方法的选择
Runtime提供三种方式来将原来的方法实现代替掉,那该怎样选择它们呢?
- Method Resolution: 由于Method Resolution不能像消息转发那样可以交给其他对象来处理,所以只适用于在 原来的类 中代替掉。
- Fast Forwarding: 它可以将消息处理转发给 其他对象 ,使用范围更广,不只是限于原来的对象。
- Normal Forwarding: 它跟Fast Forwarding一样可以消息转发,但它能通过NSInvocation对象获取 更多消息发送 的信息,例如:target、selector、arguments和返回值等信息。
Associated Objects
当使用Category对某个类进行扩展时,有时需要 存储属性 ,Category是不支持的,这时需要使用Associated Objects来给已存在的类Category添加自定义的属性。Associated Objects提供三个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+AssociatedObjectCategory添加属性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。
Method Swizzling
Method Swizzling就是在运行时将一个方法的实现代替为另一个方法的实现。如果能够利用好这个技巧,可以写出 简洁、有效且维护性更好 的代码。可以参考两篇关于Method Swizzling技巧的文章:
Aspect-Oriented Programming(AOP)
类似记录日志、身份验证、缓存等事务非常琐碎,与业务逻辑无关,很多地方都有,又很难抽象出一个模块,这种程序设计问题,业界给它们起了一个名字叫 横向关注点(Cross-cutting concern) , AOP 作用就是分离 横向关注点(Cross-cutting concern) 来提高模块复用性,它可以在既有的代码添加一些额外的行为(记录日志、身份验证、缓存)而无需修改代码。
危险性
Method Swizzling就像一把瑞士小刀,如果使用得当,它会有效地解决问题。但使用不当,将带来很多麻烦。在stackoverflow上有人已经提出这样一个问题: What are the Dangers of Method Swizzling in Objective C? ,它的 危险性 主要体现以下几个方面:
- Method swizzling is not atomic
- Changes behavior of un-owned code
- Possible naming conflicts
- Swizzling changes the method's arguments
- The order of swizzles matters
- Difficult to understand (looks recursive)
- Difficult to debug
总结
虽然在平时项目不是经常用到Objective-C的Runtime特性,但当你阅读一些iOS开源项目时,你就会发现很多时候都会用到。所以深入理解Objective-C的Runtime数据结构、消息转发机制有助于你更容易地阅读和学习开源项目。