第十六条 提供"全能初始化方法"

我们知道,所有的对象都要初始化但是有些对象可能有很多初始化方法(根据初始的信息来选择用哪个).我们以iOS的UI框架UIKit为例,其中有个类叫做UITableViewCell,初始化该类对象 时,需要指明其样式及标识符,标识符能够区分不同类型的单元格。由于这种对象的创 建成本较高,所以绘制表格时可依照标识符来复用,以提升程序效率。我们把这种可为 对象提供必要信息以便其能完成工作的初始化方法叫做“全能初始化方法”(designated initializer)

我们看下面这个NSDate的例子:

-(id)init
-(id)initWithString:(NSString*)string
-(id)initWithTimelntervalSinceNow:(NSTimelnterval)seconds
-(id)initWithTimelnterval:(NSTimelnterval)seconds
                sinceDate:(NSDate*)refDate
-(id)initWithTimeIntervalSinceReferenceDate:(NSTimelnterval)seconds
-(id)initWithTimeIntervalSincel970:(NSTimelnterval)seconds

那么多的初始化方法中,我们要选一个全能初始化方法,让其他的初始化方法都来调用它。,只有在全能初始化方法中,才会存储内部数据。这样的话,当底层数据存储机制改变时,只需修改此方法的代码就好,无须改动其他初始化方法。

我们来用代码举例:

首先定义一个表示矩形的类:

#import <Foundation/Foundation.h>
@interface EOCRectangle : NSObject
@property (nonatomic, assign, readonly) float width;
@property (nonatomic, assign, readonly) float height; 
@end

然后定义一个初始化方法:

-(id) initwithwidth: (float) width andHeight:(float)height
{
    if ((self = [super init])) {
        _width = width;
        _height = height;
    }
    return self;
}

这样就会有一个问题,当有人用[[EOCRectanglealloc]init]来创建矩形时,因为NSObject中已经实现了init方法,如果把alloc方法分配好的EOCRectangle交由此方法来初始化,那么矩形的宽度与高度就是0,因为全部实例变量都设为0了。这种情况我们应该覆写init方法:

// Using default values
-(id)init {
    return [self initWithWidth:5.Of andHeight:10.Of];
)
// Throwing an exception
-(id)init {
    @throw [NSException
    exceptionWithName:NSInternalInconsistencyException 
    reason:@"Must use initWithWidth:andHeight: instead." 
    userInfo:nil];
}

还有一种情况,当我们创建名叫EOCSquare的类,令其成为EOCRectangle的子类时,新类的初始化方法写的时候要注意:

@import "EOCRectangle.h"
@interface EOCSquare : EOCRectangle 
-(id)initWithDimension:(float)dimension; 
@end

@implementation EOCSquare
-(id)initWithDimension:(float)dimension {
    return [super initwithwidth:dimension andHeightidimension];
}
@end

这了我们发现上面代码的初始化方法调用了父类的初始化方法,这样可能会导致一个问题:创建出一个”高度”和“宽度”不相等的正方形。所以:如果子类的全能初始化方法与超类方法的名称不 同,那么总应覆写超类的全能初始化方法。EOCSquare这个例子中,应该像下面这样覆写EOCRectangle的全能初始化方法:

-(id)initWithWidth:(float)width andHeight:(float)height { 
    float dimension = MAX (width, height);  
    return [self initWithDimension:dimension];
}

覆写了这个方法之后,即便使用init来初始化EOCSquare对象,也能照常工作。原因在于, EOCRectangle类覆写了 init方法,并以默认值为参数,调用了该类的全能初始化方法。在用init方法初始化EOCSquare对象时,也会这么调用,不过由于“initWithWidth:andHeight:”已经在子类中覆写了,所以实际上执行的是EOCSquare类的这一份实现代码,而此代码又会调用本类的全能初始化方法。因此一切正常,调用者不可能创建出边长不相等的EOCSquare对象。

当然如果我们不想覆写父类的全能初始化方法,认为这是调用者自己犯了错误。在这种情况下,常用的办法是覆写超类的全能初始化方法并于其中抛出异常:

-(id) initwithwidth: (float) width andHeight: (float) height {
    @throw 
        [NSException 
            exceptionWithName:NSInternallnconsistencyException
            reason: @"Must use initWithDimension: instead."
             userInfo:nil];

有时如果某个队形对象的实例有两种完全不同的创建方式,必须分开处理,所以就要编写多个全能初始化方法。只要记住每个子类的全能初始化方法都应该调用其超类的对应方法,并逐层向上,应该先调用超类的相关方法,然后再执行与本类有关的任务。

要点

  • 在类中提供一个全能初始化方法,并于文档里指明。其他初始化方法均应调用此方法。
  • 若全能初始化方法与超类不同,则需覆写超类中的对应方法。

  • 如果超类的初始化方法不适用于子类,那么应该覆写这个超类方法,并在其中抛出异常。

2017/8/25 posted in  第三章 接口与API设计

第十五条 用前缀避免命名空间冲突

oc与其他语言一个主要区别是没有那种内置的命名空间(namespace)机制. 所以我们在编码时一定要注意命名冲突,尤其是潜在的命名冲突.

我们在创建应用程序时一定要注意,使用Cocoa创建应用程序时一定要注意,Apple宣称保留使用"两个字母前缀"的权利,也就是说选用的前缀应该都是三个字母.

我们尤其应该注意,当你使用的一个三方库引入了一个你之前引入过的三方库.如果引入的这两个库的作者都没有给自己的库加前缀,那么应用程序很容易出现重复符号错误.

要点

  • 选择与你公司,应用程序或二者有关联的名称作为类名的前缀,并在所有代码中均使用这一前缀.

  • 若自己所开发的程序库用到了第三方库,则应该为其中的名称加上前缀.

2017/8/25 posted in  第三章 接口与API设计

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

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  第二章 对象,消息,运行期