Effective Objective-C 2.0

Objective-C 语言的起源

Objective-C 使用“消息结构”(messaging structure)而非“函数调用”(function calling)。Objective-C 语言由 Smalltalk 演化而来,消息与函数调用之间的关键区别是:

  • 使用消息结构的语言,其运行时所执行的代码由运行环境决定,不论是否多态,总是在运行才会去查找所需要执行的方法。
  • 使用函数调用的语言,是由编译器决定的,如果代码中调用的函数是多态的,那么在运行时就要按照“虚方法表”(virtual table)来查找应该执行哪个函数实现。

头文件尽量少引用其他头文件

  • @class xxx ->“向前声明”(forward declaring
  • 除非必须,否则不要引用头文件,一般来说,应在某个类的头文件中使用向前声明来提及别的类,并在实现文件中引用那些类的头文件。这样做可以尽量降低类之间的耦合(coupling)。
  • 有时无法使用向前声明,比如要声明某个类遵循一项协议,这时应尽量把“该类遵循某个协议”的这条声明移至 class-continuation 分类中“。如果不行,就把协议单独放在一个头文件中,然后引用。

多用字面量语法

NSNumber *someNumber = @1;

NSArray *animals = @[@"cat", @"dog"];

NSString *dog =  animals[1];

NSMutableArray *mutaleArray = [@[@"cat", @"dog"] mutableCopy];
//replaceObjectAtIndex
mutaleArray[1] = @"dog";

NSDictionary *personData = @{@"firstName": @"Matt", @"lastName", @"Galloway", @"age": @22};

NSString *lastName = personData[@"lastName"];
  • 应该使用字面量语法来创建字符串,数值,数组,字典。和常规方法相比较更加简明扼要。
  • 应该通过取下标操作来访问数组下标或字典中的键所对应的元素。
  • 用字面语法创建数组或者字典时,如果值中有nil,就会抛出异常。

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

  • 不要用预处理指令定义常量,这样定义出来的常量不含类型信息,编译器只会在编译前据此执行查找与替换操作。即使有人重新定义了常量值,编译器也不会产生警告信息,这将导致应用程序中的常量值不一致。
  • 在实现文件中使用 static const 来定义”只在编译单元内可见的常量“(translation-unit-specific constant)。由于此类常量不在全局符号表中,所以无需为其名称加前缀。
  • 在头文件中使用 extern 来声明全局变量,并在相关实现文件中定义其值。这种常量需要出现在全局符号表中,所以其名称应加以区隔,通常用与之相关的类名做前缀。

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

枚举只是一种常量命名方式。某个对象所经历的各种状态就可以定义为一个简单的枚举集(enumeration set)。

  • 应该用枚举来表示状态机的状态,传递给方法的选项以及状态码的等值,给这些值起个易懂的名字。
  • 如果把传递给某个方法的选项表示为枚举类型,而多个选项又可同时使用,那么就将各选项值定义为 2 的幂,以便通过按位或操作将其组合起来。
  • NS_ENUMNS_OPTIONS 宏来定义枚举类型,并指明其底层数据类型。这样做可以确保枚举是用开发者所选的底层数据类型
    实现出来的,而不会采用编译器所选的类型。
  • 在处理枚举类型的 switch 语句中不要实现 default 分支。这样的话,加入新的枚举之后,编译器就会提示开发者 switch 语句并未处理所有枚举。

属性的理解

“属性”(property)是 Objective-C 的一项特性,用于封装对象中的数据。
@dynamic 关键字:不会自动创建实现属相所用的实例变量,也不会为其创建存取方法。

@property (nonatomic, readwrite, copy) NSString *name;

属性特质可以分为四类:

原子性

默认情况下,由编译器所合成的方法会通过锁定机制确保其原子性(atomicity)。如果属性具备 nonatomic 特质,则不使用同步锁。

iOS 程序中,基本所有属性都声明为 nonatomic,这样做的原因是:在 iOS 中使用同步锁的开销较大,会带来性能问题。而 atomic 并不能保证线程安全(thread safety),若要实现“线程安全”的操作,还需采用更为深层的锁定机制才行。例如,一个线程在连续多次读取某属性值的过程中有别的线程在同时改写该值,那么即便将属性声明为 atomic,也还是会读到不同的属性值。

读/写权限

  • 具备 readwrite (读写)特质的属性拥 gettersetter 方法。若该属性由 @synthesize 实现,则编译器会自动生成这两个方法
  • 具备 readonly (只读)特质的属性仅拥有获取方法,只有当该属性由 @synthesize 实现时,编译器才会为其实现 getter 方法。

内存管理语意

  • assign:针对“纯量操作”(scalar type)的简单赋值操作。
  • strong:为该属性定义了一种“拥有关系”(owning relationship)。为这种属性设置新值时,设置方法会先保留新值,并释放旧值,然后再将新值设置上去。
  • weak:为该属性定义了一种“非拥有关系”(nonowning relationship)。为这种属性设置新值时,设置方法既不保留新值,也不释放旧值。同assign类似,然而在属相所指的对象遭到摧毁时,属性值也会被清空(nil out)。
  • unsafe_unretained:语意和 assign 相同,但是它适用于“对象类型”(object type),该特质表达一种“非拥有关系”(“不保留”,unretained),当目标对象遭到摧毁时,属性值不会自动清空(“不安全”,unsafe)。
  • copy:表达的所属关系和 strong 类似。然而设置方法并不保留新值,而是将其“拷贝”(copy)。当属性类型为 NSString * 时,经常用此特质来保护其封装性,防止 NSMutableString 赋值给 NSString 时,前者修改引起后者值变化而用的。

方法名

  • getter = <name>:指定“获取方法”的方法名。
  • setter = <name>:指定“设置方法”的方法名。

在对象内部尽量直接访问实例变量

直接访问实例变量,不经过 Objective-C 的“方法派发”(method dispatch)步骤,编译器生成的代码会直接访问保存实例变量的内存。

直接访问实例变量,不会触发“键值观测”(Key-Value Observing,KVO)通知。

直接访问实例变量,不会调用“设置方法”,会绕过相关属性所定义的“内存管理语义”。

  • 在对象内部读取数据的时,应该直接通过实例变量来读,而写入数据时,则应通过属性来写。
  • 在初始化方法及 dealloc 方法中,总是应该直接通过实例变量来读写数据。
  • 有时会使用懒加载初始化配置某份数据,需要通过属性来读取数据。

类簇模式隐藏实现细节

”类簇“(class cluster)是一种很有用的模式,可以隐藏”抽象基类“(abstract base class)背后的实现细节。

  • 类簇模式可以把实现细节隐藏在一套简单的公共接口后面。
  • 从类簇的公共抽象基类中继承子类时要当心。

使用关联对象存放自定义数据

”关联对象“(Associated Object)可以给某对象关联许多其他对象,这些对象通过”键“来区分,存储对象值得时候,可以指明”存储策略“(storage policy),用来维护相应的”内存管理语义“。存储策略由名为 objc_AssociationPolicy 的枚举所定义。

关联对象 等效的 @property 属性
OBJC_ASSOCIATION_ASSIGN assign
OBJC_ASSOCIATION_RETAIN_NONATOMIC nonatomic,retain
OBJC_ASSOCIATION_COPY_NONATOMIC nonatomic,copy
OBJC_ASSOCIATION_RETAIN retain
OBJC_ASSOCIATION_COPY copy

下面的方法可以管理关联对象:

  • void objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key, id _Nullable value, objc_AssociationPolicy policy) 此方法已给定的键和策略为某对象设置关联对象值。
  • id _Nullable objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key) 此方法根据给定的键从某对象中获取相应的关联对象值。
  • void objc_removeAssociatedObjects(id _Nonnull object) 此方法移除指定对象的全部关联对象
  • 可以通过”关联对象“机制来把两个对象连起来。
  • 定义关联对象时可指定内存管理语义,用来模仿定义属性时所采用的”拥有关系“和”非拥有关系“。
  • 只有在其他做法不可行时才应选用关联对象,因为这种做法通常会引入难以查找的bug。
