第37条 理解“块”这一概念
block也是对象,也有引用计数,因此要避免循环引用,一个比较隐蔽的循环引用的例子是:
-(void)anInstanceMenthod{
void (^someBlock)() = ^{
_anInstanceVar = @"something";
//...
}
}
注意,通过下划线使用类的成员也会捕获self对象。其实这个在Xcode中会给出警告,所以只要注意每一个警告,还是很容易避开的。
书中有一个例子是错误的或者是过时的:
void (^block)();
if(/*some condition*/){
block = ^{
NSLog(@"block A");
}
}else{
block = ^{
NSLog(@"block B");
}
}
block();
书中说必须使用copy来将block 复制到堆中,实际上,在block=这句代码中,已经将block复制到了堆中。可以通过打印对象来查看。
可以通过以下几个问题理解block:
Block是继承自NSObject的么?
typedef void(^TestBlock)(void);
TestBlock b = ^{
//
};
if([b isKindOfClass:[NSObject class]]){
NSLog(@"TestBlock is subclass of NSObject.");
}else{
NSLog(@"TestBlock is not subclass of NSObject.");
}
//...TestBlock is subclass of NSObject.
block中不仅有isa指针,并且是继承自NSObject的。
Block的捕获规则?
int globalA = 1;
static int globalB = 1;
{//in function
int localA = 1;
static int localB = 1;
TestBlock b = ^{
globalA ++;
globalB ++;
// localA ++;
localB ++;
NSLog(@"in block: globalA:%d, globalB:%d, localA:%d, localB:%d", globalA, globalB, localA, localB);
};
globalA ++;
globalB ++;
localA ++;
localB ++;
NSLog(@"out block: globalA:%d, globalB:%d, localA:%d, localB:%d", globalA, globalB, localA, localB);
b();
}
对于全局变量和静态变量,block捕获的是指针,变量的任何修改都反映在block内外。
对于局部变量,如果没有使用__block参数修饰,block捕获的是值,block内不能修改,block外修改不反应在block内。也就是说捕获以后就是两个变量了。
{
Person *p = [[Person alloc] init];
p.name = @"1234";
TestBlock b = ^{
NSLog(@"in block before: block name:%@", p.name);
p.name = @"4321";
NSLog(@"in block after: block name:%@", p.name);
};
p.name = @"6789";
NSLog(@"out block: block name:%@", p.name);
b();
NSLog(@"out block after block: block name:%@", p.name);
}
/* output
2018-08-27 12:21:37.755459+0800 test[5184:141812] out block: block name:6789
2018-08-27 12:21:37.755573+0800 test[5184:141812] in block before: block name:6789
2018-08-27 12:21:37.755643+0800 test[5184:141812] in block after: block name:4321
2018-08-27 12:21:37.755709+0800 test[5184:141812] out block after block: block name:4321
*/
对于对象,就直接是指针捕获了,block内外的修改都能互相影响。但是,p指向的对象内容可以修改,p指向的对象不能修改。这一点和值变量是逻辑一致的。
3种block有什么区别?
Block也是一个类簇,如果类簇听着别扭,可以叫一个类的继承体系。实际的block类型是以下3种:_NSConcreteStackBlock,_NSConcreteMallocBlock,_NSConcreteGlobalBlock。
先看例子:
1)
int a = 1;
void(^block)(void) = ^{
//expressions with captured val
NSLog(@"%d", a);
};
NSLog(@"%@", block);
/* output
2018-08-27 14:08:18.042303+0800 test[6768:195779] <__NSMallocBlock__: 0x60000005c020>
*/
2)
static int globalA = 10;
{
void(^block)(void) = ^{
//expressions with captured val
NSLog(@"%d", globalA);
};
NSLog(@"%@", block);
}
/* output
2018-08-27 14:09:43.681282+0800 test[6801:197501] <__NSGlobalBlock__: 0x1034fe0b8>
*/
第二个例子比较好理解,因为引用了Global的变量,所以生成了Global的block;第一个例子只是捕获了局部变量,为啥是Malloc的呢?说好的Stack呢?LLVM ARC文档上说,把一个stack类型的block赋值给一个变量相当于给一个strong类型的变量赋值,会发生将stack block copy到堆上过程。因此打印block就是Malloc类型的。如果想要看到Stack类型,需要使用弱引用:
static int globalA = 10;
{
__weak void(^block)(void) = ^{
//expressions with captured val
NSLog(@"%d", a);
};
NSLog(@"%@", block);
}
/* output
2018-08-27 14:16:34.158457+0800 test[6930:202303] <__NSStackBlock__: 0x7ffee7086a68>
*/
所以规则就是捕获的变量声明周期如果是全局的,就是Global的,捕获的变量生命周期如果是局部的,那么创建初期是Stack的吗,一旦给被其他变量引用就编程Malloc的了。
第38条 为常用的块类型创建typedef
不使用typedef,每次要这样写:
int (^variableName)(BOOL flag, int value) = ^(BOOL flag, int value){
//...
}
使用typedef可以这样写:
typedef int(^EOCSomeBlock)(BOOL flag, int value);
EOCSomeBlock block = ^(BOOL flag, int value);
好读,好修改。
第39条 用handler块降低代码分散程度
异步调用可以使用委托回调,也可以使用block回调。block的好处是可以捕获上下文,处理更方便,使用委托就得自己保存这些上下文,在委托回调的时候拿出来用。但是block需要注意循环引用问题。
对于错误的处理,书中建议将成功和失败放在同一个block中,有些第三方库将成功的回调和失败的回调分开,这时候如果在成功的block回调后续有错误发生,还需要写错误处理的代码,可能和失败block中的代码重复。
使用block的另外一个缺点是,可能会造成很多层嵌套,代码不好读,内存管理困难。
第40条 用块引用其所属对象时不要出现保留环
这个在第37条中已经有了。主要是要注意每一个编译警告。
第41条 多用派发队列,少用同步锁
常见的一种多线程同步方式是@sychronized(self),这种锁锁的是self,如果对象有多个方法需要上锁,那么对于没有多线程问题的两个方法,同时锁self会造成性能下降。这时候应该使用多个NSLock。还有一个NSRecursiveLock,能保证一个线程多次获取一个锁不造成死锁。
OC中提倡通过GCD来进行线程同步。如果想让多线程顺序的访问一个资源,通过一个同步串行队列就可以了。
如果涉及到读写锁,使用barrierAPI,使用dispatch_barrier_async函数添加的任务,只能单独执行,可以用于写,使用dispatch_async向队列添加的任务,可以用于读。
第42条 多用GCD,少用performSelector系列方法
作者提倡使用GCD,主要还是从安全角度出发。performSelector提供的灵活性在一些框架级代码中还是常见的。
performSelector系列函数能够延迟方法的调用,根据行为动态执行方法,可以选择其他线程执行方法。
performSelector的缺陷是ARC无法插入适当的内存管理方法,参数和返回值有限制。
第43条 掌握GCD及操作队列的使用时机
GCD和NSOperationQueue的区别:
NSOperationQueue中可以指定依赖关系,内建支持执行前取消,可以通过KVO获得任务状态,支持任务级别的优先级。
总之NSOperationQueue的特性比GCD多,但是GCD比较直观,简洁。因此很多时候还是使用GCD。
第44条 通过Dispatch Group机制,根据系统资源状况来执行任务
通过把多个异步任务放到一个Dispatch Group中,可以在group上等待这些线程执行完毕。比如一组相关的网络请求,希望等请求全部返回后渲染页面,就可以使用。
Dispatch Group也支持非阻塞等待,通过dispatch_group_notify可以在异步任务执行完成后得到回调。
第45条 使用dispatch_one来执行只需运行一次的线程安全代码
这里给出一个单例的正确创建方式:
@interface MyClass : NSObject
+(instancetype)sharedInstance;
+(instancetype)init NS_UNAVAILABLE;
+(instancetype)copy NS_UNAVAILABLE;
@end
@implementation MyClass
+ (instancetype)sharedInstance {
static dispatch_once_t onceToken;
static MyClass *sharedInstance;
dispatch_once(&onceToken, ^{
sharedInstance = [[MyClass alloc] initPrivate];
});
return sharedInstance;
}
-(instancetype)initPrivate{
self = [super init];
if (self){
}
return self;
}
@end
第46条 不要使用dispatch_get_current_queue
这个函数从iOS6.0起已经弃用了。因为队列有层级关系,所以这个检查并不能避免死锁。
第47条 熟悉系统框架
到目前为止,iOS所有的第三方库都还是静态库,但是系统的框架是动态库。
Foundation,AVFoundation,CoreData,CoreAnimation是OC语言的,CFNetwork,CoreAudio,CoreText,CoreGraphics是C语言的。如果使用C语言的API,就失去了OC的runtime支持,需要自己管理内存。所以应该尽量使用高层的API。
第48条 多用枚举块,少用for循环
对于一个数组的遍历,可以用下边这种方式:
for(int i = 0; i < [arr count]; i++){
NSString *ele = arr[i];
NSLog(@"%@", ele);
}
这种是最“老式”的遍历方式,通过index获取元素。但是对于NSSet这样无序的集合遍历,就有点麻烦,一般是先将Set转换成数组,再遍历。
“新式”的遍历方式可以解决这一问题:
for(NSString *ele in arr){
NSLog(@"%@",ele);
}
使用for…in语句,可以直接拿到元素,这样对于无序的集合也可以直接遍历了。但是如果遍历的同时还想知道index怎么办?一般思路是这样的,如果一个有序的集合需要知道index,那么就使用“老式”的遍历方式,对于无序的集合,根本没有index,直接使用“新式“的遍历方式即可。
但是还有更两全其美的方法:
[arr enumerateObjectsUsingBlock:^(NSString* _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSLog(@"%lu, %@", (unsigned long)idx, obj);
}];
使用集合类型的enumerateObjectsUsingBlock方法,对于有序集合,同时返回index和index对应的元素,对于无序的集合,就没有index参数。
如果自己写的类想支持for…in遍历,只要遵从并实现NSFastEnumeration协议即可。
第49条 对自定义其内存管理语义的collection使用无缝桥接
使用C语言的API创建的数据类型是需要手动内存管理的,幸运的是Foundation框架中的“无缝桥接”(toll-free bridge)技术能帮助我们在这两种内存管理之间进行转换。
把一个OC对象转换成一个对应的C数据结构,比如NSArray到CFArrayRef的转换,使用__bridge
可以告诉ARC,ARC仍具有这个对象的管理权,转换出来的CFArrayRef仅仅是临时使用一下。如果使用__bridge_retained
,则告诉ARC交出对象的管理权,此时对象这块内存的管理权归CFArrayRef所有,所以用完之后需要调用CFRelease释放内存。反过来,如果创建了一个CFArrayRef,想要把所有权给ARC,应该使用__bridge_transfer
书中还介绍了一种通过创建CFDictionary来改变NSDictionary行为的办法。默认的NSDictionary是对键复制,对值保留。而CFDictionary提供了更多的控制接口,创建一个对键保留,对值复制的CFDictionary然后再将其所有权交给一个NSDictionary,就改变了NSDictionary的行为。
第50条 构建缓存时选用NSCache而非NSDictionary
第一,NSCache本身就是做缓存用的,在系统资源不够的时候,可以自动的删减缓存。如果使用NSDictionary需要自己处理。
第二,NSCache是线程安全的,NSDictionary不是。
有个类叫做NSPurgeableData,是NSMutableData的一个子类,实现了NSDiscardableContent协议。在使用的时候先调用beginContentAccess方法,在使用结束后调用endContentAccess方法,可以嵌套使用。当最后一个endContentAccess调用的时候,NSPurgeableData中的数据自动清空。这个数据结构可以用于大粒度的缓存控制。比如退出一个模块后,自动清除缓存,而不必等缓存自动淘汰。
最后作者提醒不好滥用缓存,只有获取数据的开销比较大的时候才使用缓存。
第51条 精简initialize与load的实现代码
在iOS程序中,+load函数在应用启动的时候就会被执行。 如果+load还依赖于其他程序库,那么那个程序库中的+load也会被执行。在+load中调用方法是不安全的,因为模块加载的顺序不一定。并且+load不遵从继承规则,如果一个类没有实现+load,那么它基类的+load不会被自动调用。 所以+load中的代码应该尽量精简,只要能不放在这里就不放在这里。
然而,作者建议不要使用+load方法,而是使用+initialize方法,后者相当于一个惰性初始化。更重要的是,+initialize方法中运行环境是安全的,可以调用其他方法,另外运行时保证+initialize是线程安全的。+initialize是遵从继承规则的。看下边代码
@interface EOCBaseClass : NSObject
@end
@implementation EOCBaseClass
+(void)initialize{
NSLog(@"%@ initialize", self);
}
@end
@interface EOCSubClass:EOCBaseClass
@end
@implementation EOCSubClass
@end
这段代码在EOCSubClass被第一次使用的时候打印什么?
答案是:
EOCBaseClass initialize
EOCSubClass initialize
原因是,使用EOCSubClass会先初始化基类,会打印EOCBaseClass initialize,然后初始化EOCSubClass,因为EOCSubClass没有实现initialize,所以调用基类的initialize,但是这时候self是EOCSubClass,所以打印EOCSubClass initialize。
鉴于复杂性,initialize方法中应该只设置一些成员的初始值。其实在initialize方法中做的事情可以提供一个公开的接口,要求使用者在创建对象之后必须调用这个初始化接口。但是有时候希望设计出“开箱”即用的组件,这时候可能需要通过initialize在做一些初始化工作。
第52条 别忘了NSTimer会保留目标对象
@implementation ViewController{
NSTimer *_pollTimer;
}
- (void)viewDidLoad {
[super viewDidLoad];
}
-(void)viewDidAppear:(BOOL)animated{
[super viewDidAppear:animated];
[self stopPolling];
}
-(void)viewDidDisappear:(BOOL)animated{
[super viewDidDisappear:animated];
[self stopPolling];
}
-(void)startPolling{
_pollTimer = [NSTimer scheduledTimerWithTimeInterval:5 target:self selector:@selector(p_dopoll) userInfo:nil repeats:YES];
}
-(void)p_dopoll{
NSLog(@"poll...");
}
-(void)stopPolling{
[_pollTimer invalidate];
_pollTimer = nil;
}
@end
上边的代码有问题么?
答案是没有问题,但是设计不好。timer会强引用target,这里target是self,然而self又强引用了timer。这里形成了一个保留环。但是并没有内存泄漏,因为在viewDidDisappear函数中会调用stopPolling打破这个保留环。而viewDidDisappear一定会被调用。
但是如果把stopPolling的调用放到dealloc中,就会有问题了。因为在环被打破前dealloc不会被调用。如果想完全避免内存泄漏,最合理的办法是self对timer的引用改成__weak。因为timer会被runloop保留,所以不用担心timer的释放问题。使用弱引用目的就是为了之后的取消操作。
作者给出了另外一个方案,感觉颇复杂。没有必要使用。