第十四条 理解"类对象"的用意

OC是一个极其动态的语言,那么运行期时,如何知道某个对象的类型呢? 对象类型并非在编译期就绑定好了,而是要在运行期査找。而 且,还有个特殊的类型叫做id,它能指代任意的Objective-C对象类型。一般情况下,应该指明消息接收者的具体类型,这样的话,如果向其发送了无法解读的消息,那么编译器就会产生警告信息。而类型为id的对象则不然,编译器假定它能响应所有消息。

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

我们先来了解Objective-C对象的本质:

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

NSString *pointerVariable = @"Some string";

对于通用的对象类型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;
    Class super_class;
    const char *name;
    long version;
    long info;
    long instance_size;
    struct objc_ivar_list *ivars;
    struct objc_method_list **methodLists;
    struct objc_cache *cache;
    struct objc_protocol list *protocols;
);

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

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

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

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

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

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

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

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

-(NSString*)commaSeparatedStringFromObjects:(NSArray*)array {
    NSMutablestring *string = [NSMutableStringnew]; 
    for (id object in array) {
        if ([object isKindOfClass:[NSStringclass]]) {
            [string appendFormat:@"%@,", object];
        }else if ([object isKindOfClass:[NSNumberclass]){
            [string appendFormat:@"%d", [object intValue]];
        } else if ([object isKindOfClass:[NSDataclass]])    {
            NSString *base64Encoded = /* base64 encoded data */;
            [string appendFormat: @"%@", base64Encoded];
        } else {
              //Type not supported
        }
    }   
    return string;
}

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

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

要点

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

第十三条 用"方法调配技术"调试"黑盒方法"

因为OC可以在运行期解析 一个对象究竟调用何种方法,所以我们也可以在运行期改变相对应的方法.

因为与给定的选择子名称相对应的方法也可以在运行期改变.所以若能善用此特性,则可发挥出巨大优势,因为我们既不需要源代码,也不需要通过继承子类来覆写方法就能改变这个类本身的功能。这样一来,新功能将在本类的所有实例中生效,而不是仅限于覆写了相关方法的那些子类实例。此方案经常称为“方法调配”(method swizzling) e

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


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

我们用NSString类来举例,下图表示lowercaseString、uppercaseString、capitalizedString方法映射到了不同的IMP上.

OC运行期系统,提供有方法来操作这个映射表.我们可以向这个表中,新增选择子,改变选择子所对应的方法实现,交换两个选择子所映射的指针.类似下图:

新表中,多了一个newSelector选择子,并且其他方法的实现也互换了.上述修改均无须编写子类,只是修改了“方法表”的布局.

下面我们来讨论如何互换两个方法的实现:

想要互换两个方法的实现可以,有下列函数:

//互换m1和m2的实现
void method_exchangeImplementations(Method m1, Method m2)

m1和m2的方法实现可以通过下面方法实现:


Method class_getInstanceMethod(Class aClass, SEL aSelector)

此函数根据给定的选择从类中取出与之相关的方法。

下面我们完整的举一个例子,来演示交换前面提到的lowercaseStringuppercaseString方法实现:


Method originalMethod =
    class_getInstanceMethod([NSStringclass],
                            @selector(lowercaseString));
Method swappedMethod =
    class_getInstanceMethod([NSStringclass],
                            @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

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

新方法可以添加至NSString的一个"分类(category)"中:


@interface NSString (EOCMyAdditions)

-(NSString*)eoc_myLowercaseString;

@end

上面的新方法将与原有的方法互换,如下图所示:

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


0implementation 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]f
                            @selector(eoc_myLowercaseString)); 
                            method_exchangeImplementations(originalMethod , swappedMethod);

之后我们只要如下执行就会发现:


NSString *string = @"ThIs iS tHe StRiNg";
NSString *lowercaseString = [string lowercaseString];
//输出了下面这行
// Output: This iS tHe StRiNg => this is the string

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

要点

  • 在运行期,可以向类中新增或替换选择子所对应的方法实现。

  • 使用另一份实现来替换原有的方法实现,这道工序叫做“方法调配”,开发者常用此技术向原有实现中添加新功能。

  • 一般来说,只有调试程序的时候才需要在运行期修改方法实现,这种做法不宜滥用。

2017/8/23 posted in  第二章 对象,消息,运行期

第十二条 消息转发机制(Message forwarding)

第11条讲解了对象的消息传递机制,并强调了其重要性。第12条则要讲解另外一个重要的问题,就是对象在收到无法解读的消息之后会发生什么情况。若想令类能理解某条消息,我们必须以程序码实现出对应的方法才行。但是,在编译期向类发送了其无法解读的消息并不会报错,因为在运行期可以继续向类中添加方法,所以编译器在编译时还无法确知类中到底会不会有某个方法实现。当对象接收到无法解读的消息后,就会启动“消息转发"(message forwarding)机制,程序员可经由此过程告沂对象应该如何处理未知消息。

