第二章 对象、消息、运行期

6.理解“属性”这一概念

@dynamic关键字:告诉编译器不要自动创建实现属性所用的实例变量,也不要为其创建存取方法。而且,在编译访问属性代码时,即使编译器发现没有定义存取方法,也不会报错,它相信这些方法能在运行期找到。

@property (nonatomic,readwrite,copy) NSString *firstName;

属性特质:原子性、读/写权限、内存管理语义、方法名。

  • 原子性:iOS开发中一般都是nonatomic,因为iOS中使用同步锁开销较大,会产生性能问题。一般情况下并不要求属性必须是“原子性”的,因为这并不能保证“线程安全”,若要实现“线程安全”的操作,还需采用更为深层的锁定机制才行。

  • 读/写权限:readwrite和readonly

  • 内存管理语义:

    • assign:setter方法只会执行针对“纯量类型”(scale type,例如CGFloat或NSInteger等)的简单赋值操作。

    • strong:表示“拥有关系(owning relationship)”,为这种属性设置新值时,会先保留新值,并释放旧值,然后再将新值设置上去。

    • weak:表示“非拥有关系(nonowning relationship)”为这种属性设置新值时,不保留新值,也不释放旧值。与assign类似,属性所指的对象遭到摧毁时,属性值也会清空。

    • unsafe_unretained:与assign相同,但是它适用于“对象类型(object type)”,表示“非拥有关系”,当目标对象遭到摧毁时,属性值不会自动清空,这一点与weak有区别。

    • copy:与strong类似。然而设置方法并不保留新值,而是将其“copy”。当属性类型为NSString*时,经常用到此特质来保护其封装性,因为传递给设置方法的新值有可能指向一个NSMutableString类型的实例。这个类是NSString的子类,表示一种可以修改其值的字符串,此时若是不拷贝字符串,那么设置完属性之后,字符串的值就可能会在对象不知情的情况下遭人更改。所以,这是就要拷贝一份“不可变”的字符串,确保对象中的字符串值不会无意间变动。只要实现属性所用的对象是“可变的”,就应该在设置新属性值时拷贝一份。

  • 方法名:可通过getter=<name>指定获取方法的方法名。比如:
    @property (nonatomic,getter=isOn) BOOL on;
    可通过setter=<name>指定设置方法的方法名。若是自己来实现这些存取方法,那么应该保证其具备相关属性所声明的特质。比如,如果将某个属性声明为copy,那么就应该在设置方法中拷贝相关对象,否则会误导该属性的使用者,而且,若是不遵从这一约定,还会令程序产生bug。

实例:

@interface EOCPerson : NSManagedObject
@property (copy) NSString *firstName;
@property (copy) NSString *lastName;

- (id)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName;

@end

- (id)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName
{
    if(self = [super init]){
        _firstName = [firstName copy];
        _lastName = [lastName copy];
    }
}

注意:不要在init或dealloc方法中调用存取方法。

7.在对象内部尽量直接访问实例变量

@interface EOCPerson : NSObject
@property(nonatimic,copy)NSString *firstName;
@property(nonatimic,copy)NSString *lastName;

@end

fullName与setFullName这两个便捷方法可以这样来实现:

- (NSString *)fullName{
    return [NSString stringWithFormat:@“%@ %@“,self.firstName,self.lastName];
}

- (void)setFullName:(NSString *)fullName{
    NSArray *components = [fullName componentsSeparatedByString:@“ “];  
    self.firstName = components[0];
    self.lastName = components[1];
}

直接访问实例变量:

- (NSString *)fullName{
    return [NSString stringWithFormat:@“%@ %@“,_firstName,_lastName];
}

- (void)setFullName:(NSString *)fullName{
    NSArray *components = [fullName componentsSeparatedByString:@“ “];  
    _firstName = components[0];
    _lastName = components[1];
}

