代码手工艺人

Joey 写字的地方

AudioStreamBasicDescription 简称 ASBDASBD 是 CoreAudio 用来指定线性 PCM 格式,或者使用 CBR 编码的等大小声道的格式。如果使用的是 VBR,或者使用非等大小的 CBR,需要对每个 packet 独立设置 AudioStreamPacketDescription进行描述。

在分析 ASBD 每个字段之前,我们先来搞懂几个概念:

  • Audio Stream: 一个 audio stream 表示一个声音的连续数据,比如一首歌
  • Channel:声道,一个声道是一个独立的音轨,单声道音频流只有一个 channel,立体声音频流有两个 channel
  • Sample:采样,一个采样对应一个 channel 里的一个具体的数字
  • Frame:一个 frame 包含一组相同时间的samples集合,比如一个线性立体声 PCM 文件每个 frame 包含两个 samples(左声道 sample 和 右声道 sample)
  • Packet: 一个 packet 包含一个或者多个连续的frame,在给定的音频数据格式的情况下,一个 packet 定义了一个最小有意义的 frame 集合,也是最小可测量时间的数据单位。 对于线性PCM音频这类非压缩格式,一个packet只包含一个frame
    对于压缩格式,一个 packet 通常会包含多个 frame,比如 AAC 可能会包含 1024 个 frame。
    对于某些格式,比如 Ogg,一个 packet 对应的 frame 数量可能是变化的,该字段填充 0。
Read more »

这两年来,WebRTC 越来越多地出现在人们的视野,在在线教育,在线医疗等领域的应用也越来越多。大家研究 WebRTC 的热情也越来越高涨,不过 WebRTC 的入门门槛个人觉得稍微有些高,特别是各种概念,比如 NAT 穿越,ICE,STUN,TURN,Signaling server 等等,刚开始可能会觉得比较繁杂,不易理解。然后建立连接的整个过程,异步调用比较多,很容易搞混。那么这篇文章里我们会根据 WebRTC 的官方 demo AppRTC 的 iOS 版本来分析一下 WebRTC 从进入房间到建立音视频连接的过程,为了便于了解,我们本次的讨论不涉及到底层的具体实现。

1. 相关概念

我们首先来简单地了解几个概念:

1.1 NAT 穿越(NAT Traversal)

因为 WebRTC 是 P2P 的,很多时候 peer 是隐藏在 NAT 之后的,没有外网的 IP 地址,如果两个 peer 都在 NAT 后面,都没有外网的 IP (或者说都不知道自己的外网 IP),是无法建立连接的。那么 NAT 穿越就是用来解决这个问题的,NAT 穿越也俗称 “P2P 打洞”。常见的两种穿越方式是 STUN 和 TURN。

Read more »

TURN server 可以解决点对点通信里的 NAT 穿透,并提供中继(relay) 的服务。coturn 是一个开源的 TURN 和 STUN 服务器,是基于 rfc5766-turn-server 进化过来的,目前比较成熟。之所以安装 coturn 是借助它提供的 TURN 服务,解决 WebRTC P2P 通信中 STUN 服务器解决不了的复杂内网的问题。比如我尝试过移动 4G 和 联通 WIFI 有时候无法通过 STUN 服务穿越内网连接,通过 TURN 服务的中继就可以解决这个问题。下面我们就来介绍一下 coturn 的安装,配置以及测试的过程。

Install coturn

coturn 安装可以参考官方的安装文档,按该文档安装即可。如果是 Ubuntu 系列,可以直接通过 apt 安装,安装完成后会放在 /usr/bin/turnserver

1
apt install coturn

网络配置

以阿里云服务器为例,配置网络安全组 / 防火墙,开放 3478 的 UDP/TCP 端口,这个我们我们会作为 TURN/STUN 服务器的监听端口,并开放 40000~60000 的 UDP 端口,STUN 和 TURN 可能会使用随机的 UDP 端口。

Read more »

这篇 blog 来自我们内部的分享,内容比较精简,需要更多的细节信息,请参考 WWDC 2016 - 401 的视频

相信每一个开发者在初学 iOS 的时候,都有过被 Code Signing 坑过的经历,特别是当旁边没有人指导的时候,这也是当时我个人学习 iOS 的时候最困扰的地方,证书,provisioning profile, code signing 等等这些和实际的开发无关的概念,现在还记得苦苦看文档的经历。

Image1 icon
iOS 证书申请和签名打包流程图,图来自这里

Read more »

