【Effective Objective-C 2.0读书笔记】第二章:对象、消息、运行期

来源:互联网 发布:绿化预算软件 编辑:程序博客网 时间:2024/05/16 11:52

在Objective-C等面向对象语言中,“对象”是基本构造单元,开发者可以通过对象来存储并传递数据。在对象之间传递数据并执行任务的过程即为“消息传递”。

当应用程序运行起来之后,为其提供相关支持的代码叫做“Objective-C运行期环境”(Objective-C runtime),它提供了一些使得对象之间能够传递消息的重要函数,并且包含创建类实例所用的全部逻辑。

第6条:理解”属性”这一概念

属性可以拥有的特质分为四类:

原子性

atomic:原子属性,默认就是atomic。需要消耗大量的资源。

nonatomic:非原子属性。适合内存小的移动设备。

默认情况下,由编译器所合成(synthesize)的方法会通过锁定机制来确保它是原子的(atomic)。如果属性具有nonatomic特质,则不使用同步锁。

需要注意的是,开发iOS程序时一般都会使用nonatomic属性,这是因为在iOS中使用同步锁的开销较大,这会带来性能问题。但是在Mac OS X中,使用atomic属性通常都不会有性能瓶颈。

一般情况下,并不要求属性必须是原子的,因为这并不能保证“线程安全”,若要实现线程安全,需要更为深层的锁定机制才行。例如一个线程要连续多次读取某属性的过程中,另一个线程对该属性值进行了修改,那么即便将属性声明为atomic,还是会读到不同的属性值。

读/写权限

readwrite:表明属性具有读取和设置方法。

readonly:表明属性只拥有读取方法。

若属性由@synthesize实现,则编译器才会自动合成与其读写权限相关的方法。

内存管理语义

内存管理语义仅会影响属性的“设置方法”。编译器在合成存取方法时,要根据此特质来决定所生成的代码。如果自己来编写存取方法,那么就必须与相关属性所声明的特质相符。

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

  • strong:表明该属性定义了一种拥有关系。为该属性设置新值时,会先保留新值,并释放旧值,最后再设置新值。

  • weak:表明该属性定义了一种非拥有关系。为该属性设置新值时,既不保留新值,也不释放旧值。此特质跟assign类似,然而在属性所指对象被销毁时,该属性也会清空(nil out)。

  • unsafe-unretained:此特质的语义跟assign相同,但它适用于对象类型。表明该属性定义了一种非拥有关系,在属性所指对象被销毁时,该属性不会自动清空,这点跟weak不同。

  • copy:此特质所表达的所属关系跟strong类似。然而设置方法并不保留新值,而是将其拷贝。只要实现属性所用的对象是可变的(mutable),就应该在设置新属性值时拷贝一份。当属性类型为NSString*时,经常用此特质来保护其封装性,因为传递给设置方法的新值有可能是NSMutableString类型的实例。

方法名

可通过如下特质来指定存取方法的方法名。

  • getter=<name>:指定“获取方法”的方法名。例如:
@property (nonatomic, getter=isOn) BOOL on;
  • setter=<name>:指定“设置方法”的方法名,这种用法不太常见。

第7条:在对象内部尽量直接访问实例变量

笔者强烈建议除了几种特殊情况之外,在读取实例变量时采用直接访问的形式,而在设置实例变量的时候通过属性来做。

一种特殊情况是在初始化方法中,一般设置实例变量时都是采用直接访问来设置的形式。
另一种特殊情况是惰性初始化,在这种情况下必须通过“获取方法”来访问属性,否则实例变量就永远不会初始化。

要点:

在初始化方法及dealloc方法中,总是应该直接通过实例变量来读写数据。

第8条:理解”对象等同性”(Object Equality)这一概念

比较对象的等同性是一个非常有用的功能。不过按照==操作符比较的是比较两个对象的指针地址,而不是其所指的对象。应该使用NSObject协议中的isEqual方法来判断两个对象的等同性。NSObject类对isEqual方法的默认实现是当且仅当两个对象的指针值相等时,才判定这两个对象相等,这时hash方法返回的值也必须相等。

