二
线程
线程是运行时执行的一组指令序列。
每个进程至少应包含一个线程。
进程启动时的主要线程通常被称作主线程。
所有的UI元素都需在主线程中创建和管理。
与用户交互相关的所有中断最终都会分发到UI线程,处理代码会在这些地方执行。(不是很理解)
IBAction方法的代码都会在主线程中执行。
Cocoa编程不允许其他线程更新UI元素。也就是UI更新必须在主线程中执行。
线程开销
每个线程都有一定的开销,从而影响到应用的性能。线程开销:创建时的时间开销,消耗的内存空间。
内核数据结构
每个线程大约消耗1KB的内核内存空间。这块内存用于存储与线程有关的数据结构和属性,是联动内存,无法被分页。
栈空间
主线程的栈空间大小为1M,而且无法修改。所有的二级线程默认分配512KB的栈空间。
完整的栈并不会立即被创建出来。实际的栈空间大小会随着使用而增长。
因此,即使主线程有1MB的栈空间,某个时间点的实际栈空间很可能要小很多。
在线程启动前,栈空间的大小可以被改变。
栈空间的最小值是16KB,而且其数值必须是4KB的倍数。
修改栈空间:
//size为NSUInteger类型,是4的倍数
NSThread *t = [[NSThread alloc] initWithTarget:target
selector:selector object:argument];
t.stackSize = size;
创建耗时
参考Demo中的 computeThreadCreationTime 方法
启动时间主要因为多次的上下文切换所带来的开销。
GCD
主要功能如下:
- 任务或分发队列,允许主线程中的执行、并行执行和串行执行
- 分发组,实现对一组任务执行情况的跟踪,而与这些任务所基于的队列无关
- 信号量。
- 屏障,允许在并行分发队列中创建同步的点。
- 分发对象和管理源,实现更为底层的管理和监控。
- 异步I/O,使用文件描述符或管道。
GCD同样解决了线程的创建与管理。可用于跟踪应用中线程的总数,且不会造成任何的泄露。
大多情况是单独使用GCD,偶尔用NSThread 或 NSOperationQueue
当应用中有多个长耗时的任务需要并行执行时,最好对线程的创建过程加以控制。
若代码执行的时间过长,很可能达到线程的限制64个,即GCD的线程池上限。
应避免浪费的使用dispatch_async 和 dispatch_sync ,会导致应用崩溃。
不加控制的话,会超出64个的上限的。
操作与队列
主要操作:
NSOperation封装了一个任务以及和任务相关的数据和代码,而NSOperationQueue以先入先出的顺序控制了一个或多个这类任务的执行。
NSOperation 和 NSOperationQueue 都提供控制线程个数的能力。可用maxConcurrentOperationCount属性控制队列的个数,也可以控制每个队列的线程个数。
在使用NSThread(开发人员管理全部并发)和GCD(OS管理并发)之间存在两个选择。
快速比较如下:
GCD
- 抽象程度最高。
- 两种队列开箱即用:main 和 global
- 可以创建更多的队列(使用dispatch_queue_create)
- 可以请求独占访问(使用dispatch_barrier_sync 和 dispatch_barrier_async)
- 基于线程管理
- 硬性限制创建64个线程
NSOperationQueue
- 无默认队列
- 应用管理自己创建的队列
- 队列是优先级队列
- 操作可以有不同的优先级(使用queuePriority属性)
- 使用cancel消息可以取消操作。注意,cancel仅仅是个标记。如果操作已经开始执行,则可能会继续执行下去。
- 可以等待某个操作执行完毕(使用waitUntilFinished消息)
NSThread
- 低级别构造,最大化控制。
- 应用创建并管理线程。
- 应用创建并管理线程池。
- 应用启动线程。
- 线程可以拥有优先级,操作系统会根据优先级调度它们的执行。
- 无直接API用于等待线程完成。需要使用互斥量(如NSLock)和自定义代码。
NSOperationQueue是多核安全的,可以放心的分享队列,从不同的线程中提交任务,而无需担心损坏队列。
线程安全的代码
不要使用可修改的共享状态。若无法避免使用可修改的共享状态,则确保你得代码是线程安全的。
在代码中保留不变量。
原子属性
原子属性可以保证一次更新一个值,但无法保证多属性更新一个时不能读取另一个,以及多属性同时修改。还是用栅栏或者信号变量灯更好。
同步块
使用atomic,线程仍可能不安全。只能阻止并行修改,但无法保证阻止多属性,同时修改。
比如某对象,有两个atomic属性,有两个线程同时修改,第一个线程现修改了第一个属性,第二个线程则修改不了第一个属性但却修改了第二个属性。这种情况可以用锁来避免。@synchronized
锁
以下介绍三种锁:
- NSLock 必须在锁定的过程中解锁,lock 锁定, unlock 解锁
- NSRecursiveLock:允许在解锁前,锁定多次。若解锁次数与锁定次数相匹配,则锁被释放,其他线程可以获取锁。
主要用于多方法使用同一个锁进行同步,且其中一个方法调用另一个方法。- NSCondition:有些情况需要协调线程之间的执行。如A线程需要等B线程返回结果。
简单来说,用Lock加锁,此时只有这个线程能继续往下访问。
然后wait,让当前线程停止运行。
之后另一个线程调用signal,告诉线程继续执行。
随后运行完毕unlock,解锁。
将读写锁应用于并发读写
若有多个线程试图读取一个属性,同步的代码块在同一时刻只允许单个线程进行访问。所以会很慢。
尤其是当某状态需在多个线程间共享,且需要被多个线程访问时(如cookie或登陆后的访问令牌)。
所以我们需要允许并行读取,但与写入互斥的一种机制。读写锁。
读写锁:允许并行访问只读操作,而写操作需互斥访问。也就是修改数据时需要一个互斥锁。
GCD屏障(栅栏)允许并行分发队列上创建一个同步的点,当遇到屏障时,GCD延迟提交的代码块,直到队列中所有在屏障之前的代码块都执行完毕。随后通过屏障提交的代码块会单独执行。这个代码块即为屏障块。待其完成后,队列按原有行为继续执行。(若不太理解,可参考我写的另外几篇)
步骤:
1.创建一个并行队列。2.在这个队列上使用dispatch_sync执行所有的操作。3.在相同的队列上使用dispatch_barrier_sync执行所有的写操作
ReactiveCocoa 库 实现了OC中的响应式编程。
可以实现对任意状态的观察,同步更新UI元素或响应视图的交互
可以使用这个来更新。
曾经的一些相关笔记
异步与同步主要区别就是要不要开启新线程,对于异步来说,只有主队列会不开启新线程,因为主队列就是要求在主线程上运行,但主队列异步不会造成死锁,而主队列同步则会。
对于同步来说,就是不开启新线程,在不开启新线程的情况下,程序的执行只能是串行了,即使要求用并行(能要求?),其结果也仍然与串行相同,所以在主队列的时候会造成死锁,主线程等着同步内容执行完毕,主队列的定义则又要求主线程先运行完在运行其内部操作,于是死锁现象发生,可通过异步中嵌套同步方式避免死锁。
异步优于同步
//场景A dispatch_sync(queue, ^() {
dispatch_sync(queue, ^() {
NSLog(@"nested sync call");
}); });
会造成死锁。嵌套的dispatch_sync不能分发到队列中,因为当前线程已经在队列中且不会释放锁。
//场景B
-(void) methodA1 {
dispatch_sync(queue1, ^() {
[objB methodB];
}); }
-(void)methodA2 { dispatch_sync(queue1, ^() {
NSLog(@"indirect nested dispatch_sync");
});
}
-(void) methodB { [objA methodA2];
}
类A调了类B的方法,类B调方法又回到类A的队列中建立了同步队列,于是又死锁了。
dispatch_get_current_queue弃用很久了,因为会造成死锁。具体与其实现有关。详情可查看我的后续文章。
所以为了避免死锁且易于维护的代码。强烈建议用异步风格。
最好使用Promise(可使用PromiseKit库,可以让代码看起来更加简洁)
应用委托
应用创建的第一个对象是应用委托。
应用启动
application:didFinishLaunchingWithOptions: 方法是应用启动时最核心的地方。
此方法会载入所有的依赖,并初始化应用的核心。
若此方法执行时间过长,则用户需等待一段时间才能展现UI。
程序的四种启动
1.首页启动。安装应用后的首次启动,没有缓存,没有之前的状态。
因此会没有需要加载的内容(初始时间短),或从服务器上下载初始数据(初始时间长)
2.冷启动。应用在后台放置一段时间后被挂起或关闭的启动。启动期间,可能需要恢复一些状态。比如音视频的播放,聊天记录的展示,上次同步的文章等。
3.热启动。应用处于后台但并未被挂起或关闭时又回到应用。若用户通过点击应用图标或深层链接返回应用时,不会触发启动时的回调,而是直接用applicationDidBecomeActive:(或 application:openURL:so urce:annotation:)回调。
4.升级后的启动。通常升级后的启动跟冷启动没啥差别。之所以称呼不一,是为了表明本地存储发生变化的时刻不同。变化包括模式、内容、之前版本挂起的同步操作,以及内部的API/默认依赖。
通常首次启动,会执行的多个任务。
- 加载应用的默认项(NSUserDefaults、捆绑的配置等)
- 检查版本(生产/测试)
- 初始化应用标识符,比如广告标识符、供应商标识符
- 初始化崩溃报告系统
- 建立A/B测试(A端:开发 B端:商家 C端:用户)
- 建立分析方法
- 使用操作或GCD建立网络
- 建立UI基础设施(导航、主题、初始UI)
- 显示登录提示或从服务器加载最新内容或其他更新
- 建立内存缓存(如图片缓存)
程序初始化时的优化
1.确定在展示UI前必须执行的任务。
如应用是第一次启动,那应先初始化崩溃报告系统。需用户自定义值的,暂时不需一次性加载。
2.按顺序执行任务
排序,解决任务间的依赖性。
3.任务分为两类:必须主线程中运行、可以在其他线程中执行。
也可将其他线程中的任务分为可并发与不能并发的。然后两类任务分开执行。
4.其他任务可在加载UI后执行或异步执行。
延迟其他子系统(如记录仪和分析方法)的初始化。
在应用的后续阶段将一些操作(如写日志或跟踪事件)放入队列中,直到子系统完全初始化。
根据不同的框架,优化方案的实施也会不同。
冷启动
冷启动时一个较为重要的任务是:载入之前的状态。
所以要考虑到:
- 展示有用且有意义的UI所需要的最少信息数目(min)。
- 记录从本地缓存加载M条信息花费的时间(记作tl)。
- 记录从服务器获取最新的M条信息花费的时间(记作tr)。
- 为了获得更快的速度,任何时刻在内存中存储的最大信息数目(max),特别是在快速滑动和滚动时。
务必在三秒内加载M条信息。
也有本地加载所需时间与服务端加载时间相同的。建议两者都做。
热启动:
- 点击应用图标回到应用
- 应用接收到深层链接
点击图标时,一般不需要其他操作。顶多就是状态监听而后相应的暂停或恢复动画、游戏的状态
深层链接,应用收到 application:openURL:sourceApplication:annotation: 回调。可跳转指定页面,完成操作。
若需要从服务器获取数据,则先展示与深层链接相关的原始页面,或一个进度条,等拿到数据后再刷新操作。
注意,深层链接返回到应用页面的操作。
升级后的启动
升级后的首次启动情形:
- 无本地缓存或应用完全放弃缓存。
- 本地缓存可用,可直接用或需要切换至升级版本。
第一种
- 不需要特别处理。
第二种:
- 若本地缓存有用,且需要迁移,则可通知用户。
- 若需花几分钟对数据进行迁移,则需向用户展示一个可推迟该操作的选项。
- 若数据从服务器获取更好,因此须放弃本地缓存,需通知用户。
远程通知
- 若应用是激活状态,则通过didReceiveRemoteNotification回调接收通知。不调用其它回调,也未向用户展示UI。
- 若应用在后台运行或停止,只有静默推送通知回调会被触发。基于通知设置,非静默推送通知可能会出现在通知中心或作为报警弹窗,或更新应用图标的角标计数。
- 当用户使用通知中心或报警弹窗开启通知时,可能出现两种情况:
1. 若应用处于后台,则通知回调方法会被调用。
2. 若应用处于停止状态,则application:didFinishLaunchingWithOptions: 方法的launchOptions参数中的通知对象是可用的。
需注意,application: didReceiveRemoteNotification: 只有在应用处于前台时才会被调用,若实现了application: didReceiveRemoteNotification:fetchCompletionHandler: 当应用处于后台或还未运行时,此回调也会被触发、甚至会启动应用。这就是静默推送通知。
简单来说,有个接收通知的回调,该回调有个延展方法。在app未启动或处于后台时,会通过此回调启动应用或回到应用。
注意:后一种方法可能被调用两次。
第一次是收到通知,并且payload字段包含一个键为content - available、值为1的键值对时。
第二次是用户以通知中心或警告的方式和通知交互时。
本地通知
当应用处于使用状态时,本地通知不会展示任何UI。
若应用没有处于使用状态,被挂起了,在这种情况下如何显示本地通知?
静默远程通知。若远程通知payload字段的content - available属性值被置为1,它会告诉操作系统远程通知不应展示给用户,而必须直接传递给应用。若需要,可能会唤醒应用。
用户点击了通知时,application:didiReceiveRemoteNotification:fetchCompletionHandler:回调
若没有实现,则调用
后台拉取
可以较好地从服务器定期同步数据。
想启用需三个步骤:
(1) 在项目设置中开启功能。
(2)设置刷新间隔,最好在 application:didFinishLaunchingWithOptions 中完成。使用
-UIApplication setMinimumBackgroundFetchInterval:方法请求刷新以指定的频率完成。
(3) 实现应用的 application:performFetchWithCompletionHandler: 委托方法。如果任务没
有在 30 秒内完成,操作系统会调度执行频率较低的方法去运行。 实践记录表明,应用一般使用的时间要少得多,通常在 2~4 秒。苹果公司的开发者网站 将 30 秒作为上限。