static void *key = @"key";
objc_setAssociatedObject(a, key, b, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_getAssociatedObject(a, key);
objc_removeAssociatedObjects(a);

objc_msgSend 的作用

Objective-C 中的调用方法叫做”传递消息“(pass a meaage)。消息有 nameselector,可以接受参数,而且可能还有返回值。
在 Objective-C 中,如果向某对象传递消息,那就会使用动态绑定机制来决定需要调用的方法。在底层,所有方法都是普通的 C 语言函数,然而对象收到消息之后,会调用哪个方法完全由运行期决定,甚至可以在程序运行时改变,这些特性使得 Objective-C 成为一门真正的动态语言。
objc_msgSend 的原型为: void objc_msgSend(id self, SEL cmd, ...) 这是个”参数个数可变的函数“(variadic function),能接受两个或两个以上的参数。第一个参数代表接收者,第二个参数代表'selector'(SEL是'selector'的类型),后续参数就是消息中的那些参数,其顺序不变。

objc_msgSend 函数会根据接收者与 'selector' 的类型来调用适当的方法,该方法会在接收者所属的类中搜寻其”方法列表(list of methods),如果能找到与 'selector' 名称相符的方法,就跳至其实现代码。若是找不到,那就沿着继承体系继续向上查找,等找到合适的方法之后再跳转,如果最终还是找不到相符合的方法,就会执行”消息转发“(message forwarding)。

objc_msgSend 会将匹配结果缓存在”快速映射表“(fast map)里面,每个类都有这样一块缓存。

Objective-C 运行环境中的另外一些函数:

  • objc_msgSend _stret:如果待发送的消息要返回结构体,那么可交由此函数处理。只有当 CPU 的寄存器能够容纳得下消息返回类型时,这个函数才能处理此消息。若是返回值无法容纳于 CPU 寄存器中,那么就由另一个函数执行派发。此时,那个函数会通过分配在栈上的某个变量来处理消息所返回的结构体。(注:另一个函数是啥?)
  • objc_msgSend _fpret:如果消息返回的是浮点数,那么可交由此函数处理。在某些架构的 CPU 中(x86 等架构)调用函数时,需要对”浮点数寄存器“(floating-point register)做特殊处理。
  • objc_msgSendSuper:如果要给父类发送消息,那么就交由此函数处理。

消息转发机制

由于在运行期间可以继续向类中添加方法,所以在编译期间向类发送了其无法解读的消息并不会报错,这就导致编译器在编译时还无法确定类中到底会不会有某个方法的实现。当对象接收到无法解读的消息后,就会启动”消息转发“(message forwarding)机制。

消息转发分为两大阶段:

  1. 第一阶段先征询接收者所属的类,看其是否能动态添加方法来处理这个未知事件,这就是”动态方法解析“(dynamic method resolution)。

  2. 第二阶段涉及”完整的消息转发机制“(full forwarding mechanism)。如果运行时第一阶段执行完了,那么接收者就无法再以动态新增方法来响应消息了。这时系统会请求接收者以其他的方法来处理消息,这里细分两小步:首先,接收者看看有没有其他对象能够处理这条消息。如果有,消息转发给那个对象,消息转发过程结束。如果没有”备援的接收者“(replacement receiver),就启动完整的消息转发机制,系统会把和消息有关的内容封装到 NSInvocation 对象中,给接收者最后一次机会让其解决当前还未处理的消息。

动态方法解析

对象无法解析收到的消息后,首先将调用其所属类的下列类方法:

+ (BOOL)resolveInstanceMethod:(SEL)sel

该方法返回 Boolean 类型,表示这个类能否新增一个实例方法来处理该 selector,在继续执行消息转发机制之前,本类有机会新增一个处理 selector 的方法。假如尚未实现的方法不是实例方法而是类方法,那么就会调用 resolveClassMethod : 方法。

使用这种办法的前提是:相关方法的实现代码已经写好,只是等着运行的时候动态插入类中即可。

id autoDictGetter(id self, SEL _cmd);
void autoDictSetter(id self, SEL _cmd, id value);

+ (BOOL)resolveClassMethod:(SEL)sel {
    NSString *selString = NSStringFromSelector(sel);
    if ([selString hasPrefix:@"set"]) {
        /*
         IMP是”implementation”的缩写,它是objetive-C方法(method)实现代码块的地址,可像C函数一样直接调用。
         */
        return class_addMethod(self, sel, (IMP)autoDictGetter, "v@:@");
    } else if ([selString hasPrefix:@"get"]) {
        return class_addMethod(self, sel, (IMP)autoDictSetter, "v@:@");
    }
    return [super resolveClassMethod:sel];
}

备援接收者

当前接收者第二次机会处理未知的 selector,在这一步,能不能把这条消息转给其他接收者来处理。处理方法如下:

- (id)forwardingTargetForSelector:(SEL)aSelector

若当前接收者能找到备援对象,则将其返回,找不到返回 nil

完整的消息转发

首先创建 NSInvocation 对象,把尚未处理的消息有关的内容封于其中,此对象包括 selector、目标(target 及参数。在 tem 触发 NSInvocation 对象时,”消息派发系统“(message-dispatch sys)将会把消息指派给目标对象。调用方法如下:

- (void)forwardInvocation:(NSInvocation *)anInvocation

这样实现出来的方法和”备援接收者“所实现的方法等效,一般不采用。

实现此方法时,若发现某个操作不应该是本类来处理,就需要调用父类的同名方法。这样,继承体系中的每个类都有机会处理此调用请求,直至 NSObject。如果最后调用了 NSObject 的方法,最终该方法就会继续调用 doesNotRecognizeSelector: 抛出异常,表明 'selector' 未能被处理。

消息转发流程图
消息转发

方法调配(method swizzling)

类的方法列表会把 selector 的名称映射到相关的方法实现之上,使”动态消息派发系统“能够根据此找到应该调用的方法。这些方法都以函数指针的形式来表示,这种指针叫做 IMP,原型如下:

id (*IMP) (id, SEL, ...)

Objective-C 提供了方法来操作 selector IMP 的映射表:

//获取
Method _Nullable class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name)

//交换
void method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2) 

