iOS 高性能iOS应用开发的笔记(二)


线程

线程是运行时执行的一组指令序列。
每个进程至少应包含一个线程。
进程启动时的主要线程通常被称作主线程。
所有的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


以下介绍三种锁:

  1. NSLock 必须在锁定的过程中解锁,lock 锁定, unlock 解锁
  2. NSRecursiveLock:允许在解锁前,锁定多次。若解锁次数与锁定次数相匹配,则锁被释放,其他线程可以获取锁。
    主要用于多方法使用同一个锁进行同步,且其中一个方法调用另一个方法。
  3. 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 秒作为上限。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容