转载

Effective Objective-C 2.0 总结(二)

对象、消息、运行期

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

1.“对象”(object)就是 “基本构造单元”(building block),开发者可以通过对象来存储并传递数据,在对象直接传递数据并执行任务的过程就叫做 “消息传递”(Messaging)。

2.如果对象布局在编译器就固定了,访问变量时,编译器会使用 “偏移量”(offset)来计算,这个偏移量是 “硬编码”(hardcode),表示该变量距离存放对象的内存区域的起始地址有多远。 存在一个问题:如果代码使用了编译期计算出来的偏移量,那么修改类定义之后必须重新编译,否则就会出错。

Objective-C 处理方式是:把实例变量当作一种存储偏移量所用的 “特殊变量”(speacial variable),交由 “类对象”(class object)保管。偏移量会在运行期查找,这样子总能找到正确的偏移量,这就是稳固的 “应用程序二进制接口”(Application Binary Interface,ABI)。

3.使用 “点语法” 的效果与直接调用存取方法相同,没有丝毫差别。

4.属性有很多优势:

可以使用 “点语法”

  • 编译器会自动编写访问这些属性所需的方法,此过程就做 “自动合成”(autosynthesis)

  • 编译器还会自动向类添加适当类型的实例变量,并且在属性名前面加下划线,以此作为实例变量的名字

  • 在实现代码中可以通过@synthesize 语法来指定实例变量的名字

@implementation EOCPerson @synthesize firstName = _myFirstName; @synthesize lastName = _myLastName; @end
  • @dynamic 关键字会告诉编译器:不要自动创建实现属性所用的实例变量,也不要为其创建存取方法

属性特质

1.属性可以拥有的特质分类四类:原子性、读/写权限、内存管理语义、方法名

2.原子性(atomicity)

属性默认情况下编译器所合成的方法会通过锁定机制确保其原子性,用nonatomic 特质,就不使用同步锁。

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

3.读/写权限

  • 具备readwrite(读写)特质的属性拥有 “获取方法”(getter)与 “设置方法”(setter)。若该属性由@synthesize 实现,则编译器会自动生成这两个方法。

  • 具备readonly(只读)特质的属性仅拥有获取方法,只有该属性由@synthesize 实现,编译器才会为其合成获取方法。

4.内存管理语义

  • assign

  • strong

  • weak

  • unsafe_unretained

  • copy

5.方法名

可以指定存取的方法名。

@property (nonatomic,getter = isOn) BOOL on;
  • 可以用@property 语法来定义对象中所封装的数据。

  • 通过 “特质” 来指定存储数据所需的正确语义

  • 在设置属性所对应的实例变量时,一定要遵从该属性所声明的语义。

  • 开发iOS 程序时应该使用nonatomic 属性,因为atomic 属性会严重影响性能。

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

1.强烈建议:读取实例变量的时候采用直接访问的形式,而在设置实例变量的时候通过属性来做。

2.关于直接访问跟通过属性访问的区别:

  • 由于不经过Objective-C 的 “方法派发” 步骤,所以直接访问实例变量的速度当然比较快。在这种情况下,编译器所生成的代码会直接访问保存对象实例变量的那块内存。

  • 直接访问实例变量时,不会调用其 ”设置方法“,这就绕过了为相关属性所定义的 ”内存管理语义“。比方说,如果在ARC 下直接访问一个声明为copy 的属性,那么并不会拷贝改属性,只会保留新值并释放旧值。

  • 如果直接访问实例变量,那么不会触发 ”键值观测“(Key-Value Observing,KVO)通知。

  • 通过属性来访问有助于排查与之相关的错误,因为可以给 ”获取方法“ 或 ”设置方法“ 中新增断点,进行调试。

3.在初始化方法中总是应该直接访问实例变量,避免子类重写了设置方法(处理异常情况抛出异常)但是:如果待初始化的实例声明在超类中,而我们又无法在子类直接访问此实例变量的话,那么就需要调用 “设置方法” 了。

4.在 “惰性初始化”(lazy initialization),必须通过 “获取方法” 来访问属性,不然实例变量永远不会初始化。

  • 在对象内部读取数据时,应该直接通过实例变量来读,而写入数据时,则应通过属性来写。

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

  • 有时会使用惰性初始化技术配置某份数据,在这种情况下,需要通过属性来读取数据。

第 8 条:理解 “对象等同性” 这一概念

1.使用 == 操作符比较的两个指针的本身,而不是其所指的对象;所以这里有可能会出轨,得不到我们想要的结果。NSObject 提供 “isEqual” 方法,某些对象也提供了特殊的 “等同性判定方法”

NSString *foo =@"Badger 123"; NSString *bar = [NSString stringWithFormat:@"Badger %i",123]; BOOL equalA = (foo == bar);    //equalA = NO BOOL equalB = [foo isEqual:bar];    //equalB = YES BOOL equalC = [foo isEqualToString:bar];    //equalC = YES