//新增
BOOL class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types) 
IMP _Nullable

//替换
class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types) 

类对象

每个 Objective-C 对象实例都是指向某块内存数据的指针。描述 Objective-C 对象所用的数据结构定义在运行期程序的头文件里,id 类型本身也在定义在这里:

struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};
/// A pointer to an instance of a class.
typedef struct objc_object *id;

Class 类的变量定义了对象所属的类,通常称为 isa 指针。Class 对象定义在运行期程序库的头文件:

ypedef struct objc_class *Class;
struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

此结构体存放类的”元数据“(metadata)。

  • isa 指针:说明 Class 为 Objective-C 对象。
  • super_class:定义了本类的父类。
  • 类对象所属的类型(isa 指针指向的类型)是”原类“(metaclass),用来表示类对象所具备的元数据。”类方法“定义在这里,每个类仅有一个”类对象“,而每个”类对象“仅有一个与之相关的”元类“。

假设有一个 CustomClass 的子类从 NSObject 继承而来,继承体系图如下:

类继承体系

类继承体系中查询类型消息

  • isMemberOfClass 能够判断出对象是否是某个特定类的实例。
  • isKindOfClass 能够判断出对象是否为某类或其派生类的实例。

查询方法使用 isa 指针获取对象所属的类,然后通过 super_class 指针在继承体系中游走。

