41. 多用派发队列,少用同步锁
在OC中,如果多个线程要执行同一份代码,那么可能会出现问题。这种情况下,通常使用锁来实现某种同步机制。
在GCD出现之前,有两种方法:
- 第一种:synchronization
- (void)someMethod
{
@synchronized(self){
//Safe
}
}
这种会根据给定的对象(self),自动创建一个锁,并等待块中的代码执行完毕。
执行到这段代码结尾处,锁就释放了。
然鹅,过多的使用@synchronized(self),会降低代码效率,因为共用同一个锁的那些同步块,都必须按顺序执行
- 第二种:NSLock对象
_lock = [[NSLock alloc] init];
- (void)someMethod
{
[_lock lock];
//Safe
[_lock unlock];
}
也可以使用递归锁:NSRecursiverLock
,线程可以多次持有该锁,而不会出现死锁(deadlock)现象。
- 在有GCD之后,同步问题可以使用GCD,它更简单、更高效。
在设置属性的时候,如果需要属性为“原子的”,则经常需要用到同步,使用atomic来修饰属性,就可以实现。
但无法保证访问该对象时绝对是线程安全的。
有种简单而高效的方法可以代替同步块或锁对象,那就是使用“串行同步队列”
将读写操作都安排在同一个队列里,即可保证数据同步
例如:
即可实现数据的同步
还可以将设置方法写成异步执行:
多读单写
GCD有一个栅栏(barrier),可以实现多读单写功能。
dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);
dispatch_barrier_sync(dispatch_queue_t queue, dispatch_block_t block);
- 栅栏只对并发队列有意义,串行队列的话,只是普通的一个一个执行
举个例子:
_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,全局并发+dispatch_barrier_sync)
- 栅栏使用dispatch_barrier_async和dispatch_barrier_sync是否都可以?
栅栏使用串行队列可以吗?什么效果?
两篇文章都提到了不能使用串行队列
验证:
self.queue = dispatch_queue_create("rw_queue", DISPATCH_QUEUE_SERIAL);
打印结果:
2022-03-20 11:33:44.526704+0800 008[1941:46200] -[ViewController read]_block_invoke
2022-03-20 11:33:45.528229+0800 008[1941:46200] -[ViewController read]_block_invoke
2022-03-20 11:33:46.530097+0800 008[1941:46200] -[ViewController read]_block_invoke
2022-03-20 11:33:47.535326+0800 008[1941:46200] -[ViewController write]_block_invoke
2022-03-20 11:33:48.540525+0800 008[1941:46200] -[ViewController write]_block_invoke
2022-03-20 11:33:49.544776+0800 008[1941:46200] -[ViewController write]_block_invoke
2022-03-20 11:33:50.546115+0800 008[1941:46200] -[ViewController read]_block_invoke
可以看出,读、写都是每秒执行一次,从而失去了多读单写的功能
栅栏使用全局并发队列可以吗?
- 全局并发+dispatch_barrier_async
验证:
self.queue = dispatch_get_global_queue(0, 0);
- (void)write
{
dispatch_barrier_async(self.queue, ^{
sleep(1);
NSLog(@"%s", __func__);
});
}
结果:
2022-03-20 11:37:00.889954+0800 008[2015:50136] -[ViewController read]_block_invoke
2022-03-20 11:37:00.889936+0800 008[2015:50138] -[ViewController write]_block_invoke
2022-03-20 11:37:00.889946+0800 008[2015:50140] -[ViewController read]_block_invoke
2022-03-20 11:37:00.889989+0800 008[2015:50142] -[ViewController write]_block_invoke
2022-03-20 11:37:00.889960+0800 008[2015:50141] -[ViewController read]_block_invoke
2022-03-20 11:37:00.890017+0800 008[2015:50137] -[ViewController write]_block_invoke
2022-03-20 11:37:00.890017+0800 008[2015:50149] -[ViewController read]_block_invoke
2022-03-20 11:37:00.890041+0800 008[2015:50148] -[ViewController read]_block_invoke
2022-03-20 11:37:00.890098+0800 008[2015:50156] -[ViewController read]_block_invoke
2022-03-20 11:37:00.890096+0800 008[2015:50153] -[ViewController write]_block_invoke
基本上一下就执行完了,写是同步进行的,没有休眠,因此,不满足“单写”
- 全局并发+dispatch_barrier_sync
验证:
self.queue = dispatch_get_global_queue(0, 0);
- (void)write
{
dispatch_barrier_sync(self.queue, ^{
sleep(1);
NSLog(@"%s", __func__);
});
}
结果:
2022-03-20 11:38:08.063348+0800 008[2048:51424] -[ViewController write]_block_invoke
2022-03-20 11:38:08.065732+0800 008[2048:51476] -[ViewController read]_block_invoke
2022-03-20 11:38:08.065732+0800 008[2048:51472] -[ViewController read]_block_invoke
2022-03-20 11:38:08.065732+0800 008[2048:51471] -[ViewController read]_block_invoke
2022-03-20 11:38:09.064681+0800 008[2048:51424] -[ViewController write]_block_invoke
2022-03-20 11:38:10.066012+0800 008[2048:51424] -[ViewController write]_block_invoke
2022-03-20 11:38:11.067604+0800 008[2048:51424] -[ViewController write]_block_invoke
2022-03-20 11:38:11.067633+0800 008[2048:51471] -[ViewController read]_block_invoke
2022-03-20 11:38:11.067635+0800 008[2048:51476] -[ViewController read]_block_invoke
2022-03-20 11:38:11.067635+0800 008[2048:51472] -[ViewController read]_block_invoke
2022-03-20 11:38:12.069155+0800 008[2048:51424] -[ViewController write]_block_invoke
读一下就执行完毕,满足多读
写是每一秒执行一次,也满足
但有个小问题,写执行完毕,要执行读的操作的时候,基本上最后一个写和第一个读是同时执行的
栅栏使用dispatch_barrier_async和dispatch_barrier_sync是否都可以?
- 自定义并发+dispatch_barrier_sync
验证:
self.queue = dispatch_queue_create("rw_queue", DISPATCH_QUEUE_CONCURRENT);
- (void)write
{
dispatch_barrier_sync(self.queue, ^{
sleep(1);
NSLog(@"%s", __func__);
});
}
结果:
2022-03-20 11:41:08.055321+0800 008[2114:54371] -[ViewController read]_block_invoke
2022-03-20 11:41:08.055321+0800 008[2114:54372] -[ViewController read]_block_invoke
2022-03-20 11:41:08.055321+0800 008[2114:54376] -[ViewController read]_block_invoke
2022-03-20 11:41:09.056482+0800 008[2114:54163] -[ViewController write]_block_invoke
2022-03-20 11:41:10.058033+0800 008[2114:54163] -[ViewController write]_block_invoke
2022-03-20 11:41:11.059538+0800 008[2114:54163] -[ViewController write]_block_invoke
2022-03-20 11:41:12.060971+0800 008[2114:54371] -[ViewController read]_block_invoke
2022-03-20 11:41:12.060979+0800 008[2114:54376] -[ViewController read]_block_invoke
满足多读单写
- 自定义并发+dispatch_barrier_async
验证:
self.queue = dispatch_queue_create("rw_queue", DISPATCH_QUEUE_CONCURRENT);
- (void)write
{
dispatch_barrier_async(self.queue, ^{
sleep(1);
NSLog(@"%s", __func__);
});
}
结果:
2022-03-20 11:42:58.414861+0800 008[2159:56378] -[ViewController read]_block_invoke
2022-03-20 11:42:58.414861+0800 008[2159:56382] -[ViewController read]_block_invoke
2022-03-20 11:42:58.414861+0800 008[2159:56383] -[ViewController read]_block_invoke
2022-03-20 11:42:59.420234+0800 008[2159:56382] -[ViewController write]_block_invoke
2022-03-20 11:43:00.425576+0800 008[2159:56382] -[ViewController write]_block_invoke
2022-03-20 11:43:01.426410+0800 008[2159:56382] -[ViewController write]_block_invoke
2022-03-20 11:43:02.431424+0800 008[2159:56378] -[ViewController read]_block_invoke
2022-03-20 11:43:02.431423+0800 008[2159:56380] -[ViewController read]_block_invoke
2022-03-20 11:43:02.431445+0800 008[2159:56382] -[ViewController read]_block_invoke
2022-03-20 11:43:03.436903+0800 008[2159:56378] -[ViewController write]_block_invoke
2022-03-20 11:43:04.440432+0800 008[2159:56378] -[ViewController write]_block_invoke
总结:
- 栅栏不能使用串行队列
- 使用自定义并发队列+同步/异步栅栏都可以实现多读单写
- 使用全局并发队列+异步栅栏不可以🙅🏻♀️
- 使用全局并发队列+同步栅栏可以
MJ老师讲的barrier只讲了dispatch_barrier_async,没有讲dispatch_barrier_sync
由于只讲了dispatch_barrier_async,全局global不能满足多读单写功能,因此得出不能使用全局global定义队列的结论
总体还是没有考虑到dispatch_barrier_sync的情况
42. 多用GCD,少用performSelector系列方法
- (id)performSelector:(SEL)aSelector;
SEL是选择因子,例如@selector(selectorName)
局限性:
- (id)performSelector:(SEL)aSelector;
返回值是void或对象类型。
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
最多接收两个参数
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;
不能处理两个参数的选择子
因此,本书建议使用GCD,而非performSelector系列方法
43. 掌握GCD及操作队列的使用时机
在执行后台任务时,GCD不一定是最佳方式
GCD是纯C的API,而操作队列(NSOperationQueue)是OC对象。
NSOperation的好处:
-
可以取消某个操作
在运行任务之前,可以再NSOperation对象上调用cancel方法,不过,已经启动的任务无法取消
而把块安排到GCD队列,就无法取消了。如果开发者想自己实现取消功能,需要编写很多代码,而那些代码其实已经由操作队列实现好了 -
指定操作间的依赖关系
-
通过键值观测机制监控NSOperation对象的属性
NSOperation对象有许多属性都适合通过键值观测机制(简称KVO)来监听,比如可以通过isCancelled属性来判断任务是否已取消,isFinished属性判断任务是否已完成。 -
指定操作的优先级
GCD的队列也有优先级,但那是针对整个队列来说的,而不是针对每个块来说的。
44. 通过Dispatch Group机制,根据系统资源状况来执行任务
dispatch group能够把任务分组。其最重要的用法就是把将要并发执行的多个任务合为一组,在执行完毕后,统一处理结果。
创建一个group
dispatch_group_t dispatch_group_create(void);
想把任务编组,有两种方法:
方法一:
void dispatch_group_async(dispatch_group_t group,
dispatch_queue_t queue,
dispatch_block_t block);
方法二:
void dispatch_group_enter(dispatch_group_t group);
void dispatch_group_leave(dispatch_group_t group);
调用了dispatch_group_enter必须有与之对应的dispatch_group_leave
如果有enter,无leave,那么这一组任务就永远执行不完
在并发队列中,执行任务所用的并发线程数量,取决于各种因素,这也就是GCD为了处理并发任务而有的复杂调度器。
45. 使用dispatch_once来执行只需要运行一次的线程安全代码
使用dispatch_once可以简化代码,并且彻底保证线程安全
static id _instance;
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_instance = [super allocWithZone:zone];
});
return _instance;
}
+ (instancetype)sharedInstance
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_instance = [[self alloc] init];
});
return _instance;
}
- (id)copyWithZone:(NSZone *)zone
{
return _instance;
}
- (id)mutableCopyWithZone:(NSZone *)zone {
return _instance;
}
由于每次调用时,都必须使用完全相同的标记,因此,标记要声明为static:static dispatch_once_t onceToken;
stitic的作用:
- 在修饰变量的时候,static 修饰的静态局部变量只执行初始化一次,而且延长了局部变量的生命周期,直到程序运行结束以后才释放。
- static 修饰全局变量的时候,这个全局变量只能在本文件中访问,不能在其它文件中访问,即便是 extern 外部声明也不可以。
- static 修饰一个函数,则这个函数的只能在本文件中调用,不能被其他文件调用。
- static 修饰的变量存放在全局数据区的静态变量区,包括全局静态变量和局部静态变量,都在全局数据区分配内存。初始化的时候自动初始化为 0。
- 不想被释放的时候,可以使用static修饰。比如修饰函数中存放在栈空间的数组。如果不想让这个数组在函数调用结束释放可以使用 static 修饰。
46. 不要使用dispatch_get_current_queue
当需要判断当前代码正在哪个队列上执行,可能会使用dispatch_get_current_queue()
函数
但此函数的行为常常与开发者所预期的不同,已经废弃
第七章 系统框架
大部分代码都是来源于系统框架,比如NSObject就属于Foundation框架
47. 熟悉系统框架
将一系列代码封装为动态库(dynamic library),并在其中放入描述其接口的头文件,这就是框架
有时候为iOS平台构建的第三方框架所使用的是静态库(static library),这是因为iOS应用程序不允许在其中包含动态库。这些东西严格来讲并不是真的框架,然而也经常被视为框架 。
iOS平台的系统框架是动态库
- Cocoa框架:开发带图形界面时,用到的框架。其实Cocoa本身并不是框架,但是里面集成了一批创建应用程序时经常会用到的框架
- Foundation框架:包括NSObject、NSArray、NSDictionary等类,主要是NS前缀,其前身是NeXTSTEP公司开发的
- CoreFoundation框架:Foundation框架中的许多功能,都可以在此框架中找到对应的C语言API。两者联系紧密。有一个“无缝桥接”(tollfree bridging)功能,可以将CoreFoundation中的C语言数据结构平滑转换为Foundation中的OC对象,也可以反向转换。
- CFNetwork框架:提供了C语言级别的网络通信能力
- CoreAudio框架:提供C语言API可用来操作设备上的音频硬件。
- AVFoundation框架:提供OC对象,可用来回放并录制音频及视频
- CoreData框架:提供OC接口,可将对象放入数据库,便于持久保存
- CoreText框架:提供C语言接口,高效执行文字排版及渲染操作
可以看出,OC编程经常需要使用底层的C语言级API。好处是可以绕过OC的运行期系统,从而提升执行速度,当然ARC只负责OC对象,所以使用这些API时,尤其需要注意内存管理问题。
48. 多用块枚举,少用for循环
本条所讲的collection包含NSArray、NSDictionary、NSSet这几个频繁使用的类型。
for循环
for循环遍历NSArray很简单,不叙述
for循环遍历NSDictionary需要将NSDictionary转换为NSArray:[dictionary allKeys]
NSDictionary *dic = @{@"key1": @"value1", @"key2": @"value2", @"key3": @"value3"};
NSArray *array = [dic allKeys];
for (int i = 0; i < array.count; i++) {
NSString *key = array[i];
NSString *value = dic[array[i]];
NSLog(@"key = %@, value = %@", key, value);
}
for循环遍历NSSet需要将NSSet转换为NSArray:[set allObjects]
NSSet *set = [NSSet setWithObjects:@"1", @"3", @"4", @"6", nil];
NSArray *array = [set allObjects];
for (int i = 0; i < array.count; i++) {
NSObject *obj = array[i];
NSLog(@"obj = %@", obj);
}
使用NSEnumerator遍历
NSEnumerator是一个抽象基类
本书说该类只定义了两个方法:
- (NSArray *)allObjects
- (id)nextObject
查了文档,现在是一个方法、一个属性:
@interface NSEnumerator<ObjectType> : NSObject <NSFastEnumeration>
- (nullable ObjectType)nextObject;
@end
@interface NSEnumerator<ObjectType> (NSExtendedEnumerator)
@property (readonly, copy) NSArray<ObjectType> *allObjects;
@end
nextObject返回枚举里的下个对象。每次调用该方法时,其内部数据结构都会更新,使得下次调用方法时能返回下个对象。
等到枚举中的全部对象都已返回之后,再调用就将返回nil,表示已经达到枚举末端了。
遍历数组:
NSArray *array = @[@"1", @"2", @"3", @"100"];
NSEnumerator *enumerator = [array objectEnumerator];
NSString *str;
while ((str = [enumerator nextObject]) != nil) {
NSLog(@"%@", str);
}
遍历字典:
NSDictionary *dic = @{@"key1": @"value1", @"key2": @"value2", @"key3": @"value3"};
NSEnumerator *enumerator = [dic keyEnumerator];
NSString *key;
while ((key = [enumerator nextObject]) != nil) {
NSLog(@"key = %@, value = %@", key, dic[key]);
}
遍历集合:
NSSet *set = [NSSet setWithObjects:@"1", @"3", @"4", @"6", nil];
NSEnumerator *enumerator = [set objectEnumerator];
NSString *str;
while ((str = [enumerator nextObject]) != nil) {
NSLog(@"%@", str);
}
快速遍历
其实就是for in
遍历数组:
不叙述
遍历字典:
NSDictionary *dic = @{@"key1": @"value1", @"key2": @"value2", @"key3": @"value3"};
for (NSString *key in dic) {
NSLog(@"key = %@, value = %@", key, dic[key]);
}
遍历集合:
NSSet *set = [NSSet setWithObjects:@"1", @"3", @"4", @"6", nil];
for (NSString *str in set) {
NSLog(@"%@", str);
}
基于块的遍历方式
NSArray定义了下面这个方法:
- (void)enumerateObjectsUsingBlock:(void (NS_NOESCAPE ^)(ObjectType obj, NSUInteger idx, BOOL *stop))block;
当然,还有一系列类似的遍历方法
上面这个方法,有三个参数,分别是:
- obj: 当前迭代所针对的对象
- idx: 所针对的下标
- *stop:指向布尔值的指针
遍历数组:
NSArray *array = @[@"1", @"2", @"3", @"100"];
[array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if(idx == 2){
*stop = YES;
}
NSLog(@"array[%ld] = %@", idx, obj);
}];
结果:
2022-03-22 09:55:57.656598+0800 008[2373:61464] array[0] = 1
2022-03-22 09:55:57.656722+0800 008[2373:61464] array[1] = 2
2022-03-22 09:55:57.656823+0800 008[2373:61464] array[2] = 3
遍历字典:
NSDictionary *dic = @{@"key1": @"value1", @"key2": @"value2", @"key3": @"value3"};
[dic enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
NSLog(@"%@, %@", key, obj);
}];
结果:
2022-03-22 09:54:42.467207+0800 008[2318:59522] key1, value1
2022-03-22 09:54:42.467437+0800 008[2318:59522] key2, value2
2022-03-22 09:54:42.467534+0800 008[2318:59522] key3, value3
遍历集合:
NSSet *set = [NSSet setWithObjects:@"1", @"3", @"4", @"6", nil];
[set enumerateObjectsUsingBlock:^(id _Nonnull obj, BOOL * _Nonnull stop) {
NSLog(@"%@", obj);
}];
结果:
2022-03-22 09:57:38.333864+0800 008[2404:63047] 3
2022-03-22 09:57:38.334076+0800 008[2404:63047] 1
2022-03-22 09:57:38.334170+0800 008[2404:63047] 6
2022-03-22 09:57:38.334283+0800 008[2404:63047] 4
块遍历的优势在于:遍历时可以直接从块里获取更多信息:
在遍历数组时,可以知道当前所针对的下标;
在遍历字典时,无须额外编码,即可同时获取键与值,因而省去了根据给定键来获取对应值这一步。
另外,在方法里面,可以将id直接转换为已知类型
49. 对自定义其内存管理语义的collection使用无缝桥接
Foundation框架的一些东西,比如NSArray,可以与CoreFoundation框架中的一些东西,比如CFArray可以类型平滑转换,这种转换就是”无缝桥接“,简称==”桥接“==
例如:
NSArray *array = @[@"1", @"2", @"3", @"100"];
CFArrayRef aCFArray = (__bridge CFArrayRef)(array);
NSLog(@"%ld", (long)CFArrayGetCount(aCFArray));
结果:
2022-03-22 10:11:43.372291+0800 008[2714:74336] 4
转换操作中的 __bridge 告诉ARC如何处理转换所涉及的OC对象。
__bridge 本身的意思是:ARC仍然具备这个OC对象的所有权。
__bridge_retained则相反,意味着ARC将交出对象的所有权。
50. 构建缓存时选用NSCache 而非 NSDictionary
从网络缓存图片的时候,需要把内存中的图片保存到本地,这样,再稍后使用的时候,就不需要再次下载了
一般都是用字典来做,但,NSCache可能会更好些。
NSCache可以在系统资源将要耗尽时,自动删减缓存。此外,NSCache还会进行”最久未使用的“对象
NSCache是线程安全的
在操作缓存删减内容中,有两个与系统资源相关的尺度可供调整,一个是缓存中的对象总数,一个是所有对象的”总开销“
比如,可缓存的总对象数目上限设为100,将”总开销“上限设为5MB
51. 精简initialize与load的实现代码
有时候,类必须先执行某些初始化操作,才能正常使用。
在OC中,绝大多数类都继承NSObject这个根类,而该类有两个方法,可用来实现这种初始化操作。
+ (void)load
对于加入运行期系统中的每个类,以及分类来说,必定会调用此方法,而且仅调用一次。
调用时机:当包含类或者分类的程序库载入系统时,就会执行此方法,而这通常就是指应用程序启动的时候。
-
如果分类和其所属的类都定义了load方法,则先调用类里的,再调用分类里的。(父子原分)
-
load方法,在运行期系统处于”脆弱状态“,在执行子类的load方法之前,必定会先执行所有超类的load方法。如果代码还依赖了其他程序库,那么程序库里相关类的load方法也必定会先执行。
-
如果某个类A没有实现Load方法,不管他的父类AA实现或者没有实现load范发个,系统都不会调用类A
-
在load方法中,务必实现的精简一些,尽量减少操作,因为整个应用程序在执行load方法时都会阻塞
类相关初始化的操作:
+ (void)initialize
对于每个类来说,该方法会在程序首次用该类之前调用,且只调用一次。
调用时机:运行期系统调用
- 它是”惰性调用的“,即只有当程序用到了相关的类时,才会调用。
- initialize方法,如果某个类未实现它,而其超类实现了,那么就会运行超类的实现代码
52. 别忘了 NSTimer会保留其目标对象
计时器NSTimer和”运行循环“(run loop)相关联,运行循环到时候会触发任务。
只有把计时器放在运行循环里,它才能正常触发任务。
- 计数器会保留其目标对象target,因此,要避免产生循环引用
- 可以使用NSTimer的block方法,避免循环引用
- 还可以使用NSProxy