Effective Objective-C 2.0 读书笔记二(下)

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_stretobjc_msgSend_fpretobjc_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运行时是一个很强大的东西,有兴趣的同学可以好好研究一下。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容