Objective-C 使用”动态系统系统“(dynamic typing),从 collection 中获取对象时,通常会查询类型信息,这些对象不是”强类型的“(strongly typed),其通常是 id 类型,可以使用类型信息查询方法获取具体类型。

- (NSString *)appendStringFromObjects:(NSArray *)array {
    NSMutableString *string = [[NSMutableString alloc] init];
    for (id object in array) {
        if ([object isKindOfClass:[NSString class]]) {
            [string appendFormat:@"%@,",object];
        } else if ([object isKindOfClass:[NSNumber class]]) {
            [string appendFormat:@"%ld,",[object integerValue]];
        } else if ([object isKindOfClass:[NSData class]]) {
            [string appendFormat:@"%@",[[NSString alloc] initWithData:object encoding:NSUTF8StringEncoding]];
        } else {
            //other type
        }
    }
    return string;
}

也可以用 == 来比较对象是否相同,不采用 isEaual 方法的原因在于,类对象是”单例“(singleton),在应用程序范围内,每个类的 class 仅有一个实例。

应该尽量使用类型信息查询方法,不应该采用 == 比较两个类对象。

类型信息查询方法可以正确处理那些使用了消息传递机制的对象。比如,某个对象可能会把其收到的所有 selector 都转发给另外一个对象(叫做”代理“(proxy),NSProxy 的子类)。在这种情况下,如果在此种代理对象上调用 class 方法,那么返回的是代理对象本身(NSProxy 子类),而非接收代理的对象所属的类。

