编写高质量iOS与OS X代码的52个有效方-Effective Objective-C 2.0阅读笔记

第1条:了解基本OC对象

NSString *someString = @"The String";

这种语法基本上是办照C语言的,它声明一个名为 someString的变量,类型为NSString* 。也就是说此变量指向 NSString的指针。所有Objective-C语言的对象都必须这样声明,因为对象所占内存总是分配在”“堆空间”(heap space)中, 而绝不会分配在”“栈”(stack)上。不能在栈中分配 Objective-C 对象;

someString 变量指向分配在堆里的某个内存区域,其中包含一个NSString对象。也就是说,如果再创建一个变量,另其指向同一个地址,那么并不会拷贝该对象,只是这两个变量会同时指向此对象:

NSString *someString = @"The String";
NSString *anotherString = someString;

只有一个 NSString 实例,然而有两个变量指向此实例。两个变量都是 NSString* 类型,这说明当前”“栈帧”(stack frame)里分配了两块内存,每块内存的大小都能容下一枚指针(在32位架构的计算机上是4字节,64位计算机上是8字节)。这两块内存里的值都一样,就是NSString实例的地址。

图1-1

图1-1 此内存布局图演示了一个分配在堆中的NSSting实例,有两个分配在栈上的指针指向该实例。

分配在堆中的内存必须直接管理,而分配在栈上用于保存变量的内存则会在其栈帧弹出时自动清理。

OC将堆内存管理抽象出来了。不需要malloc以及free来分配或者释放内存。OC运行期环境把这部分工作抽象为一套内存管理架构,名为引用计数器

在OC代码中,有时会遇到定义不含*的变量,它们可能会使用”“栈控件”(stack space)。这些变量所保存的不是OC对象。比如CoreGraphice框架中的CGRect:

CGRect frame;
frame.origin.x = .0f;
frame.origin.y = .0f;
frame.size.width = 100.0f;
frame.size.height = 200.0f;

CGRect 是 C 结构体,其定义是:

struct CGRect {
    CGPoint origin;
    CGSiez size;
};
typedef struct CGRect CGRect;

整个系统框架都在使用这种结构体,因为如果改用OC对象来做的话,性能会受影响。与创建结构体相比,创建对象还需要额外开销,例如分配及释放内存等。如果只需保存 int、float、double、char等”“非对象类型”(non object type),那么使用CGRect 这种结构体就可以了。

  • 我们可以利用这一特点、运用到开发中。

第2条: 在类的头文件中尽量少引入其他头文件


在编译一个MSPerson类的文件时,不要知道 MSEmployer 类的全部细节,只需要知道有一个类名叫 MSEmployer 就好:

@Class MSEmployer
这叫做向前声明(forward declaring)该类。这样 MSPerson 就能设置 MSEmployer 类型的属性

@Class MSEmployer
@Interface MSPerson : NSObject
@porperty (nonatomic, strong) MSEmployer *employer;
@porperty (nonatomic, copy) NSSting *name;
@end

如果MSPerson类需要知道B类头文件的所有细节,那么通过使用#improt "MSEmployer"替换@Class MSEmployer

将引入头文件的时机尽量延后,只在却有需要时才引入,这样就可以减少类的使用者所需引入的头文件的数量。

假设将 MSEmployer.h 引入到 MSPerson.h 中,那么其他的类引入 MSPerson.h ,就一并会引入MSEmployer.h的所有内容。此过程若持续下去,则要引入许多根本用不到的内容,这当然会增加编译时间。

向前声明(forward declaring)也解决了两个类互相引用的问题。假设要为MSEmployer类
加入新增及删除雇员的方法,那么其头文件会加入下述定义:

- (void)addEmployee:(MSPerson *)person;
- (void)remvoeEmployee:(MSPerson *)person;

此时需要编辑 MSEmployer,则编辑器必须知道 MSPerson 这个类,而编译 MSPerson,则又必须知道 MSEmployer。如果在各自的头文件引入对方的头文件,则导致”“循环引用”。

当解析其中一个头文件时,编辑器会发现它引入了另一个头文件,而那个头文件又回过头来引用第一个头文件。使用#Import而非 #include指令虽然不会导致死循环,但却这意味着两个类有一个无法被正确编译。

向前声明只能告诉编辑器有某个类,某个协议。