2.NSObject 协议中有两个用于判断等同性的关键方法:

- (BOOL)isEqual:(id)object; - (NSUInteger)hash;

如果 “isEqual” 方法判定两个对象相等,那么其hash 方法也必须返回同一个值。但是,如果两个对象的hash 方法返回同一个值,那么 “isEqual” 方法未必会认为两者相等。

对于实现hash 方法需要一些技巧:

- (NSUInteger)hash {

    return 1337;

}

//这样子是可以的,但是会对collection 使用这个对象产生性能问题。因为在collection 在检索哈希表的时,会用对象的哈希码来做索引,在set 集合中,会根据哈希码把对象分装到不同的数组里面,在添加新对象的时候,要根据其哈希码找对与之对应的数组,依次检查其中各个元素,看数组已有的对象是否和将要添加的新对象相等,如果相等,就说明添加的对象已经在set 集合中了,是添加失败的。(如果所有对象的hash 值对一样,这样子set 集合只会有一个数组,所有数据都在一起了,每次插入数据都会遍历这个数组,这样子就会出现性能问题)

- (NSUInteger)hash {

    NSString *stringToHash = [NSString stringWithFormat@"%@:%@",_firstName,_lastNmae];

    return [stringToHash hash];

}

//这样子能保证返回不同的哈希码,但是这里会存在创建字符串的开销,会比返回单一值要慢

- (NSUInteger)hash {

    return [self.firstName hash] ^ [self.lastNmae hash];

}

//这样子可以保存较高的效率,又不会过于频繁的重复

3.特定类所具有的等同性判定方法

isEqualToString、isEqualToArray、isEqualToDictionary

如果需要经常判断等同性,可以自己创建等同性判断方法,这样子可以避免检测参数的类型,提升检测效率。

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

等同性判定的执行深度

在我们只需要通过判断一个标识符就可以判断对象相等的时候,我们重写方法可以很方便的达到目的,比如判断一个idectifier 就能确定这两个对象相等,就不用判断那么多属性了。

容器中可变类的等同性

把某个对象放入colloection 之后,不应该再去改变其哈希码了,不然会出现问题,在set 集合会导致改变之后对象存在在一个在原则上 “错误” 的位置。

  • 若想检测对象的等同性,请提供 “isEqual:” 与 hash 方法。

  • 相同的对象必须具有相同的哈希码,但是两个哈希码相同的对象却未必相同。

  • 不要盲目地逐个检测每条属性,而是应该依照具体需求来指定检测方案。

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

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

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

2.用户无须自己创建子类实例,只需要调用基类方法来创建即可。

创建类族

1.每个 “实体子类” 都从基类继承而来,“工厂模式” 是创建类族的办法之一,调用基类方法返回子类实例。

2.如果对象所属的类位于某个类族中,那么查询其类型信息要注意,你可能觉得自己创建了某个类的实例,然后实际上创建的却是其子类的实例。

-(BOOL) isKindOfClass: classObj; 判断是否是这个类或者这个类的子类的实例 -(BOOL) isMemberOfClass: classObj; 判断是否是这个类的实例

Cocoa 里的类族

1.系统框架中有许多类族,大部分collection 类都是类族。

  • 在使用NSArray 的alloc 方法来获取实例时,该方法首先会分配一个属于某类的实例,此实例充当 “占位数组”(placeholder array)。该数组稍后会转换另一个类的实例,而那个类是NSArray 的实体子类。

2.对于Cocoa 中NSArray 这样子的类族,新增子类需要遵循几条规则:

  • 子类应该继承自类族中的抽象基类。

若要编写NSArray 类族的子类,则需要其继承自不可变数组的基类或可变数组的基类。

  • 子类应该定义自己的数据存储方式。

编写NSArray 子类时,必须用一个实例变量来存放数组中的对象;NSArray 本身只是包在其他隐藏对象外面的壳,它仅仅定义了所有数组都需要的一些接口。

  • 子类应当覆写超类文档中指明需要覆写的方法

在每个抽象基类中,都有一些子类必须覆写的方法,编码前需要看下文档。

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

  • 系统框架经常使用类族。

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

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

1.可以给类关联许多其他的对象,这些对象通过 “键” 来区分。

2.储存对象值的时候,可以指明 ”存储策略“(storage policy),用以维护相应的 ”内存管理语义“,

objc_AssociationPolicy

的枚举定义存储策略。

| 关联类型 | 等效的@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 |

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

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

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

id objc_getAssociatedObject(id object, const void *key)

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

void objc_removeAssociatedObjects(id object)

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

设置关联对象用的键是不透明指针(opaque pointer),其指向的数据结构不局限于某种特定类型的指针。

设置关联对象值时,若想令两个键匹配到同一个值,则两者必须是完全相同的指针,所以在设置关联对象值时,通常使用静态全局变量做键。

  • 可以通过 “关联对象” 机制来把两个对象连起来。

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

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

正文到此结束
Loading...