接口和 API 设计

利用前缀避免命名冲突

  • 选择和你的公司、应用程序或其他有关联的名称作为前缀,并在所有代码中使用这个前缀。
  • 开发程序中用到了第三方库,也应为其中的名称加上前缀。
  • 前缀不要用两个字母,因为这是苹果公司预留的。

提供“全能初始化方法”

  • 在类中提供一个全能初始化方法,并在文档中指明,其他初始化方法均应调用此方法。
  • 若全能初始化方法和父类不同,则需要覆写父类中的对应方法。
  • 如果父类的初始化方法不适用于子类,那么应该覆写这个方法,并抛出异常。

实现description和debugDescription方法

  • 实现description方法返回一个有意义的字符串,用来描述该实例(NSLog打印)。
  • 实现debugDescription方法返回一个有意义的字符串,用来调试(LLDBpo命令)。

尽量使用不可变对象

  • 尽量创建不可变对象。
  • 若某个属性仅可于对象内部修改,则在class-continuation分类中将readonly属性扩展为readwrite属性。
  • 不要把可变的collection作为属性公开,而应该提供相关方法来修改对象中可变collection

命名

  • 命名应该遵从标准的Objective-C命名规范,方法名要言简意赅,不使用缩略名称。
  • 方法名要确保其风格和你自己的代码或使用的框架相符合。

为私有方法添加前缀

  • 给私有方法的名称添加前缀,将其同公共方法区分开。
  • 不要单用一个下划线做私有方法的前缀,因为这是苹果公司预留的。

Objective-C错误类型

”自动引用计数“(ARC)在默认情况下并不是”异常安全的“(exception safe)。如果抛出异常,本应该在作用域末尾释放的对象现在不会自动释放了,如果想生成“异常安全”的代码,就需要设置编译器的标志来实现(-fobjc-arc-exceptions)。

  • 只有发生了使整个应用程序崩溃的错误时,才应使用异常。
  • 在不严重的错误情况下,可以指派“委托方法”(delegate method)来处理错误,也可以把错误信息放在 NSError 对象里输出。

NSCopying 协议

在 Objective-C 中,通过 copy 或者 mutableCopy 来拷贝对象。如果自定义的类需要支持拷贝操作,就需要实现 NSCopying 或者 NSMutableCopying 协议:

- (id)copyWithZone:(nullable NSZone *)zone

- (id)mutableCopyWithZone:(nullable NSZone *)zone;

以前会把内存分为不同的区(zone),而对象会创建在某个区里面。现在每个程序只有一个默认的区(default zone)。

  • 深拷贝(deep copy):在拷贝对象自身时,将其底层数据也一并复制过来。
  • 浅拷贝(shallow copy):只拷贝对象自身,而不复制其中数据。
  • Foundation 框架中所有的 collection 类默认情况下都是浅拷贝。这样做的主要原因在于,容器内的对象未必都能拷贝,而调用者也未必想拷贝其中的每个对象。
    CopyingCollections

协议和分类

通过委托和数据源协议进行对象间通信

  • 委托模式为对象提供了一套接口将相关事件告知其他对象。
  • 将委托对象应该支持的接口定义成协议,在协议中把可能需要处理的事件定义成方法。
  • 当某对象需要从另外一个对象中获取数据时,可以使用委托模式。该模式也被称为“数据源协议”(data source protocal)。
  • 若有必要,可实现含有位段的结构体,将委托对象是否能够响应相关协议方法缓存至其中。

将类的实现代码分散到分类之中

  • 使用分类机制把类的实现代码划分为易于管理的小块。
  • 将应该视为“私有”的方法归入名叫 Private 的分类中,以隐藏实现细节。

为第三方类的分类名称添加前缀

