第五章 内存管理
第29条 理解引用计数
虽然现在Objective-C的内存管理方式是ARC(自动引用计数),但写出合格的代码还是要关注它的内存管理机制
Objective-C 语言利用引用计数机制来管理内存
什么是引用计数机制?
每个OC对象都有一个可以递增或递减的计数器,想让这个对象继续存活就递增,用完之后就递减,当计数变为0就表示没有人需要这个对象了,这个对象就可以被销毁。
引用计数工作原理
NSObject协议声明了三个方法来操作每个对象的引用计数
- retain 递增引用计数
- release 递减引用计数
- autorelease 稍后清理“自动释放池”的时候,再递减引用计数
一个对象被创建出来后,引用计数至少为1(在alloc或其他初始化方法中也许还有其他对象保留了此对象,所以引用计数可能大于1) ,若想让这个对象继续存在,就要调用retain方法,若是某部分代码不再使用此对象,不需要她继续存在,就调用release或autorelease方法,当对象的引用计数归零时,它就被回收了,系统会将它占用的内存标记为“可重用”状态
应用程序在它的声明周期内会创建许多对象,这些对象都又互相的联系,就像一棵树,如果按照“引用树”回溯,那么最后是一个“根对象”,(在Mac OS X 程序中 根对象是NSApplication,在iOS程序中根对象是UIApplication,两者都是程序启动时创建的单例)
对了避免在不经意间使用无效对象,一般调用完release之后要清空指针,这样就不会出现可能指向无效对象的指针(野指针)
NSMutableArray *array = [NSMutableArray array];
NSNumber *number = @10;
[array addObject:number];
[number release];
number = nil;//如果这里没有置空,而是访问了number对象,则有可能出现野指针,因为number对象此时的引用计数可能已经为0
属性存取方法中的内存管理
一个对象要持有另一个对象,一般通过访问“属性”来实现,如果属性的修饰符为“strong”,则属性会保留,例如一个属性为person,由_person实例变量实现,那么它的set方法应该是这样
- (void)setPerson:(Person *)person {
[person reatin];
[_person release];
_person = person;
}
注意一定要先调用[person reatin];
再调用[_person release];
,否则这个实例变量有可能被系统永久回收,后续的retain方法 无法让它起死回生,造成野指针
自动释放池
release方法会理解递减对象的引用计数,autorelease则会在稍后递减引用计数,通常会在下一次事件循环(event loop)时递减
autorelease可以延长对象生命周期,使对象在跨越方法调用边界后依然可以存活一段时间
例如:
- (NSString *)stringValue {
NSString *str = [[NSString alloc] initWithFormat:@"I an this:%@",self];
return str;
}
这段代码 如果在return str
之前没有调用release方法,那么str对象的引用计数就会一直大于0(有可能是1,有可能大于1),但是如果执行了release方法的话,还没等这个方法返回,系统就把str回收了,所以此时应该用autorelease,保证str对象在跨越“方法调用边界”后一定存活,然后释放操作会在清空最外层自动释放池时执行(除非有自己的自动释放池,否则这个释放的时机指的就是当前线程的下一次事件循环)
所以应该这样写
- (NSString *)stringValue {
NSString *str = [[NSString alloc] initWithFormat:@"I an this:%@",self];
return [str autorelease];
}
保留环
保留环(通常叫做循环引用)是指多个对象呈环状地互相引用,会导致内存泄漏,此时循环中的每个对象都至少有另外一个对象引用着,永远不会被释放
在垃圾收集环境中,这种情况会被认定为“孤岛”,垃圾收集器会把循环中的对象全部回收,但是在引用计数机制中则不会,需要利用“弱引用”的方式来解决
内存泄漏指的是没有正确释放已经不再使用的内存
第30条 以ARC简化引用计数
Clang编译器的“静态分析器”(static analyzer)会指出程序中引用计数出现问题的地方,所以也可以利用它在合适的地方预先加入适当的保留或释放操作以避免内存为题,这正式自动引用计数的机制(ARC)由来
在ARC环境下,该执行的引用计数操作还是要执行的,只不过这些代码由ARC自动添加,所以retain
release
aurorelease
dealloc
这些方法不可以在ARC环境中主动去调用
为了节省资源使用,ARC会直接调用retain
release
aurorelease
dealloc
这些方法对应的C语言底层函数
ARC除了自动调用上述方法外,还会对MRC下的某些操作进行优化,比如会在编译期把能够互相抵消的retain、release、autorelease同时移除。
注意:CoreFoundation对象不归ARC管理,开发者必须适时调用CFRetain/CFRelease
第31条 在dealloc方法中只释放引用并解除监听
当对象的保留计数降为0的时候,会调用dealloc方法,系统会将对象回收
开发者不应该主动调用dealloc方法
可以再dealloc方法中执行的操作
- 释放本对象引用的所有对象(ARC会自动完成)
- 释放本对象引用的非OC对象,例如CoreFoundation对象
- 清理原来配置过的监测行为(KVO或者NSNotificationCenter通知)
注意:
- 如果对象持有某些开销较大或者系统内稀缺的资源,则需要在这些资源使用完之后立即调用特定的方法来释放他们,不能等到dealloc方法执行时再调用,(因为这样做有可能会使这些稀缺资源被保留的时间过长,而且对象的dealloc方法在有些情况下可能不会被执行)
- 不要在dealloc方法里执行异步任务的方法,也不要执行只能在正常状态下才可以执行的方法,因为此时对象已经出去正在回收的状态了
第32条 编写“异常安全代码”时留意内存管理问题
- 捕获异常时,一定要注意try块内创建的对象清理干净
- 在默认情况下,ARC不生成安全处理异常所需要的清理代码。开启编译器标志后,可生成这种代码不过会导致应用程序变大,而且会降低运行效率。
第33条 以弱引用避免保留环
保留环会导致内存泄漏,泄漏后系统便无法再访问其中的对象了,应该避免
最简单的保留环是由两个对象相互引用引起的
@interface EOCClassA : NSObject
@property (nonatomic, strong) EOCClassB *other;
@end
@interface EOCClassB : NSObject
@property (nonatomic, strong) EOCClassA *other;
@end
image
多个对象之间也可能保留环
image可以利用弱引用的方式避免保留环,弱引用代表对象之间的非拥有关系,在代码中weak
修饰属性即可,系统将此属性对象回收后,属性值会自动设为nil,比较安全(ARC环境下),(unsafe_unretained也可以修饰属性达到非拥有关系,但它是不安全的,在系统回收这个属性对象后,它不会自动被设为nil,继续访问就会出现异常)
第34条 以“自动释放池块”降低内存峰值
自动释放池(autorelease pool)是Objective-C的引用计数架构的一项特性,释放对象可以调用release方法使它的引用计数立即递减,也可以调用autorelease方法,将这个对象加入自动释放池中,自动释放池中可能会存在多个对象,清空自动释放池时,系统会向池中所有的对象发送一条release消息
创建自动释放池
@autoreleasepool {
//……
}
每条线程中默认都会有自动释放池,每次执行“事件循环”(event loop)时,就会将其清空。所以不需要自己创建自动释放池
自动释放池可以嵌套,系统会在最内层的池里释放对象,在自动释放池范围末尾处向里面的对象发送release消息
可以灵活使用自动释放池来降低程序运行时的内存峰值
例如
NSArray *dataArray = model.daraArray;
NSMutableArray *people = [NSMutableArray array];
for (NSDictionary *record in dataArray) {
@autoreleasepool {
Person *p = [[Person alloc] initWithRecord:record];
[people addObject:p];
}
}
在ARC之前可能会用到NSAutoreleasePool
对象,这个对象专门表示自动释放池,但是它更加重量级,通常用来创建那种偶尔需要清空的池,而现在@autoreleasepool方法因为更轻量和作用域优势所以更好用
第35条 用“僵尸对象”调试内存管理问题
向已经回收的对象发送消息是不安全的,除非对象所占用的内存还没有为其他内容所覆写,但这是无法确定的事情,如果这块内存恰好被一个新对象占用,新对象就会收到这条消息,收到后也许能响应,也许不能,不能响应就会崩溃,就算能响应,代码也不是按照代码设想的那样地去执行的,总之就是不安全。
Cocoa提供了“僵尸对象”(Zombie Object)功能,启用后系统不会将对象真正回收,而是 会把他们转换为特殊的“僵尸对象”,这样僵尸对象就会依然占据之前那块内存保证内存不被其他对象占用,而且僵尸对象如果收到消息,就会抛出异常,并把收到的消息和原来的对象告诉开发者,便于调试。
僵尸对象工作过程:系统修改被回收对象的isa指针,指向僵尸对象,僵尸类能响应所有选择子,打印包含消息内容和原对象的消息并终止程序
第36条 不要使用retainCount
-(NSUInteger)retainCount
方法用来查询对象的引用计数,在ARC中已经被废弃,但是在MRC中依然不建议使用,因为它不一定是准确的,比如对象调用了此方法查询了引用计数,但稍后系统释放了自动释放池,此对象在自动释放池中,那这次查询的引用计数就是错的
第六章 块与大中枢派发
说白了就是block和GCD
苹果公司以全新的方式设计了多线程,核心就是“块”(block)与“大中枢派发”(Grand Central Dispatch,GCD)
block属于“词法闭包”(lexical closure),开发者可以借此机制将代码像对象一样传递,GCD是一种与block相关的技术,提供了对线程的抽象
第37条 理解“块”这一概念
块(block)可以实现闭包,从技术上讲,这是一个C语言层面的特性
block类型的语法结构:return_type(^block_name)(parameters)
block与函数类似,只不过是直接定义在另一个函数里,并且能共享外层这个函数范围内的东西, 这正是他的强大之处:在于在声明它的范围里,所有变量都可以为其所捕获,例如:
int additional = 5;
int (^addBlock)(int a, int b) = ^(int a, int b){
return a + b + additional;
};
int add = addBlock(2,5);//add = 12
使用__block修饰block外面的变量可以达到在block内部修改这个变量的效果,否则编译会报错
block本身也是一个对象
block分为三类
- 栈block
- 堆block
- 全局block
block在最初定义的时候,内存区域是分配在栈上的,所以块只在定义它的那个范围内有效
void (^block)();
if ( /* some condition */ ) {
block = ^{
NSLog(@"Block A");
};
} else {
block = ^{
NSLog(@"Block B");
};
}
block();
上面这两个block,只在定义自己的范围内有效,如果出了这个范围的时候,程序覆写了它原来占用的内存,那么程序就会崩溃
问题:为什么会崩溃?并没有在外界访问这个block啊?猜想是因为块声明在外部,还有可能继续被访问,如果是一个普通的变量呢?会不会崩溃?
可以给block发送copy消息,将它拷贝到堆中,此时block变成了带引用计数的对象了,可以在定义它的范围外使用
void (^block)();
if ( /* some condition */ ) {
block = [^{
NSLog(@"Block A");
} copy];
} else {
block = [^{
NSLog(@"Block B");
} copy];
}
block();
除此之外,还有一种全局block,它不会捕捉任何状态(比如外围的变量),它占用的内存在编译期间就决定了,而且执行copy操作对它因为无效,因为在程序运行期间它不会被回收,实际上这相当于一个单例,它声明在全局内存里,不需要没次使用的时候都去栈中创建
void (^block)() = ^{
NSLog(@"This is a block");
};
第38条 为常用的块类型创建typedef
block也是有类型的,类型取决于它所接收的参数和返回值
如果我们需要重复创建同一类型的block,可以通过typedef给某一种block自定义一个类型
例如
int (^variableName)(BOOL flag, int value) =^(BOOL flag, int value){
// Implementation
return someInt;
}
可以定义为typedef int(^EOCSomeBlock)(BOOL flag, int value);
使用和改动都更加方便
EOCSomeBlock block = ^(BOOL flag, int value){
// Implementation
};
block作为参数的情况也是一样
- (void)startWithCompletionHandler: (void(^)(NSData *data, NSError *error))completion;
上述代码可以改变为
typedef void(^EOCCompletionHandler)(NSData *data, NSError *error);
- (void)startWithCompletionHandler:(EOCCompletionHandler)completion;
第39条 用handler块降低代码分散程度
对象间的通信可以通过委托协议来实现,但是代码会比较松散,不够清晰,还会有可能出现需要处理多代理的情况(多个实例需要监控时);改用block实现这个效果可以避免这些问题,使用API变得更紧凑,调试更加方便。
对比
用委托协议实现:
- (void)fetchFooData {
NSURL *url = [[NSURL alloc] initWithString:@"http://www.example.com/foo.dat"];
_fooFetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
_fooFetcher.delegate = self;
[_fooFetcher start];
}
- (void)fetchBarData {
NSURL *url = [[NSURL alloc] initWithString: @"http://www.example.com/bar.dat"];
_barFetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
_barFetcher.delegate = self;
[_barFetcher start];
}
- (void)networkFetcher:(EOCNetworkFetcher*)networkFetcher didFinishWithData:(NSData*)data
{ //判断下载器类型
if (networkFetcher == _fooFetcher) {
_fetchedFooData = data;
_fooFetcher = nil;
} else if (networkFetcher == _barFetcher) {
_fetchedBarData = data;
_barFetcher = nil;
}
}
用block实现
- (void)fetchFooData {
NSURL *url = [[NSURL alloc] initWithString:@"http://www.example.com/foo.dat"];
EOCNetworkFetcher *fetcher =
[[EOCNetworkFetcher alloc] initWithURL:url];
[fetcher startWithCompletionHandler:^(NSData *data){
_fetchedFooData = data;
}];
}
- (void)fetchBarData {
NSURL *url = [[NSURL alloc] initWithString: @"http://www.example.com/bar.dat"];
EOCNetworkFetcher *fetcher =[[EOCNetworkFetcher alloc] initWithURL:url];
[fetcher startWithCompletionHandler:^(NSData *data){
_fetchedBarData = data;
}];
}
第40条 用块引用其所属对象时不要出现保留环
如果block捕获的对象直接或间接地保留了block本身,就可能出现保留环问题
@implementation EOCClass {
EOCNetworkFetcher *_networkFetcher;
NSData *_fetchedData;
}
- (void)downloadData {
NSURL *url = [[NSURL alloc] initWithString:@"http://www.example.com/something.dat"];
_networkFetcher =[[EOCNetworkFetcher alloc] initWithURL:url];
[_networkFetcher startWithCompletionHandler:^(NSData *data){
NSLog(@"Request URL %@ finished", _networkFetcher.url);
_fetchedData = data;
}];
}
解决方案:在块中取得了data后,将_networkFetcher设为nil。
- (void)downloadData {
NSURL *url = [[NSURL alloc] initWithString:@"http://www.example.com/something.dat"];
_networkFetcher =[[EOCNetworkFetcher alloc] initWithURL:url];
[_networkFetcher startWithCompletionHandler:^(NSData *data){
NSLog(@"Request URL %@ finished", _networkFetcher.url);
_fetchedData = data;
_networkFetcher = nil;
}];
}
第41条 多用派发队列,少用同步锁
当多条线程需要执行同一份代码的时候,会出现争夺资源的情况,导致数据不同步,出现问题
可以利用GCD加锁的方法避免出现这种问题
一 使用串行同步队列
_syncQueue = dispatch_queue_create("com.effectiveobjectivec.syncQueue", NULL);
//读取字符串
- (NSString*)someString {
__block NSString *localSomeString;
dispatch_sync(_syncQueue, ^{
localSomeString = _someString;
});
return localSomeString;
}
//设置字符串
- (void)setSomeString:(NSString*)someString {
dispatch_sync(_syncQueue, ^{
_someString = someString;
});
}
此时读写操作都是在串行队列中执行的,不容易出错
将写操作放入栅栏块中,让他单独执行,将读取操作并发执行,性能更高
_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//读取字符串
- (NSString*)someString {
__block NSString *localSomeString;
dispatch_sync(_syncQueue, ^{
localSomeString = _someString;
});
return localSomeString;
}
//设置字符串
- (void)setSomeString:(NSString*)someString {
dispatch_barrier_async(_syncQueue, ^{
_someString = someString;
});
}
数据的正确性主要取决于写入操作,所以只要保证写入时,线程是安全的就可以,那么即便读取操作不会改变变量的内容,所以可以并发执行,提高效率
这里的dispatch_barrier_async方法使得操作放在了同步队列里“有序进行”,保证了写入操作的任务是在串行队列里
第42条 多用GCD,少用performSelector系列方法
开发者可以利用performSelector调用方法,还可以实现延迟执行的效果,但是有一些局限性:
- 无法处理带有多个参数的选择子
- 返回值只能是void或者对象类型
GCD所提供的方法可以弥补这些不足
比如 延后执行某个方法
// 使用 performSelector:withObject:afterDelay:
[self performSelector:@selector(doSomething) withObject:nil afterDelay:5.0];
// 使用 dispatch_after
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC));
dispatch_after(time, dispatch_get_main_queue(), ^(void){
[self doSomething];
});
在主线程执行某个任务
//使用 performSelectorOnMainThread:withObject:waitUntilDone:
[self performSelectorOnMainThread:@selector(doSomething) withObject:nil waitUntilDone:NO];
//使用 dispatch_async
//(or if waitUntilDone is YES, then dispatch_sync)
dispatch_async(dispatch_get_main_queue(), ^{
[self doSomething];
});
waitUntilDone 为YES时对应的时GCD的dispath_sync方法
第43条 掌握GCD及操作队列的使用时机
除GCD外,操作队列(NSOperationQueue
)也是一种多线程技术,它在GCD出现之前就有了,并且从iOS 4 与Max OSX 10.6开始,操作队列的底层就是由GCD来实现了
两者差别:GCD是纯C语言API,操作队列是Objective-C对象,操作队列功能更丰富,GCD性能更高
NSOperationQueue的优势:
- 取消某个操作:可以在运行任务之前在NSOperation对象上调用cancel方法来实现(但任务开始后不可取消),GCD则无法实现,因为GCD的fire and forget 架构相当于“安排好任务之后就不管了”
- 指定操作间的依赖关系:开发者可以指定操作之间的依赖关系,使特定的操作在另一个操作完成之后方可执行
- 通过KVO监听NSOperation对象的属性:可以通过监听isCancelled、isFinished等属性来判断它的不同的状态变化
- 指定操作的优先级:可以指定此操作与队列中其他操作之间的优先关系
- 重用NSOperation对象:可以继承NSOperation对象自定义一些特殊的功能
第44条 通过Dispatch Group机制,根据系统内资源状况来执行任务
dispatch group 函数可以实现“等待多个并行任务结束后那一刻执行某个任务” 这种需求,(例如同时请求多个接口拿到结果后统一刷新页面),通过dispatch group函数可以把并发执行的多个任务合为一组,调用者就可以知道这些任务何时能够全部调用完毕
//一个优先级低的并发队列
dispatch_queue_t lowPriorityQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
//一个优先级高的并发队列
dispatch_queue_t highPriorityQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
//创建dispatch_group
dispatch_group_t dispatchGroup = dispatch_group_create();
//将优先级低的队列放入dispatch_group
for (id object in lowPriorityObjects) {
dispatch_group_async(dispatchGroup,lowPriorityQueue,^{ [object performTask]; });
}
//将优先级高的队列放入dispatch_group
for (id object in highPriorityObjects) {
dispatch_group_async(dispatchGroup,highPriorityQueue,^{ [object performTask]; });
}
//dispatch_group里的任务都结束后调用块中的代码
dispatch_queue_t notifyQueue = dispatch_get_main_queue();
dispatch_group_notify(dispatchGroup,notifyQueue,^{
// Continue processing after completing tasks
});
第45条 使用dispatch_once来执行只需运行一次的线程
dispatch_once 方法可以很容易地实现“只需执行一次的线程安全代码(thread-safe single-code execution)”
使用dispatch_once方法 可以更简单地实现“单例”,它可以在保证block中的代码只执行一次的同时还可以彻底保证线程的安全(由GCD底层实现),开发者不用担心加锁或同步,而且dispatch_once性能更高
+ (id)sharedInstance {
static EOCClass *sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
注意 dispatch_onec_t onceToken 标记应该标记在static或global作用域中,可以保证编译器在每次执行获取单例方法时都会复用这个变量而不是重新创建一个
第46条 不要使用dispatch_get_current_queue
由于队列间有层级关系,所以“检查当前队列是否为执行同步派发所用的队列”这种方法并不总是奏效
image安排在某条队列中的块,会在其上层队列中执行,而层级地位最高的那个队列总是全局并发队列。
在这里,B,C中的块会在A里执行。但是D中的块,可能与A里的块并行,因为A和D的目标队列是并发队列。
正因为有了这种层级关系,所以检查当前队列是并发的还是非并发的就不会总是很准确。
第七章 系统框架
第47条 熟悉系统框架
将一系列代码封装为动态库(dynamic library),并在其中放入描述其接口的头文件,就叫做框架,有时候第三方框架使用的是静态库(static library),是因为iOS应用程序不允许第三方框架包含动态库,所以严格来说第三方这种并不叫框架
开发者应该尽量熟悉系统所提供的框架,否则可能会将系统已有的功能再手动实现一遍,而且使用系统框架还可以在系统升级后直接享受升级所带来的改进
iOS系统中主要的框架:
- Foundation框架:NSObject, NSArray,NSDictionary等,这个框架是Objective-C应用的基础
- CoreFoundation框架:C语言API,Foundation中的很多功能,都可以在这个框架中找到对应的C语言API
- CFNetwork框架:提供了C语言级别的网络通信能力
- AVFoundation框架:可以用来回来回访并录制音视频,播放音视频的框架
- CoreData框架:此框架可以使用Objective-C对象实现数据库功能,而且是跨Mac OS X 及 iOS 平台
- CoreText框架:C语言API,提供高效的文字排版及渲染操作
- UIKit框架:核心UI框架,提供了基础的UI元素和粘合机制
- CoreAnimation:OC框架,渲染图形并播放动画,它是QuzrtzCore框架的一部分
- CoreGraphics:C语言框架,低通了2D渲染所必备的数据结构与函数
- MapKit:提供地图功能
- Social:提供社交网络功能
iOS开发中有许多框架都是C语言API,这些框架可以绕过Objective-C的运行期系统,提升执行速度,但是使用这些API要注意内存管理问题
第48条 多用块枚举,少用for循环
遍历collection有四种方式
- for循环
- NSEnumerator
- 快速遍历法
- 块枚举法
最新,最先进的方式是“块枚举法”
for循环:
NSArray *anArray = /* ... */;
for (int i = 0; i < anArray.count; i++) {
id object = anArray[i];
// Do something with 'object'
}
// Dictionary
NSDictionary *aDictionary = /* ... */;
NSArray *keys = [aDictionary allKeys];
for (int i = 0; i < keys.count; i++) {
id key = keys[i];
id value = aDictionary[key];
// Do something with 'key' and 'value'
}
// Set
NSSet *aSet = /* ... */;
NSArray *objects = [aSet allObjects];
for (int i = 0; i < objects.count; i++) {
id object = objects[i];
// Do something with 'object'
}
遍历NSDictionary和NSSet的时候需要新建数组,加大了系统开销
快速遍历:
NSArray *anArray = /* ... */;
for (id object in anArray) {
// Do something with 'object'
}
// Dictionary
NSDictionary *aDictionary = /* ... */;
for (id key in aDictionary) {
id value = aDictionary[key];
// Do something with 'key' and 'value'
}
NSSet *aSet = /* ... */;
for (id object in aSet) {
// Do something with 'object'
}
比for循环更加简洁易懂,但是无法获取元素的下标
利用“块枚举法”:
NSArray *anArray = /* ... */;
[anArray enumerateObjectsUsingBlock:^(id object, NSUInteger idx, BOOL *stop){
// Do something with 'object'
if (shouldStop) {
*stop = YES; //使迭代停止
}
}];
// Dictionary
NSDictionary *aDictionary = /* ... */;
[aDictionary enumerateKeysAndObjectsUsingBlock:^(id key, id object, BOOL *stop){
// Do something with 'key' and 'object'
if (shouldStop) {
*stop = YES;
}
}];
// Set
NSSet *aSet = /* ... */;
[aSet enumerateObjectsUsingBlock:^(id object, BOOL *stop){
// Do something with 'object'
if (shouldStop) {
*stop = YES;
}
不用创建新数组,可以获取数组元素的序号,可以获取字典元素的键值,还可以随时终止遍历
块枚举还有一个优点是可以修改块的方法签名,以免进行类型转换操作:
for (NSString *key in aDictionary) {
NSString *object = (NSString*)aDictionary[key];
// Do something with 'key' and 'object'
}
NSDictionary *aDictionary = /* ... */;
[aDictionary enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop){
// Do something with 'key' and 'obj'
}];
如果我们可以知道集合里的元素类型,就可以修改签名。这样做的好处是:可以让编译期检查该元素是否可以实现我们想调用的方法,如果不能实现,就做另外的处理。这样一来,程序就能变得更加安全。
第49条 对自定义其内存管理语义的collection使用无缝桥接
Foundation中的collection类(数组、字典、集合等)在CoreFoundation中也有表示这些类相似的数据结构,比如NSArray和CFArray。
使用“无缝桥接”技术可以让这两个类型平滑地转换
比如
NSArray *anNSArray = @[@1, @2, @3, @4, @5];
CFArrayRef aCFArray = (__bridge CFArrayRef)anNSArray;
NSLog(@"Size of array = %li", CFArrayGetCount(aCFArray));
__bridge表示ARC仍然具备这个Objective-C对象的所有权,__bridge_reatined与之相反,意味着ARC将交出对象所有权
为什么要使用无缝桥接?
因为有些OC对象的特性是其对应的CF数据结构不具备的,反之亦然。因此我们需要通过无缝桥接技术来让这两者进行功能上的“互补”。
第50条 构建缓时选用NSCache而非NSDictionary
有时候从网上获取的数据(比如图片)可以缓存下来方便再次使用,避免了每次使用都重新下载,很多人首先想到的是吧数据放入字典中(NSMutableDictionary),但是Foundation框架中有一个NSCache类更适合做这件事,这个类是专门为缓存而设计的
NSCache的优势:
- 系统资源将要耗尽时自动删减缓存
- 会自行删减“最久未使用”的对象
- 在键不支持“拷贝”的情况下,比NSDictionary更方便
- NSCache 是线程安全的,用NSDictionary的话不自己处理就可能会出现多个线程争夺资源的情况
缓存中的对象总数和所有对象的“总开销”是可以自定义的(关于开销值,需要保证在能很快计算出开销值的情况下才应该调整这个尺度,否则会适得其反,因为缓存的目的就是为了减少访问时间)
NSCache的用法
// Network fetcher class
typedef void(^EOCNetworkFetcherCompletionHandler)(NSData *data);
@interface EOCNetworkFetcher : NSObject
- (id)initWithURL:(NSURL*)url;
- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)handler;
@end
// Class that uses the network fetcher and caches results
@interface EOCClass : NSObject
@end
@implementation EOCClass {
NSCache *_cache;
}
- (id)init {
if ((self = [super init])) {
_cache = [NSCache new];
// Cache a maximum of 100 URLs
_cache.countLimit = 100;
/**
* The size in bytes of data is used as the cost,
* so this sets a cost limit of 5MB.
*/
_cache.totalCostLimit = 5 * 1024 * 1024;
}
return self;
}
- (void)downloadDataForURL:(NSURL*)url {
NSData *cachedData = [_cache objectForKey:url];
if (cachedData) {
// Cache hit:存在缓存,读取
[self useData:cachedData];
} else {
// Cache miss:没有缓存,下载
EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
[fetcher startWithCompletionHandler:^(NSData *data){
[_cache setObject:data forKey:url cost:data.length];
[self useData:data];
}];
}
}
@end
这里将url设成了key,对象总数为100,开销值为5MB
NSPurgeableData
NSPurgeableData是NSMutableData的子类,通常和NSCache搭配使用,当系统资源紧张时,可以把NSPurgeableData那块内存释放。
需要访问某个NSPurgeableData对象时,调用beginContentAccess
方法,此时对象就不会丢弃自己所占的内存,使用完之后,调用endContentAccess
方法,这时候系统会在必要时回收这块内存,这两个方法类似于“引用计数”的递增和递减操作,只有“引用计数”为0时,这个对象才可以丢弃。
- (void)downloadDataForURL:(NSURL*)url {
NSPurgeableData *cachedData = [_cache objectForKey:url];
if (cachedData) {
//如果存在缓存,需要调用beginContentAccess方法
[cacheData beginContentAccess];
//Use the cached data
[self useData:cachedData];
//使用后,调用endContentAccess
[cacheData endContentAccess];
} else {
//没有缓存
EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
[fetcher startWithCompletionHandler:^(NSData *data){
NSPurgeableData *purgeableData = [NSPurgeableData dataWithData:data];
[_cache setObject:purgeableData forKey:url cost:purgeableData.length];
//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的实现代码
Objective-C中的大部分类都继承自NSObject类(基类),基类中有两个方法来实现初始化操作:load方法和initialize方法。
load方法
+(void)load
应用程序启动的时候,每个类和分类会调用这个方法(类优先于分类调用),而且只调用一次,在load方法中调用其他类是不安全的,因为load方法执行时,其他的类还不确定有没有加载完成。
load方法不遵从普通的继承规则,如果某个类本身没有实现load方法,那他的父类和超类都不会调用
load方法中包含过多逻辑的话会使程序阻塞
initialize方法
+(void)initlialize
这个方法会在程序第一次调用某个类的时候调用(如果程序还没用到这个类,是不会调用的),而且只调用一次,开发者不应该主动去调用它
initialize方法调用时系统是安全的,可以安全使用并调用任意类中的任意方法
如果子类没有实现这个方法,它的超类却实现了,那么就会运行超类的代码(遵守通常的继承规则)
#import <Foundation/Foundation.h>
@interface EOCBaseClass : NSObject
@end
@implementation EOCBaseClass
+ (void)initialize {
NSLog(@"%@ initialize", self);
}
@end
@interface EOCSubClass : EOCBaseClass
@end
@implementation EOCSubClass
@end
打印结果
EOCBaseClass initialize
EOCSubClass initialize
如果只想打印本类的可以这样
+(void)initialize {
if (self == [EOCBaseClass class]) {
NSLog(@"%@ initialized", self);
}
}
我们可以察觉到,如果在这个方法里执行过多的操作的话,会使得程序难以维护,也可能引起其他的bug。因此,在initialize方法里,最好只是设置内部的数据(比如无法在编译期设定的全局常量),不要调用其他的方法,因为将来可能会给这些方法添加其它的功能,那么会可能会引起难以排查的bug。
第52条 别忘了NSTimer会保留其目标对象
在程序中用到定时器(NSTimer
类)时,NSTimer
类会保留其目标对象(初始化方法中的target参数),但通常定时器是实例变量存放的,所以目标对象也保留了定时器,产生了保留环。
#import <Foundation/Foundation.h>
@interface EOCClass : NSObject
- (void)startPolling;
- (void)stopPolling;
@end
@implementation EOCClass {
NSTimer *_pollTimer;
}
- (id)init {
return [super init];
}
- (void)dealloc {
[_pollTimer invalidate];
}
- (void)stopPolling {
[_pollTimer invalidate];
_pollTimer = nil;
}
- (void)startPolling {
_pollTimer = [NSTimer scheduledTimerWithTimeInterval:5.0
target:self
selector:@selector(p_doPoll)
userInfo:nil
repeats:YES];
}
- (void)p_doPoll {
// Poll the resource
}
@end
不主动调用stopPolling
方法 就无法打破保留环
这种情况可以通过给NSTimer
添加一个分类来解决
#import <Foundation/Foundation.h>
@interface NSTimer (EOCBlocksSupport)
+ (NSTimer*)eoc_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
block:(void(^)())block
repeats:(BOOL)repeats;
@end
@implementation NSTimer (EOCBlocksSupport)
+ (NSTimer*)eoc_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
block:(void(^)())block
repeats:(BOOL)repeats
{
return [self scheduledTimerWithTimeInterval: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
使用时,再将self用weak修饰 打破保留环
- (void)startPolling {
__weak EOCClass *weakSelf = self;
_pollTimer = [NSTimer eoc_scheduledTimerWithTimeInterval:5.0 block:^{
EOCClass *strongSelf = weakSelf;
[strongSelf p_doPoll];
} repeats:YES];
}
部分图片、代码、笔记源自J_Knight_的博客
网友评论