必须引入头文件

  • 继承
  • 遵守某个协议,且知道协议方法

第二条的#import 是难免的。鉴于此,最好是把协议单独放在一个头文件中。


第4条: 多用类型常量,少用#define预处理指令


编码时经常要定义常量。例如,要写一个UI视图类,此视图显示出来就播放,然后消失。你可能想把播放动画的时间提取为常量。掌握了OC与C语言基础的人,也许会用这种方法来做:

#define ANIMATION_DURATION 0.3

上述预处理指令会把源代码中的 ANNIMATION_DURATION 字符串替换为0.3。这可能就是你想要的效果,不过这样定义出来的常量没有类型信息。”持续”(dutation)这个词看上去应该与时间有关,但是代码中又未明确指出。此外,预处理过程会把碰到的所有 ANIMATION_DURATION 一律替换成0.3, 这样的话,假设此指令在某个头文件中,那么所有引入了这个头文件的代码,其ANMIATION_DUTATION都会被替换。


第9条:以”类族模式” 隐藏实现细节


“类族”是一中很有用的模式,可以隐藏”抽象基类”,背后的实现实现细节。OC的框架中普遍使用此模式。比如,iOS 的用户界面框架 UIKit 中就有一个名为 UIButton 的类。想创建按妞,需要调用下面这个 “类方法”:

+ (UIButton *)buttionWtihType:(UIButtonType)type;

该方法返回的对象,其类型取决于传入的按钮类型(Button type)。然而,不管返回什么类型的对象,它们都继承自同一个基类: UIButton。这么做的意义在于: UIButton 类的使用者无需关心创建出来的按钮具体属于哪个子类,也不用考虑按钮的绘制方式等实现细节。使用者只需要明白如何创建按钮。如何设置项想 “标题(title)” 这样的属性,如何增加触摸动作的目标对象等问题就好。

回到开头说的那个问题上,我们可以把各种按钮的绘制逻辑都放在一个类里,并根据按钮类型来切换:

- (void)drawRect:(CGRect)rect {
    if  (_type = TypeA){
        //Draw 按钮A
    }else if (_type = TypeB){
        //Draw 按钮B
    }/* ... */
}

这样写现在看上去还算简单,然而,若是需要依按钮类型来切换的绘制方法有许多种,那么就会变得很麻烦了。优秀的程序员会将这种代码重构为多个子类,把各种按钮所用的绘制方式放到相关子类中去。不过,这么做需要用户知道各种子类才行。此时应该使用 “类族模式”,该模式可以灵活应对多个类。将它们的实现细节隐藏在抽象基类后面,以保持接口简洁。用户无需自己创建子类实例,只需要调用基类方法来创建即可。

创建类族

现在举例来演示如果创建类族。假设有一个处理雇员的类,每个雇员都有 “名字” 和 “薪水” 这个两个属性,管理者可以命令其执行日常工作。但是,各种雇员的工作内容却不同。经理在带领雇员做项目时,无需关系每个人如何完成其工作,仅需指示其开工即可。

下面看代码:

typedef NS_ENUM(NSUInteger, MSEmployeeType) {
    MSEmployeeTypeDeveloper,
    MSEmployeeTypeDesigner,
    MSEmployeeTypeFinance,
}

@interface  MSEmployee : NSObject
@property(copy) NSStirng *name;
@property NSUInterger salary;

///类方法创建MSEmployee实例
+ (MSEmployee *)employeeWhihType:(MSEmployeeType)type;

- (void)doADaysWork;

@end

@implementation MSEmployee

///类方法创建MSEmployee实例
+ (MSEmployee *)employeeWhihType:(MSEmployeeType)type {
    swicth(type){
          case MSEmployeeTypeDeveloper:
        return [MSEmployeeTypeDeveloper new];
        break;
           case MSEmployeeTypeDesigner:
        return [MSEmployeeTypeDesigner new];
        break;
           case MSEmployeeTypeFinance:
        return [MSEmployeeTypeFinance new];
        break;
    }
}
- (void)doADaysWork {
    // Subcalasses implement this.
}

@end

每个 “实体子类” 都是继承基类而来。例如:

@interface MSEmployeeDeveloper : MSEmployee
@end