分类机制通常用于向无源码的既有类中添加新功能,分类中的方法是直接添加到类里面的,多次覆盖的结构以最后一个为准。

  • 向第三方类中添加分类时,应给其名称及其中的方法名添加前缀。

不要在分类中声明属性

属性是封装数据的方式,除了 class-continuation 分类,其他分类都无法向类中添加实例变量(关联对象可以解决在分类中无法合成实例变量的问题)。

  • 把封装数据所用的全部属性都定义在主接口中。
  • class-continuation 分类之外的其他分类,可以定义存取方法,但尽量不要定义属性。

使用 class-continuation 分类隐藏实现细节

class-continuation -> 类扩展

通过协议提供匿名对象

  • 协议可在某种程度上提供匿名类型,具体的对象类型可以淡化成遵从某协议的 id 类型,协议里规定了对象所应实现的方法。
  • 使用匿名对象来隐藏类型名称(或类名)。
  • 如果具体类型不重要,重要的是对象能够响应协议里面的方法,可以使用匿名对象来表示。

内存管理

引用计数

NSObject 协议声明了下面三个方法用于操作计数器,以递增或递减其值:

  • retain:递增保留计数。
  • release:递减保留计数。
  • autorelease:待清理“自动释放池”(autorelease pool)时,在递减保留计数。

对象创建出来时,其保留计数至少为1,调用 retain 方法,保留计数+1,调用 releaseautorelease,保留计数 -1,最终当保留计数归零时,对象会被回收,系统会将其占用的内存标记为“可重用”(reuse)。

对象所占内存在“解除分配”(deallocated)之后,只是放回“可用内存池”(avaiable pool),如果尚未覆写对象内存,那么该对象仍然有效。为了避免使用无效对象,一般调用完 release 之后就会清空指针,“悬挂指针”(dangling pointer)能保证不会出现可能指向无效对象的指针。

ARC

  • ARC 会自动执行 retainreleaseautorelease 等操作,ARC 在调用这些方法时,并不通过普通的 Objective-C 消息派发机制,而是直接调用其底层 C,这样节省很多CPU 周期,性能更好。比如,ARC 会调用与 retain 等价的底层函数 objc_retain 等。
  • ARC 必须遵守方法命名规则:allocnewcopymutableCopy
  • ARC 借用 Objective-C++ 的一项特性来生成清理例程(cleanup routine),回收Objective-C++ 对象时,待回收的对象会调用所有 C++ 对象的析构函数(destructor),编译器如果发现某个对象里含有 C++ 对象,就会生成名为 .cxx_destruct 的方法,在 dealloc 中添加释放代码。
  • ARC 只负责管理 Objective-C 对象的内存,CoreFoundation(纯C的API)需要开发者适时调用 CFRetain/CFRelease

自动释放池

释放对象有两种方式:一种是调用 release 方法,使其保留计数立即递减;另一种是调用 autorelease 方法,将其加入“自动释放池”中。自动释放池用于存放那些需要在稍后某个时刻释放的对象,清空(drain)自动释放池时,系统会向其中的对象发送 release 消息,通常是在下一次“事件循环”(event loop)是递减。

  • 自动释放池排布在栈中,对象收到 autorelease 消息后,系统将其放入最顶端。
  • 合理运用自动释放池,可降低应用程序的内存峰值。

僵尸对象调试程序

  • 系统回收对象时,可以不将其真的回收,而是把它转为僵尸对象。通过环境变量 NSZombieEnabled 可开启这个功能。
  • 系统会修改对象的 isa 指针,让其指向特殊的僵尸类,从而让该对象变成僵尸对象,僵尸类能够响应所有的 selector,打印一条包含消息内容及其接收者的消息,然后终止程序。

Block 和 GCD

理解block

block 可以实现闭包,这个特性是作为“扩展”(extension)而加入 GCC 编译器中的。

block的基础

block 和函数类似,只不过是直接定义在另一个函数里的。block^ 符号来表示,后面一对 {},括号里面是实现代码。
block 其实就是一个值,和 Objective-C 对象一样,也可以赋值给变量,然后使用它。
block 类型的语法结构如下:

return_type (^block_name)(parameters)

block 的强大之处就是:在声明它的范围里,所有变量都可以被其所捕获。

