前言:
虽说不使用系统框架也能编写OC代码,但几乎没人这么做,即便是NSObject这个标准的根类,也属于Foundation框架,而非语言本身。若不使用Foundation,就必须自己编写根类,同时还要自己来写collection、事件循环、以及其他会用到的类。
第47条:熟悉系统框架
用户升级操作系统后,你所开发的应用程序也可以使用最新版的系统库了。将一系列代码封装为动态库,并在其中放入描述接口的头文件,这样做出来的东西就叫框架。iOS系统框架都使用动态库。iOS程序不允许在其中包含动态库,因此第三方框架都要使用静态库。
Cocoa框架(iOS上称为Cocoa Touch)本身不是框架,只是集成了一批创建应用程序会用到的框架。
“无缝桥接”(toll-free bridging),可以把CoreFoundation中的C语言数据结构平滑转换为Foundation中的OC对象,也可以方向转换,这里参考《理解__bridge,__bridge_transfer和__bridge_retained》无缝桥接技术是用某些相当复杂的代码实现出来的,这些代码可以使运行期系统把CoreFoundation框架中的对象视为普通的Objective-C对象。
用C语言来实现API的好处是,可以绕过Objective-C的运行期系统,从而提升执行速度。当然,由于ARC只负责Objective-C的对象,所以使用这些API时尤其要注意内存管理问题。
第48条:多用块枚举,少用for循环
collection也就是我们常用的NSArray、NSDictionary、NSSet类型
有以下4种遍历方式:
第一种:for循环
第二种:NSEnumerator遍历
NSEnumerator是个抽象基类,其中只定义了两个方法,供其具体子类来实现:
- (NSArray*)allObjects;
- (nullable ObjectType)nextObject;
使用如下:
//Array
NSArray *anArray =/* ... */;
NSEnumerator *enumerator = [anArray objectEnumerator];//反向遍历用reverseObjectEnumerator
id object;
while ((object = [enumerator nextObject]) != nil)
{
// Do something with 'object'
}
// Dictionary
NSDictionary *aDictionary =/* ... */;
NSEnumerator *enumerator = [aDictionary keyEnumerator];
id key;
while ((key = [enumerator nextObject]) != nil)
{
id value = aDictionary[key];
// Do something with 'key' and 'value'
}
第三种:快速枚举
如果某个类的对象支持快速遍历,那么就可以宣称自己遵从名为NSFastEnumeration的协议,从而令开发者可以采用此语法来迭代该对象。此协议只定义了一个方法:
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state
objects:(id*)stackbuffer
count:(NSUInteger)length;
第四种:基于块的遍历方式
在当前的Objective-C 语言中美最新引入的一种做法就是基于block来遍历。NSArray和NSDictionary中定义了下面这些方法,它可以实现最基本的遍历功能:
- (void)enumerateObjectsWithOptions:(NSEnumerationOptions)options
usingBlock:(void(^)(id obj,NSUInteger idx, BOOL *stop))block;
- (void)enumerateKeysAndObjectsWithOptions:(NSEnumerationOptions)options
usingBlock: (void(^)(id key,id obj, BOOL *stop))block;
它的优点在于遍历时可以直接从块里获取更多信息。
NSEnumerationOptions类型是个enum,其各种取值可用“按位或”(bitwise OR)连接,用以表明遍历方式。例如,开发者可以请求以冰法方式执行各轮迭代,也就是说,如果当前系统资源状况允许,那么执行每次迭代所用的block就可以并行执行了。通过NSEnumerationConcurrent选项即可开启此功能。如果使用此选项,那么底层会通过GCD来处理冰法执行事宜,具体实现时很可能会用到dispatch group。
扩展:
他们的效率测试
我枚举相同的数组并打印,所用的时间如下:
0.00382315 s —->方法一
0.0019234 s —->方法二
0.00197317 s —->方法三
0.00178474 s —->方法四
可见使用块的优势所在
第49条:对自定义其内存管理语义的collection使用无缝桥接
这里参考《理解__bridge,__bridge_transfer和__bridge_retained》
这里还有个知识点就是通过桥接技术使NSDictionary不拷贝键,实现如下:
#import <Foundation/Foundation.h>
#import <CoreFoundation/CoreFoundation.h>
const void* EOCRetainCallback(CFAllocatorRef allocator,
const void *value)
{
return value;
}
void EOCReleaseCallback(CFAllocatorRef allocator,
const void *value)
{
CFRelease(value);
}
CFDictionaryKeyCallBacks keyCallbacks = {
0,
EOCRetainCallback,
EOCReleaseCallback,
NULL,
CFEqual,
CFHash
};
CFDictionaryValueCallBacks valueCallbacks = {
0,
EOCRetainCallback,
EOCReleaseCallback,
NULL,
CFEqual
};
CFMutableDictionaryRef aCFDictionary =
CFDictionaryCreateMutable(NULL,
0,
&keyCallbacks,
&valueCallbacks);
NSMutableDictionary *anNSDictinary =
(__bridge_transfer NSMutableDictionary *)aCFDictionary;
在CoreFoundation层创建字典,修改内存管理语义,对键执行“保留”而非“拷贝”操作。
第50条:构建缓存时选用NSCache而非NSDictionary
NSCache胜过NSDictionary之处在于,当系统资源将要耗尽时,它可以自动删减缓存。如果采用普通的字典,那么就要自己编写挂钩,在系统发出“低内存”(lowmemory)通知时手工删减缓存。而NSCache则会自动删减,由于其是NSFoundation框架的一部分,所以与开发者相比,它能在更深的层面上插入挂钩。此外,NSCache还会先行删减”最久未使用的”对象。若想自己编写代码来为字典添加此功能,则会十分复杂。
NSCache并不会“拷贝”键,而是会“保留”它。此行为用NSDictionary也可以实现,然而需要编写相当复杂的代码。(第49条)NSCache对象不拷贝键的原因在于:很多时候,键都是由不支持拷贝操作的对象来充当的。另外,NSCache是线程安全的。而NSDictionary则绝对不具备此优势。(备注:本人觉得这里的NSDictionary应该是说的可变的NSMutableDictionary,因为NSDictionary是线程安全的,而且不可变)
开发者在将对象加入缓存时,可为其指定”开销值“。当对象总数或总开销超过上限时,缓存就可能会删减其中的对象了,在可用的系统资源趋于紧张时,也会这么做。然而要注意,是”可能“会删减某个对象,而不是”一定“会删减某个对象。删减对象时所遵照的顺序,由具体实现来定。
向缓存中添加对象时,只有在能很快计算出”开销值“的情况下,才应该考虑采用这个尺度。若计算过程很复杂,那么照这种方式来使用缓存就达不到最佳效果了。
还有个类叫NSPurgeableData,和NSCache搭配起来用,效果很好,此类事NSMutableData的子类,而且实现了NSDiscardableContent协议。如果某个对象所占的内存能够根据需要随时丢弃,那么就可以实现该协议所定义的接口。这就是说,当系统资源紧张时,可以把保存NSPurgeableData对象的那块内存释放掉。NSDiscardableContent协议里定义了名为isContentDiscarded的方法,可用来查询相关内存是否已释放。
如果需要访问某个NSPurgeableData对象,可以调用其beginContentAccess方法,告诉它现在还不应丢弃自己所占的内存。用完之后,调用endContentAccess方法,告诉它在必要时可以丢弃自己所占的内存了。这些调用可以嵌套,所以说,它们就像递增与递减引用计数所用的方法那样。只有对象的”引用计数“为0时才可以丢弃。
如果将NSPurgeableData对象加入NSCache,那么当该对象为系统所丢弃时,也会自动从缓存中移除。通过NSCache的evictsObjectsWithDiscardedContent属性,可以开启或关闭此功能。
使用如下:
-(void)downloadDataForURL:(NSURL *)url{
NSPurgeableData *cachedData = [_cache objectForKey:url];
if(cachedData){
//Stop the data being purged
[cachedData beginContentAccess];
//Use the cached data
[self useData:cachedData];
//Mark that the data may be purged again
[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:data.length];//这里应该要调用这个有cost的方法,totalCostLimit才会有效,因为它不会自己去计算缓存大小
//Don't need to beginContentAccess as it begins
//with access already marked
//Use the retrieved data
[self useData:data];
//Mark that the data may be purged now
[purgeableData endContentAccess];
}];
}
}
注意,创建好NSPurgeableData对象之后,其”purge引用计数“会多1,所以无须再调用beginContentAccess了,然而气候必须调用endContentAccess,将多出来的这个”1“抵消掉。
第51条:精简initialize与load的实现代码
+ (void)load;
对于加入运行期系统中的每个类及分类来说,必定会调用此方法,而且仅调用一次。不管你的程序有没有用到该类。在main前调用。先调用类里的,再调用分类里的。
在执行子类的load方法之前,必定会先执行所有超类的load方法,而如果代码还依赖了其他程序库,那么程序库里相关类的load方法也必定会先执行。然而,根据某个给定的程序库,却无法判断出其中各个类的载入顺序。因此,在load方法中使用其他类是不安全的。
load并不遵从继承规则,如果某个类本身没实现load方法,不会继而去查找并调用超类的。超类的load方法会在加载超类信息时调用(如果实现了的话)。
而且load方法务必实现得精简一些,也就是要尽量减少其所执行的操作,因为整个应用程序在执行load方法时都会阻塞。如果load方法中包含繁杂的代码,那么应用程序在执行期间就会变得无响应。不要在里面等待锁,也不要调用可能会加锁的方法。总之,能不做的事情就别做。
+ (void)initialize;
对于每个类来说,该方法会在程序首次用该类之前调用,且只调用一次。它是由运行期系统来调用的,绝不应该通过代码直接调用。如果整个程序都没有用到该类,自然不会调用这个方法了。在mian启动后调用。
initialize方法与其他消息一样,如果某个类未实现它,而其超类实现了,那么就会运行超类的实现代码。因此,通常这样来实现
+(void)initialize
{
if(self = [EOCBaseClass class])
{
NSLog(@"%@ initialized",self);
}
}
运行期系统也能保证initialize方法一定会在”线程安全的环境“中执行,这就是说,只有执行initialize的那个线程可以操作类或类实例。其他线程都要先阻塞,等着initialize执行完。
load与initialize方法都应该实现得精简一些,这有助于保持应用程序的响应能力,也能减少引入”依赖环“(interdependency cycle)的几率。
第52条:别忘了NSTimer会保留其目标对象
NSTimer对象会保留其目标,直到计时器本身失效为止,调用invalidate方法可令计时器失效,另外,一次性的计时器在触发完任务之后也会失效。开发者若将计时器设置成重复执行模式,那么必须自己调用invalidate方法,才能令其停止。
由于计时器会保留其目标对象,所以设置成重复执行模式的那种计时器,很容易引入“保留环”。解决的方法如下:
#import <Foundation/Foundation.h>
@interface NSTimer(EOCBlockSupport)
+(NSTimer*) eoc_scheduledTimerWithTimeInterval:(NSTimeInterval)interval block:(void(^)())block repeats:(BOOL)repeats;
@end
@implementation NSTimer(EOCBlockSupport)
+(NSTimer*) eoc_scheduledTimerWithTimeInterval:(NSTimeInterval)interval block:(void(^)())block repeats:(BOOL)repeats{
return [self scheduledTimerWithTimerInterval:interval target:self selector:@selector(eoc_blockInvoke:) userInfo:[block copy] repeats:repeats];
}
+(void)eoc_blockInvoke:(NSTimer*)timer{
void(^block)() = timer.userInfo;
if(block){
block();
}
}
@end
-(void) dealloc{
[_pollTimer invalidate];
}
-(void) startPolling{
__weak EOCClass *weakSelf = self;
_pollTimer = [NSTimer eoc_scheduledTimerWithTimeInterval:50 block:^
{ EOCClass *strongSelf = weakSelf;
[strongSelf p_doPoll];
} repeats:yes];
}