谈到属性的原子性,即操作读写的原子性(安全的,单一的,完整的) 可以这么理解。有什么方法可以做到?自然,我们想到了锁的概念。

锁的种类

我们在开发Apps的时候,经常会用到多线程。而锁的概念可以抽象成线程同步。主要作用即保护授权某段代码块的原子性执行。iOS中锁有很多种类:

  • 信号量 Semaphore (允许N个有限线程在某一时间执行指定代码块)
  • 线程锁 Mutex 保证在某一时间内执行某代码块只有一个线程进入。
  • 自璇锁 Spinlock 指的是一个线程获取了一个自旋锁后,另外一个线程期望获取该自旋锁,获取不到的情况下会循环等待,不断的判断。
  • 读写锁 Read-write-lock 并发只读,串行写。
  • 递归锁 Recursive-lock 这个锁可以被同一线程多次请求,而不会引起死锁。它主要是用在循环或递归操作中。

    Lock

    NSLock 和 NSRecursiveLock 都是OC类,在Swift中直接使用。 C语言线程锁pthread_mutex_t可以在Swift中直接使用。

    Spinlock

    自璇锁在iOS10被抛弃了,在Swift中没有与之对应的锁。相近的有os_unfair_lock(不会在争夺锁时候循环判断,而是在操作系统内核中等待被解锁唤醒)

    Read-write lock

    读写锁在Swift中可以用pthread_rwlock_t

    Semaphore

    在Swift中,通过DispatchSemaphore来实现信号数控制。

Grand Central Dispatch (GCD),CPU多核处理,线程间异步高效地执行任务。在GCD的管理下,只需提供DispatchWorkItem作为单个任务,本质上是个swift 闭包。这些任务项item由GCD根据一定地规则排队自动执行。通过 queue.async(execute: workItem)。通过GCD可设置任务的优先级,以及执行顺序。
几个任务项也可以作为一个group, 通过DispatchGroup组织多任务执行,最后将所有处理完毕的结果作为单一整体汇总。
队列Queue管理着任务项的执行,可串行也可并发。串行队列一次执行一个任务,并发队列无需等待执行中的任务结束才开启下一个任务(即可同时执行多项任务)。两种队列都按照先进先出(FOFO)执行。
内部,有GCD线程池服务所有的队列。当某任务执行完成后线程即销毁。当所有的线程属于busy状态,新的线程需要暂时挂起。GCD预备了五种队列可直接使用。一种串行队列:主队列。四种异步队列分别权限为:hign,default,low,background。

1
2
3
4
5
// Do work synchronously
DispatchQueue.global().sync { ... }

// Do work asynchronously
DispatchQueue.global().async { ... }

自定义串行队列:

1
2
3
4
5
6
7
let queue = DispatchQueue(label: "Some serial queue")

// Do work synchronously
queue.sync { ... }

// Do work asynchronously
queue.async { ... }

有时,我们需要对某个任务项取消执行,这个时候需要用workItem
1
2
3
4
5
6
7
8
9
10
11
12
13
class Service {
private var pendingWorkItem: DispatchWorkItem?
let queue = DispatchQueue(label: "Some serial queue")

func doSomething() {
pendingWorkItem?.cancel()

let newWorkItem = DispatchWorkItem { ... }
pendingWorkItem = newWorkItem

queue.async(execute: newWorkItem)
}
}

批量任务处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let queue = DispatchQueue(label: "Serial queue")
let group = DispatchGroup()

queue.async(group: group) {
sleep(1)
print("Task 1 done")
}

queue.async(group: group) {
sleep(2)
print("Task 2 done")
}

group.wait()

print("All tasks done")

Task 1 done
Task 2 done
All tasks done

另外一种group.enter()方式不会阻塞当前任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let queue = DispatchQueue(label: "Serial queue")
let group = DispatchGroup()

group.enter()
queue.async {
sleep(1)
print("Task 1 done")
group.leave()
}

