在用Objective -C等面向对象的语言编程时,“对象”就是“基本构造单元”,开发者可以通过对象保存或传递数据,对象之间的数据传递并执行任务的过程就叫做“消息传递”,若当程序运行起来以后,为其提供相关支持的代码叫做“Objective Runtime”,要想写出高质量的代码,一定要熟悉他们的特性和工作原理。
- 对象
- 对象同等性
- 编写高质量类的几个技巧
- 类簇
- 关联对象存放自定义数据
- runtime 消息发送objc_msgSend
- runtime 消息转发机制
- 第一步:动态方法解析
- 第二步:重定向
- 第三部: 完整的消息转发
- 方法调配(method swizzling)
对象
同等性
我们通常使用“==”进行比较操作,但比较出来的的结果并不是我们想要的,他仅仅是对两个指针的比较。例如:
NSString *str1 = @"string";
NSString *str2 = [NSString stringWithFormat:@"string"];
if (str1 == str2) {
NSLog(@"str1 is equal to str2 ");
}else{
NSLog(@"str1 is not equal to str2 ");
}
//输出 runtime_csdn[14285:737020] str1 is not equal to str2
此时比较的是str1和str2的内存地址,而变量str1 和str2 内存地址是不同的。NSObject协议中声明了isEqual:
方法来判别对象的同等性。其次有些类例如NSString 有自己独有的判断同等性的方法isEqualToString:
NSString *str1 = @"string";
NSString *str2 = [NSString stringWithFormat:@"string"];
//情景1
if ([str1 isEqual: str2]) {
NSLog(@"str1 is equal to str2 ");
}else{
NSLog(@"str1 is not equal to str2 ");
}
//情景2
if ([str1 isEqualToString: str2]) {
NSLog(@"str1 is equal to str2 ");
}else{
NSLog(@"str1 is not equal to str2 ");
}
//都会输出str1 is equal to str2
对象
编写高质量类的几个技巧
- 对象内部读取数据时,应该直接访问实例变量来读,而写入数据是则通过属性方法来写。
- 对于某些属性其创建成本较高,可以使用惰性初始化方式,这种情况下要通过属性来读取数据。
- 通过类簇来隐藏抽象基类的实现细节,下面我会仔细介绍类簇。
类簇
类簇是一种很有用的模式,特别在oc的自带框架中也用的特别多。我们最熟悉的UIButton的类其实也应用了这种模式。
- (UIButton *)buttonWithType:(UIButtonType)type;
该方法的返回对象都是继承自同一个类UIButton,这样做的好处是:UIButton类的使用则无需关心按钮到底来自于哪个子类。例如:
id obj1 = [NSArray alloc]; // __NSPlacehodlerArray *
id obj2 = [NSMutableArray alloc]; // __NSPlacehodlerArray *
id obj3 = [obj1 init]; // __NSArrayI *
id obj4 = [obj2 init]; // __NSArrayM *
这里看出+ alloc
后并非生成了我们期望的类实例,而是一个__NSPlacehodlerArray的中间对象,后面的- init
或- initWithXXXXX
消息都是发送给这个中间对象,再由它做工厂,生成真的对象。
关联对象存放自定义数据
我们在对象中想储存相关数据时,通常是从对象所属类中继承一个子类,在根据自己的需求来改用这个子类,单有时候并非所有的情况多适合这种情况。这时候就可以使用“关联对象”解决这类问题。通常我们写一个UIAlertView:
UIAlertView *alert = [[UIAlertView alloc]initWithTitle:@"test" message:@"hello word" delegate:self cancelButtonTitle:@"yes " otherButtonTitles:@"no" , nil];
[alert show];
//delegate 方法
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex{
if (buttonIndex == 0) {
//do something
}else{
//do something
}
}
如果一个类中有多个这样的视图,我们经常就是给AlertView的Tag赋值,然后在代理方法中进行判断,这样做代码会变得更为复杂,而且创建视图的代码和处理视图的代码分离,代码晦涩难懂。这里可以利用“关联对象”。
UIAlertView *alert = [[UIAlertView alloc]initWithTitle:@"test" message:@"hello word" delegate:self cancelButtonTitle:@"yes " otherButtonTitles:@"no" , nil];
objc_setAssociatedObject(alert,@"KEY",myBlock,OBJC_ASSOCIATION_RETAIN);
//代理方法
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex{
void (^myBlock)(NSInteger) = objc_getAssociatedObject(alertView, @"KEY");
myBlock(buttonIndex);
}
这样做代码逻辑无疑更加清晰。注意OBJC_ASSOCIATION_RETAIN
和定义属性时是一样的,表示一种“拥有关系”和“非拥有关系”。
runtime 消息发送objc_msgSend
Runtime 又叫运行时,是一套底层的 C 语言 API,其为 iOS 内部的核心之一,平常我们之所以说 Objective-C 是一门动态语言,因为它会将一些工作放在代码运行时才处理而并非编译时。也就是说,有很多类和成员变量在我们编译的时是不知道的,而在运行时,我们所编写的代码会转换成完整的确定的代码运行。通常我会调用某个方法都会写出如下代码:
[receiver message];
// 底层运行时会被编译器转化为:objc_msgSend(receiver, selector)
// 如果其还有参数比如:
[receiver message:(id)arg...];
// 底层运行时会被编译器转化为:objc_msgSend(receiver, selector, arg1, arg2, ...)
我们可以从字面上简单理解为:对某个对象receiver发送消息message。若想深入了解其中的原理,这样显然是不够的。首先我们从方法开始了解一些数据结构,然后层层解析整个运行机制。
id objc_msgSend ( id self, SEL op, ... );
SEL
SEL
是函数objc_msgSend
第二个参数的数据类型,表示方法选择器,其数据结构是:
typedef struct objc_selector *SEL;
其实它就是映射到方法的C字符串,你可以通过Objc编译器命令@selector()
或者Runtime系统的sel_registerName
函数来获取一个SEL类型的方法选择器。也可以通过NSStringFromSelector(SEL aSelector)
来将方法选择器转化成字符串。
Id
接下来看objc_msgSend
第一个参数的数据类型id,id是通用类型指针,能够表示任何对象。结构体如下:
typedef struct objc_object *id;
struct objc_object { Class isa; };
看到 objc_object
结构体包含一个 isa 指针,根据 isa 指针就可以找到对象所属的类。
注意:根据Apple的官方文档Key-Value Observing Implementation Details提及,key-value observing是使用isa-swizzling的技术实现的,isa指针在运行时被修改,指向一个中间类而不是真正的类。所以,你不应该使用isa指针来确定类的关系,而是使用class方法来确定实例对象的类。
Class
typedef struct objc_class *Class;
Class 其实是指向 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 *` */
//注意:OBJC2_UNAVAILABLE是一个Apple对Objc系统运行版本进行约束的宏定义,主要为了兼容非Objective-C 2.0的遗留版本。
让我们分析一些重要的成员变量表示什么意思和对应使用哪些数据结构。
- isa表示一个Class对象的Class,同时Class中也有isa指针,它指向 MetaClass,MeteClass的isa指针最终会指向RootClass。在面向对象设计中,一切都是对象,Class在设计中本身也是一个对象。
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不能添加实例变量的原因。其结构体如下:
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,它是一个函数指针,结构如下:
/// 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 ####
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 为方法调用的性能进行优化,每当实例对象接收到一个消息时,它不会直接在 isa 指针指向的类的方法列表中遍历查找能够响应的方法,因为每次都要查找效率太低了,而是优先在 Cache 中查找。Runtime 系统会把被调用的方法存到 Cache 中,如果一个方法被调用,那么它有可能今后还会被调用,下次查找的时候就会效率更高。就像计算机组成原理中 CPU 绕过主存先访问 Cache 一样
Property
typedef struct objc_property *Property;
typedef struct objc_property *objc_property_t;//这个更常用
可以通过class_copyPropertyList 和 protocol_copyPropertyList 方法获取类和协议中的属性:
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)
返回的是属性列表,列表中每个元素都是一个objc_property_t
指针.我们可以通过property_getName()
用来查找属性的名称,返回 c 字符串。property_getAttributes()
方法掘属性的真实名称和 @encode 类型,返回 c 字符串。
目前一些runtime术语差不多介绍完了。runtime下面详细讲消息发送的步骤:
- 首先检测这个 selector 是不是要忽略。比如 Mac OS X 开发,有了垃圾回收就不理会 retain,release 这些函数。
- 检测这个 selector 的 target 是不是 nil,Objc 允许我们对一个 nil 对象执行任何方法不会 Crash,因为运行时会被忽略掉。
- 如果上面两步都通过了,那么就开始查找这个类的实现 IMP,先从 cache 里查找,如果找到了就运行对应的函数去执行相应的代码。
- 如果 cache 找不到就找类的方法列表中是否有对应的方法。
如果类的方法列表中找不到就到父类的方法列表中查找,一直找到 NSObject 类为止。 - 如果还找不到,就要开始进入动态方法解析了,后面会提到。
在消息的传递中,编译器会根据情况在 objc_msgSend , objc_msgSend_stret , objc_msgSendSuper , objc_msgSendSuper_stret 这四个方法中选择一个调用。如果消息是传递给父类,那么会调用名字带有 Super 的函数,如果消息返回值是数据结构而不是简单值时,会调用名字带有 stret 的函数。
runtime 消息转发机制
首先看看消息转发的整个流程图:
第一步:动态方法解析
当对象收到无法解析的消息后,第一步会先调用消息接收者所在类的resolveInstanceMethod:
方法,该方法返回一个BOOL值,表示是否动态添加一个方法来响应当前消息选择器。如果发送的消息是一个类方法,则会调用另一个类似的方法resolveClassMethod:
。我们需要用 class_addMethod 函数完成向特定类添加特定方法实现的。
void dynamicMethodIMP(id self, SEL _cmd) {
// implementation ....
}
@implementation MyClass
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
if (aSEL == @selector(resolveThisMethodDynamically)) {
class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:aSEL];
}
@end
第二步:重定向
如果上一步过程中,并没有新方法能响应消息选择器,则会进入消息转发流程的第二步。在第二步中系统会调用当前消息接收者所在类的forwardingTargetForSelector:方法,用以询问能否将该条消息发送给其他接收者来处理,方法的返回值就代表这个新的接收者,如果不允许将消息转发给其他接收者则返回nil或self。
- (id)forwardingTargetForSelector:(SEL)aSelector
{
if(aSelector == @selector(mysteriousMethod:)){
return alternateObject;
}
return [super forwardingTargetForSelector:aSelector];
}
这里我们可以用“组合”来模拟“多继承”的某些特性。在一个对象的,可能还有一系列的其他对象,该对象可由此方法返回能够处理这个选择子的对象。这样的话,在外接看来就好像该对象亲自处理了一些消息。
第三部: 完整的消息转发
如果forwardingTargetForSelector:
方法的返回值为nil,那么消息转发机制还要继续进行最后一步。在这一步中,系统会将尚未处理的消息包装成一个NSInvocation对象,其内部包含与该消息相关的所有信息,比如消息的选择器、目标接收者、参数等。之后系统会调用消息接收者所在类的forwardInvocation:
方法,并将生成的NSInvocation对象作为参数传入。
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
if ([someOtherObject respondsToSelector:
[anInvocation selector]])
[anInvocation invokeWithTarget:someOtherObject];
else
[super forwardInvocation:anInvocation];
}
当一个类无法响应某个消息的时候,runtime会通过forwardInvocation
通知该对象。每个对象都继承了NSObject的forwardInvocation
方法,但是NSObject并没有实现,所以需要我们手动的实现forwardInvocation
就像是一个消息分发中心或者是一个中转站,能将消息分发给不同的对象。它还可以将某些消息更改,或者是’吃掉’,不响应这些消息也不会出错。
方法调配(method swizzling)
我们已经了解了OC中对象的类型和消息处理机制,这些有助于我们进一步了解OC运行时的其他功能和特性。接下来就介绍其中一种叫做Method Swizzing(方法调配)的技术,该技术经常被称为iOS开发中的黑魔法。
首先,当对象接收到某个消息时,编译器首先将代码转换为objc_msgSend函数,并将消息的接收者和选择器当做函数的参数传入,接下来系统会根据接收者的isa指针找到它所对应的类,在类的元数据信息中找到该类所拥有的方法列表,然后遍历方法列表,将每一个方法内部的SEL选择器同传入的消息选择器进行匹配,当找到相同的选择器后,就根据方法内部的IMP函数指针跳转到方法的具体实现。当然,为了提高方法多次执行的效率,系统会将遍历查询的结果缓存起来,储存在类的元数据信息中,此处就不再继续深入讨论。
了解清楚选择器和方法实现之间的一对一关系后,我们接下来开始介绍方法调配技术,它其实就是利用运行时提供的函数来动态修改选择器和方法实现之间的对应关系的一种技术。利用这种技术,我们可以在运行时为某个类添加选择器或更改选择器所对应的方法实现,甚至可以更换两个已有选择器所对应的方法实现,从而实现一种极其诡异的效果。下面就写一段示例程序,通过方法调配技术来更换NSString类的大小写转换方法的实现(仅供娱乐使用)。
int main(int argc, const char * argv[]) {
@autoreleasepool {
Method lowercase = class_getInstanceMethod([NSString class], @selector(lowercaseString));
Method uppercase = class_getInstanceMethod([NSString class], @selector(uppercaseString));
method_exchangeImplementations(lowercase, uppercase);
NSLog(@"%@ -- %@", [@"AbCd" lowercaseString], [@"AbCd" uppercaseString]);
// 输出结果:ABCD -- abcd
}
return 0;
}
方法调配技术的作用肯定不在于此,那么开发者通常如何使用这种技术呢?在总结方法调配技术的用处之前,我们先再来看一个示例程序。同样以NSString类为例,我们为其lowercaseString方法增加一些日志输出功能(不改变方法名,只是更改方法的实现)。你可能第一时间想到用继承来实现该需求,然而当项目中有多个类需要同样需求时,你需要每个类都去继承一下,然后还要保证别人都是去用你的子类而不是原本的父类,这样显然并不是一种很好的解决办法。此时我们就可以尝试使用方法调配技术,完整的示例代码如下。
// NSString+Logging.h
#import <Foundation/Foundation.h>
@interface NSString (Logging)
- (NSString *)lowercaseStringWithLogging;
@end
// NSString+Logging.m
#import "NSString+Logging.h"
@implementation NSString (Logging)
- (NSString *)lowercaseStringWithLogging {
NSString *lowercaseString = [self lowercaseStringWithLogging];
NSLog(@"%@ -> %@", self, lowercaseString);
return lowercaseString;
}
// main.m
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import "NSString+Logging.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Method lowercase = class_getInstanceMethod([NSString class], @selector(lowercaseString));
Method lowercaselogging = class_getInstanceMethod([NSString class], @selector(lowercaseStringWithLogging));
method_exchangeImplementations(lowercase, lowercaselogging);
[@"AbCd" lowercaseString];
// 输出结果:AbCd -> abcd
}
return 0;
}
@end
本文参考:
详解Runtime运行时机制
OC学习Runtime之消息传递,消息转发机制
Objective-C Runtime Programming Guide