block 的内部结构

每个 Objective-C 对象都占据着某个内存区域,block 本身也是一个对象,在存放 block 对象的内存区域中,首个变量是指向 Class 对象的指针,该指针叫做 isa。其余内存里含有 block 对象正常运转所需要的各种信息。

block对象的内存布局

在内存布局中,最重要的就是 invoke 变量,这是一个函数指针,指向 block 的实现代码。函数原型至少要接受一个 void* 型的参数,此参数代表 block

descriptor 变量是指向结构体的指针,每个 block 都包含这个结构体,其中声明了 block 对象的大小,还声明了 copydispose 两个辅助函数所对应的函数指针。辅助函数在拷贝和丢弃 block 对象是运行,前者会保留捕获的对象,后者会将其释放。

block 还会把它捕获的所有变量都拷贝一份,这些拷贝放在 descriptor 变量后面,有多少变量就需要占用多少内存空间。(拷贝的并不是对象本身,而是指向这些对象的指针变量。)

invoke 函数把 block 对象作为参数传进来,主要原因是执行 block 时,要从内存中把这些捕获的变量读出来。

全局 block、栈 block 和堆 block

定义 block 的时候,其所占的内存区域是分配在栈中的。意思就是说,block 只在定义它的那个范围内有效。为解决此问题,可用 copy 来拷贝 block 对象,这样就可以把 block 从栈复制到堆了,拷贝之后的 block 就可以在定义它的范围之外使用。而且,一旦复制到堆上,block 就变成了带引用计数的对象了,后续的 copy 都不会真的执行操作,只是递增 block 对象的引用计数。

除了栈 block 和堆 block 外,还有一类 block 叫做全局 blockglobal block),这种 block 不会捕捉任何状态,运行时也无需其他状态来参与,block 所使用的整个内存区域,在编译期间就已经完全确定了,因此,全局 block 可以声明在全局内存里,而不需要在每次用到的时候在栈中创建,全局 blockcopy 操作是一个空操作,因为全局 block 不会被系统所回收,实际相当于一个单例。

为常见的 block 创建 typedef

每个 block 都具备“固有类型”(inherent type),故可将其赋给适当类型的变量。

为了隐藏复杂的 block 类型,需要使用 C 语言中名为“类型定义”(type definition)的特性,typedef 关键字用于给类型起个别名。

typedef <returnType>(^<name>)(<arguments>);
  • typedef 重新定义block类型,可令 block 变量用起来更加简单。
  • 定义新类型时应该遵从现有的命名习惯,勿使用与别的类型相冲突的名称。

handler 块降低代码分散程度

  • 在创建对象时,可以使用内联的 handler 将相关业务逻辑一并声明。
  • 在有多个实例需要监控时,如果采用委托模式,那么需要根据传入的对象来切换,而若改用 handler 来实现,则可直接将 block 和相关对象放在一起。
  • 设计 API 时如果用到了 handler,那么可增加一个参数,通过这个参数来决定 block 在哪个队列上执行。

多用 GCD,少用同步锁