直接访问与属性访问(点语法)的区别:

  • 直接访问实例变量的速度快;
  • 直接访问实例变量不会调用其“设置方法”,这就绕过了为相关属性所定义的“内存管理语义”。比如:如果在ARC下直接访问一个声明为copy的属性,那么并不会拷贝其属性值,只会保留新值并释放旧值。
  • 如果直接访问实例变量,那么不会触发“键值观察(KVO)”通知。
  • 通过属性来访问有助于排查与之相关的错误,因为可以给获取方法和设置方法中新增断点,监控该属性的调用者及其访问时机。

折中方案:

  • 在对象内部读取数据时,应该直接通过实例变量来读,而写入数据时,则应通过属性来写。
  • 在初始化方法及dealloc方法中,总是应该直接通过实例变量来读写数据。
  • 有时会使用“惰性初始化”技术配置数据,这时需要通过属性来读取数据。

8.理解“对象等同性”这一概念

若要检测对象的等同性,请提供isEqual:与hash方法。
相同的对象必须具有相同的哈希码,但是两个哈希码相同的对象却未必相同。
不要盲目地逐个检测每条属性,而是应该依照具体需求来制定检测方案。
编写hash方法时,应该使用计算速度快而且哈希码碰撞几率低的算法。

9.以“类簇模式”隐藏实现细节

“类簇”(class cluster)是一种很有用的模式,可以隐藏“抽象基类”背后实现细节。

比如UIButton类的类方法:
+(UIButton*)buttonWithType:(UIButtonType)type;
该方法返回的对象,其类型取决于传入的按钮类型。UIButton类的使用者无须关心创建出来的按钮具体属于哪个子类,也不用考虑按钮的绘制方式等实现细节。

“工厂模式(Factory pattern)”是创建类簇的方法之一:
创建类簇举例:假设有一个处理雇员的类,每个雇员都有“名字”和“薪水”这两个属性,管理者可以命令其执行日常工作。但是,各种雇员的工作内容却不同。经理在带领雇员做项目时,无须关心每个人如何完成其工作,仅需指示其开工即可。

首先要定义抽象基类:

typedef NS_ENUM(NSUInteger, ECOEmployeeType){
    ECOEmployeeTypeDeveloper,
    ECOEmployeeTypeDesigner,
    ECOEmployeeTypeFinance,
}

@interface EOCEmployee: NSObject
@property (copy) NSString *name;
@property NSUInter salary;

//Helper for creating Employee objects
+ (EOCEmployee*)employereWithType:(ECOEmployeeType)type;

//Make Employees do their respective day’s work
- (void)doADaysWork;

@end

@implementation EOCEmployee

+ (EOCEmployee*)employereWithType:(ECOEmployeeType)type{
    switch(type){
        case ECOEmployeeTypeDeveloper:
            return [ECOEmployeeTypeDeveloper new];
            break;
        case ECOEmployeeTypeDesigner:
            return [ECOEmployeeTypeDesigner new];
            break;
        case ECOEmployeeTypeFinance:
            return [ECOEmployeeTypeFinance new];
            break;
    }
}

- (void)doADaysWork{
    //Subclasses implement this
}

10.在既有类中使用关联对象存放自定义数据

“关联对象”(Associated Object):可以给某对象关联许多其他对象,这些对象通过“键”来区分。存储对象值的时候,可以指明“存储策略”(storage policy),用以维护相应的“内存管理语义”。存储策略由名为objc_AssociationPolicy的枚举所定义,下表列出了该枚举值,同时还列出了与之等效的@property属性:假如关联对象成为了属性,那么它就会具备对应的语义。

对象关联类型

下列方法可以管理关联对象:

void objc_setAssociatedObject(id object, void *key, id value, objc_AssociationPolicy policy)

此方法以给定的键和策略为某对象设置关联对象值。

id objc_getAssociatedObject(id object, void *key)

此方法根据给定的键从某对象中获取相应的关联对象值。

void objc_removeAssociatedObjects(id object)