group.enter()
queue.async {
sleep(2)
print("Task 2 done")
group.leave()
}

group.notify(queue: queue) {
print("All tasks done")
}

print("Continue execution immediately")

Continue execution immediately
Task 1 done
Task 2 done
All tasks done

留意线程死锁
死锁1:

1
2
3
4
5
6
7
8
let queue = DispatchQueue(label: "my-queue")
queue.sync {
print("print this")

queue.sync {
print("deadlocked")
}
}

死锁2:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/// Create concurrent queue
let queue = DispatchQueue(label: "Concurrent queue", attributes: .concurrent)

for _ in 0..<999 {
// 1
queue.async {
sleep(1000)
}
}

// 2
DispatchQueue.main.sync {
queue.sync {
print("Done")
}
}

通过信号量控制线程数

1
2
3
4
5
6
7
8
9
10
11
12
let concurrentTasks = 3

let queue = DispatchQueue(label: "Concurrent queue", attributes: .concurrent)
let sema = DispatchSemaphore(value: concurrentTasks)

for _ in 0..<999 {
queue.async {
// Do work
sema.signal()
}
sema.wait()
}

首先我们带着几点疑问去看文章:

  • 值类型(结构体、枚举) 和 引用类型 class 在内存中是如何存储的?
  • 值类型和引用类型性能上有什么不同?
  • 如果两者混合,会发生什么?比如struct 包含了 class 对象。
  • 到底该使用哪个呢?

值类型的定义

  • 值类型直接存储到栈中。每个值类型的变量持有自己的数据,之间互不影响。
  • 引用类型通过指针引用数据(存储在堆中),多个变量可指向同一数据,当操作某一变量时会影响其他变量。

值类型涉及到值拷贝,引用类型涉及到内存分配,引用计数。接下来我们大致了解一下内存段。

Property Wrappers 是 Swift5.1语言的一个功能点,允许我们定义一个自定义的类型,实现get,set方法的一些包装,以便重用。那接下来你可能会提出几个问题?

  • 它是用来解决什么问题?
  • 怎样实现一个属性包裹?
  • 怎样去接收一个属性变量?
  • 属性包裹器被Swift编译器编译成什么了?
  • 属性包裹器有什么限制?

理解属性包裹

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Bar {
private var _x = 0

var x: Int {
get { _x }
set {
_x = newValue
print("New value is \(newValue)")
}
}
}

var bar = Bar()
bar.x = 1 // Prints 'New value is 1'

为了复用,我们将变量定义成泛型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct ConsoleLogged<Value> {
private var value: Value

init(wrappedValue: Value) {
self.value = wrappedValue
}

var wrappedValue: Value {
get { value }
set {
value = newValue
print("New value is \(newValue)")
}
}
}

Ok,我们用ConsoleLogged重写Bar
1
2
3
4
5
6
7
8
9
10
11
struct Bar {
private var _x = ConsoleLogged<Int>(wrappedValue: 0)

var x: Int {
get { _x.wrappedValue }
set { _x.wrappedValue = newValue }
}
}

var bar = Bar()
bar.x = 1 // Prints 'New value is 1'

Swift提供@propertyWrapper关键字对以上封装。
1
2
3
4
@propertyWrapper
struct ConsoleLogged<Value> {
// The rest of the code is unchanged
}

依赖注入是一种让代码低耦合的设计模式。
有四种实现方式:

  • 初始化注入
  • 属性赋值注入
  • 接口注入
  • 容器注入

初始化注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
protocol Dependency {
func foo()
}

struct DependencyImplementation: Dependency {
func foo() {
// Does something
}
}

class Client {
let dependency: Dependency

init(dependency: Dependency) {
self.dependency = dependency //注入后不可再次更改。
}

func foo() {
dependency.foo()
}
}

let client = Client(dependency: DependencyImplementation())
client.foo()

