1 第一节
1.多有块枚举,少用for循环
- 遍历collection有四种方法,最基本的办法是for循环,其次是NSEnumerator遍历法及快速遍历法,最新最先进的法则是"块枚举法"
NSArray *array = [NSArray array];
//for循环遍历
for (int i = (int)(array.count - 1); i>0; i--) {
id obj = array[i];
//do something with obj
}
//NSEnumerator遍历法
NSEnumerator *enumerator = [array reverseObjectEnumerator];
id obj;
//enumerator关键方法是nextobject,他返回枚举里的下个对象,每次调用该方法时,其内部数据结构都会更新
//使得下次调用方法时能返回下个对象,等到枚举中的全部对象都已返回之后,再调用就返回nil.这表示到末端了
while ((obj = [enumerator nextObject]) != nil) {
//do something with obj
}
//快速遍历法
//某个对象支持快速遍历,就表示该对象遵守了NSFastEnumeration协议
for (id obj in array) {
//do something with obj
}
//块枚举法
//NSEnumerationReverse 反向
[array enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
//do something with obj
}];
- "块枚举法"本事就是通过GCD来并发执行遍历操作,无效另行编写代码,而采用其他遍历方式则无法轻易实现这一点
- 若提前知道代遍历的collection含有何种对象,则应该修改块签名,指出对象的具体类型
NSDictionary *dictionary = @{@"key1":@"value1",@"key2":@"value2",@"key3":@"value3"};
//指定对象的精确类型之后,编译器就可以检测出开发者是否调用了该对象所不具备的方法,并在发现这种问题的 //时候报错
[dictionary enumerateKeysAndObjectsUsingBlock:^(NSString *key,NSString *obj, BOOL * _Nonnull stop) {
//do something
}];
2 对自定义其内存管理语意的collection使用无缝桥接
Function框架定义了NSDictionary,NSArray,NSSet等collection所对应的OC类,与之相似,CoreFoundation框架也定义了一套C语言API,用于操作表示这些collection及其他各种collection的数据结果.如NSArray是Foundation框架中表示数组的OC对象,而CFArray则是CoreFoundation框架中的等价物.这2中创建数组的方式有区别,但是我们可以使用"无缝桥接"(toll-free bridging)来平滑转换.如
NSArray *anArray = @[@1,@2,@3];
CFArrayRef aCFArray = (__bridge CFArrayRef)(anArray);
NSLog(@"size of array = %li",CFArrayGetCount(aCFArray));
//output:size of array = 3
转换操作中的__bridge
告诉ARC,如何处理转换所涉及的OC对象.
__bridge
:本事的意思是:ARC仍然具备这个OC对象的所有权
__bridge_retained
:则与之相反,意味着ARC将交出对象的所有权.若是前面那段代码改用他来实现,那么用完数组之后要加上CFRelease来手动释放.
__bridge_transfer
:可以把CFArrayRef转换为NSArray*,并且相令ARC获得所有权,那么就可以采用此种方式转换
何种情况下会使用呢?
其实Foundation框架中的collection对象拥有CoreFoundation中的collection结构所不具备的功能.然而使用Foundation中的字典对象时会遇到一个大问题,那就是其键的内存管理语意为"拷贝",而值的语意却是"保留".除非使用强大的无缝桥接技术,否则无法改变其语义.
CoreFoundation中有个CFMutableDictionary.创建方法:
CFDictionaryCreateMutable(
<#CFAllocatorRef allocator#>,//第一个参数内存分配器,一般传NULL,表示使用默认的分配器
<#CFIndex capacity#>,//字典初始大小,它并不会限制字典的最大容量,只是向分配器提示了一个开始应该分配多少内存
<#const CFDictionaryKeyCallBacks *keyCallBacks#>,//指向结构体的指针如下
<#const CFDictionaryValueCallBacks *valueCallBacks#>)//指向结构体的指针如下
typedef struct {
CFIndex version;
CFDictionaryRetainCallBack retain;
CFDictionaryReleaseCallBack release;
CFDictionaryCopyDescriptionCallBack copyDescription;
CFDictionaryEqualCallBack equal;
CFDictionaryHashCallBack hash;
} CFDictionaryKeyCallBacks;
typedef struct {
CFIndex version;
CFDictionaryRetainCallBack retain;
CFDictionaryReleaseCallBack release;
CFDictionaryCopyDescriptionCallBack copyDescription;
CFDictionaryEqualCallBack equal;
} CFDictionaryValueCallBacks;
关于2个结构体指针的说明:
第一个参数version参数目前应设为0.
第二个参数函数指针,接受2个参数,类型分别为CFAllocatorRef与void *.传入的value参数表示即将存入字典的键或值.返回的void *则表示要加到字典的最终值
typedef const void * (*CFDictionaryRetainCallBack)(CFAllocatorRef allocator, const void *value);
如下实现:把即将加入字典的值照样返回,用它来充当retain回调函数来创建字典,则不会保留键与值
const void * CustomCallBack(CFAllocatorRef allocator,const void *value){ return value;
}
后面几个参数同第二个参数.
下列完整的展示了这种字典的创建:
const void * EOCRetainCallBack(CFAllocatorRef allocator,const void *value){
return CFRetain(value);
}
void EOCReleaseCallBack(CFAllocatorRef allocator,const void *value){
CFRelease(value);
}
- (void)createAnCFDictionary{
CFDictionaryKeyCallBacks keyCallBacks = {
0,
EOCRetainCallBack,
EOCReleaseCallBack,
NULL,
CFEqual,
CFHash
};
CFDictionaryValueCallBacks valueCallBacks = {
0,
EOCRetainCallBack,
EOCReleaseCallBack,
NULL,
CFEqual
};
CFMutableDictionaryRef aCFDictionary = CFDictionaryCreateMutable(NULL, 0, &keyCallBacks, &valueCallBacks);
NSMutableDictionary *anNSDictionary = (__bridge_transfer NSMutableDictionary *)aCFDictionary;
}
在设定回调函数的时候,copyDescription取值NULL,表示采用默认实现.而equal与hash回调函数分别设为CFEqual与CFHash.与NSMutableDictionary的默认实现相同.CFEqual最终会调用NSObject的"isEqual"方法
键与值所对应的retain与release回调函数指针分别指向EOCRetainCallBack与EOCReleaseCallBack函数.我们在使用Foundation框架中的dictionary加入键值的时候,字典会自动拷贝键,保留值.如果用作键的对象不支持拷贝操作,会报错
在使用CoreFoundation创建的字典,我们修改了内存管理语意,对键执行保留而非拷贝操作
3 构建缓存时选用NSCache而非NSDictionary
在开发应用程序时,经常会遇到需要用缓存的时候.在实现缓存的时候我们应该使用NSCache而非NSDictionary.因为:
- 当系统资源将要耗尽时候,NSCache会自动删减缓存,如果使用NSDictionary需要手动删减.此外NSCache还会先删除"最久未使用的"对象
- NSCache不会拷贝键,而是会保留它.此行为可自己实现NSDictionary(见上面一点).NSCache不拷贝的原因,很多时候,键都不支持拷贝操作的对象来充当的.
- NSCache是线程安全的.NSDictionary绝对不具备
- 使用NSCache可以操控缓存删除的时机,一个是缓存中对象的总数,一个是"总开销".当对象总数或者总开销超过上线,缓存就"有可能"会删减其中对象了.
注意:向缓存中添加对象时,只有在能很快计算出"开销值"的情况下,才应该考虑第四点.若计算过程很复杂,那么按照这种方式来使用缓存就达不到最佳效果,因为每次先缓存中放入对象,还要专门花时间来计算这个附加因素的值.而缓存的本意则是要增加应用程序响应用户操作的速度.例如如果对象是NSData对象,那么就不妨指定"开销值",可以把数据大小当作"开销值"来用,因为NSData对象的数据大小是已知的,只需要读取这一属性.
下面代码演示缓存的用法:
typedef void(^SJNetWorkToolCompletionHandler) (NSData *data);
@interface SJNetWorkTool : NSObject
- (instancetype)initWithURL:(NSURL *)url;
- (void)startWithCompletionHandler:(SJNetWorkToolCompletionHandler)handler;
@end
#import "SJNetWorkTool.h"
@implementation SJNetWorkTool{
NSCache *_cache;
}
- (instancetype)init{
if (self = [super init]) {
_cache = [NSCache new];
//chache a maximum of 100 urls
_cache.countLimit = 100;
// set a cost limt of 5MB
_cache.totalCostLimit = 5 *1024 * 1024;
}
return self;}
- (void)downLoadDataForURL:(NSURL *)url{
NSData *cacheData = [_cache objectForKey:url];
if (cacheData) {
[self useData:cacheData];
}else{
SJNetWorkTool *tool = [[SJNetWorkTool alloc] initWithURL:url];
[tool startWithCompletionHandler:^(NSData *data) {
[_cache setObject:data forKey:url cost:data.length];
[self useData:data];
}];
}}
- (void)useData:(NSData *)data{
//use data do something
}
@end
还有个类叫做NSPurgeableData,和NSCache搭配起来效果更好,此类是NSMutableData的子类,而且实现了NSDiscardableContent协议,如果某个对象所占内存能够根据需要随时丢弃,那么就可以实现改协议所定义的接口.也就是说.当系统资源紧张时,可以把保存NSPurgeableData对象的那块内存释放.NSDiscardableContent协议里定义了名为isContendDiscarded的方法,用来查询相关内存是否已释放.
如果需要访问某个NSPurgeableData对象,可以调用其beginContendAccess方法,告诉他现在还不应该丢弃自己所占据的内存.用完之后,调用endContendAccess方法,告诉他在必要时可以丢弃自己所占据的内存.
如果将NSPurgeableData对象加入NSCache,那么当该对象为系统所丢弃时,也会自动从缓存总移除.通过NSCache的evictsObjectsWithDiscardedContend属性,可以开启或关闭此功能.
刚才哪个例子可用NSPurgeableData改写如下:
- (void)downLoadDataForURL:(NSURL *)url{
NSPurgeableData *cacheData = [_cache objectForKey:url];
if (cacheData) {
//stop the data being purged
[cacheData beginContentAccess];
//use the cacheData
[self useData:cacheData];
//mark that the data may be purged again
[cacheData endContentAccess];
}else{
SJNetWorkTool *tool = [[SJNetWorkTool alloc] initWithURL:url];
[tool startWithCompletionHandler:^(NSData *data) {
NSPurgeableData *purgeableData = [NSPurgeableData dataWithData:data];
//cache
[_cache setObject:purgeableData forKey:url cost:purgeableData.length];
//don't need to beginContentAccess as it begin
//use the cacheData
[self useData:purgeableData];
//mark that the data may be purged again
[purgeableData endContentAccess];
}];
}
}
注意:创建好的NSPurgeableData对象之后,其"purge引用计数"会多1.所以无需在调用beginContentAccess了.然而其后必须调用endContentAccess,将多出来的这个"1"抵消掉
4 精简initialize 与 load 的实现代码
//在类被加载的时候调用
+ (void)load {
}
//在类被使用的时候调用
+ (void)initialize{
}
- 在加载阶段,如果类实现了load方法,那么系统就会调用它.分类里也可以定义此方法,类的load方法要比分类的先调用.与其他方法不同,load方法不参与覆写机制.(也就是说,如果当前来没有实现load方法,就算父类实现了load方法,也不会调用父类的load方法)
- 首次使用某个类之前,系统会向其发送initialize消息,由于此方法遵从普通的覆写规则,所以通常应该在里面判断当前要初始化的是哪个类
+ (void)initialize{
if (self == [SJNetWorkToolclass]) {
//do something
}
}
- 无法在编译期设定的全局常量(如NSArray等),可以放在initialize方法里面初始化
- 先加载(load)父类再加载子类,先初始化(initialize)父类再初始化子类
- 无论是load还是initialize中都最好不要引入其他类,或者本类的方法.因为其他类可能还没有加载或者初始化完成
5 别忘了NSTimer会保留其目标对象
- NSTimer对象会保留其目标值,知道计时器本身失效为止,调用invalidate方法可令计时器失效,另外,一次性的计时器在触发完任务之后也会失效
- 反复之下任务的计时器(repeating timer),很容易引入保留环.如果这种计时器的目标对象又保留了计时器本身.那肯定会导致保留环.这种环状保留关系,可能是直接发生的,也可能是通过对象图里的其他对象间接发生的.
@interface SJTool : NSObject
- (void)startPolling;
- (void)stopPolling;
@end
#import "SJTool.h"
@implementation SJTool{
NSTimer *_pollTimer;
}
- (void)startPolling{
_pollTimer = [NSTimerscheduledTimerWithTimeInterval:5.0target:selfselector:@selector(p_doPoll) userInfo:nilrepeats:YES];
}
- (void)stopPolling{
[_pollTimerinvalidate];
_pollTimer = nil;
}
- (void)p_doPoll{
//do poll thing
}
- (void)dealloc{
[_pollTimerinvalidate];
}
@end
上面的例子中,SJTool强引用了timer,由于timer是repeat的所以会强引用SJTool.如图
- 可以扩充NSTimer的功能,用块来打破保留环.不过除非timer将在公共接口里提供此功能,否则必须创建分类,将相关代码加入其中
@interface NSTimer (BlocksSupport)
+ (NSTimer *)sj_scheduledTimerWithTimeInterval:(NSTimeInterval)interval block:(void(^)())block repeats:(BOOL)repeats;
@end
#import "NSTimer+BlocksSupport.h"
@implementation NSTimer (BlocksSupport)
+ (NSTimer *)sj_scheduledTimerWithTimeInterval:(NSTimeInterval)interval block:(void (^)())block repeats:(BOOL)repeats{
return [NSTimerscheduledTimerWithTimeInterval:interval target:selfselector:@selector(sj_blockInvoker:) userInfo:[block copy] repeats:repeats];
}
+ (void)sj_blockInvoker:(NSTimer *)timer{
void(^block)() = timer.userInfo;
if (block) {
block();
}
}
@end
创建timer的时候引入分类
#import "NSTimer+BlocksSupport.h"
- (void)startPolling{
_pollTimer = [NSTimersj_scheduledTimerWithTimeInterval:5.0block:^{
[selfp_doPoll]; } repeats:YES];
}
仔细看看代码,还是有保留环,因为块捕获了self变量,所以块要保留当前类的实例self.而计时器又通过userInfo参数保留了块,最后,实例本身还要保留计时器.如图
解决办法可以改用weak修饰当前引用计时器的实例对象self
- (void)startPolling{
//before
// _pollTimer = [NSTimer sj_scheduledTimerWithTimeInterval:5.0 block:^{
// [self p_doPoll];
// } repeats:YES];
//now
__weakSJTool *weakSelf = self;
_pollTimer = [NSTimersj_scheduledTimerWithTimeInterval:0.5block:^{
SJTool *strongSelf = weakSelf;
[strongSelf p_doPoll];
} repeats:YES];
}
在这段代码中采用了一种很有效的写法,weak-strong-dance.先定义一个弱引用,令其指向self,然后使块捕获这个引用,而不直接去捕获普通的self变量.也就是说,self不会为计时器所保留.当块开始执行时.立刻生成strong引用.保证实例在执行期间继续存活.