该笔记对应第10节课
「written by Talaxy on 2020/3/26」
多线程(Multithreading)
队列
多线程就像许多个队列一样,每个队列由许多个闭包函数组成。这些闭包函数会在队列中一个接着一个被执行。这些队列们可能是一连串的,也可能在同时发生中。
主队列
队列中有个特别的队列称为"主队列"。所有的UI活动必须在仅这个队列上执行。相反的,非UI活动不允许出现在主队列中。
之所以这样设定,是为了让我们的UI更加灵敏有效。并且我们希望我们的UI能够按预期活动。只有当UI没有什么活动时,可以在主队列中运行一些闭包函数。
全局队列
对于一些非主队列的操作,你通常想用一个可共享访问的、全局的、队列
获得一个队列
获得主队列(用来运行UI活动)
let mainQueue = DispatchQueue.main
以下是你经常会用到的全局共享的后台队列(除了主队列),每个队列用于不同的服务:
// QoS: quality of service
let backgroundQueue = Dispatch.global(qos: DispatchQoS)
// 最高优先级,通常只做些短的快的事情,比如一些用户交互
// 这些事几乎是可以选择在主队列运行的
DispatchQoS.userInteractive
// 也有高优先级,可能会花一点时间,但必须立即执行,因为是用户要求的
// 比如用户按了个按钮的开关
DispatchQoS.userInitiated
// 非用户强制要求的事情,可以等后台有空的情况下运行,优先级较低
DispatchQos.background
// 通常是你的应用程序想做的事,优先级低
DispatchQos.utility
将代码块放入队列
多线程仅仅是指放进队列们的闭包的运行,将闭包放入一个队列有两种首要的方法:
// 将闭包放入队列中,并继续执行当前的队列
queue.async { ... }
// 阻塞暂定当前的队列,知道添加的闭包运行完毕
queue.sync { ... }
我们通常用第一种方法
获得一个非全局的队列
你很少会去需要一个这样的队列
获得一个串型队列(队列里每个闭包依次运行):
let serialQueue = DispatchQueue(label: "MySerialQueue")
获得一个同步队列(队列里每个闭包同时运行):
let concurrentQueue = DispatchQueue(label: "MyConcurrentQueue", attributes: .concurrent)
还有更多的关于多线程的东西
你可以使用GCD(Grand Central Dispatch),来锁定保护程序中的关键部分、处理并发读写等。感兴趣可以查阅文档。
其他的一些API
OperationQueue & Operation
通常我们会用 DispatchQueue API,因为这些派发线程(Dispatching)是固定的。但 Operation API 也十分有用(对于更加复杂的多线程,比如线程依赖)
Multithreaded iOS API
在iOS中很少地方,会在除了主队列外运行代码块。这些其余的队列可能一直处在空闲状态。也许iOS会请求你从主线程中挪出一些闭包到别的线程运行。
如果你在处理一个UI相关的事,不要忘记将这个事派遣回主队列中。
一个 iOS API 多线程样例
这个API让你获取http的URL的数据内容(在非主队列上)
// step 1
let session = URLSession(configuration: .defualt)
// step 2
if let url = URL(string: "http://stanford.edu/...") {
// step 3
let task = session.dataTask(with: url) { (data: Data?, response, error) in
// step 6 处理data
...
// step 7 派发线程
DispatchQueue.main.async {
// step 9 在这儿做UI相关的事
...
}
// step 8
...
}
// step 4
task.resume()
}
// step 5
...
上面给出了大概的代码执行步骤(极大部分情况下是这么执行的),但第9步也可能在第8步前被执行。
ImageViewController.swift批注
import UIKit
class ImageViewController: UIViewController, UIScrollViewDelegate {
var imageURL: URL? {
didSet {
// mark start
image = nil
if imageView.window != nil {
fetchImage()
}
// mark end
}
}
private var image: UIImage? {
get {
return imageView.image
}
set {
imageView.image = newValue
imageView.sizeToFit()
// 6 以下两行均用了可选链,这两者之所以可能为空
// 是因为他在 Outlet 还未加载出来的情况下就设置了image属性
// 如果删除了上面的 mark批注块,则可以不使用可选链
// 这个点课上没有讲
scrollView?.contentSize = imageView.frame.size
// 5 图片加载完了,可以暂停spinner了
spinner?.stopAnimating()
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if image == nil {
fetchImage()
}
}
// 1 添加了进度显示
@IBOutlet weak var spinner: UIActivityIndicatorView!
@IBOutlet weak var scrollView: UIScrollView! {
didSet {
scrollView.minimumZoomScale = 1/25
scrollView.maximumZoomScale = 1.0
scrollView.delegate = self
scrollView.addSubview(imageView)
}
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return imageView
}
var imageView = UIImageView()
private func fetchImage() {
if let url = imageURL {
// 2 开始加载图片,我们让spinner动起来
spinner.startAnimating()
// 3 为了不阻塞当前 main队列,我们将图片加载放到 userInitiated队列
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
let urlContents = try? Data(contentsOf: url)
// 4 如果找到了图片,我们返回到主队列,将图片实装
DispatchQueue.main.async {
if let imageData = urlContents, url == self?.imageURL {
self?.image = UIImage(data: imageData)
}
}
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
if imageURL == nil {
imageURL = DemoURLs.shoreline
}
}
}
CassiniViewController.swift批注
import UIKit
class CassiniViewController: UIViewController {
// MARK: - Navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// 2 在storyboard中记得设置segue的identifier
if let identifier = segue.identifier {
// 3 我们根据identifier选择要加载的图片
if let url = DemoURLs.NASA[identifier] {
// 4 在这里,我们回忆一下
// 我们为了让iPad上的SVC的detail有标题栏
// 就需要将detail封装在NC里
// 但是在storyboard中,我们将segue指向了NC,而不是detail
// 所以我们先获取NV下的detail,再进行imageURL、title设置
if let imageVC = segue.destination.contents as? ImageViewController {
imageVC.imageURL = url
imageVC.title = (sender as? UIButton)?.currentTitle
}
}
}
}
}
extension UIViewController {
// 1 contents用于获取NC下可见的VC
var contents: UIViewController {
if let navicon = self as? UINavigationController {
return navicon.visibleViewController ?? self
} else {
return self
}
}
}
自动布局(Autolayout)
我们已经尝试了一些自动布局
- 使用蓝色虚线(中心、边缘)对齐
- 在右下角重置建议的约束(Constrait)
- 使用Ctrl-拖拽来设置边界大小
- 在Inspector中查看编辑设置好的约束
- 在storyboard中点击一个约束并在Inspector中查看编辑
- storyboard左侧栏是一个好地方来查看设置约束
掌握自动布局需要一定的经验
没有别的办法,你只能不断的学习练习
自动布局可以用代码实现
在UIView文档中搜索"anchor"、"auto layout"
掌握以上这些都还不够
通常几何的变化十分剧烈,简单的自动布局将无法应对。事实上你需要重新定位你的view来使布局适合。
Concentration
举个例子,如果在Concentration中我们有20个按钮。竖屏状态下我们会选择5行4列,而横屏状态下我们会选择4行5列。我们没有办法通过约束边缘对齐来实现这一点。
那解决方法呢?我们可以根据大小分类(size class)来改变UI
一个VC当前的大小分类会告诉你空间的布局种类。这个布局种类不会涉及具体的数字大小或者空间范围,它仅仅告诉你当前的宽和高分别是紧实的(compact)还是正常的(regular)
这样一个大小分类能方便你根据不同的屏幕情况来做不同的屏幕适配
iPhone
所有的iPhone在竖屏(portrait)状态下,宽和高都归为compact。所有的"非plus"iPhone在横屏(landscape)状态下,宽和高都为compact。
iPhone Plus
在横屏状态下,高为compact,宽为regular
iPad
宽和高总是regular。但还是有可能为compact,这取决于你所在的MVC(比如一个splitView的master)。
基于大小分类,我们能做些什么?
你可以根据大小分类,更改各种属性,比如字体、背景颜色、(view的)构造与隐藏。更重要的是,你可以根据大小分类设置不同的约束。因此,实质上你可以根据大小分类来设置不同的UI布局。我们的InterfaceBuilder完全支持这一操作。
利用大小分类
在代码中你可以用这个属性获取一个VC的大小分类:
// 返回值为 .compact .regular 或者 .unspecified
let myHorizSizeClass: UIUserInterfaceSizeClass = traitCollection.horizontalSizeClass