初始化注入后,属性不可再被更改。当超过三个以上的属性需要注入,那么这个时候适合用属性注入的方式。

UserDefaults可以存储一些基础数据类型,如:Data,String,Date,Bool,Int,Double,Float,Array,Dictionary,URL,等基础对象。也可以存储自定义对象(但需要实现编码成Data进行存储)。
尽管如此,我们还是不推荐使用UserDefaults来存储数据量大的数据。因为读写非常昂贵,userDefaults最终用.plist文件进行存储。存储大数据会使此文件变得臃肿。
对于自定义的对象,我们可以通过写文件方式将json写到沙盒文件中。或者存储到coreData. Sqlite等。UserDefaults一般用于存储一些简单的数据类型,存储用户的偏好设置等。在写少读多的情况下,性能才会高。底层是通过读写xml文件方式都数据进行读写。

接下来我们用PropertyWrapper对UserDefaults进行封装。

1
2
3
4
5
6
7
8
9
@propertyWrapper
struct UserDefault<T: PropertyListValue> {
let key: Key

var wrappedValue: T? {
get { UserDefaults.standard.value(forKey: key.rawValue) as? T }
set { UserDefaults.standard.set(newValue, forKey: key.rawValue) }
}
}

需要对传入的泛型数据做约束,遵循PropertyListValue协议
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//定义约束协议
protocol PropertyListValue {}

extension Data: PropertyListValue {}
extension String: PropertyListValue {}
extension Date: PropertyListValue {}
extension Bool: PropertyListValue {}
extension Int: PropertyListValue {}
extension Double: PropertyListValue {}
extension Float: PropertyListValue {}

// 每个元素必须是 PropertyListValue 类型
extension Array: PropertyListValue where Element: PropertyListValue {}
extension Dictionary: PropertyListValue where Key == String, Value: PropertyListValue {}

然后我们定义Key的结构体,遵循 RawRepresentable 协议。
1
2
3
struct Key: RawRepresentable {
let rawValue: String
}

使用:
1
2
3
4
public struct StorageTest {
@UserDefault(key: Key(rawValue: "isFirstLaunch"))
var isFirstLaunchFlag: Bool?
}

为了达到@UserDefault(key: “isFirstLaunch”)这样的简写效果,我们让Key实现ExpressibleByStringLiteral协议。
1
2
3
4
5
6
7
8
9
10
extension Key: ExpressibleByStringLiteral {
init(stringLiteral: String) {
rawValue = stringLiteral
}
}

public struct StorageTest {
@UserDefault(key: "isFirstLaunch")
var isFirstLaunchFlag: Bool?
}

为了统一的管理,我们将存储键写在Key结构体中。
1
2
3
4
5
6
7
extension Key {
static let isFirstLaunch: Key = "isFirstLaunch"
}
public struct StorageTest {
@UserDefault(key: .isFirstLaunch)
var isFirstLaunchFlag: Bool?
}

KVO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class DefaultsObservation: NSObject {
let key: Key
private var onChange: (Any, Any) -> Void

// 1
init(key: Key, onChange: @escaping (Any, Any) -> Void) {
self.onChange = onChange
self.key = key
super.init()
UserDefaults.standard.addObserver(self, forKeyPath: key.rawValue, options: [.old, .new], context: nil)
}

// 2
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
guard let change = change, object != nil, keyPath == key.rawValue else { return }
onChange(change[.oldKey] as Any, change[.newKey] as Any)
}

// 3
deinit {
UserDefaults.standard.removeObserver(self, forKeyPath: key.rawValue, context: nil)
}
}

@propertyWrapper
struct UserDefault<T: PropertyListValue> {
var projectedValue: UserDefault<T> { return self }

func observe(change: @escaping (T?, T?) -> Void) -> NSObject {
return DefaultsObservation(key: key) { old, new in
change(old as? T, new as? T)
}
}

// The rest of the code is unchanged
}

用法:

1
2
3
4
5
6
7
8
var storage = Storage()

var observation = storage.$isFirstLaunch.observe { old, new in
print("Changed from: \(old) to \(new)")
}

storage.isFirstLaunch = true
storage.isFirstLaunch?.toggle()

SUT

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
extension URLRequest {
static func search(term: String) -> URLRequest {
var components = URLComponents(string: "https://itunes.apple.com/search")
components?.queryItems = [
.init(name: "media", value: "music"),
.init(name: "entity", value: "song"),
.init(name: "term", value: "\(term)")
]

return URLRequest(url: components!.url!)
}
}

struct MusicService {

func search(_ term: String, completion: @escaping (Result<[Track], Error>) -> Void) {
URLSession.shared.dataTask(with: .search(term: term)) { data, response, error in
DispatchQueue.main.async {
completion(self.parse(data: data, error: error))
}
}.resume()
}

func parse(data: Data?, error: Error?) -> Result<[Track], Error> {
if let data = data {
return Result { try JSONDecoder().decode(SearchMediaResponse.self, from: data).results }
} else {
return .failure(error ?? URLError(.badServerResponse))
}
}
}

视图测试关注点:

  • tableview的行数是否正确?
  • label的文本显示是否正确?
  • button 是否启用或禁用?
  • view的frame是否正确?

viewController如果可以从他们依赖项隔离出来,那么就可以测试。依赖注入是一种可以把视图控制器隔离出来的技术。在测试中,我们可以用假数据替换依赖项,模拟真实数据的行为。
viewController有两个职责:渲染数据、响应用户交互。在MVVM设计模式中,控制器不主动从model中拉数据,也不负责从model中取出数据来更新控制器。

我们定义好要显示的状态属性等,然后通过协议来约束控制器实现渲染数据的动作。

1
2
3
4
5
6
7
8
9
10
struct ArtistDetailProps {
let title: String
let fullName: String
let numberOfAlbums: String
let numberOfFollowers: String
}

protocol ArtistDetailComponent: AnyObject {
func render(_ props: ArtistDetailProps)
}

viewController遵循该协议,实现渲染数据的动作
1
2
3
4
5
6
7
8
extension ArtistDetailViewController: ArtistDetailComponent {
func render(_ props: ArtistDetailProps) {
navigationItem.title = props.title
fullNameLabel.text = props.fullName
numberOfAlbumsLabel.text = props.numberOfAlbums
numberOfFollowersLabel.text = props.numberOfFollowers
}
}

测试ViewController

控制器的render是由presenter决定,我我们可以定义一个presenter的mockPresenter子类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//定义好交互方法
class ArtistDetailPresenterMock: ArtistDetailPresenter {

private(set) var onViewLoadedCalled = false

func onViewLoaded() {
onViewLoadedCalled = true
}

private(set) var onEditCalled = false

func onEdit() {
onEditCalled = true
}
}

准备好测试数据:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class ArtistDetailViewControllerTests: XCTestCase {
let presenter = ArtistDetailPresenterMock()

func makeSUT() -> ArtistDetailViewController {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let sut = storyboard.instantiateViewController(identifier: "ArtistDetailViewController") as! ArtistDetailViewController
sut.presenter = presenter
sut.loadViewIfNeeded()
return sut
}


//验证交互:
func testViewDidLoadCallsPresenter() {
let sut = makeSUT()

sut.viewDidLoad()

XCTAssertTrue(presenter.onViewLoadedCalled)
}

func testOnEditCallsPresenter() {
let sut = makeSUT()

sut.onEdit(.init())

XCTAssertTrue(presenter.onEditCalled)
}

//验证测试数据是否正确
func testRender() {
let props = ArtistDetailProps(title: "TITLE", fullName: "NAME", numberOfAlbums: "1", numberOfFollowers: "2")

let sut = makeSUT()

sut.render(props)

XCTAssertEqual(sut.navigationItem.title, "TITLE")
XCTAssertEqual(sut.fullNameLabel.text, "NAME")
XCTAssertEqual(sut.numberOfAlbumsLabel.text, "1")
XCTAssertEqual(sut.numberOfFollowersLabel.text, "2")
}
}

