注:该读书笔记是为参加牛客网举办的“有书共读”活动所写,按照约定会在牛客网发布。

第1条 了解Objective-C的起源

1.1 使用消息结构而非函数调用

  • Objective-C起源自Smalltalk,后者是消息型语言的鼻祖。

  • 使用消息结构的语言,不论是否多态,总是在运行时才会去查找所要执行的方法。编译器不关心接收消息的对象是何种类型,而是运行时进行“动态绑定”。

  • 使用函数调用的语言,则由编译器决定运行时所应执行的代码,如果调用的函数是多态的,那么会按照虚方法表,来查出到底应该执行哪个函数。

1.2 运行期组件

  • 使用Objective-C的面向对象特性所需的全部数据结构及函数都在运行期组件里面。

  • 运行期组件中含有全部的内存管理方法。

  • 运行期组件本质上就是一种与开发者所编代码相链接的“动态库”,其代码能把开发者编写的所有程序粘合起来。

  • 更新运行期组件可以提升应用程序性能。

1.3 Objective-C是C的超集

  • 理解C语言的内存模型会有帮助

  • Objective-C语言的指针用于指向对象,因为对象所占的内存总是分配在“堆”中。如果对象不声明为指针,会被提示:error: interface type cannot be statically allocated.

  • Objective-C将内存管理抽象出来,不需要malloc及free来分配或释放对象内存。而是使用“引用计数”来进行管理。

  • 有时候会遇到不含有*的变量,它们可能会使用“栈”控件,这些变量保存的不是Objective-C对象。

    • 比如CoreGraphics框架中的CGRect,实际上是个结构体(使用对象的话性能会受影响):
Struct CGRect {
  CGPoint origin;
  CGSize size;
};
typedef struct CGRect CGRect;

第2条 在类的头文件尽量少引入其他头文件

  • 使用#import而不使用#include。

  • 减少引用的头文件可以减少编译时间。

  • (对此我小声逼逼一句,好怀念做安卓开发时候用的Android Studio,没用到的头文件都会变成灰色,而在Xcode则完全看不到,就算想减少头文件引入,也不知道应该删除哪一个。。有啥好方法吗)

2.1 使用“向前声明”

  • 在不需要知道某个类的细节的时候,可以使用@class ClassName来“向前声明”一个类,减少头文件的引入。比如在某类的头文件中,只需要知道有这么一个类就可以了,就会使用这种声明;而在.m实现文件中,需要使用到该类的细节,则会需要正式引入头文件。

  • 向前声明也解决了两个类互相引用的问题:两个类需要互相引用时,如果各自引用对方的头文件,则会导致“循环引用”。使用#import虽然不会导致死循环,但还是会意味着这两个类中有一个无法被正确编译。

2.2 有时候“#import”是难避免的

  • 这两种情况都必须在头文件中引入其他头文件。如果你写的类继承自某个超类,则必须引入定义的那个超类的头文件;同理,如果要声明你写的类遵从某个协议,那么该协议必须有完整定义,并且不能使用向前声明。

2.3 协议放在头文件中

  • 最好把协议单独放在头文件中,这个时候#import是无可避免的

  • 然而有些协议,如“委托协议”就不用单独写一个头文件了。在那种情况下,协议只有与接收协议的委托的类放在一起才有意义。此时最好能在实现文件中声明此类实现了该委托协议,并把这段实现代码放在分类里。这样的话,只要在实现文件中包含委托协议的头文件即可,而不需要将其放在公共头文件里。

第3条 多用字面量语法,少用与之等价的方法

3.1 字符串

NSString *someString = @"Effective Objective-C 2.0";

3.2 字面数值

NSNumber *intNumber = @1;

NSNumber *floatNumber = @2.5f;

NSNumber *doubleNumber = @3.14159;

NSNumber *boolNumber = @YES;

NSNumber *charNumber = @'a';

int x = 5;

float y = 6.32f;

NSNumber *expressionNumber = @(x * y);

3.3 字面量数组

NSArray *arr = @[@"1", @"2", @"3", @"4", ];

NSString *one = arr[0];

注意:使用字面量语法创建数组时,数组元素中不能有nil,否则会抛出异常。

3.4 字面量字典

NSDictionary *params = @{
  @"hello" : @"Bob",
  @"hi" : @"Xiaohong",
  @"hehe" : @"Lily",
};

注意:使用字面量语法创建字典时,字典元素中不能有nil,否则会抛出异常。

3.5 可变数组与字典

mutableArray[1] = @"dog";

mutableDictionary[@"dog"] = @"A dog.";

优点

  • 缩减代码长度,使之更为易读

  • 使用字面量语法创建数组(字典也一样)时,数组元素中不能有nil,否则会抛出异常。但是,这种抛出异常,可能比NSArray的arrayWithObjects:更好,因为元素为nil,是程序有误,这时终止运行,总比创建好数组之后才发现元素个数少了要好,也能更快发现错误。

第4条 多用类型常量,少用#define 预处理指令

4.1 预处理指令

  • 使用#define可以写一个预处理指令。

  • #define MAX_VALUE 0.3这句预处理指令会把源代码中的MAX_VALUE全部替换为0.3,不过这样定义出来的常量没有类型信息。

  • 在头文件里声明预处理指令并不是明智的做法,当常量名称可能冲突的时候更是如此,因为所有引入了这份头文件的其他文件中都会出现这个名字。

