第15条 用前缀避免命名空间冲突
最好遵循苹果的编程规范,使用 3个字的前缀。
对于全局的变量,常量以及C函数,也应该加上前缀。
第16条 提供“全能初始化方法“
这种编程模式就是定义一个参数最全的初始化方法,在其中初始化所有的成员变量,其余的初始化方法都调用这个初始化方法。目的是确保所有的成员变量都已经初始化,所有必要的过程都已经调用。
下边是书上的例子:
@implementation EOCRectangle
-(id)initWithWidth:(float)width andHeight:(float)height{
if(self = [super init]){
_width = width;
_height = height;
}
return self;
}
-(id)init{
return [self initWithWidth:5.0f andHeight:10.0f];
}
@end
如果用户使用默认的init函数初始化,会调用全能初始化函数得到一个宽5高10的长方形。这个例子其实不太好,对于矩形这个类来说,默认长宽为0可能是用户期望的。如果其他类,比如一个人,可能期望的默认名字是@“匿名”或者是@“”,而不是nil。(另外如果有未预料nil,在后续的操作中可能还会引起bug)。
总之,使用全能初始化函数能保证一个对象的所有成员变量默认都是自己设置的值。
在继承体系中,初始化函数有些复杂。比如有一个正方形类继承自EOCRectangle:
@implementation EOCSquare
-(id)initWithDimension:(float)dimension{
return [super initWithWidth:dimension andHeight:dimension];
}
@end
EOCSquare只需要一个参数就可以创建,因此提供了一个额外的初始化函数initWithDimension,在这个初始化函数中调用了基类ECORectangle的全能初始化函数。
但是用户在初始化的时候,仍然可以使用ECORectangle的initWithWidth:andHeight:方法,还有init方法。这样就可以创建出长和宽不等的正方形了。为了避免这种情况出现,又有了如下规则:
子类应该覆盖基类的全部全能初始化函数。
一个可能的实现是:
-(id)initWithWidth:(float)width andHeight:(float)height{
float dimension = MAX(width, height);
return [super initWithWidth:dimension andHeight:dimension];
}
如果在EOCSquare上调用init,此时self指向的是EOCSquare,因此会调用EOCSquare的initWithWidth:andHeight:方法。所以子类就不需要覆盖基类的init方法了。
上边的这套逻辑在Swift语言中可以通过编译器保证。
第17条 实现description方法
description和debugDescription是NSObject上的方法。自定义对象实现前者可以通过NSLog打印,实现后者可以实现在控制台po。
第18条 尽量使用不可变对象
这也是遵循了程序设计中的权限最小原则,使用不可变对象的好处显然是简单,你能确定这个值不会被修改。还有一个额外的好处就是在多线程环境下,不可变的对象可以简化编程难度。
有几个点:
1)如果对象的属性对外只读,但是内部可以修改,可以在扩展中使用readwrite改写。也可以在内部使用可变对象,对外提供一份不可变的copy。
2)readonly不能阻止外界使用KVC的方式改变
3)readonly更不能阻止外界通过运行时获取变量地址的方式改变
第19条 使用清晰而协调的命名方式
起名这种东西,是编程最难的部分之一,不可能通过一个条目学会,只能多看规范的代码。
第20条 为私有方法名加前缀
为私有方法加上前缀,以便于区分,但是不要使用下划线作为前缀。因为OC的机制,下划线可能覆盖了苹果自己的方法。
第21条 理解Objective-C错误模型
Objective-C一般不使用异常机制传递错误,这是因为ARC机制不是异常安全的,在抛出异常的时候会产生内存泄漏。如果想让ARC代码支持异常,需要打开编译器的-fobjc-arc-exceptions标志。但是这会产生额外的代码,即使没有发生异常也会执行。
Objective-C的错误处理编程范式是传递NSError。一个函数如果可能发生错误,就返回一个BOOL值,然后通过参数返回的方式检查错误:
-(BOOL)fooWithParams:(NSDictionary *)params error:(NSError **)error{
//if some error happen
*error = [NSError errorWithDomain:@"test" code:-1
userInfo:@{NSLocalizedDescriptionKey: @"this is a test error"}];
return YES;
}
-(void)useFoo{
NSError *error = nil;
BOOL res = [self fooWithParams:nil error:&error];
if(!res){
NSLog(@"error happend: %@", error.localizedDescription);
}
}
如果是异步过程,则通过代理返回一个error参数来传递错误。一般代理的协议中会有这样的方法签名。
-(void)finishedWithData:(NSData *)data withError:(NSError *)error;
第22条 理解NSCopying协议
NSCopying协议里只有一个方法:
-(id)copyWithZone:(NSZone: )zone;
如果我们自定义的类需要实现copy的方法,声明遵守NSCopying协议并实现这个方法就行了。
一个例子:
@interface EOCPerson : NSObject<NSCopying>
@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
-(id)initWithFirstName:(NSString *)firstName
andLastName:(NSString *)lastName;
@end
@implementation EOCPerson{
NSMutableSet *_friends;
}
//其他实现
-(id)copyWithZone:(NSZone *)zone{
EOCPerson *copy = [[[self class] allocWithZone:zone]
initWithFirstName:_firstName
andLastName:_lastName];
copy->_friends = [_friends mutableCopy];
return copy;
}
@end
1)zone是历史遗留的参数,现在已经不需要理解,照样传过去就行了。实际上不使用allocWithZone初始化函数,直接用alloc也不会有什么问题。
2)[self class]是一个要注意的地方,不要使用EOCPerson,因为如果有子类继承了EOCPerson,比如EOCTeacher,EOCTeacher也继承了copy方法,如果此处使用EOCPerson,那么EOCTeacher copy出来的就是一个EOCPerson,这不合语义。
3)在copyWithZone中对可变的集合类型的成员,要进行可变的copy,否则copy出来的对象不独立。对于不可变的,不用copy,反正也没有办法变化,两个对象公用一个还节省内存。
实际上,不可变对象的copy并不复制内存,而是直接返回自己。这点可以通过打印对象地址来验证。因此,即使用了copy也没有关系。
第23条 通过委托与数据源协议进行对象间通信
委托模式有几种应用场景:
1)组件内部有些事情不能确定,需要使用者来提供。比如UITableView cell的高度。
2)不应该由组件负责的功能,比如组件内部事件的响应,如UITableView cell的点击响应。
3)异步事件的回调,如网络请求完成后的回调。
使用委托模式的注意:
1)代理对象的弱引用,避免循环引用
2)在给代理对象发送消息之前判断代理对象是否响应这个消息。
技巧:
如果代理方法调用频繁,可能判断对象是否响应方法是一个耗时的操作,如果确定瓶颈在这,可以缓存代理对象的判断结果。书中介绍了一种使用C结构体缓存方法。
第24条 将类的实现代码分散到便于管理的数个分类之中
Objective-C没有私有方法机制,可以使用一个Private的分类来管理私有方法,增加代码的可读性。除此之外,分类还可以用于将方法分门别类,接口更好读。
第25条 总是为第三方类的分类名称加前缀
这是为了解决Objective-C没有命名空间产生的编程范式。避免冲突。
第26条 勿在分类中声明属性
Objective-C的分类设计目的是扩展功能,而不是封装数据。因此在分类中声明的属性不会被自动合成,需要使用关联对象的方式,或者动态解析的方式来添加。如果有其他更正规的设计可以达到目的,尽量不要用。
第27条 使用“class-continueation”分类隐藏实现细节
就是每次Xcode创建viewController之后.m文件中的匿名分类,也叫扩展。这里可以放置"私有"成员。也可以修改.h文件中的readonly修饰符。
还有一个应用场景,如果类的实现中使用了C++代码,如果在头文件中声明了C++的类,那么要求使用者使用C++编译器编译代码,如果放在.mm文件中,对外可以隐藏C++细节。
第28条 通过协议提供匿名对象
这就是协议的使用方式,面向接口编程。通过id来使用匿名对象,即不关系对象是什么,只要实现了这个协议就可以使用。
下边是NSMutableDictionary的setObject:forKey接口:
- (void)setObject:(ObjectType)anObject forKey:(id<NSCopying>)aKey;
其中,key在设置的时候是要copy一份的,因此要求key服从NSCopying协议,至于具体是什么类型,不用关心。
第29条 理解引用计数 & 第30章 以ARC简化引用计数
ARC 管理内存的方式是自动的插入内存管理代码,但是机制仍然是引用计数。
在ARC下,retain,release,autorelease,dealloc都是不能主动调用的,因为会干扰ARC添加这些内存管理的代码。
使用ARC必须遵循方法的命名规则:
alloc,new,copy,mutableCopy,以这4个开头的方法都会返回一个对象,并且对象的所有权归调用者。否则,返回的对象调用者不用管。实际上,在ARC下,从来不需要操心一个函数返回对象的内存释放问题,主要关心的是循环引用。
使用ARC需要注意一点:CoreFoundation中的对象ARC不负责,因此需要自己手动管理内存。所以会经常看到CFRelease(XXX)
第31条 在dealloc方法中只释放引用并解除监听
在dealloc中应该做的事情有:
1)释放CoreFoundation的对象
2)取消对消息中心的监听
3)取消KVO的监听,是指取消对其他对象的监听。如果有其他对象监听自己,可以不用管。
4)释放资源,如数据库连接,套接字连接等。这些资源的释放应该给出一个独立的方法,这样在需要的时候可以提前释放,而不必等到dealloc再释放。
5)在dealloc中不应该做任何业务的事情,不应该调用对象属性(因为属性可能处于KVO监听中)。只做和释放资源有关的事情。
6)dealloc不能确保在哪个线程被调用,因此,不要假定执行在主线程中。
第32条 编写“异常安全代码”时留意内存管理问题
参见第21条
1)纯OC代码不应该使用异常,如果使用,也是在程序无法继续运行的时候使用。之后程序就崩溃了。
2)一般和C++混编的代码会使用异常,这时候应该打开编译选项-fobjc-arc-exceptions。
第33条 以弱引用避免保留环
引用计数和垃圾回收的一个区别就是,引用计数无法释放环状的引用孤岛。应该使用weak来保证没有循环引用。还有一个关键字unsafe_unretained,和weak的区别是,weak在指向的对象释放后,自动将指针指向nil,更加安全,因此能使用weak的时候应该使用weak。有些类型不支持weak,才使用unsafe_unretained。
第34条 以”自动释放池块“降低内存峰值
书中给出的例子是一个for循环里边创建了很多临时对象,如果不使用@autoreleasepool块,内存峰值会比较高。但是我自己实验得出的结论不一样:
@implementation BigMemObj{
NSMutableArray *_mutArr;
}
-(instancetype)init{
if(self = [super init]){
_mutArr = [[NSMutableArray alloc] initWithCapacity:1024*1024*30];
for(int i = 0; i < 1024*1024*30; i++){
[_mutArr addObject:@(i)];
}
}
return self;
}
NSMutableArray *arr = [[NSMutableArray alloc] init];
for(int i = 0 ; i < 10000; i++){
@autoreleasepool {
BigMemObj *mem = [[BigMemObj alloc] init];
NSLog(@"%@", mem);
}
}
NSMutableArray *arr = [[NSMutableArray alloc] init];
for(int i = 0 ; i < 10000; i++){
BigMemObj *mem = [[BigMemObj alloc] init];
NSLog(@"%@", mem);
}
定义一个类,创建实例的时候分配30M个数字对象,添加到数组中。第二段代码是使用了@autoreleasepool的,第三段没有使用。下边是运行一分钟的内存使用情况,实验手机是iPhone6s。
使用autoreleasepool的情况:
不使用autoreleasepool的情况:
发现不使用autoreleasepool的内存峰值反而更小。
在StackOverFlow上提问得到的回到是在ARC下不需要使用@autoreleasepool块:
10.16补充:
Stack Overflow 上的Matic Oblak给出了详尽的回答。这里把结论给出,讨论过程请点上边链接。
对于我们自己创建的对象,没有使用autorelease释放的,并不通过autoreleasepool释放。而是在超出作用域之后直接释放。因为ARC编译环境下不允许使用autorelease释放了,所以我们自己创建的对象不会有区别。但是框架中有些代码还是使用了autorelease的,这时候使用autoreleasepool能减少内存峰值。下边是两个例子:
- (void)viewDidLoad {
for (int i = 0; i < 10e5 * 2; i++) {
@autoreleasepool {
NSString *str = [NSString stringWithFormat:@"hi + %d", i];
}
}
NSLog(@"finished!");
}
NSMutableArray *allBigValues = [[NSMutableArray alloc] init];
NSString *path = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"profile.png"];
for(int i = 0; i<100000; i++){
@autoreleasepool {
[allBigValues addObject:[UIImage imageWithContentsOfFile:path]];
[allBigValues removeAllObjects];
}
}
第35条 用“僵尸对象”调试内存管理问题
使用僵尸对象调试,主要处理向已经释放了的对象发送消息引起的Crash问题。在ARC环境下,Foundation的对象已经很少出现这种问题。
开关在这里:
在对象即将被系统回收时,如果发现打开了zombie objects开关,那么就不回收这个对象,将这个对象转化成为僵尸对象。如果后续代码向僵尸对象发送消息,则打印一条警告。
僵尸对象的原理是通过运行时创建一个僵尸类,然后修改对象的isa指针指向这个僵尸类,这样这个对象的类型就在运行时改变了,这个僵尸类没有实现任何方法,只是通过message forwarding来响应发送过来的消息。
第36条 不要使用retainCount
ARC环境下已经不能使用了,会编译报错。