ok,以上测试用例我们验证了用户交互行为是否触发 以及 渲染数据的正确与否。
总结:如果要让viewController可测试,我们需要让viewController数据渲染被动而非主动(MVVM模式或者MVP模式),可以通过依赖注入来定义mock模型,实现可测试化。控制器和视图的测试不是测试颜色、布局等样式,而是测试交互以及数据渲染的准确性!

什么是单元测试

单元测试是运行一个方法(程序的已部分功能代码)验证其输入和输出结果的正确性。通过调用开放的API和行为属性,一般有:

  • 方法返回值
  • 改变Class的属性
  • 缓存数据的CURD
  • 发送通知

为什么需要写单元测试

单元测试可有效减少程序bug,保证程序结果的准确性,改善代码结构。

定义一个好的单元测试

一个好的单元测试具有以下特点:

  • 可读性高
  • 可维护性
  • 可靠的

可读性

当单元测试失败的时候,你能够快速找到失败的原因代码。单元测试需要符合代码规范,格式、命名、模式等等。

  • 简洁的命名、变量
  • 清晰的异常信息
  • 遵循合适的断言结构

可维护性

  • 只测试public,或者internals变量或者方法。不要仅仅为了验证私有和文件私有属性或方法而削弱封装。
  • 不要把测试代码放到生成环境
  • 每项测试只测试一个业务功能
  • 重用代码。如果测试用例包含很多复制重复的代码,需要进行抽象封装,创建一个共享共用的方法如下片段UserStorageTests代码。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class UserStorageTests: XCTestCase {
    let userDefaults = UserDefaultsMock()
    let keychain = KeychainMock()
    let user = User(id: 1, username: "U1", password: "P1")

    func testUsernameSavedToStorage() {
    makeSUT().save(user)
    XCTAssertNotNil(userDefaults.inputUsername)
    }

    func testPasswordSavedToSecureStorage() {
    makeSUT().save(user)
    XCTAssertNotNil(keychain.inputPassword)
    }

    func makeSUT() -> UserStorage {
    return UserStorage(storage: userDefaults, secureStorage: keychain)
    }
    }

可靠性

  • 100%的通过率
  • 测试用例可以重现步骤。
  • 避免在编写单元测试时候写测试逻辑。不要写for,while等循环,不要写if,else,Switch等条件语句,不要写do{}catch{}异常捕获(例如下列片段代码,我们可以用throws将异常抛出)。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    struct Item:Codable,Equatable {
    let id:Int
    }

    //`Item` has custom Codable implementations that we want to test

    class SwiftTestUITests: XCTestCase {

    func testItemCodable() throws {
    let expected = Item(id: 1)
    let encoded = try JSONEncoder().encode(expected)
    let decoded = try JSONDecoder().decode(Item.self, from: encoded)
    XCTAssertEqual(decoded, expected)
    }
    }

iOS工程师都知道tableview 在开发中是用得最多的,大部分页面几乎都可以用列表来做。但有没发现大量工作其实是重复的比如cell的注册,delegate数据源的匹配,cell的类型转换。接下来,我们需要将这些代理方法为tableview提供数据封装成数据模型。
数据源:dataSource[section[ rows],section[ rows]]

封装DataSource

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct Section<Item> {
var items: [Item]
}

struct DataSource<Item> {
var sections: [Section<Item>]

func numberOfSections() -> Int {
return sections.count
}

func numberOfItems(in section: Int) -> Int {
guard section < sections.count else { return 0 }
return sections[section].items.count
}

func item(at indexPath: IndexPath) -> Item {
return sections[indexPath.section].items[indexPath.row]
}
}