注:该读书笔记是为参加牛客网举办的“有书共读”活动所写,按照约定会在牛客网发布。
第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分支使得编译器不发出警告信息。