@inplementation MSEmployeeDeveloper
- (void)doADaysWork {
    [self writeCode];
}
@end

在本例中,基类实现了一个 “类方法”, 该方法根据创建的雇员类别分配好对应的雇员实类实例。这种 “工厂模式” 是创建类族的办法之一。

可惜 OC 这门语言没办法指明某个基类是 “抽象的”。于是,开发者通常会在文档中写明类的用法。这种情况下,基类接口一般都没有 “init” 的初始化方法, 这暗示该类的实例也许不会由用户直接创建。还有一种办法可以确保用户不会使用基类实例,那就是在基类的 doADaysWrok 方法中抛出异常。然而这种方法相当极端,很少人用。(个人觉得一定程度上违反了 OC 对象基本架构)。

在 MSEmployee 这个例子中,【employee isMemberOfClass:[MSEmployee class]】似乎会返回YES,但实际上返回确是NO, 因为 employee 并非 MSEmployee 类的示例,而是其某个子类的示例。

Cocoa 里的类族

系统框架中有许多类族。大部分 collection 类都是类族。例如 NSArray 与其可变版本 NSMutableArray 。这样看来,实际上有两个抽象基类,一个用于不可变数组,另一个用于可变数组。尽管具备公共接口的类有两个,但仍然可以合起来算作一个类族。不可变的类定义了对所有数组都通用的方法,而可变的类则定义了那些只适用于可变数组的方法。两个类共同属于一个类族,这意味着二者在实现各自类型的数组时可以共用实现代码,此外,还能够把可变数组复制为不可变数组,反之亦然。


第10条


第11条:理解objc_msgSend的作用


在对象上调用方法是OC中常用的功能。用OC的术语来说,这叫传递消息。消息有 “名称” 或者 “选择器(selector)”,可以接受参数,而且可能还有返回值。

由于OC是C的超集,所以最好理解C的函数调用方式。C使用 “静态绑定”,也就是说,在编译期就能决定运行时所对应调用的函数。以下代码为例:

#import <stdio.h>

void printHello(){
    printf("Hello,world\n");
}
void printGoodbye(){
    printf("Goodbye,world\n");
}
void doTheThing(int type) {
    if (type ==0) {
        printHello();
    } else {
        printGoodbye();
    }
    return 0;
}

如果不考虑 “内联”,那么编译器在编译代码的时候就已经知道程序中有 printGoodbye 和 printHello 两个函数了,于是会直接生成调用这些函数的指令。而函数地址实际上是硬编码指令之中的。若是将刚才的代码写成下面这样,会如何呢?

#import <stdio.h>

void printHello(){
    printf("Hello,world\n");
}
void printGoodbye(){
    printf("Goodbye,world\n");
}
void doTheThing(int type) {
   void (* func)();
    if (type ==0) {
        func = printHello;
    } else {
       func = printGoodbye;
    }
    return 0;
}

这时就是使用 “动态绑定” 了,因为所要调用的函数直到运行期才能确定。编译器在这种情况下生成的指令与刚才那个例子不同,在第一个例子中,if与else语句里都有函数调用指令。而在第二个例子中,只有一个函数调用指令,不过待调用的函数地址无法硬编码在指令之中,而是要运行期读取出来。

在OC中,如果向对象传递消息,那就会使用动态机制来决定诀要调用的方法。在底层,所有的方法都是普通的C语言函数,然而对象收到消息后,究竟该调用哪个方法则完全于运行期决定,甚至可以再程序运行时改变,这些特性使得OC成为一门真正的动态语言。

给对象发送消息可以这样来写:

id returnValue = [someObject messageName:parameter];

在本例中,someObject 叫做 “接收者(receiver)”,messageName 叫做 “选择器(selector)”。选择器与参数合起来称为 “消息(message)”。编译器看到消息后,将其转换一条标准的 C 语言函数调用,所调用的函数乃是消息传递机制中的核心函数,叫做 objc_msgSend,其 “原型(prototype)”如下:

void objc_msgSend(id self, SEL cmd, ...)

这是个 “参数个数可变的函数”,能接受两个或者两个以上的参数。第一个参数代表接收者,第二个参数代表选择器(SEL是选择器的类型),后续参数就是消息中的那些参数,其顺序不变。选择器值得就是方法的名字。”选择器” 与 “方法” 这两个词经常交替使用。上述例子转换成C函数如下:

id returunValue = objc_msgSend(someObject, @selector(messageName:), parameter);

objc_msgSend 函数会依据接收者与选择器的类型来调用适当的方法。为了完成此操作,该方法需要在接收者所属的类中搜寻其 “方法列表”,如果能找到与选择器名称相符的方法,就跳至其实现代码。若是找不到,那就沿着继承体系继续向上查找,等找到合适的方法之后再跳转。如果最终还是找不到相符的方法,那就执行 “消息转发” 操作。

这么说来,想调用一个方法似乎需要很多步骤。所幸 objc_msgSend 会将匹配结果缓存在 “快速映射表”里面,每个类都有这样一块缓存,若是稍后还向该类发送与选择器相同的消息,那么执行起来就很快了。当然,这种 “快速执行路径” 还是不如 “静态绑定的函数调用操作”那样迅速,不过只要把选择器缓存起来,也不会慢很多,实际上,消息派发并非用应用程序的瓶颈所在。

小结:

  • 消息由接收者、选择器以及参数构成。给某对象 “发送消息” 也就相当于给在该对象上 “调用方法”。
  • 发给某对象的全部消息都要由 “动态消息派发系统”来处理,给系统会查出对应的方法,并执行其代码。

第 12 条: 理解消息转发机制


第 11 条讲解了对象的消息传递机制,并强强调了其重要性。第12条则要讲解另外一个重要的问题,就是对象在收到无法解读的消息之后会发什么什么情况。

若想令类能理解某条消息,我们必须以程序码实现出对应的方法才行。但是,在编辑期向类发送其无法解读的消息并不会报错,因为运行期可以继续向类中添加方法,所以编译器在编译时还无法确知类中到底会不会有某个方法实现。当对象接收到无法解读的消息后,就会启动 “消息转发”机制,程序员可经由此过程告诉对象应该如何处理未知消息。

你可能早就遇到过经由消息转发流程处理的消息,只是未加留意。如果在控制台中看到下面这种提示信息,那就说明你曾像某个对象发送一条无法解读的消息,从而启动了消息转发机制,并将此消息转发给了NSObject的默认实现。

 +[__NSCFNumber lowercaseString]: unrecognized selector sent to class 0x10c0e4600

 *** Terminating app due to uncaught exception 'NSInvalidArgumentException', 

 reason: '+[__NSCFNumber lowercaseString]: unrecognized selector sent to class 0x10c0e4600'

上面这段异常信息是由 NSObject 的 “doesNotRecognizeSelector:” 方法所抛出的,此异常表明: 消息接收者类型是__NSCFNumber, 而该接收者无法理解名为 lowercaseString的选择器。

消息转发分为两大阶段。第一阶段先征询接收者,所谓的类,看其是否能动态添加方法,以处理当前这个 “未知的选择器”,这叫做 “动态方法解析”。第二阶段涉及 “完整的消息转发机制”。 如果运行期系统已经把第一阶段执行完了,那么接收者自己就无法再以动态的新增方法的手段来响应包含该选择器的消息了。此时,运行期系统会请求接收者以其他手段来处理与消息相关的方法调用。这又细分为两小步。首先,请接收者看看有没有其他对象能处理这条消息。若有,则运行期系统则会把消息转给那个对象,于是消息转发过程结束,一切如常。若没有 “备援的接收者”,则启动完整的消息转发机制,运行期系统会把消息有关的所有细节都封装到 NSIvocation 对象中,再给接收者最后一次机会,另其设法解决当前还未出来的这条消息。

动态方法分析

对象在收到无法解读的消息后,首先调用其所属类的下列类方法。


+ (BOOL)resolveInstanceMethod:(SEL)selector

该方法的参数就是那个未知的选择器,其返回值是 Boolean 类型,表示这个类是否能新增一个实例方法用以处理此选择器。

加入尚未实现的方法不是实例方法而是类方法,那么运行期系统就会调用另一个方法,该方法与 “resolveInstanceMethod:”类似,叫做 “resolveClassMethod:”。

使用这种方法的前提是: 相关方法的实现代码已经写好,只等着运行的时候动态插在类里面就可以了。

此方案常用来实现@dynamic属性,比如说,要访问CoreData框架中的NSManagerObjects 对象的属性时就可以这么做,因为实现这些属性所需的存取方法在编译期就能确定。

