如何编写高质量的Objective-C代码

本文为《Effect Objective-C 2.0 编写高质量iOS与OS X代码的52个有效的方法》的读后感。由于书中的内容很多都是经典的案例,然而直接记住有一定的压力。所以将重点列在下面,方便后期回顾。

第一章 熟悉 Objective-C

  1. Objective-C为C语言添加了面向对象这一个特性,是C的超集。由于动态绑定消息结构,只有在运行时候才会检查对象的类型,所以接收到消息后,会在运行期而非编译器时期来决定。(冠以动态绑定将在下面的后面详细介绍。)
  2. 改变头文件引用情况,从而改善依赖关系。
    • 方法:在不需要调用类内部对象的时候,直接使用@class className来取代#import className.h
    • 原因:
      • 在.h文件中尽可能减少不必要的头文件,一般如果只是为了加入某个类直接可以使用@class className来进行添加,否则容易产生循环import,在编译的时候编译器会报错。
      • 对于保存在一个较大类中的 protocol ,可以将这个 protocol 单独放在一个文件中,以防引入类导致的编译时间的降低。
  3. 采用字面量来代替等价的方法。
    • 方法:
      • NSString 对象创建采用@"this is a NSString Object";
      • NSArray 对象创建采用@[obj1, obj2, obj3];
      • NSDictionary 对象创建采用@{ key1 : obj1, key2 : obj2, key3 : obj3};
    • 原因:Objective-C 对于创建一些常用的对象给予了我们一些语法糖,从而更加简明扼要。
    • 注意点:
      • 如果在创建的过程中如果有对象是nil的,那么会导致程序 crash。
      • 产生的对象都是不可变的,也就是 immutable, 如果要创建对应的可变对象的话,需要使用[object mutableCopy]
  4. 多用类型常量,少用#define

    • 方法:

      • static const NSTimeInterval kAnimationDuration = 0.3f;代替#define ANIMATION_DURATION 0.3;
      • static NSString *const StringValue = @"this is a string"代替#define STRING_VALUE @"this is a String"
      • 对于需要暴露给外部的属性命名如下
        1
        2
        3
        4
        5
        // Class.h
        extern NSString *const StringValue;
        // Class.m
        NSString *const StringValue = @"this is a string"
    • 原因:

      • 由于#define采用的是直接在预处理的过程中就把对应的内容进行替换。而在编写的过程中不会成功提醒。
      • 这样能够清晰的描述所需要设置的类型。
    • 注意点:
      • static 不能出现在 .h 文件中,因为在 .h 文件中使用了 static 也就意味着这个变量为全局变量。
      • const 是为了防止被修改而存在的。
      • 对于不会改变的变量,使用 const
      • .m 文件中使用 static 进行修饰的意思就是每一份该变量的作用于只局限在该编译单元中。编译器每收到一份编译单元(一般是一个类的实现文件),那就会生成一份目标文件(.o文件)。如果没有使用 static 进行修饰,那么当编译器进行编译,如果别的类里面也使用了这个变量名,那么就会提示
        1
        2
        3
        duplicate symbol _property in:
        a.o
        b.o
  5. 对于状态码使用枚举类型。

    • 方法:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      typedef NS_ENUM(NSUInteger, ValueStatu) {
      ValueAStatus,
      ValueBStatus,
      ValueCStatus,
      }
      typedef NS_OPTION(NSUInteger, ValueOption) {
      ValueAOption = 1 << 0,
      ValueBOption = 1 << 1,
      ValueCOption = 1 << 2,
      }
    • 原因:

      • 枚举类型可以更好地表达有限个的类型的意思。方便后期添加。
      • 对于NS_OPTION这个类型,可以判断语句中直接使用 ValueMyOption & ValueBOption来进行判断我所提供的变量是否包含了多个选项。
      • 使用typedef而不是直接使用原有的enum是为了匹配各种编译器,因为苹果给我们在底层对相关内容进行了封装。

