Effective Objective-C读书笔记(1)

熟悉 Objective-C

Objective-C 中的消息

Objective-C 起源于 Smalltalk,所以借鉴了 Smalltalk 的消息结构(messaging structure),而非像 C++ 那样的函数调用(function calling)
主要区别:使用消息结构的语言,其运行时所应执行的代码由运行环境来决定。而使用函数调用的语言,由编译器决定。如果示例代码是多态的,那么在运行时就会按照虚方法表(virtual table)来查看到底该执行哪个函数实现。而采用消息结构的语言,不论是否是多态,总是在运行时才会查找要执行的方法。编译器不关心接收消息的对象是哪种类型。接收消息的对象问题也要在运行时处理。这个过程叫做动态绑定

Objective-C 的工作都是由 运行期组件(runtime component)而非编译器来完成,运行期组件本质就是一种与开发者所编写代码相连接的动态库,这样只需更新运行期组件,就可以提升应用程序性能。而那种许多工作都在编译期完成的语言,想要获得类似的性能提升,则要重新编译应用程序代码。Objective-C 是 C 的“超集”,必须要理解 C 语言的内存模型。举个例子:

1
2
NSString *someString = @"some string";
NSString *anotherString = someString;

对象是存储在堆内存当中,anotherString 指向了 string 变量,它们两个共享一块存储区域,并不会拷贝对象。其内存空间分配是这样的:

需要注意的是:分配在堆中的内存必须直接管理,而分配在栈上用于保存变量的内存则会在栈上弹出时自动清理。Objective-C 将内存管理抽象出来了,不用像 C 那样使用 mallocfree 来回收内存,Objective-C 在运行时把这部分工作抽象为一套内存管理的框架,叫做引用计数

在 Objective-C 程序当中,遇到不含*的变量,它们可能使用栈空间。比如CGRect等等。

类的头文件中尽量少引入其他头文件

更优雅的做法是使用 @class **** 来引入某个类,这叫做向前声明(forward declaring)该类。在.m文件中import该类。这样就隐藏了该类的所有接口实现。将引入头文件的时机尽量延后,只有在需要时才引入,这样就会减少类的使用者所需引入的头文件数量。减少编译时间。还可以避免两个互相import导致循环引用的问题。

如果某些类需要遵循某个协议,那么该协议必须要有完整的定义。且不能使用向前声明,这时候可以将该协议单独放在一个头文件当中。特例是委托协议就不用单独写一个头文件,协议只有与接受委托的类放在一起才有意义。这种情况下,最好能在实现文件中声明这个类实现了该委托协议,并把这段实现代码放在分类当中。这样只要在实现文件中引入包含委托协议的头文件即可,而不需将其放在公共头文件里。好处是降低依赖程度,缩短编译时间,代码清晰容易维护。

总结一下:优先使用向前声明来进行解耦,如果无法使用向前声明,尽量把该类遵循某个协议移至该类的分类当中。实在不行的话,就把协议单独放在一个头文件,然后将其引入。

多使用字面量,少用与之等价的方法

在使用字面量的时候,多使用字面数值,代码更加整洁:

1
2
3
4
5
6
7
8
9
10
// 不使用字面量
NSNumber *someNumber = [NSNumber numberWithInt:1];

// 使用字面量
NSNumber *intNumber = @1;
NSNumber *boolNumber = @YES;

int x = 5;
int y = 6.32f;
NSNumber *number = @(x * y);

字面量数组:使用字面量数组更加直观,易于理解,并能尽可能早发现程序设计中的问题。

1
2
3
4
5
6
id obj1 = /* ... */;
id obj2 = /* ... */;
id obj3 = /* ... */;

NSArray *arrayA = [NSArray arrayWithObjects:obj1, obj2, obj3, nil];
NSArray *arrayB = @[obj1, obj2, obj3];

如果 obj2 为 nil,字面量语法创建的数组 arrayB 会崩溃,而 arrayA 虽然能创建出对象,但是只包含 obj1,因为arrayWithObjects:会依次处理各个参数,直到发现nil为止。使用字面量语法更加安全,更快的发现错误。

字典字面量:与上面的数组类似,字典在遇到值为nil时抛出异常

1
2
3
4
5
6
7
8
NSDictionary *dict1 = [NSDictionary dictionaryWithObjectsAndKeys:@"value1", @"key1", @"value2", @"key2", nil];
NSDictionary *dict2 = @{
@"key1" : @"value1",
@"key2" : @"value2",
};

[dict1 objectForKey:@"value1"];
NSString *key1 = dict2[@"value1"];

注意上面的dictionaryWithObjectsAndKeys:的参数是 <Object> : <key> 的形式。很显然使用字面量更加简洁。

字面量语法也有局限性:就是除了字符串意外,所创建出来的对象必须属于 Foundation 框架才行。如果自定义了这些类的子类,则无法使用字面量语法创建对象。当然很少有人这么做,因为 NSArray、NSDictionary 都是已定型的 “子族”,无需再改动。

使用字面量创建的对象都是不可变的,虽然多创建一个对象,但好处还是多于缺点的:

1
NSMutableArray *mutable = [@[@1, @2] mutableCopy];

多使用类型常量,少用 #define 预处理指令

不要使用#define预处理指定定义常量,这样定义出来的类型不含类型信息。编译器只是会在编译器根据此执行查找与替换操作。即使有人重新定义了常量值,编译器也不警告。可以利用编译器特性来定义。比如:

1
2
3
@implementation ViewController
static const NSTimeInterval kAnimationDuration = 0.3;
@end

在实现文件中使用static const来定义”只在编译单元内可见的常量”(每个.m为一个编译单元),此类常量不在全局符号表当中,所以无需加前缀。

在头文件中使用extern来声明全局变量,这种常量出现在全局符号表当中。所以其名称应该加以区分,通常用类名作为前缀:

1
2
3
extern NSString *const EOCLoginManagerDidLoginNotification;

NSString *const EOCLoginManagerDidLoginNotification = @"EOCLoginManagerDidLoginNotification";

用枚举表示状态、选项、状态码

使用 enum 的时候需要注意,应该用枚举来表示状态机的状态、传递给方法的选项以及状态码等值,给这个值起个通俗易懂的名字。

如果把传递给某个方法的选项表示为枚举类型,而多个选项可同时使用,那么将各个选项定义为2的幂,以便通过按位或操作将其组合起来。
在使用 NS_ENUMNS_OPTHONS 宏来定义枚举类型,要指名其数据类型,这样做可以确保枚举是用开发者所选的数据类型实现出来的,而不会采用编译器所选的类型。NS_ENUMNS_OPTHONS 其实内部做了这样一件事情:

其实就是判断编译器是否支持新式枚举,编译器按 C++ 模式编译,那么enum定义枚举的时候,其展开方式与NS_ENUM相同。枚举值使用按位或运算来组合的时候,C++ 认为运算结果的数据类型应该是枚举的底层类型,也就是NSUInteger,C++ 不允许将这个底层类型进行隐式转换,其展开方式为NS_OPTHONS。鉴于此,凡是需要按位或操作来组合的枚举都应该使用NS_OPTHONS来定义。若是枚举不需要互相组合,则应使用NS_ENUM来定义。通常我们会这样使用:

在处理switch语句中,不要实现default 分支。在枚举值改变后,编译器就会提醒开发者:switch语句并未处理所有枚举。