三读Objective-C2.0 笔记~(作为一个OC开发者,必读之书)
gitbook地址
一、熟悉Objective-C
1、了解Objective-C语言的起源
Objective-C在C语言的基础上添加了面向对象特性。使用“消息结构”(message structure)而非“函数调用”(function calling)。OC由Smalltalk
演化而来,后者是消息型语言的鼻祖。
消息与函数调用的关键区别在于:使用消息结构的语言,其运行时所应执行的代码有运行环境来决定;而使用函数调用的语言,则有编译器决定。
Objective-C的重要工作都由“运行期组件”(runtime component)而非编译期来完成。OC面向对象特性所需的全部数据结构和函数都在运行期组件里。
Objective-C是C的“超集”(superset),所以C语言中的所有功能在编写Objective-C代码时依然适用。因此必须同时掌握C与OC这两门语言的核心概念,方能写出高效的OC代码来。其中有为重要的是要理解C语言的内存模型(memory model),这有助于理解OC的内存模型及其“引用计数”(reference counting)机制的工作原理。需明白OC中的指针是用来指示对象的。想要声明一个变量,令其指代某个对象,可用如下语法:
代码语言:javascript复制NSString *string = @"Hello world";
这种语法基本上是照搬C语言的,它声明了一个名为string的变量,其类型是NSString*
。也就是说此变量为指向NSString的指针。所有OC语言的对象都必须这样声明,因为对象所占内存总是分配再“堆空间”(heap space)中,而绝不会分配在“栈”(stack)上。
如下两个指针指向同一个内存地址:
代码语言:javascript复制NSString *str1 = @"Hello world";
NSString *str2 = str1;
只有一个NSString实例,两个变量指向此实例,两个变量都是NSString*
型。及当前”栈帧“(stack frame)里分配了两块内存,每块内存大小都能容下一枚指针(在32位机器上是4字节,64位上是8字节)。这两块内存里的值都一样,就是NSString
实例的内存地址。
分配在堆中的内存必须直接管理,而分配在栈上用于保存变量的内存则会在其栈帧弹出时自动清理。OC将堆内存管理抽象出来,不需要用malloc及free来分配或释放对象所占内存。OC运行期环境把这部分工作抽象为一套内存管理架构:“引用计数”。
在OC代码中,有时候会遇到定义里不含*
的变量,它们可能会使用“栈空间”(stack space)。这些变量所保存的不是OC对象。如:CoreGraphics
框架中的CGRect:
CGRect frame;
// CGRect是C结构体,定义如下:
struct CGRect {
CGPoint origin;
CGSize size;
};
typedef struct CGRect CGRect;
整个系统框架都在使用这种结构体,因为改用OC对象来做的话,性能会受影响。与创建结构体相比,创建对象还需要额外开销(如:分配及释放堆内存等)。如果只需保存int
、float、double
、char等“非对象类型”(nonobject type),那么通常使用CGRect
这种结构体就可以了。
要点:
- OC为C语言添加了面向对象特性,是其超集。OC使用动态绑定的消息结构。及在运行时才会检查对象来袭,接收一消息后,究竟应执行何种代码,由运行期环境而非编译期决定
- 理解C语言的核心概念有助于写好OC程序,尤其要整我内存模型与指针
2、在类的头文件中尽量少引入其他头文件
在不需要知道某个类实现细节时,用“向前声明”(forward declaring):@class SomeClass
,而不是直接导入:#import "SomeClass.h"
。
如果在各自头文件中引入对方的头文件,则会导致“循环引用”(chicken-and-egg situation)。当解析其中一个头文件时,编译期会发现它引入了另一个头文件,而那个头文件又回过头来引用第一个头文件。使用#import
而非#include
指令虽然不会导致死循环,但这却意味着两个类有一个无法被正确编译,而报错❗️。
要点:
- 除非确有必要,否则不要引入头文件。一般来说,应在某个类的头文件中使用向前声明来提及别的类,并在实现文件中引入那些类的头文件。这样做可以尽量降低类之间的耦合度(coupling)。
- 有时无法使用向前声明,比如要声明某个类遵循一项协议。这种情况下,尽量把“该类遵循某协议”的这条声明移至“class-continuation分类”中。如果不行的话,就把协议单独放在一个头文件中,然后将其引入。
3、多用字面量语法,少用与之等价的方法
字面量语法更精简、整洁,没有多余的语法成分。
代码语言:javascript复制// 创建字符串,使用
NSString *str = @"Hello world";
// 而非: NSString *str = [[NSString alloc] initWithString:@"Hello world"];
// 创建数值,使用
NSNumber *num = @1;
// 而非: NSNumber *num = [NSNumber numberWithInt:1];
// 创建数组,使用
NSMutableArray *arr = [@[@"OC", @"Swift"] mutableCopy];
// 而非: NSArray *arr = [NSArray arrayWithObjects:@"OC", @"Swift", nil];
// 从数组里取值,使用
NSString *language = arr[0]; // 下标语法
// 而非: NSString *language = [arr objectAtIndex:1];
// 修改数组,使用
arr[0] = @"Objective-C";
// 而非: [arr replaceObjectAtIndex:0 withObject:@"Objective-C"];
// 创建字典,使用
NSMutableDictionary *dic = [@{@"key1": @"value1", @"key2": @"value2"} mutableCopy];
// 而非: NSDictionary *dic = [NSDictionary dictionaryWithObjectsAndKeys:@"value1": @"key1", @"value2": @"key2", nil];
// 从字典里取值,使用
NSString *value1 = dic[@"key1"];
// 而非: NSString *value1 = [dic objectForKey:@"key1"];
// 修改字典,使用
dic[@"key1"] = @"OC";
// 而非: [dic setObject:@"OC" forKey:@"key1"];
不过使用字面量语法创建数组时要注意,若数组元素对象中有nil,则会抛出异常,因为字面量语法实际上只是一种“语法糖”(syntactic sugar),其效果等于是先创建了一个数组,然后把方括号内的所有对象都加到这个数组中。Nr
要点:
- 应该使用字面量语法来创建字符串、数值、数组、字典。与创建此类对象的常规方法相比,这么做更加简明扼要。
- 应该通过取下标操作来访问数组下标或字典中的键所对应的元素
- 用字面量语法创建数组或字典时,若值中有nil,则会抛出异常。因此,务必确保值里不喊nil
4、多用类型常量,少用#define预处理指令
代码语言:javascript复制// ------ 使用静态常量
// 1.类之内可见
static const NSTimeInterval kAnimationDuration = 0.3; // .m
// 若不加static,这会创建“外部符号”(external symbol)),其他类有同名的会冲突
// static const: 不会创建符号,直接替换,但是有类型检查
// 2.类之外可见:
extern NSString *const MOBTManagerConnectedNotification; // .h
NSString *const MOBTManagerConnectedNotification = @"value"; // .m
// 会创建符号,放在“全局符号表”(constant variable)中
// ------ 而不是宏定义
#define ANIMATION_DURATION 0.3
// 宏定义没有类型,编译期只是单纯替换,不会做类型检查
- 常量局限于某“编译单元”(translation unit,也就是“实现文件”,implementation file)之内,则在前面加字母k;
- 常量在类之外可见,则通常以类名为前缀。
尽量不要在头文件里声明
#define
orstatic const
,因为OC没有“名称空间”(namespace)这一概念,所以那样做等于声明了一个全局变量
要点:
- 不要用预处理指令定义常量。这样定义出来的常量不含类型信息,编译期只是会在编译前据此执行查找与替换操作。即使有人重新定义了常量值,编译器也不会产生警告信息,这将导致应用程序中的常量值不一致
- 在实现文件中使用
static const
来定义“只在编译单元内可见的常量”(translation-unit-specific constant)。由于此类常量不在全局符号表中,所以无须为其名称加前缀 - 在头文件中使用extern来声明全局常量,并在相关实现文件中定义其值。这种常量要出现在全局符号表中,所以其名称应该加区隔,通常用与之相关的类名做前缀。
5、用枚举表示状态、选项、状态码
- 应该用枚举来表示状态机的状态、传递给方法的选项以及状态码等值,给这些值起一个易懂的名字
- 如果把传递给某个方法的选项表示为枚举类型,而多个选项有可同时使用,那么就将各选项定义为2的幂,一遍通过
按位或
(|
)操作符将其组合起来 - 用
NS_ENUM
与NS_OPTIONS
宏来定义枚举类型,并指明其底层数据类型。这样做可以确保枚举是用开发者所选的底层数据类型实现出来的,而不会采用编译期所选的类型 - 在处理枚举类型的
switch
语句中不要实现default
分支。这样的话,加入新的枚举之后,编译期就会提示开发者:switch
语句并未处理所有枚举
二、对象、消息、运行期
6、理解“属性”这一概念
可以看这篇:iOS_理解“属性”(property)这一概念
要点:
- 可以用
@property
语法来定义对象中所封装的数据 - 通过“特质”来指定存储数据所需的正确语义
- 在设置属性所对应的实例变量时,一定要遵从该属性所声明的语义
7、在对象内部尽量直接访问实例变量
建议:读取变量时用直接访问实例变量的形式,设置变量时通过属性来做。
两种写法的区别:
- 直接访问,由于没经过OC的“方法派发”(method dispatch)步骤,所以速度比较快。其编译所生成的代码会直接访问保存对象实例变量的那块内存
- 直接访问,不会调用
setter
方法,绕过了为相关属性定义的“内存管理语义”。(如:ARC下直接访问一个copy属性,不会拷贝该属性,只会保留新值并释放旧值) - 直接访问,不会出访“键值观测”(KVO)通知,这样做是否有问题,还取决于具体的对象行为
- 通过属性访问,有助于排查与之相关的错误,因为可以在
setter
或getter
中新增“断点”(breakpoint),监控该属性调用者及其访问时机
要点:
- 在对象内部读取数据时,应该直接通过实例变量来读,而写入数据时,则应该通过属性来写
- 在初始化方法及
dealloc
方法中,总是应该直接通过实例变量来读写数据 - 有时会使用惰性初始化技术配置某份数据,此时需要通过属性来读取数据
8、理解“对象等同性”这一概念
可以看这篇:iOS_理解“对象等同性”这一概念(==、isEqual、hash)
要点:
- 若想检测对象的等同性,请提供
isEqual:
与hash
方法 - 相同的对象必须具有相同的哈希码,但是两个哈希码相同的对象却未必相同
- 不要盲目地逐个检测每条属性,而应该依照具体需求来制定检测方案
- 编写hash`方法时,应该使用计算速度快而且哈希码碰撞几率低的算法
9、以“类族模式”隐藏实现细节
“类簇”(class cluster)是一种很有用的模式(pattern),可以隐藏“抽象基类”(abstract base class)背后的实现细节。OC的系统框架中普遍使用此模式,大部分的collection
类都是类簇,如:UIButton
、NSArray
、NSMutableArray
等:
// 返回的对象,其类型取决于传入的按钮类型(button type)
// 不管返回什么类型的对象,都是继承自同一个基类:UIButton
// 意义:使用者无须关心属于哪个类,和其绘制等实现细节,只须知道如何创建,如何设置,如何使用就好
(UIButton *)buttonWithType:(UIButtonType)type;
// 如果放在同一个类里,你可能会写出这样的代码:
- (void)drawRect:(CGRect)rect {
if (_type == TypeA) {
// Draw TypeA button
} else if (_type == TypeB) {
// Draw TypeB button
}
// 但是当类型和方法越来越多时,这样做救护很麻烦
}
优秀的程序猿会重构为多个子类,把各个按钮所用的绘制方法放到相关子类中去。不过需要用户知道各种子类才行,此时应使用“类簇模式”:可以灵活应对多个类,讲他们的实现细节隐藏在抽象基类后面,以保持接口简洁。用户无须自己创建子类实例,只需调用基类方法来创建即可。
创建类簇:
代码语言:javascript复制// .h:声明枚举、属性、方法。。。
// .m:实现初始化方法:工程模式,是创建类簇的方式之一
(MOEmployee *)employeeWithType:(MOEmployeeType)type {
swith (type) {
case MOEmployeeTypeDeveloper: return [MOEmployeeDeveloper new];
case MOEmployeeTypeDesigner: return [MOEmployeeDesigner new];
case MOEmployeeTypeFinance: return [MOEmployeeFinance new];
}
}
- (void)doWork {
// 子类们实现各自的工作内容
}
// 子类继承MOEmployee,并实现doWork方法。。。
// 另外注意:
[anEmployee isMemberOfClass:[MOEmployee class]] // 永远为NO!!!
Cocoa里的类簇:
你要是知道NSArray
是个类簇,就不会写出下面第一行这样的代码,[anArray class]
所返回的类绝对不可能是NSArray
类本身!!!
if ([anArray class] == [NSArray class]) {} // 永远为NO!!!
// Array的初始化方法返回的是隐藏的某个内部类型
// 不过可以这样判断是都属于其类簇中
if ([anArray isKindOfClass:[NSArray class]]) {} // YES
想为类簇新增子类,需要遵循几条规则:
- 子类应该继承自类簇中的抽象基类(如:不可变数组的基类 or 可变数组的基类)
- 子类应该定义自己的数据存储方式(如:Array子类,可以用Array来存储,Array不会自动保存数据)
- 子类应当覆写超类文档中指明需要覆写的方法(如:
Array
的count
、objectAtIndex:
)
要点:
- 类簇模式可以把实现细节隐藏在一套简单的公共接口后面
- 系统框架中经常使用类簇
- 从类簇的公共抽象基类中继承子类时要当心,若有开发文档,则应首先阅读
10、在既有类中使用关联对象存放自定义数据
可以给某对象关联许多其他对象,这些对象通过“键”来区分。存储对象值的时候,可以指明“存储策略”(storage policy),用以维护相应的“内存管理语义”。策略有名为objc_AssociationPolicy
的枚举所定义:
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 object, void* key, id value, objc_AssociationPolicy policy)
此方法以给定的键和策略为某对象设置关联对象值id objc_getAssociatedObject(id object, void* key)
此方法根据给定的键从某对象中获取相应的关联对象值void objc_removeAssociatedObject(id object)
此方法移除指定对象的全部关联对象 可以把某对象想像成NSDictionary
,把关联到该对象的值理解为字典中的条目,存储关联对象的值就相当于在字典对象调用setObject:valueforKey:
和objectForKey:
方法。然而两者之间有个重要差别:设置关联对象时用的键(key)时一个“不透明的指针”(opaque pointer)。如果在两个键上调用**isEqual:
**方法返回YES,那么字典人为二者相等;而在设置关联对象值时,若向令两个键匹配到同一个字,则二则必须是完全相同的指针才行。鉴于此,在设置关联对象值时,通常使用静态全局变量做键。
关联对象用法举例:
可以看这篇文章(iOS_理解“属性”(property)这一概念)的 @dynamic
(二-2):在Category中为类添加属性。
要点:
- 可以通过“关联对象”机制来把两个对象连起来
- 定义关联对象时可指定内存管理语义,用以模仿定义属性时所采用的“拥有关系”与“非拥有关系”
- 只有在其他做法不可行时才应选用关联对象,因为这种做法通常会引入难于查找的bug
11、理解objc_msgSend的作用
可以看这篇: iOS_Objective-C 消息发送(消息查找 及 消息转发)过程中的 一二、OC中的消息和消息查找
上面文章讲述的自描述了部分消息的调用过程,其他“边界情况”(edge case)则需要交由Objective-C运行环境中的另一些函数来处理:
objc_msgSend_stret
:消息返回结构体时调用此方法。(当CPU的寄存器能容纳的心返回值类型时,否则用的是另一个函数)objc_msgSend_fpret
:消息返回浮点数时调用此方法。需要对“浮点数寄存器”(floating-point register)做特殊处理。objc_msgSendSuper
:消息是给超类发的。如:[super someMethod:parameter]
。也有另外两个与objc_msgSend_stret
和objc_msgSend_fpret
等效的函数,用于处理发给super
的相应消息。
还有一个概念需要理解一下:“尾调用优化”(tail-call optimization)技术:
如果某函数的最后一项操作是调用另外一个函数且不会返回值另作他用时,那么就可以运用“尾调用优化”技术。编译器会生成调转至另一函数所需的指令码,且不会向调用堆栈中推入新的“栈帧”(frame stack)。(这项优化对objc_msgSend
非常关键,若不做优化的话,每次调用OC方法前,都需要为objc_msgSend
函数准备“栈帧”(可以在“栈踪迹”stack trace
中看到),还会过早地发生“栈溢出”(stack overflow)现象)。
明白这一点,就能理解为何在在调试的时候,栈“回溯”(backtrace)信息中总是出现objc_msgSend
了。
要点:
- 消息由接收者、选择子、参数构成。给某对象“发送消息”(invoke a message)也就相当于在该对象上“调用方法”(call a method)。
- 发给某对象的全母消息都要用“动态消息派发系统”(dynamic message dispatch system)来处理,该系统会查出对应的方法,并执行其代码。
12、理解消息转发机制
可以看这篇: iOS_Objective-C 消息发送(消息查找 及 消息转发)过程中的三、消息转发
要点:
- 若对象无法响应某个选择子,则进入消息转发流程
- 通过运行期的动态方法解析功能,我们可以在需要用到某个方法时再将其加入类中
- 对象可以把其无法解读的某些选择子转交给其他对象来处理
- 经过上述两步之后,如果还是没办法处理选择子,那就启动完整的消息转发机制
13、用“方法调配技术”调试“黑盒方法”
类的方法列表会把选择子的名称映射到相关的方法实现上,使得“动态消息派发系统”能够据此找到应该调用的方法。这些方法均以函数指针的形式来表示,这种指针叫IMP
,其原型如下:
id (*IMP)(id, SEL, ...)
“方法调配”(method swizzling)
代码语言:javascript复制// 获取方法实现
Method class_getInstanceMethod(Class aClass, SEL aSelector)
// 交换两个方法的实现
void method_exchangeImplementations(Method m1, Method m2)
使用可以看这篇: iOS_Runtime是什么?原理?作用?怎么实现weak?使用后面的举例。
要点:
- 在运行期,可以向类中新增或替换选择子所对应的方法实现
- 使用另一份实现来替换原有的方法实现,这到工序叫做“方法调配”,开发者常用此技术向原有实现中添加新功能
- 一般来说,只有调试程序时才需要在运行期修改方法实现,这种做法不易滥用
14、理解“类对象”的用意
“在运行期检视对象类型”这一操作也叫做“类型信息查询”(introspection,“内省”),这一个强大而有用的特性内置于Foundation
框架的NSObject
协议里,凡是由公共根类(common root class,即NSObject
与NSProxy
)继承而来的对象都要遵从此协议。在程序中不要直接比较对象所属的类,明智的做法是调用“类型信息查询方法”。
OC对象的本质:
代码语言:javascript复制// 每一个OC对象实例都是指向某块内存数据的指针。所以在声明变量时,类型后面都要跟一个`*`字符
NSString *pointerVariable = @"Hello world";
// 对于通用对象类型`id`,由于其本身已经是指针了,所以我们能够这样写
id genericTypeString = @"Hello world"
上面两种定义方式区别在于:
第一种声明时指定了具体类型,当调用其没有的方法时,编译器会发出警告⚠️。
OC对象所用的数据结构定义在运行期程序库的头文件里,id
类型本身也定义在这里:
typedef struct objc_object {
Class isa,
} *id;
由此可见,每个对象结构体的收个成员是Class
类的变量。该变量定义了对象所属的类,通常称为“is a”指针。(如:string对象“是一个”(is a
)NSString
,所以其“is a”指针就指向NSString
)
Class
对象也定义在运行期程序库的都文件中:
typedef struct objc_class *Class;
struct objc_class { // 吃结构体存放类的“元数据”(metadata)
Class isa; // 说明Class本身也是OC对象
Class super_class; // 本类的超类
const char *name;
long version;
long info;
long instance_size;
struct objc_ivar_list *ivars; // 实例变量
struct objc_method_list **methodLists; // 方法
struct objc_cahce *cache;
struct objc_protocol_list *protocols;
}
类对象所属的类型(即isa
指针指向的类型)是另外一个类,叫“元类”(metaclass),用来表述类本身所剧本的元数据。“类方法”就定义于此处,这些方法可以理解为类对象的实例方法。每个类仅有一个“类对象”,每个“类对象”仅有一个与之相关的“元类”。
假如有一个名为SomeClass
的子类继承自NSObject
,则其继承体系如下:
super_class
指针确立了继承关系,而isa
指针描述了实例所属的类。通过这张布局关系图即可执行“类信息查询”。可以查出对象是否能响应某个选择子,是否遵从某项协议,看出此对象位于“类继承体系”(class hierarchy)的哪一部分。
在类继承体系中查询类型信息:
isMemberOfClass:
:判断对象是否为某个特定类的实例isKindOfClass:
:判断对象是否为某类/其派生类的实例
使用isa
指针获取对象所属的类,然后通过super_class
指针在继承体系中游走。由于OC对象是动态的,所以此特性显得极为重要。从collection
中取出的对象类型通常是id
的,可以使用类型信息查询方法。如:
NSMutableString *str = [NSMutableString new];
for (id obj in array) {
if ([obj isKindOfClass:[NSString class]]) {
[str appendFormat:@"%@", obj];
} else if ([obj isKindOfClass:[NSNumber class]]) {
[str appendFormat:@"%d", [obj intValue]];
} else {
// ...
}
}
不应该直接比较两个类对象是否等同,如:某个对象可能会把收到的所有选择子都转发给另外一个对象。这样的对象叫做“代理”(proxy),通常继承自NSProxy
。通常在此对象上调用class
方法,返回的是代理对象本身(NSProxy
的子类),而非接受代理的对象所属的类。(用isKindOfClass:
这样的类型信息查询方法,代理对象会转发给“接受代理的对象”;而用class
返回的是发起代理的对象,而非“接受代理的对象”)。
要点:
- 每个实例都有一个指向
Class
对象的指针,用以表明其类型,这些Class
对象构成了类的继承体系 - 如果对象类型无法在编译期确定,那么就应该使用类型信息查询方法来探知
- 尽量使用类型信息查询方法来确定对象类型,而不要直接比较类对象,因为某些对象可能实现了消息转发功能
三、接口与API设计
15、用前缀避免命名空间冲突
因为OC没有命名空间这一机制,所以改用为所有名称都加上适当的前缀来变相实现命名空间(比如:你的公司叫Effective Widgets
,那么可以使用EWS
做前缀),减少重名几率。
使用Cocoa
创建应用程序时要注意了,Apple
宣称其保留使用所有两个字母前缀
的权利,所以你自己选用的前缀应该时三个字母的。(如果现在系统类里没有,保不准之后更新会出现跟你同名的两个字母前缀
的系统类)
不仅时类名,程序中所有名称都应加上前缀,如要为既有类新增分类
,则一定要给**分类
**及**分类
**中的方法加上前缀。还有个可能会忽视的容易引发命名冲突的地方,就是类的实现文件中所用的纯C函数
及全局变量
,在编译好的目标文件中,这些名称都是“顶级符号”。如:
// 应加前缀:EOCSoundPlayerCompletion (项目前缀 类前缀 方法名)
void completion(SystemSoundID ssID, void *clientData) {
// 触发代理,实现某些方法
}
- (void)playSound {
// 播放声音
AudioServiceAddSystemSoundCompletion(
_systemSoundID,
NULL,
NULL,
completion, // 播放完后回调
(__bridge void *)self
);
AudioServicesPlaySystemSound(_systemSoundID);
}
这段代码看上去很正常,不过再看看该类目标文件中的符号表(symbol table),就会发现问题了:
虽说completion
函数是在实现文件里定义的,并没有声明于头文件中,不过它仍是顶级符号
。此时若在别处创建一个名为completion
的函数,则会于链接时报错(重复符号错误
):
duplicate symbol _completion in:
build/EOCSoundPlayer.o
build/EOCAnotherClass.o
如果将代码发布为程序库供他人使用,就更糟糕了。使用此程序库的开发者再无法创建名为completion
的函数了。
若自己编写程序库提供给他人使用,其中用到别人的三方库时,应该为其加上自己的前缀。因为别人可能也用到了这个库;亦或者:别人要用A版本,而你用的B版本,那么他必须自己再引入一份;更有甚:别人还用了另一个三方库(里也同样用了你用的三方库),此时如果都没加前缀,那么程序依然会出现重复符号错误。
要点:
- 选择与你的公司、应用程序或二者皆有关联之名称作为类名的前缀,并在所有代码中均使用这一前缀
- 若自己所开发的程序库中用到了第三方库,则应为其中的名称加上前缀
16、提供“全能初始化方法”
全能初始化方法 or 指定初始化方法 Designated initializer
可以看这篇:iOS_指定初始化方法Designated Initializer和非指定初始化方法Secondary Initializer
要点:
- 在类中提供一个全能初始化方法,并于文档里指明。其他初始化方法均应调用此方法
- 若全能初始化方法与超类不同,则需覆写超类中的对应方法
- 如果超类的初始化方法不适用于子类,那么应该覆写这个超类方法,并在其中抛出异常
17、实现description方法
首先description
方法是定义在NSObject协议
里的,然后NSObject
和NSProxy
俩“根类”都遵循了该协议,并有默认实现:打印类名和内存地址(如:<Person: 0x7f9a1600600>
)。
- 用
NSLog
%@
打印时调用的是description
方法 - 程序运行打断点时,在调试控制台输入
LLDB
的po
命令,调用的是debugDescription
方法
可以实现如下:
代码语言:javascript复制- (NSString *)description {
return [NSString stringWithFormat:@"%@ %@", _firstName, _lastName];
} // Bob Smith
- (NSString *)debugDescription {
return [NSString stringWithFormat:@"<%@: %p," %@ %@">", [self class], self, _firstName, _lastName];
} // (Person *) $1 = 0x07117fb0 <Person: 0x7117fb0, "Bob Smith">
要点:
- 实现
description
方法返回一个有意义的字符串,用以描述该实例 - 若想在调试时打印出更详尽的对象描述信息,则应实现
debugDescription
方法
18、尽量使用不可变对象
如8条,若把可变对象放到collection之后又修改其内容,那么很容易就会破坏set的内部数据结构,使其失去固有的语义。因此,建议大家尽量减少对象中的可变内容。
尽量把对外公布出来的属性设为只读,而且只在确有必要时才将属性对外公布。
如:person有一个friends全部朋友的属性,放在一个“列表”(list)里外界可以增删。通常应该提供一个readonly属性返回不可变set(内部可变set的copy)供外界使用。
代码语言:javascript复制// .h
@property (nonatomic, strong, readonly) NSSet *friends;
- (void)addFriend:(Person *)person;
- (void)removeFriend:(Person *)person;
// .m
@property (nonatomic, strong, readwrite) NSSet *friends;
NSMutableSet *_interalFriends;
- (NSSet)firends {
return [_interalFriends copy]; // 拷贝不可变版本
}
- (void)addFriend:(Person *)person {
[_interalFriends addObject:person];
}
- (void)removeFriend:(Person *)person {
[_interalFriends removeObject:person];
}
如果直接提供可变版本NSMutableSet
供外部使用,不是借助addFriend:
与removeFriend:
方法,而是直接操作此属性。这种过分解耦数据
的做法很容易出bug。如:在添加或删除朋友时,Person
对象可能还要执行其他操作,此时就等于直接从底层修改了其内部用于存放朋友对象的set
。在Person
对象不知情时,直接从底层修改set
可能会令对象内的各个数据之间互不一致。
要点:
- 尽量创建不可变的对象
- 若某属性仅可于对象内部修改,则在
class-continuation分类
中将其由readonly
属性扩展为readwrite
属性 - 不要把可变的
collection
作为属性公开,而应提供相关方法修改对象中的可变collection
19、使用清晰而协调的命名方式
1、方法命名:
- 如果方法的返回值是新创建的,那么方法名的首个词应该是返回值的类型。也可以看情况在前面添加修饰语,如:
localizedString
。属性的存取方法不遵循这种命名方式~ - 应该把表示参数类型的名词放在参数前面
- 如果方法要在当前对象上执行操作,那么就应该包含动词;若执行操作时还需要参数,则应该在动词后面加上一个或多个名词
- 不要使用
str
这种简称,应该用string
这样的全称 Boolean
属性应加is前缀。如过某个方法返回Boolean
值,应根据其功能添加has
或is
前缀- 将
set
这个前缀留给那些借由输出参数
来保存返回值的方法,比如说,把返回值填充到C言语式数组
(C-stye array)里的那种方法就可以使用这个词做前缀
2、类与协议的命名
应该为类与协议的名称加上前缀,以避免命名空间冲突,而且应该像给方法起名时那样把词句组织好,使其从左至右读起来较为通顺。
要点:
- 起名时应遵从标准的
Objective-C
命名规范,这样创建出来的接口更容易为开发者所理解 - 方法名要言简意赅,从左至右读起来要像个日常用语中的句子才好
- 方法名里不要使用缩略后的类型名称
- 给方法起名时第一要务就是确保其风格淤泥自己的嗲吗或所要继承的框架相符
20、为私有方法名加前缀
要点:
- 给私有方法的名称加上前缀,这样可以很容易的将其同公共方法区分开
- 不要单用一个下划线做私有方法的前缀,因为这种做法是预留给苹果公司用的
21、理解Objective-C错误模型
Error
对象里封装了三条信息:
- Error domain:错误范围,字符串
产生错误的根源,通常用一个特有的全局变量来定义。如:
NSURLError
表示解析URL出错 - Error code:错误码,整数 独有的错误码,指明在某个范围内具体发生了何种错误,通常用enum定义。如:HTTP请求出错时,可能回把HTTP的状态码设为错误码
- User info:用户信息,字典 有关此错误的额外信息,其中或许包含一段“本地化的描述”(localized description),或许还含有导致该错误发生的另外一个错误,经由此种信息,可将相关错误串成一条“错误链”(chain of errors)
NSError的domain、code、userInfor应该根据具体错误情况填入适当内容,方便调用者根据错误类型分别处理各错误。domain定义成NSString
类型的全局常量,而code则定义成枚举类型为佳。如:
// XXXErrors.h
extern NSString *const XXXErrorDomain;
typedef NS_ENUM(NSUInteger, XXXError) {
XXXErrorUnKnown = -1,
XXXErrorGeneralFault = 100,
XXXErrorBadInput = 101,
}
// XXXErrors.m
NSString *const XXXErrorDomain = @"XXXErrorDomain";
建议:
- 为自己的程序库中所发生的错误制定一个专用的“错误范围”字符串
- 用枚举定义错误码,不仅解释错误码的含义,还给它们起了个有意义的名字
要点:
- 只有发生了会使整个应用程序崩溃的严重错误时,才使用异常
- 在错误不那么严重的情况下,可以指派“委托方法”(delegate method)来处理错误,也可以把错误信息放在
NSError
对象里,经由“输出参数”返回给调用者
22、理解NSCoping协议
Foundation
框架中的所有collection
类在默认情况下都执行浅拷贝,即只拷贝容器对象本身,而不复制其中数据。(主要是因为:容器里的对象未必都能拷贝,而且调用者尾部想在拷贝容器时一并拷贝其中的每个对象)
另外,不要假定遵从了NSCopying
协议的对象都会执行深拷贝。在绝大多数情况下执行的都是浅拷贝。如果需要在某个对象上执行深拷贝,那么除非该类的文档说它是用深拷贝来实现的NSCopying
协议,否则:要么寻找能够指向深拷贝的相关方法,要么自己编写方法实现。
要点:
- 若想令自定义的对象具有拷贝功能,则需实现
NSCopying
协议 - 若自定义对象分为可变版本与不可变版本,则应同事实现
NSCopying
与NSMutableCopying
协议 - 复制对象时需决定采用
浅拷贝
还是深拷贝
,一般情况下应尽量执行浅拷贝 - 若自定义对象需要深拷贝,那么可考虑新增一个专门执行深拷贝的方法,如
deepCopy
四、协议与分类
23、通过委托与数据源协议进行对象间通信
“委托模式”/“代理模式”(Delegate pattern)主旨:
定义一套接口,其他对象若想接收当前对象的委托,则需遵从此接口成为其“委托对象”(delegate)。而当前对象则可以给其委托对象传递一些信息,也可以在放生相关事件时通知委托对象。(此模式可将数据与业务逻辑解耦)
通常情况下delegate
对象会持有当前对象,所以需要将delegate
属性定义成weak
,否则会造成循环引用导致内存泄露。
若要向外界公布此类实现了某协议,那么就在接口(.h文件)中声明;而如果这个协议是个委托协议的话,就可以在类内部(class-continauation分类中)声明。
委托协议中的方法一般都是“可选的”(用@optional
声明,后面的都是可选),因为代理未必关心其中的所有方法。
在调用delegate中的方法时,总是应该把当前对象也一并传入方法中,这样delegate
在实现相关方法时,就能根据传入的实例分别执行不同的代码了。如:
- (void)networkFetcher:(NetworkFetcher *)fetcher
didReceiveDate:(NSData *)data {
if (fetcher == _myFetcherA) {
} else if (fetcher == _myFetcherB) {
}
}
可以在当前对象中声明一个含有位段的结构体为其实例对象,结构体中的每个位段表示delegate
是否实现了协议中的相关方法:
@interface NetworkFetcher () {
struct {
unsigned int didReceiveData : 1;
unsigned int didFailWithError : 1;
unsigned int didUpdateProgressTo : 1;
} _delegateFlages; // 实例对象!!!
}
// 设置代理时,就缓存当前delegate是否能响应协议中的相关方法
- (void)setDelegate:(id< NetworkFetcher>)delegate {
_delegate = delegate;
_delegateFlages.didReceiveData = [delegate respondsToSelector:@selector(networkFetcher:didReceiveData:)];
....
}
协议方法要调用很多次时,值得进行这种优化。而是否需要优化,则应依照具体代码来定。这种需要分析代码性能,并找出瓶颈,若发现执行速度需要改进,则可使用此技巧。
要点:
- 委托模式为对象提供了一套接口,使其可由此将相关事件告知其他对象
- 将委托对象应该支持的接口定义成协议,在协议中把可能需要处理的时间定义成方法
- 当某对象需要从另外一个对象中获取数据时,可使用委托模式。这种情境下,该模式亦称“数据源协议”(data source protocal)
- 若有必要,可实现含有段位的结构体,将委托对象是否能响应相关协议方法这一信息缓存至其中
24、将类的实现代码分散到便于管理的数个分类之中
类的基本要素(诸如:属性、初始化方法等)都声明在“主实现”(main implementation)里。执行不同类型的操作所用的另外几套方法则归入各个分类中。
处在分类中的所有方法,其符号中会包含分类名。如:addFriend:
方法的“符号名”(symbol name)如下:
- [Person(Frendship) addFriend:]
根据回溯信息中的分类名称,很容易就能精确定位到类中的方法所属的功能区,这对于某些应该视为私有的方法来说更是极为有用。
在编写准备分享给其他开发者使用的程序库时,可以考虑创建Private
分类。经常会有些方法:他们不是公共API的一部分,然而确非常适合在程序库之内使用。将其放入Private
分类中,哪里用到就引入。而分类的头文件不随公共API一并公开。这样使用者旧不知道库里还有这些私有方法了。
要点:
- 使用分类机制把类的实现代码划分成易于管理的小块,以便单独检视
- 将应该视为“私有”的方法归入名教
Private
的分类中,已隐藏实现细节
25、总是为第三方类的分类名称加前缀
因为OC中没有命名空间这一概念,所以只能用给类名/方法名添加前缀
的方式实现。一般来说这个前缀应该与当前项目/当前模块相同。如:
@interface NSString (XXX_HTTP) // 为分类名添加前缀!!!!
- (NSString *)xxx_urlEncodeString; // 为分类方法名名添加前缀!!!
- (NSString *)xxx_urlDecodeString;
要点:
- 向第三方类中添加分类时,总应给其
名称
和方法名
加上你专用的前缀
26、勿在分类中声明属性
虽然说我们可以在分类中运用runtime
实现关联对象的方式,实现属性(实现方式之前这篇文章的@dynamic
部分有写)。但是这么做不理想。要把相似的代码写很多边,而且在内存管理问题上容易出错(如:当修改了某个属性的特质attrubute
时,还要记得修改setter
方法中设置关联对象时所用的内存管理语义)。
分类的目标在于扩展类的功能,而非封装数据。
但有时候只读属性还是可以在分类中使用的。如:为NSCalendar
类创建分类,返回各个月份名称数组。虽说仅是访问数据不需要实例变量来实现。但此时最好不要用属性,用一个方法就好。因为属性表达的意思是:类中有数据在支持着它。
@interface NSCalendar (XXX_Additions)
- (NSArray *)xxx_allMothns;
@end
@implementation NSCalendar (XXX_Additions)
- (NSArray *)xxx_allMothns {
if ([self.calendar isEqualToString:NSGegorianCalendar]) {
return @[@"January", @"February", ...];
} else if (/* other calendar identifiers */) {
/* return months for other calendars */
}
}
@end
要点:
- 把封装数据所用的全部属性都定义在主接口里
- 在"class-continuation分类"之外的其他分类中,可以定义存取方法,但尽量不要定义属性
27、使用“class-continuation”分类隐藏实现细节
将使用到的C 文件在实现文件中导入,仅使实现文件扩展名为.mm
,使用OC 编译。头文件仍就是.h
,使用OC编译。从而实现隐藏C 代码
的效果。如系统的WebKit
和CoreAnimation
就用到了此模式,内部很多都用C 写成,但对外公布的却是一套纯OC接口
要点:
- 通过“class-continuation分类”向类中新增实例变量
- 如果某属性在主接口中什么为“只读”,而类的内部又要用设置方法修改此属性,那么就在“class-continuation分类”中将其扩展为“可读写”
- 把私有方法的原型声明在“class-continuation分类”里
- 若想是使类遵循的协议不为人所知,则可以于“class-continuation分类”中声明
28、通过协议提供匿名对象
若接口背后有多个不同的实现类,而你有不想指明具体使用哪个类,那么可以考虑使用遵从某协议的纯id类型
—>因为有时候这些类可能会边,有时候它们又无法容纳于标准的类继承体系中,因而不能以某个公共基类来统一表示。
如:代理(类型不重要,只要遵循协议就行)
代码语言:javascript复制@property (nonatomic, weak) id <XXXDelegate> delegate;
如:NSDictionary设置键值对方法
代码语言:javascript复制- (void)setObject:(id)object forKey:(id<NSCopying>)key;
如:处理数据库连接的程序库,以匿名对象来表示从另一个库中返回的对象
代码语言:javascript复制@protocol XXXDatabaseConnection
- (void)connect;
- (void)disconnect;
- (void)isConnect;
- (NSArray *)performQuery:(NSString *)query;
@end
@interface XXDataBaseManager: NSObject
(id)sharedInstance;
// 返回连接对象
- (id<XXXDatabaseConnection>)connectionWithIdentifier:(NSString)identifier;
@end
如此,处理数据库连接所用的类名就不会泄露,可能来自不同款家的类型现在均可用同一个方法返回了。
再如:CoreData
框架在负责查询接口的NSFetchedResultsContoller
中,有个sections属性表示数据分区,是个数组如下:
NSArray *sections = controller.sections;
id <NSFecthedResultsSectionInfo> sectionInfo = section[section];
NSUInteger numberOfObjects = sectionInfo.numberOfObjects;
在幕后,此对象可能使由处理结果的控制器所创建的内部对象,没必要把表示翅中数据的类对外公布,因为使用控制器的人绝对不关心查询结果中的数据分区使如何保存的,他们只要知道可以在这些对象上查询数据就行
要点:
- 协议可在某种程度上提供匿名类型。具体的对象类型可以淡化成
遵从某协议的id类型
,协议里规定了对象所应实现的方法(如果具体类型不重要,重要的使对象能够响应(定义在协议里的)特定方法,那么可使用匿名对象来表示) - 使用匿名对象来隐藏类型名称(或类名)
五、内存管理
29、理解引用计数
要点:
- 引用计数机制通过可以递增递减的计数器来管理内存。对象创建好之后,其保留计数至少为1。若保留计数为正,则对象继续存活。当保留计数降为0时,对象就被销毁了。
- 在对象生命期中,其余对象通过引用来保留或释放此对象。保留与释放操作分别会递增及递减保留计数
对象回收后,系统会降其占用的内存标记为“可重用”(reuse)。
如果按“引用数”回溯,那么最终会发现一个“根对象”(root object)。在iOS应用程序中,则是UIApplication
对象,是程序启动时创建的单例。
调用autorelease
会在稍后递减计数,通常是在下一次“事件循环”(event loop)时递减,不过可能会执行得更早一些。此方法可保证对象在跨越“方法调用边界"(method call boundary)后一定存活。(实际上,释放操作会在清空最外层的自动释放池时执行,除非你有自己的自动释放池,否则这个时机指的是当前线程的下一次事件循环。)
30、以ARC简化引用计数
要点:
- 有ARC后,程序员无须担心内存管理问题,可省去类中的许多“样板代码”
- ARC管理对象生命期的办法基本上就是:在合适的地方插入“保留”及“释放”操作。在ARC环境下,变量的内存管理语义可以通过修饰符指明,而原来则需要手工执行“保留”及“释放”操作
- 由方法返回的对象,其内存管理语义是通过方法名来体现。ARC将此确定为开发者必须遵守的规则
- ARC只负责管理OC对象的内存。尤其要注意:
CoreFoundation
对象不归ARC管理,开发者必须适时调用CFRetain
/CFRelease。
Clang编译器项目带有一个“静态分析器”(static analyzer),用于指明程序里引用计数出问题的地方。
方法命名规范:
alloc
、new、copy
、mutableCopy开头的方法内部会调用retain,使引用计数 1,方便调用者持有返回的对象,调用者也需要负责释放该对象。- 其他开头的方法内部会调用
autorelease
,使对象在跨越方法调用边界后依然有效,但过段时间会被释放。要想令调用者持有它,需要执行保留方法才行。
维系这些规则所需的全部内存管理事宜均由ARC自动处理,如:
代码语言:javascript复制// 以new开头,调用者需要持有对象;所以其引用计数需要 1
(Person *)newPerson {
Person *person = [[Person alloc] init]; // alloc开头:引用计数已经 1
return person;
} // 所以ARC什么代码都不需要加
// 不是以“owning prefixs”开头,调用者不需要持有对象;所以其引用计数不需要 1
(Person *)somePerson {
Person *person = [[Person alloc] init]; // alloc开头:引用计数已经 1
return person;
// 所以ARC会在改为类似 return [person autorelease]; 的代码
}
(Person *)doSomething {
Person *personOne = [Person newPerson];
Person *personTwo = [Person somePerson]; // autorelease
// 所以ARC会添加类似 [personOne release]; 的代码
}
ARC会把能互相抵消的retain
、release、autorelease
操作约简。如在发现同一个对象上执行了多次“保留”与“释放”操作,则可以成对移除这两操作。如:
Person *tmp = [Person personWithName:@"momo"];
_myPerson = [tmp retain];
// personWithName: 里的autorelease和后面的retain都是多余的。
// 为了提升性能皆可省去
为了优化代码,在方法中返回自动释放的对象时,调用的是objc_autoreleaseReturnValue
,此函数会检视当前方法返回之后即将要执行的那段代码,若发现那段代码要在返回的对象上执行retain
操作,则会根据当前对象在全局数据结构中设置一个标志位,并不执行autorelease
操作。相似的保留对象调用的是objc_retainAutoreleaseReturnValue
函数,此函数会根据当前对象到全局数据结构中找到刚才那个标志位,若已置位,则不执行retain
操作。(设置并检测标志位,要比调用autorelease
和retain
更快)例如:
// objc_xxx:直接调用C函数,不经过OC的消息派发,速度更快
(Person *)personWithName:(NSSrting *)name {
Person *person = [[Person alloc] init];
person.name = name;
objc_autoreleaseReturnValue(person); //
}
Person *tmp = [Person personWithName:@"Momo"];
_myPerson = objc_retainAutoreleaseReturnValue(tmp); //
31、在dealloc方法中只释放引用并解除监听
要点:
- 在
dealloc
方法里,只应释放指向其他对象的引用,并取消原来订阅的“键值观察”(KVO)或NSNotificationCenter
等通知,不要做其他事情 - 若对象持有文件描述符等系统资源,那么应专门写方法释放此资源。这样的类要和其使用者约定:用完资源后必须调用
close
方法 - 执行异步任务的方法不应在
dealloc
里调用;只能在正常状态下执行的那些方法也不应在dealloc里调用,因为此时对象已处于正在回收的状态了
- (void)dealloc {
CFRelease(coreFoundationObject);
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
代码语言:javascript复制// 为稀有资源添加:创建 和 释放 方法
- (void)open:(NSString *)address;
- (void)close;
代码语言:javascript复制// iOS应用程序终止时调用 UIApplicationDeleagte 的方法:(如:crash时)
- (void)applicationWillTerminate:(UIApplication *)application;
dealloc
方法:
- 不要随便调用其他方法,在这里无论调用什么方法都不太应该,因为对象此时“已近尾声”(in a winding-down state)。若所调用的方法又要异步执行任务或又要继续调用他们自己的某些方法,等到那些任务执行完毕时还行通知当前对象,而系统已经把当前待回收的对象彻底摧毁了。这会导致很多问题,经常crash。
dealloc
方法所在的线程会执行“最终的释放操作”(final release),令对象的保留计数降为0,而某些方法必须在特定的线程里(如:主线程)调用才行。(若在dealloc里调用了哪些方法,则无法保证当前这个线程就是那些方法所需的线程)- 不要调用属性的存取方法,因为有人可能会覆写这些方法,并于其中做一些无法在回收阶段安全执行的操作。另外属性可能正出去“键值观测”(KVO)机制的监控之下,该属性的观察者(observer)可能会在属性值改变时“保留”或使用这个即将回收的对象
32、编写“异常安全代码”时留意内存管理问题
虽然OC只有在发生严重错误导致程序无法继续运行时,才应跑出异常;但如果使用OC 编码或使用了第三方库抛出的异常不受控制时,就需要捕获及处理异常了。
有些系统库也会抛出异常,如:使用KVO时,若注销一个尚未注册的“观察者”,则会抛出异常;
发生异常时应如何管理内存又是个值得研究的问题。在try
块中,如果先保留了某个对象,然后在释放它之前又抛出了异常,此时除非catch块能释放对象,否则就会导致内存泄露。
ARC模式下,不会在finally
块里加代码处理内存泄露问题,因为者需要添加大量的样板代码,会严重影响运行期的性能,即便在不抛异常时也如此。(而且添加的额外代码还会明显增加应用程序的大小。这些副作用都不甚理想)虽说默认不会添加,但可以通过-fobjc-arc-exceptions
这个编译器标志来开启此功能。并且处于OC 模式时编译器会自动把-fobjc-are-exceptions
标志打开。
33、以弱引用避免保留还
要点:
- 将某些引用设置为
weak
,可避免出现“保留环” - weak引用可自动清空,也可以不自动清空。自动清空(automiling)是随着ARC而引入的新特性,由运行期系统来实现。在具备自动清空功能的弱引用上,可随意读取其数据,因为这种引用不会指向已经回收过的对象
34、以“自动释放池块”降低内存峰值
要点:
- 自动释放池排布在栈中,对象收到·消息后,系统将其放入最顶端的池里
- 合理运用自动释放池,可降低应用程序的内存峰值
@autoreleasepool
这种新式写法能创建出更为轻便的自动释放池
系统会自动创建一些线程,如:主线程、GCD机制中的线程,这些线程默认都有自动释放池,每次执行“事件循环”(event loop)时,就会将其清空。
如:man函数中的自动释放池:
代码语言:javascript复制int main(int argc, char *argv[]) {
@autoreleasepool { // 如果不写,那么有UIApplicationMain函数所自动释放的对象,就没有自动释放池可以容纳,系统就会发出警告来表明
return UIApplicationMain(argc, argv, nil, @"XXXAppdelegate");
}
}
如:需要从数据库中读出许多对象
代码语言:javascript复制NSArray *databaseRecords = /* ... */;
NSMutableArray *peoples = [NSMutableArray new];
for (NSDictionary *record in databaseRecords) {
// @autoreleasepool {
Person *person = [[Person alloc] initWithRecord:record];
[peoples addObject:person];
// }
}
Person
的初始化函数也许会像上例那样,再创建出一些临时对象。若记录有很多条,则内存中也会有很多不必要的临时对象,它们本来应该提早回收的。此时增加一个自动释放池即可解决此问题。(及:解开注释部分)
然而是否应该用池来优化效率,完全取决于具体的应用程序。首先的监控内存用量,判断其中有没有需要解决的问题,如果没有完成这一步,那就别记者优化。尽管自动释放池的开销不太大,但毕竟还是有的,所以尽量不要建立额外的自动释放池。
@autoreleasepool
语法还有一个好处:每个自动释放池均有其范围,可以避免无意间误用了那些在清空池后已被系统回收的对象。
35、用“僵尸对象”调试内存管理问题
调式内存管理问题很令人头疼。(因为有些问题不是必现的!!!)
大家都知道,向已回收的对象发送消息是不安全的。这么做有时可以,有时不行。具体可行,完全取决于对象所占内存有没有为其他内容所覆写。而这块内存有没有移做他用,又无法确定。因此,应用程序只是偶尔崩溃。在没用崩溃的情况下,那块内存可能只复用了其中一部分,所以对象中的某些二进制数据依然有效。
还有一种可能,就是那块内存恰好为另外一个有效且存活的对象所占据。这种情况下,运行期系统会吧消息发到新对象那里,而此对象也许能应答,也许不能。如果能,那么程序就不崩溃,可你会觉得奇怪:为什么收到消息的对象不是预想的那个呢?若新对象无法响应选择子,则程序依然会崩溃。
所幸Cocoa
提供了“僵尸对象”(Zombie Object)模式:运行期系统会把所有已经回收的实例转化成特殊的“僵尸对象”,而不会真正回收它们。这种对象所在的核心内存无法重用,因此不可能遭到覆写。僵尸对象收到消息后,会抛出异常,并给出描述。
开启方式:Scheme -> Run -> Diagnostics -> Enable Zombie Objects (勾选)
runtime
发现如果开启该模式,则NSObject
的dealloc
方法会被“调配”(swizzle),从而执行将对象的类改为指向_NSZombie_OriginalClass
类。_NSZombie_
类并未实现任何方法,没有超类,跟NSObject
一样是个“根类”,该类只有一个实例变量isa,所有OC的根类都必须有此变量。由于这个轻量级的类没有实现任何方法,所以发给它的消息需要寄过“完整的消息转发机制”(full forwarding mechanism)。
要点:
- 系统在回收对象时,可以不将其真的回收,而是将其转化为僵尸对象。通过环境变量
NSZombieEnabled
可开启此功能 - 系统会修改对象的
isa
指针,令其指向特说的僵尸类,从而使该对象变为僵尸对象。僵尸类能够响应所有的选择子,响应方式为:打印一条包含消息内容及其接受者的消息,然后终止应用程序
36、不用使用retainCount
OC通过引用计数类管理内存。每个人对象都有一个计数器,其值表明还有多少个其他对象想令此对象继续存活。对象创建好之后,其保留计数大于0。保留与释放操作分别会使计数递增or递减。当技术变为0时,对象就被系统回收了。
retainCount
无用的原因:
- 它所返回的保留计数只是某个给定时间点上的值。该方法并未考虑到系统会稍后把自动释放池清空,因而不会将后续的释放操作从返回值里减去,因此此值未必能真实反映实际的保留计数。
retainCount
可能永远不返回0,因为有时系统会优化对象的释放行为,在保留计数还是1的时候就把它回收了。只有在系统不打算这么优化时,计数值才会递减至0。
如果发现某个对象的内存泄露了,应该检查还有谁仍然保留这个数,并查明为何没有释放此对象。
要点:
- 对象的保留计数看似有用,实则不然,因为任何给定时间点上的“绝对保留计数”(absolute retain count)都无法反映对象声明期的全貌
- 引入ARC之后,
retainCount
方法就正式废止了,在ARC下调用该方法会导致编译器报错
六、块与大中枢派发
块与GCD是当前OC编程的基石。因此,必须理解其工作原理及功能。
可以看这篇:iOS_理解Block(代码块) 底层实现
37、理解“块”这一概念
块其实就是个值,而且自有其相关类型。块类型的语法与函数指针近似。
块的强大之处是:在声明它的范围里,所有变量都可以为其所捕获。有些变量若需在块内修改,需要加上__block
修饰符。
如果块所捕获的变量是对象类型,那么就会自动保留它。系统在释放这个块的时候,也会将其一并释放。
descriptor
:块对象的总体大小;声明了copy
与dispose
两个辅助函数对于的函数指针(在拷贝or丢弃块对象时执行)。
块还会把它所捕获的所有变量都拷贝一份。这些拷贝放在descriptor
变量后边,捕获了多少个变量,就要占用多少的内存空间。
块定义时时存储在栈重的。一旦复制到堆上,块就成了带引用计数的对象了。后续的复制操作都不会真的执行复制,只是递增快对象的引用计数。
要点:
- 块是C、C 、Objective-C中的词法闭包
- 块可接受参数,也可返回值
- 块可以分配在栈or堆上,也可以时全局的。分配在栈上的块可以拷贝到堆里,此时就跟标砖的OC对象一样,具备引用计数了
38、为常用的块类型创建typedef
使用块别名,当需要修改时只需修改块类型即可,无须修改所有使用到的地方。
要点:
- 以
typedef
重新定义块类型,可令块变量用起来更加简单 - 定义新类型时应遵从现有的命名习惯,勿使用其名称与别的类型相冲突
- 不妨为同一个块签名定义多个类型别名,如果要重构的代码使用了块类型的某个别名,那么只需修改相应的
typedef
中的块签名即可,无须改动其他typedef
39、用handler块降低代码分散程度
笔者建议使用同一个块来处理成功与失败的情况:
- 缺点:全部逻辑写在一起会比较长和复杂
- 优点:处理成功响应的过程中可能会发现错误(更灵活)
有时需要在相关时间点指向会掉操作,这种情况也可以使用handler
块。
NSNotificationCenter
就提供了一个参数,可以让调用这指定块在哪个队列里执行。默认是跟通知同一个线程:
- (id)addObserverForName:(NSString *)name
object:(id)object
queue:(NSOperationQueue *)queue
usingBlock:(void(^)(NSNotification *)block
要点:
- 在创建对象时,可以使用内联的
handler
块将相关业务逻辑一并声明 - 在有多个实例需要监控时,如果采用委托模式,那么经常需要根据传入的对象来切换,而该用
handler
块来实现,则可直接将块与相关对象放在一起 - 设计
API
时如果用到了handler
块,那么可以增加一个参数,使调用这可以通过此参数来决定应该把块安排在哪个队列上执行
40、用块应用其所属对象时不要出现保留环
要点:
- 如果块所捕获的对象直接或间接地保留了块本身,那么就得当心保留环问题
- 一定要找个适当的时机接触保留环,而不能把责任推给API的调用者
41、多用派发队列、少用同步锁
1、同步块:@synchronized(self)
(多个属性时不宜这么写)
2、使用锁:NSLock
orNSRecursiveLock
递归锁(线程能够多次持有该说锁,而不会出现死锁deadlock
现象)
3、GCD:串行同步队列
代码语言:javascript复制_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
- (NSString *)name {
__block NSString *localName;
dispatch_sync(_syncQueue, ^{
localName = _name;
});
return localName;
}
- (void)setName:(NSString *)name {
dispatch_barrier_sync(_syncQueue, ^{ // sync 比 async 效率高一些
_name = name;
});
}
在队列中栅栏块必须单独执行,不能与其他块并行。这只对并发队列有意义,因为串行队列中的块总是按顺序逐个执行。并发队列如果发现接下来要处理的块使栅栏块(barrier block),那么就一直等当前所有并发块都执行完毕后才会单独执行这个栅栏块。待栅栏块执行过后,再按正常方式继续向下处理。
最好还是测一测每种做法的性能,然后从中选出最合适当前场景的方案。
要点:
- 派发队列可以用来表述同步语句,这种做法要比使用
@synchronized
块或NSLock
对象更简单 - 将同步与异步派发结合起来,可以实现与普通加锁机制一样的同步行为,而这么做却不会阻塞执行异步派发的线程
- 使用同步队列及栅栏快,可以令同步行为更加高效
42、多用GCD,少用performSelector系列方法
NSObject
定义了几个方法,可以推迟执行方法调用,也可以指定运行方法所用的线程。这些功能原来很有用,但是在出现了大中枢派发及块这样的新技术之后,就显得不那么必要了。虽说有些代码还是会经常用到它们,但笔者劝你还是避开为妙。
如果选择子是在运行时决定的,那么就能体现出此方法的强大之处了:
代码语言:javascript复制SEL selector;
if ( /* some codition */ ) {
selector = @selector(newObject); // 需要释放
} else if ( /* some other codition */ ) {
selector = @selector(copy); // 需要释放
} else {
selector = @selector(someProrperty); // 不需要释放
}
id ret = [object performSelector:selector];
这种编程方式看起来比较灵活,但是在ARC模式下会发出警告:
代码语言:javascript复制warning: performSelector may case a leak because its selector
is unknow [-Warc-performSelector-leaks]
因为编译器并不知道将要调用的选择子是什么,不了解其方法签名及返回值,甚至连私发欧有返回值都不清楚。所以没办法用ARC
的内存管理规则来判定返回值是不是应该释放。鉴于此,ARC
采用了比较谨慎的做法,就是不添加释放操作。因而可能导致内存泄露。
这个问题很容易忽视,而且就算用静态分析器,也很难侦测到随后的内存泄露。(所以需谨慎使用performSelector方法)
大中枢GCD
出现之后,performSelector
系列方法所提供的功能,都可以用GCD实现:
// 延迟执行
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC));
dispatch_after(time, dispatch_get_main_queue(), ^(void){
[self doSomething];
});
// 把任务放到主线程执行
dispatch_async(dipatch_get_main_queue(), ^{
[self doSomething];
});
要点:
preformSelector
系列方法在内存管理方面容易有疏失。它无法确定将要执行的选择子具体是什么,因而ARC编译器也就无法插入适当的内存管理方法performSelector
系列方法所能处理的选择子太过局限了,选择子的返回值类型及发送给方法的参数个数都受到限制- 如果想把任务放在另一个线程上执行,那么最好不要用
preformSelector
系列方法,而是应该把任务封装到块里,然后调用大中枢派发机制的相关方法来实现
43、掌握GCD及操作队列的使用时机
开发者可以把操作以NSOperation
子类的形式放在队列中,而这些操作也能后并发执行。虽说“操作队列”(operation queue)在GCD之前就有了,其中某些设计原理因操作队列而流行,GCD就是基于这些原理构建的。实际上,从iOS4与Mac OS10.6开始,操作队列在底层是用GCD来实现的。
GCD是纯C的API,而操作队列这是OC的对象;在GCD中,任务用块来表示,而块时一个轻量级数据结构,而“操作”(operation)则是个更为重量级的OC对象。
使用NSOperation
及NSOperationQueue
的好处:
- 取消某个操作。已经启动的任务无法取消(GCD队列的任务就无法取消)
- 指定操作间的依赖关系。
- 通过键值观察机制监控
NSOperation
对象的属性。isCancelled
、isFinished
… - 指定操作的优先级。此操作与队列中其他操作之间的优先关系(GCD队列确实有优先级,不过那是针对整个队列的不是针对每个块的)
- 重用NSOperation对象。可以继承该类,存放任何信息。对象在执行时可以充分利用继承而来的各种信息,还可以随意调用其方法。
操作队列有很多地方胜过派发队列:操作队列提供了多种执行任务的方式,而且都是写好的,直接就能使用。开发者不用再编写复杂的调度器,也不用自己来实现取消操作或指定操作优先级的功能,这些事情操作队列都已经实现好了。
要点:
- 在解决多线程与任务管理问题时,派发队列并非唯一方案
- 操作队列提供了一套高层的OC API,能实现纯GCD所具备的绝大部分功能,而且还能完成一些更为复杂的操作,那些操作若改用GCD来实现,则需另外编写代码
44、通过Dispatch Group机制,根据系统资源状况来执行任务
dispatch group
是GCD的一项特性,能够把任务分组。调用者可以等待这组任务执行完毕,也可以在提供会掉函数之后继续往下执行,这组任务完成时,调用者会得到通知。
// 把任务编组
// 方法1:
void dispatch_group_async(dispatch_group_t group,
dispatch_queue_t queue,
dispatch_block_t block);
// 方法2:
void dispatch_group_enter(dispatch_group_t group);
void dispatch_group_leave(dispatch_group_t group);
// 等待group执行完毕:
// 方法1:
long dispatch_group_wait(dispatch_geoup_t group,
dispatch_time_t timeout); // 会阻塞
// 方法2:
void dispatch_group_notify(dispatch_group_t group,
dispatch_queue_t queue,
dispatch_block_t block);
要点:
- 一系列任务可归入一个
dispatch group
中。开发者可以在这组任务执行完毕时获得通知 - 通过
dispatch group
,可以在并发式派发队列里同事执行多项任务,此时GCD会根据系统资源状况来调度这些并发执行的任务。开发者若自己来实现此功能,则需编写大量代码
45、使用dispatch_once来执行只需要运行一次的线程安全代码
代码语言:javascript复制 (id)sharedInstance {
static EOCClass *sharedInstance = nil;
static dispatch_once_t onceToken; // 只初始化一次
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
dispatch_once
更高效,没有使用重量级的同步机制。
要点:
- 经常需要编写“只需执行一次的线程安全代码”(thread-safe single-code execution)。通过GCD所提供的
dispatch_once
函数,很容易就能实现此功能 - 标记应该声明在
static
或global
作用域中,这样的话,在把只需执行一次的块传给dispatch_once
函数时,传进去的标记也是相同的
46、不要使用dispatch_get_current_queue
代码语言:javascript复制- (NSString *)name {
__block NSString *localName;
// 如果调用方法的队列恰好使_syncQueue,则会死锁 (同步线程里加同步事件)
dispatch_sync(_syncQueue, ^{
localName = _name;
});
return localName;
}
代码语言:javascript复制dispatch_queue_t queueA = dispatch_queue_create("com.mo.queueA", NULL);
dispatch_queue_t queueB = dispatch_queue_create("com.mo.queueB", NULL);
dispatch_sync(queueA, ^{
dispatch_sync(queueB, ^{
dispatch_block_t block ^{ /* ... */ };
if (dispatch_get_current_queue() == queueA) { // dispatch_get_current_queue 返回的是queueB
block();
} else {
dispatch_sync(queueA, block); // 所以还是会进入死锁
}
});
});
由于队列间有层级关系,所以“检查当前队列是否为执行同步派发所用的队列”这种办法,并不总是凑效。
在这种情况下,正确的做法是:不要把存取方法做成可重入的,而是应该确保同步操作所有的队列,绝不会访问属性,也就是绝对不会调用name
方法。这种队列只应该用来同步属性。由于派发队列是一种极为轻量的机制,所以不妨为每个每项属性创建专用的同步队列。
GCD提供了一个功能,设定“队列特有数据”,可以把任意数据以键值对的形式关联到队列里。最重要的是,若在当前层级获取不到关联数据时,系统会沿着层级体系向上查找,直到 找到数据 / 到根队列 为止。
代码语言:javascript复制dispatch_queue_t queueA = dispatch_queue_create("com.mo.queueA", NULL);
dispatch_queue_t queueB = dispatch_queue_create("com.mo.queueB", NULL);
dispatch_set_target_queue(queueB, queueA); // B嵌套在A里
static int kQueueSpecific;
CFStringRef queueSpecificValue = CFSTR("queueA");
// 为queueA设置“队列特定值”
dispatch_queue_set_specific(queueA, // 待设置数据的队列
&kQueueSpecific, // key
(void *)queueSpecificValue, // value
(dispatch_funcion_t)CFRelease); // 析构方法
dispatch_sync(queueB, ^{
dispatch_block_t block = ^{
NSLog(@"No deadlock!");
};
CFStringRef retrievedValue = dispatch_get_specific(&kQueueSpecific); // 获取队列特定值
if (retrievedValue) { // 在A队列里
block();
} else {
dispatch_sync(queueA, block);
}
});
要点:
dispatch_get_current_queue
函数的行为常常与开发者所预期的不同。此函数已经废弃,只应做调试之用- 由于派发队列是按层级来组织的,所以无法单用某个队列对象来描述“当前队列”这一概念
Dispatch_get_current_queue
函数用于解决由不可重入的代码所引发的死锁,然而能用此函数解决的问题,通常该用“队列特定数据”来解决
七、系统框架
47、熟悉系统框架
框架:将一些列代码封装为动态库,并在其中放入描述其接口的头文件。
主要框架:
Foundation
:NSObject
、NSArray
、NSDictionary
。。。(用NS前缀),还提供了collection
等基础核心功能,还提供了字符串处理这样的复杂功能。如:NSLinguisticTagger可以解析字符串并找到其中的全名名词、动词、代词等。CoreFoundation
:Foundation
框架中许多功能,都可以在此框架中找到对应的C语言API。“无缝桥接”(toll-free bridging)功能可以把此框架中的C语言数据结构平滑转换为Foundation
中的OC对象,也可以方向转换。
其他框架:
CFNetwork
:提供了C语言级别的网络通信能力,将“BSD套接字”(BSD socket)抽象成易于使用的网络接口。而Foundation则将该框架里的部分内容封装为OC语言接口,以便进行网络通信,如:NSURLConnection
从URL
中下载数据CoreAudio
:提供C语言API可用来操作设备上的音频硬件。此框架属于比较难用的那种,因为音频处理本身就很复杂。所幸由这套API中可以抽象除另外一套OC式API,用后者来处理音频问题会简单些AVFouncation
:提供OC对象可用来回放并录制音频及视频,如:在UI视图类里播放视频- CoreData:提供OC接口可将对象放入数据库,便于持久保存。处理数据的获取及存储事宜,且可跨越Mac OS X及iOS平台
CoreText
:提供C语言接口可以高效执行文字排版及渲染操作
可以看出OC编程一项重要特点:经常要使用底层C语言API,好处是可以绕过OC的运行期系统,从而提升指向速度。当然ARC只负责OC对象,所以使用这些API时尤其需要注意内存管理问题。
核心UI框架:Mac OS X的是AppKit
、iOS的是UIKIt
,都提供了构建在Foundaton
与CoreFoundation
之上的OC类。
CoreAnimation
:OC写成,提供了一些工具,UI框架用这些工具来渲染图形并播放动画。CoreAnimation
本身不是框架,是QuartzCore框架的一部分。
CoreGaphics
:C语言写成的框架,提供了2D渲染所必备的数据结构与函数。其中定义了:CGPoint
、CGSize
、CGRect
等数据结构。
要点:
- 许多系统框架都可以直接使用。其中最重要的是
Foudation
与CoreFoundation,这两个框架提供了构建应用程序所需的许多核心功能 - 很多常见任务都能用框架来做,例如音频、视频处理、网络通信、数据管理等
- 请记住:用纯C写成的框架于用OC写成的一样重要,若想成为优秀的OC开发者,应该掌握C语言的核心概念
48、多用块枚举,少用for循环
1、for循环
代码语言:javascript复制// 遍历NSArray
for (int i = 0; i < anArray.count; i ) {
id object = anArray[i];
}
// 遍历NSDictionary
NSArray *keys = [aDic allKeys];
for (int i = 0; i < keys.count; i ) {
id key = keys[i];
id value = aDic[key];
}
// 遍历NSSet
NSArray *objects = [aSet allObjects];
for (int i = 0; i < objects.count; i ) {
id object = objects[i];
}
创建附加数组会有额外开销,而且还会多创建一组对象,它会保留collection
中的所有元素对象。释放时这些附加对象也要释放,这样就调用了一些本来不需要执行的方法。其他各种遍历方式都无须创建这种中介数组。
2、使用OC1.0的NSEnumerator
来遍历
NSEnumerator是一个抽象基类,其中自定义了两个方法,供子类实现:
代码语言:javascript复制- (NSArray *)allObjects;
- (id)nextObjects; // 返回枚举里的下一个对象
Foundation
框架中内建的collection
类都实现了这种遍历方式:`
// 遍历NSArray
NSEunmerator *enumerator = [anArray objectEnumerator]; // reverseObjectEnumerator 方向遍历
id object;
while ((object == [enumerator nextObject]) != nil) {/*...*/}
// 遍历NSDictionary
NSEnumerator *enumerator = [aDic keyEnumerator];
id key;
while ((key = [enumerator nextObject]) != nil) {
id value = aDic[key];
}
// 遍历NSSet
NSEunmerator *enumerator = [aSet objectEnumerator];
id object;
while ((object == [enumerator nextObject]) != nil) {/*...*/}
3、快速遍历
OC2.0引入了快速遍历,为for循环开设了in关键字。从而大幅简化了遍历collectin所需的语法:
代码语言:javascript复制// 遍历NSArray
for (id object in anArray) {/*...*/}
// 遍历NSDictionary
for (id key in anDic) {
id value = aDic[key];
}
// 遍历NSSet
for (id object in aSet) {/*...*/}
代码语言:javascript复制支持快速遍历,遵从`NSFastEnumeration`协议就好,只有一个协议方法:
代码语言:javascript复制- (NSinterger)countBeyEnumeratingWithState:(NSFastEnumerationState *)state
objects:(id *)stackbuffer
count:(NSUInteger)length;
NSEnumerator
对象也实现了NSFastEnumeration
协议,所以能用来执行方向遍历:
for (id object in [anArray reverseObjectEnumerator]) {/*...*/}
4、基于块的遍历方式(遍历时可以获取更堵信息)
代码语言:javascript复制NSArray *anArray = [NSArray array];
[anArray enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if (shouldStop) {
*stop = YES; // 优雅的终止遍历(其他的遍历可以用break终止)
}
}];
NSDictionary *aDic = [NSDictionary dictionary];
[aDic enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
if (shouldStop) {
*stop = YES;
}
}];
NSSet *aSet = [NSSet set];
// 可以修改方法签名,以免进行类型转换操作
[aSet enumerateObjectsUsingBlock:^(NSString *obj, BOOL * _Nonnull stop) {
if (shouldStop) {
*stop = YES;
}
}];
可以利用另一个版本指向方向遍历、并发遍历:
代码语言:javascript复制// NSEnumerationConcurrent 并发的方式遍历
// NSEnumerationReverse 方向遍历
[anArray enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
}];
要点:
- 遍历
collection
有四种方式。最基本的办法是for循环,其次是NSEnumerator
遍历法及快速遍历法。最新、最先进的方式则是“块枚举法” - “块枚举法”本身就能通过GCD来并发执行遍历操作,无须另行编写代码。而采用其他遍历方式则无法轻易实现这一点
- 若提前知道待比啊你了的
collection
含有何种对象,则应该修改块签名,指出对象的具体类型
49、对自定义其内存管理语义的collection使用无缝桥接
代码语言:javascript复制NSArray *anNSArray = @[@1, @2, @3];
CFArrayRef aCFArray = (__bridge CFArrayRef)anNSArray;
CFArrayGetCount(aCFArray); // array.count
// __bridge: ARC仍拥有该OC对象的所有权
// __bridge_retained: ARC交出该OC对象的所有权,之后需要 CFRealease(aCFArray) 来释放
NSArray *anNSArray = (__bridge_transfer)aCFArray; // ARC获得所有权
无缝桥接必要性:因为Foundation
框架中OC类所具备的某些功能,是CoreFoundation
框架中的C语言数据结构所不具备的,反之亦然。
(具体使用,这里不再赘述了)
要点:
- 通过无缝桥接技术,可以在
Foundation
框架中昂的OC对象与CoreFoundaton
框架中的C语言数据结构之间来回转换 - 在
CoreFoundation
层面创建collection
时,可以指定许多回调函数,这些函数表示此collection应如何处理其元素。然后可运用无缝桥接技术,将其转换成具备特殊内存管理语义的OCcollection
50、构建缓存时选用NSCache而非NSDictionary
NSCache
:当系统资源将耗尽时,自动删减缓存(若用普通字典,需要自己写挂钩,在“低内存”警告时通知删减缓存)。 还会先行删减“最久未使用的”对象,不会“拷贝”键,而是会“保留”它(当键不支持拷贝时很合适)。是线程安全的。可以设置缓存 对象总数 和 “总开销”。
NSCache
搭配NSPureableData
使用:
typedef void(^EOCNetworkFetcherCompletionHander)(NSData *data);
NSCache *_cache;
- (instancetype)init {
self = [super init];
if (self) {
_cache = [NSCache new];
_cache.countLimit = 100; // 100 URLs
_cache.totalCostLimit = 5 * 1024 * 1024; // 5MB
}
return self;
}
- (void)downloadDataForUrl:(NSURL *)url {
NSPurgeableData *cachedData = [_cache objectForKey:url];
if (cachedData) { // Cache hit
// cachedData.isContentDiscarded // 相关内存是否已释放
[cachedData beginContentAccess]; // 告诉它不应丢弃自己所占用的内存
[self useData:cachedData];
[cachedData endContentAccess]; // 告诉它在必要时可以丢弃自己所占用的内存
} else { // Cache miss
EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
[fetcher startWithCompletionHander:^(NSData *data){
NSPurgeableData *purgeableData = [NSPurgeableData dataWithData:data];
[_cache setObject:purgeableData forKey:url cost:purgeableData.length];
[self useData:purgeableData];
[purgeableData endContentAccess];
}];
}
}
要点:
- 实现缓存时应选用
NSCache
而非NSDictionary
对象。因为NSCache
可以提供优雅的自动删减功能,而且是“线程安全的”,此外,它与字典不同,并不会拷贝键值 - 可以给
NSCache
对象设置上限,用以限制缓存中的对象总个数,但是绝不要把这些设置当成可靠的“硬限制”,他们仅对NSCache起指导作用 - 将
NSPurgeableData
与NSCache
搭配使用,可实现自动清楚数据的功能。及当NSPurgeableData
对象所占用内存被系统丢弃时,该对象自身也会从缓存中移除 - 如果缓存使用得当,那么应用程序的响应速度就能提高。只有那种“重新计算起来很费事的”数据,才值得放入缓存,如:需要从网络获取或从磁盘读取的数据
51、精简load与initialize的实现代码
load
:当包含类或分类的程序库载入系统时,就会执行此方法。iOS指应用程序启动时。load
方法中使用其他类时不安全的(如:其他类又用到了当前类,则无法正确加载了)。整个应用程序在执行load
方法时都会阻塞(不要在里面等待锁/加锁)。总之,能不做的事情就别做。
initialize
:在程序首次用该类之前调用,且只调用一次。它时运行期系统来调用的,绝不应该通过代码直接调用。运行期系统在执行该方法时处于正常状态,可以安全使用并调用任意类中的任意方法。运行期系统会确保其在“线程安全的环境”中执行。遵循继承规则。
只应用来设置内部数据,不应调用其他方法,即便时本类自己的方法,也最好别调用。
initialize
需要保持精简的原因:
- 对某个类而言,任何线程都可能初次使用到它,若碰巧时UI线程,那么初始化期间会一直阻塞,导致应用无法响应。
- 开发者无法控制类的初始化时机。不能令代码依赖特定的时间点,否则会很危险
- 若某个了实现很复杂,那么其中可能直接或间接用到其他类。若那些类尚未初始化,则系统会迫使其初始化。其他类的初始化又可能依赖本类的某些数据。代码就无法正常运行了。
还有些详情可看这篇:iOS_Extension、Category、load、initialize
要点:
- 在加载阶段,如果类实现了
load
方法,那么系统就会调用它。分类也可以定义此方法,类的load
方法要比分类中的先调用。与其他方法不同,load
方法不参与覆写机制。 - 首次使用某个类之前,系统会向其发送
initialize
消息。由于此方法遵从普通的覆写规则,所以通常应该在里面判断当前要初始化的是哪个类 load
与initialize
方法都应该实现的精简一些,这有助于保持应用程序的响应能力,也能减少引入“依赖环”的几率- 无法在编译器设定的全局常亮,可以放在
initialize
方法里初始化
52、别忘了NSTimer会保留其目标对象
可以看这篇:iOS_定时器:NSTimer、GCDTimer、DisplayLink
要点:
NSTimer
对象会保留其目标,直到计时器本身失效为止,调用invalidate
方法可令计时器失效,另外,一次性的计时器在触发完任务之后也会失效- 反复执行任务的计时器,很容易引入保留环,如果这种计时器的目标又保留了计时器本身,那肯定会导致保留环。这种关系,可能直接发送,也可能通过对象图里的其他对象间接发生
- 可以扩充
NSTimer
的功能,用“块”来打破保留环。不过除非NSTimer
将来在公共接口里提供此功能,否则需创建分类,将相关实现代码加入其中