此方法移除指定对象的全部关联对象。

我们可以把某对象想象成NSDictionary,把关联到该对象的值理解为字典中的条目。然而两者之间有个重要差别:设置关联对象时用的键是个“不透明的指针”。如果在两个键上调用“isEqual:”方法的返回值为YES,那么NSDictionary就认为二者相等;然而在设置关联对象值时,若想令两个键匹配到同一个值,则二者必须是完全相同的指针才行。鉴于此,在设置关联对象时,通常使用静态全局变量做键。

关联对象举例:

#import <objc/runtime.h>

static void *EOCMyAlertViewKey = "EOCMyAlertViewKey";

-(void)askUserQuestion
{
    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];
    
}

#pragma mark---UIAlertViewDelegate
-(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
    void (^block)(NSInteger) = objc_getAssociatedObject(alertView, EOCMyAlertViewKey);
    block(buttonIndex);
}

创建完警告视图之后,设定一个与之关联的“块”(block),等到执行delegate方法时再将其读出来。创建警告视图与处理操作结果的代码都放在一起了,这样比原来更易读懂,因为我们无需在两部分代码之间来回游走,即可明白警告视图的用处。但是,采用该方案时需注意:块可能要捕获某些变量,这也许会造成循环引用。

可以通过“关联对象”机制来把两个对象连起来。
定义关联对象时可指定内存管理语义,用以模仿定义属性时采用的“拥有关系”与“非拥有关系”。
只有在其他做法不可行时才应选用关联对象,因为这种做法通常会引入难以查找的bug。

11.理解objc_msgSend的作用

在对象上调用方法是Objective-C中经常使用的功能。用Objective-C的术语来说,这叫做“传递消息”(pass a message)。消息有“名称”(name)或“选择子”(selector),可以接受参数,而且可能还有返回值。

由于Objective-C是C的超集,所以最好先理解C语言的函数调用方式。C语言使用“静态绑定”(static binding),也就是说,在编译期就能决定运行时所应调用的函数。
以下列代码为例:

#include <stdio.h>


void printHello(){
    printf("Hello,world!\n");
}

void printGoodbye(){
    printf("Goodbye,world!\n");
}

void doTheThing(int type){
    if(type==0){
        printHello();
    }else{
        printGoodbye();
    }
}

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

void doTheThing(int type){
    void (*fnc)();
    if(type==0){
        fnc = printHello;
    }else{
        fnc = printGoodbye;
    }
    fnc();
}

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

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

id returnValue = [someObject messageName:parameter];

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

void objc_msgSend(id self, SEL cmd, …)

第一个参数的代表接收者,第二个参数代码选择子(SEL是选择子的类型),后续参数是消息中那些参数,其顺序不变。选择子指的就是方法的名字。“选择子”与“方法”这两个词经常交替使用。编译器会把刚才那个例子中的消息转换为如下函数:

id returnValue = objc_msgSend(someObjct, @selector(messageName:), parameter);

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

objc_msgSend会将匹配结果缓存在“快速映射表”(fast map)里面,每个类都有这样一块缓存,若是稍后还向该类发送与选择子相同的消息,那么执行起来就很快了。当然,这种“快速执行路径”(fast path)还是不如“静态绑定的函数调用操作”(statically bond fuction call)那样迅速,不过只要把选择子缓存起来了,那就不会慢很多,

有些特殊情况需要Objective-C运行环境中的另一些函数来处理:

  • objc_msgSend_stret 如果待发送的消息要返回结构体,那么可交由此函数处理。只有当CPU的寄存器能够容纳得下消息返回类型时,这个函数才能处理此消息。若是返回值无法容纳与CPU寄存器中(比如说返回的结构体太大了),那么就由另一个函数执行派发。此时,那个函数会通过分配在栈上的某个变量来处理消息所返回的结构体。
  • objc_msgSend_fpret 如果消息返回的是浮点数,那么可交由此函数处理。在某些架构的CPU中调用函数时,需要对“浮点数寄存器”(floating-point register)做特殊处理,也就是说,通常所用的objc_msgSend在这种情况下并不合适。这个函数是为了处理x86等架构CPU中某些令人稍觉惊讶的奇怪状况。
  • objc_msgSendSuper 如果要给超类发消息,例如[super message:parameter],那么就交由此函数处理。也有另外两个与objc_msgSend_stret和objc_msgSend_fpret等级的函数,用于处理发给super的相应消息。