我们知道 Swift 语言支持函数式编程范式,所以函数式编程的一些概念近来比较火。有一些相对于 OOP 来说不太一样的概念,比如 Applicative, Functor 以及今天的主题 Monad. 如果单纯的从字面上来看,很神秘,完全不知道其含义。中文翻译叫做单子,但是翻译过来之后对于这个词的理解并没有起到任何帮助。

我的理解很简单,Functor 是实现了 map 函数的容器,Monad 就是实现了 flatMap 方法的容器,比如在 Swift 里,Optional, CollectionType 等等都可以称为 Monad。

既然有了 map, flatMap 又有什么作用呢?两者有什么联系和区别呢?

map vs flatMap

map 和 flatMap 的共同点都是接受一个 transform 函数,把一个容器转换为另外一个容器。

下面主要从维度这一块来解释两者的区别,我们先来简单的定义一下维度

对于类型 T,如果类型 S 是一个容器,且元素的类型包含 T,那我们就说:
S (维度) = T (维度) + 1

举个🌰, [Int] (维度) = Int (维度) +1, Int? (维度) = Int (维度) + 1.

Read more »

花了几个小时把 Blog 平台引擎从 Jekyll 迁移到了 Hexo 上,换成 Hexo 的主要原因是 Jekyll 用起来总是出问题,比如提交到 GitHub 上生成的静态页速度很慢,以及一些莫名其妙的错误,和对 MarkDown 支持的比较差等等。

找了一圈综合对比,最终发现 Hexo 能完全满足我的需求,功能强大,插件和主题比较丰富,支持多种平台一键部署,比如 GitHub Pages, Heroku, AS3, 甚至直接 rsync, 有了之前的教训,我果断选择了通过 rsync 放在了自己闲置的一台 Digital Ocean 的 VPS 上。更吸引我的是 Hexo 是基于 NodeJS 的,我对 Node 和 EJS 模版引擎比较熟悉,有无法满足的需求时,就直接顺手添加上了,比如这次分页的方式,对友言的评论的支持,以及 UI 上的一些改动等等。

Read more »

今天科比正式退役,在未来的日子里,也许还会有像科比一样有天赋又努力的球员出现,但我们却不再有青春去追随了。
— 沃茨・其索特

1 什么是协变与逆变

刚开始看到协变 (Covariance) 和逆变 (Contravariance) 的时候,差点晕菜,反复查了一些资料,才稍有些自己的体会,难免有理解不对的地方,欢迎指出 :]

在计算机科学和类型的领域内来看,变化 (variance) 这个词指的是两个类型之间的关系是如何影响从它们衍生出的两种复杂类型之间的关系的。相对于原始类型,这两种复杂类型之间的关系只能是不变 (invariance),协变 (covariance) 和逆变 (contravariance) 之中的某一种。

这段比较拗口,我们一步一步拆解,既然上面提到了两个类型之间的关系,在主流的编程观念里,类型之间的关系中通常会包含子类型 (subtype) 和 父类型 (supertype)。

首先假设 Cat 是 Animal 的子类,就是说 Cat 是 Animal 的 subtype,可以看作上面的 “原始类型”,然后有两个衍生出来的 List<Cat>List<Animal> 类型,就是从 Cat 和 Animal 衍生出来的两种复杂类型。

那么我们就可以这么来解释协变和逆变了:

  • 协变:如果说 List<Cat> 也是 List<Animal> 的 subtype,也就是衍生类型的关系和原来类型( Cat 与 Animal)的关系是一致的,那我们就说 List 是和它的原来类型协变(共同变化)的。
  • 逆变:如果说 List<Cat>List<Animal>supertype,也就是衍生类型的关系和原来类型( Cat 与 Animal)的关系是相反的,那我们就说 List 是和它的原来类型逆变(反变)的。
  • 不变:如果说 List<Cat> 既不是 List<Animal> 的 subtype,也不是 supertype,也就是说没有关系,则说是不变的。
Read more »

介绍

FLEXLoader 是一个我在上周末写的一个可以动态加载 FLEX 的开源越狱插件,它以加载动态库的方式注入到系统 App 和用户的 App 中 (欢迎使用 star, fork, clone 等一切方法蹂躏我~~)。FLEX 全称是”Flipboard Explorer”,是 Flipboard 团队开发一组调试和探测 App 的开源工具,功能非常强大,比如查看和修改 View 的层级结构,查看和修改堆内存中的对象信息等等,更多 FLEX 介绍和使用信息参考这里

