本文共 11783 字,大约阅读时间需要 39 分钟。
内容来自《Effective Objective-C 2.0》
关联对象(Associated Object)
有时候需要在对象中存放相关信息。这时我们通常可以从对象所属的类中继承一个子类,然后改用这个子类对象。然而并不是所有情况下都能这么做,有时候类的实例可能是由某种机制创建的,开发者无法令这种机制创建出自己所写的子类实例。Objective-C中有一项强大的特性可以解决此问题,这就是“关联对象(Associated Object)”。
可以给对象关联许多其他对象,这些对象通过“键”来区分。存储对象值的时候,可以指明其“存储策略”(storage policy),用于维护相应的“内存管理语义”。存储策略由名为objc_AssociationPolicy的枚举定义。如下:
关联类型 | 等效的@property属性 |
---|---|
OBJC_ASSOCIATION_ASSIGN | assign |
OBJC_ASSOCIATION_RETAIN_NONATOMIC | nonatomic,retain |
OBJC_ASSOCIATION_COPY_NONATOMIC | nonatomic,copy |
OBJC_ASSOCIATION_RETAIN | retain |
OBJC_ASSOCIATION_COPY | copy |
以下方法可以管理关联对象:
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)
此方法移除指定对象的全部关联对象
关联对象用法举例
使用UIAlertView时,要使用代理,这样就得把创建警告视图和处理按钮动作的代码分开。如下:
- (void)askUserAQuestion{ UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Question" message:@"What do you want to do?" delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"Continue", nil]; [alert show];}- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex{ if (buttonIndex == 0) { [self doCancel]; }else{ [self doContinue]; }}
但如果想在同一个类里处理多个警告信息视图,那么代码将会变得更为复杂。
要是能在创建警告视图的时候直接把处理每个按钮的逻辑都写好,那就简单多了。这可以通过管理对象来做。创建完警告视图之后,设定一个与之关联的“块(block)”,等到执行delegate方法时再将其读出来。此方案的实现代码如下:
static void *EOCMyAlertViewKey = "EOCMyAlertViewKey";- (void)askUserAQuestion{ UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Question" message:@"What do you want to do?" delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"Continue", nil]; void(^block)(NSInteger) = ^(NSInteger buttonIndex){ if (buttonIndex == 0) { [self doCancel]; }else{ [self doContinue]; } }; objc_setAssociatedObject(alert, EOCMyAlertViewKey, block, OBJC_ASSOCIATION_COPY); [alert show];}- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex{ void (^block)(NSInteger) = objc_getAssociatedObject(alertView, EOCMyAlertViewKey); block(buttonIndex);}
以这种方式改写之后,创建警告视图与处理操作结果的代码都放在了一起。
也可以参考
要点
- 可以通过“关联对象”机制把两个对象连起来
- 定义管理对象是可指定内存管理语义,用以模仿定义属性时所采用的“拥有关系”与“非拥有关系”。
- 只有在其他做法不可行时才应选用关联对象,因为这种做法会引入难于查找的bug。
参考文档
理解objc_msgSend的作用
在Objective-C中,如果向某对象传递消息,那就会使使用动态绑定机制来决定需要调用的方法。在底层,所有方法都是普通的C语言函数,然而对象收到消息之后,究竟该调用哪个方法则完全于运行期决定,甚至可以在程序运行时改变,这些特性使得Objective-C成为一门正真的动态语言。
给对象发消息可以这样写:
id returnValue = [someObject messageName: parameter];
someObject叫做“接受者”(receiver),messageName叫做“选择子”(selector)。选择子与参数合起来称为“消息”(message)。编译器看到此消息后,将其转换为一条标准的C语言函数调用,所调用的函数是消息传递机制中的核心函数,叫做objc_msgSend,其“原型”(prototype)如下:
void objc_msgSend(id self, SEL cmd, ...)
第一参数代表接受者,第二个参数代表选择子(SEL是选择子的类型),后续参数就是消息中的那些参数。
编译器会把刚才那个例子的消息转换为如下函数:id returnValue = objc_msgSend(someObject, @selector(messageName:), parameter);
objc_msgSend函数会依据接受者和选择子的类型来调用适当的方法。为了完成此操作,该方法需要在接受者所需的类中搜寻其“方法列表”(list of methods),如果能找到与选择子名称相符的方法,就跳至其实现代码。若找不到,那就沿着继承体系继续向上查找,等找到合适的方法之后再跳转。如果最终还是找不到相符的方法,那就执行“消息转发”(message forwarding)操作。
消息转发机制message forwarding
消息转发分为两个阶段:
- 第一阶段先征询接收者,所属的类,看其是否能动态添加方法,以处理当前这个“未知的选择子”(unknown selector),这叫做“动态方法解析”(dynamic method resolution)。
第二阶段涉及“完整的消息转发机制”(full forwarding mechanism)。如果运行期系统已经把第一阶段执行完了,那么接收者自己就无法再以动态新增方法的手段来响应包含该选择子的消息了。此时,运行期系统会请求接受者以其他手段来处理与消息相关的方法调用。这又分为两小步。
- 首先,请接收者看看有没有其他对象能处理这条消息。若有,则运行期系统会把消息转给那个对象,于是消息转发过程结束。
- 若没有“备援的接收者”(replacement receiver),则启动完整的消息转发机制,运行期系统会把与消息有关的全部细节都封装到NSInvocation对象中,再给接收者最后一个机会,令其设法解决当前还未处理的这条消息。
动态方法解析
对象在收到无法解读的消息后,首先将调用其所属类的下列类方法:
+ (BOOL)resolveInstanceMethod:(SEL)selector
该方法的参数是未知的选择子,其返回值为Boolean类型,表示这个类是否能新增一个实例方法用以处理此选择子。在继续往下执行转发机制之前,本类有机会新增一个处理此选择子的方法。假如尚未实现的方法不是实例方法而是类方法,那么运行期系统就会调用另外一个方法,叫做“+ (BOOL)resolveClassMethod:(SEL)sel ”。
使用这种办法的前提是:相关方法的实现代码已经写好,只等运行的时候动态插入在类里面就可以了。此方案常用来实现@dynamic属性。
下列代码演示了如何使用“resolveInstanceMethod”来实现@dynamic属性:id autoDictionaryGetter(id self, SEL _cmd);void autoDictionarySetter(id self, SEL _cmd, id value);+ (BOOL)resolveInstanceMethod:(SEL)selector{ NSString *selectorString = NSStringFromSelector(selector); if (/*selector is from a @dynamic property*/) { if ([selectorString hasPrefix:@"set"]) { class_addMethod(self, selector, (IMP)autoDictionarySetter, "v@:@"); }else{ class_addMethod(self, selector, (IMP)autoDictionaryGetter, "v@:"); } return YES; } return [super resolveInstanceMethod:selector];}
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types) 方法的说明如下:
- cls:The class to which to add a method.
- name:A selector that specifies the name of the method being added.
- imp:A function which is the implementation of the new method. The function must take at least two arguments—self and _cmd.
- types:An array of characters that describe the types of the arguments to the method. For possible values, see > . Since the function must take at least two arguments—self and _cmd, the second and third characters must be “@:” (the first character is the return type).
本例中”v@:@”的意思是,返回值为void,第一个参数为id类型为@,第二个参数为SEL为:,第三个参数为id类型为@。
备援接收者
当前接收者还有第二次机会能处理未知的选择子,在这一步中,运行期系统会问它:能不能把这条消息转给其他接收者来处理。该步骤对应的处理方法如下:
- (id)forwardingTargetForSelector:(SEL)aSelector
方法参数代表未知的选择子,若当前接收者能找到备援对象,则将其返回,若找不到,就返回nil。通过此方案,我们可以用“组合”(composition)来模拟出“多重继承”(multiple inheritance)的某些特性。
请注意,我们无法操作经由这一步所转发的消息。若是想在发送给备援接收者之前先修改消息内容,那就得通过完整的消息转发机制来做了完整的消息转发
如果转发算法到了这一步的话,那么唯一能做的就是启动完整的消息转发机制了。首先创建NSInvocation对象,把尚未处理的那条消息有关的全部细节都封装于其中,此对象包含了选择子、目标(target)及参数。在触发NSInvocation对象时,“消息派发系统”(message-dispatch system)将亲自出马,把消息指派给目标对象。
此步骤会调用下列方法来转发消息:
- (void)forwardInvocation:(NSInvocation *)anInvocation
这个方法可以实现的很简单:只需改变调用目标,时消息在新目标上得以调用即可。然而这样实现出来的方法与“备援接收者”方案所实现的方法等效,所以很少有人采用这么简单的实现方式。比较有用的实现方式为:在触发消息前,先以某种方式改变消息的内容,比如追加另外一个参数,或是改换选择子,等等。
实现此方法时,若发现某调用操作不应由本类处理,则需调用超类的同名方法。这样的话,继承体系中每个类都有机会处理此调用请求,直至NSObject。如果最后调用了NSObject类的方法,那么该方法还会继续而调用“doesNotRecognizeSelector”以抛出异常,此异常表明选择子最终未能得到处理。消息转发全流程
接收者在每一步中均有机会处理消息。步骤越往后,处理消息的代价就越大。最好能在第一步就处理完,这样的话,运行期系统就可以将此方法缓存起来了。如果这个类的实例稍后还收到同名选择子,那么根本无须启动消息转发流程。若想在第三步里把消息转给备援的接收者,那还不如把转发操作提前到第二步。因为第三步只是修改了调用目标,这项改动放在第二步执行会更为简单,不然的话,还得创建并处理完整的NSInvocation。
以完整的例子演示动态方法解析
为了说明消息转发的意义,下面示范如何以动态方法解析来实现@dynamic属性。假设要编写一个类似于“字典”的对象,它里面可以容纳其他对象,只不过开发者要直接通过属性来存取其中的数据。这个类的设计思路是:由开发者来添加属性定义,并将其声明为@dynamic,而类则会自动处理相关属性值的存放与获取操作。
该类的接口可以写成:
#import@interface EOCAutoDictionary : NSObject@property (nonatomic, strong) NSString *string;@property (nonatomic, strong) NSNumber *number;@property (nonatomic, strong) NSDate *date;@property (nonatomic, strong) id opaqueObject;@end
将属性声明为@dynamic,这样的话,编译器就不会为其自动生成实例变量及存取方法了:
#import "EOCAutoDictionary.h"#import@interface EOCAutoDictionary ()@property (nonatomic, strong) NSMutableDictionary *backingStore;@end@implementation EOCAutoDictionary- (instancetype)init{ if (self = [super init]) { _backingStore = [NSMutableDictionary new]; } return self;}
本例的关键在于resolveInstanceMethod:方法的实现代码:
+ (BOOL)resolveInstanceMethod:(SEL)sel{ NSString *selectorString = NSStringFromSelector(sel); if ([selectorString hasPrefix:@"set"]) { class_addMethod(self, sel, (IMP)autoDictionarySetter, "v@:@"); }else{ class_addMethod(self, sel, (IMP)autoDictionaryGetter, "@@:"); } return YES;}
getter函数可以用下列代码实现:
id autoDictionaryGetter(id self, SEL _cmd){ //从对象中获取backing store EOCAutoDictionary *typeSelf = (EOCAutoDictionary *)self; NSMutableDictionary *backingStore = typeSelf.backingStore; //key是selector的名称 NSString *key = NSStringFromSelector(_cmd); //返回值 return [backingStore objectForKey:key];}
而setter函数则可以这么写:
void autoDictionarySetter(id self, SEL _cmd, id value){ //从对象中获取backing store EOCAutoDictionary *typeSelf = (EOCAutoDictionary *)self; NSMutableDictionary *backingStore = typeSelf.backingStore; //例如selector为“setOpaqueObject:”,我们需要移除掉“set”,“:”,并把剩余字符串的首字母小写 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]; }}
EOCAutoDictionary的用法很简单:
EOCAutoDictionary *dict = [EOCAutoDictionary new];dict.date = [NSDate dateWithTimeIntervalSince1970:475372800];NSLog(@"dict.date = %@", dict.date);//dict.date = 1985-01-24 00:00:00 +0000
方法调配(method swizzling)
Objective-C对象收到消息之后,究竟会调用何种方法需要在运行期才能解析出来。那你也许会问:与给定的选择子名称相对应的方法是不是也可以在运行期改变呢?没错,就是这样。若能善用此特性,则可以发挥巨大优势,因为我们既不需要源代码,也不需要通过继承子类来覆写方法就能改变这个类本身的功能。这样一来,新功能将在本类的所有实例中生效,而不是仅限于覆写了相关方法的那些子类实例。此方案经常成为“方法调配(method swizzling)”。
类的方法列表会把选择子的名称映射到相关的方法实现之上,使得“动态消息派发系统”能够据此找到应该调用的方法。这些方法均以函数指针的形式来表示,这种指针叫做IMP,其原型如下:
id (*IMP)(id, SEL, ...)
NSString类可以响应lowercaseString,uppercaseString,capitalizedString等选择子。这张映射表中的每个选择子都映射到了不同的IMP之上。
Objective-C运行期系统提供的几个方法都能够用来操作这张表。开发者可以向其中新增选择子,也可以改变其选择子所对应的方法实现,还可以交换两个选择子所映射到的指针。
想交换方法实现,可用下列函数:
void method_exchangeImplementations(Method m1, Method m2)
此函数的两个参数表示待交换的两个方法实现,而方法实现则可以通过下列函数或得:
Method class_getInstanceMethod(Class cls, SEL name)
此函数根据给定的选择从类中取出与之相关的方法。执行下列代码,即可交换前面提到的lowercaseString与uppercaseString方法实现:
Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString)); Method swappedMethod = class_getInstanceMethod([NSString class], @selector(uppercaseString)); method_exchangeImplementations(originalMethod, swappedMethod);
从现在开始,如果在NSString实例上调用lowercaseString,那么执行的将是uppercaseString的原有实现:
NSString *string = @"ThIs iS tHe StRiNg";NSString *lowercaseString = [string lowercaseString];NSLog(@"lowercaseString = %@", lowercaseString);//lowercaseString = THIS IS THE STRINGNSString *uppercaseString = [string uppercaseString];NSLog(@"uppercaseString = %@", uppercaseString);//uppercaseString = this is the string
可以通过这一手段为既有的方法实现增添新功能。比方说,想要在调用lowercaseString时记录某些信息,这时就可以通过交换方法实现来达成此目标。我们新编写一个方法,在此方法中实现所需的附加功能,并调用原有实现。
新方法可以添加至NSString的一个“分类(category)”中:#import@interface NSString (EOCMyAdditions)- (NSString *)eoc_myLowercaseString;@end
上述新方法将与原有的lowercaseString方法互换,交换之后的方法表如下:
新方法的实现代码可以这样写:
@implementation NSString (EOCMyAdditions)- (NSString *)eoc_myLowercaseString{ NSString *lowercase = [self eoc_myLowercaseString]; NSLog(@"%@ => %@", self, lowercase); return lowercase;}@end
这段代码看上去好像会陷入递归调用的死循环,不过大家要记住,此方法是准备和lowercaseString方法互换的。所以在运行期,eoc_myLowercaseString选择子实际上对应于原有的lowercaseString方法实现。最后,通过下列代码来交换这两个方法实现:
Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));Method swappedMethod = class_getInstanceMethod([NSString class], @selector(eoc_myLowercaseString));method_exchangeImplementations(originalMethod, swappedMethod)
执行完上述代码之后,只有在NSString实例上调用lowercaseString方法,就会输出一行记录消息:
NSString *string = @"ThIs iS tHe StRiNg";NSString *lowercaseString = [string lowercaseString];//ThIs iS tHe StRiNg => this is the string
通过此方案,开发者可以为那些“完全不知道其具体实现的”(completely opaque,“完全不透明”)黑盒方法增加日志记录功能,这非常有助于程序调试。
其它应用实例
参考文档
转载地址:https://windzen.blog.csdn.net/article/details/48288837 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!