id autoDictionaryGetter(id slef, 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,"@@:")
        }
        return YES;
    }
        return [super resolveInstanceMehtod:selector];
}

首选将选择器化为字符串,然后检车其是否表示设置方法。若前缀为set,则表示设置方法,否则就是获取方法。不管哪种情况,都会把处理该选择器的方法夹加到类里面,所添加的方法使用纯C函数实现的。C函数可能会用代码来操作相关的数据结构,类之中的属性数据就存放在那些数据结构里面。

备援接收者

当接收者还有第二次机会能处理未知的选择器,在这一步中,运行期系统会问它:能不能把这条消息转给其他接收者来处理。与该步骤对应的处理方法如下:

- (id)forwardingTargerForSelector:(SEL)selector

若当前接收者能找到备援对象,有则将其返回,否则返回nil。

通过此方案,我们可以用 “组合” 来模拟出 “多继承” 的某些特性。在一个对象内部,可能还有一些列的其他对象,该对象可经由此方法将能够处理某些选择器的相关内部对象返回,这样的话,在外界看来,好像是该对象亲自处理了这些消息似的。

在我们看完这里,反正我是比较蒙的。下面给大家写个具体实现,大家也就明白了。

//消息转发第二步 备选接收者
- (id)forwardingTargetForSelector:(SEL)aSelector{
    Developer *dev = [[Developer alloc] init:@"Huang"];
    if ([dev respondsToSelector:aSelector]) {
        return dev;
    }
    return [super forwardingTargetForSelector:aSelector];
}

示例中创建了Developer示例对象,把消息转由Developer对象处理。既然我们创建了Developer对象,那么我们就获得Developer对象相关的属性已经方法,就能模拟出 “多继承”了。

请注意: 我们无法操作经由这一步所转发的消息。若是想在发送给备援接收者之前修改消息内容,那就得通过完整的消息转发机制了。

完整的消息转发

如果转发算法已经来到这一步的话,那么唯一的能做的就是启动完整的消息转发机制。

首先创建 NSInvocation 对象,把尚未处理的那条消息有关的全部细节都封于其中。此对象包含选择器、目标以及参数。

在触发 NSInvocation 对象时,”消息派发系统” 将亲自出马,把消息指派给目标对象。

完整的消息转发步骤会调用下列方法来转发消息:

- (void)forwardInvocation:(NSInvocation *)invocation;

这个方法可以实现得很简单:

  • 只需要改变调用目标,使消息在新目标上得以调用即可。然而这样实现出来的方式与 “备援接收者” 方案所实现的方法等效,所以很少有人采用这么简单的实现方式。

  • 比较有用的实现方式为: 在触发消息前,先以某种方式改变消息内容,比如追加另外一个参数,或是改换选择器等等。

实现此方法时,若发现某调用操作不应由本类处理,则需调用超类的同名方法。这样的话,继承体系中的每个子类都会有机会处理此调用请求,直至 NSObject。如果最后调用了 NSObject类的方法,那么该方法还会继而调用 “doesNotRecognizeSelector:” 以抛出异常,表明选择器最终未得到处理。

消息转发全流程

消息转发

接收者在每一步骤中均有机会处理消息。步骤越往后,处理消息的代价就越大。最好是在第一步就处理完,这样的话,运行期系统就可以将此方法缓存起来了。如果这个类的实例稍后还收到同名选择器,那么根本无须启动消息转发流程。

以完整的例子演示动态方法分析

为了说明消息转发机制的意义,下面示范如何解析来实现 @dynamic 属性。假设要编写一个类似于 “字典” 的对象,它里面可以容纳其他对象,只不过开发者要直接通过属性来存取其中的数据。这个类的设计思路是: 由开发者来添加属性定义,并将其声明为 @dynamic,而类则会自动处理相关属性值的存放与获取操作。

#improt <Foundation/Foundation.h>
@interface EOCAutoDictionary : NSObject
@property (nonatomic, strong) NSString *string;
@property (nonatomic, strong) NSNumber *number;
@property (nonatomic, strong) NSDate *date
@property (nonatomic, strong) id opaqueObject;
@end

基础实现部分

#improt "EOCAutoDictionary.h"
#improt <objc/runtime.h>

