41 派发队列与同步锁
- 内置的同步锁,根据给定的对象创建锁,等待块中的代码执行完毕。滥用会降低代码效率,若在self上频繁加锁,程序必须按顺序执行,可能要等一段其他代码执行完毕,才能执行当前代码。不能保证完全的线程安全,同一线程多次调用获取值,可能有其他线程改写值,而导致获取的结果不同。
|
|
- 第二种是NSLock,或递归锁NSRecursiveLock,线程能多次持有该锁,而不会死锁。同步块会导致死锁,效率不高。用锁对象一旦遇到死锁会很麻烦。
|
|
- 使用串行同步队列,将读取与写入操作都放在一个块里,保证数据同步。设置方法不一定非得是同步的,改为异步派发后,性能会变慢,因为异步派发需要拷贝块。如果异步执行任务复杂,需要更多时间,这种方法值得考虑。
|
|
- 多个获取方法可并发执行,设置方法与获取方法之间不能并发执行。改用并发队列,还能继续提升速度。并发队列+栅栏,可实现同步。栅栏块必须单独执行,不能与其他块并行,只对并发队列有意义。并发队列发现栅栏块,先将当前所有块执行完毕,然后单独执行栅栏块。可使用栅栏块实现设置方法。(时序图169页)
|
|
42 少用performSelector
- 若选择子是运行期决定,这个方法较有用。如下在ARC下编译代码,编译器会给出警告信息。此种情况,编译器不知道方法名,不能运用内存管理机制判断返回值是否应释放。因此ARC不添加释放操作,可能导致内存泄露,并且难以侦测。
|
|
- performSelector返回值是id类型,只能用于选择子返回类型为void或对象类型。如果想返回整数或浮点数等类型的值,需要执行复杂的转换。若返回值类型为C结构体则不可用performSelector方法。
- 传递参数的版本,所传参数必须是id类型,最多只能接受2个参数。延后执行的无法带2个参数,如果要用这些方法,需要将许多参数打包到字典中,这会增加开销且容易出bug.
|
|
- 用块来代替,延后执行用dispatch_after实现,另一个线程执行任务通过 dispatch_sync 和 dispatch_async 来实现。
|
|
43 GCD与操作队列的使用时机
- 操作队列NSOperationQueue执行后台任务比较好,用操作NSOperation子类的方式放在队列中,也能并发执行。操作队列在底层是用GCD实现的。
- GCD是纯C,操作队列是oc对象,操作队列更为重量级,有时候操作队列的开销比较小,但带来的好处可以超过其缺点。
- 用NSOperationQueue的addOperationWithBlock: 方法搭配 NSBlockOperation 类来使用操作队列。下列为使用操作队列的好处。
- 操作队列可取消某个操作,NSOperation 的cancel方法,不过已经启动的任务无法取消。GCD无法取消。
- 操作队列可指定操作间的依赖关系,使某个操作需要依赖于其他操作完成。
- 通过KVO监控NSOperation对象的属性,例如监控isCancelled属性判断是否取消,isFinished属性判断任务是否完成。
- 指定操作的优先级,优先级高的操作先执行。GCD也有优先级,但是是针对整个队列来说的,而不是具体每个块。NSOperation对象也有线程优先级,GCD也有此功能,但是操作队列使用起来更简单,只需设置一个属性。
- 可以重用NSOperation对象,NSBlockOperation即是其子类,也可以自己创建子类。
- NSNotificationCenter 使用的是操作队列。
|
|
44 Dispatch Group
- 将并发任务合为一组
|
|
- 数组中每个对象都执行某项任务
|
|
- 可以把任务放到优先级高的线程执行,再把所有任务归入一个group
|
|
- 可以用串行队列,这样group作用不大,因为是逐个执行,在提交完全部任务后再提交块即可。
|
|
- 并发队列中可能会创建和复用多个线程,并发线程数量根据系统资源状况判断,cpu有多个核心,gcd会给该队列配备多个线程。
- 还有个多次执行的块,每次传给块的值递增,从0开始,直至iterations-1。dispatch_apply所用的队列可以是并发队列。
|
|
- dispatch_apply会持续阻塞,直到所有任务都执行完毕。如果把块派给了当前队列或高于当前队列的某个串行队列,将导致死锁。
45 dispatch_once
- 单例,即某类只有一个实例,不会每次使用都创建新的实例。
|
|
- GCD的实现方式,dispatch_once,该函数保证块只执行一次,首次调用时执行,线程安全。每次调用token参数必须相同,常声明在static或global作用域里。
- 该实现方式可以简化代码,保证线程安全。把token定义在static作用域,保证编译器每次都复用该变量,而不创建新变量。这种方法更快。
|
|
46 不使用dispatch_get_current_queue
- iOS已废弃该方法,macOS未废弃。主线程相当于GCD的主队列。下面代码获取方法可能会死锁,如果执行块的队列恰好是当前执行获取方法的队列,dispatch_sync就会一直不返回,直到块执行完毕才行。而执行块的队列是当前队列,其dispatch_sync一直在阻塞,在等待块执行完。块会没机会执行。(意思就是当前队列调dispatch_sync,实际就已经阻塞了,阻塞之后还想在当前队列执行块,这个块是不会执行的)
|
|
- 下例这种情况有可能死锁
|
|
- 下列代码肯定死锁,外层的queueA想执行完毕,要等最内层的dispatch_sync执行完毕,而内层是不可能执行完毕的,于是死锁。
|
|
- 应该确保同步操作所用的队列绝不访问属性,其队列只应该用来同步属性。每项属性可以有专用的同步队列,可以创建多个队列。
- 队列之间有层级体系,某个队列中的块可以在其上级队列中执行,层级里地位最高的队列是全局并发队列。(182页层级图)
- 检查当前队列是否为执行同步派发所用的队列,不一定奏效。如果将操作放在上级队列中执行,而上级队列和当前队列都是串行队列,依然会死锁。
- 使用GCD提供的队列特定数据,可以将数据关联到队列里,根据队列找关联数据时,会沿着体系向上找,直到找到数据或到达根队列为止。这个方法可解决获取当前队列却不是认为会取到的队列的问题。
|
|
47 系统框架
- iOS平台的系统框架使用动态库。Foundation框架使用NS前缀(NeXTSTEP,macOS的前身)
- CoreFoundation,Foundation里的许多功能,都能在其中找到对应的C语言API,用桥接可相互转换。
- CFNetwork C语言级别的网络通信能力,NSURLConnection 是 Foundation 对其内容封装的OC接口
- CoreAudio C语言API操作音频硬件
- AVFoundation 录制音视频 oc
- CoreText 文字排版渲染 c
- 使用c语言api可提升速度,但要注意内存管理问题
- CoreAnimation oc 渲染图形,播放动画,其本身不是框架,而是QuartzCore框架的一部分
- CoreGraphics c 2d渲染数据结构与函数,有CGPoint CGSize CGRect
- Social 社交网络
48 块枚举
- for循环方式遍历比其他方式简单
- NSEnumerator 抽象基类,定义了2个方法。nextObject返回枚举里的下个对象,全部对象返回之后,就会返回nil。遍历字典和set也可用一样的写法
|
|
- for in 遍历,也可以反向遍历数组
|
|
- 块的遍历,数组、字典、NSSet的遍历方法。可以对元素或键值改参数类型,因为id可以被其他类型覆写,如果确定知道元素是什么类型,就应该在方法参数里指明。
|
|
49 桥接
- 转换oc对象和cf数据结构,__bridge表明ARC仍保留对象的所有权,而__bridge_retained表明ARC将交出对象的所有权
- __bridge_transfer 将cf数据结构转为oc对象,可令ARC获得所有权
|
|
- NSDictionary 键内存管理语义是拷贝,值是保留。桥接可以改变其语义。
|
|
- 将其与桥接搭配使用,可以创建出与oc创建出的字典不同的特殊字典。(代码196页)
- 如果加入NSMutableDictionary 中的键不支持拷贝,会报错 copyWithZone 方法未实现。用 CoreFoundation 创建字典,可以对键执行保留而非拷贝,因为可修改其内存管理语义
50 缓存NSCache
- NSCache 在系统资源将耗尽时,可以自动删减缓存,先删减最久未使用的对象
- NSCache 不会拷贝键,而是保留它。不支持拷贝键是因为很多时候键所用的对象不支持拷贝
- NSCache是线程安全的,多个线程可同时访问NSCache,而不用加锁
- 可以控制删减缓存的时机,根据缓存中对象总数或所有对象的总开销。在将对象加入缓存时可以指定其开销值,当对象总数或开销值超上限,就会自动删减了,可用系统资源紧张时也会这么做。
- 如果开销值计算复杂耗时,例如需要访问磁盘或数据库才能计算出来,就不太好了。因为使用缓存的目的是为了提升速度的。如果要加入的是NSData可以指定开销值,其开销值就是数据大小,这样仅是读取属性而已。
|
|
- NSPurgeableData 是 NSMutableData子类,实现了NSDiscardableContent协议,如果某对象所占内存能随时丢弃,就可实现该协议的方法。协议里有isContentDiscarded方法,可查询相关内存是否已释放。
- NSPurgeableData 对象有beginContentAccess和endContentAccess方法,告诉系统不可丢弃和可丢弃的状态变化。与cache搭配使用,当其被系统丢弃时,也会自动从缓存中移除。
|
|
51 精简initialize 和 load 的代码
- load方法,每个类及其分类必定会调用,仅调用一次。当类或分类载入系统时,就会执行,通常是程序启动的时候。如果类与分类都定义了load方法,则先调用类里再调用分类里的。由于无法判断出类的载入顺序,在load方法中使用其他类是不安全的。如果使用了其他类,而该类没有载入并完成初始化,其可能无法正常使用。
- 如果load没有实现,就不会调用。务必精简,因为整个程序在执行load时都会阻塞。不要在里面使用锁。其真正用途在于调试程序。
|
|
- initialize 在类首次使用前调用,是惰性调用,只调用一次。不能通过代码调用。如果该类没有使用,就不会调用。而load是每个类都会调,不管使用与否。
- 在initialize可以调用任意类的任意方法,是线程安全的。只有执行initialize的线程可以操作类或实例,其他线程都要先阻塞。其有超类实现,如果某类没实现,会调用其超类的方法,并且其超类方法先执行。
- 也不要在其中执行复杂的代码,加锁之类,会阻塞初始化,如果是主线程就不好了。第二开发者无法控制类的初始化时机。第三,如果其中使用其他类,而其他类被迫使初始化,其中又用到了本类的数据,这些数据还未初始化好。
- initialize 只应该用来设置内部数据,也最好不要调用本类的方法。
- 全局状态可以在initialize 中初始化。整数可以在编译器确定,可变数组不行,会报错。它是oc对象,必须先激活运行期系统。NSString可以在编译期创建。
|
|
52 NSTimer
- 计时器和 run loop关联,运行循环会触发任务。可以创建并安排在当前运行循环中,也可以创建后,自己调度。计时器会保留目标对象,等到自身失效时再释放。invalidate方法可令计时器失效。一次性的计时器会自动失效,重复执行模式需要自己调用invalidate才行。
|
|
- 重复执行模式容易导致循环引用(代码206页),这种内存泄露很严重,因为计时器还在反复执行任务。
|
|
|
|