例如有一EOCPerson类,包含若干字段,isEqual方法可实现如下:

- (BOOL) isEqual:(id)object{    if([self class] == [object class]){        return [self isEqualToPerson: (EOCPerson *)object];    } else {        return [super isEqual: object];    }    return NO;}- (BOOL) isEqualToPerson:(EOCPerson *)otherPerson{    if(self == otherPerson) return YES;    if([_firstName isEqualToString: otherPerson.firstName] && [_lastName isEqualToString: otherPerson.lastName] && _age != otherPerson.age])        return YES;    return NO;}

计算hash值的方法可实现如下,这样既能保持高效率,又能使生成的hash码至少落在一定范围之内,不会频繁重复:

- (NSUInteger)hash{    NSUInteger firstNameHash = [_firstName hash];    NSUInteger lastNameHash = [_lastName hash];    NSUInteger ageHash = _age;    return firstNameHash ^ lastNameHash ^ ageHash;}

要点:

若要检查对象的等同性,请提供isEqualhash方法。

相同的对象必须具有相同的hash码,但拥有相同hash码的对象却不一定相同。

编写hash方法时,应该使用计算速度快而且哈希码碰撞几率低的算法。

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

在Cocoa中,许多类实际上是以类簇的方式实现的,即它们是一群隐藏在通用接口之下的与实现相关的类。例如创建NSString对象时,实际上获得的可能是NSLiteralString、NSCFString、NSSimpleCString、NSBallOfString或者其他未写入文档的与实现相关的对象。

类簇的抽象基类的实现示例:

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 day's 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

类簇的实体子类的实现示例:

@interface EOCEmployeeDeveloper : EOCEmployee@end@implementation EOCEmployeeDeveloper- (void)doADaysWork {    [self writeCode];}@end

判断某对象是否位于类簇中,不要直接检测两个“类对象”(class)是否相等,而应该使用类型信息查询方法:

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

要点:

类簇模式可以把实现细节隐藏在一套简单的公共接口后面。

系统框架中经常使用类簇。

从类簇的公共抽象基类中继承子类时要当心,若有开发文档,则应首先阅读。

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

管理关联对象的方法有:

// Sets up an association of object to value with the given key and policy.void objc_setAssociatedObject(id object, void *key, id value, objc_AssociationPolicypolicy)// Retrieves the value for the association on object with the given key.id objc_getAssociatedObject(id object, void *key)// Removes all associations against object.void objc_removeAssociatedObjects(id object)

要点:

可以通过“关联对象”(associated objects)机制将两个对象连起来。

定义关联对象时,可指定内存管理语义,用以模仿定义对象属性时所采用的“拥有关系”与“非拥有关系”。

只有在其他方法不可行时才采用关联对象,因为这种做法通常会引入难以查找的bug。

第11条:理解objc_msgSend的作用

C语言中有静态绑定和动态绑定两种函数调用方式。Objective-C作为C语言的超集,向对象发送消息时使用动态绑定机制来决定需要调用的方法。在底层,所有方法都是普通的C语言函数,然而在对象收到消息后究竟调用哪个方法则完全于运行期决定,甚至可以在程序运行时改变,这些特性使得Objective-C成为一门真正的动态语言。

objc_msgSend函数会根据接收者与选择子的类型来调用适当的方法。为了完成此操作,该函数需要在接收者所属的类中搜寻其“方法列表”,如果找到与选择子名称相符的方法,就跳转到其实现代码;否则即沿着继承体系继续向上查找;如果仍未找到,就执行“消息转发”(message forwarding)操作。

objc_msgSend调用方法有个优化操作。它会将匹配结果缓存在“快速映射表”(fast map)里。每个类都有这样一块缓存,若是稍后还发送相同的消息,就会加快执行效率。

前面讲的这部分内容只描述了部分消息的调用过程,其他“边界情况”(edge case)则需要交由Objective-C运行环境中的另外一些函数来处理:

  • objc_msgSend_stret
  • objc_msgSend_fpret
  • objc_msgSendSuper

要点:

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

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