@interface EOCAutoDictionary ()
@property (nonatomic, strong) NSMutableDictionary *backingStore;
@end

@implementation EOCAutoDictionary
@dynamic string, number, date, opaqueObject;

- (id)init {
    self = [super init];
    if (self){
        _backingStore = [NSMutableDictionary new];
    }
    return self;
}

@end

本例的关键在于 resolveInstanceMethod: 方法的实现代码:

+ (BOOL)resolveInstanceMethod:(SEL)selector {
    NSString *selectorString = NSStringFromSelector(selector);
    if ([selectorString hasPrefix:@"set"]){
        class_addMethod(self, selector, (IMP)autoDictionarySetter,"v@:@");
    }else{
         class_addMethod(self, selector, (IMP)autoDictionaryGetter,"@@:");
    }
    return YES;
}

当开发者首次在 EOCAutoDictionary 实例上访问某个属性时,运行期系统还找不到对应的选择器,因为所需的选择器既没有直接实现,也没有合成出来。现在假设要写入 opaqueObject 属性,那么系统就会以 “setOpaqueObject:” 为选择器来调用上面这个方法。同理,在读取该属性时,系统也会调用上述方法,只不过传入的选择器是 opaqueObject。

resolveInstanMethod 方法会判断选择器的前缀是否为set,以此分辨是 set 选择器还是 get 选择器。在这两种情况下,都要向类中新增一个处理该选择器所用的方法,这两个方法分别以 autoDictionarySetter 及 autoDictionaryGetter 函数指针的形式出现。此时就用到了 class_addMethod 方法,它可以向类中动态的添加方法,用以处理给定的选择器。第三个参数为函数指针,指向待添加的方法。而最后一个参数则表示待添加方法的 “类型编码”。在本例中,编码开头的字符标识方法的返回值类型,后续字符表示其所接受的各个参数。

getter 函数可以用下列代码实现:

id autoDictionaryGetter(id self, SEL _cmd) {
    // Get the backing store from the object
    EOCAutoDictionary *typedSelf = (EOCAutoDictionary *)self;
    NSMutableDictionary *backingStore = typedSelf.backingStore;

    // The key is simply the slector name
    NSString *key = NSStringFromSelector(_cmd);

    // Return the value
    return [backingStore objectForKey:key];
}

而 setter 函数则可以这么写:

void autoDictionarySetter(id self, SEL _cmd, id value) {
    // Get the backing store from the object
    EOCAutoDictionary *typedSelf = (EOCAutoDictionary *)self;

    NSMutableDictionary *backingStore = typedSelf.backingStore;

    NSString *selectorString = NSStringFromSelector(_cmd);

    NSMutableString *key = [selectorString mutableCopy];

    // Remove the ':' at the end
    [key deleteCharatersInRange:NSMakeRange(key.length - 1, 1)];

    // Remove the 'set' prefix 
    [key deleteCharactersInRange:NSMakeRange(0,3)];

    //Lowercase the firset character
    NSString *lowercaseFirstChar = [[key substringToIndex:1] lowecaseString];

    [key replaceCharatersInRange:NSMakeRnage(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);
//Output : dict.date = 1985-01-24 00:00:00 +000

小结

  • 若对象无法响应某个子选择器,则进入消息转发流程。
  • 通过运行期的动态方法解析功能,我们可以需要在某个方法时再讲其加入类中。
  • 对象可以把其无法解读的某些选择器转交给其他对象来处理。
  • 经过动态分析,备援接收者之后,如果还是没有办法处理选择器,那么久启动完整的消息转发机制。

第 13 条: 用 “方法调配技术” 调试 “黑盒方法” - 黑魔法(method swizzling)


  • 不急不燥脚踏实地

在第 11 条中解释过: OC 对象收到消息后,究竟会调用何种方法需要在运行期才能解析出来。那么你也许会问: 与给定的选择器名称相应的方法是不是也可以在运行期改变呢? 没错,就是这样。若能善用此特性,则可发挥出巨大优势,因为我们既不需要源代码,也不需要通过继承子类来覆写方法就能改变这个类本身的功能。这样一来,新功能将在本类的所有实例中生效,而不是仅限覆写了相关方法的那些子类实例。此方法经常称为 “方法调配” 也叫 “黑魔法”(method swizzling)。

类的方法列表会把选择器的名称映射到相关的方法实现之上,使得 “动态消息派发系统” 能够据此找到调用的方法。这些方法均以函数指针的形式来表示,这种指针叫做 IMP, 其原型如下:

id (*IMP)(id, SEL, ...)

NSString 类可以响应 lowercaseString、uppercaseString、capitalizedString等选择器。这张映射表中的每个选择器都映射到了不同的IMP之上,如下图。

image

OC 运行期系统提供的几个方法都能够用来操作这张表。开发者可以向其中新增选择器,也可以改变某个选择器所对应的方法实现,还可以交换两个选择器所映射到的指针。经过几次操作之后,类的方法就会变成图下图这个样子。

image

在新的映射表中,多了一个名为 newSelector 的选择器,lowercaseString 与 uppercaseString 的实现则互换了。上述修改均无须编写子类,只要修改了 “方法表” 的布局,就会反映到程序中所有的 NSString 实例之上。

想要交换方法实现,可以用下列函数:

void method_exchangeImplementations(Method m1, Method m2);

此函数的两个参数表示待交换的两个方法实现,而方法实现则可通过下列函数获得:

Method class_getInstanceMethod(Class aClass, SEL aSelector);

此函数根据给定的选择器从类中取出与之相关的方法。执行下列代码,既可交换 lowercaseString 与 uppercaseString 方法实现。

Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));

