Effective Objective-C 2.0 要点摘录

  1. 了解 Objective-C 语言的起源
    1. Objective-C 为 C 语言添加了面向对象的特性,是其超集。Objective-C 使用动态绑定的消息结构,也就是说在运行时才会检查对象类型。接收一条消息之后,究竟应执行何种代码,由运行期环境决定而非编译器决定。
    2. 理解 C 语言的核心概念有助于写好 Objective-C 程序。尤其要掌握内存模型与指针。
  2. 在类的头文件中尽量少引入其他头文件
    1. 除非确有必要,否则不要引入头文件。一般来说,应在某个类的头文件中使用向前声明来提及别的类,并在实现文件中引入那些类的头文件。这样做可以尽量降低类之间的耦合。
    2. 有时无法使用向前声明,比如要声明某个类遵循一项协议。这种情况下,尽量把 “该类遵循某协议” 的这条声明移至 “class-continuation 分类” 中。如果不行的话,就把协议单独放在一个头文件中,然后将其引入。
  3. 多用字面量语法,少用与之等价的方法
    1. 应该使用字面量语法来创建字符串、数值、数组、字典。与创建此类对象的常规方法相比,这么做更加简明扼要。
    2. 应该通过下标操作来访问数组下标或字典中的键所对应的元素。
    3. 用字面量语法创建数组或字典时,若值中有 nil,则会抛出异常。因此务必确保值里不含 nil。
  4. 多用类型常量,少用 #define 预处理指令
    1. 不要使用预处理指令定义常量。这样定义出来的常量不含类型信息,编译器只是会在编译前据此进行查找与替换的操作。即使有人重新定义了常量值,编译器也不会产生警告信息,这将导致应用程序中的常量值不一致。
    2. 在实现文件中使用 static const 来定义 “只在编译单元内可见的常量”(translation-unit-specific constant)。由于此类常量不在全局符号表中,所以无须为其名称加前缀。
    3. 在头文件中使用 exten 来声明全局变量,并在相关实现文件中定义其值。这种常量要出现在全局符号表中,所以其名称应加以区隔,通常用与之相关的类名做前缀。
  5. 用枚举表示状态、选项、状态码
    1. 应该用枚举来表示状态机的状态、传递给方法的选项以及状态码等值,给这些值起个易懂的名字。
    2. 如果把传递给某个方法的选项表示为枚举类型,而多个选项有可同时使用,那么就将各选项值定义为2的幂,以便通过按位或操作将其组合起来。
    3. 用 NS_ENUM 与 NS_OPTIONS 宏来定义枚举类型,并指明其底层的数据类型。这样做可以确保枚举是用开发者所选的底层数据类型实现出来的,而不会采用编译器所选的类型。
    4. 在处理枚举类型的 switch 语句中不要实现 default 分支。这样的话,加入新枚举之后,编译器就会提示开发者:switch 语句并未处理所有枚举。
  6. 理解 “属性” 这一概念
    1. 具备 readwrite(读写)特质的的属性拥有 “获取方法”(getter)与 “设置方法”(setter)。若该属性由 @synthesize 实现,则编译器会自动生成这两个方法。
    2. 具备 readonly(只读)特质的属性仅用于获取方法,只有当该属性由 @synthesize 实现时,编译器才会为其合成获取方法。你可以用此特质把某个属性对外公开为只读属性,然后在 “class-continuation 分类” 中将其重新定义为读写属性。
    3. assign “设置方法” 只会执行针对 “纯量类型”(scalar type,例如 CGFloat 或 NSInteger 等)的简单赋值操作。
    4. strong 此特质表明该属性定义了一种 “拥有关系”(owning relationship)。为这种属性设置新值时,设置方法会先保留新值,并释放旧值,然后再将新值设置上去。
    5. weak 此特质表明该属性定义了一种 “非拥有关系”(nonowning relationship)。为这种属性设置新值时,设置方法既不保留新值,也不释放旧值。此特质同 assign 类似,然而在属性所指的对象遭到摧毁时,属性值也会清空(nil out)。
    6. unsafe_unretained 此特质的语义和 assign 相同,但是它适用于 “对象类型”(object type),该特质表达一种 “非拥有关系”(“不保留”,unretained),当目标对象遭到摧毁时,属性值不会自动清空(“不安全”,unsafe),这一点与 weak 有区别。
    7. copy 此特质所表达的所属关系与 strong 类似。然而设置方法并不保留新值,而是将其 “拷贝”(copy)。当属性类型为 NSString * 时,经常用此特质来保护其封装性,因为传递给设置方法的新值有可能指向一个 NSMutableString 类的实例。这个类是 NSString 的子类,表示一种可以修改其值的字符串,此时若是不拷贝字符串,那么设置完属性之后,字符串的值就可能会在对象不知情的情况下遭人更改。所以,这时候就要拷贝一份 “不可变”(immutable)的字符串,确保对象中的字符串值不会无意间变动。只要实现属性所用的对象是 “可变的”(mutable),就应该在设置新属性值时拷贝一份。
    8. getter=<name> 制定 “获取方法” 的方法名。如果某属性时 Boolean 型,而你想为其获取方法上加上 “is” 前缀,那么就可以使用这个办法来指定。比如说,在 UISwitch 类中,表示 “开关”(switch)是否打开的属性值就是这样定义的:@property (nonatomic, getter=isOn) BOOL on;
    9. setter=<name> 指定 “设置方法” 的方法名。这种用法不太常见。
    10. 可以用 @property 语法来定义对象中所封装的数据。
    11. 通过 “特质” 来指定存储数据所需要的正确语义。
    12. 在设置属性所对应的实例变量时,一定要遵从该属性所声明的语义。
    13. 开发 iOS 程序时应该使用 nonatomic 属性,因为 atomic 属性会严重影响性能。
  7. 在对象内部尽量直接访问实例变量
    1. 由于不经过 Objective-C 的 “方法派发”(method dispatch)步骤,所以直接访问实例变量的速度当然比较快。在这种情况下,编译器所产生的代码会直接访问保存对象实例变量的那块内存。
    2. 直接访问实例变量时,不会调用其 “设置方法”,这就绕过了为相关属性所定义的 “内存呢管理语义”。比方说,如果在 ARC 下直接访问一个声明为 copy 的属性,那么并不会拷贝该属性,只会保留新值并释放旧值。
    3. 如果直接访问实例变量,那么不会触发 “键值规则”(Key-Value Observing,KVO)通知。这样做是否会产生问题,还取决于具体的对象行为。
    4. 通过属性来访问有助于排查与之相关的错误,因为可以给 “获取方法” 和/或 “设置方法” 中新增 “断点”(breakpoint),监控该属性的调用者及其访问时机。
    5. 在对象内部读取数据时,应该直接通过实例变量来读,而写入数据时则应通过属性来写。
    6. 在初始化方法及 dealloc 方法中,总是应该直接通过实例变量来读写数据。
    7. 有时会使用惰性初始化技术配置某份数据,在这种情况下,需要通过属性来读取数据。

      事实上,我个人在开发中并不推崇这种模式。我几乎全部使用惰性初始化技术,通常我会在类的内部使用这种技术重写几乎所有属性的 getter 方法,只在其 getter 方法内部直接使用实例变量,而在之外的所有地方都使用点语法来读写它们。这样无论是从代码风格上、还是美观性、一致性上都相对较好。

  8. 理解 “对象等同性” 这一概念
    1. 若要检测对象的等同性,请提供 “isEqual:” 与 hash 方法。
    2. 相同的对象必须具有相同的哈希码,但是两个哈希码相同的对象却未必相同。
    3. 不要盲目的逐个检测每条属性,而是应该依照具体需求来制定检测方案。
    4. 编写 hash 方法时,应该使用计算速度快而且哈希码碰撞几率低的算法。
  9. 以 “类族模式” 隐藏实现细节
    1. 增加某一类族的子类时,需要遵循:
      1. 子类应该继承自类族中的抽象基类。
      2. 子类应该定义自己的数据存储方式。
      3. 子类应当覆写超类文档中指明需要覆写的方法。
    2. 类族模式可以吧实现细节隐藏在一套简单的公共接口后面。
    3. 系统框架中经常使用类族。
    4. 从类族的公共抽象基类中继承子类时要当心,若有开发文档,则应首先阅读。
  10. 在既有类中使用关联对象存放自定义数据

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

    1. 下列方法可以管理关联对象:
      1. void objc_setAssociatedObject(id object, void *key, id value, objc_AssociationPolicy policy) 此方法以给定的键和策略为某对象设置关联对象值。
      2. id objc_getAssociatedObject(id object, void *key) 此方法根据给定的键从某对象中获取相应的关联对象值。
      3. void objc_removeAssociatedObjects(id object) 此方法移除指定对象的全部关联对象。
    2. 可以通过 “关联对象” 机制来把两个对象连起来。
    3. 定义关联对象时可指定内存管理语义,用以模仿定义属性时所采用的 “拥有关系” 与 “非拥有关系”。
    4. 只有在其他做法不可行时才应选用关联对象,因为这种做法通常会引入难于查找的 bug。
  11. 理解 objc_msgSend 的作用
    1. objc_msgSend_stret。如果待发送的消息要返回结构体,那么可交由此函数处理。只有当 CPU 的寄存器能够容纳得下消息返回类型时,这个函数才能处理此消息。若是返回值无法容纳于 CPU 寄存器中(比如说返回的结构体太大了),那么就由另一个函数执行派发。此时,那个函数会通过分配在栈上的某个变量来处理消息所返回的结构体。
    2. objc_msgSend_fpret。如果消息返回的是浮点数,那么可交由此函数处理。在某些架构的 CPU 中调用函数时,需要对 “浮点数寄存器”(floating-point register)做特殊处理,也就是说,通常所用的 objc_msgSend 在这种情况下并不适合。这个函数是为了处理 x86 等架构 CPU 中某些令人稍觉惊讶的奇怪情况。
    3. objc_msgSendSuper。如果要给超类发消息,例如 [super message:parameter],那么就交由此函数处理。也有另外两个与 objc_msgSend_stretobjc_msgSend_fpret 等效的函数,用于处理发送给 super 的相应消息。
    4. 消息由接收者、选择子及参数构成。给某对象 “发送消息”(invoke a message)也就相当于在该对象上 “调用方法”(call a method)。
    5. 发给某对象的全部消息都要由 “动态消息派发系统”(dynamic message dispatch system)来处理,该系统会查出对应的方法,并执行其代码。
  12. 理解消息转发机制
    1. 若对象无法响应某个选择子,则进入消息转发流程。
    2. 通过运行期的动态方法解析功能,我们可以在需要用到某个方法时再将其加入类中。
    3. 对象可以把其无法解读的某些选择子转交给其他对象来处理。
    4. 经过上述两步之后,如果还是没办法处理选择子,那就启动完整的消息转发机制。
  13. 用 “方法调配技术” 调试 “黑盒方法”
    1. 在运行期,可以向类中新增或替换选择子所对应的方法实现。
    2. 使用另一份实现来替换原有的方法实现,这道工序叫做 “方法调配”,开发者常用此技术向原有实现中添加新功能。
    3. 一般来说,只有调试程序的时候才需要在运行期修改方法实现,这种做法不宜滥用。
  14. 理解 “类对象” 的用意
    1. 每个实例都有一个指向 Class 对象的指针,用以表明其类型,而这些 Class 对象则构成了类的继承体系。
    2. 如果对象类型无法在编译期确定,那么就应该使用类型信息查询方法来探知。
    3. 尽量使用类型信息查询方法来确定对象类型,而不要直接比较类对象,因为某些对象可能实现了消息转发功能。
  15. 用前缀避免命名空间冲突
    1. 选择与你的公司、应用程序或二者皆有关联之名称做为类名的前缀。
    2. 若自己开发的程序库中用到了第三方库,则应为其中的名称加上前缀。
  16. 提供 “全能初始化(Designated Initializer)方法”
    1. 在类中提供一个全能初始化方法,并于文档里声明。其他初始化方法均应调用此方法。
    2. 若全能初始化方法与超类不同,则需覆写超类中的对应方法。
    3. 如果超类的初始化方法不适用子类,那么应该覆写这个超类方法,并在其中抛出异常。
  17. 实现 description 方法
    1. 实现 description 方法返回一个有意义的字符串,用以描述该实例。
    2. 若想在调试时打印出更详尽的对象描述信息,则应该实现 debugDescription 方法。
  18. 尽量使用不可变对象
    1. 尽量创建不可变的对象。
    2. 若某属性仅可于对象内部修改,则在 “class-continuation 分类” 中将其由 readonly 属性扩展为 readwrite 属性。
    3. 不要把可变的 collection 作为属性公开,而应提供相关方法,以此修改对象中的可变 collection。
  19. 使用清晰而协调的命名方式
    1. 起名时应遵从标准的 Objective-C 命名规范,这样创建出来的接口更容易为开发者所理解。
    2. 方法名要言简意赅,从左至右读起来要像个日常用语中的句子才好。
    3. 方法名里不要使用缩略后的类型名称。
    4. 给方法起名时的第一要务就是确保其风格与你自己的代码或所需要集成的框架相符。
  20. 为私有方法名加前缀
    1. 给私有方法的名称加前缀,这样可以很容易地将其同公共方法区分开。
    2. 不要单用一个下划线做私有方法的前缀,因为这种做法是预留给苹果公司使用的。
  21. 理解 Objective-C 错误类型
    1. 只有发生了可使整个应用程序崩溃的严重错误时,才应使用异常。
    2. 在错误不那么严重的情况下,可以指派 “委托方法”(delegate method)来处理错误,也可以把错误信息放在 NSError 对象里,经由 “输出参数” 返回给调用者。
  22. 理解 NSCopying 协议
    1. 若想令自己所写的对象具有拷贝功能,则需要实现 NSCopying 协议。
    2. 如果自定义的对象分为可变版本与不可变版本,那么就要同时实现 NSCopying 与 NSMutableCopying 协议。
    3. 复制对象时需决定采用浅拷贝还是深拷贝,一般情况下应该尽量执行浅拷贝。
    4. 如果你所写的对象需要深拷贝,那么可以考虑新增一个专门执行深拷贝的方法。
  23. 通过委托与数据源协议进行对象间通信
    1. 委托模式为对象提供了一套接口,使其可由此将相关事件 告知其他对象。
    2. 将委托对象应该值的接口定义成协议,在协议中把可能需要处理的事件定义成方法。
    3. 当某对象需要从另外一个对象中获取数据时,可以使用委托模式。这种情境下,该模式亦称 “数据源协议”(data source protocal)。
    4. 若有必要,可实现含有段位的结构体,将委托对象是否能够响应相关协议这一信息缓存至其中。
  24. 将类的实现代码分散到便于管理的数个分类之中
    1. 使用分类机制把类的实现代码划分成易于管理的小块。
    2. 将应该视为 “私有” 的方法归入名叫 Private 的分类中,以隐藏实现细节。
  25. 总是为第三方类的分类名称加前缀
    1. 向第三方类中添加分类时,总应给其名称加上你专用的前缀。
    2. 向第三方类中添加分类时,总应给其中的方法名加上你专用的前缀。
  26. 勿在分类中声明属性
    1. 把封装数据所用的全部属性都定义在主接口里。
    2. 在 “class-continuation 分类” 之外的其他分类中,可以定义存取方法,但尽量不要定义属性。
  27. 使用 “class-continuation 分类” 隐藏实现细节
    1. 通过 “class-continuation 分类” 向类中新增实例变量。
    2. 如果某属性在主接口中声明为 “只读”,而类的内部又要用设置方法修改此属性,那么就在 “class-continuation 分类” 中将其扩展为 “可读写”。
    3. 把私有方法的原型声明在 “class-continuation 分类” 里面。
    4. 若想使类所遵循的协议不为人所知,则可于 “class-continuation 分类” 中声明。
  28. 通过协议提供匿名对象
    1. 协议可在某种程度上提供匿名类型。具体的对象类型可以淡化成遵从某协议的 id 类型,协议里规定了对象所应实现的方法。
    2. 使用匿名对象来隐藏类型名称(或类名)。
    3. 如果具体类型不重要,重要的是对象能够响应(定义在协议里的)特定方法,那么可使用匿名对象来表示。
  29. 理解引用计数
    1. 引用计数机制通过可以递增递减的计数器来管理内存。对象创建好之后,其保留计数至少为1。若保留计数为正,则对象继续存活。当保留计数降为0时,对象就被销毁了。
    2. 在对象生命周期中,其余对象通过引用来保留或释放此对象。保留与释放操作分别会递增及递减保留计数。
  30. 以 ARC 简化引用计数
    1. 在应用程序中,可以用下列修饰符来改变局部变量与实例变量的语义:
      1. __strong:默认语义,保留此值。
      2. __unsafe_retained:不保留此值,这么做可能不安全,因为等到再次使用变量时,其对象可能已经回收了。
      3. __weak:不保留此值,但是变量可以安全使用,因为如果系统把这个对象回收了,那么变量也会自动清空。
      4. __autoreleasing:把对象 “按引用传递”(pass by reference)给方法时,使用这个特殊的修饰符。此值在方法返回时自动释放。
    2. 有 ARC 之后,程序员就无须担心内存管理问题了。使用 ARC 来编程,可省去类中的许多 “样板代码”。
    3. ARC 管理对象生命期的办法基本上就是:在合适的地方插入 “保留” 及 “释放” 操作。在 ARC 环境下,变量的内存管理语义可以通过修饰符指明,而原来则需要手工执行 “保留” 及 “释放” 操作。
    4. 由方法所返回的对象,其内存管理语义总是通过方法名来体现。ARC 将此确定为开发者必须遵守的规则。
    5. ARC 只负责管理 Objective-C 对象的内存。尤其要注意:CoreFoundation 对象不归 ARC 管理,开发者必须适时调用 CFRetain/CFRelease。
  31. 在 dealloc 方法中只释放引用并解除监听
    1. 在 dealloc 方法里,应该做的事情就是释放指向其他对象的引用,并取消原来订阅的 “键值观测”(KVO)或 NSNotificationCenter 等通知,不要做其他事情。
    2. 如果对象持有文件描述符等系统资源,那么应该专门编写一个方法来释放此种资源。这样的类要和其使用者约定:用完资源后必须调用 close 方法。
    3. 执行异步任务的方法不应该在 dealloc 方法内调用;只能在正常状态下执行的那些方法也不应在 dealloc 里调用,因为此时对象已经处于正在回收的状态了。
  32. 编写 “异常安全代码” 时留意内存管理的问题
    1. 捕获异常时,一定要注意将 try 块内所创立的对象清理干净。
    2. 在默认情况下,ARC 不生成安全处理异常所需的清理代码。开启编译器标志后,可生成这种代码,不过会导致应用程序变大,而且会降低运行效率。
  33. 以弱引用避免保留环
    1. 将某些引用设置为 weak,可避免出现 “保留环”。
    2. weak 引用可以自动清空,也可以不自动清空。自动清空(autonilling)是随着 ARC 而引入的特性,由运行期系统来实现。在具备自动清空功能的若引用上,可以随意读取其数据,因为这种引用不会指向已经回收过的对象。
  34. 以 “自动释放池块” 降低内存峰值
    1. 自动释放池排布在栈中,对象收到 autorelease 消息后,系统将其放入最顶端的池里。
    2. 合理运用自动释放池,可降低应用程序的内存峰值。
    3. @autoreleasepool 这种新式写法能创建出更为轻便的自动释放池。