第一条 了解Objective-C预言起源
起源:Smalltalk
类型:使用消息结构的语言
区别:使用消息结构的语言,其运行时所应执行的代码由运行环境来决定;而使用函数调用的语言则由编译器决定。
对象分配在堆空间上,指针分配在栈空间上
结构体分配在栈空间(CGRect)
第二条 在类的头文件中尽量少引入其他头文件
用OC编写任何类几乎都需要引入Foundation.h。
向前声明:@class EOCEmployer;
使用#import而非#include互相引用时不会死循环
要点:
1、除非有必要,不要引入头文件。应在类的头文件中使用向前声明来提及别的类,在实现文件中引入那个类的头文件。可以尽量降低类之间的耦合。
2、无法使用向前声明,如声明某个类遵循一项协议。尽量把“遵循某协议”的声明移到“class-continuation分类”中。若不行,就把协议单独放在一个头文件中,再引入。
第三条 多用字面量语法,少用与之等价的方法
字面量语法只是一种“语法糖”
疑问:mrc下字面量创建的对象到底有没有reatain+1
使用字面量语法创建出来的字符串、数组、字典对象都是不可变的,若想要可变版本的对象,需要复制一份
要点:
1、使用字面量语法来创建字符串、数值、数组、字典。
2、通过取下标操作来访问数组下标或字典中的键所对应的元素
3、用字面量语法创建数组或字典时,若值中有nil,则会抛出异常。因此,务必确保值里不含nil
第四条 多用类型常量,少用#define 预处理指令
#define ANIMATION_DURATION 0.3
static const NSTimeInterval kAnimationDuration = 0.3;
若不打算公开某个常量,则应将其定义在使用该常量的实现文件里。static修饰符意味着该变量仅在定义此变量的编译单元可见。在OC语境下,“编译单元”指每个类的实现文件(以.m为后缀名)。假如声明此变量时不加static,则编译器会为它创建一个“外部符号”。此时若是另一个编译单元中也声明了同名变量,那么编译器就会抛出错误。
如果一个变量既声明为static,又声明为const,编译器根本不会创建符号,而是会像#define一样,把所遇到的变量都替换成常值。
常量放在“全局符号表中”:
//In the header file
extern NSString *const EOCStringConstant;
//In the implementation file
NSString *const EOCStringConstant = @"VALUE";
第五条 用枚举表示状态、选项、状态吗
要点
如果把传递给某个方法的选项表示为枚举类型,而多个选项又可同时使用,那么就将各选项值定义为2的幂,以便通过按位或操作将其组合起来。
用NS_ENUM与NS_OPTIONS宏定义枚举类型,并指明底层数据类型。这样可以确保枚举是用开发者所选的底层数据类型实现出来,而不会采用编译器所选的类型。
在处理枚举类型的switch语句中不要实现default分支。这样的话,加入新枚举之后,编译器就会提示开发者:switch语句并未处理所有枚举。
第六条 理解 “属性” 这一概念
编译器会把“点语法”转换为对存取方法的调用,使用“点语法”的效果与直接调用存取方法相同。
如果使用了属性,编译器会自动编写访问这些属性所需的方法,此过程叫“自动合成(autosynthesis)”。这个过程由编译器在编译期执行,所以编辑器里看不到“合成方法(synthesied method)”的源代码。除了生成方法代码,编译器还要自动向类中添加适当类型的实例变量,并且在属性名前面加下划线,以此作为实例变量的名字。也可以在类的实现代码里通过@synthesize语法来指定实例变量的名字
@implementation EOCPerson
@synthesize firstName = _myFirstName;
@synthesize lastName = _myLastName;
@end
有一种办法能阻止编译器自动合成存取方法,就是使用@dynamic关键字,它会告诉编译器:不要自动创建实现属性所用的实例变量,也不要为其创建存取方法。而且,在编译访问属性的代码时,即使编译器发现没有定义存取方法,也不会报错,它相信这些方法能在运行期找到。
@interface EOCPerson : NSManagedObject
@property NSString *firstName;
@property NSString *lastName;
@end
@implementation EOCPerson
@dynamic firstName, lastName;
@end
属性的特质:原子性,读/写权限,内存管理语义,方法名
要点
用@property语法来定义对象中所封装的数据
通过“特质”来指定存储数据所需的正确语义
在设置属性所对应的实例变量时,一定要遵从该属性所声明的语义
开发iOS程序时应该使用nonatomic属性,因为atomic属性会严重影响性能
第七条 在对象内部尽量直接访问实例变量
直接访问实例变量,不会触发 kvo通知。
在写入实例变量时,通过设置方法来做,在读取实例变量时,则直接访问。
在初始化方法中设置属性值应该直接访问实例变量,因为子类可能会覆写设置方法
delloc中直接通过
惰性化初始技术需要采用存取方法。
第八条 理解“对象等同性” 这一概念
使用NSObject协议中声明的 isEqual 方法来判断两个对象的等同性
NSObject协议中两个用于判断等同性的关键方法:
- (BOOL)isEqual:(id)object;
- (NSUInterger)hash;
等同性约定
如果 isEqual: 判定两个对象相等,那么hash方法也必须返回同一个值.但是,如果两个对象的hash方法返回同一个值, isEqual: 未必会认为两者相等
有一种情况要注意,在容器中放入可变类对象时,把某个对象放入collection之后,就不应再改变其哈希码了。
要点:
若想检测对象的等同性,请提供“isEquel:”与hash方法
相同的对象必须具有相同的哈希码,但是两个哈希码相同的对象却未必相同
第九条 以“类族模式”隐藏实现细节
类族可以隐藏“抽象基类”背后的实现细节。
系统框架有很多类族。大部分collection类都是类族。
在传统的类族模式中,通常只有一个类具备“公共借口”,这个类就是类族中的抽象基类
Cocoa中NSArray这样的类族来说,新增子类需遵守几条规则:
自类应该继承自类族中的抽象基类
子类应该定义自己的数据存储方式
子类应当覆写超类文档中指明需要覆写的方法
第十条 在既有类中使用关联对象存放自定义数据
设置关联对象
void objc_setAssociatedObject(id object, void *key, id value, objc_AssociationPolicy policy)
取关联对象值
id objc_getAssociatedObject(id object, void *key)
移除指定对象的全部关联对象
void objc_removeAssociatedObjects(id object)
要点
定义关联对象可指定内存管理语义,用以模仿定义属性时所采用的“拥有关系”与“非拥有关系”
只有在其他做法不可行时才应选用关联对象,因为这种做法通常会引入难于查找的bug
第十一条 理解objc_msgSend的作用
C语言使用“静态绑定”,在编译期就能决定运行时调用的函数
在Objective-C中,如果向某个对象传递消息,那就会使用动态绑定机制来决定需要调用的方法。在底层,所有方法都是普通的C语言函数,然而对象收到消息后,究竟该调用哪个方法则完全于运行期决定,甚至可以在程序运行时改变,这些特性使得Objective-C成为一门真正的动态语言。
id returnValue = [someObject messageName:parameter];
someObject: 接受者(receiver)
messageName: : 选择子(selector)
选择子与参数 合起来称为“消息(message)”
编译器看到此消息后,将其转换为一条标准的C语言函数调用,其原型
void objc_msgSend(id self, SEL cmd, ...)
转换后:
id returnValue = objc_msgSend(someObject, @selector(messageName:), parameter);
该方法需要在接收者所属的类中搜寻其“方法列表”,如果能找到与选择子名称相符的方法,就跳至其实现代码。若找不到,就沿着继承体系继续向上查找,等找到合适的方法之后再跳转,如果最终还是找不到相符的方法,就执行“消息转发(message forwarding)”
objc_msgSend会将匹配结果缓存在“快速映射表”(fast map)里,每个类都有这样一个缓存,稍后还向该类发送与选择子相同的消息,会快很多,但还是不如“静态绑定的函数操作”那样迅速。
消息派发并非应用程序的瓶颈所在。假如是瓶颈,可以只编写纯C函数
objc_msgSend等函数一旦找到应该调用的方法实现后,就会“跳转过去。”之所以能这样,因为Objective-C对象的每个方法都可以视为简单的C函数,其原型如下
<return_type> Class_selector(id self, SEL _cmd, ...)
每个类里面都有一张表,其中的指针都指向这种函数,而选择子的名称是查表时所用的“键”。objc_msgSend正式通过这张表格来寻找应该执行的方法并跳至实现的。
要点
发给某对象的全部消息都要由“动态消息派发系统”(dynamic message dispatch system)来处理,该系统会查出对应的方法,并执行其代码。
第十二条 理解消息转发机制
当对象接收到无法解读的消息后,就会启动“消息转发(message forwarding)”机制,程序员可经由此过程告诉对象应该如何处理未知消息。
消息转发分为两大阶段。第一阶段先征询接收者,所属的类,看其是否能动态添加方法,以处理当前这个“未知的选择子(unknown selector)”,这叫做“动态方法解析”(dynamic method resolution)。
第二阶段涉及“完整的消息转发机制”(full forwarding mechanism)。如果运行期系统已经把第一阶段执行完了,那么接收者自己就无法再以动态新增方法的手段来响应包含该选择子的消息了。此时,运行期系统会请求接收者以其他手段来处理与消息相关的方法调用。这又细分为两小步
a、首先,接收者看看有没有其他对象能处理这条消息。若有,则运行期系统会把消息转给那个对象,消息转发过程结束。
b、若没有“备援的接收者”(replacement receiver),则启动完整的消息转发机制,运行期系统会把与消息有关的全部细节都封装到NSInvocation对象中,再给接收者最后一次机会,令其设法解决当前还未处理的这条消息。
对象在收到无法解读的消息后,首先将调用其所属类的下列类方法:
+ (BOOL)resolveInstanceMethod:(SEL)selector
+ (BOOL)resolveClassMethod:(SEL)selector
使用这种方法的前提是:相关方法的实现代码已经写好,只等着运行的时候动态插在类里面就可以了。此方案常用来实现@dynamic 属性。
备援接收者:当前接收者还有第二次机会能处理未知的选择子,在这一步中,运行期系统会问它:能不能把这条消息转给其他接收者来处理。该步骤对应的处理方法如下:
- (id)forwardingTargetForSelector:(SEL)selector
完整的消息转发
- (void)forwardInvocation:(NSInvocation *)invocation
这个方法可以实现得很简单:只需改变调用目标,使消息在新目标得以调用即可。然而这样实现出来的方法与“备援接收者”方案所实现的方法等效,所以很少有人采用这么简单的实现方式。比较有用的实现方式为:在出发消息前先以某种方式改变消息内容,比如追加另外一个参数,或者改变选择子,等等。
接收者在每一步中均有机会处理消息。步骤越往后,处理消息的代价就越大。
第十三条 用“方法调配技术” 调试 “黑盒方法”
类的方法列表会把选择子的名称映射到相关的方法实现上,使得“消息派发系统”能够根据此找到应该调用的方法。这些方法均以函数指针的形式来表示,这种指针叫做IMP,其原型如下:
id (*IMP)(id, SEL, ...)
交换方法实现,可用下列函数:
void method_exchangeImpentations(Method m1, Method m2)
此函数的两个参数表示待交换的两个方法实现,而方法实现则可通过下列函数获得
Method class_getInstanceMethod(Class aClass, SEL aSelector)
要点:
使用另一份实现来替换原有的方法实现,这道工序叫做“方法调配”,开发者常用此技术向原有实现中添加新功能。
一般来说,只有调试程序的时候才需要在运行期修改方法实现,这种做法不宜滥用
第十四条 理解“类对象”的用意
描述Objective-C对象所用的数据结构定义在运行期程序库的头文件里,id类型本身也在定义在这里:
typedef struct objc_object {
Class isa;
} *id;
typedef struct objc_class *Class;
struct objc_class {
Class isa;
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_cache *cache;
struct objc_protocol_list *protocols;
}
每个类仅有一个“类对象”,而每个“类对象”仅有一个与之相关的“元类”
isMemberOfClass: 判断对象是否为某个特定类的实例
isKindOfClass: 对象是否为某类或其派生类的实例
类对象是“单例”,在应用程序范围内,每个类的Class仅有一个实例。
要点
每个实例都有一个指向Class对象的指针,用以表明其类型,而这些Class对象则构成类的集成体系
如果对象类型无法在编译器确定,那么就应该使用类型信息查询方法来探知
尽量使用类型信息查询方法来确定对象类型,而不要直接比较类对象,因为某些对象可能实现了消息转发功能
第十五条 用前缀避免命名空间冲突
Objective-C没有其他语言语言那种内置的命名空间机制
使用Cocoa创建应用程序时一定要注意,Apple宣称其保留使用所有“两字母前缀”的权利,所以你自己选用的前缀应该是三字母的。
要点
选择与你的公司,应用程序或二者皆有关联之名称作为类名的前缀,并在所有代码中均使用这一前缀,并在所有代码中均使用这一前缀
若自己所开发的程序库中用到了第三房库,则应为其中的名称加上前缀
第十六条 提供“全能初始化方法”
如果创建类实例的方法不止一种,那么这个类就会有多个初始化方法。要再其中选定一个作为全能初始化方法。
要点
在类中提供一个全能初始化方法,并于文档里指明。其他初始化方法均应调用此方法
若全能初始化方法与超类不同,则需覆写超类中的对应方法
如果超类的初始化方法不适用于子类,那么应该覆写这个超类方法,并在其中抛出异常
第十七条 实现description方法
- (NSString *)description {
return [NSString stringWithFormat:@"<%@: %p, \"%@ %@\">", [self class], self, _firstName, _lastName];
}
//output:
// person = <EOCPerson: 0x7fb249c030f0, "Bob Smith">
要点
实现description方法返回一个有意义的字符串,用以描述该实例
若想在调试时打印出更详细的对象描述信息,则应实现debugDescription方法
第十八条 尽量使用不可变对象
应该尽量把对外公布出来的属性设为只读,而且只在确有必要时才将属性对外公布。
有时可能想修改封装在对象内部的数据,但是却不想令这些数据为外人所改动。这种情况下,通常做法是在对象内部将readonly属性重新声明为readwrite
在定义类的公共API时,还要注意一件事情:对象里表示各种collection的那些属性究竟应该设成可变的还是不可变的。例如,用某个类表示个人信息,该类里还存放了一些引用,指向此人的诸位朋友。把这个人的全部朋友放在一个列表里,并将其做成属性,可以添加或删除此人的朋友,这个属性需要用可变的set来实现。在这种情况下,通常应该提供一个readonly属性供外界使用,该属性返回不可变的set,而此set是内部不可变set的一份拷贝
要点
尽量创建不可变的对象
若某属性仅可于对象内部修改,则在“class-continuation分类”中将其由readonly属性扩展为readwrite
不要把可变的collection作为属性公开,而应提供相关方法,以此修改对象中的可变collection
第十九条 使用清晰而协调的命名方式
不要吝于使用长方法名。把方法名起得稍微长一点,可以保证其能准确传达出方法所执行的任务
给方法命名时的注意事项:
1、如果方法的返回值是新创建的,那么方法名的首个词应是返回值的类型,除非前面还有修饰语,例如localizedString。属性的存取方法不遵循这种命名方式,因为一般认为这些方法不会创建新对象,即便有时返回内部对象的一份拷贝,我们也认为那相当于原有的对象。这些存取方法应该按照其所对应的属性来命名
2、应该把表示参数类型的名词放在参数前面。
3、如果方法要在当前对象上执行操作,那么就应该包含动词,若执行操作时还需要参数,则应该在动词后面加上一个或多个名词。
4、不要使用str这种简称,应该用string这样的全称
5、Boolean属性应加is前缀。如果某方法返回属性的Boolean值,那么应该根据其功能,选用has或is当前缀。
6、将get这个前缀留给那些借由“输出参数”来保存返回值的方法。
第二十条 为私有方法名加前缀
与公有方法不同,私有方法不出现在接口定义中。有时可能要在“class-continuation分类”里声明私有方法,然而最近修订的编译器已经不需要在使用方法前必须先声明了。所以说,私有方法一般只在实现的时候声明。
要点
给私有方法的名称加上前缀,这样可以很容易地将其同公共方法区分开
不要单用一个下划线做私有方法前缀,因为这种做法是预留给苹果公司用的
第二十一条 理解Objective-C错误模型
ARC在默认情况下不是异常安全的
Objective-C语言现在所采用的办法是:只在极其罕见的情况下抛出异常,异常抛出之后,无须考虑恢复问题,而且应用程序此时也应该退出。这就是说不用编写复杂的异常安全代码。
在出现不严重的错误时,Objective-C语言所用的编程范式为:令方法返回nil/0,或是使用NSError
要点
只有发生了可使整个应用程序崩溃的严重错误时,才应使用异常
在错误不严重情况下,可以指派“委托方法(delegate method)”来处理错误,也可以把错误信息放在NSError对象里,经由“输出参数”返回给调用者
第二十二条 理解NSCopying协议
如果想令自己的类支持copy操作,就要实现NSCopying协议,该协议只有一个方法
- (id)copyWithZone:(NSZone *)zone
以前开发程序时,会据NSZone把内存分成不同的区(zone),而对象会创建在某个区里面。现在不用了,每个程序只有一个区:默认区(default zone)。所以说,尽管必须实现这个方法,但是不必担心其中的zone参数。
copy方法由NSObject实现,该方法只是以默认区为参数调用copyWithZone:。我们总是想覆写copy方法,其实真正需要实现的却是copyWithZone:方法。
要点
如果自定义的对象分为可变版本与不可变版本,那么就要同时实现NSCopying与NSMutableCopying协议
复制对象时需决定采用浅拷贝还是深拷贝,一般情况下应该尽量执行浅拷贝
如果你所写的对象需要深拷贝,那么可考虑新增一个专门执行深拷贝的方法
第二十三条 通过委托与数据源协议进行对象间通信
Objective-C开发者广泛使用一种名叫“委托模式(Delegate pattern)”的变成设计模式来实现对象间通信的通信,该模式的主旨时:定义一套接口,某对象若想接受另一个对象的委托,则需遵从此接口,以便成为其“委托对象(delegate)”。而这“另一个对象”则可以给委托对象回传一些信息,也可以在发生相关事件时通知委托对象
@protocol EOCNetworkFetcherDelegate
- (void)networkFetcher:(EOCNetworkFetcher *)fetcher didReceiveData:(NSData *)data;
- (void)networkFetcher:(EOCNetworkFetcher *)fetcher didFailWithError:(NSError *)error;
@end
有了协议后,类就可以用一个属性来存放其委托对象了
@interface EOCNetworkFetcher : NSObject
@property (nonatomic, weak) id <EOCNetworkFetcherDelegate> delegate;
@end
实现委托对象的的办法:声明某个类遵从委托协议,然后把协议中想实现的那些方法在类里实现。可以在接口中声明,也可以在“class-continuation分类”中声明。
@interface EOCDataModel () <EOCNetworkFetcherDelegate>
@end
@implementation EOCDataModel
- (void)networkFetcher:(EOCNetworkFetcher *)fetcher didReceiveData:(NSData *)data {
}
- (void)networkFetcher:(EOCNetworkFetcher *)fetcher didFailWithError:(NSError *)error {
}
@end
委托对象上调用可选方法
NSData *data = xxx;
if ([_delegate respondsToSelector:@selector(networkFetcher:didReceiveData:)) {
[_delegate networkFetcher:self didReceiveData:data];
}
通常把委托对象能否响应某个协议方法这一信息缓存起来,以优化程序效率
第二十四条 将类的实现代码分散到便于管理的数个分类之中
要点
使用分类机制把类的实现代码划分成易于管理的小块
将应该视为“私有”的方法归入名为Private的分类中,以隐藏实现细节
第二十五条 总是为第三方类的分类名称加前缀
分类中的方法是直接添加在类里面,它们就好比这个类中的固有方法。将分类方法加入类中这一操作是在运行期系统加载分类时完成的。运行期系统会把分类中所实现的每个方法都加入类的方法列表中。如果类中本来就有此方法,而分类又实现了一次,那么分类中的方法会覆盖原来那一份实现代码。实际上可能会发生很多次覆盖,比如某个分类中的方法覆盖了“主实现”中的相关方法,而另一个分类中的方法又覆盖了这个分类中的方法。多次覆盖的结果以最后一个分类为准。
要点:
向第三方类中添加分类时,总应该给其名称加上你专有的前缀
向第三方类中添加分类时,总应该给其中的方法名加上你专有的前缀
第二十六条 勿在分类中声明属性
尽管从技术上说,分类里也可以声明属性,但这种做法还是要尽量避免。原因在于,除了“class-continuation分类”之外,其他分类都无法向类中新增实例变量,因此,它们无法把实现属性所需的实例变量合成出来。
开发者需要在分类中为属性实现存取方法。此时可以把存取方法声明为@dynamic,也就是说,这些方法等到运行期再提供,编译器目前是看不见的。如果决定使用消息转发机制在运行期拦截方法调用,并提供实现,那么或许可以采用这种方法。
关联对象能够解决在分类中不能合成实例变量的问题。
#import <objc/runtime.h>
static const char *kFriendsPropertyKey = "kFriendsPropertyKey";
@implementation EOCPerson (Friendship)
- (NSArray *)friends {
return objc_getAssociatedObject(self, kFriendsPropertyKey);
}
- (void)setFriends:(NSArray *)frends {
objc_setAssociatedOjbect(self, kFriendsPropertyKey, friends, OBJC_ASSOCIATION__RETAIN_NONATOMIC);
}
本例中,正确做法是把所有属性都定义在主接口里。类所封装的全部数据都应该定义在主接口中,这里是唯一能够定义实例变量(也就是数据)的地方。而属性知识定义实例变量及相关存取方法的语法糖,所以也应遵循同实例变量一样的规则。至于分类机制,则应将其理解为一种手段,目标在于扩展类的功能,而非封装数据。
要点
把封装数据所用的全部属性都定义在主接口里
在“class-continuation分类”之外的其他分类中,可以定义存取方法,但尽量不要定义属性
第二十七条 使用“class-continuation分类”隐藏实现细节
Objective-C动态消息系统的工作方式决定了其不可能实现真正的私有方法或私有变量
“class-continuation分类”和普通分类不同,它必须定义在其所连续的那个类的实现文件里。其重要之处在于,这是唯一能声明实例变量的分类,而且此分类没有特定的实现文件,其中的方法都应该定义在类的主实现文件里。与其他分类不同,“class-continuation分类”没有名字
@interface EOCPerson () {
NSString *_anInstanceVariable;
}
//Method declarations here
@end
@implementation EOCPerson {
int _anotherInstanceVariable;
}
//Method implementations here
@end
"class-continuation分类"还有一种合理用法,就是将public接口中声明为“只读”的属性扩展为“可读写”
只会在类的实现中用到的私有方法也可以声明在“class-continuation分类”中。
新版编译器不强制要求开发者在使用方法前必须先声明。
若对象所遵从的协议只应视为私有,则可在“class-continuation分类”中声明
第二十八条 通过协议提供匿名对象
要点
协议可在某种程度上提供匿名类型。具体的对象类型可以淡化成遵从某协议的id类型,协议里规定了对象所应实现的方法
使用匿名对象来隐藏类型名称
如果具体类型不重要,重要的是对象能够响应(定义在协议里的)特定方法,那么可使用匿名对象来表示
第二十九条 理解引用计数
retain 递增保留计数
release 递减保留计数
autorelease 待稍后清理“自动释放池”(autorelease pool)时,再递减保留计数
查看保留计数的方法叫retainCount,此方法不太有用,不推荐使用这个方法
按“引用树”回溯,那么最终会发现一个“根对象”(root object)。在Mac OS X应用程序中,此对象就是NSApplication对象;而在iOS应用程序中,则是 UIApplication对象。两者都是应用程序启动时创建的单例
调用autorelease,此方法会在稍后递减计数,通常是在下一次“事件循环”(event loop)时递减,不过也可能执行得更早些。
此方法可以保证对象在跨越“方法调用边界”(method call boundary)后一定存活。实际上,释放操作会在清空最外层的自动释放池时执行,除非你有自己的自动释放池,否则这个时机指的就是当前线程的下一次事件循环。
要点
引用计数机制通过可以递增递减的计数器来管理内存。对象创建好之后,其保留计数至少为1。若保留计数为正,则对象继续存活。当保留计数降为0时,对象就被销毁了。
在对象生命期中,其余对象通过引用来保留或释放此对象。保留与释放操作分别会递增及递减保留计数
第三十条 以ARC简化引用计数
使用ARC时一定要记住,引用计数实际上还是要执行的,只不过保留与释放操作现在是由ARC自动为你添加。
由于ARC会自动执行retain,release,autorelease等操作,所以直接在ARC下调用这些内存管理方法是非法的。具体来说,不能调用 retain,release,autorelease,dealloc
将内存管理语义在方法名中表示出来早已成为Objective-C的惯例,而ARC则将之确立为硬性规定。这些规则简单地体现在方法名上。若方法名以下列词语开头,则其返回的对象归调用者所有:alloc,new,copy,mutableCopy
若方法名不以上述四个词语开头,则表示其返回的对象并不归调用者所有。这种情况下,返回的对象会自动释放,所以其值在跨越方法调用边界后依然有效。
使用ARC还有其他好处,它可以执行一些手工操作很难甚至无法完成的优化。例如,编译期,ARC会把能够互相抵消的retain、release、autorelease操作简约。如果发现在同一个对象上执行了多次“保留”与“释放”操作,那么ARC有时可以成对地移除这两个操作。
ARC也会处理局部变量与实例变量的内存管理。默认情况下,每个变量都是指向对象的强引用。
@interface EOCClass : NSObject {
id __weak _weakObject;
id __unsafe_unretained _unsafeUnretainedObject;
}
不论采用上面哪种写法,在设置实例变量时都不会保留其值。只有使用新版(Mac OS X 10.7,iOS 5.0及其后续版本)运行期程序库时,加了__weak修饰符的weak引用才会自动清空。
块会自动保留其所捕获的全部对象,而如果这其中有某个对象又保留了块本身,那么就可能导致“保留环”。可以用__weak局部变量来打破这种“保留环”
用了ARC后,就不需要再编写这种dealloc方法了,不过如果有非Objective-C的对象,比如CoreFoundation中的对象或是由malloc()分配在堆中的内存,那么仍然需要清理。
- (void)dealloc {
CFRelease(_coreFoundationObject);
free(_heapAllocateMemoryBlob);
}
要点
ARC只负责管理Objective-C对象的内存。尤其要注意:CoreFoundation对象不归ARC管理,开发者必须适时调用CFRetain/CFRelease。
第三十一条:在dealloc方法中只释放引用并解除监听
在dealloc方法中,通常还要做一件事,那就是把原来配置过的观测行为(observation behavior)都清理掉。如果用NSNotificationCenter给此对象订阅过某种通知,那么一般应该在这里注销,这样的话,通知系统就不再把通知发给回收后的对象了,若是还向其发送通知,则必然会令应用程序崩溃
- (void)dealloc {
CFRelease(coreFoundationObject);
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
在dealloc里也不要调用属性的存取方法,因为有人可能会覆写这些方法,并于其中做一些无法在回首阶段安全执行的操作。此外,属性可能正处于“键值观测”(Key-Value Observation, KVO)机制的监控之下,该属性的观察者(observer)可能会在属性值改变时保留或使用这个即将回收的对象。这种做法会令运行期系统的状态完全失调,从而导致一些莫名其妙的错误。
要点
执行异步任务的方法不应该在dealloc里调用;只能在正常状态下执行的那些方法也不应在dealloc里调用,因为此时对象已经处在回收的状态了。
第三十二条 编写“异常安全代码”时留意内存管理问题
异常处理例程将销毁对象,然而在手动管理引用技术时,销毁工作有些麻烦
EOCSomeClass *object;
@try {
object =[ [EOCSomeClass alloc] init];
[object doSomethingThatMayThrow];
}
@catch (...) {
NSLog(@"Whoops, there was an error. Oh well...");
}
@finally {
[object release];
}
在ARC下,问题更严重
@try {
EOCSomeClass *object =[ [EOCSomeClass alloc] init];
[object doSomethingThatMayThrow];
}
@catch (...) {
NSLog(@"Whoops, there was an error. Oh well...");
}
你可能会认为这种情况ARC自然会处理的。但实际上ARC不会自动处理。
-fobjc-arc-exceptions:开启异常捕捉,默认不开启
不开启的原因:在Objective-C代码中,只有当应用程序必须因异常状况而终止时才应抛出异常。因此,如果应用程序即将终止,那么是否还会发生内存泄漏就无关紧要了。在应用程序必须立即终止的情况下,还去添加安全处理异常所用的附加代码是没有意义的。
有种情况编译器会自动把-fobjc-arc-exceptions标志打开,就是处于Objective-C++模式时
要点
捕获异常时,一定要注意将try块内所创立的对象清理干净。
在默认情况下,ARC不生成安全处理异常所需的清理代码。开启编译器标志后,可生成这种代码,不过会导致应用程序变大,而且会降低运行效率
第三十三条 以弱引用避免保留环
用unsafe_unretained修饰的属性特质,其语义同assign特质等价。然而,assign通常只用于“整体类型”(int、float、结构体等),unsafe_unretained则多用于对象类型。这个词本身就表明其所修饰的属性可能无法安全使用。
weak引用可以自动清空,也可以不自动清空。自动清空(autonilling)是随着ARC而引入的新特性,由运行期系统来实现。在具备自动清空功能的弱引用上,可以随意读取其数据,因为这种引用不会指向已经回收过的对象。
第三十四条 以“自动释放池块”降低内存峰值
@autoreleasepool {
}
int main(int argc, char *argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, @"EOCAppDelegate");
}
}
要点
自动释放池排布栈中,对象收到autorelease消息后,系统将其放入最顶端的池里
合理运用自动释放池,可降低应用程序的内存峰值
@autoreleasepool这种新式写法能够创建出更为轻便的自动释放池
第三十五条 用“僵尸对象”调试内存管理问题
Cocoa提供了“僵尸对象”这个功能。启用这项调试功能之后,运行期系统会把所有已经回收的实例转化为特殊的“僵尸对象”,而不会真正回收它们。这种对象所在的核心内存无法重用,因此不可能遭到覆写。僵尸对象收到消息后,会抛出异常,其中准确说明了发送过来的消息,并描述了回首之前的那个对象。
将NSZombieEnabled环境变量设为YES,即可开启此功能。比方说,在Mac OSX系统中用bash运行程序时,可以这么做
export NSZombieEnabled="YES"
./app
要点
系统会修改对象的isa指针,令其指向特殊的僵尸类,从而使该对象变为僵尸对象。僵尸类能够响应所有的选择子,响应方式为:打印一条包含消息内容及其接受着的消息,然后终止应用程序。
第三十六条 不要使用retainCount
此方法之所以无用,其首要原因在于:他所返回的保留计数只是某个给定时间上的值。该方法并未考虑到系统会稍后把自动释放池清空,因为不会将后续的释放操作从返回值里减去,这样的话,此值就未必能真实反映实际的保留计数了。
retainCount可能永远不返回0,因为有时系统会优化对象的释放行为,在保留技术还是1的时候就把它回首了。只有在系统不打算这么优化时,计数值才会递减至0.
要点
引入ARC后,retainCount方法就正式废止了,在ARC下调用该方法会导致编译器报错
第三十七条 理解“块”这一概念
块用^符号表示,后面跟着一对花括号,括号里面是块的实现代码。
^{
//Block implementation here
}
块类型的语法与函数指针近似。下面列出的块,没有参数,也不返回值
void (^someBlock)() = ^{
//Block implementation here
};
块类型的语法结构如下:
return_type (^block_name) (parameters)
下面这种写法所定义的块,返回nil,并且接受两个int做参数:
int (^addBlock)(int a, int b) = ^(int a, int b) {
return a + b;
};
int add = addBlock(2, 5);
块的强大之处是:在声明它的范围里,所有变量都可以为其所捕获。
int additional = 5;
int (^addBlock)(int a, int b) = ^(int a, int b) {
return a + b + additional;
};
int add = addBlock(2, 5);
默认情况下,为块所捕获的变量,是不可以在块里修改的。不过,声明变量的时候可以加上__block修饰符,就可以在块内修改了。
如果块捕获的变量是对象类型,那么就会自动保留它。系统在释放这个块时,也会将其一并释放。块本身也和其他对象一样,有引用计数。当最后一个指向块的引用移走后,块就回收了。回收时也会释放块所捕获的变量,以便平衡捕获时所执行的保留操作。
如果将块定义在类的实例方法中,除了可以访问类的所有实例变量外,还可以使用self变量。块总能修改实例变量,所以在声明时无须加__block。不过,如果通过读取或写入操作捕获了实例变量,那么也会自动把self变量一并捕获,因为实例变量与self所指代的实例关联在一起的。
定义块的时候,其所占的内存区域是分配在栈中的。这就是说,块只在定义它的那个范围内有效。
不安全:
void (^block) ();
if (/*some condition*/) {
block = ^{
NSLog(@"Block A");
}
}
else {
block = ^{
NSLog(@"Block B");
};
}
block();
为解决此问题,可给块对象发送copy消息以拷贝之。
void (^block) ();
if (/*some condition*/) {
block = [^{
NSLog(@"Block A");
} copy];
}
else {
block =[ ^{
NSLog(@"Block B");
} copy];
}
block();
要点
块可以分配在栈或堆上,也可以是全局的。分配在栈上的块可拷贝到堆里。
第三十八条 为常用的块类型创建typedef
为隐藏复杂的块类型,需要用到C语言中名为“类型定义”的特性。
typedef int(^EOCSomeBlock)(BOOL flag, int value);
EOCSomeBlock block = ^(BOOL flag, int value){
//Implementation
};
第三十九条 用handler块降低代码分散程度
与使用委托模式的代码相比,用块写出来的代码显然更为整洁。
建议使用同一个块来处理成功与失败情况。
要点
在创建对象时,可以使用内联的handler块将相关业务逻辑一并声明
在有多个实例需要监控时,如果采用委托模式,那么经常需要根据传入的对象来切换,若改为handler块来实现,则可直接将块与相关对象放在一起。
设计API时如果用到了handler块,那么可以增加一个参数,使调用者可通过此参数来决定应该把块安排在哪个队列上执行
第四十条 用块引用其所属对象时不要出现保留环
要点
如果块所捕获的对象直接或间接地保留了块本身,那么就得当心保留环问题
一定要找个适当的机会接触保留环,而不能把责任推给API的调用者。
第四十一条 多用派发队列,少用同步锁
在OC中,如果有多个线程要执行同一份代码,那么有时可能会出问题。这种情况下,通常要使用锁来实现某种同步机制。在GCD出现之前,有两种办法,第一种是采用内置的“同步块”(synchroniztion block):
- (void)synchronizedMethod {
@synchronized(self) {
//Safe
}
}
另一个办法是直接使用NSLock对象:
_lock = [[NSLock allock] init];
- (void)synchronizedMethod {
[_lock lock];
//Safe
[_lock unlock];
}
有种简单而高效的办法可以替代同步块或锁对象,那就是使用“串行同步队列”,将读取操作及写入操作都安排在同一个队列里,既可保证数据同步。其用法如下:
_syncQueue = dispatch_queue_create("com.effectiveobjectivec.syncQueue", NULL);
-(NSString *)someString {
__block NSString *localSomeString;
dispatch_sync(_syncQueue, ^{
localSomeString = _someString;
});
return localSomeString;
}
- (void)setSomeString:(NSString *)someString {
dispatch_sync(_syncQueue, ^{
_someString = someString;
});
}
可以进一步优化,设置方法并不一定非得是同步的。设置实例变量所用的块,并不需要向设置方法返回什么值。也就是说,设置方法的代码可以改成下面这样:
- (void)setSomeString:(NSString *)someString {
dispatch_async(_syncQueue, ^{
_someString = someString;
});
}
但这么改有个坏处:如果你测以下程序性能,那么可能会发现这种写法比原来慢,因为执行异步派发时,需要拷贝块。若拷贝块所用的时间明显超过执行块所花的时间,则这种做法将比原来更慢,若派发给队列的块要执行更繁重的任务,那么可考虑这种备选方案。
多个获取方法可以并发进行,而获取方法和设置方法之间不能并发执行。这次不用串行队列,改用并行队列.
在队列中,栅栏块必须单独执行,不能与其他块并行。这只对并发队列有意义,因为串行队列中的块总是按照顺序逐个来执行的。
_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
-(NSString *)someString {
__block NSString *localSomeString;
dispatch_sync(_syncQueue, ^{
localSomeString = _someString;
});
return localSomeString;
}
- (void)setSomeString:(NSString *)someString {
dispatch_barrier_async(_syncQueue, ^{
_someString = someString;
});
}
注意:设置函数也可以改为同步的栅栏块来实现,那样做可能更高效,其原因刚才已经解释过。
第四十二条 多用GCD, 少用performSelector系列方法
下面两行代码的执行效果相同
[object performSelector:@selector(selectorName)];
[object selectorName];
在ARC下可能内存泄漏,因为编译器并不知道将要调用的选择子是什么,因此,也就不了解其方法签名及返回值,甚至连是否有返回值都不清楚。而且,由于编译器不知道方法名,所以就没办法运行ARC的内存管理规则来判定返回值是不是应该释放。鉴于此,ARC采用了比较谨慎的做法,就是不添加释放操作。然而这么做坑能导致内存泄漏,因为方法在返回对象时可能已经将其保留了。
另一个原因,返回值只能是void或对象类型,若返回值的类型为C语言结构体,则不可使用performSelector方法。
延后操作有两种,优先考虑第二种
[self performSelector:@selector(doSomething)
withObject:nil
afertDelay:5.0];
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];
});
要点
performSelecor系列方法所能处理的选择子太过局限了,选择子的返回值类型及发送给方法的参数个数都受到限制
第四十三条 掌握GCD及操作队列的使用时机
GCD与操作队列区别:首先GCD是纯C的API,而操作队列则是Objective-C的对象。
使用NSOperation及NSOperationQueue的好处如下:
取消某个操作。
指定操作间的依赖关系。一个操作可以依赖其他多个操作。开发者能够指定操作之间的依赖体系,使特定的操作必须在另外一个操作顺利执行完毕后方可执行。
通过键值观测机制(KVO)来监听,比如可以通过isCancelled属性来判断任务是否取消,又比如可以通过isFinished属性来判断任务是否已完成。
指定操作的优先级。操作的优先级表示此操作与队列中其他操作之间的优先关系。
重用NSOperation对象。
要点
在解决多线程与任务管理问题时,派发队列并非唯一方案
操作队列提供了一套高层的OC API,能实现纯GCD所具备的绝大部分功能,而且还能完成一些更为复杂的操作,那些操作若改用GCD来实现,则需另外编写代码。
第四十四条 通过Dispatch Group机制,根据系统资源状况来执行任务
创建dispatch group
dispatch_group_t dispatch_group_create();
任务编组
方法一
void dispatch_group_async(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);
方法二
void dispatch_group_enter(dispatch_group_t group);
void dispatch_group_leave(dispatch_group_t group);
等待dispatch group执行完毕:
long dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout);
除了可以用上面的函数外,还可以换个方法,使用下列函数:
void dispatch_group_notify(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);
可以向此函数传入块,等dispatch group执行完毕之后,块会在特定的线程执行。假如当前线程不应阻塞,而开发者又想在那些任务全部完成时得到通知,那么此做法就很有必要了
dispatch_queue_t queue = dispatch_get_globla_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t dispatchGroup = dispatch_group_create();
for (id object in collection) {
dispatch_group_async(disaptchGroup, queue, ^{
[object performTask];
});
}
dispatch_group_wait(dispatchGroup, DISPATCH_TIME_FOREVER);
//Continue processing after completing tasks
若当前线程不应阻塞,则可用notify
dispatch_queue_t notifyQueue = dispatch_get_main_queue();
dispatch_group_notify(dispatchGroup, notifyQueue, ^{
//Continue processing after completing tasks
});
在前面的范例代码中,我们遍历某个collection,并在其每个元素执行任务,而这也可以用另一个GCD函数实现
void dispatch_apply(size_t iterations, dispatch_queue_t queue, void(^block)(size_t));
此函数会反复执行一定次数,每次传给块的参数值都会递增,从0开始,直至iterations - 1
dispatch_queue_t queue = dispatch_queue_create("com.effectiveobjectives.queue", NULL);
dispatch_apply(10, queue, ^(size_t i){
//perform task
});
可以用并发队列
dispatch_apply会持续阻塞,直到所有任务都执行完毕为止。由此可见,假如把块派给了当前队列,就讲导致锁死。
第四十五条 使用dispatch_once来执行只需运行一次的线程安全代码
+sharedInstance {
static EOCClass *sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
要点
经常要编写“只需执行一次的线程安全代码”。通过GCD所提供的dispatch_once函数,很容易就能实现此功能。
标记应该声明在static或global作用域中,这样的话,在把只需执行一次的块传给dispatch_once函数时,穿进去的标记也是相同的。
第四十六条 不要使用dispatch_get_current_queue
该函数有种典型错误用法,就是用它检测当前队列是不是某个特定的队列,试图以此来避免执行同步派发时可能遭遇的死锁问题。
dispatch_queue_t queueA = dispatch_queue_create("com.effectiveobjectivec.queueA", NULL);
dispatch_queue_t queueB = dispatch_queue_create("com.effectiveobjectivec.queueB", NULL);
dispatch_sync(queueA, ^{
dispatch_sync(queueB, ^{
dispatch_blcok_t block = ^{ /*...*/};
if (dispatch_get_current_queue() == queueA) {
block();
}
else {
dispatch_sync(queueA, block);
}
});
});
要解决这个问题,最好的办法就是通过GCD所提供的功能来设定“队列特有数据”,此功能可以把任意数据以键值对的形式关联到队列里。最重要之处在于,假如根据制定的键过去不到关联数据,大么系统就会沿着层级体系向上查找,直至找到数据或到达根队列为止。
dispatch_queue_t queueA = dispatch_queue_create("com.effectiveobjectivec.queueA", NULL);
dispatch_queue_t queueB = dispatch_queue_create("com.effectiveobjectivec.queueB", NULL);
dispatch_set_target_queue(queueB, queueA);
static int kQueueSpecific;
CFStringRef queueSpecificValue = CFSTR(”queueA”)
dispatch_queue_set_specific(queueA, &kQueueSpecific, (void *)queueSpecificValue, (dispatch_function_t)CFRelease);
dispatch_sync(queueB, ^{
dispatch_blcok_t block = ^{ /*.NO DEADLOCK!*/};
CFStringRef retrievedValue = dispatch_get_specific(&kQueueSpecific);
if (retrievedValue) {
block();
}
else {
dispatch_sync(queueA, block);
}
});
要点
dispatch_get_current_queue函数的行为常常与开发者所预期的不同。此函数已经废弃,只应做调试之用。
由于派发队列是按层级来组织的,所以无法单用某个队列对象来描述“当前队列”这一概念。
dispatch_get_current_queue函数用于解决由不可重入的代码所引发的死锁,然而能用此函数解决的问题,通常也能改用“队列特定数据”来解决
第四十七条 熟悉系统框架
将一系列代码封装为动态库,并在其中放入描述其接口的头文件,这样做出来的东西就叫框架。有时iOS平台构建的第三方框架所使用的是静态库,这是因为iOS应用程序不允许在其中包含动态库。iOS系统框架仍然使用动态库。
Foundation框架中的许多功能,都可以在此框架中找到对应的C语言API。
要点
许多系统框架都可以直接使用。其中最重要的是Foundation 与CoreFoundation,这两个框架提供了构建应用程序所需的许多核心功能。
很多常见任务都能用框架来做,例如音频与视频处理、网络通信、数据管理等
第四十八条 多用块枚举,少用for循环
要点
遍历collection有四种方式。最基本的办法是for循环,其次是NSEnumerator遍历法及快速遍历法,最新、最先进的方式则是“块枚举法”。
“块枚举法”本身就能通过GCD来并发执行遍历操作,无须另行编写代码。而采用其他遍历方式则无法轻易实现这一点。
若提前知道待遍历的collection含有何种对象,则应修改块签名,指出对象的具体类型。
第四十九条 对自定义其内存管理语义的collection使用无缝桥接
NSArray *anNSArray = @[@1, @2, @3, @4, @5];
CFArrayRef aCFArray = (__bridge CFArrayRef)anNSArray;
NSLog(@"Size of array = %li", CFArrayGetCount(aCFArray));
转换操作的__bridge告诉ARC如何处理转换所涉及的OC对象。__bridge的意思是:ARC仍然具备这个OC对象的所有权。而__bridge_retained则与之相反,意味着ARC将交出对象的所有权。若是前面那段代码改用它来实现,那么用完数组之后就要加上CFRelease以释放其内存。与之相似,反向转换可以通过__bridge_transfer来实现,例如,想把CFArrayRef转换为NSArray*,并且想令ARC获得对象所有权,那么就可以采用这种转换方式。这三种转换方式称为“桥式转换”。
要点
通过无缝桥接技术,可以在Foundation框架中的OC对象与CoreFoundation框架中的C语言数据结构之间来回转换。
在CoreFoundation层面创建collection时,可以指定许多回调函数,这些函数表示此collercion应如何处理其元素。然后,可运用无缝桥接技术,将其转换成剧本特殊内存管理语义的OC collection.
第五十条 构建缓存时选用NSCache而非NSDictionary
NSCache胜过NSDictionary之处在于,当系统资源将要耗尽时,它可以自动删减缓存。此外,NSCache还会先行删减“最久未使用的”对象。
NSCache并不会copy键,而是会retain它。
原因:很多时候,键都时由不支持拷贝操作的对象来充当的。所以说,在键不支持拷贝操作的情况下,该类用起来必字典更方便。
另外,NSCache是线程安全的。在开发者自己不编写加锁代码前提下,多个线程便可以同时访问NSCache.
开发者可以操控缓存删减其内容的时机。其一是缓存中的对象总数,其二是所有对象的“总开销”。
#import <Foundation/Foundation.h>
//Network fetcher class
typedef void (^EOCNetworkFetcherCompletionHandler)(NSData *data);
@interface EOCNetworkFetcher : NSObject
- (id)initWithURL:(NSURL *)url;
- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)handler;
@end
//Class that use the network fetcher and caches results
@interface EOCClass : NSobject
@end
@implementation EOCClass {
NSCache *_cache;
}
- (id)init {
if (self = [super init]) {
_cache = [NSCache new];
//Cache a maximum of 100 URLs
_cache.countLimit = 100;
//**
*The Size in bytes of data is used as the cost,
*so this set a cost limit of 5MB
*/
_cache.totalCostLimt = 5 * 1024 *1024;
}
return self;
}
- (void)downloadDataFromURL:(NSURL *)url {
NSData *cachedData = [_cache objectForKey:url];
if (cachedData) {
//Cache hit
[self useData:cachedData];
}
else {
//cache miss
EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
[fetcher startWithCompletionHandler:^(NSData *data) {
[_cache setObject:data forKey:url cost data.length];
[self useData:data];
}];
}
}
使用NSPurgeableData
- (void)downloadDataFroURL:(NSURL *)url {
NSPurgeableData *cachedData = [_cache objectForKey:url];
if (cachedData) {
//Cache hit
[cachedData beginContentAccess];
[self useData:cachedData];
[cachedData endContentAccess];
}
else {
//cache miss
EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
[fetcher startWithCompletionHandler:^(NSData *data) {
NSPurgeableData *purgeableData = [NSPurgeableData dataWithData:data];
[_cache setObject:purgeableData forKey:url cost: purgeableData.length];
[self useData:data];
[purgeableData endContentAccess];
}];
}
}
要点
实现缓存时应选用NSCache,因为NSCache可以提供优雅的自动删减功能,而且是“线程安全的”,此外,它与字典不同,不会拷贝键。
可以给NSCache对象设置上限,用以限制缓存中的对象总个数及“总成本”,而这些尺度则定义了缓存删减其中对象的时机。但是绝对不要把这些尺度当成可靠的“硬限制”,它们仅对NSCache起指导作用。
将NSPurgeableData与NSCache搭配使用,可实现自动清除数据的功能,也就是说,当NSPurgeableData对象所占用内存为系统所丢弃时,该对象也会从缓存中移除。
第五十一条 精简initialize与load的实现代码
+ (void)load
对于加入运行期系统中的每个类及分类来说,必定会调用此方法,而且仅调用一次。当包含类或分类的程序库载入系统时,就会执行此方法,而这通常就是指应用程序启动的时候,若程序是为iOS平台设计的,则肯定会在此时执行。Mac OS X应用程序更自由一些,它们可以使用“动态加载”之类的特性,等应用程序启动好之后再去加载程序库。如果分类和其所属的类都定义了load,则先调用类里的,再调用分类里的。
load方法的问题在于,执行该方法时,运行期系统处于“脆弱状态”。在执行子类的load方法之前,必定会先执行所有超类的load方法,而如果代码还依赖其他程序库,那么程序库里相关类的load方法也必定会先执行。然而,根据某个给定的程序库,却无法判断出其中各个类的载入顺序。因此,在load方法中使用其他类是不安全的。
分类和其所属的类里,都可能出现load方法。此时两种实现代码都会调用,类的实现要比分类的实现先执行。
想执行与类相关的初始化操作,还有个办法,就是复写下列方法:
+ (void)initialize
该方法会在程序首次用该类之前调用,且只调用一次。它是由运行期系统来调用的,绝不应该通过代码直接调用。其虽与load相似,但却有几个非常重要的微妙区别。
1、它是“惰性调用”,只有当程序用到了相关的类时,才会调用。因此,如果某个类一直都没有使用,那么其initialize方法就一直不会运行。也就是说,应用程序无须先把每个类的initialize都执行一遍,这与load方法不同,对于load来说,应用程序必须阻塞并等着所有类的load都执行完,才能继续
2、运行期系统在执行该方法时,是处于正常状态的,此时可以安全使用并调用任意类中的任意方法。而且,运行期系统也能确保initialize方法一定会在“线程安全的环境”中执行,只有执行initialize那个线程可以操作类或类实例。其他线程都要先阻塞,等着initialize执行完。
3、initialize方法与其他消息一样,如果某个类未实现它,而其超类实现了,那么就会运行超类的实现代码。
要点
在加载阶段,如果类实现了load方法,那么系统就会调用它。分类里也可以定义此方法,类的load方法要比分类中的先调用。与其他方法不同,load方法不参与覆写机制。
首次使用某个类之前,系统会向其发送initialize消息。由于此方法尊从普通的覆写规则,所以通常应该在里面判断当前要初始化的是哪个类。
load与initialize方法都应该实现得精简一些,有助于保持应用程序的响应能力,也能减少引入“依赖环”的几率。
无法在编译期设定的全局常量,可以放在initialize方法里初始化。
第五十二条 别忘了NSTimer会保留其目标对象
计时器会保留其目标对象,等到自身“失效”时再释放此对象。调用invalidate方法可令计时器失效;执行完相关任务后,一次性的计时器也会失效。
要点
NSTimer对象会保留其目标,直到计时器本身失效为止,调用invalidate方法可令计时器失效,另外,一次性的计时器在触发完任务之后也会失效。
反复执行任务的计时器,很容易引起保留环,如果这种计时器的目标对象又保留了计时器本身,那肯定会导致保留环。这种环状保留关系,可能是直接发生的,也可能是通过对象图里的其他对象间接发生的。
可以扩充NSTimer的功能,用“块”来打破保留环。不过,除非NSTimer将来在公共接口里提供此功能,否则必须创建分类,将相关实现代码加入其中。
网友评论