这是Effective系列丛书中的一本,讲述了在iOS开发中对于代码编写的一般规范和作者对于高效编写Objective-C代码的见解,总共52条建议分成了7个章节,这里分章节记录下来。
第一章讲了Objective-C中的一些核心概念,总共有五条。
第1条 了解Objective-C语言的起源
1.1 Objective的运行时特性
众所周知,Objective-C是一种面向对象的语言,与其他面向对象语言有点不一样的是,OC中使用了较多的方括号,而且方法名一般都比较长。对于其他语言的开发者来说会显得有点冗长,但是这样的代码十分易读,从方法名就很明显看出方法的实际意义。
在语法上,OC使用的是一门消息型语言,由Smalltalk演化而来,而像java、C++等是“函数调用”型语言。
// Message(Objective-C)
Object *obj = [Object new];
[obj performWith:parameter1 and:parameter2];
// Function calling(C++)
Object *obj = new Object;
obj->perform(parameter1, parameter2);
两种类型的语言主要区别:
- 消息型结构语言,其运行时要执行的代码由运行环境决定;
- 函数型语言,要执行的代码由编译器决定。
所以针对OC是一门消息型语言,很多工作都可以在运行时来完成,从而通过运行时组件(runtime component)提供了很多运行时操作的方法。
1.2 Objective-C的内存管理
在Objective-C中有一点需要明白,就是OC中语言中的指针是用来指示对象的。如定义一个字符串:
NSString *string = @"Is a string";
它声明了一个名为string的变量,类型是NSString,也就是这个变量是指向NSString的一个指针。对象分配的内存都在堆(heap space)里面。
如果再创建一个变量,指向同一地址,那么这两个变量会同时指向同一对象:
NSString *string = @"Is a string";
NSString *string1 = string;
这两行代码的意义是,在栈区(stack frame)分配了两块内存,每块内存的大小都能容下一枚指针(32位架构4字节,64位架构为8字节),存放了一个相同的堆区的内存地址。
分配在堆里面的内存必须直接管理,而分配在栈上的内存则会在栈帧弹出时自行清理掉。在Objective-C中,内存管理是在运行环境下抽象成了一套内存管理架构,叫“引用计数”机制。
另外在OC中有时定义不含*的变量,这些变量也可能会使用栈的内存空间。通常像常量类型定义时都是分配的栈区内存,如CGFrame、CGRect等。
第2条 在类的头文件中尽量少引入其他头文件
2.1 缩短编译时间
像C语言一样,Objective-C中创建一个类会有两个文件,一个是头文件(.h),一个是实现文件(.m)。来创建一个JCPerson类看看:
其中的代码格式大致是这样的:
.h文件中
#import <Foundation/Foundation.h>
@interface JCPerson : NSObject
@end
.m文件中
#import "JCPerson.h"
@implementation JCPerson
@end
可以看出头文件中使用#import引入了一个Foundation框架,这个框架几乎是所有类都需要引入。另外还有一个很普遍的框架就是UIKit框架,几乎所有与UI有关的操作都需要引入这个框架。所以,一般情况,在一个类中需要用到另一个类时,我们会引入头文件。但是在“用到”这个点上,可以有两层理解:
- 需要知道另一个类的所有细节,包括其.m文件中的各种实现和定义的属性等。
- 只需知道有这个类名就可以,而不需要细节。
比如在上面的JCPerson类的.h文件中定义了一个employer属性:
@interface JCPerson : NSObject
@property (nonatomic, copy) JCEmployer *employer;
@end
这时如果进行编译就会报错找不到Employer这个类,最开始我们想到的肯定是#import这个类的头文件。但是在JCPerson的头文件中我们并不需要知道JCEmployer类的所有实现,只需要知道有这个名字就可以了,这时可以使用向前声明。
@class JCEmployer
@interface JCPerson : NSObject
@property (nonatomic, copy) JCEmployer *employer;
@end
所以这里的规则是,引入头文件的时机尽量延后,只有在确实需要时才引用,这样可以减少类的使用者所需的头文件数量,同时缩短编译时间。
2.2 避免类的循环引用
我们经常听过Block的循环引用导致内存泄漏,不恰当的头文件引用也会出现循环引用,导致程序报错。
还是上面的例子,这个时候在JCEmployer类.h文件中添加一个方法:
- (void)addEmployer:(JCPerson *)person;
如果需要正常编译,此时编译器需要知道JCPerson这个类,而要编译JCPerson,又需要JCEmployer类。假如我们在这个两个类的头文件中都各自引入对方,就造成了循环引用。另外我们也可能知道,在我们使用#import的时候,不像C语言的#include,此时并不会发生死循环,但是问题是这两个类最终会有一个无法被正确编译。
当然在有些情况下,比如需要实现属性、实例变量或者遵循协议等必须引入头文件时,就要考虑使用类扩展。我们无时无刻都在使用类扩展,只是没有觉察罢了。
@interface JCPerson()
@end
@implementation JCPerson
@end
这个是定义在.m文件中的,可以添加属性,方法实现都定义在主实现文件中。
第3条 多用字面量语法,少用与之等价的方法
字面量语法听上去有点陌生,但是实际很多时候都在用,先说一下使用字面量的两个好处:
- 使用字面量语法可以缩减代码长度,易读性更好。
- 使用字面量语法更安全,可以规避很多由于nil造成的问题。
那么下面来分变量类型逐个说明:
3.1 字符串
一段定义字符串的方法:
// 字面量语法
NSString *string1 = @"string1";
// 等价方法
NSString *string2 = [[NSString alloc] initWithFormat:@"%@", string1];
可以看出第一直接定义了一个字符串string1,第二个则是通过封装的方法来定义一个字符串变量。第一种方法就是所谓的“字符串字面量(literal syntax)”。这种语法还适合于声明NSNumber、NSArray和NSDictionary类。
3.2 字面数值
有时需要把整数、浮点数、布尔值封入OC对象中,数据库的操作里面可能比较常见,在这中情况下就可以使用NSNumber类。创建一个NSNumber对象:
NSNumber *number = [NSNumber numberWithInt:1];
这行代码创建了一个number对象,它的值是1,那么如果使用字面语法就是下面这样,看上去简单很多:
NSNumber *number = @1;
其他数值类型的也可以通过字面量语法来实现
NSNumber *floatNumber = @1.25;
NSNumber *doubleNumber = @3.1415926;
NSNumber *boolNumber = @YES;
NSNumber *charNumber = @'a';
3.3 字面量数组
创建数组的一般方法:
NSArray *animalArray = [NSArray arrayWithObjects:@"wolf", @"sheep", @"dog", @"cat", nil];
但是使用字面量语法呢:
NSArray *animalArray = @[@"wolf", @"sheep", @"dog", @"cat"];
另外在数组的操作中,如果我们需要取数组中的值可以通过如下方式:
NSString *dog = [animalArray objectAtIndex:2];
也可以通过
NSString *dog = animalArray[2];
而第二种是通过字面量语法获取,要简单了很多,而且当数组中的元素为nil时,字面量语法会直接抛出异常,而第一种方法就不会。所以字面量语法可以避免很多由于nil所造成的问题,是一种“语法糖”
语法糖,是指计算机语言中与另外一套语法等效但是开发者用起来却更方便的语法。语法糖可令程序更易读,减少代码出错几率。
3.4 字面量字典
字典是一种映射型数据结构,一般的创建方式如下:
NSDictionary *person = [NSDictionary dictionaryWithObjectsAndKeys:@"Matt", @"firstName", @"Damon", @"secondName", [NSNumber numberWithInt:28], @"age", nil];
这种方式貌似用得比较少,因为看上去很繁琐,而且是按照值-键的方式一个接一个定义的。但是用字面量语法看上去就轻松很多:
NSDictionary *person = @{@"firstName":@"Matt", @"secondName":@"Damon", @"age":[NSNumber numberWithInt:28]};
与数组一样,字典的字面量语法同样可以避免值为空导致错误不易发现的问题。
字面量创建的对象必须属于Foundation框架,如果自定义了这些类的子类,则无法使用字面量语法来创建这些对象。
第4条 多用类型常量,少用#define预处理指令
在开发中,经常会使用到常量,最开始的时候很多常量可能会直接在每一行代码中分别传递,到后来有人会建议用宏来定义一个经常要使用的常量,这样便于全局修改。比如定义一个UI动画的时间,大多数情况会像下面这样定义一个宏:
#define ANIMATION_DURATION 0.3
这样定义的常量ANIMATION_DURATION就可以用来取代0.3这个值了,但是在上面这行用#define定义的预处理指令看不出这个常量的类型信息,所以可以使用另一种更好的方式来定义一个常量。
static const NSTimeInterval kAnimationDuration = 0.3;
这里告诉了我们这个常量的类型是NSTimeInterval类型,注意这种方式最后要跟一个分号。
用类型常量时要注意以下几点:
- 注意命名方法,如果这个常量定义在实现文件里面,在起始字母加一个k;如果常量对其他类可见,一般是指定义在.h文件中时,通常这个常量的名字以类名为前缀。
- 注意常量定义的位置,一般不要把常量定义在头文件里面,容易引起名字的冲突。
- 类型常量一定要同时使用static和const来声明,这样如果在代码中尝试去修改这个常量时编译器就会报错。另外如果不使用static时,编译器会为这个常量创建一个“外部符号”,这时如果在另外一个类中也定义了同样名字的常量时,编译器就会抛出异常。
但是有的时候我们会需要公开一个常量,就需要通过extern关键字对其向外公开。用得最多的是定义通知名称的时候,定义方式一般是这样的:
// .h头文件中
extern NSString *const JCStringConstant;
// .m实现文件中
NSString *const JCStringConstant = @"JCStringConstant";
可以理解为这个常量在头文件中进行声明,在实现文件中进行了定义。
第5条 用枚举表示状态、选项、状态码
5.1 枚举基本定义
Objective-C继承了C语言的枚举类型,关键字也是用enum,在开发中用得比较频繁。
枚举也可以理解为是一组常量的集合,一个基础的🌰,用一个枚举类型来表示socket中的连接状态。
enum JCSocketConnnetState {
JCSocketConnectStateDisconnected,
JCSocketConnectStateConnecting,
JCSocketConnectStateConnected,
};
这个枚举中我们没有给每一种状态定义值,编译器会自动分配一个独有的编号给每一种状态,起始值是0,往后的每个枚举递增+1。
上述定义的枚举类型,如果需要使用的话有点麻烦:
enum JCSocketConnectState state = JCSocketConnectStateConnecting;
每次都得带上一个enum,在这样的情况下,在定义枚举时一般是与typedef一起使用的,相当于用typedef起了个别名。
typedef enum JCSocketConnnetState {
JCSocketConnectStateDisconnected,
JCSocketConnectStateConnecting,
JCSocketConnectStateConnected,
} JCSocketConnnetState;
在需要使用到这个枚举的地方,就可以像平时使用其他变量定义一样,直接把JCSocketConnectState当做一个变量类型来用了。
JCSocketConnectState state = JCSocketConnectStateConnected;
5.2 枚举的数据类型
在上面定义的枚举中,我们可能会有个疑问,这样定义出来的枚举是什么数据类型呢?
在C++11之前,枚举类型所用的数据类型取决于编译器,通过最大编号来确定。比如上面那个枚举只有三个值,最大编号是2,一个字节就够了,所以使用一个字节类型就可以了,对应的就是char类型。
在C++11以后,可以自定义一个底层数据类型给枚举,直接告诉编译器这个数据类型需要多少存储空间。定义方法如下:
enum JCSocketConnectState : NSInteger{};
这里把这个枚举类型定义成了NSInteger类型。
上面讲到了,如果定义枚举时没有给序号,编译器会从0开始分配,逐个递增+1,同样我们也可以自定义序号。
typedef enum JCSocketConnnetState {
JCSocketConnectStateDisconnected = 1,
JCSocketConnectStateConnecting,
JCSocketConnectStateConnected,
} JCSocketConnnetState;
那么接下来的几个枚举值则是从1开始逐个递增+1。
5.3 用枚举定义选项
还有一种类型的枚举,其中定义的枚举值是可以互相组合的,这个时候可以定义一个枚举,使用时通过按位“或”操作符来定义变量。
比如UI框架中的一个枚举类型:
typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) {
UIViewAutoresizingNone = 0,
UIViewAutoresizingFlexibleLeftMargin = 1 << 0,
UIViewAutoresizingFlexibleWidth = 1 << 1,
UIViewAutoresizingFlexibleRightMargin = 1 << 2,
UIViewAutoresizingFlexibleTopMargin = 1 << 3,
UIViewAutoresizingFlexibleHeight = 1 << 4,
UIViewAutoresizingFlexibleBottomMargin = 1 << 5
};
这个枚举类型中定义的每个选项都可同时开启或禁用,他们分别只通过一个二进制位的值表示,就可以通过按位“或”操作来把他们进行组合操作,下面这张图表示了这个枚举定义的成员中的二进制表示的值,其中最后一个是表示UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight的组合表示:
另外我们可以看到上面的枚举定义中使用了NS_OPTIONS这个东东,它是Foundation框架中定义的辅助宏,这些宏的主要作用是保证当前宏向后兼容,即满足编译器版本向后兼容。同样与一开始的普通枚举类型对应也定义了一个辅助宏NS_ENUM来实现编译器向后兼容。所以,我们在OC中看到系统定义的枚举一般就是类似这样子:
typedef NS_ENUM(NSInteger, UIViewAnimationTransition) {
UIViewAnimationTransitionNone,
UIViewAnimationTransitionFlipFromLeft,
UIViewAnimationTransitionFlipFromRight,
UIViewAnimationTransitionCurlUp,
UIViewAnimationTransitionCurlDown,
};
最后还讲到了枚举类型在switch语句中使用时,最好不要加default分支,这样在后续枚举值有新增时,编译器就会直接发出警告,便于排错。