击水湘江

Born To Fight!


  • Home

  • About

  • Tags

  • Archives

Vapor中的数据校验

Posted on 2017-08-10

Validation
服务端在有数据请求时需要对数据进行校验然后返回响应的校验结果,比如要求必须输入邮箱,必须输入电话等,Validation工具给我们提供了非常方便的常用操作,接下来就对其使用过程做以总结(本文用到的是Validation 1.2.0版本)。

Vapor中添加Validation依赖包

在Package.swift文件中添加如下的依赖,比如:

.Package(url: "https://github.com/vapor/vapor.git", majorVersion: 2),
.Package(url: "https://github.com/vapor/validation-provider.git", majorVersion: 1)

之后要执行vapor clean或者rm -rf .build Package.pins,然后执行vapor update或者swift package update,vapor xcode,这样才可以安装好依赖。

校验实例

Alphanumeric校验

接下来我们来做一个简单的校验,校验输入的字符串是否是a-z或者0-9在请求中加入如下代码:

get("alpha") { request in
            guard let input = request.data["input"]?.string else {
                throw Abort.badRequest
            }
            let validInput = try input.tested(by: OnlyAlphanumeric())
            return "validated:\(validInput)"
}

我们运行程序,然后在PostMan中输入http://localhost:8080/alpha?input=example@github.com,这时会得到下面的返回值:

{"identifier":"Validation.ValidatorError.failure","reason":"Internal Server Error","debugReason":"OnlyAlphanumeric failed validation: example@github.com is not alphanumeric","error":true}

这也就说明了,我们传输的问本内容不符合alphanumeric。
然后我们将URL改为http://localhost:8080/alpha?input=example123,然后就会看到我们的返回值

validated:example

邮箱校验

我们可以利用EmailValidator来做邮箱的校验,方法同上面一样:

get("email") { request in
            guard let input = request.data["input"]?.string else {
                throw Abort.badRequest
            }
            let validaInput = try input.tested(by: EmailValidator())
            return "validated:\(validaInput)"
    }     

然后我们输入URL:http://localhost:8080/email?input=wallaceicdi@outlook.com,然后就会的到:

Validated: wallaceicdi@outlook.com

其余自带校验工具

校验类 功能 用法
Unique 输入内容是否唯一 someCharacter.tested(by: Unique())
Compare 输入内容的数值比较 int.tested(by:Compare.greaterThan(1))
Contains 输入的内容是否包含某个 someArray.tested(by: Contains(“1”))
Count 输入的内容个数 someArray.tested(by: Count.max(2))
Equals 输入的内容是否相同 someConent.tested(by: Equals.init(“equal”))
In 输入内容是否被包含 input.tested(by: In.init([“1”,”2”,”3”]))

创建自己的校验工具

通过参考工具自带的Equals.Swift:

/// Validates that matches a given input
public struct Equals<T>: Validator where T: Validatable, T: Equatable {
    /// The value expected to be in sequence
    public let expectation: T

    /// Initialize a validator with the expected value
    public init(_ expectation: T) {
        self.expectation = expectation
    }

    public func validate(_ input: T) throws {
        guard input == expectation else {
            throw error("\(input) does not equal expectation \(expectation)")
        }
    }
}

从这里面我们可以看出,只要遵守Validator协议,并且实现其validate方法即可。

参考文件:
https://github.com/vapor/validation/blob/master/Tests/ValidationTests/ValidationConvenienceTests.swift

Swift进阶

Posted on 2017-08-07

本文是对WWDC2014–AdvancedSwift的总结

改变参数名

比如我们要改变Thing对象的参数名:

class Thing {
init(location: Thing?, name:String,
     longDescription: String){ ... }
}

如果我们不想用默认的Thing.init(location:Beijing, name:"wall",longDescription:"An amazing city")这种方式进行初始化,我们可以在参数前面添加label的形式:

class Thing {
  init(newLocation location: Thing?, newName name: String,
newLongDescription longDescription: String) { ... }
}

这样我们就可以使用Thing.init(newLocation:Beijing, newName:"wall",newLongDescription:"An amazing city")这种参数来进行初始化。

匿名参数

比如下面的例子,我们不需要字典中的value,那么我们就只需要遍历其key即可。

for (key,_) in dictionary {
 print(\(key))
}

在这个例子中,我们使用下划线_来进行你匿名操作,略过了我们不关心的value值,而只输出了key的值。

在上面Thing的例子中,如果我们要移除其参数名称,那么我们可以将label变为下划线,这样我们在调用的时候就不必写参数名了。

class Thing {
  init(_ location: Thing?, _ name: String,

   _ longDescription: String) { ... }
}

这样我们就可以使用Thing.init(Beijing, "wall","An amazing city")来初始化了。

Protocol

如果我们要给上面的Thing对象添加一个方法performPull来执行其是否可以被拉动的方法。

        // The parser will call this. func performPull(object: Thing) 
{ if /* object is   pullable */ {
     /* pull it */
}else{
     /* complain */
  }
} 

这时我们可以添加一个Protocol:

protocol Pullable {
  func pull()
} 
class Boards: Thing, Pullable {
func pull() {
....
}
}

这样我们的Boards类就遵守了Pullable协议,当我们来检查某个对象是否遵守了某个协议时,我们可以这样做:

func performPull(object: Thing) {
if let pullableObject = object as Pullable { pullableObject.pull()
   }else{ 
     print("You are not sure how to print a \(object.name).")
   } 
} 

对象转String

如果我们要打印某个对象,并且需要打印出其中的有效信息,那么我们要像OC中实现description方法一样,来遵守CustomStringConvertible协议并且实现其中的description方法。
还有很多类似的方法

Protocol 作用
ExpressibleByStringLiteral “abc”
ExpressibleByArrayLiteral [ a, b, c ]
ExpressibleByDictionaryLiteral [a: x, b: y]
Sequence for x in sequence
CustomStringConvertible “(convertible)”

同样,如果我们要对某个对象使用下表,那么我们要subscript。怎样对某个类使用下表呢?

泛型

比如以下三个方法,其参数的形式是一样的,但是其参数类型不同,我们想把它变成一个函数,这时候我们最常想到的做法就是使用Any来表示任何类型的参数和任何类型的返回值。

// 之前的三个函数
func peek(interestingValue: String) {
  println("[peek] \(interestingValue)")
}
func peek(interestingValue: Int) {
  println("[peek] \(interestingValue)")
}
func peek(interestingValue: Float) {
  println("[peek] \(interestingValue)")
} 
// 变为一个函数
func peek(interestingValue: Any) {
  println("[peek] \(interestingValue)")
} 

但是这有个问题,在我们要调用返回值的某个方法时候编译器会报错,因为我们的返回值Any并没有所调用的方法,也就是说我们需要一次强转,强转在代码层面是很难看的,也很麻烦,这时候我们就可以使用泛型来解决这个问题,如果我们使用了泛型,那么编译器会将输入的参数和输出的参数给我们推断出来,这样就不必强转了。并且当编译器可以推断出来是那种type的时候它会给我们做各种优化。

泛型在Swift中很常见,比如:
Array以及Dictionary<K,V>:是范型的结构体
Optional:范型枚举
我们也可以创建自己的泛型类

Type间的关系

比如我们要转换两个变量的值,我们可以调用下面的方法:

 // Exchange the values of x and y
func swap<T>(inout x: T, inout y: T) { 
let tmp = x
    x = y
    y = tmp 
} 
var studentCount = 42
var teacherCount = 7
swap(&studentCount, &teacherCount) // OK
var schoolName = “Homestead High School"
swap(&studentCount, &schoolName) // error: 'Int' is not identical to 'String'

有了这样的编译器提示,这样我们就可以保证了输入的两种类型是相同的,这样就会更加安全,代码也会少很多bug。

对泛型进行Protocol限制

我们可以给泛型加上限制,让它遵守某些协议,比如:

func indexOf<T>(sought: T, inArray array: T[]) -> Int? {
  for i in 0..array.count {
    if array[i] == sought { // error: could not find an overload for '==' that accepts the supplied arguments 
      return i
} } 
return nil } 

这是编译不通过的,因为编译器不知道我们的T是否遵守了Equatable协议,如果我们将其变为func indexOf<T:Equatable>(Sought: T, inArray array: T[]) -> Int?就可以编译通过了。

实现Equatable协议

Enum,Class以及Struct都可以实现协议,比如我们有如下的Struct,实现Equatable协议如下:

struct Temperature : Equatable { 
  let value: Int = 0
}

func == (lhs: Temperature, rhs: Temperature) -> Bool {
  return lhs.value == rhs.value
} 

当我们实现了Equatable,那么对于!=这种操作,Swift在底层就会自动帮我们实现。

斐波那契数列的例子

什么是菲波那切数列?数列的前两个数相加等于后面的一个数。 0, 1, 1, 2, 3, 5, 8, 13, 21, ...
我们创建一个菲波那切数列的函数:

func fibonacci(n: Int) -> Double {
  return n < 2 ? Double(n) : fibonacci(n - 1) + fibonacci(n - 2)
} 

这个函数的返回值仍然是一个函数,它是一个递归调用的函数,这个函数的效率极低。当你在Playground中实验的时候就会卡的不行。如果调用fibonacci(44)估计要十几秒的时间。原因就是它需要一个调用一个树状结构,我们用fibonacci(5)做个图示:
fibonacci(n:5)

在图中我们发现fib(1),fib(2)等函数被持续的调用,我们如果可以把这个已经计算过的数值存下来,那么以后不是就就可以直接计算了呢?这样不就可以极大的提高计算的速度。这样我们就只计算下面带星号的函数即可:
valuable func

这时最先想到的就是用一个全局的字典手动来存储这个数值,实现方法如下:

var fibonacciMemo = Dictionary<Int, Double>() // implementation detail 
// Return the nth fibonacci number: 0, 1, 1, 2, 3, 5, 8, 13, 21, ...
func fibonacci(n: Int) -> Double { 
  if let result = fibonacciMemo[n] {
    return result
  }
  let result = n < 2 ? Double(n) : fibonacci(n - 1) + fibonacci(n - 2)
  fibonacciMemo[n] = result
  return result
} 

// 1.61803399...
let phi = fibonacci(45) / fibonacci(44) //0.1 seconds = 100x speedup 

这样一来,我们之前计算fibonacci(44)中十几秒的计算时间一下子缩短到了0.1秒,缩减了100倍。
如果这样做的话,我们以后每次用到这个方法就都需要写一个字典,每次把这个计算过程过一遍,并且更重要的是它不具备通用型,如果我要放进去字符串,那就不起作用了。下面我们写一个通用的函数来解决这种需要保留中间数值,并且不需要额外的全局变量fibonacciMemo就可以解决问题的方法。先看一个不递归时候的函数:

func memoize<T: Hashable, U>(work: @escaping (T)->U) -> (T)->U {

    var memo = Dictionary<T, U>()
    return { x in
        if let q = memo[x] { return q }
        let r = work(x)
        memo[x] = r
        return r
    }
}

然后再来一个可以递归的方法:

func memoize<T: Hashable, U>(work: @escaping ((T)->U, T) -> U) -> (T)->U {
    var memo = Dictionary<T, U>()
    func wrap(x: T)->U {
        if let q = memo[x] { return q }
        let r = work(wrap, x)
        memo[x] = r
        return r
    }
    return wrap
}

这两个函数包含了swift中的很多高级语法:

  • 强大的编译器推断匹配,比如你调用:
     let fibonacci = memoize {
    (n: Int) in
    String(n)
}

那么上面泛型中的T就会被推断成Int,而U怎会被推断成String。

  • 尾随闭包,比如调用let fibonacci = memoize {...}的时候。
  • 更通用,更安全,性能更高的泛型函数。

关于这个函数,有人专门写了一篇博客来说明:https://medium.com/@mvxlr/swift-memoize-walk-through-c5224a558194

泛型结构体的一个例子

struct StringStack {
  mutating func push(x: String) {
items += x } 
  mutating func pop() -> String {
    return items.removeLast()
} 
  var items: String[]
}

这里我们创建了一个字符串的栈,如果我们要变为泛型,则需要:

struct Stack<T> {
  mutating func push(x: T) {
items += x } 
  mutating func pop() -> T {
    return items.removeLast()
} 
  var items: T[]
}

这样我们就可以往里面放任何数据类型了:

var intStack = Stack<Int>()
intStack.push(42)

但是当我们需要对这个Stack做for in操作时候,却得到了下面的错误提示:
for_in_error
这时候我们需要理解下,什么是Sequence,当Swift给我们做for in操作的时候在底层给我们做了什么?

// 我们代码
 for x in someSequence {
  ...
 }
// Swift翻译后的代码
var __g = someSequence.generate()
while let x = __g.next(){
...
}

从上面可以看到,首先我们的someSequence需要关联一个generate,并且这个generate需要实现一个next方法,这样我们就可以对这个结构体做for in操作了。
我们先看这个generate(),它是一个结构体,其遵守Generator(protocol)。

protocol Generator {
 typealias Element
 mutating func next() -> Element ?
}

然后我们创建一个遵守Generator的结构体:

struct StackGenerator<T> : Generator {
  typealias Element = T
  mutating func next() -> T? {
    if items.isEmpty { return nil }
    let ret = items[0]
    items = items[1..items.count]
    return ret
} 
  var items: Slice<T>
} 

那么什么是Sequence呢?它也是一个protocol

 protocol Sequence {
  typealias GeneratorType : Generator
  func generate() -> GeneratorType
} 

它关联了一个Generator,然后我们让新建的Stack遵守这个Sequence协议:

extension Stack : Sequence {
  func generate() -> StackGenerator<T> {
    return StackGenerator( items[0..itemCount] )
  }
} 

这样就可以对Stack做for in操作了。

func peekStack(s: Stack<T>) {
  for x in s { println(x) }
} 

关于Sequence和Generator的详细说明,请看我的另外一篇博客Swift中Collection的Protocol

执行效率

Swift是静态编译,很小的runtime。写好的代码传到设备上之后不需要再重新编译,只需要等着运行即可。Swift中的编译器,相比于C,C++,Objective-C的Clang,做了一步优化:
SwiftComplier
除此之外,Swift会在编译时候做以下几点

  • 全局分析App
  • 使用Struct不会对Runtime的性能造成影响
  • Int,Float等很多标准库都是Struct的

Swift还有去虚拟化的特点,它可以让再运行期确定的事情放到了编译期,这样就会更快了,详细内容可以参考我的另外一篇博客为什么Swift比OC快

AVFoundation--视频播放

Posted on 2017-08-05

使用AVPlayer对象可以用来控制asset的播放。在播放期间,你可以使用AVPlayerItem实例来asset的presentation state。并且一个AVPlayerItemTrack对象可以管理某个独立track的展示状态。展示一个视频,你可以使用AVPlayerLayer对象。

播放Assets

一个player是一个控制器对象,你可以使用它来管理一个asset的播放,比如,开始和停止播放以及寻找特殊的时间点。使用AVPlayer实例来播放一个asset。你可以使用AVQueuePlayer对象来按序播放一系列的item(AVQueuePlayer是AVPlayer的一个子类)。

一个player给你提供了播放状态的信息,如果有需要,你可以让你的UI和player的状态相同步。通常情况下,你可以直接指出player的输出到一个特定的Core Animation的layer上(AVPlayerLayer或者AVSynchronizedLayer)对象。

多个player layer:你可以对一个AVPlayer实例创建很多的AVPlayerLayer对象,但是只有最近创建layer才可以在屏幕上展示在视频内容。

你不用给AVPlayer对象直接提供assets,尽管你最终想要播放的是asset。相反,你需要提供一个AVPlayerItem的实例。以个item用来管理其相关联的asset的presentation state。一个item包含一个AVPlayerItemTrack的实例,这个实例和asset中的track相对应。结构如下:
PlaeyItem关系图
下面的图说明了你可以用不同的player同时播放一个指定的asset,但是每个player都可以用不同的方式进行渲染。例如,使用item track,你可以在播放期间让一个特定的track失效(比如,你可能不想播放一个音频部分)。
AVPlayerItem

你可以使用一个已经存在的asset来初始化一个player,或者你可以用一个URL来初始化一个player,以便于你可以再一个特定的点来播放这个资源(AVPlayerItem将会对这个资源的创建和配置这个asset)。和AVAsset一样,仅仅初始化一个player item并不意味着它可以直接用来播放。你可以使用KVO来观察这个item的status属性来决定播放的时机及播放的逻辑。

处理不同类型的Asset

你可以根据将要播放的不同的Asset类型来决定怎样配置asset。一般说来,有两种不同的类型:文件类型的assets,有几种可以选择,比如:本地文件,相机胶卷,或者媒体库;另外就是基于流的assets(HTTP直播流形式)。

基于文件的视频加载,为了播放基于文件的视频,有以下步骤:

  • 创建一个AVURLAsset对象。
  • 使用asset创建一个AVPlayerItem对象
  • 将一个AVPlayer和这个item对象相关联
  • 等待,一直到这个item的status属性指明可以播放了(利用KVO)