第11条讲述了对象的消息传递机制;第12条承接第11条,在对象收到无法解读的消息之后,就会启发“消息转发”(message forwording)机制,程序员可经由此过程告诉对象应该如何处理未知消息。

消息转发的顺序如下:

  • 动态方法解析: 向当前对象的所属类发送resolveInstanceMethod:(针对实例方法)或resolveClassMethod(针对类方法)消息,检查是否动态向该类添加了方法。使用此方案的前提是:相关的实现代码已经写好,只等着运行时直接插在类中。此方案常用来实现@dynamic属性。

  • 快速消息转发: 检查该对象是否实现了forwardingTargetForSelector:实例方法,若实现了则调用这个方法,将对象无法解读的某些选择子转交给其他对象来处理。如果对象实现了此方法,并返回一个非nil的结果,则这个对象会作为消息的新接收者。当然返回值不能是self自身,否则会无限循环。如果我们没有指定相应的对象来处理选择子,则应调用父类的实现来返回结果。使用这个方法通常是在对象内部,可能还有一系列其它对象能处理该消息,我们便可借这些对象来处理消息并返回,这样在对象外部看来,还是由该对象亲自处理了这一消息。

  • 标准消息转发: 经过上述两步之后,如果还是无法处理选择子,则启动完整的消息转发机制。我们需要重写methodSignatureForSelector:forwardInvocation:实例方法。runtime发送 methodSignatureForSelector:消息获取选择子对应的方法签名,即参数与返回值的类型信息。runtime则根据方法签名创建描述该消息的NSInvocation,以创建的NSInvocation对象作为参数,向当前对象发送forwardInvocation:消息。forwardInvocation:方法定位能够响应封装在此NSInvocation中的消息的对象。此NSInvocation对象将会保留调用结果,运行时系统会提取这一结果并将其发送到消息的原始发送者。在这个方法中我们可以实现一些更复杂的功能,可对消息内容进行修改,比如追回一个参数等,然后再去触发消息。另外,若发现某个消息不应由本类处理,则应调用父类的同名方法,以便继承体系中的每个类都有机会处理此调用请求。NSObject的forwardInvocation:方法实现只是简单调用了doesNotRecognizeSelector:方法,它不会转发任何消息,只抛出异常导致程序退出。1

消息转发机制

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

方法调配技术可以在运行期改变与给定的选择子名称相对应的方法。如果善用该特性,则可发挥巨大优势,因为我们不需源代码,也不需通过继承子类来覆写方法就能改变这个类本身的功能。新功能将在本类的所有实例中生效。2
要点:

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

使用另一份实现来替换原有方法的实现,称为”方法调配”(method swizzling),开发者常用此技术向原有实现中添加新功能。一般来说,只有调试程序的时候才需要在运行期修改方法实现,这种做法不宜滥用。

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

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

类型信息查询方法包括isMemberOfClass:(判断对象是否为某个特定类的实例),isKindOfClass:(判断对象是否为某类或其派生类的实例)。像这样的类型信息查询方法使用isa指针获取对象所属的类,然后通过super_class指针在继承体系里游走。

另外一种可精确判断出对象是否为某类实例的办法是:

id object = /* ... */;if([object class] == [EOCSomeClass class]){    // 'object' is an instance of EOCSomeClass}

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

要点:

每个实例都有一个指向Class对象的指针,用以表明其类型,而这些Class对象则构成了类的继承体系。

如果对象类型无法在编译期确定,那么就应该使用类型信息查询方法来探知。

尽量使用类型信息查询方法来确定对象类型,而不要直接比较类对象,因为某些对象可能实现了消息转发功能。


  1. http://southpeak.github.io/blog/2014/11/03/objective-c-runtime-yun-xing-shi-zhi-san-:fang-fa-yu-xiao-xi-zhuan-fa/ “Objective-C Runtime 运行时之三:方法与消息” ↩
  2. http://southpeak.github.io/blog/2014/11/06/objective-c-runtime-yun-xing-shi-zhi-si-:method-swizzling/ “Objective-C Runtime 运行时之四:Method Swizzling” ↩
0 0
原创粉丝点击