消息转发分为两大阶段:

  1. 先征询接收者,所属的类,看其是否能动态添加方法,以处理当前这个“未知的选择子"(unknown selector),这叫做“动态方法解析”(dynamic method resolution)。

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

动态方法解析

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


+ (BOOL)resolvelnstanceMethod:(SEL)selector

该方法的参数就是那个未知的选择子,其返回值为Boolean类型,表示这个类是否能新增一个实例方法用以处理此选择子。在继续往下执行转发机制之前,本类有机会新增一个处理此选择子的方法。假如尚未实现的方法不是实例方法而是类方法,那么运行期系统就会调用另外一个方法,该方法与 “resolvelnstanceMethod:” 类似,叫做 “resolveClassMethod:”。使用这种办法的前提是:相关方法的实现代码已经写好,只等着运行的时候动态插在类里面就可以了。此方案常用来实现@dynamic属性(参见第6条),比如说,要访问CoreData框架中NSManagedObjects对象的属性时就可以这么做,因为实现这些属性所需的存取方法在编译期就能确定。

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


id autoDictionaryGetter(id self, SEL _cmd);
void autoDictionarySetter(id selff SEL _cmd, id value);
+ (BOOL)resolvelnstanceMethod:(SEL)selector {
    NSString *selectorString = NSStringFromSelector(selector);
    if ( /* selector is from a ©dynamic property ★/ ){
        if([selectorstring has Prefix: @"set"]){
            class addMethod(self,
                            selector,
                            (IMP)autoDictionarySetter,
                            "v@ :@");
    } else {
        class_addMethod(self,
                        selector,
                        (IMP)autoDictionaryGetter,
                        "@ @:");
        }              
        return YES;
    }
return [super resolvelnstanceMethod:selector];

}

备援接受者

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


-(id)forwardingTargetForSelector:(SEL)selector

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

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

完整的消息转发

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


-(void)forwardlnvocation:(NSInvocation*)invocation

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

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

消息转发全流程

下图展示了消息转发机制处理消息的各个步骤:

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

完整的示例

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

定义该类的接口为:


#import <Foundation/Foundation.h>
@interface EOCAutoDictionary : NSObject
@property (nonatomic, strong) NSNumber *number;
@property (nonatomic, strong) NSDate *date;
@property (nonatomic, strong)id qpaqueObject;

@end

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


#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;
}

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


+ (BOOL)resolvelnstanceMethod:(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;
}
@end

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

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

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 fsetf 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 = [EOCAutoDictionarynew];
    diet.date = [NSDatedateWithTimeIntervalSincel970:475372800];
    NSLog (@"diet .date = %@" , dict.date);
    // Output: diet.date = 1985-01-24 00:00:00 +0000

其他属性的访问方式与date类似,要想添加新属性,只需来定义,并将其声明为@dynamic即可。

要点

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

第十一条:理解Objc_msgSend的作用

在对象中调用方法,在oc中称作"传递消息".消息有“名称"(name)或“选择子"(selector),可以接受参数,而且可能还有返回值。

因为OC是C语言的超集,所以我们用C语言来举例.C语言使用“静态绑定”(static binding),也就是说,在编译期就能决定运行时所应调用的函数。

#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;
}

编译器在编译代码的时候就已经知道程序中有printHelloprintGoodbye这两个函数了,于是会直接生成调用这些函数的指令。而函数地址实际上是硬编码在指令之中的。

但是将程序改写为下面这样后:

#import <stdio.h>

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

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

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

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

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


id returnValue = [someObject messageName:parameter];

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


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

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


id returnValue = objc_msgSend(someObject,
                            ^selector(messageName:),
                            parameter);

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

其它特殊情况需要由一些函数来处理:

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

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


<return type> Class_selector(id self, SEL _cmd, •••)

真正的函数名和上面写的可能不太一样,笔者用“类”(class)和“选择子”(selector)来命名是想解释其工作原理。每个类里都有一张表格,其中的指针都会指向这种函数,而选择子的名称则是査表时所用的“键”。ObjC_msgSend等函数正是通过这张表格来寻找应该执行的方法并跳至其实现的。请注意,原型的样子和ObjC_msgSend函数很像。这不是巧合,而是为了利用“尾调用优化”技术,令“跳至方法实现”这一操作变得更简单些。

要点

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

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

有时需要在对象中存放相关信息。这时我们通常会从对象所属的类中继承一个子类,然后改用这个子类对象。然而并非所有情况下都能这么做,有时候类的实例可能是由某种机制所创建的,而开发者无法令这种机制创建出自己所写的子类实例

Objective-C中有一项强大的特性可以解决此问题,这就是“关联对象"(Associated Object)

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

关联类型 等效的@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, void*key, id value, objc AssociationPolicypolicy)
    此方法以给定的键和策略为某对象设置关联对象值。

  • id objc_getAssociatedObject(id object, void*key)
    此方法根据给定的键从某对象中获取相应的关联对象值。

  • void objc removeAssociatedObjects(id object)
    此方法移除指定对象的全部关联对象。

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

关联对象用法举例

开发iOS时经常用到UIAlertView类,该类提供了一种标准视图,可向用户展示警告信息.当用户按下按钮关闭该视图时,需要用委托协议(delegate protocol)来处理此动作,但是,要想设置好这个委托机制,就得把创建警告视图和处理按钮动作的代码分开。由于代码分作两块,所以读起来有点乱。比方说,我们在使用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];
}

// UIAlertViewDelegate protocol method
-(void)alertView:(UIAlertView *)alertView
        clickedButtonAtlndex:(NSInteger)buttonIndex
{
    if (buttonlndex == 0) {
        [self doCancel];
    } else {
        [self doContinue];
    }
}

如果想在同一个类里处理多个警告信息视图,那么代码就会变得更为复杂,我们必须在 delegate方法中检査传人的alertView参数,并据此选用相应的逻辑。要是能在创建警告视图的时候直接把处理每个按钮的逻辑都写好,那就简单多了。这可以通过关联对象来做。创建完警告视图之后,设定一个与之关联的"块"(block),等到执行delegate方法时再将其读出来。此方案的实现代码如下:


#import <objc/runtime.h>

static void *EOCMyAlertViewKey = "EOCMyAlertViewKey";

-(void)askUserAQuestion {
    UIAlertView *alert = [[UIAlertViewalloc]
                            initWithTitle:@"Question"
                                  message: @"What do you want to do?"
                                 delegate:self
                        cancelButtonTitle: @"Cancel",
                        otherButtonTitles: @"Continue", nil];
        void (^block)(NSInteger) = ^(NSInteger buttonIndex){
            if (buttonlndex == 0) {
                [self doCancel];
            }else {
                [self doContinue];
            }
        };
        objc_setAssociatedObject(alert,
                                 EOCMyAlertViewKey,
                                 block,
                                 OBJC_ASSOCIATION_COPY);
        [alert show];
}

// UIAlertViewDelegate protocol method
-(void)alertView:(UIAlertView*)alertView
       clickedButtonAtIndex:(NSInteger)buttonIndex
{
    void (^block)(NSInteger)=
        objc_getAssociatedObject(alertView, EOCMyAlertViewKey);
    block(buttonIndex);
}


以这种方式改写之后,创建警告视图与处理操作结果的代码都放在一起了,这样比原来更易读慷,因为我们无须在两部分代码之间来回游走,即可明白警告视图的用处。但是,采用该方案时需注意:块可能要捕获(capture)某些变量,这也许会造成“保留环”(retain cycle)。第40条详述了此问题。

正如大家所见,这种做法很有用,但是只应该在其他办法行不通时才去考虑用它。若是
滥用,则很快就会令代码失控,使其难于调试。
“保留环”产生的原因很难査明,因为关联对象之间的关系并没有正式的定义(formal definition),其内存管理语义是在关联的时候才定义的,而不是在接口中预先定好的。使用这种写法时要小心,不能仅仅因为某处可以用该写法就一定要用它。想创建这种UIAlertView还有个办法,那就是从中继承子类,把块保存为子类中的属性。笔者认为:若是需要多次用到alert视图,那么这种做法比使用关联对象要好。

要点

  • 可以通过"关联对象"机制来把两个对象连起来
  • 定义关联对象时可指定内存管理语义,用以模仿定义属性时所采用的“拥有关系”与“非拥有关系”。
  • 只有在其他做法不可行时才应选用关联对象,因为这种做法通常会引入难于査找的bug。
2017/7/5 posted in  第二章 对象,消息,运行期

第九条 以"类族模式"隐藏实现细节

"类族"(class cluster,也叫类簇)是一种很有用的模式(pattern),可以隐藏"抽象基类"(abstract base class)背后的实现细节.

Objective-C的系统框架中普遍使用此模式。比如,iOS的用户界面框架(user interface framework) UIKit中就有一个名为UIButton的类。想创建按钮,需
要调用下面这个“类方法”(class method):

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

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

我们使用"类簇",是为了可以灵活应对多个类,将它们的实现细节隐藏在抽象基类后面,以保持接口简洁。用户无须自己创建子类实例,只需调用基类方法来创建即可。

创建类簇

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

首先要定义抽象类:

typedef NS_ENUM(NSUInteger, EOCEmployeeType) {
    EOCEmployeeTypeDeveloper,
    EOCEmployeeTypeDesigner,
    EOCEmployeeTypeFinance,
};

@interface EOCEmployee : NSObject

@property (copy) NSString *name;
@property NSUInteger salary;

// Helper for creating Employee objects
+ (EOCEmployee*)employeeWithType:(EOCEmployeeType)type;
//Make Employees do their respective day1s work
- (void)doADaysWork;

@end

@implementation EOCEmployee
+ (EOCEmployee*)employeeWithType:(EOCEmployeeType)type {
    switch (type) {
        case EOCEmployeeTypeDeveloper:
            return [EOCEmployeeDeveloper new];
            break;
        case EOCEmployeeTypeDesigner:
            return (EOCEmployeeDesigner new];
            break;
        case EOCEmployeeTypeFinance:
            return (EOCEmployeeFinance new];
            break;
        }
}
-(void)doADaysWork {
    // Subclasses implement this.
}

@end 

每个"实体子类"(concrete subclass) 都从基类继承而来。例如:


@interface EOCEmployeeDeveloper : EOCEmployee
@end

@implementation EOCEmployeeDeveloper

-(void)doADaysWork {    
    [self writeCode];
}

@end

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

OC这门语言没有办法致命某个基类是"抽象的"(abstract).于是,开发者通常会在文档中写明类的用法。这种情况下,基类接口一般都没有名为init的成员方法,这暗
示该类的实例也许不应该由用户直接创建。还有一种办法可以确保用户不会使用基类实例,
那就是在基类的doADaysWork方法中拋出异常。然而这种做法相当极端,很少有人用。

如果对象所属的类位于某个类族中,那么在査询其类型信息(introspection)时就要当心了(参见第14条)。你可能觉得自己创建了某个类的实例,然而实际上创建的却是其子类的实例。在 Employee 这个例子中,[employee isMemberOfClass:[EOCEmployee class]]似乎会返回YES,但实际上返回的却是NO,因为employee并非Employee类的实例,而是其某个子类的实例。

Cocoa里的类簇

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

像NSArray这样的类的背后其实是个类族(对于大部分collection类而言都是这样),明白这一点很重要,否则就可能会写出下面这种代码:


id maybeAnArray = /* ••• */;
if ([maybeAnArray class) == [NSArray class]) {
"Will never be hit
}

你要是知道NSArray是个类族,那就会明白上述代码错在哪里:其中的if语句永远不可能为真。[maybeAnArray class]所返回的类绝不可能是NSArray类本身,因为由NSArray的初始化方法所返回的那个实例其类型是隐藏在类族公共接口(public facade)后面的某个内部类型(internal type)。

不过,仍然有办法可以判断出某个实例所属的类是否位于类族之中。我们不用刚才那种写法,而是改用类型信息查询方法(introspectionmethod)。本书第14条解释了这些方法的用法。若想判断某对象是否位于类族中,不要直接检测两个“类对象”是否等同,而应该采用下列代码

id maybeAnArray = /* ••• */;
if ([maybeAnArray isKindOfClass:[NSArray class])) {
    "Will be hit"
}

我们经常需要向类族中新增实体子类,不过这么做的时候得留心。在Employee这个例子中,若是没有“工厂方法”(factory method)的源代码,那就无法向其中新增雇员类别了。然而对于Cocoa中NSArray这样的类族来说,还是有办法新增子类的,但是需要遵守几条规则。这几条规则如下。

  • 子类应该继承自类族中的抽象基类。
    若要编写NSArray类族的子类,则需令其继承自不可变数组的基类或可变数组的基类。

  • 子类应该定义自己的数据存储方式。
    开发者编写NSArray子类时,经常在这个问题上受阻。子类必须用一个实例变量来存放数组中的对象。这似乎与大家预想的不同,我们以为NSArray自己肯定会保存那些对象,所以在子类中就无须再存一份了。但是大家要记住,NSArray本身只不过是包在其他隐藏对象外面的壳,它仅仅定义了所有数组都需具备的一些接口。对于这个自定义的数组子类来说,可以用NSArray来保存其实例

  • 子类应当覆写超类文档中指明需要覆写的方法。
    在每个抽象基类中,都有一些子类必须覆写的方法。比如说,想要编写NSArray的子
    类,就需要实现count“objectAtlndex:”方法。像lastObject这种方法则无须实现,因为基类可以根据前两个方法实现出这个方法。

在类族中实现子类时所需遵循的规范一般都会定义于基类的文档之中,编码前应该先看看.

要点

  • 类族模式可以把实现细节隐藏在一套简单的公共接口后面。
  • 系统框架中经常使用类族。
  • 从类族的公共抽象基类中继承子类时要当心,若有开发文档,则应首先阅读。
2017/7/4 posted in  第二章 对象,消息,运行期