4.2 常量

  • 定义常量的的好处是清楚地描述了常量的含义:static const NSTimeInterval kAnimationDuration = 0.3
  • 常量名称的命名法:若常量局限于某“编译单元”(也就是实现文件)中,则在前面加字母k;若常量在类之外可见,可通常以类名为前缀。
  • 刚提到在头文件中声明预处理指令不是明智的做法,实际上,使用static const定义的常量也不应该出现在头文件里,因为Objective-C没有“命名空间”这一概念,如果非要在头文件中增加,最好添加类名前缀,表明其所属的类。
  • 如果不打算公开某个常量,则应将其定义在使用该常量的实现文件里。
  • 变量一定要同时用static与const来声明,const保证变量的值不被修改,而static修饰符则意味着该变量仅在定义此变量的编译单元可见。若声明的时候不加static,则编译器会为它创建一个“外部符号”,此时若是另一个编译单元也有同名变量,那么编译器就会抛出错误信息。
  • 实际上,如果一个变量被声明为static const,那么编译器根本不会创建符号,而是会向#define预处理指令一样,把所有遇到的变量都替换为常值——不过它仍然带有类型信息。

注:

编译器每收到一个编译单元,就会输出一份“目标文件”。在Objective-C语境下,“编译单元”一词通常是指每个类的实现文件(以.m为后缀名)。

4.3 全局符号表中的常量

  • 当使用变量的人无需知道某一变量的实际值,只需以符号的形式使用该变量时,可以将常量放在“全局符号表”中,以便可以在定义该常量的编译单元以外使用,可以这样定义:
//.h 文件
extern NSString *const EOCStringConstant;

// .m 文件
NSString *const EOCStringConstant = @"VALUE";

​ 注意const修饰符在常量类型中的位置。这个定义应该从右往左解读,所以在本例中,EOCStringConstant就是“一个常量,而这个常量是指针,指向NSString对象”,这个常量指针会一直指向@"VALUE"的这个对象。

  • extern关键字是告诉编译器,在全局符号表中将会有一个名为EOCStringConstant的符号,编译器无需查看其定义,就可以允许代码使用此常量。当链接成二进制文件之后,肯定能在全局符号表中找到这个常量。
  • 此类常量只能定义一次,通常将其定义在与声明该常量的头文件相关的实现文件里。由实现文件生成目标文件时,编译器会在“数据段”为字符串分配存储空间。链接器会把此目标文件与其他目标文件相链接,以生成最终的二进制文件。
  • 这种常量的名字同样是最好用与之相关的类名做前缀。

第5条 用枚举表示状态、选项、状态码

5.1 枚举的定义

enum EOCConnectionSate {
    EOCConnectionStateDisconnected,
    EOCConnectionStateConnecting,
    EOCConnectionStateConnected,
}

声明一个枚举变量的方式如下:

enum EOCConnectionState state = EOCConnectionStateDisconnected;

需要写enum关键字,有些麻烦,可以增加以下语句来省略该关键字:

typedef enum EOCConnectionState EOCConnectionState;

5.2 枚举的向前声明

C++11标准修订了枚举的某些特性,其中一项改动是:可以指明用何种“底层数据类型”来保存枚举类型的变量。这样可以让编译器知道数据的大小,可以向前声明枚举变量。

enum EOCConnectionState : NSInteger { /* ...*/ };

然后可以这样向前声明枚举类型:

enum EOCConnectionState : NSInterger;

5.3 手工指定枚举变量的值

enum EOCConnectionState {
  EOCConnectionStateDisconnected = 1,
  EOCConnectionStateConnecting,
  EOCConnectionStateConnected,
}

接下来的几个枚举值都会在上一个的基础上递增1.

5.4 定义可组合枚举选项

enum UIViewAutoresizing {
  UIViewAutoresizingNone                 = 0;
  UIViewAutoresizingFlexibleLeftMargin   = 1 << 0,
  UIViewwAutoresizingFlexibleWidth       = 1 << 1,
  UIViewwAutoresizingFlexibleRightMargin = 1 << 2,
  /*...*/
}

使用时,将选项使用“按位或 | ”运算,即可将选项组合起来;使用“按位与 & ”运算,即可以判断出是否已启用某选项。

5.5 据平台支持标准来定义枚举类型

Foundation框架中定义了一些辅助的宏,用这些宏来定义枚举类型时,也可以指定用于保存枚举值的底层数据类型。这些宏具备向后兼容的能力,如果目标平台编译器支持新标准,那就使用新式语法,否则使用旧式语法。

使用方法如下:

/* 定义普通枚举类型 */
typedef NS_ENUM(NSUInteger, EOCConnectionState) {
    EOCConnectionStateDisconnected,
  EOCConnectionStateConnecting,
  EOCConnectionStateConnected,
};
/* 定义可组合选项枚举类型 */
typedef NS_OPTIONS(NSUInteger, EOCViewAutoresizing) {
    EOCViewAutoresizingNone                 = 0;
  EOCViewAutoresizingFlexibleLeftMargin   = 1 << 0,
  EOCViewwAutoresizingFlexibleWidth       = 1 << 1,
  EOCViewwAutoresizingFlexibleRightMargin = 1 << 2,
};

5.6 枚举与switch

使用枚举来定义状态机时,不建议添加default分支。因为如果枚举的状态增加时,编译器会警告新加入的状态在switch中未被处理,而default分支使得编译器不发出警告信息。