10. 在既有类中使用关联对象存放自定义数据
注意关键词“关联对象”,就是把两个对象关联起来,例如把对象B关联到对象A上面,这样只要我们知道对象A,就能通过关联方法拿到对象B,这是一个很有用的特性,可以帮助我们携带一些数据,以及一些信息。如果通俗一点理解的话可以把对象A理解成一个字典,对象B是存放在对象A中的一个对象,通过对应的key值就能拿到对应的对象B。
下面是关联对象对应的三个方法(只有三个方法):
1.通过给定的键值和关联策略对某对象设置关联对象
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
第一个参数,被关联对象,对应上面的对象A。
第二个参数,键值,通过参数形式我们知道,这是一个指针,一般我们在定义这个指针的时候使用静态全局变量,因为这是一个“不透明指针”(自行查找什么是“不透明指针”)。
第三个参数,关联的对象,对应上面的对象B。
第四个参数,关联策略,是一个枚举值,对应定义属性时候添加的属性特性,用于维护内存管理,下表列出对应关系:
关联类型 | 等效的属性特性 |
---|---|
OBJC_ASSOCIATION_ASSIGN | assign |
OBJC_ASSOCIATION_RETAIN_NONATOMIC | nonatomic, retain |
OBJC_ASSOCIATION_COPY_NONATOMIC | nonatomic, copy |
OBJC_ASSOCIATION_RETAIN | retain |
OBJC_ASSOCIATION_COPY | copy |
2.通过给定的键值取出相应的关联对象
id objc_getAssociatedObject(id object, const void *key)
第一个参数,被关联的对象,对应对象A。
第二个参数,键值。
返回值,关联对象,对应对象B。
3.移除被关联对象的所有关联对象
void objc_removeAssociatedObjects(id object)
参数,被关联对象,对应对象B。
上面就是关联对象的所有方法,但是在用的时候需要注意,关联对象应该被我们列在最后的选择方案,因为关联对象之间的关系没有正式的定义,其内存管理是在设置关联的时候才定义的,而不是在接口中预先设定好的,有时会出现一些不易查找的错误。
PS:偶尔在代码中写点这样的代码,会增加代码的“气质”,你懂的。
11. 理解objc_msgSend作用
这一小节的内容和我们写代码没有什么关系,但是我们可以了解一下OC中方法的调用过程,对我们的程序调试很是很有用的。
首先说一下C语言的函数调用方式,用以和OC做比较,C语言使用“静态绑定”,也就是说,在编译期就能决定运行时应该调用的函数,而大家都知道,OC是一门动态语言,与之差别的就是OC中有时候是使用“动态绑定”,就是在运行期调用对应的函数,甚至可以在程序运行时改变。
写一个简单的方法调用的例子,解释一下方法的构成:
id returnValue = [someObject messageName:parameter];
在这句调用语句中,someObject就是类或类的实例,messageName就是方法名,parameter就是参数,编译器会把这条语句编译成一条标准的C语句,编译后的语句如下:
id returnValue = objc_msgSend(someObject, @selector(messageName), parameter)
objc_msgSend是一个可变参数的函数,对应OC中方法参数的增加,参数也会增加,相信大家都知道这个方法中参数的意思。
objc_msgSend函数会根据参数,找到对应类的对应“方法列表”,然后找到对应实现代码,若找不到会沿着继承关系向上查找,如果还没找到,触发“消息转发”机制(后面会介绍这个机制)。
这样下来调用一个方法大家可能感觉步骤太多,其实不会,objc_msgSend会将匹配结果放到一张“快速映射表”里,每个类都有一个这样的表,加快调用速度。另外还有一些特殊情况,OC运行环境中还有另外一些相关的处理函数,例如objc_msgSend_stret
、objc_msgSend_fpret
、objc_msgSendSuper
就不在一一介绍。
另外提一个点,OC对象的每一个方法当编译成C语言的时候可以看成是下面这种的形式的
<returnType> Class_selector(id self, SEL _cmd, ...)
其中的方法名是随意起的,大家发现这个函数和objc_msgSend的形式很想,这是为了利用“尾调用优化”,是调用函数更简单、高效。
12. 理解消息转发机制
这小节介绍一下上面提到的消息转发机制,大家都知道,触发了消息转发机制,是因为我们没有找到对应的方法,下面看消息转发机制怎么处理这个问题。
介绍一下消息转发机制,大致分为三个阶段:
1.第一阶段,动态方法解析
对象在无法解读方法的时候,首先会调用所属类下面这个方法
+ (BOOL)resolveInstanceMethod:(SEL)sel
sel
就是方法名,返回值为Boolean类型,表示这个类是否能新增实例方法处理这个方法(如果是类方法会调用+ (BOOL)resolveClassMethod:(SEL)sel
方法),我们需要自定义一些处理方法,用于动态添加到类中,用以解决问题(可以看后面的例子),如果这一步不能解决问题,转到第二阶段。
2.第二阶段,备援接收者
来到这一步,我们就要改变解决问题的思路,既然这个类不能处理这个方法,我们可不可以找别的类处理,这时候对应的处理方法:
- (id)forwardingTargetForSelector:(SEL)aSelector
aSelector是方法名,如果当前类能够找到一个类帮忙处理这个方法,就返回这个类,若找不到就放回nil(通过这个方法我们可以实现类似“多继承”)。
3.第三阶段,完整的消息转发
如果已经来到了这一步,我们就要做一个完整的消息转发。首先创建一个NSInvocation对象,把未处理方法的所有信息封装在里面,此对象包含方法名、目标、参数,这一步要调用下面的方法:
- (void)forwardInvocation:(NSInvocation *)anInvocation
这一步处理的方法很简单,就是在新的类上调用方法,如果这样做的话就和第二阶段没有什么差别了。通常在这一步的时候会做一些改进,会选择某种方式改变消息内容,例如追加参数,改变方法名等。
对于消息的处理,越早越好。
下面粘贴一个利用动态解析方法实现@dynamic属性的例子:
这个例子实现一个类,类似字典的功能,只不过写入和读取信息的时候用属性,而不是像字典一样用关键字。
.h文件中:
#import <Foundation/Foundation.h>
@interface EOCAutoDictionary : NSObject
@property (nonatomic, strong) NSString *string;
@property (nonatomic, strong) NSNumber *number;
@property (nonatomic, strong) NSData *date;
@property (nonatomic, strong) id opaqueObject;
@end
.m文件中:
#import "EOCAutoDictionary.h"
#import <objc/runtime.h> // 主要头文件的引用
@interface EOCAutoDictionary ()
@property (nonatomic, strong) NSMutableDictionary *backingStore;
@end
@implementation EOCAutoDictionary
@dynamic string, number, date, opaqueObject;
- (id)init{
if ((self = [super init])) {
_backingStore = [NSMutableDictionary new];
}
return self;
}
+(BOOL)resolveInstanceMethod:(SEL)sel{
NSString *selectorString = NSStringFromSelector(sel);
// 通过是否以“set”开头判断方法名
if ([selectorString hasPrefix:@"set"]) {
/**
* 向类中添加一个方法
* 参数一 指定类名.
* 参数二 新添加的方法的方法名.
* 参数三 函数指针,指向待添加方法.
* 参数四 待添加方法的类型编码.
*/
class_addMethod(self, sel, (IMP)autoDictionarySetter, "v@:@");
} else {
class_addMethod(self, sel, (IMP)autoDictionaryGetter, "@@:");
}
return YES;
}
id autoDictionaryGetter(id self, SEL _cmd){
// 拿到存储数据的字典
EOCAutoDictionary *typedSelf = (EOCAutoDictionary *)self;
NSMutableDictionary *backingStore = typedSelf.backingStore;
// 拿到方法名
NSString *key = NSStringFromSelector(_cmd);
// 返回对应的值
return [backingStore objectForKey:key];
}
void autoDictionarySetter(id self, SEL _cmd, id value){
// 拿到存储数据的字典
EOCAutoDictionary *typedSelf = (EOCAutoDictionary *)self;
NSMutableDictionary *backingStore = typedSelf.backingStore;
// 拿到方法名并对其进行处理
NSString *selectorString = NSStringFromSelector(_cmd);
NSMutableString *key = [selectorString mutableCopy];
// 移除方法名中的“:”
[key deleteCharactersInRange:NSMakeRange(key.length-1, 1)];
// 移除方法名中的“set”
[key deleteCharactersInRange:NSMakeRange(0, 3)];
// 将方法名第一个字符转为小写
NSString *lowercaseFirstChar = [[key substringToIndex:1] lowercaseString];
[key replaceCharactersInRange:NSMakeRange(0, 1) withString:lowercaseFirstChar];
// 如果有值,写入字典中
if (value) {
[backingStore setObject:value forKey:key];
} else {
[backingStore removeObjectForKey:key];
}
}
@end
EOCAutoDictionary的用法也很简单,只要直接通过对应的属性名,就可以进行数据的存储。
13. 用“方法调配技术”调试“黑盒方法”
方法调配技术,简言之就是,将方法名和方法实现分割开来,任意组合。这样一来我们可以任意改变一个方法的实现,另外还可以通过这种办法给原有方法添加功能,对不知道内部实现的方法添加提示语句(黑盒调试)等等。
之所以能这么做,主要是因为方法均以指针的形式来表示,这种指针叫IMP,我们在调用方法的时候,只要将指针指向改变,就能实现我们想要的效果,运用起来也很简单,通过下面的例子大家就会运用(注意运行时头文件的引用):
Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
Method swappedMethod = class_getInstanceMethod([NSString class], @selector(uppercaseString))
method_exchangeImplementations(originalMethod, swappedMethod);
通过上面的例子,我们就把NSString的lowercaseString方法和uppercaseString方法调换了,是不是很简单。
其实这样做并没有什么意义,因为具体的方法实现已经都存在了,我们没必要改变一个方法实现,但是我们通过这种方法给已知的方法添加功能,例如下面的例子:
.h文件:
@interface NSString (EOCMyAdditions)
- (NSString *)eoc_myLowercaseString; // 在分类中给NSString添加功能
@end
.m文件:
@implementation NSString (EOCMyAdditions)
- (NSString *)eoc_myLowercaseString{
NSString *lowercase = [self eoc_myLowercaseString];
NSLog(@"%@ => %@", self, lowercase);
return lowercase;
}
@end
然后我们使用方法调配技术,将上面的方法和lowercaseString方法进行调换:
Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
Method swappedMethod = class_getInstanceMethod([NSString class], @selector(eoc_myLowercaseString));
method_exchangeImplementations(originalMethod, swappedMethod);
这样执行完后,当我们再调用lowercaseString方法的时候会有下面的结果:
NSString *string = @"This is tHe StRing";
NSString *lowercaseString = [string lowercaseString];
// Output:This is tHe StRing => this is the string
通过这个方法我们发现,我们可以为那些不知道内部实现的黑盒方法添加日志记录功能。
一般来说,我们很少用“方法调配”,只有在调试程序的时候才需要在运行期修改方法实现。
14. 理解“类对象”的用意
首先我们要知道,OC的实例对象是指向某块内存数据的指针,所以在声明变量时,要用*号。同时我们知道OC中有一种通用对象类型“id”(id本身已是一个指针),所以我们在用“id”声明变量的时候可能和平常有点不同:
NSString *aString = @"some string";
id aString = @"some string";
上面两种定义方式相比,语法意义相同,区别在于,指定具体类型后,当实例调用方法的时候,编辑器会给我们提示。
下面看一下“id”类型的定义:
typedef struct objc_object *id;
id其实是objc_object类型的结构体,而objc_object定义如下:
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
结构体中是一个Class类型的变量,该变量定义对象所属的类。下面我们看一下Class类型是个什么东西:
typedef struct objc_class *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;
我们看到,这个结构体存放类的各种信息(元数据),例如类有多少个实力变量,类名等等信息。
通过上面的关系,我们知道在objc的runtime中,类是用objc_class结构体表示的,对象是用objc_object结构体表示的, 对象的isa用来标示这个对象是哪个类的实例。
这些源码是属于objc runtime的,objc runtime的源代码苹果已经开源了,你可以在这里下载到objc的runtime源代码。
其实到这里大家可能会有一个疑问,为什么objc_class结构体里面也有一个isa,那么这个isa指向谁呢?我们往下看,[NSObject class],这里我们调用了+ (Class)class这个类方法,我们再开发中经常用到这个方法,它返回的是这个类所属的Class类型。+ (Class)class类方法的实现源码是这样的:
+ (Class)class {
return self;
}
为什么会返回self,self总是指的自身,而在这里没有实例啊!这时候看开发文档我们会发现,实际上函数的返回值是一个类对象class object,所以其本质上还是一个对象而已。既然是一个对象,它拥有一个self指针也就不奇怪了,所以对于像NSObject这样的类来说,它其实代表的是一个类对象,本质上还是一个普通的实例对象,那么又会问了,这个类对象是谁的实例呢?很遗憾,要找到这个问题的答案,我们在 objc runtime 这一层上已经没办法办到了,我们需要到更低层,也就是 objc 语言层去寻找答案了,但是 objc 语言层是不开源的,如果想继续学习,大家可以在网上找模仿OC低层的代码。
以上了解一下就好,我们只要知道类的继承体系就行了,下面用一个例子:有一个类(暂且叫SomeClass)继承于NSObject,那么这些类和元类的继承关系是,SomeClass实例有一个isa指针指向SomeClass类,SomeClass类有一个isa指针指向SomeClass元类,NSObject类也有一个isa指针指向NSObject元类,SomeClass的父类是NSObject,SomeClass元类的父类是NSObject元类,通过这种关系,我们在类继承体系中查询类型信息,用isMenberOfClass:
判断对象是否是某个特定类的实例,用isKindOfClass:
判断对象是否为某类或其派生类的实例。因为OC是动态型语言的特性,上面两个方法非常有用。
有时我们可以用比较类对象是否等同的办法来进行比较,这时要用==
操作符,而不是用isEqual方法,因为类对象是单利,在应用程序中,每个类的类对象只有一个实例,也就是说另外一种判断对象是否为某类实例的办法是:
id object = /*...*/
if ([object class] == [SomeClass class]){
}
这一部分基本都是关于OC运行时的知识,可能我们平时写代码的时候涉及很少,但是了解这些,对于我们的开发是很有帮助的,OC运行时是一个很强大的东西,有兴趣的同学可以好好研究一下。