objc_msgSend等函数一旦找到应该调用的方法实现之后,就会“跳转过去”。之所以能这样做,是因为Objective-C对象的每个方法都可以视为简单的C函数,其原型如下:

<return_type> Class_selector(id self, SEL _cmd, ….)

每个类里都有一张表格,其中的指针都会指向这种函数,而选择子的名称则是查表时所用的“键”。objc_msgSend等函数正是通过这张表格来寻找应该执行的方法并跳转至其实现的。请注意,原型的样子和objc_msgSend函数很像。这不是巧合,而是为了利用“尾调用优化”(tail-call optmization)技术,令“跳至方法实现”这一操作变得更简单些。(尾调用:就是指某个函数的最后一步是调用另一个函数。柯里化:意思是将多参数的函数转换成单参数的形式。参见:尾调用优化)

如果某函数的最后一项操作是调用另外一个函数,那么就可以运用“尾调用优化”技术。编译器会生成跳转至另一个函数所需的指令码,而且不会向调用堆栈中推入新的“栈帧”(frame stack)。只有当某函数的最后一个操作仅仅是调用其他函数而不是将其返回值另作他用时,才能执行“尾调用优化”。这项优化对objc_msgSend非常关键,如果不这么做的话,那么每次调用Objective-C方法之前,都需要为调用objc_msgSend函数准备“栈帧”,大家在“栈踪迹”(stack trace)中就可以看到这种“栈帧”。此外,若是不优化,还会过早地发生“栈溢出”(stack overflow)现象。

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

12.理解消息转发机制

在编译期向类发送了其无法解读的消息并不会报错,因为在运行期可以继续向类中添加方法,所以编译器在编译时还无法确知类中到底会不会有某个方法实现。当对象接收到无法解读的消息后,就会启动“消息转发”(message forwarding)机制,程序员可经由此过程告诉对象应该如何处理未知消息。

如果在控制台中看到下面这种提示信息,那就说明你曾向某个对象发送过一条其无法解读的消息,从而启动了消息转发机制,并将此消息转发给了NSObject的默认实现。

[__NSCFNumber lowercaseString]: unrecognized selector sent to instance 0xb0000000000000a2

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', 
reason: '-[__NSCFNumber lowercaseString]: 
unrecognized selector sent to instance 0xb0000000000000a2'

上面这段异常信息是由NSObject的“doesNotRecognizeSelector:”方法所抛出的,此异常表明:消息接收者的类型是__NSCFNumber,而该接收者无法理解名为lowercaseString的选择子。控制台中看到的__NSCFNumber是为了实现“无缝桥接”(toll-freebridging)而使用的内部类(internal class),配置NSNumber对象时也会一并创建此对象。

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

  • 动态方法解析
    对象在收到无法解读的消息后,首先将调用其所属类的下列类方法:
+(BOOL)resolveInstanceMethod:(SEL)sel;

该方法的参数就是那个未知的选择子,其返回值为Boolean类型,表示这个类是否能新增一个实例方法用以处理此选择子。在继续往下执行转发机制前,本类有机会新增一个处理此选择子的方法。假如尚未实现的方法不是实例方法而是类方法,那么运行系统就会调用另外一个方法:

+(BOOL)resolveClassMethod:(SEL)sel;

使用这种办法的前提是:相关方法的实现代码已经写好,只等着运行的时候动态插在类里面就可以了。此方案常用来实现@dynamic属性。比如,要访问CoreData框架中的NSManagedObject对象的属性时就可以这么做,因为实现这些属性的存取方法在编译期就能确定。

