第七章 系统框架(EffectiveObjective-C)

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.如图

retainCycle.png
  • 可以扩充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参数保留了块,最后,实例本身还要保留计时器.如图

aRetainCycle.png

解决办法可以改用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引用.保证实例在执行期间继续存活.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容