Method swappedMethod = class_getInstanceMethod([NSString class], @selector(uppercaseString));

method_exchangeImplementations(originalMethod, swappedMethod);

从现在开始,如果在 NSSring 实例上调用lowercaseString,那么执行的将是 uppercaseString 的原有实现,反之亦然。

那么在实际开发中,我们可以通过这一手段来为既有的方法实现增添新功能。比方说, 想要在调用 lowercaseString 时记录某些信息,这时就可以通过交换方法来达成此目标。看下面案例:

@interface NSString (EOCMyAdditions)
- (NSString *)eoc_myLowercaseString;
@end

将上述新方法 eoc_myLowercaseString 与 lowercaseString 方法交换。

image

新方法的实现代码可以这样写:

@implementation NSString (EOCMyAdditions)
- (NSString *)eoc_myLowercaseString {
    NSString *lowercase = [self eoc_myLowercaseString];
    return lowercase;
}
@end

这段代码看上去会陷入递归调用的死循环,不过大家要记住,此方法是准备和 lowercaseString 方法互换的。所以,在运行期,eoc_myLowercaseString 选择器实际上对应于原有的 lowercaseString 方法实现。

示例看上述  lowercaseString 与 uppercaseString 方法实现, 把uppercaseString替换成eoc_myLowercaseString即可。

通过此方案,开发者可以为那些 “完全不知道其具体实现的” 的黑盒方法增加日志功能,这非常有助于程序调试。然而,此做法只在调试程序时有用。很少人在调试程序之外的场合用上述 “方法调配技术” 来永久改动某个类的功能。不能仅仅因为 OC 语言有这个特性就一定要用它。若是滥用,反而会令代码变得不易读懂且难于维护。

小结

  • 在运行期,可以向类中新增或者替换选择器所对应的方法实现。
  • 使用另一份实现来替换原有的方法实现,这道工序叫做 “方法调配”,开发者常用此技术想原有的实现中添加新功能。
  • 一般来说,只有调试程序的时候才需要在运行期修改方法实现,这种做法不宜滥用。

第 14 条: 理解 “类对象” 的用意


OC 实际上是一门极其动态的语言。

  • 第 11 条讲解了运行期系统如何查找并调用某方法的实现代码。
  • 第 12 条讲解了消息转发原理: 如果类无法立即响应某个选择器,那么就会启动消息转发流程。

然而,消息的接收者究竟为何物?我们只知道,这个接收者的类,会逐步调用一些相关消息流程的方法。那么这个接收者是对象本身吗? 运行期如何知道某个对象的类型呢? 对象类型并非在编译期就绑定好了,而是在运行期查找。而且,还有个特性的类型叫做 id,它能代指任意 OC 对象类型。一般情况下,应该指明消息接收者的类型,这样的话,如果向其发送了无法解读的消息,那么编译器就会产生警告信息。而类型为 id 的对象则不然,编辑器假设它能响应所有消息。