下面代码演示了如何用“resolveInstanceMethod: ”来实现@dynamic属性:

#import <objc/runtime.h>

id autoDictionaryGetter(id self, SEL _cmd);
void autoDictionarySetter(id self, SEL _cmd, id value);

+(BOOL)resolveInstanceMethod:(SEL)sel
{
    NSString *selectorString = NSStringFromSelector(sel);
    if(/* sel is from a @dynamic property */){
        if([selectorString hasPrefix:@"set"]){
            class_addMethod(self, sel, (IMP)autoDictionarySetter, "v@:@");
        }else{
            class_addMethod(self, sel, (IMP)autoDictionaryGetter, "@@:");
        }
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

首先将选择子化为字符串,然后检查其是否表示设置方法。若前缀为set,则表示设置方法,否则就是获取方法。不管哪种情况,都会把处理该选择子的方法加到类里面,所添加的方法是纯C函数实现的。C函数可能会用代码来操作相关的数据结构,类之中的属性数据就存放在那些数据结构里面。以CoreData为例,这些存取方法也许要和后端数据库通信,以便获取或更新相应的值。

  • 备援接收者

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

-(id)forwardingTargetForSelector:(SEL)aSelector

方法参数代表未知的选择子,若当前接收者能找到备援对象,则将其返回,若找不到,就返回nil。通过此方案,我可以用“组合”(composition)来模拟出“多重继承”(multiple inheritance)的某些特性。在一个对象内部,可能还有一系列其他对象,该对象可经由此方法将能够处理某选择子的相关内部对象返回,这样的话,在外界看来,好像是该对象亲自处理了这条消息似的。

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

  • 完整的消息转发

如果转发算法已经来到这一步的话,那么唯一能做的就是启用完整的消息转发机制了。首先创建NSInvocation对象,把与尚未处理的那条消息有关的全部细节都封装于其中。此对象包含选择子、目标(target)及参数。在触发NSInvocation对象时,“消息派发系统”(message-dispatch system)将亲自出马,把消息指派给目标对象。

此步骤会调用一列方法来转发消息:

-(void)forwardInvocation:(NSInvocation *)anInvocation

这个方法可以实现得很简单:只需改变调用目标,使消息在新目标上得以调用即可。然而这样实现出来的方法与“备援接收者”方案所实现的方法等效,所以很少有人采用这么简单的实现方式。比较有用的实现方式为:在触发消息前,先以某种方式改变消息内容,比如追加另外一个参数,或者改换选择子,等等。

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

  • 消息转发全流程

下面这张流程图描述了消息转发转发机制处理消息的各个步骤:

消息转发全流程

接收者在每一步中均有机会处理消息。步骤越往后,处理消息的代价就越大。最好能在第一步就处理完,这样的话,运行期系统就可以将此方法缓存起来了。如果这个类的实例稍后还收到同名选择子,那么根本无须启动消息转发流程。若想在第三步里把消息转给备援的接收者,那还不如把转发操作提前到第二步。因为第三步只是修改了调用目标,这项改动放在第二步执行会更为简单,不然的话,还得创建并处理完整的NSInvocation。

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

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

该类的接口可以写成:

#import <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

本例中,这些属性具体是什么其实无关紧要。笔者采用了这么多种数据类型,只是想演示此功能很有用。在类的内部,每个属性的值还是会存放在字典里,所以我们先在类中编写如下代码,并将属性声明为@dynamic,这样的话,编译器就不会为其自动生成实例变量及存取方法了:

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

id autoDictionaryGetter(id self, SEL _cmd);
void autoDictionarySetter(id self, SEL _cmd, id value);

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

@end

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

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

+(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;
}

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

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

Objective-C type Encodings

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 selector 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;
    
    /*The selector will be for example,"setOpaqueObject:".
     *We need to remove the "set",“:” and lowercase the first
     *letter of the remainder.*/
    NSString *selectorString = NSStringFromSelector(_cmd);
    NSMutableString *key = [selectorString mutableCopy];
    
    //Remove the ":"at the end
    [key deleteCharactersInRange:NSMakeRange(key.length-1, 1)];
    
    //Remove the "set" prefix
    [key deleteCharactersInRange:NSMakeRange(0, 3)];
    
    //Lowercase the first character
    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:4753728000];

其他属性的访问方式与date类似,要想添加新属性,只需要@property来定义,并将其声明为@dynamic即可。在iOS的CoreAnimation框架中,CALayer类就用了本例相似的实现方式,这使得CALayer成为“兼容于键值编码的”(key-value-coding-compliant)容器类,这就等于说,能够向里面随意添加属性,然后以键值对的形式来访问。于是,开发者就可以向其中新增自定义的属性了,这些属性值的存储工作由基类直接负责,我们只需在CALayer的子类中定义新属性即可。

要点:

  • 若对象无法响应某个选择子,则进入消息转发流程。
  • 通过运行期的动态方法解析功能,我们可以在需要用到某个方法时再将其加入类中。
  • 对象可以把无法解读的某些选择子转交给其他对象来处理。
  • 经过上述两步之后,如果还是没办法处理选择子,那就启动完整的消息转发机制。

13.用“方法调配技术”调试“黑盒方法”

与给定的选择子名称相对应的方法也可以在运行期改变。我们既不需要源代码,也不需要通过继承子类来覆写方法就能改变这个类本身的功能,这样一来,新功能将在本来的所有实例中生效,而不是仅限于覆写了相关方法的那些子类实现。此方案经常成为“方法调配”(method swizzling)或“方法混合”、“方法调和”。

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

id (*IMP)(id, SEL, …)

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

NSString类的选择子都映射表

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

交换之后的方法表

在新的映射表中,多了一个名为newSelector的选择子,capitalizedString的实现也变了,而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);

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

    NSString *string = @"ThIs iS tHe StRiNg";
    NSString *lowercaseString = [string lowercaseString];
    NSLog(@"lowercaseString = %@",lowercaseString);
    //Output:lowercaseString = THIS IS THE STRING
    
    NSString *uppercaseString = [string uppercaseString];
    NSLog(@"uppercaseString = %@",uppercaseString);

    //Output:uppercaseString = this is the string

刚才演示了如何交换两个方法实现,然而在实际应用中,像这样直接交换两个方法实现的,意义并不大。因为lowercaseString与uppercaseString这两个方法已经各自实现的很好了,没必要再交换了。但是,可以通过这一手段来为既有的方法实现增添新功能。比方说,想要在调用lowercaseString时记录某些信息,这时就可以通过交换方法实现来达成此目标:

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

@end

上述新方法将与原有的lowercaseString方法互换,交换之后的方法表如下:

交换之后的方法表

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

-(NSString *)eoc_myLowercaseString
{
    NSString *lowercase = [self eoc_myLowercaseString];
    NSLog(@"%@ => %@",self,lowercase);
    return lowercase;
}

这段代码看上去好像会陷入递归调用的死循环,不过大家要记住,此方法是准备和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];
NSLog(@"lowercaseString = %@",lowercaseString);
//ThIs iS tHe StRiNg = this is the string

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

要点:

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

14.理解“类对象”的用意

“在运行期检视对象类型”这一操作也叫做“类型信息查询”(introspection,“内省”),这个强大而有用的特性内置于Foudation框架的NSObject协议里,凡是由公共根类(common root class,即NSObject与NSProxy)继承而来的对象都要遵从此协议。在程序中不要直接比较对象所属的类,明智的做法是调用“类型信息查询方法”。

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

NSString *pointerVariable = @“Some string”;

