一. 常见实用技巧
1. 在类的头文件中尽量少引用其他头文件
一般来说,应当在某个类的头文件使用向前声明来提及别的类,并在类的实现引入那些类的头文件。这样可以降低类之间的耦合。
有时无法使用声明,比如声明某个类遵守一项协议。尽量把该协议的声明放在分类中,或单独头文件引入。
2. 多用字面量语法,少用与之等价的语法
字面量语法其实是一种“语法糖”,与常规方法比更加扼要简介。
NSString *string = @"string";
NSNumber *number = @1;
NSArray *array = @[@"1",@"2",@"3"];
NSDictionary *dic = @[@"key":@"value"];
array[1];
dic[@"key"];
//前面都是不可变对象,要想创建可变对象
NSMutableArray *mutableArr = [@[@"1",@"2",@"3"] mutableCopy];
3. 多用类型常量,少用#define预处理指令
不要用预处理指定定义常量,这样定义出来的常量不含类型信息,编译器只是在编译前据此执行查找和替换操作。即使有人重新定义了常值量,编译器也不会警告。
在实现文件中使用static const 定义“只在编译单元内可见的常量”。此类常量不在全局符号表,无需加前缀。
static const NSTimeInterval kAnimationDuration = 0.5;
对外公开常量时在头文件使用extern声明全局常量,在实现文件定义其值。这种常量会出现在全局符号表,其名称要加以区隔,通常用类名作为前缀。
//.h中
extern NSString *const EOCStringConstant;
//.m中
NSString *const EOCStringConstant = @"VALUE";
4. 用枚举表示状态、选项、状态码
用NS_ENUM和NS_OPTION(用于多个选项同时使用)宏来定义枚举类型,并指明其底层数据类型。在处理枚举类型的switch语句中不要实现default分支。
5. 用前缀避免命名空间冲突
Objective-C没有其他语言那种内置的命名空间机制。如果发生命名冲突,那么应用程序的链接过程就会出错。所以要选择与你的公司、应用程序或二者皆有关联之名称作为类名(包括分类)的前缀。一个容易忽略的地方,实现文件里面的纯C函数和全局变量,在编译好的目标文件中,这些名称要作为“顶级符号”,所以也要加上前缀。
6. 总为第三方的分类名称加前缀
向第三方添加分类时,要为分类名称和里面的方法名加上自己专用的前缀。
7. 勿在分类中声明属性
把封装数据所用的全部属性都定义在主接口(主文件)。在分类拓展其他功能(包括存取方法),但尽量不要定义属性。
8. 尽量使用不可变对象
尽量使用不可变对象。若某属性仅对对象内部修改,则在.m中将其readonly属性拓展为readwrite属性。
不要把可变的collection作为属性公开(防止通过该属性直接修改内容,有可能还要执行某些操作),而应是提供相关方法来修改对象中的collection。
9. 为私有方法名加前缀
为私有方法的名称加前缀容易将其和公共方法区分开。不能单用_作为前缀,这是预留给苹果公的。
- (void)p_doSomething{
//.........
}
二. 高级技巧部分
1. 提供全能初始化方法
在类中提供一个全能初始化方法,其他初始化方法均应调用此方法。
若全能初始化方法与父类不同,则需要覆盖父类对应的方法。如果父类的初始化方法不适合子类,那么应该覆写这个父类方法,并在其中抛出异常。
- (instancetype)init{
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Must use initWithDimension: instead " userInfo:nil];
}
2. 实现description方法
实现description方法返回一个有意义的字符串,用以描述该实例。如果想在调试时打印出更详细的对象信息(用LLDB调试),可以实现debugDescription方法。
3. 在对象内部尽量直接访问实例变量
在对象内部读取数据时,应该直接使用实例变量来读(不需要经过方法派送,直接访问内存),而写入数据时,应该通过属性来写。
在初始化方法和dealloc方法中,总是应该直接使用实例变量来读写数据。懒加载情况下,需要通过属性来读写数据。
4. 以“类族模式”隐藏实现细节
使用类族模式可以灵活应对多个类,将它们的实现细节隐藏在抽象基类中,以保持接口简介。用户无需创建子类实例,只需调用基类方法来创建即可。系统框架中有很多类族,比如NSArray和NSMutableArray。
/* Employee.h */
typedef NS_ENUM(NSUInteger,EmployeeType) {
EmployeeTypeDeveloper,
EmployeeTypeDesginer,
EmployeeTypeFinance
};
@interface Employee : NSObject
+ (Employee)employeeWithTypt:(EmployeeType)type;
- (void)doWork;
@end
/* Employee.m */
@implementation Employee
+ (Employee)employeeWithTypt:(EmployeeType)type{
switch (type) {
case EmployeeTypeDeveloper:
return [[EmployeeDeveloper alloc] init];
break;
case EmployeeTypeFinance:
return [[EmployeeFinance alloc] init];
break;
case EmployeeTypeDesginer:
return [[EmployeeDesginer alloc] init];
break;
}
}
- (void)doWork{
//子类实现
}
@end
/* EmployeeDesginer.m */
@implementation EmployeeDesginer
- (void)doWork{
//具体实现
}
@end
5. 在既有类中使用关联对象存放自定义数据
有时需要在某类存放相关信息,当我们不方便继承该类来改写,就可以直接在该类使用关联对象。不过使用关联对象容易引入难以查找的BUG,比如循环引用。
//设置关联对象
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key, id _Nullable value, objc_AssociationPolicy policy)
//获取关联对象值
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
//移除所有关联对象值
objc_removeAssociatedObjects(id _Nonnull object)
6. 通过委托与数据源协议进行对象间的通讯
常规的委托模式中,信息从类流向受委托者(Delegate)。也可以用协议定义一套数据源接口,让类从数据源(DataSource)获取数据,这样信息就是从数据源流向类。(比如UITableView中的delegate和DataSource,一个处理用户和列表的操作,一个提供列表显示的数据)
若有必要,可实现含有位段的结构体,将委托对象能否响应协议方法缓存其中。
struct {
unsigned numberOfSectionsInTableView : 1;
unsigned titleForHeaderInSection : 1;
unsigned titleForFooterInSection : 1;
unsigned commitEditingStyle : 1;
unsigned canEditRowAtIndexPath : 1;
} _dataSourceHas;
7. 将类的实现代码分散到便于管理的数个分类中
使用分类机制把类的实现代码按功能划分成易于管理的小块。这样类中的方法也不会过于臃肿,使用分类也便于调试。
将私有方法归入名叫Private的分类中,以隐藏细节。
8. 使用类拓展(匿名分类)隐藏实现细节
通过类扩展向类中新增实例变量,也把私有方法的声明放在其中。
如果某属性在主接口声明readonly,而类内部又要设置方法修改此属性,那就在类拓展中将其改为readwrite。
如果想让遵守的协议不为人知,则可在类拓展中声明。
9. 通过协议提供匿名对象
协议可在某种程度上提供匿名类型。具体的对象类型可以淡化成遵守某协议的id类型,协议里规定对象所应实现的方法。如果具体类型不重要,重要的是对象能够响应(定义在协议里)的特定方法,那么可以使用匿名对象来表示。
- (void)setValue:(id<NSCopying>)value forKey:(NSString *)key
10. 在dealloc方法中只释放引用并解除监听
在dealloc方法中,应该做的事就是释放指向其他对象的引用,并取消监听(KVC或通知)。如果对象持有文件描述符或套接字等系统资源,应该在dealloc之前提供一个close方法来释放资源。
执行异步任务的方法不应在dealloc里调用,只能在正常状态下执行的那些方法也不应在dealloc里调用,因为此时对象已经处于被回收阶段。
11. 编写“异常安全代码”时留存内存管理问题
如果手动管理引用计数,而且必须捕获异常,一定要注意将try内所创立的对象清理干净。
UIView *view = nil;
@try{
view = [[UIView alloc] init];
[view addSubview:[UIView new]];
}
@catch(...){
NSLog(@"there was an error")
}
@finally{
[view release];
}
若只用ARC且要捕获异常,则需要打开编译器的-fobjc-arc-expections标志,因为ARC不自动生成安全处理异常所需的清理代码。开启标志后,编译器会自动生成这种代码,不过会导致程序变大,降低运行效率。在发现大量异常捕获操作时,应该考虑重构代码,使用NSError错误传递法来取代异常。
12. 用僵尸对象调试内存管理问题
系统在回收对象时,可以不将其真的回收,而是将它转成僵尸对象。通过环境变量NSZombieEnabled可开启此功能。
系统会修改对象的isa指针,另其指向特俗的僵尸类,从而使该对象变成僵尸对象。僵尸类能响应所有的选择子,响应方式为:打印一条包含消息内容及其接收者的消息。然后终止程序。
13.多用块枚举,少用for循环
遍历collection有四种方法。
- for循环
- NSEnumerator遍历法
NSArray *array = @[@"2",@"3",@"4"];
NSEnumerator *enumer = [array objectEnumerator];
id object;
while ((object = [enumer nextObject]) != nil){
NSLog(@"%@",object);
}
- 快速遍历法
//反向
for (id object in [array reverseObjectEnumerator]) {
NSLog(@"%@",object);
}
- 块枚举法
[array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSLog(@"%@",obj);
//遍历到下标1就停止
if (idx == 1) {
*stop = YES;
}
}];
块遍历法是最新、最先进的。块遍历法可以获取更多信息,也可以修改方法签名(id obj -> 确定类型),避免类型转换。另外,块遍历法本身能通过GCD并发执行遍历操作,无需另外编写代码,而其他遍历方式则无法轻易实现这一点。
16.对自定义其内存管理语义的collection使用无缝桥接
通过无缝桥接技术,可以在Foundation框架的OC对象和CoreFoundation框架的C语言结构体之间来回转换。
NSArray *array = @[@"1",@"2",@"3"];
//cfArray是指向struct__CFArray的指针
CFArrayRef cfArray = (__bridge CFArrayRef)array;
NSLog(@"count = %ld",CFArrayGetCount(cfArray));
在CoreFoundation层面创建collection时,可以指定许多回调函数来处理其元素。然后,通过无缝桥接技术,将其转换为具备特殊内存管理语义的OC对象。
14.构建缓存时选用NSCache而非NSDictionary
实现缓存时应选用NSCache而非NSDictionary。因为NSCache可以提供优雅的自动删减功能,而且是线程安全的,此外,它与字典不同,并不会拷贝键。
可以给NSCache对象设置上限,用以限制缓存中对象个数及总成本,而这些尺度则定义了删减其中对象的时机。但是绝对不能把这些尺度当成可靠的硬限制,它们仅对NSCache起指导作用。
- (void)downloadDataWithURL:(NSURL *)url{
NSPurgeableData *cahceData = [_cache objectForKey:url];
if (cahceData) {
//purge引用计数 +1,
[cahceData beginContentAccess];
[self useData:cahceData];
//purge引用计数 -1,变为0告诉系统必要时可以丢弃自己占据的内存
[cahceData endContentAccess];
}else{
//网络下载数据
NSData *fetchData = [self fetchDataWithURL:url];
NSPurgeableData *purgeableData = [NSPurgeableData dataWithData:fetchData];
[_cache setObject:purgeableData forKey:url cost:purgeableData.length];
//不需要beginContentAccess,类似于内存管理,创建的过程purge引用计数也会加1
[self useData:cahceData];
[cahceData endContentAccess];
}
}
将NSPurgeableData和NSCache配套使用,可以实现自动清除功能。当NSPurgeableData对象所占内存为系统所丢弃时,该对象自身也会从缓存中清除。
如果缓存使用得当,那么应用程序的响应速度就会提高。只有那种重新计算起来很费事的数据,才值得放入缓存,比如那些需要从网络获取或从磁盘读取的数据。
15.精简load和ininialize的实现代码
在加载阶段,如果类实现了load方法,那么系统就会调用它。分类里也可以定义此方法,类的load方法要比分类的先调用。与其他方法不同,load方法不参与覆写机制(只会实现类自身的load方法)。
在load方法中,尽量减少执行的操作,因为整个程序在执行load方法时变得阻塞,不要在里面调用可能加锁的方法,正常也不写其他任务。其主要作用是用来调试,比如在分类写方法判断是否正确加载。
首次使用某个类之前,系统会向其发送ininialize消息(惰性加载)。由于此方法遵从覆写规则,所以通常要在里面判断初始化的是哪个类。无法在编译期设定的全局常量,可以放在ininialize初始哈。
static NSMutableArray *array;
//会先调用父类的再调用自己的
+ (void)initialize{
if (self == [YCCache class]) {
//执行操作
array = [NSMutableArray array];
}
}
所以load方法和ininialize方法应该实现得精简一点,有助于保持应用程序响应能力,也能减少引入依赖环。
16.别忘了NSTimer会保留其目标对象
NSTimer对象会保留其目标,直到计时器本身失效为止,调用invalidate方法可使计时器失效。另外,一次性的计时器在触发完任务后也会失效。反复执行的计时器容易引入保留环(比如计时器和控制器),可以扩充NSTimer的功能,用Block来打破保留环。
@implementation NSTimer (YCBlocksSupport)
- (NSTimer *)yc_scheduledTimerWithTimeInterval:(NSTimeInterval)interval block:(void (^)())block repeats:(BOOL)repeats{
return [NSTimer scheduledTimerWithTimeInterval:interval target:self selector:@selector(yc_blockInvoke:) userInfo:block repeats:repeats];
}
- (void)yc_blockInvoke:(NSTimer *)timer{
void (^block)() = timer.userInfo;
if (block) {
block();
}
}
@end
17. 不要使用retainCount
虽然在ARC已将retainCount方法废弃了,但是即使在MRC中也是不应该调用的。对象的保留计数看似有用,实际上在任何给定的时间点的“保留计数”都无法反应对象生命周期的全貌。
比如像单例对象,其引用计数很大,也绝对不会变,其保留和释放操作都是空操作。即使是普通对象,可能也处于自动释放池中,其保留计数也不是准确的。retainCount可能永远不返回0,因为有时系统会优化对象的释放行为,在保留计数为1的时候就把它回收了。
三. 对象相关概念
1. 了解Objective-C的起源
Objective-C使用的是“消息结构”而非“函数调用”。区别在于使用消息结构的语言,其运行时所需执行的代码由运行环境决定,而使用函数调用的语言,则由编译器决定。
Objective-C为C语言添加了面向对象的特性,是其超集。Objective-C使用动态绑定的消息结构,在运行时才检查对象类型。接收一条消息之后,执行什么代码由编译环境决定。
2. 理解“属性”
使用属性@property,编译器会在编译器自动合成访问这些属性所需的方法,并且自动向类中添加适当类型的实例变量(以_开头)。如果想改名,可以使用@syntheszize语法。如果想阻止编译器自动合成存取方法,可以使用@dynamic关键字。
3. 理解“对象等同性”
想判断对象的等同性,需要重写“isEqual:”和hash方法。相同的对象必须有相同的哈希码,但是哈希码相同的对象却未必相同。编写hash方法时,应该使用计算速度最快且哈希碰撞几率低的算法。
不要盲目地逐个监测每个属性,应该依照具体需求来指定监测方案。
4. 理解Objective-C错误模型
只有发生了可使整个应用程序崩溃的严重错误时,才使用异常。
- (instancetype)mustOverwriteMethod{
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Must be overwriden " userInfo:nil];
}
在错误不那么严重的情况下(提醒用户即可),可以用委托方法来处理错误(将NSError传给处理异常的类)。
- (void)connection:(NSURLConnection *)connection didiFailWithError:(NSError *)error;
也可以将错误信息放在NSError对象里,经由"输出参数"返回给调用者。
- (void)test{
NSError *error = nil;
[self doSomething:&error];
if (error) {
//.....
}
}
- (void)doSomething:(NSError * __autoreleasing *)error{
//.....
}
5. 理解NSCopying协议
若想让自己写的对象有Copy功能,则需要实现NSCopying协议。若自定义的对象有可变版本和不可变版本,就要同时实现NSCopying和NSMutableCopying协议。
复制对象时需决定采用深拷贝还是浅拷贝,一般情况下推荐使用浅拷贝。如果需要深拷贝,则考虑新增一个专门进行深拷贝的方法。
四. GCD相关技巧
1. 多用派发队列,少用同步锁
有多个线程要执行同一份代码时,为防止出错,通常要使用锁来实现某种同步机制。
第一种是采用内部的同步块。
- (void)synchronizedMethod{
@synchronized(self){
//....(safe)
}
}
该实例中同步行为针对的对象使self。虽然可以保证每个对象实例都能不收干扰得运行synchronizedMethod方法,但是滥用@synchronized(self)会降低代码效率。因为共用一个同步锁的同步块,都必须按顺序执行。若是在self对象频繁加锁,那么程序可能要等另一段与此无关的代码执行完毕,才能继续执行当前代码。
另一种是直接使用NSLock对象。
_lock = [NSLock alloc] init];
- (void)synchronizedMethod{
[lock lock];
//....(safe)
[lock unlock];
}
这两种方法都很好,不过也有缺陷。同步块会导致死锁,效率也不高。直接用锁对象的话,遇到死锁会很麻烦。所以可以使用GCD加锁,更加简单、高效。
- 使用串行队列:
_syncQueue = dispatch_queue_create("com.text.www", DISPATCH_QUEUE_SERIAL);
//设置可以不用同步,所以把同步派发改成异步派发。但是异步派发需要拷贝代码块,所以在执行代码块的任务比较繁重时才考虑这样子做
- (void)setSomething:(NSString *)something{
dispatch_async(_syncQueue, ^{
_something = something;
});
}
- (NSString *)something{
__block NSString *temp;
dispatch_sync(_syncQueue, ^{
temp = _something;
});
return temp
}
- 使用并行队列
_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
- (void)setSomething:(NSString *)something{
dispatch_barrier_async(_syncQueue, ^{
_something = something;
});
}
- (NSString *)something{
__block NSString *temp;
dispatch_sync(_syncQueue, ^{
temp = _something;
});
return temp
}
将同步和异步派发结合起来,可以实现和普通加锁机制一样的同步行为,这样做不会阻塞执行异步派发的线程。使用同步队列和栅栏块,可以使同步行为更加高效。
2. 多用GCD,少用performSelector系列方法
performSelector系列方法在内存管理方法容易有疏失。它无法确定将要执行的选择子具体是什么,因此ARC编译器无法适当地插入内存管理方法。
performSelector系列方法所能处理的选择子(方法)太过局限,选择子的返回值类型(只能是id,即对象)及发送给方法的参数个数(2个)都受到限制。
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2
所以应该要用对应的GCD方法来替代。
3. 掌握GCD(派发队列)和操作队列的使用时机
GCD是纯C的API,操作队列(NSOperation和NSOperationQueue)是Objective-C的对象。使用操作队列的优点有:
- 取消某个操作。(GCD添加到队列中不能取消了,NSOperation在运行任务前可以cancel,不过已经启动的任务也取消不了。)
- 指定操作间的依赖关系。
- 通过键值观察监控NSOperation的属性,用比GCD更精细的方式来监听到任务状态的改变。(比如监听isCancelled或isFinished)
- 指定操作的优先级。(GCD的优先级是针对队列来说的。NSOperation的线程优先级决定了运行此操作的线程处于何种优先级上。)
- 重用NSOperation对象。
操作队列有很多地方胜过派发队列,提供了多种执行任务的方法。但是具体选择还要看运用场景。
4.不要使用dispatch_get_current_queue
//创建A、B两个串行队列
dispatch_queue_t queueA = dispatch_queue_create("com.test.queueA", NULL);
dispatch_queue_t queueB = dispatch_queue_create("com.test.queueB", NULL);
dispatch_sync(queueA, ^{
dispatch_sync(queueB, ^{
dispatch_sync(queueA, ^{
//deadLock
});
});
});
这段代码执行到最内层的派发操作时,总会死锁。最里面的任务(最里层的dispatch_sync)是加到A的队列后面,所以必须最外层的dispatch_sync执行完,而最外层的dispatch_sync又必须等里面所有的任务执行完(包括最里层的dispatch_sync)。
为了怕重入(从原来的串行队列又派发任务),可能想使用dispatch_get_current_queue判断是不是之前的队列来解决,代码如下。
dispatch_sync(queueA, ^{
dispatch_sync(queueB, ^{
dispatch_block_t block = ^{ /*任务*/ };
if (dispatch_get_current_queue() == queueA) {
block();
}else{
dispatch_sync(queueA, block);
}
});
});
实际上,这样也不能避免。因为dispatch_get_current_queue返回的是queueB而不是queueA。
队列之间会形成一套层级体系,意味着排在某个队列的块,会在其上级队列中执行。层级里地位最高的总是那个全局并发队列。dispatch_get_current_queue获取的是它当前的队列。
要解决这个问题,最好的方法就是使用dispatch_get_current_queue给队列设置标识,然后判断是否是原队列。
//创建A、B两个串行队列
dispatch_queue_t queueA = dispatch_queue_create("com.test.queueA", NULL);
dispatch_queue_t queueB = dispatch_queue_create("com.test.queueB", NULL);
dispatch_set_target_queue(queueB, queueA);
static int specificKey;
CFStringRef specificValue = CFSTR("queueA");
dispatch_queue_set_specific(queueA, &specificValue, &specificKey, (dispatch_function_t)CFRelease);
dispatch_sync(queueA, ^{
dispatch_sync(queueB, ^{
dispatch_block_t block = ^{ /*任务*/ };
//通过key获取标识符
CFStringRef retrievedValue = dispatch_get_specific(&specificKey);
if (retrievedValue) {
block();
}else{
dispatch_sync(queueA, block);
}
});
});