在 Objective-C 中,如果有多个线程执行同一份代码,就可能会出现问题,这时通常要使用锁来实现某种同步机制。在 GCD 之前,有两种方法,第一种是采用内置的“同步块”(`synchronization block):

@synchronized(self) {
    //safe
}

这种写法会根据给定的对象,自动创建一个锁,并等待 block 中的代码执行完毕后,锁就释放了。同步行为针对的对象是 self,滥用 @synchronized(self) 会降低代码效率,因为共用一个同步锁的代码块,都必须按顺序来执行。

另一种方法是使用 NSLock 对象:

NSLock *lock = [[NSLock alloc] init];
[lock lock];
//safe
[lock unlock];

也可以使用 NSRecursiveLock “递归锁”(recursive lock),线程能够多次持有该锁,而不会出现死锁(deadkock)现象。

  • 派发对象可用来表述同步语义(synchronization semantic),这种做法比使用@synchronized 块或 NSLock 对象更加简单。
  • 将同步和异步派发结合起来,可以实现和普通加锁机制一样的同步行为,这么做不会阻塞执行异步派发的线程。
  • 使用同步队列和栅栏函数,可以让同步行为更加高效。

多用 GCD,少用 performSelector 系列方法

  • performSelector 系列方法在内存管理方面容易有疏失,它无法确定将要执行的 selector 具体是什么,故而在 ARC 下编译器无法插入适当的内存管理方法。
  • performSelector 系列方法能够处理的 selectorselector 的返回值及发送给方法的参数个数都受到限制。

GCD 及 Operation Queue 的使用时机

从 iOS4 开始,"操作队列"(Operation Queue)在底层使用 GCD 来实现的。

GCD 是纯 C 的 API,而 Operation Queue 是 Objective-C 的对象。
使用 NSOperationNSOperationQueue 相比较 GCD 的好处是:

  • 取消某个操作。
  • 指定操作间的依赖关系。
  • 通过键值观测机制监控 NSOperation 对象的属性。
  • 指定操作的优先级。
  • 重用 NSOperation 对象。

熟悉系统框架

  • Foundation 框架。
  • CoreFoundation 框架,“无缝桥接”(toll-free bridging)可以互相转换 FoundationCoreFoundation
  • CFNetwork 框架提供了 C 语言级别的网络通信能力,它将 “BSD套接字”(BSD socket)抽象成易于使用的网络接口。而 Foundation 则将该框架里的部分内容封装为 Objective-C 语言的接口,进行网络通信。
  • 'CoreAudio' 框架提供的 C 语言 API 可以操作设备上的音频硬件。
  • AVFoundation 框架提供可用来录制并播放音频和视频的 Objective-C 接口。
  • CoreData 框架提供可将对象放入 Objective-C 的接口。
  • CoreText 框架提供可以高效执行文字排版及渲染操作的 C 语言接口。
  • CoreAnimation 框架提供了渲染图形并播放图形的 Objective-C 接口。
  • CoreGraphics 框架提供了 2D 渲染必备的数据结构和函数的 C 语言接口。
  • MapKit 框架提供了地图功能。
  • Social 框架提供社交功能。
    。。。。。。

使用 Objective-C 的 NSEnumerator 来遍历

NSEnumerator 是个抽象基类:

@interface NSEnumerator<ObjectType> : NSObject <NSFastEnumeration>

- (nullable ObjectType)nextObject;

@end

@interface NSEnumerator<ObjectType> (NSExtendedEnumerator)

@property (readonly, copy) NSArray<ObjectType> *allObjects;

@end
  • 遍历 collection 有四种方式,基本的 for 循环,其次是 NSEnumerator 遍历方法及快速遍历法,最好的是方法是“块枚举法”。
  • “块枚举法”本身是通过 GCD 来并发执行遍历操作,无需自己编写代码。

缓存选用 NSCatch 而非 NSDictionary

  • 缓存应该选用 NSCatch 而非 NSDictionary 对象,因为 NSCatch 可以提供自动删减功能,而且是线程安全的,此外,它与字典不同,并不会拷贝键。
  • 可以给 NSCatch 对象设置上限,用以限制缓存中的对象总个数。
  • NSPurgeableDataNSCatch 搭配使用,可以实现自动清除数据的功能。

精简 initialize 和 load 的实现代码

load 方法原型:

+ (void)load;

应用程序启动时,就会执行此方法,仅调用一次。

initialize 方法原型:

+ (void)initialize;

对于每个类来说,该方法会在程序首次用该类之前调用,且只调用一次。

  • 在加载阶段,如果类实现了 load 方法,系统就会调用它。分类里也可以定义此方法,类的 load 方法要比分类中的先调用。与其他方法不同,load 方法不参与覆写机制。
  • 首次使用某个类之前,系统会调用 initialize 方法,此方法遵从普通的覆写规则,应该在里面判断当前初始化的是哪个类。
  • loadinitialize 方法都应该实现的简单一些,以保持应用程序的响应能力。

NSTimer 会保留其目标对象

  • NSTimer 对象会保留其目标,直到计时器本身失效为止,调用 invalidate 方法可以让计时器失效,另外,一次性的计时器在触发任务之后也会失效。
  • 重复执行任务的定时器,很容易导致循环引用。
  • 可以扩充 NSTimer 的功能,用“块”来打破循环引用。

推荐阅读更多精彩内容