FLEXLoader 参考了 RevealLoader,顾名思义,它是一个加载 Reveal 动态库的越狱插件,是一款非常方便的插件,如果你经常用 Reveal 来查看和调试,一定不要错过。我把它的源码做了一些修改,把 Reveal 的动态库改成了 FLEX 的动态库,因为 FLEX 官方只提供了源代码,所以我参考了 Tony 的这篇文章编译了一个动态库,如有有兴趣,也可以直接用我已经构建好的 Xcode 工程 FLEXDynamicLibProject 来编译。

安装 FLEXLoader

有下面两种安装方式:

  1. 在 Cydia 中搜索 Flipboard FLEX loader 并安装 (BigBoss 源)
  2. 如果安装有越狱的开发环境,比如 theos,可以自己来编译安装,配好环境变量后,make package install 一下 (也可以自己编译 FLEX 的动态库替换掉工程中的 FLEXDylib.dylib).
Read more »

用 C 实现一个 assert(),通常是这么做的:

1
2
3
4
5
6
7
8
#ifdef NDEBUG
#define assert(e) ((void)0)
#else
#define assert(e) \
((void) ((e) ? ((void)0) : __assert (#e, __FILE__, __LINE__)))
#define __assert(e, file, line) \
((void)printf ("%s:%u: failed assertion `%s'\n", file, line, e), abort())
#endif

assert 就是断言,这里采用条件编译,作用是如果在调试情况下,检查参数 e,如果是 false,就给出错误提示并终止程序执行,如果是非 DEBUG 情况下,就什么都不做。这种宏实现的方式是没有运行时性能影响的,因为我们知道宏展开基本是直接替换的,没有对表达式求值的过程。

比如这样简单的一个宏,用来返回两个数中的较大值:

1
#define MAX(A,B) (A >= B ? A : B)

当我们使用的时候,比如 MAX(10, 20), 宏展开后的结果是 (10 >= 20 ? 10 : 20), 而不是计算到最终的结果 20. 但是在方法调用中,参数值是直接求值的,比如我们有个判断一个数是否偶数的函数:

1
2
3
func isEven(num : Int) -> Bool {
return num % 2 == 0;
}

当我们调用 isEven(10 + 20) 的时候,先计算 10 + 20 的结果,然后把 30 作为参数传递到 isEven 函数中。

OK. 在 Swift 里也实现了这样一个功能的 assert () 函数,而且没有用到宏 (你骗人,明明用到了啊?!, 就是#if !NDEBUG 啊。 好吧,相信苹果 Swift 官方 Blog 在下一篇文章中应该会有相应的机制来判断当前的环境的,这里的意思是没用宏来实现表达式的延迟求值。),是怎么实现的呢?

首先在 Swift 里没有办法写一个函数,它接受一个表达式作为参数,但是却不执行它。比如,我们想这么实现:

1
2
3
4
5
6
func assert(x : Bool) {
#if !NDEBUG

/*noop*/
#endif
}

然后这么用:

1
assert(someExpensiveComputation() != 42)

我们发现,总是要计算一遍表达式 someExpensiveComputation() != 42 的值,是真是假,然后把这个值传递到 assert 函数中。即便我们在非 Debug 的情况下编译也是一样,那怎么样条件执行呢,像上面的使用宏的方式,当条件满足的时候才对表达式求值?还是有办法的,就是修改这个方法,把参数类型改为一个闭包,像这样:

1
2
3
4
5
6
7
func assert(predicate : () -> Bool) {
#if !NDEBUG
if predicate() {
abort()
}
#endif
}

然后调用的时候创建一个匿名闭包,然后传给 assert 函数:

1
assert({ someExpensiveComputation() != 42 })

这样当我们禁用 assert 的时候,表达式 someExpensiveComputation() != 42 就不会被计算,减少了性能上的消耗,但是显而易见,调用的代码就显的不那么清爽优雅了。

于是乎 Swift 引入了一个新的 @auto_closure 属性,它可以用在函数的里标记一个参数,然后这个参数会先被隐式的包装为一个 closure,再把 closure 作为参数给这个函数。好绕啊,直接看代码吧,使用 @auto_closure, 上面的 assert 函数可以改为:

1
2
3
4
5
6
7
func myassert(predicate : @auto_closure () -> Bool) {
#if !NDEBUG
if predicate() {
abort()
}
#endif
}

然后我们就可以这么调用了:

1
assert(someExpensiveComputation() != 42)

哇。好神奇!

仔细看一下 myassert () 函数的参数:

1
predicate : @auto_closure () -> Bool

predicate 加上了 @auto_closure 的属性,后面是个 closure 类型 () -> Bool。其实 predicate 还是 () -> Bool 类型的,只是在调用者可以传递一个普通的值为 Bool 表达式,,然后 RunTime 会自动把这个表达式包装为一个 () -> Bool 类型的闭包作为参数传给 myassert () 函数,简而言之就是中间多了一个由表达式到闭包的自动转换过程。

@auto_closure 的功能非常强大和实用,有了它,我们就可以根据具体条件来对一个表达式求值,甚至多次求值。在 Swift 的其他地方也有 @auto_closure 的身影,比如实现短路逻辑操作符时,下面是 && 操作符的实现:

1
2
3
func &&(lhs: LogicValue, rhs: @auto_closure () -> LogicValue) -> Bool {
return lhs.getLogicValue() ? rhs().getLogicValue() : false
}

如果 lhs 已经是 false 了,rhs 也就没有必要计算了,因为整个表达式肯定为 false。这里使用 @auto_closure 就轻松实现了这个功能。

最后,正如宏在 C 中的地位一样,@auto_closure 的功能也是非常强大的,但同样应该小心使用,因为调用者并不知道参数的计算被影响 (推迟) 了。@auto_closure 故意限制 closure 不能有任何参数(比如上面的 () -> Bool),这样我们就不会把它用于控制流中。

编译自 Swift 的官方 Blog Building assert() in Swift, Part 1: Lazy Evaluation 一文

Swift 语言使用 var 定义变量,但和别的语言不同,Swift 里不会自动给变量赋初始值,也就是说变量不会有默认值,所以要求使用变量之前必须要对其初始化。如果在使用变量之前不进行初始化就会报错:

1
2
3
4
5
var stringValue : String
//error: variable 'stringValue' used before being initialized
//let hashValue = stringValue.hashValue
// ^
let hashValue = stringValue.hashValue

上面了解到的是普通值,接下来 Optional 值要上场了。经喵神提醒,Optional 其实是个 enum,里面有 NoneSome 两种类型。其实所谓的 nil 就是 Optional.None, 非 nil 就是 Optional.Some, 然后会通过 Some(T) 包装(wrap)原始值,这也是为什么在使用 Optional 的时候要拆包(从 enum 里取出来原始值)的原因,也是 PlayGround 会把 Optional 值显示为类似 {Some "hello world"} 的原因,这里是 enum Optional 的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
enum Optional<T> : LogicValue, Reflectable {
case None
case Some(T)
init()
init(_ some: T)

/// Allow use in a Boolean context.
func getLogicValue() -> Bool

/// Haskell's fmap, which was mis-named
func map<U>(f: (T) -> U) -> U?
func getMirror() -> Mirror
}

声明为 Optional 只需要在类型后面紧跟一个 ? 即可。如:

1
2
var strValue: String?   //?相当于下面这种写法的语法糖
var strValue: Optional<String>

上面这个 Optional 的声明,意思不是” 我声明了一个 Optional 的 String 值”, 而是” 我声明了一个 Optional 类型值,它可能包含一个 String 值,也可能什么都不包含”,也就是说实际上我们声明的是 Optional 类型,而不是声明了一个 String 类型,这一点需要铭记在心。

一旦声明为 Optional 的,如果不显式的赋值就会有个默认值 nil。判断一个 Optional 的值是否有值,可以用 if 来判断:

1
2
3
if strValue {
//do sth with strValue
}

然后怎么使用 Optional 值呢?文档中也有提到说,在使用 Optional 值的时候需要在具体的操作,比如调用方法、属性、下标索引等前面需要加上一个 ?,如果是 nil 值,也就是 Optional.None,会跳过后面的操作不执行,如果有值,就是 Optional.Some,可能就会拆包 (unwrap),然后对拆包后的值执行后面的操作,来保证执行这个操作的安全性,比如:

1
let hashValue = strValue?.hashValue

strValue 是 Optional 的字符串,如果 strValue 是 nil,则 hashValue 也为 nil,如果 strValue 不为 nil,hashValue 就是 strValue 字符串的哈希值 (其实也是用 Optional wrap 后的值)

另外,? 还可以用在安全地调用 protocol 类型方法上,比如:

1
2
3
4
5
6
7
8
9
10
11

@objc protocol Downloadable {
@optional func download(toPath: String) -> Bool;
}

@objc class Content: Downloadable {
//download method not be implemented
}

var delegate: Downloadable = Downloadable()
delegate.download?("some path")

因为上面的 delegate 是 Downloadable 类型的,它的 download 方法是 optional,所以它的具体实现有没有 download 方法是不确定的。Swift 提供了一种在参数括号前加上一个 ? 的方式来安全地调用 protocol 的 optional 方法。

另外如果你需要像下面这样向下转型 (Downcast),可能会用到 as?

1
2
3
if let dataSource = object as? UITableViewDataSource {
let rowsInFirstSection = dataSource.tableView(tableView, numberOfRowsInSection: 0)
}

到这里我们看到了 ? 的几种使用场景:

  1. 声明 Optional 值变量
  2. 用在对 Optional 值操作中,用来判断是否能响应后面的操作
  3. 用于安全调用 protocol 的 optional 方法
  4. 使用 as? 向下转型 (Downcast)

另外,对于 Optional 值,不能直接进行操作,否则会报错:

1
2
3
4
5
//error: 'String?' does not have a member named 'hashValue'
//let hashValue = strValue.hashValue
// ^ ~~~~~~~~~

let hashValue = strValue.hashValue

上面提到 Optional 值需要拆包 (unwrap) 后才能得到原来值,然后才能对其操作,那怎么来拆包呢?拆包提到了几种方法,一种是 Optional Binding, 比如:

1
2
3
if let str = strValue {
let hashValue = str.hashValue
}

还有一种是在具体的操作前添加 ! 符号,好吧,这又是什么诡异的语法?!

直接上例子,strValue 是 Optional 的 String:

1
let hashValue = strValue!.hashValue

这里的 ! 表示 “我确定这里的的 strValue 一定是非 nil 的,尽情调用吧” ,比如这种情况:

1
2
3
if strValue {
let hashValue = strValue!.hashValue
}

{} 里的 strValue 一定是非 nil 的,所以就能直接加上!,强制拆包 (unwrap) 并执行后面的操作。
当然如果不加判断,strValue 不小心为 nil 的话,就会出错,crash 掉。

考虑下这一种情况,我们有一个自定义的 MyViewController 类,类中有一个属性是 myLabel,myLabel 是在 viewDidLoad 中进行初始化。因为是在 viewDidLoad 中初始化,所以不能直接声明为普通值:var myLabel : UILabel,因为非 Optional 的变量必须在声明时或者构造器中进行初始化,但我们是想在 viewDidLoad 中初始化,所以就只能声明为 Optional:var myLabel: UILabel?, 虽然我们确定在 viewDidLoad 中会初始化,并且在 ViewController 的生命周期内不会置为 nil,但是在对 myLabel 操作时,每次依然要加上 ! 来强制拆包 (在读取值的时候,也可以用 ?,谢谢 iPresent 在回复中提醒),比如:

1
2
3
myLabel!.text = "text"
myLabel!.frame = CGRectMake(0, 0, 10, 10)
...

对于这种类型的值,我们可以直接这么声明:var myLabel: UILabel!, 果然是高 (hao) 大 (gui) 上 (yi) 的语法!,这种是特殊的 Optional,称为 Implicitly Unwrapped Optionals, 直译就是隐式拆包的 Optional,就等于说你每次对这种类型的值操作时,都会自动在操作前补上一个 ! 进行拆包,然后在执行后面的操作,当然如果该值是 nil,也一样会报错 crash 掉。

1
2
var myLabel: UILabel!  //!相当于下面这种写法的语法糖
var myLabel: ImplicitlyUnwrappedOptional<UILabel>

那么 ! 大概也有两种使用场景

  1. 强制对 Optional 值进行拆包 (unwrap)
  2. 声明 Implicitly Unwrapped Optionals 值,一般用于类中的属性

Swift 是门新生的语言,我们有幸见证了它的诞生,激动之余也在佩服苹果大刀阔斧的推出一个新的语言替代一个已经比较成熟语言的魄力,今天在知乎日报上看到一个回答是说 Swift 是一门玩具语言,正当想去吐槽,发现回答已经被删除了。个人认为苹果是很认真的推出 Swift 的,从 Swift 的各种细微的设计也能看的出来。

另外这两个小符号就花费了我不少的时间来理解,可能依然会有错误和不妥之处,欢迎大家指正,本文旨在抛砖引玉。除此之外,Swift 还有很多很棒的特性,WWDC 2014 会有四五个和 Swift 语言相关的 Video,大家也可以去关注一下。

最后要感谢喵神的纠正了多处有问题的地方,thx, have fun!

REF

  1. The Swift Programming Language
  2. Understanding Optionals in Swift
0%