基于HTTP直播视频流来播放,利用该URL创建一个AVPlayerItem。(你不可以直接创建一个AVAsset对象来代表HTTP Live Stream的媒体)

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
	NSURL *url = [NSURL URLWithString:@"<#Live stream URL#>];
// You may find a test stream at
<http://devimages.apple.com/iphone/samples/bipbop/bipbopall.m3u8>.
self.playerItem = [AVPlayerItem playerItemWithURL:url];
[playerItem addObserver:self forKeyPath:@"status" options:0
context:&ItemStatusContext];
self.player = [AVPlayer playerWithPlayerItem:playerItem];
```

当你将这个player item和以个player相结合的时候,它就变为待播放状态了。当它准备好播放的时候,这个player item创建`AVAsset`以及`AVAssetTrack`实例,你可以利用它来检测直播流的内容。想要得到这个item的播放时长,你可以观察其`duration`属性。当这个item状态变为可以播放时,这个属性就会更新到这个视频流的准确数值。
*注:当这个状态变为AVPlayerItemStatusReadyToPlay的时候,这个播放时长可以使用下面的代码来获取时长*

```objc
[[[[[playerItem tracks] objectAtIndex:0] assetTrack] asset] duration];
```

如果你仅仅想要播放一个直播流,那么你可以走个捷径,直接使用这个`URL`创建一个player:

```objc
self.player = [AVPlayer playerWithURL:<#Live stream URL#>];
[player addObserver:self forKeyPath:@"status" options:0
context:&PlayerStatusContext];
```

和asset和item一样,初始化完player之后并不意味着你可以立即使用播放了。你需要观察其`status`属性,该属性变为`AVPlayerStatusReadyToPlay`的时候就表明它可以播放了。你也可以观察`currentItem`来获取已经创建的item。

**如果你不知道你要播放的URL的类型**,那么你需要这样做:

1. 尝试使用这个URL来初始化一个`AVURLAsset`对象,然后加载其`tracks`key。如果tracks加载成功,那么你可以为这个asset创建一个player item。
2. 如果1失败了,那么利用这个URL直接创建一个`AVPlayerItem`对象。观察这个player的`status`属性,来决定是否可以播放了。

只要其中一个成功,你就可以得到一个player item,然后将其和一个player对象相关联。

## 播放一个Item

为了开始播放,你需要调用player的`play`方法即可:

```objc
- (IBAction)play:sender {
[player play];

}
```

除了紧紧播放以外,你可以管理播放过程中各种方面,比如,playhead的位置和速度。你也可以观察player的stata。比如,你如果想将UI和asset的presention state相同步,你就要这样做。

### 改变播放速度

你可以通过设定player的`rate`属性来改变其播放的速度。

```objc
aPlayer.rate = 0.5;
aPlayer.rate = 2.0;
```

1.0的数值表示利用当前`item`的正常速度播放,0.0速度和暂停是一样的效果。
支持回播的player,可以使用一个负值来设置这这个播放速度。你可以使用`canPlayReverse`(是否支持数值-1.0的播放速度)属性来检测其是否可以支持回播,使用`canPlaySlowReverse`(支持0.0到1.0的播放速度),以及`canPlayFastReverse`(支持小于-1.0的播放速度)

### 寻找-重置Playhead

为了将playhead移动到一个特定的时间点,你通常需要使用`seekToTime`:

```objc
CMTime fiveSecondsIn = CMTimeMake(5,1);
[player seekToTime: fiveSecondsIn];
```

然而,这个`seekToTime:`方法不是很精确,尽管其性能较高。如果你要精确得移动这个`playhead`,你可以使用下面的`seekToTime:toleranceBefore:toleranceAfter:`方法。

```objc
CMTime fiveSecondsIn = CMTimeMake(5,1);
[player seekToTime: fiveSecondsIn toleranceBefore: kCMTimeZero toleranceAfter: kCMTimeZero];
```

上面的例子中将`tolerance`设置为零需要框架解码大量的数据。因此,仅仅在必要的时候再使用零,比如:你需要写一个精确的媒体编辑应用,它需要精确的控制。

在视频播放之后,player的head被设定在了item的尾部,因此接下来调用`play`操作是不起作用的。为了将playhead放到item的起始位置,你需要注册一个item的`AVPlayerItemDidPlayToEndTimeNotification`通知,在该通知的回调方法中,你调用`seekToTime:`方法,并且传入`kCMTimeZero`参数。

```objc
// Register with the notification center after creating the player item.
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(playerItemDidReachEnd:)
name:AVPlayerItemDidPlayToEndTimeNotification
object:<#The player item#>];

- (void)playerItemDidReachEnd:(NSNotification *)notification {
[player seekToTime:kCMTimeZero];
}
```

## 很多Item的播放

你可以使用`AVQueuePlayer`对象来播放一系列的`item`。这个`AVQueuePlayer`类是`AVPlayer`类的子类。通过一个`item`的数组,你可以初始化一个`queue player`。

```objc
NSArray *items = <#An array of player items#>;
AVQueuePlayer *queuePlayer = [[AVQueuePlayer alloc] initWithItems:items];
```

然后调用其`play`方法即可。这个player会按序播放这些`item`。如果不想播放某个`item`可以调用它的`advanceToNextItem`方法。
可以使用`insertTtem:afterItem:`,`removeItem:`,以及`removeAllItems`方法,如果要插入一个item,你需要首先调用`canInsertItem:afterItem:`方法来确定它是否可以插入到这个`queue`中。你可以传给第二个参数`nil`,来检测是否新的`item`可以被加到`queue`的后面。

```objc
AVPlayerItem *anItem = <#Get a player item#>;
if ([queuePlayer canInsertItem:anItem afterItem:nil]) {
[queuePlayer insertItem:anItem afterItem:nil];
}
```

## 监控视频播放

你可以监控正在player的显示状态以及其正在播放的item的各个方面。这对你所不能控制的状态改变来说是极其有益的,比如:

* 比如如果用户使用多任务操作来切换应用,那么一个player的`rate`属性就会掉到0.0;
* 如果你正在播放一个远程的媒体,一个player item的`loadedTimeRangs`和`seekableTimeRanges`属性就会在更多的数据变得可用的时候改变。这些属性告诉你这些player item的那些部分是可用的。
* 在HTTP直播流被创建的时候,player item的`tracks`属性就会改变。如果这个视频流对内容提供了不同的编码格式,那么这就会发生;在player切换不同的编码的时候,这个`tracks`就改变了。
* 如果一个视频播放失败,那么这个player或者player item的`status`属性可能会改变。

你可以使用KVO来监控这些属性值的改变。

>你应该将注册KVO以及取消注册KVO都放到主线程中。这样,如果另外一个线程发生了改变,这将会避免收到部分通知的可能。尽管这些属性的变化会在其它线程上,但是AV Foundation触发`observeValueForKeyPath:ofObject:change:context:`是在主线程上。

### 响应某个状态的改变

当一个player或者一个player item的状态改变的时候,它发出一个KVO的变化通知。如果一个对象由于某种原因不能够被播放(比如,媒体服务被重置),其状态将会变为`AVPlayerStatusFailed`或者`AVPlayerItemStatusFailed`。在这种情况下,这个对象的`error`属性将会变为一个error 对象,这个对象中包含了其不能够播放的原因描述。

AVFoundation不会指明这个通知发送的线程。如果你想改变UI,你必须要确保任何相关的代码会在主线程中执行。比如,你可以使用`dispatch_async`来在主线程中执行代码。

```objc
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary *)change context:(void *)context {
if (context == <#Player status context#>) {

AVPlayer *thePlayer = (AVPlayer *)object;
if ([thePlayer status] == AVPlayerStatusFailed) {
NSError *error = [<#The AVPlayer object#> error];
// Respond to error: for example, display an alert sheet.
return;
}
// Deal with other status change if appropriate.
}
// Deal with other change notifications if appropriate.
[super observeValueForKeyPath:keyPath ofObject:object
change:change context:context];
return;
}
```

### 追踪可视化播放的准备状态

你可以观察`AVPlayerLayer`对象的`readyForDisplay`属性来获取当`layer`的用户可视内容改变时发出的通知。尤其重要的是,只有在用户可以看到一些东西的时候,你才可以将一个player layer插入到layer tree中,然后执行转化操作。

### 追踪时间

为了追踪一个`AVPlayer`对象中`playhead`的位置,你可以使用addPeriodicTimeObserverForInterval: queue: usingBlock:或者addBoundaryTimeObserverForTimes: queue: usingBlock:比如,你可以用过去的时间以及剩余的时间来更新用于界面,或者执行其它的UI同步操作。

* 如果时间超过了你所指定的周期点,以及视频播放开始或者停止,这个时候`addPeriodicTimeObserverForInterval:queue:usingBlock:`将会被触发。
* 你也可以指定在某些时间触发`addBoundaryTimeObserverForTimes:queue:usingBlock:`,这里你需要传递一个包含被`NSValue`封装的`CMTime`数组。

如果你要想这个基于时间的`observation block`被触发,那么你必须要对这两个方法返回的对象做强引用。同时你必须在每次触发这些方法时调`removeTimeObserver:`。使用这些方法,AV Foundation不会保证在每次的时间间隔或者时间范围达到的时候都触发这些操作。如果之前的block没有执行完毕,那么AV Foundation不会执行接下来的block。因此,你必须保证在block中执行的任务不能消耗太多的CPU资源。

```objc
/ Assume a property: @property (strong) id playerObserver;

Float64 durationSeconds = CMTimeGetSeconds([<#An asset#> duration]);
CMTime firstThird = CMTimeMakeWithSeconds(durationSeconds/3.0, 1);
CMTime secondThird = CMTimeMakeWithSeconds(durationSeconds*2.0/3.0, 1);
NSArray *times = @[[NSValue valueWithCMTime:firstThird], [NSValue
valueWithCMTime:secondThird]];

self.playerObserver = [<#A player#> addBoundaryTimeObserverForTimes:times queue:NULL usingBlock:^{
NSString *timeDescription = (NSString *) CFBridgingRelease(CMTimeCopyDescription(NULL, [self.player currentTime]));
NSLog(@"Passed a boundary at %@", timeDescription);
}];
```

### 某个Item结束了

当一个Item播放结束的时候,你可以收到一个`AVPlayerItemDidPlayToEndTimeNotification`通知。你可以注册这个通知。

```objc
[[NSNotificationCenter defaultCenter] addObserver:<#The observer, typically self#> selector:@selector(<#The selector name#>)
name: AVPlayerItemDidPlayToEndTimeNotification object:<#A player item#>];
```

## 综合:使用AVPlayerLayer播放一个视频文件

接下来会用简单的代码实例来演示怎样使用`AVPlyer`对象来播放一个视频文件。它包含以下几部分内容:

* 使用`AVPlayerLayer`来配置View
* 创建一个`AVPlayer`对象
* 基于视频文件创建一个`AVPlayerItem`,并且使用KVO来观察其状态
* 通过使能按钮来让该Item准备播放
* 播放该`Item`并且将这个播放完的Item的head重置到开始

*注:为了展示最关键的代码,该实例略去了一个完整的应用所需要的功能点,比如:内存管理,注销观察者(对KVO的观察或者对某个通知的监听)。为了能够很好的使用AV Foundation,你需要对Cocoa有丰富的经验,以便处理可能遗漏的功能点。*

### Player View

为了播放一个Asset的可视部分,你需要一个包含了AVPlayerLayer的View以便这个AVPlayer对象的输出可以被获取。创建一个UIView的子类就可以完成这些内容:

```objc
#import <UIKit/UIKit.h>
#import <AVFoundation/AVFoundation.h>
@interface PlayerView : UIView
@property (nonatomic) AVPlayer *player;
@end
@implementation PlayerView
+ (Class)layerClass {
return [AVPlayerLayer class];
}
- (AVPlayer*)player {
return [(AVPlayerLayer *)[self layer] player];
}
- (void)setPlayer:(AVPlayer *)player {
[(AVPlayerLayer *)[self layer] setPlayer:player];
}
@end
```

### 一个简单的ViewController

假如你有一个类似下面的ViewController:

```objc
@class PlayerView;
@interface PlayerViewController : UIViewController

@property (nonatomic) AVPlayer *player;
@property (nonatomic) AVPlayerItem *playerItem;
@property (nonatomic, weak) IBOutlet PlayerView *playerView;
@property (nonatomic, weak) IBOutlet UIButton *playButton;
- (IBAction)loadAssetFromFile:sender;
- (IBAction)play:sender;
- (void)syncUI;
@end

这个syncUI的方法可以将button和player的状态相同步。

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
 - (void)syncUI {
if ((self.player.currentItem != nil) &&
([self.player.currentItem status] == AVPlayerItemStatusReadyToPlay)) {
self.playButton.enabled = YES;
}
else {
self.playButton.enabled = NO;
} }
```

在ViewController的`viewDidLoad`方法中你可以触发这个`syncUI`来确保View在首次展示时候的用户界面是统一的。

```objc
- (void)viewDidLoad {
[super viewDidLoad];
[self syncUI];
```

其余的属性和方法将会在接下来的部分中给以描述。

### 创建Asset

使用`AVURLAsset`来将一个URL创建为一个Asset(接下来的例子假如你的项目包含了一个可用的Video资源)。


```objc
- (IBAction)loadAssetFromFile:sender {
NSURL *fileURL = [[NSBundle mainBundle]
URLForResource:<#@"VideoFileName"#> withExtension:<#@"extension"#>];
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:fileURL options:nil];
NSString *tracksKey = @"tracks";
[asset loadValuesAsynchronouslyForKeys:@[tracksKey] completionHandler:
^{
// The completion block goes here.
}];
}
```

在完成的Block回调中,你可以给这个asset创建一个`AVPlayerItem`对象,并且将其设置为`player view`的`player`。和创建`asset`一样,仅仅创建一个`player item`并不意味着就可以立即使用了。为了确定什么时候可以播放,你需要观察`item`的`status`属性。你需要将该player item对象和player关联之前来配置这种KVO的监听。

当你将player item和player关联的时候,你需要触发player item的准备。

```objc
// Define this constant for the key-value observation context.
static const NSString *ItemStatusContext;
// Completion handler block.
dispatch_async(dispatch_get_main_queue(),
^{
NSError *error;

error:&error];
AVKeyValueStatus status = [asset statusOfValueForKey:tracksKey
if (status == AVKeyValueStatusLoaded) {
self.playerItem = [AVPlayerItem playerItemWithAsset:asset];

// ensure that this is done before the playerItem is associated with the player
[self.playerItem addObserver:self forKeyPath:@"status"
options:NSKeyValueObservingOptionInitial
context:&ItemStatusContext];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(playerItemDidReachEnd:)
name:AVPlayerItemDidPlayToEndTimeNotification
object:self.playerItem];
self.player = [AVPlayer playerWithPlayerItem:self.playerItem]; [self.playerView setPlayer:self.player];
} else {

// You should deal with the error appropriately.
NSLog(@"The asset's tracks were not loaded:\n%@", [error
localizedDescription]);
}
});
```

### 响应Player Item的状态变更

当一个player item的状态变更的时候,这个View Controller收到一个KVO的变更通知。AV Foundation不会去指明这个通知发送到哪个线程上。如果你需要变更UI,那么你必须确保相关代码要在主线程中执行。改代码使用`diapatch_async`来将将同步UI的操作放到主线程中去。

```objc
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary *)change context:(void *)context {
if (context == &ItemStatusContext) {
dispatch_async(dispatch_get_main_queue(),
^{
[self syncUI];
});

return;
}
[super observeValueForKeyPath:keyPath ofObject:object
change:change context:context];

return;
}
```

### 播放该Item

播放视频需要给player对象发送一个`play`的消息:

```objc
- (IBAction)play:sender {
[player play];
}
```

这个Item被播放了一次。在播放完成之后,playhead被放置在item的尾部,所以如果进一步触发其`play`方法是不起作用的。为了playhead放置在item的起始位置,你可以对该item注册并收到一个`AVPlayerItemDidPlayToEndTimeNotification`的通知。然后在通知的会调中触发`seekToTime:`方法,传入`kCMTimeZero`参数即可:

```objc
// Register with the notification center after creating the player item.
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(playerItemDidReachEnd:)
name:AVPlayerItemDidPlayToEndTimeNotification
object:[self.player currentItem]];
- (void)playerItemDidReachEnd:(NSNotification *)notification {
[self.player seekToTime:kCMTimeZero];
}

AVFoundation--使用Asset

Posted on 2017-08-04

Asset可能来自于文件,或者用户的library或者Photo。创建完Asset对象之后,你所需要的所有信息不回立马就可用。一旦你有一个movie asset,你可以从它里面抽取静态图片,转码成其它格式,或者将其内容剪切。

新建一个Asset对象

使用一个URL,就可创建出一个Asset,使用AVURLAsset。最简单的就是从一个文件中创建一个Asset:

1
2
NSURL *url = <#A URL that identifies an audiovisual asset such as a movie file#>;
AVURLAsset *anAsset = [[AVURLAsset alloc] initWithURL:url options:nil];

最后的options参数是一个字典。唯一的key是AVURLAssetPreferPreciseDurationAndTimingKey它相应的Value是一个布尔值(包含在NSValue对象里),它表明了是否这个Asset应该用来指明一个精确的持续时间提供一个精确地随机获取(provide precise random access)。
但是获取精确的持续时间需要提前处理很多东西。使用粗略的持续时间通常是更快捷的操作,并且这对回播来说已经足够了。因此

  • 如果你只需要播放这个Asset,那么要么传nil,要么传一个子字典,其key为AVURLAssetPreferPreciseDurationAndTimingKey,它相应的值为NO(NSValue对象)。
  • 如果你要将该Asset进行视频合成(AVMutableComposition),那么通常你需要一个精确的random access。将这个字典的Value值传YES(用NSValue对象)。

创建方式如下所示:

1
2
3
4
5
NSURL *url = <#A URL that identifies an audiovisual asset such as a movie file#>; 
NSDictionary *options = @{ AVURLAssetPreferPreciseDurationAndTimingKey :
@YES };
AVURLAsset *anAssetToUseInAComposition = [[AVURLAsset alloc]
initWithURL:url options:options];

获取用户的Asset

为了获取iPod library或者相册,你首先需要得到这个Asset的URL。

  • 为了获取iPod Library,你需要创建一个MPMediaQuery对象来找到你需要的item,然后用MPMediaItemPropertyAssetURL来获取其URL。
  • 为了获取相册的Asset,你需要使用ALAssetsLibrary。

下面这个例子说明了怎样获取展示在相册中的第一个视频:

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
ALAssetsLibrary *library = [[ALAssetsLibrary alloc]init];
// Enumerate just the photos and videos group by using ALAssetsGroupSavedPhotos.
[library enumerateGroupsWithTypes:ALAssetsGroupSavedPhotos usingBlock:^(ALAssetsGroup *group, BOOL *stop) {

// Within the group enumeration block, filter to enumerate just videos.
[group setAssetsFilter:[ALAssetsFilter allVideos]];
[group enumerateAssetsAtIndexes:[NSIndexSet indexSetWithIndex:0] options:0 usingBlock:^(ALAsset *result, NSUInteger index, BOOL *stop) {

if (result) {

ALAssetRepresentation *represention = [result defaultRepresentation];
NSURL *url = [represention url];
// Do something interesting with the AV asset.
AVAsset *avAsset = [AVURLAsset URLAssetWithURL:url options:nil];


}

}];

} failureBlock:^(NSError *error) {

// Typically you should handle an error more gracefully than this.
NSLog(@"No groups");

}];

这里需要引入#import <AssetsLibrary/AssetsLibrary.h>。

准备Asset来用

初始化一个Asset(或者Track)并不意味着你立即就可以使用所有的信息。它还需要时间来计算这个Asset的时长(比如:一个MP3文件可能会包含一些总结信息)。你可以遵守AVAsynchronousKeyValueLoading协议来异步获取其结果,这样可以不用阻塞主线程(AVSeet和AVAssetTrack都遵守AVAsynchronousKeyValueLoading协议)。可以使用statusOfValueForKey:error:来测试是否某个某个属性的值被正常加载了。首次加载的时候,它所有属性的值是AVKeyValueStatusUnknown。使用loadValuesAsynchronouslyForKeys:completionHandler:,可以加载一个或者多个属性的值。由于网络原因或者加载被取消,所以需要一直准备加载。

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
ALAssetRepresentation *represention = [result defaultRepresentation];
NSURL *url = [represention url];
// Do something interesting with the AV asset.
AVAsset *avAsset = [AVURLAsset URLAssetWithURL:url options:nil];
NSArray *keys = @[@"duration"];
[avAsset loadValuesAsynchronouslyForKeys:keys completionHandler:^{

NSError *error = nil;
AVKeyValueStatus tracksStatus = [avAsset statusOfValueForKey:@"duration" error:&error];

switch (tracksStatus) {
case AVKeyValueStatusLoaded:

[self updateUserInterfaceForDuration];
break;
case AVKeyValueStatusFailed:

[self reportError:error forAsset:asset];
break;
case AVKeyValueStatusCancelled:
// Do whatever is appropriate for cancelation.
break;
}

}];

如果想准备一个asset来回播,那么还需要加载它的tracks属性。

从视频中获取静态图片

如果想要从asset中得到了一静态的图片,例如缩略图,你需要使用AVAssetImageGenerator对象。使用asset初始化一个generator。尽管在初始化的时候asset不会处理不可视的track,但是初始化还是可能会失败的,这就需要在必要的时候检查是否asset有track中包含有可视化的,使用tracksWithMediaCharacteristic方法:

1
2
3
4
5
6
AVAsset anAsset = <#Get an asset#>;
if ([[anAsset tracksWithMediaType:AVMediaTypeVideo] count] > 0) {
AVAssetImageGenerator *imageGenerator =
[AVAssetImageGenerator assetImageGeneratorWithAsset:anAsset];
// Implementation continues...
}

可以对图片生成器的不同方面做不同的配置,例如可以使用maximumSize来指明其最大的尺度,使用apertureMode来指明孔径的模式。然后你就可以在给定的时间点来产生一张或者多张图片。必须要强引用这个图片生成器否则,其会被释放掉。

产生单独的一张图片

使用copyCGImageAtTime:actualTime:error:来产生在特定时间点的图片。AVFoundation框架可能不会在你请求的时候立即产生出一张图片,因此你可以给第二个参数传递一个指针,指向一个CMTime,该指针包含了图片实际产生的时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
AVAsset *myAsset = <#An asset#>];
AVAssetImageGenerator *imageGenerator = [[AVAssetImageGenerator alloc]
initWithAsset:myAsset];
Float64 durationSeconds = CMTimeGetSeconds([myAsset duration]);
CMTime midpoint = CMTimeMakeWithSeconds(durationSeconds/2.0, 600);
NSError *error;
CMTime actualTime;
CGImageRef halfWayImage = [imageGenerator copyCGImageAtTime:midpoint
actualTime:&actualTime error:&error];
if (halfWayImage != NULL) {
NSString *actualTimeString = (NSString *)CMTimeCopyDescription(NULL, actualTime);
NSString *requestedTimeString = (NSString *)CMTimeCopyDescription(NULL,
midpoint);
NSLog(@"Got halfWayImage: Asked for %@, got %@", requestedTimeString,
actualTimeString);
// Do something interesting with the image.
CGImageRelease(halfWayImage);
}

产生一系列的图片

为了产生一系列的图片,你应该调用图片生成器的generateCGImagesAsynchronouslyForTimes:completionHandler:方法,第一个参数是一个包含NSValue对象的数组,NSValue对象是由CMTime生成的,它指明了你需要生成的图片所在的时间点。第二个参数是每个图片生成之后的回调。其中包含的字段包括

  • 图片
  • 请求图片的时间和图片生成的真实时间
  • 失败时候错误信息的error对象

在实现block的过程中,需要检测结果来决定图片是否生成了。除此之外:在生成图片之前要一直强引用这个图片生成器。

AVAsset *myAsset = nil;
self.imageGenerator = [AVAssetImageGenerator assetImageGeneratorWithAsset:myAsset]; 
Float64 durationSeconds = CMTimeGetSeconds([myAsset duration]);
CMTime firstThird = CMTimeMakeWithSeconds(durationSeconds/3.0, 600);
CMTime secondThird = CMTimeMakeWithSeconds(durationSeconds*2.0/3.0, 600);
CMTime end = CMTimeMakeWithSeconds(durationSeconds, 600);
NSArray *times = @[ NSValue valueWithCMTime:kCMTimeZero],
                    [NSValue valueWithCMTime:firstThird], 
                    [NSValue valueWithCMTime:secondThird],
                    [NSValue valueWithCMTime:end]];
[imageGenerator generateCGImagesAsynchronouslyForTimes:times completionHandler:^(CMTime requestedTime, CGImageRef image, CMTime actualTime,
AVAssetImageGeneratorResult result, NSError *error) {
NSString *requestedTimeString = (NSString *) CFBridgingRelease(CMTimeCopyDescription(NULL, requestedTime));
NSString *actualTimeString = (NSString *)
                      CFBridgingRelease(CMTimeCopyDescription(NULL, actualTime));
 NSLog(@"Requested: %@; actual %@", requestedTimeString, actualTimeString);

if (result == AVAssetImageGeneratorSucceeded) {
    // Do something interesting with the image.
} 
if (result == AVAssetImageGeneratorFailed) {
NSLog(@"Failed with error: %@", [error localizedDescription]); 
}
if (result == AVAssetImageGeneratorCancelled) {
    NSLog(@"Canceled");
}
}];

如果想要在中途终止图片序列的生成,可以调用cancelAllCGImageGeneration方法。

视频的裁剪和转码

使用AVAssetExportSession对象,可以将视频从一种格式到另一种格式进行转码,并且可以裁剪视频。一个export session是一个控制器对象,它管理着一个asset的异步输出。你可以将想要输出的asset以及输出的用来表明输出选项的preset来初始化一个session。然后你可以配置这个输出的session来指明输入的URL,以及文件格式,以及其它设置的一些可选配置,如metadata,以及是否输出需要被优化。

视频转码
可以使用exportPresetsCompatibleWithAsset:来检测你是否可以输出一种给定的asset,如下所示

AVAsset *anAsset = <#Get an asset#>;
NSArray *compatiblePresets = [AVAssetExportSession
exportPresetsCompatibleWithAsset:anAsset];
if ([compatiblePresets containsObject:AVAssetExportPresetLowQuality]) {
    AVAssetExportSession *exportSession = [[AVAssetExportSession alloc]
        initWithAsset:anAsset presetName:AVAssetExportPresetLowQuality];
    // Implementation continues.
}

你可以通过提供输出的URL(该URL必须是一个文件的URL)来配置这个session,AVAssetExportSession可以根据输出的URL的路径扩展,来推断需要输出的文件类型;然而也可以通过使用outputFileType直接进行配置。你也可以指明一些其它的属性,比如时间范围,输出长度的限制,是否输出的文件需要被优化来作为网络使用,以及一个视频的合成。下面的例子说明了如何使用timeRange属性来裁剪视频:

exportSession.outputURL = <#A file URL#>;
exportSession.outputFileType = AVFileTypeQuickTimeMovie;
CMTime start = CMTimeMakeWithSeconds(1.0, 600);
CMTime duration = CMTimeMakeWithSeconds(3.0, 600);
CMTimeRange range = CMTimeRangeMake(start, duration);
exportSession.timeRange = range;

调用exportAsynchronouslyWithCompletionHandler:方法可以用来创建一个新的文件。当输出操作结束的时候这个文成的block将会被回调。如果你要处理这个回调,那么你需要检测这个seesion的status值来确定是否这次输出是成功的,失败的,或者是被取消的。

[exportSession exportAsynchronouslyWithCompletionHandler:^{
          switch ([exportSession status]) {
              case AVAssetExportSessionStatusFailed:
                  NSLog(@"Export failed: %@", [[exportSession error]
  localizedDescription]);
                  break;
              case AVAssetExportSessionStatusCancelled:
                  NSLog(@"Export canceled");
                  break;
              default:
break; } 
}];

通过调用session的cancelExport方法,可以取消某次输出。
如果你试图往一个已经存在的文件中重复写入,或者往应用的沙盒之外的其它地方写文件,那么将会造成输出文件失败。下面的情况也会造成输出失败:

  • 电话呼入
  • 你的应用推到了后台,同时另一个应用也开始执行回播

这种情况下,你需要提醒用户输出失败,以便其可以重新输出。

AVFoundation--简介

Posted on 2017-08-04

AVFoundation是很多处理基于时间的音视频文件的框架之一。你可以用它来检查,创建,编辑或者对媒体文件重编码。可以从设备中得到输入流,以及在实时捕捉和播放的时候对视频进行处理。

AVFoundation

  • 如果你仅仅需要播放视频,在IOS上你可以使用Media Player框架中的MPMoviePlayerController或者MPMoviePlayerViewController,如果是基于Web的视频,那么你可以使用UIWebView。
  • 为了录制视频,并且几乎不需要关注其格式,那么你可以使用UIKit框架中的UIImagePickerController。

然而请注意:在AVFoundation中使用的一些原始数据(包括基于时间的数据结构以及携带及包含媒体信息的封装对象)都是在Core Media框架中声明的。

AVFoundation框架简介

AV Foundation中有两个方面的API(处理视频的和处理音频的)。

  • 使用AVAudioPlayer来播放音频文件
  • 使用AVAudioRecorder来录制音频文件

你可以通过AVAudioSessin对象来对配置音频的播放行为,相关配置可以参考:Audio Session Programming Guide。

AVFoundation中用来代表媒体信息的关键类是AVAsset。该框架的大部分功能都是有AVAsset来表现的。理解AVAsset将有助于理解整个框架是如何工作的。AVAsset是一片或者多片媒体数据的集合。它统一提供这个集合的信息,包括其标题,时长,自然表现大小(natural presentation size)等。一个AVAsset不合具体的数据格式相绑定。AVAsset是其它用URL来创建asset实例的父类。
asset中的每一个独立媒体片是一个统一的类型并且其成为track。

使用Assets

AVAsset是AVFoundation的基础类。AVFoundation框架的设计很大程度上都是通过这个类来表现的。它提供了视频的名称,时间,正常展示的大小(natural presentation size)。AVAsset没有和具体的数据形式相绑定。AVAsset是从一个URL来创建出Asset实例的父类,并且可以创建出新的组合。每个asset中独立的视频片段都是统一的类型叫做track。一般情况下:一个track代表音频成分,一个track代表视频成分。很重要的一点是:初始化一个asset并不意味着立即就可以用。它还需要一些时间来计算。但是这种计算不会阻塞主线程,他会异步的执行。

播放

presentation state是由player item对象管理的,某个track的presentation state是由player item track对象管理的。你可以使用player对象来播放,并且直接将其输出到Core Animation上。可以使用一个player queue有续地来管理一系列的player items。

读,写以及重新编码Asset

AV Foundation让你给一个Asset用不同的方式创建出新的展示。你可以仅仅对一个已经存在的asset进行重编码(ios4.1之后),你可以在一个asset的内容上执行不同的操作,然后将结果用新的asset来存储。将一种表现转化成其他类型的表现,可以使用asset reader和asset writer来串联两个视频动画。

缩略图

使用AVAssetImageGenerator对象可以来创建缩略图。编辑,AV Foundation使用compositions来从现有的媒体片段中新建assets。可以设置相对volume,音频track ramp,设置透明度。

Media捕捉和使用Camera

可以使用preview player来展示camera录制的内容。

AV Foundation和多线程并发

有两点需要注意的:

  • UI相关的通知应该在主线程中触发。
  • 需要自己明确指定类和方法所在线程的话会在响应的线程中年触发通知

如果要开发多线程的应用,那么你应该使用isMainThread或者[[NSThread currentThread] isEqual:<#A stored thread reference#>]来提前测试是否该线程是你期望的要执行的线程。如果要跨线程,你可能需要如下方法

1
2
3
performSelectorOnMainThread:withObject:waitUntilDone: 
performSelector:onThread:withObject:waitUntilDone:modes:
dispatch_async

为了更好的开发AVFoundation,你需要具备如下前提知识:

  • 全面理解基础的Cocoa开发工具及技术
  • 基本理解Block
  • 基本理解KVC和KVO
  • 对于视频回播,需要理解合理动画,对于基本的回播,需要理解AV Kit框架参考手册

Swift中级

Posted on 2017-08-04

本文源于对:WWDC2014–Intermediate Swift的总结。

为什么需要Optional?

我们队某个对象的操作可能会返回错误的结果:比如我们将某个字符串转为Int型,执行下面的指令:

let age = response.toInt()

这response可能是用户输入的,可能会输入不确定的数值Do you konw?那么肯定会得到错误的结果,在OC中遇到这种情况我们怎么处理呢?我们有如下的值可以表示错误:
Wrong!
但是你必须从不同的接口中去选择相应的错误类型,并且要记住这些错误类型。为了解决这个问题,Swift中引入了Optional的概念,将这种可能是nil的值进行打包。它可以表示上述所有错误的类型,同时,如果我们使用Optional,就一定要对它进行拆包,使用!号,如果不拆包会造成编译器错误,如下所示:
Need Unwrap
也可以使用Optional Binding将判断是否有值和拆包结合在一起使用:if let。

var neighbors = ["Alex", "Anna", "Madison", "Dave"] let index = findIndexOfString("Anna", neighbors)

if let indexValue = index { 
    println("Hello, \(neighbors[indexValue])")
} else {
    println("Must've moved away")
}

当然我们还可以进一步使用Optional Binding–Optional Chain:

Optional Chain Binding

在Optional Chain中,只要其中一个Optional的值是nil,那么整个Optional Chain都将是nil,并且不会再执行接下来的取值,如果不是nil则继续执行。这样让我们的代码更加简洁更加安全。
Swift中的Optional其实是个枚举:

enum Optional<T> {
   case None
   case Some(T)
}

Swift中的内存管理

在Swift中也用的是ARC,也容易出现循环引用,这时需要使用weak属性。需要注意的是

weak引用的类型是Optional的。
Binding该Optional Type将会产生一个强引用。
如果仅仅是判断即用if判断,则不会产生强引用。

例如:

if let tenant = apt.tenant {
  tenant.buzzIn()
 } 

但是有些时候我们同时需要weak,又同时需要非Optional的。那么该怎么办?我们需要unowned属性,他也是weak的。

class Person {
    var card: CreditCard?
}
class CreditCard {
unowned let holder: Person 
    init(holder: Person) {
        self.holder = holder
  } 
} 

这说明holder没持指向Person,但是holder离开了Person它就不存在了。unowned很像unsafe unretain

Swift中的初始化

在Swift的初始化中需要谨记:

所有的变量在使用前必须初始化
设定完自己所有的变量之后再调用Super的初始化方法

在下面这个初始化的例子中:
init wrong
这样在init方法中没有初始化完自己的hasTurbo变量就直接调用super方法是会在编译的时候报错的,Swift为什么要这么做呢?因为可能会出现如下的情况:
why init wrong
也就是说在父类的init方法中可能会调用filGasTank()这个方法,而这个方法被子类所覆盖了,所以这时候就可能发生意向不到的bug。
初始化方法的覆盖也可能会产生问题:
比如我们有这样一个Car的类:

class Car {
    var paintColor: Color
    func fillGasTank() {...}
    init(color: Color) {
        paintColor = color
        fillGasTank()
    }
} 
class RaceCar: Car {
    var hasTurbo: Bool
    init(color: Color, turbo: Bool) {
        hasTurbo = turbo
        super.init(color: color)
} 
    convenience init(color: Color) {
        self.init(color: color, turbo: true)
} 
    convenience init() {
        self.init(color: Color(gray: 0.4))
} } 

class FormulaOne: RaceCar {
    let minimumWeight = 642

    // inherited from RaceCar
    /*init(color: Color, turbo: Bool) {
        hasTurbo = turbo
        super.init(color: color)
    }
    convenience init(color: Color) {
        self.init(color: color, turbo: true)
    }
    convenience init() {
        self.init(color: Color(gray: 0.4))
    }
    */
} 

上面注释的内容是从父类中继承过来的,如果我们在子类中调用convenience init(color: Color)这个方法的时候,想让turbo这个参数的默认值为false,这时候我们就需要覆盖掉父类的convenience init方法了。这时我们需要实现自己的designed initializer

class FormulaOne: RaceCar {
    let minimumWeight = 642
    init(color: Color) {
        super.init(color: color, turbo: false)
} 
    // not inherited from RaceCar
    /*init(color: Color, turbo: Bool)
    convenience init()
    */
} 

这样以后被注释的内容就不会再被继承了。就会直接掉用子类的designed init方法了。

懒加载属性

如果我们的某个属性需要很大的性能消耗,那么我们希望在使用的时候再创建该类,那么我们不必像在OC中那样重写其get方法,我们只需要在变量声明的前面加上lazy关键字即可。

lazy var color:UIColor = UIColor.red

这样就可以声明了一个懒加载的属性了。

Closures

基本用法

Swift中Array的sort方法实现了Closure,我们来看下:

var clients = ["Pestov", "Buenaventura", "Sreeram", "Babbage"]

clients.sort({(a: String, b: String) -> Bool in 
return a < b }) 

println(clients)
// [Babbage, Buenaventura, Pestov, Sreeram]

这样就实现了数组中的元素排序。
但是基于Swift强大的类型推断功能,我们可以将其简化为:

clients.sort({ a, b in
return a < b
})

因为这个Closure是有返回值的,所以编译器可以再次推断,所以我们可以这样写

clients.sort({ a, b in a < b })

编译器还可以推断出其参数值,所以,我们这里可以写成

clients.sort({$0 < $1})

因为我们还有尾随闭包,所以我们可以进一步简化

clients.sort{$0 < $1}

Functional Programming

我们有很多函数式编程的高阶函数可以供调用:

let result = words.filter{ $0.hasSuffix("gry")}.map{$0.uppercaseString}

这样我们就可以找到所有以gry结尾的单词,并且将其转化为大写字母。如果这时结果是

ANGRY
HUNGRY

我们还可以调用reduce方法将其和成一个字符串

let reducedResult = result.reduce("HULK"){"\($0) \($1)"}

这时结果如下:

HULK ANGRY HUNGRY

函数值

比高可以传递一个函数,例如:

 numbers.map {
        println($0)
} 
numbers.map(println)    // 可以将一个函数传递过去


var indexes = NSMutableIndexSet()
numbers.map {
    indexes.addIndex($0)
} 

numbers.map (indexes.addIndex) // 可以将一个Method传过去

闭包是一个ARC对象

我们可以声明一个Closure属性:

var onTempratureChange: (Int) -> Void = {}
func logTemperatureDifferences(initial: Int) {
    var prev = initial
    onTemperatureChange = { next in
        println("Changed \(next - prev)°F")
prev = next 
} 

因为function也是closure,那么我们可以这样写:

func logTemperatureDifferences(initial: Int) {
    var prev = initial
    func log(next: Int) {
        println("Changed \(next - prev)°F")
prev = next } 
    onTemperatureChange = log

闭包的循环引用问题

和OC中的Block一样,Swift中也会出现循环引用的问题,我们来看看怎样解决:

class TemperatureNotifier {
    var onChange: (Int) -> Void = {}
    var currentTemp = 72
    init() {
        onChange = { temp in
currentTemp = temp
   } // error: requires explicit 'self' 
  } 
} 

如果出现上面的循环引用问题,编译器会直接报错的,所以我们可以用上文提到的unowned来解决。我们可以将init()方法用下面的来取代:

init() { 
    unowned let uSelf = self
    onChange = { temp in
      uSelf.currentTemp = temp
    }

但是这样写还会出现一个问题,就是如果别处有一份逻辑一样的代码,某个人不注意拷贝过来了忘记将self改成uSelf,或者这个方法很长,写到下面的是忘记了将self改成uSelf,那么就会出现内存泄漏的问题。为了解决这个问题Swift中提出了下面的优雅做法:

init() {
onChange = {[unowned self] temp in 
self.currentTemp = temp 
} } 

Pattern Matching

switch中可以有范围,字符串和数字,并且Enum中可以关联属性,比如:

// case中含有范围
func describe(value: Int) { switch value { 
case 0...4: println("a few") 
case 5...12: println("a lot") 
      default:
        println("a ton")
} } 
// case中

enum TrainStatus {
    case OnTime
    case Delayed(Int)
}

使用的时候如下:

switch trainStatus {
  case .OnTime:
} 
  println("on time")
case .Delayed(let minutes)
                        :
println("delayed by \(minutes) minutes")

我们可以对这个delay做各种各样的匹配:

switch trainStatus {
  case .OnTime:
    println("on time")
  case .Delayed(1):
    println("nearly on time")
  case .Delayed(2...10):
    println("almost on time, I swear")
  case .Delayed(_):
    println("it'll get here when it's ready")

Pattern Compose

也就是说Pattern可以组合出现,一个Pattern中可以包含其它的Pattern,比如对上文的TrainStatus再做以Pattern Compose:

enum VacationStatus {
    case Traveling(TrainStatus)
    case Relaxing(daysLeft: Int)
} 

switch vacationStatus {
  case .Traveling(.OnTime):
    tweet("Train's on time! Can't wait to get there!")
  case .Traveling(.Delayed(1...15)):
    tweet("Train is delayed.")
  case .Traveling(.Delayed(_)):
    tweet("OMG when will this train ride end #railfail")
  default:
  print("relaxing")

Type Pattern

Pattern不仅仅可以作用于Enum,还可以作用于动态的类型,如:Class

func tuneUp(car: Car) {
    switch car {
      case let formulaOne as FormulaOne:
        formulaOne.enterPit()
      case let raceCar as RaceCar:
        if raceCar.hasTurbo { raceCar.tuneTurbo() }
        fallthrough
      default:
        car.checkOil()
        car.pumpTires()
} } 

这样在多态中就会变得非常有用了。

Tuple Patterns

Tuple pattern有其极其强大的功能,其可以对tuple的各个数值做以类型匹配。

let color = (1.0, 1.0, 1.0, 1.0)
switch color {
  case (0.0, 0.5...1.0, let blue, _):
    println("Green and \(blue * 100)% blue")
  case let (r, g, b, 1.0) where r == g && g == b:
    println("Opaque grey \(r * 100)%")

我们甚至可以对其中的各个数值做以相应的模式匹配。

Pattern Matching的应用PList校验

比如我们有下面的方法来校验Plist中的内容是否有效

func stateFromPlist(list: Dictionary<String, AnyObject>)
  -> State?
stateFromPlist(["name": "California",
                "population": 38_040_000,
                "abbr": "CA"])

这时我们要对population的值做以限制,如果是字符串返回nil,如果是超过某个范围的时候返回nil,如果是abbr中字母的个数大于2时候我们也返回nil,利用tuple pattern matching的强大特性,我们可以这样去做:

func stateFromPlist(list: Dictionary<String, AnyObject>)
  -> State? {

switch (list["name"], list["population"], list["abbr"]) { 
case ( 
        .Some(let listName as NSString), 
        .Some(let pop as NSNumber),
        .Some(let abbr as NSString)
      ) where abbr.length == 2:
    return State(name: listName, population: pop, abbr: abbr)
  default:
return nil 
   } 
} 

这就利用了tuple和限制想结合的方式优雅的解决了这个问题。

怎样用好Value Type?

Posted on 2017-08-02

Swift–怎样用好Value Type?

Value Type

为什么要用Value Type?

首先我们要说明为什么我们需要Value Type。因为我们通常使用的Reference Type不能满足我们的需求,并且容易出Bug,什么是Reference Type?
Reference Type就是我们常说的Class,我们不是用的很好吗?我们举两个例子来看看
例一:但是现在又一种需求:进入用于信息页面编辑页面,如果用户的信息改变了,并且没有点击保存,这时如果点击导航栏的退出,要给用户提醒”您编辑了信息,确定不保存退出吗?”,这时为了保存进入编辑页面的信息,我们一般这样做

  1. 增加一个originInfo的property。
  2. 在ViewDidLoad的时候赋值:self.originInfo = passedInfo;(这个passedInfo从上个页面传来)
  3. 在点击退出的时候对比passedInfo和self.originInfo是否相等,如果不相等则提醒用户,如果相等则返回。
    这时你会发现一个bug,这两个对象永远都是相同的,为什么?

例二:我们要从商品列表中进入商品编辑页面,这时,我们点击了编辑,将goodModel传到第二个页面,在第二个页面操作完之后,用户没有保存信息,返回了,这时你也可能会发现,你列表中的商品信息改变了。

例三:Cell复用造成的布局混乱。
上述两种情景是Reference Type,也就是说他们的都指向了堆上的相同对象,这种类型对对象的公用造成了一系列的bug。并且这种bug很难被发现,并且往往也不是必现的。
怎样解决呢?
这时我们就需要Copy原来的instance,在OC中我们需要遵守NSCopying,或者NSMutableCopying协议,因为这些,然后实现相应的协议,例如:

@interface HYLocationModel : NSObject<NSMutableCopying>
// cityDic{@"name":@"",@"id":@""}
@property (nonatomic, strong) NSDictionary *cityDic; //City
@property (nonatomic, strong) NSDictionary *areaDic; //
@property (nonatomic, strong) NSDictionary *districtDic;

@end
@implementation HYLocationModel
- (id)mutableCopyWithZone:(NSZone *)zone {

    HYLocationModel *locationModel = [[HYLocationModel alloc]init];
    locationModel.cityDic = self.cityDic;
    locationModel.areaDic = self.areaDic;
    locationModel.districtDic = self.districtDic;
    return locationModel;
}
@end

这样我们就可以Copy对象了,将Copy的对象赋值给self.originInfo,就可以解决上述的bug。
但是这会消耗性能,因为需要在堆里开辟内存空间。NSCopying协议在OC中很常见,比如NSString,NSArray,NSDictionary等都遵守NSCopying协议。其中NSDictionary的Key,默认是实现了NSCopying协议的,因为在给NSDictionary赋值的时候,系统默认是Copy了它的Key,因为如果不Copy它的Key,如果你给字典赋值之后改变了这个Key,那么它将会使整个NSDictionary混乱,出现意想不到的Bug。当然这种Copy也会消耗性能。

不可变对象是否可以解决上述问题呢?

在函数式编程中,我们会使用不可变的Reference Type来消除其可变所带来的问题,想象下如果你做数学题题目A中的X值被题目B改变了,那么会有怎样的结果?
在Swift中,我们可以使用let来使其不可变,但是这种不可变的数据结构有以下弊端:

  1. 可能导致很恶心的接口(见下文)。
  2. 不能有效地和机器模型相匹配(因为我们的寄存器,我们的Cache,Memory,Storage都是可变状态的)。

比如下面的代码

// With mutability 
home.oven.temperature.fahrenheit += 10.0

//Without mutability 
let temp = home.oven.temperature
home.oven.temperature = Temperature(fahrenheit: temp.fahrenheit + 10.0) 

在上面的例子中,我们把Temperature类的某个属性改成了let,那么如果我们要更改这个数值,我们就需要在堆上开辟内存空间然后创建一个新的Temperature,最后更换掉整个Temperature类。
Cocoa[Touch]中有很多的不可变类比如:NSDate,NSURL,UIImage,NSNumber等
这更加安全了(不需要使用Copy),也不必担心接下来的程序会改变这个数值。

NSArray<NSString *> *array = [NSArray arrayWithObject: NSHomeDirectory()];
NSString *component; 
while ((component = getNextSubdir()) { 
     array = [array arrayByAddingObject: component]; } 
     url = [NSURL fileURLWithPathComponents: array];

Value Type将怎样解决这种问题呢?

Swift中的所有基础类型都是Value Type的,像:Int,Double,String …
Swift中所有的Collection都是Value Type的,像:Array,Set,Dictianry…
Swift中的Tuples,Struct,Enums如果只包含Value Types那么他们自身也是Value Type的。
Value Type要是完全可以直接比较的,可以直接使用==,自定义的Value Type并且其必须要遵守Equable协议,覆盖==方法才可以使用的。

var a: [Int] = [1, 2, 3]
var b: [Int] = [3, 2, 1].sorted(by:<)
assert(a == b) // true

如果是自身的定义的Struct,那么需要遵守Equable协议,并且覆盖==方法来实现。比如:

struct Temperature: Equatable {
  var celsius: Double = 0
  var fahrenheit: Double {
    get { return celsius * 9 / 5 + 32 }
    set { celsius = (newValue - 32) * 5 / 9 }
 }
}
func ==(lhs: Temperature, rhs: Temperature) -> Bool {
  return lhs.celsius == rhs.celsius
} 

使用Value Type不用担心竞争条件。也就是不用担心资源抢夺,加锁等。看下面的代码:

var numbers = [1, 2, 3, 4, 5]
scheduler.processNumbersAsynchronously(numbers) //异步处理numbers
for i in 0..<numbers.count { numbers[i] = numbers[i] * i }
scheduler.processNumbersAsynchronously(numbers) //异步处理numbers

在Reference Type中这个numbers将会发生资源抢夺,但在Swift中是Value Type的,在执行for i in 0..<numbers.count { numbers[i] = numbers[i] * i }的时候会发生Copy操作,所以不会发生资源抢夺。也就是说每次将Value Type赋值给其它的Value Type的时候会发生拷贝(逻辑拷贝)操作,但是这种Copy消耗的时间很微小,并且系统会将Copy推迟到写操作执行的时候,这就是Copy on Write。在Swift中可以利用Protocol将Struct等ValueType封装,类似于OOP中的多态

Swift中的Value Type和Reference Type混用会怎样呢?

我们来看Structh中含有结构体的情况:

struct ButtonWrapper {
   var button: Button
}

在这种情况下,复制ButtonWrapper的时候将会共享button这个Reference Type。这就违背了我们上文所说的Value Type在重新赋值的时候拷贝(深拷贝,和原来的Struct没有关系)。怎样才可以做到这一点呢?比如下面的

Value Type 中含有不可变的Reference Type

struct Image: Drawable {
  var topLeft:CGPoint
  var image: UIImage
}
var image = Image(topLeft:CGPoint(x:0,y:0),image:UIImage.init(named: "someImage.png"))
var image2 = image

这时image和image2将会公用一个UIImage:
ValueType Contains Reference Type
在实现Equatable协议的时候,我们这么做:

extension Image: Equatable {   }
func == (left:Iamge, right:Image) -> Bool {
 return left.topLeft == right.topLeft && left.image === right.image
}

但是由于UIImage是不可变的,所以我们不必担心image2的image对象的改变会影响到image的image对象。
注:上面===表示引用相同,但是不表示其指向Image是相同的,如果要表示其相同,需要使用==操作。

Value Type 中含有可变的Reference Type

下面我们来看一个可变的Reference Type。

struct BezierPath: Drawable {
  var path = UIBezierPath()
  var isEmpty: Bool {
    return path.empty
} 
  // **注意这种写法是错误的**
  func addLineToPoint(point: CGPoint) {
    path.addLineToPoint(point)
} } 

其内存结构是这样的:
Value Contains Reference Type

这时如果我们如果执行下面的代码

var bezierPath1 = bezierPath0

就会发现意想不到的Bug,因为你对bezierPath1的任何改动都将会显示到bezierPath0上。
怎样解决这样的问题呢?这时我们需要使用Copy On Write

对Value Type中的Reference Type做改动将会破坏Value Type的”完全独立”特性。
所以我们必须将可变的Reference Type和不可变的操作分开
不可变操作总是安全的
可变操作必须首先Copy

怎样做到Copy On Write呢?我们需要给BezierPath中加入如下代码:

struct BezierPath: Drawable { 
private var _path = UIBezierPath() 
var pathForReading: UIBezierPath { 
return _path 
} 
var pathForWriting: UIBezierPath {
    mutating get { 
    _path = _path.copy() as! UIBezierPath 
    return _path 
} } 
}

这样我们就可以将上述的错误代码改为:

extension BezierPath { 
var isEmpty: Bool {
return pathForReading.empty 
} 
 mutating func addLineToPoint(point: CGPoint) {
    pathForWriting.addLineToPoint(point)
  }
}

这样,我们在执行:

var path = BezierPath()
var path2 = path
if path.empty { print("Path is empty") }
var path2 = path
path.addLineToPoint(CGPoint(x: 10, y: 20))
path.addLineToPoint(CGPoint(x: 100, y: 125))

这段代码的时候就会在addLineToPoint的时候执行Copy,这就不会出现改动path而影响path2的现象了。
但是还有一个问题,每次执行addLineToPoint的时候都需要执行Copy操作,有时候如果这个对象只有一个引用那么就不需要这种操作,所以我们可以利用isUniquelyReferencedNonObjC()方法来判断时候需要Copy,如果返回true,说明只有一个对象在用,就不必Copy,如果返回false,说明很多对象在用,这个时候就需要执行Copy操作了。
用法如下:

struct MyWrapper {
  var _object: SomeSwiftObject
  var objectForWriting: SomeSwiftObject {
    mutating get {
     if !isUniquelyReferencedNonObjC(&_object)) {
        _object = _object.copy()
     }
     return _object
    }
} }

注:

  1. 需要标示记忆过程的时候,比如实现撤销操作,需要恢复之前数值的时候。(备忘录模式)
  2. 比如需要对新的变化做特殊处理的时候,因为我们已经记忆了之前的过程,只需要对最新的Value Type改变,比如:本博客中的第一张图片,如果衣服颜色改变了,那么就只改变衣服颜色的那几个方格的值即可。

参考资料

https://developer.apple.com/videos/play/wwdc2015/414/

为什么Swift比OC快?

Posted on 2017-08-01

Swift相比OC以及其它语言,有很多的优化点,这篇文章将从方法调度的角度去说明为什么Swift要比OC更快。OC是一门动态的语言,很多实际执行需要在运行时才可以确定,Swift不一样,Swift将很多在运行时才可以确定的信息,在编译期就决定了。这就让Swift更加快速。
方法调度就是程序在触发方法时选择需要执行指令的过程,它在每次方法执行时都会发生。如果这种调度发生在编译期,我们称它为静态调度(Static Dispatch),如果调度发生在运行时,那么我们称它为动态调度(Dynamic Dispatch)。静态调度往往要比动态调度要快。那么问题来了,为什么我们需要动态调度呢?全部用静态调度不就得了?
问题就在于我们很多时候我们需要用到多态,看看下面这段非常简单的代码

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
class Animal {

func eat() {
print("animal eat");
}
func sleep() {
print("animal sleep")
}
}

class Dog: Animal {

override func sleep() {
print("dog sleep")
}
}

class Rabbit: Animal {

override func eat() {
print("rabbit sleep");
}
override func sleep() {
print("rabbit sleep")
}

}
var animal:Animal?
var somThingTrue = false
//执行很多业务逻辑
if somThingTrue {
animal = Rabbit()

}else{

animal = Dog()
}
animal?.eat()

上面的代码中animal?.eat()就不能够在编译期确定,因为其中需要很多的业务逻辑(比如根据用户的不同,或者网络请求结果的不同)来确定就究竟创建出来的对象是Rabbit还是Dog,也就无法最终确定要调用那个对象的eat()方法。相似的代码在OC中是怎样执行的呢?在OC中编译器会将这个方法翻译成objc_msgSend(target,@selector(eat),nil)这个方法,然后到了运行时,会分为以下几步进行调用:

  1. 找到方法target中isa对应的Class(如果是类方法要到其metaClass中找)。
  2. 从其中的struct objc_method_list **methodLists找到对应的方法实现。
  3. 如果没有找到就到superClass的methodLists中找。

如果在Swift中,它是怎样做方法调度的呢?

  1. 找到target对应的class

  2. 从class的V-Table中的那得到函数的实现
    Swift中的类会创建一个V-Table,这个Table是一个数组,其中存放的是函数指针。子类会按照父类V-Table中函数的存放,如果子类没有覆盖某个方法,那么就会拷贝父类方法的地址,如上面的例子会得到下面的V-Table。

    Animal

    Index0 eat 0x0001
    Index1 sleep 0x0004

    Dog

    Index0 eat 0x0001 (copied)
    Index1 sleep 0x0008 (overrideen)

    Rabbit

    Index0 eat 0x0002 (overrideen)
    Index1 sleep 0x0003 (overrideen)

    可以注意到Dog因为没有覆盖父类的eat方法,所以其copy了父类的0x0010指针。因为Swift是Type Safe的,所以在调用它的时候它不会变成Robot或者其它的类(如果不能通过编译),所以无论是调用上面结构中的Animal,Dog,还是Rabbit类,它都是调用相同的Index,得到对应的方法实现。将函数指针和Index所做的映射在编译期就确定了,这就大大减少了运行时的工作量,提高了运行速度。所以在运行时它没有必要知道是哪个类型的实例调用了这个方法,只需要找到相应的V-Table即可,至于是其中的哪个Index已经在编译期确定了,没必要再去查找Index的值。
    然而Swift的方法调度不仅仅是动态方法调度,还有很多静态方法调度。
    如果我们将某个方法标记为final或者private,或者我们不用类,而使用结构体,枚举,这时就不需要动态调度,只需要静态调度即可,这样速度会更快。

理解Swift的性能

Posted on 2017-07-30

草稿:
本博客主要来源于WWDC2016-416,UnderStanding Swift Performance。
Swift性能指标:

  1. Stack VS Heap
  2. Reference Count
  3. Method Dispatch中Static多还是Dynamic多
    栈上的操作更加高效,降低栈指针来创建,增加栈指针来释放栈。
    堆上可以存放高级数据结构,但是堆的操作效率是更低的,因为每次开辟堆空间都需要寻找空闲的内存块。在消除的时候需要重新插入一个内存块。除此之外,Class等存在堆上的对象,需要开辟我们看不到的空间,这些空间是需要消耗内存的。
    引用计数的不断操作会降低性能。所以要尽可能少的创建Class。
    在Struct中包含Class是会降低性能的,我们要尽量减少这种情况的出现,比如在使用纯字符串的时候,我们可以使用Enum,或者其它的值类型来替代。
    如果Struct中包含Class对象,那么在CopyStruct的时候也会拷贝其引用计数,包含的Class越多,就需要操作更多的retain和release方法方法。
    Swift在调用一个函数的时候,我们需要跳到对应的方法实现。
    Static 调度:如果在编译时期就可以确定某个方法的实现的话,就可以直接跳转到相应的方法的。并且是inline方法。既然静态调度这么好,为什么还需要Dynamic Dispatch呢?因为我们需要执行各种业务逻辑,需要根据具体的场景来决定究竟调用那个方法,我们需要使用多态。
    对于类,那么编译器会增加一个指针来存储来指向那个类的Type信息,并且存到静态区。在执行函数调用时,编译器会到该类型对应的虚拟方法表(VTable)中找到相应的方法实现。将类标记为final时,其方法的调用将变为静态派发。在不需要动态派发的时候,我们应该尽量使用静态派发。

怎样是用Struct写多态呢?Protocol类型的变量是怎样被存储,被拷贝,以及方法调度是怎样工作的呢?
Static Dispatches VS Dynamic Dispatch
Static方法:可以在编译时确定需要调用的方法实现,在运行时,可以直接调用并且可以做inline优化。
Dynamic方法:需要在运行时在表中找到对应的方法的实现。并且会妨碍inlining及其它的性能优化。当我们利用了多态之后,我们就难以确定这个方法究竟是调用那个方法,也就是说必须在运行时去确定到底要执行那个方法的实现。
Swift中Protocol Type的多态,由于没有继承,所以就没有了V-Table方式的调度。Swift使用了一种Protocol Witness Table的技术来调度Protocol Type的方法,这个表格的入口和该Type的实现先链接,因此找到这个表格就找到了方法的实现,那么我们怎样找到这个Table呢?,注意数组中的数值都有相同的offset,因此Swift使用Existential Container来封装protocol type,它的前三个字(两个字节称为一个字)是valueBuffer,小数据,例如两个word的Point可以存储,但是大于三个字时候,swift将会开辟一个堆空间,并且将这个控件的地址存储在Existential Container里面。
Swift让value Type,比如Strut和protocol一起获得了动态调度行为,实现了动态多态。
Copy on write:Swift自己提供了Copy on Write的机制,但是如果我们自己写了结构体,并且结构体比较重,还有其它的引用类型,在Copy结构体的时候,我们同时需要Copy这个对象,所以在Write的时候,我们需要判断这个引用类型的引用计数,然后做相应的改变。
因此Existential Container需要处理不同的数据类型,这是怎样实现的呢?这需要另外一个基于表的机制–Value Witness Table,程序中的每一个type都有一个这样的表,它包含以下几部分:
allocate:
copy:
destruct:
deallocate:
Whole Model优化可以让同一个模块中的不同文件都可以同时优化。
Type不会在运行时改变。

Generics-Small Value
如果使用泛型,那么每次函数的调用就只能产生一个call context,并且call context中的类型是一定的,这里Swift不会使用Existential container,它可以直接传递value witness table和 protocol witness table

  1. 没有比必要开辟堆空间,没有在堆上进行操作,所以不需要考虑线程安全,无需对线程进行加锁。
  2. 没有引用计数,操作引用计数也需要是线程安全的,因为引用计数操作极其频繁,所以其性能消耗会逐渐增多,并且不是可以忽略的。
  3. 通过Protocol Witness Table进行动态调度以实现多态
    Generics-Large Value
  4. 使用indirect storage 来开辟堆空间
  5. 如果包含引用类型的话会有引用计数
  6. 通过Protocol Witness Table进行动态调度实现多态

总结:
尽可能少的使用dynamic runtime,选择合适的抽象。这样编译器可以做错误检查,并且可以做优化,提升代码执行速度。
Class类型:identity或者OOP类型的多态。
结构体&枚举类型:值语义。
Protocol types:动态多态。
泛型和值语义相结合可以实现静态多态。
使用indirect Storage来处理大数值。

Swift中Collection的Protocol

Posted on 2017-07-28

Sequences

Sequence协议位于系统架构的底层,其定义是

1
2
3
4
5
6
protocol Sequence {

associatedtype Iterator: IteratorProtocol
func makeIterator()-> Iterator

}

Iterators

1
2
3
4
5
protocol IteratorProtocol{
associatedtype Element
mutating func next()-> Element?

}

实现了IteratorProtocol协议就可以遍历实例中的数据了,比如:

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
struct PrefixIterator:IteratorProtocol {
let string:String
var offset:String.Index
init(string:String) {
self.string = string
offset = string.startIndex
}
typealias Element = String // 这里可以省略,因为编译器可以从返回值的类型中推断出来
mutating func next() -> String? {

guard offset < string.endIndex else { return nil }
offset = string.index(after: offset)
let offSetString = string[string.startIndex..<offset]
return offSetString;
}

}

var prefixIterator = PrefixIterator.init(string: "Hello")
while let content = prefixIterator.next() {
print(content)
}

struct PrefixSequence:Sequence {
let string: String
func makeIterator() -> PrefixIterator {

return PrefixIterator(string:string)

}
}

for prefix in PrefixSequence(string:"Hello"){
print("for in\(prefix)")
}
PrefixSequence(string:"Hello").map {$0.uppercased()}

如上所示,我么要想对一个Type实现for in 操作,需要两步:

  1. 创建一个遍历器:让其遵守IteratorProtocol协议
  2. 创建一个Type:让其遵守Sequence协议,在实现makeIterator方法时将第一步中的Iterator返回即可
    Iterator有两种不同的语义,值语义(将要遍历的实例拷贝)和引用语义(不拷贝所遍历的实例),比如
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
		let sequ = stride(from: 0, to: 10, by: 1)
var i1 = sequ.makeIterator() // StrideToIterator
i1.next() //Optional(0)
i1.next() //Optional(1)

var i2 = i1;
i1.next() //Optional(2)
i1.next() //Optional(3)

i2.next() //Optional(2)
i2.next() //Optional(3)
```
这个StrideToIterator是值语义,所以在赋值的时候会执行拷贝操作。
AnyIterator会将基础Iterator用内部box对象封装,这个box对象时引用类型的,所以其是引用语义。比如:
```Swift
var i3 = AnyIterator(i1)
var i4 = i3
i3.next() // Optional(4)
i4.next() // Optional(5)
let fibsSequence2 = sequence(state:(0, 1)) { (state:inout (Int, Int)) -> Int? in

let upcomingNumber = state.0
state = (state.1 , state.0 + state.1)
return upcomingNumber

}
Array(fibsSequence2.prefix(10))

Sequence的闭包是懒执行的,只有到获取某个数值的时候才执行,比如

Array(fibsSequence2.prefix(10))

这才让构造方法fibsSequence2.prefix(10)产生作用,如果Sequence提前计算了其数值,因为其实无穷Sequence,所以当越界的时候其就会崩溃。

有些Sequence是不稳定的,也就是说每次的遍历,结果可能不一样,比如网络流,磁盘文件,UI事件流,以及其它类型的数据,它们都可以被建模,作为Sequence。这也就是为什么,取出第一个元素的属性只存在于Collection中,而不是在Sequence中,因为改这个first方法一定要是nondestructive的,也就是说取出第一个元素不应该对其输出结果产生影响,也就是说必须是稳定的。

怎样判断一个Sequence是否是稳定的?
如果一个Sequence遵守Collection,那么它就是稳定的。
反过来,就不成立了:如果一个Sequence是稳定的,那么它就是遵守Collection协议的。这时不成立的。比如:StrideTo和StrideThrough类型,就是稳定的,然而他们没有遵守Collection协议。

Sequences和Iterator之间的关系是怎样的?

Sequences和Iterator那么相似,为什么不把他们合并成一个呢?在destructively consumed sequence中可以,因为他们可以共用一个Iterator,但是在stable sequence中是不行的,因为它们需要Iterator提供的隔离的遍历状态和遍历逻辑(这种遍历状态就是Iterator创建的)。makeIterator也就是为了创建这种遍历状态。

SubSequence

Sequence有另外的一个关联属性SubSequence

1
2
3
4
5
public protocol Sequence {
associatedtype Iterator : IteratorProtocol
associatedtype SubSequence
...
}

我们可以对其SubSequence做以限制,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
extension Sequence where Iterator.Element: Equatable,
SubSequence: Sequence,
SubSequence.Iterator.Element == Iterator.Element {

func headMirrorsTrail(_ n:Int) -> Bool {

let head = self.prefix(n)
let trail = self.suffix(n).reversed()
return head.elementsEqual(trail)

}
}
let result = [1,2,3,4,2,1].headMirrorsTrail(1)

Collection Protocol

如上文所示Collection都是稳定的Sequence,它可以稳定的被屡次遍历,并且没有破坏。并且其元素可以使用脚标的形式被访问。Colllection的Index通常是integer,存在数组中,并且是有限的。也就是说不像Sequence,Collection必须是有限的。这也就是为啥其有count属性。
不仅仅标准库中的Array,Set,Dictionary,CountableRange以及UnsafeBufferProinter等等遵守Collection协议,Foundation库中的Data和IndexSet也遵守Collection协议。

A Queue Implementation中对算法的解释不是很懂。

如果一个Type要遵守Collection协议,那么它需要满足以下要求:

  1. 提供startIndex属性
  2. 提供endIndex属性
  3. 提供一个subscript,它至少是readonly的,用来获取Type的Element
  4. index函数,来寻找Collection的Index

由于Collection的协议的很多方法都有默认实现,所以我们在没有特殊处理的时候不需要再实现其他的方法,比如我们定义完Collection之后就可以使用map,flatMap,filter,sorted,joined等方法。
为了让我们定义的Collection使用字面量的形式来创建,我们需要实现ExpressibleByArrayLiteral协议,该协议只有一个方法:

1
2
3
4
5
6
7
public protocol ExpressibleByArrayLiteral {

/// The type of the elements of an array literal.
associatedtype Element
/// Creates an instance initialized with the given elements.
public init(arrayLiteral elements: Self.Element...)
}

也就是在这个初始化方法里面调用自己的Designated初始化方法。

注:
只要是遵守ExpressibleByArrayLiteral的Type都可以使用如

1
let queue:FIFOQueue = [1,2,3,4]

这样的方式进行创建,至于创建的类型需要根据声明的类型以及上下文来推断,也就是说在利用[1,2,3,4]这样的方式进行初始化的时候,系统会自动的调用协议中规定的初始化方法。

1…3456
击水湘江

击水湘江

努力让明天的自己爱上今天的自己!

56 posts
10 tags
© 2019 击水湘江
Powered by Hexo
|
Theme — NexT.Muse v6.0.3