pointerVariable可以理解成存放内存地址的变量,而NSString本身的数据就存放于那个地址中。所有Objective-C对象都是如此,若是想把对象所需的内存分配在栈上,编译器则会报错:

String stackVariable = @“Some string”;
//error:instance type cannot be statically allocated

对于通用的对象类型id,由于其本身已经是指针了,所以我们能够这样写:

id genericTypedString = @“Some string”;

上面这种定义方式与用NSString*来定义相比,其语法意义相同。唯一区别在于,如果声明时制定了具体类型,那么在该类实例上调用其所没有的方法时,编译器会探知此情况,并发出警告信息。

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

typedef struct objc_object{
 Class isa; 
} *id; 

由此可见,每个对象结构体的首个成员是Class类型的变量。该变量定义了对象所属的类,通常称为“is a”指针。例如,刚才例子中所用的对象“是一个”(is a)NSString,所以其“is a”指针就指向NSString。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;

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

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

继承体系

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

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

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

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

像这样的类型信息查询方法使用isa指针获取对象所属的类,然后通过super_class指针在继承体系中游走。由于对象是动态的,所以此特性显得极为重要。

由于Objective-C使用“动态类型系统”(dynamic typing),所以用于查询对象所属类的类型信息查询功能非常有用。从collection中获取对象时,通常会查询类型信息,这些对象不是“强类型的”(strongly typed),把它们从collection中取出来时,其类型通常是id。如果想知道具体类型,那就可以使用类型信息查询方法。

例:

-(NSString *)commaSeparatedStringFromObjects:(NSArray *)array{
    NSMutableString *string = [NSMutableString new];
    for(id object in array){
        if([object isKindOfClass:[NSString class]]){
            [string appendFormat:@"%@,",object];
        }else if ([object isKindOfClass:[NSNumber class]]){
            [string appendFormat:@"%d,",[object intValue]];
        }else if ([object isKindOfClass:[NSData class]]){
            NSString *base64Encoded = /*base 64 encodeed data*/;
            [string appendFormat:@"%@",base64Encoded];
        }else{
            //Type not supported
        }
    }
    return string;
}

也可以用比较类对象是否等同的办法来做。若是若此,那就要使用==操作符,而不要使用比较Objective-C对象时常用的“isEqual:”方法。原因在于,类对象是“单例”(singleton),在应用程序范围内,每个类的Class仅有一个实例。也就是说,另外一种可以精确判断出对象是否为某类实例的办法是:

id object = /*…*/;
if([object class]==[EOCSomeClass class]){
//‘object’ is an instance of EOCSomeClass
}

即便能这样做,我们也应该尽量使用类型信息查询方法,而不应该直接比较两个类对象是否等同,因为前者可以正确处理那些使用了消息传递机制的对象。比方说,某个对象可能会把其收到的所有选择子都转发给另外一个对象。这样的对象叫做“代理”(proxy),此种对象均以NSProxy为根类。

通常情况下,如果在此种代理对象上调用class方法,那么返回的是代理对象本身(此类是NSProxy的子类),而非接受的代理的对象所属的类。然而,若是改用“isKindOfClass:”这样的类型信息查询方法,那么代理对象就会把这条消息转给“接受代理的对象”(proxied object)。也就是说,这条消息的返回值与直接在接受代理的对象上面查询其类型所得的结果相同。因此,这样查出来的类对象与通过class方法所返回的那个类对象不同,class方法所返回的类表示发起代理的对象,而非接受代理的对象。

要点

  • 每个实例都有一个指向Class对象的指针,用以表明其类型,而这些Class对象则构成了类的继承体系。
  • 如果对象类型无法在编译器确定,那么就应该使用类型信息查询方法来探知。
  • 尽量使用类型信息查询方法来确定对象类型,而不要直接比较类对象,因为某些对象可能实现了消息转发功能。

转载请注明出处:第二章 对象、消息、运行期

参考:《Effective Objective-C 2.0》

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

推荐阅读更多精彩内容