如果看过 12 条,你就会明白,编译器无法确定某类型对象到底能解读多少种选择器,因为运行期还能动态新增。然而,即使使用了动态新增技术,编译器也觉得应该能在某个头文件找到方法原型的定义,据此可了解完整的 “方法签名”,并生成派发消息所需的正确代码。

“在运行期检视对象类型” 这一操作也叫做 “类型信息查询”(introspection, “内省”), 这个强大而有用的特性内置于 Foundation 框架的 NSObject 协议里,凡是由公共根类(common root class, 即 NSObject 与 NSProxy),继承而来的对象都需要遵从此协议。在程序中不要直接比较对象所属的类,明智的做法是调用 “类型信息查询方法”,其原因笔者稍后解释。不过在介绍类型信息查询技术之前,我们先讲一些基础知识,看看 OC 对象的本质是什么。

每个 OC 对象实例都是指向某块内存数据的指针。所以在声明变量时,类型后面要跟一个 “*” 字符:

NSString *pointerVarible =  @"Some string";

描述 OC 对象所用的数据结构定义在运行期程序库的头文件里,id类型本身也定义在这里:

typedef struct objc_object {
    Class isa;
} *id;

由此可见,每个对象结构体的首个成员是Class类的变量。该变量定义了对象所属的类,通常称为 “is a”指针。例如,刚才的例子中所用的对象 “是一个”(is a)NSSring, 所以其 “is a”指针就指向 NSString。Class对象也定在运行期程序的头文件中:

typedef struct objc_class *Class
struct objc_class {
    Class isa;
    Class super_class;
    const char *name;
    long version;
    long info;
    long instance_size;
    struct objc_ivar_list *ivars;
    struct objc_mehtod_list **methodLists;
    struct objc_cache *cache;
    struct objc_protocol_list *protocols;
}

此结构体存放类的 “元数据”,例如类的实例实现了几个方法,具备多少个实例变量等信息。此结构体的首个变量也是 isa 指针,这说明 Class 本身亦为 OC 对象。结构体里面还有个变量叫做super_class, 它定义了本类的超类。类对象所属的类型(也就是 isa指针所指向的类型)是另外一个类,叫做 “元类”,用来描述对象本身所具备的元数据。”类方法” 就定义于此处,因为这些方法可以理解成类对象的实例方法。每个类仅有一个 “类对象”,而每个 “类对象” 仅有一个与之相关的 “元类”。

假设有个名为 SomeClass 的子类从 NSObject 中继承而来,则其继承体系如下图所示。

这里写图片描述

super_class 指针确立了继承关系,而 isa 指针描述了实例所属的类。通过这张布局关系图即可执行 “类型信息查询”。我们可以查出对象是否能响应某个选择器,是否遵从某项协议,并且能看出此对象位于 “类继承体系”(class hierarchy) 的哪一部分。

在类继承体系中查询类型信息

可以用类型信息查询方法来检视类继承体系。”isMemberOfClass:” 能够判读出对象是否为某个特定类的实例,而 “isKandOfClass:” 则能够判读出对象是否为某类或其派生类的实例。

NSMutableDictionary *dict = [NSMutableDictionary new];
[dict isMemberOfClass:[NSDictionary class]]; //<NO
[dict isMemberOfClass:[NSMutableDictionary class]]; //<YES
[dict isKandOfClass:[NSDictionary class]]; //<YES
[dict isKandOfClass:[NSArray class]]; //<NO

像这样的类型信息查询方法使用 isa 指针获取对象所属的类,然后通过 super_class 指针在继承体系中游走。由于对象是动态的,所以此特性显得极为重要。OC 与你可能属性的其他语言不同,在此语言中,必须查询类型信息,方能完全了解对象的真是类型。

由于 OC 使用 “动态类型系统”, 所以用于查询对象所属类的类型查询功能非常有用。从 collection 中获取对象,通常会查询类型信息,这些对象不是 “强类型的”(strongly typed), 把它们从 colletion 中取出来,其类型通常是id。如果想知道具体类型,那就可以使用类型信息查询方法。例如,想根据数组中存储的对象生成以逗号分隔的字符串(comma-separated string), 并将其存至文本文件,就可以使用下列代码:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章