第二章 对象、消息、运行期(runtime)

  1. 理解“属性”这一概念
    • 方法:使用@property (nonatomic, strong) *Class className来代替直接使用@public NSData *_data
    • 原因:
      1. 如果使用 JAVA、C++ 的创建对象的方法,编译在编译器计算偏移量,那么在修改这个类的定以后需要进行重新编译。
      2. 如果在2个属性中间加入了第三个属性,那么就会打乱原有的偏移量,如果其他代码还在调用这个偏移量,那么就会发生错误。
      3. Objective-C 使用类对象对相关的内容进行管理,从而永远能够获得正确的偏移量。(在运行期的过程中向类中新增实例变量也可以正确获取。)这就是稳定的应用程序二进制接口(Application Binary Interface, ABI)
    • 注意点:
      • 使用@property来声明变量,编译器会在编译期自动帮你创建 setter 方法和 getter ,这个过程叫做“自动合成(autosynthesis)”,同时对于这个属性,会生成对应的实例变量。同时 Objective-C 中延续了 C 中 struct 的语法糖,对于属性可以直接使用点语法获得对应的实例变量。
      • 你可以使用@synthesize来手动创建对应的实例变量的名字,默认编译器会自动帮你创建实例变量的名字,一般是原有名字前面加上下划线(_)
      • 对于如果你不想让编译器自动帮你创建 setter 或 getter 方法,你直接手动创建即可,但是如果你不想让编译器帮你创建 setter 和 getter 方法,那么就需要在 implementation 中使用@dynamic来声明对应的属性。
  2. 属性的特质
    在前面的@property (nonatomic, strong) *Class className中,括号内,有2个特质,这两个特质表示了这个属性有哪些特性。而这些特质我们分为4类
    • 原子性:
      • atomic(default),原子性,表示该属性在读写时候会加上同步锁,从而防止读取脏数据的可能性。但是极大的影响性能。
      • nonatomic,非原子性,表示该属性在一般情况下有可能会读取到脏数据。
    • 读/写权限。
      • readwrite (default),具有读写特质,如果使用了@synthesize说明编译器会自动帮你创建对应的读写方法。
      • readonly, 只读属性,如果使用了@synthesize说明编译器会自动帮你创建读取方法。
    • 内存管理语义
      • assign(default),只会针对“纯量类型”,比如说CGFloat,NSInteger等等。
      • strong,表示了一种“拥有关系”,设置新值的时候,会保留新值,释放旧值。
      • weak,表示了一种“非拥有关系”,设置新值的时候,不会保留新值,也不会保留旧值,如果他指向的对象被释放,他就会置 nil 。(实现原理类似于写一个字典中,然后根据字典中的 key ,来获得他的 value ,从而确定是否需要置为空)
      • unsafe_unretained,表示了一种“非拥有关系”,他和 weak 类似,不过在目标被摧毁的时候,他不会置空,所以是不安全的。
      • copy,这个特性和 strong 相似,不过他在创建新值的时候不会保留新值,而是创建一份新值的拷贝(copy),但是这个时候,他创建的是新值是不可变的(即不是MutableCopy)。
    • 方法名
      • getter=,指定对应的getter方法名,比如说@property (nonatomic, getter=isOn) BOOL on;这样他的 getter 方法就是isOn,从而在调用的时候更加明白。
      • setter=,一般很少去实现这个方法。实现 setter 方法可能会导致程序出现 Bug。
      • 注意:在赋值语句的时候,最好直接使用实例变量进行赋值。因为这样防止因为 setter 方法发生改变导致的问题。同时,如果属性是readonly那么他就没有setter方法这样做会导致使用setter方法失败。
    • 注意点:在iOS开发中,因为性能原因,所以我们一般使用 nonatomic ,而在 OS X 中进行开发的时候,我们使用 atomic 一般也不会遇到瓶颈。
  3. 初始化的过程中尽可能使用实例变量进行访问。
    • 使用实例变量来获得对应的实例变量进行赋值速度更快
    • 使用实例变量不会调用 setter 方法,如果该属性是 copy 方法,那么就不会触发 copy 方法,只会保留新值并释放旧值。
    • 使用实例变量不会触发 KVO 通过属性来访问,可以方便的使用断点来找到对应的那个属性的时机。
    • 在对象内部的时候,读取使用实例变量,赋值使用属性。
    • 对于懒加载,需要使用属性进行调用,从而创建实例变量。
  4. 对象等同性。

    • 方法:使用 isEqual 来保证两个对象是相同的。
    • 原因:

      • 因为直接 == 比较的是两个指针本身。
      • NSObject 类的协议中用两个判断相同的方法使用isEqualhash。默认是两个指针值相同才返回真。
      • 对于某个对象,我们使用下面代码来返回他的哈希值(哈希值相同,两个对象不一定相同)
        1
        2
        3
        4
        5
        6
        7
        - (NSUInteger)hash {
        NSUInteger firstNameHash = [_firstName hash];
        NSUInteger lastNameHash = [_lastName hash];
        NSUInteger ageHash = _age;
        return firstNameHash ^ lastNameHash ^ age;
        }
    • 注意:一般对于特定类的相等的描写:

      1. 首先判断指针是否相等。
      2. 然后判断类是否相等
      3. 然后判断值是否相等。
      4. 容器相同性:{(1,2)}->{(1),(1,2)}->{(1,2),(1,2)}->copy->{(1,2)}
  5. 以“类族模式”隐藏实现细节。
    • 方法:子类继承父类,父类在创建方法中使用枚举来返回不同子类的类型,同时将绘制逻辑放到父类中实现。
    • 原因:基于Objective-C不是强制类型,隐藏实现方法,使得调用更加简单,更加清晰。
  6. 在既有类中使用关联对象存放自定义数据。
    • 方法:引入<objc/runtime.h>,然后调用void objc_setAssociatedObject(id object, void *key, id value, objc_AssociationPolicy policy)id objc_getAssociatedObject(id object, void *key)void objc_removeAssociatedObject(id object)三个方法来创建,调用,删除关联自定义数据。
    • 原因:更易读懂,无需在代码块中来回游走。
    • 注意点:
      • 关联类型种类包括 assign, nonatomic, atomic, retain, copy, retain, copy。
      • 不能大量使用,否则容易导致代码失控。
      • 该方法是最后解,即用其他方法无法实现的时候才会使用这个方法。
  7. 理解objc_msgSend的作用
    • 方法:使用id returnValue = objc_msgSend(someObject,@selector(messageName:),parameter),其中someObject是接受者,@selector(messageName:)是选择子。这样返回的是是一个id对象。同理存在objc_msgSend_stret(返回struct),objc_msgSend_fpret(返回浮点数)objc_msgSendSuper(给id对象的超类发送信息。)
    • 原因:Objective-C 是一门动态语言,objc_msgSend 就是发送去查找“方法列表”(每个类都有自己的方法列表,同时在查找完之后会保存在这个类自己的快速映射表,方便下次查找)并调用相关函数,如果没有调用到,那么就会沿着继承体系去进行查找,如果最后还是没有,那么就使用转发操作。
    • 具体表现:尾调用优化(tail-call optimization)
  8. 消息转发机制
    • 使用方法: