WWDC 2016 - Session 401 - What's New in Xcode App Signing 笔记

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

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

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

Xcode 团队在 Xcode 8 中移除了 fix issue 之后还需要 fix issue 但是可能还是不能 fix issue 的 Fix Issue 按钮,并完全重新设计了 code signing 的交互,流程和架构,不管对于 iOS 初学者还是有经验的程序员,都能大大简化 code signing 的流程,让我们把精力更专注于实际的业务开发上。

Xcode 8 支持两种签名方式,自动化签名 (Automatic Singing)和自定义签名(Customized Signing) 的。下面我们说一下基础概念和这两种签名方式。

1. 基础概念 (Fundamentals)

  1. 证书 (Certificate) 在 Xcode 8 之前,每个账号一般有两个证书,一个是开发证书,一个是发布证书。开发证书和发布证书都只能存在一份,所以如果有多台 Mac 开发设备,需要通过证书的导出导入来同步证书(和密钥)。在 Xcode 8 之后,支持多个 开发证书 (发布证书依然只能有一个),也就是说,多台 mac 开发设备可以 自动 生成多份有效的开发证书(和密钥),就不再需要导出导入了。(这里有雷鸣般的掌声)

  2. Provisioning profile 用来授权给包含在 profile 里的 iOS 开发设备来安装 app。在 Xcode 8 之前,每次添加新的设备都会生成新一个新的 profile,并产生一个唯一的 id,所以在每次添加设备之后,因为 profile id 变了,需要更新并提交 project 文件,Xcode 8 以后是用文件名的方式引用,就是说添加了新的设备,只要 profile 文件名不变,就不会修改 project 文件了。这也算是一个比较方便的改进吧。(掌声)

  3. Entitlement 其实就是管理我们开启的 Capabilities,比如 IAP,Push Notifications,iCloud 等等

  4. 签名的流程 首先 Xcode 根据所择的 team 从 key chains 里选择 最新的 证书,然后根据 app identifier 选择 最新的 provisioning profile,在 build 的时候 profile 会被放在 app 包里,code sign 工具根据证书生成一个 code seal(可以理解为盖一个戳)。如果有人篡改了 app,这个戳就不 match 了,iOS 系统会阻止 app 安装。

想了解更多?代码签名探析 @objc.io

2. 自动化签名 (Automatic Singing)

在这种模式下,Xcode 全自动的为我们管理整个签名的流程,整个过程会在后台执行,会保证所有签名需要的文件是最新的。

Image1 icon

我们所需要做的就是勾选上自动化签名,然后选择 team。剩下的 Xcode 都会接管。比如创建证书,创建和更新 profile 等等。但是当插入了一台新的 iOS 设备,Xcode 8 还是会提示是否把这台设备添加到测试设备中,如果选择是,Xcode 8 会自动添加到设备列表里,并自动更新 profile 文件。(鼓掌)

如果比较好奇 Xcode 自动为我们做了什么,可以在 Reports 里看查看 log, (鼓掌) 比如:
Image1 icon

Xcode 自动化签名只会自动化开发阶段的签名,不会修改发布的签名设置。既然这样,如何设置 release 版本的签名呢?其实我们在 Archive 的时候,Xcode 默认使用的还是开发证书做的签名,然后在 Orgnizer 里选择 export 到 App Store 发布版本的时候,会让我们重新选择 证书重新签名,这里再选择发布证书(演讲者这里说的是开发证书,应该是口误)。

3. 自定义签名(Customized Signing)

如果我们想自己管理签名所需的文件,可以选择自定义签名方式。这种模式下,Xcode 不会对签名设置做任何的修改。

操作很简单,就是取消勾选自动化签名,然后就可以对每个 build configuration 做不同的签名设置了,注意不用去 Build Setting 里设置了,直接 General 里就可以完成签名的设置了。如下图,对免费版和收费版设置不同的 profile:
Image1 icon

虽然我们设置了自定义签名,但 Xcode 并不是真的什么都不做了,相反如果签名的设置有问题, Xcode 提供更多友好和精确的提示:
Image1 icon
Image1 icon

4. 最佳实践 (Best Practices)

blah blah...
一句话:使用自动化签名 (to make your life as easy as possible)
blah blah...

Enjoy!

揭开 Monad 的神秘面纱

我们知道 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.

map 和 flatMap 的区别是,对于 map,容器里的一个元素经过 transform 后只产生一个元素,是 one-to-one 的关系,也就是说经过转换后,纬度是不变的。比如:

1
2
3
4
5
var intArray: [Int] = [1, 2, 3, 4, 5]
var stringArray: [Int] = intArray.map { (value: Int) -> String in
return "\(value)"
}
//stringArray: ["1", "2", "3", "4", "5"]

这个 transform 函数的是 Int -> Int 的,两边的维度是一致的。

对于 flatmap,容器里的一个元素经过 transform 可能转换成 0 个,1 个 或者多个元素,也就是 one-to-any 的关系,既然是 any 的关系,就需要一个容器来存放 any 个元素,所以经过 transform 的返回值通常是一个容器,所以 transform 函数执行之后,相当于维度+1。

1
2
3
4
var oddIntArray: [Int] = intArray.flatMap { (value: Int) -> Int? in
return value % 2 == 1 ? value : nil
}
//oddIntArray: [1, 3, 5]

这里的 transform 是 Int -> Int? 的,我们知道 Int? 是 Int 的包装类型,所以说 transform 相当于对每个元素都包了一层,提升了一个维度.

但是我们看一下上面例子里 stringArray 和 oddIntArray 的类型,都是 [Int],也就是说 flatMap 函数对 transform 函数的返回值做了降维处理。那么 flat 的意思在这里也就知道了,就是把 transform 返回的容器 降维攻击(拍扁),拿出里面的元素。

flatMap 函数为什么要这么做呢?在函数式编程中,通常会对一个值 / 操作进行链式操作,为了保证后面还可以继续方便的进行链式操作,一般需要保持维度不变。其实可以看作一个约定,大家都遵循一定的规则,才都有得玩。

如何确定使用 map or flatMap 的时机?

从上面可以看到 map 对 transform 的返回值没有做特殊的处理,flatMap 对于 transform 的返回值会做降维处理,比如 unwrap optional 值等。

其实可以反推,如果给定的 transform 函数会对调用者容器里的每个元素做升维,那我们需要用 flatMap 对它的结果进行降维,来保证调用 flatMap 前后维度保持一致。如果说 transform 调用前后维度没有变化,使用 map 方法就行了。

Swift 中的 map 和 flatMap 方法

首先看看 Optional<Wrapped> 的 map 和 flatMap 方法:

1
2
3
4
5
6
/// If `self == nil`, returns `nil`.  Otherwise, returns `f(self!)`.
@warn_unused_result
public func map<U>(@noescape f: (Wrapped) throws -> U) rethrows -> U?
/// Returns `nil` if `self` is `nil`, `f(self!)` otherwise.
@warn_unused_result
public func flatMap<U>(@noescape f: (Wrapped) throws -> U?) rethrows -> U?

map 的 transform 是 Wrapped -> U 维度不变, flatMap 的 transform 方法是 Wrapped -> U?,维度 +1。因为 Optional 的特殊性,flatMap 提供了 one-to-zero/one 的关系。

继续看看 CollectionType:

1
2
3
4
5
public func map<T>(@noescape transform: (Self.Generator.Element) throws -> T) rethrows -> [T]

public func flatMap<T>(@noescape transform: (Self.Generator.Element) throws -> T?) rethrows -> [T]

public func flatMap<S : SequenceType>(transform: (Self.Generator.Element) throws -> S) rethrows -> [S.Generator.Element]

有一个 map 函数和两个 flatMap, map 的 transform 函数是 Element -> T 维度不变,两个 flatMap 的 transform 函数分别是 Element -> T?(one-to-zero/one) 和 Element -> S: SequenceType, SequenceType 是个集合,相当于 one-to-any,这两个 transform 维度都升了一级。

特别感谢我的同事 王轲, 本文的很多思路都得益于和他的讨论。

把 Blog 从 Jekyll 迁移到 Hexo

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

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

<!-- more -->

主题比较多,试了几个,发现还是比较喜欢简洁的风格,太花里胡哨不容易把精力集中到阅读本身上。基于这个初衷,选择了 Hacker 主题,稍微改了一点点样式。插件比较喜欢的是 hexo-filter-auto-spacing,可以自动在中日韩文和西文之间补上空格,懒癌发作的时候就可以省去敲空格的麻烦了。

如果你厌倦了 Jekyll 或者 Octopress,个人推荐可以尝试一下 Hexo.

Swift 之类型的协变与逆变

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

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,也就是说没有关系,则说是不变的。

2 为什么要了解协变与逆变?

我们知道 subtype 是可以替换 supertype 的,反之则不行,比如说:

1
2
let animal: Animal = Cat();  //Right
let cat:Cat = Animal(); //Wrong

来看不同返回值类型的函数替换:

1
2
3
4
5
func animalF() -> Animal { return Animal() }
func catF() -> Cat { return Cat() }

let returnsAnimal: () -> Animal = catF //Right
let returnsCat: () -> Cat = animalF //Wrong

第一个赋值语句通过编译是正确的 () -> Cat 和 () -> Animal 的关系与 Cat 和 Animal 之间的关系一致,也就是说是在 Swift 中函数的返回值是协变的。

再看看不同参数的函数的变化:

1
2
3
4
5
func printCat(cat: Cat) -> Void { print("\(cat)") }
func printAnimal(animal: Animal) -> Void { print("\(animal)") }

let logCat: Cat -> Void = printAnimal //Right
let logAnimal: Animal -> Void = printCat //Wrong

我们先不运行这段代码,从 caller 角度思考一下两个赋值语句可能的结果,假设我们要调用 logCat(Cat()) ,实际会执行 printAnimal: Animal -> Void 函数,printAnimal 是能接受 Cat 类型的参数的,运行应该没有问题。

然后如果调用 logAnimal(Animal()),实际会运行 printCat: Cat -> Void 函数,但是我们发现 printCat 理论上无法接受一个 Animal 的对象,因为它是 Cat 的父类.

我们可以看到函数 Animal -> Void 可以替换 Cat -> Void,反之行不通,也就是说 Animal -> Void 是 Cat -> Void 的 subtype,和 Animal 是 Cat 的关系是 supertype 是相反的!也就是说函数的参数是逆变的。

得到的结论是: 函数的参数是逆变的,返回值是协变的。 我们知道了变化的规则,就能判断出类型的关系,就可以知道一个类型是否可以替换另外一个类型。

思考下面这些 testCatAnimal 函数调用那些是正确的,如果把 testCatAnimal 换成 testAnimalCat 呢?

1
2
3
4
5
6
7
8
9
10
11
func testCatAnimal(f: (Cat -> Animal)) { print("cat -> animal") }

func catAnimal(cat: Cat) -> Animal { return Animal();}
func catCat(cat: Cat) -> Cat { return Cat(); }
func AnimalCat(animal: Animal) -> Cat { return Cat(); }
func AnimalAnimal(animal: Animal) -> Animal { return Animal(); }

testCatAnimal(catAnimal)
testCatAnimal(catCat)
testCatAnimal(AnimalCat)
testCatAnimal(AnimalAnimal)

3. 其他类型的协变和逆变

上面我们提到了函数的参数和返回值的分别是逆变和协变,在 Swift 中除了函数,还有属性 (property),范型(Generic) 等。

对于属性来说,如果是 readonly 的,属性是协变的,子类如果要覆盖,必须是父类属性的 subtype。如果是 readwrite 的,属性是不变的,子类必须和父类的属性类型完全一致。

对于范型来说,范型本身其实没有特殊的变化,它的变化与范型使用的环境紧密相关,如果是用作函数的返回值或者覆盖父类的 readonly 属性,它的协变的,如果用做函数的参数,它是逆变的,如果是用做覆盖父类的 readwrite 的属性,或者同时用做函数的返回值和参数,那它必须是不变的,也就是说范型类型必须和要求完全一致,不能使用 subtype 或者 supertype.

Reference

  1. Swift 2.1 Function Types Conversion: Covariance and Contravariance

  2. Friday Q&A 2015-11-20: Covariance and Contravariance

动态加载 FLEX 的越狱插件 - FLEXLoader

介绍

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).

使用方法

安装后,打开“设置”-> "FLEXLoader"->“Enabled Applications”, 勾选上你想要注入 FLEX 的 App,打开 App 就能看到 FLEX 的身影了,简直不能再简单了,:]

后记

写完这个 tweak 后,不敢也不能独享,心怀忐忑地放到了 GitHub 上,然后就打算放到 Cydia 上。Cydia 的诸多源中,感觉 BigBoss 最值得信赖一点,所以就打算传到 BigBoss 上,后来证明这个选择是非常正确的。从搜索 BigBoss 的网址,到填写表单上传完成,前后不到 10 分钟,甚至都没要求我注册,这个体验还是蛮爽的。

BigBoss 承诺 24 小时之后会处理,到了第二天,BigBoss 的审核员 @0ptimo 就给我发邮件,说 tweak 被拒掉了,原因是我没有把 FLEX 的 license 加上,这个确实是我疏忽了,我把 RevealLoader 的 license 加上却忘了 FLEX 的,于是就速度加上,然后名字和现有的一个叫 Flex 比较相似,建议我改一下名字,还有一些细节比如 icon 的名字直接叫 icon.png 容易被别人覆盖掉,动态库的位置放到 /Library/Application Support/FLEXLoader 比较好等等。我表示了感谢,然后都一一修改之后提交,过了不到一天就通过审核了。

如果你有好的想法或者问题,欢迎 PR 或者联系我. 最后感谢下面 REF 中的各位开源项目和文章的作者,他们才是创造者,我只是开源代码的组装工~~

REF

欢迎小伙伴在 微博 上关注我, :],Enjoy!

Swift 之 @auto_closure

用 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 之 ? 和 !

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

Run loop 和 Thread

Run-loop 是什么?

首先考虑这个问题:你的 Cocoa 程序大部分的时间什么都没做,更具体点,是在等待输入。然而,一旦你触摸屏幕,相应的事件被触发,就可能会执行你的一段事件处理代码。同理,socket 中返回一些数据,或者计时器触发等也是一样的情况。而且更重要的是,一旦触发事件的代码执行完,程序就会回到等待状态。在很多情况下,代码执行的时间要远小于程序等待输入的时间。

我认为 run loop 就是较好的利用了这个事实的一种机制。一个 run loop 就是跑在单个线程上进行事件处理的循环。你在 run loop 上注册输入源,并指定当这些源有输入时应该执行的代码。当特定的源上有输入时,run loop 就会执行对应的代码,然后继续等待下一个输入事件。如果在 run loop 正在执行处理代码时,另外一个源的输入到了,run loop 会在执行完正当前的处理后处理这个输入事件。好处是虽然你不知道具体的输入顺序,但你知道它们最终会一个接一个地被串行处理。这就是说你不会遇到多线程的问题,这也是 run loop 非常有用的原因。

和线程的关系?

每个线程,包括应用的主线程都有一个相关联的 run loop 对象,在应用中你不需要显式的创建 run loop 对象。在 Carbon 和 Cocoa 应用中,主线程会自动设置并运行它的 run loop,这个过程也是应用启动过程的一部分。

Run loop 的使用

默认情况下,iPhone 上的所有触摸事件都会被 main run loop 放在队列里等待处理,所以你不需要对 UI 组件做额外的事情,而其他输入源需要一些额外的编码。比如在 run loop 上 schedule 一个 NSInputStream,你需要像下面这样:

1
2
[iStream setDelegate:self];
[iStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

在上面的代码中,一旦 iStream 有输入数据,就会执行 selfstream:handleEvent的方法。而且这个 stream 可以是任意类型的输入源,包括 socket.

另外,timer 对象也可以被 schedule 在 run loop 上,比如:

1
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(doStuff) userInfo: nil repeats:YES];

上面的代码把计时器 schedule 到当前的 run loop 上,每 2 秒就会调用 selfdoStuff方法。

不适用 run loop 的情况

那什么时候不适合使用 run loop 呢?根据 run loop 的特点,输入事件会一个接一个的被串行处理,那么如果一个事件的处理需要的时间特别长的话,就会导致在这个事件处理完之前,app 无法响应别的输入事件。在这种情况下,新开一个线程处理更合适。 然而,大部分情况下,我们的代码处理屏幕、socket 或者计时器事件都非常快,这时使用 main run loop 处理起来更简单,也更安全。

编译自Run-loops vs. Threads in Cocoa

配图来自苹果官方文档Run Loops

使用 Theos 做一个简单的 Mobile Substrate Tweak

Mobile Substrate 和 Theos

Mobile Substrate是 Cydia 的作者 Jay Freeman (@saurik)的另外一个牛 X 的作品,也叫 Cydia Substrate, 它的主要功能是 hook 某个 App,修改代码比如替换其中方法的实现,Cydia 上的 tweak 都是基于 Mobile Substrate 实现的。目前支持 iOS 和 Android 平台。

根据 github 上的介绍,theos是一个跨平台 iPhone Makefile 系统。它的主要功能是生成 iPhone 越狱 App、tweak 等程序的框架结构,并提供 makefile 来编译、打包和安装。

需要的准备工作:

#Mac

  • 安装 Theos,从 Theos 的 GitHub 上 clone 下来一份,放到某个目录下,这里我放到了 /opt/ 下。
  • 安装 Xcode Command Line Tools,可以在命令行下执行 xcode-select --install 来安装或者参考 SO 来安装,安装完之后再进行下一步。
  • 安装 dpkg ,首先安装 MacPorts,可以通过它的 官网 , 根据自己的系统版本来选择。安装好之后,重启 Terminal,执行port version,显示出版本号说明安装成功。如果提示command not found,尝试在/etc/paths 文件中加入下面两个路径:/opt/local/bin /opt/local/sbin,需要使用 root 权限来编辑,比如用 Vim 的话:sudo vi /etc/paths. 重启 Terminal,再次输入 port version 就应该会显示版本号了,然后执行 sudo port selfupdate 来更新一下, 之后执行 sudo port install dpkg 来安装 dpkg. 安装 dpkg 的目的是把我们写的 tweak 打成 deb 包。

#JailBreaked iPhone iOS 5/6

  • 安装 OpenSSH,打开 Cydia 的主界面就能看到 OpenSSH Access How-To 以及Root Password How-To 的选项,可以按照它的提示一步一步安装,这里不赘述了,需要提醒的是一定要改掉 root 的密码,防止别人通过 SSH 连接到你的手机。这一步是为了后面我们通过 SSH 连接到手机,把 deb 包安装到手机上准备的。
    iOS7 上的 Mobile Substrate 还有 bug,32 位的系统下每次重启后需要重新安装 Mobile Substrate 才能正常使用, 64 位今天貌似才能用。推荐暂时在 iOS5/6 的机器上测试[2014-01-01]。
  • apt. 在 cydia 中搜索 Apt 检查是否已经安装,没有安装就安装一下。
  • ldid. 全名是 Link Identify Editor, 也直接可以在 Cydia 中搜索全名安装。

创建 Tweak 并安装到手机上

首先我在桌面上创建一 mytweaks 的文件夹,保存我们要创建的 tweak 程序。

1
2
3
➜  ~        cd ~/Desktop
➜ Desktop mkdir mytweaks
➜ Desktop cd mytweaks

然后执行我们刚才的获得的 theos 来生成一个 tweak 的模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
➜  mytweaks  /opt/theos/bin/nic.pl
NIC 2.0 - New Instance Creator
------------------------------
[1.] iphone/application
[2.] iphone/library
[3.] iphone/preference_bundle
[4.] iphone/tool
[5.] iphone/tweak
Choose a Template (required): 5
Project Name (required): FirstTweak
Package Name [com.yourcompany.firsttweak]: com.joeyio.firsttweak
Author/Maintainer Name [Joey]:
[iphone/tweak] MobileSubstrate Bundle filter [com.apple.springboard]:
[iphone/tweak] List of applications to terminate upon installation (space-separated, '-' for none) [SpringBoard]:
Instantiating iphone/tweak in firsttweak/...
Done.
在创建模板的时候,我们选择 5,创建一个 iPhone 的 tweak. 其他 4 个选项可以自己去搜索下。名字输入 FirstTweak,包名我输入 com.joeyio.firsttweak,下面的三个选项都直接回车使用缺省值。
MobileSubstrate Bundle filter这一项表示要 hook 的程序,默认是com.apple.springboard,就是 hook Spring Board,如果你想 hook 别的 App,这里改成那个 App 的 BundleID.

OK,那么我们的第一个 tweak 就创建好了,好像一点也不难啊。进入到 firsttweak 目录下,使用 make 编译一下,可能结果是这样的:

1
2
3
4
5
6
7
8
9
10
➜  firsttweak  make
/Users/qiaoxueshi/Desktop/mytweaks/firsttweak/theos/makefiles/targets/Darwin/iphone.mk:41: Deploying to iOS 3.0 while building for 6.0 will generate armv7-only binaries.
Making all for tweak FirstTweak...
Preprocessing Tweak.xm...
Name "Data::Dumper::Purity" used only once: possible typo at /Users/qiaoxueshi/Desktop/mytweaks/firsttweak/theos/bin/logos.pl line 615.
Compiling Tweak.xm...
Linking tweak FirstTweak...
Stripping FirstTweak...
Signing FirstTweak...
/bin/sh: ldid: command not found
我们看到里面有 2 个警告,第一个我没有搜索到什么结果,第二个是只要手机上安装 ldid 就行了, 这里不用管它。我自己试了一下,是可以安装到手机上的,可以暂时忽略,如果哪位小伙伴知道什么原因,欢迎告知。

在部署到手机之前确认手机和电脑在一个 wifi 环境下,并且可以通过 SSH 连接到手机,方法是在 Terminal 下,通过 SSH 连接到手机,之后会提示你输入 root 密码(上面安装 SSH 步骤中有提到),确保连接成功再往下进行。手机的 IP 地址可以在 wifi 设置中看到。

1
ssh root@手机 IP 地址
然后把手机 IP 地址放在 THEOS_DEVICE_IP 环境变量中,这样 theos 才知道安装到哪里,如下:
1
export THEOS_DEVICE_IP= 手机 IP 地址
然后执行 make package install 打包并安装到手机上 (如果 Cydia 在前台,把它退到后台,否则安装会失败):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
➜  firsttweak  make package install
/Users/qiaoxueshi/Desktop/mytweaks/firsttweak/theos/makefiles/targets/Darwin/iphone.mk:41: Deploying to iOS 3.0 while building for 6.0 will generate armv7-only binaries.
Making all for tweak FirstTweak...
make[2]: Nothing to be done for `internal-library-compile'.
Making stage for tweak FirstTweak...
dpkg-deb:正在新建软件包“com.joeyio.firsttweak”,包文件为“./com.joeyio.firsttweak_0.0.1-2_iphoneos-arm.deb”。
install.exec"cat > /tmp/_theos_install.deb; dpkg -i /tmp/_theos_install.deb && rm /tmp/_theos_install.deb"<"./com.joeyio.firsttweak_0.0.1-2_iphoneos-arm.deb"
root@192.168.199.126's password:

Selecting previously deselected package com.joeyio.firsttweak.
(Reading database ... 6250 files and directories currently installed.)
Unpacking com.joeyio.firsttweak (from /tmp/_theos_install.deb) ...
Setting up com.joeyio.firsttweak (0.0.1-2) ...
install.exec "killall -9 SpringBoard"
root@192.168.199.126's password:

安装过程中需要输入两次手机 Root 密码,一次是为了把打包后的 deb 程序文件传到手机上,另外一次是 kill 掉 SpringBoard,使 SpringBoard 重启。

完成后在 Cydia 里的“变更”里,往下翻一翻,就能看到一个名字为“FirstTweak”的插件了了,想想接下来出任 CEO,迎娶白富美,走向人生巅峰,有木有一点小激动?

完成一个小功能

到目前为止,我们还没写过一行代码呢。下面我们要完成一个小功能:在锁屏界面增加一个 UILabel 显示一行文字,可以是你的座右铭或者其他的,这里我们显示Hello, MobileSubstate!!

打开我们刚才创建的 firsttweak 目录下的 Makefile 文件,在 FirstTweak_FILES = Tweak.xm 下面增加一行 FirstTweak_FRAMEWORKS = UIKit 并保存文件,前缀都是 TWEAK_NAME 的值, 也就是FirstTweak, 注意根据你自己的情况来修改。增加这行的原因很明显,增加 UILabel 需要用到 UIKit Framework。整个文件看起来像这样:

1
2
3
4
5
6
7
8
9
10
include theos/makefiles/common.mk

TWEAK_NAME = FirstTweak
FirstTweak_FILES = Tweak.xm
FirstTweak_FRAMEWORKS = UIKit

include $(THEOS_MAKE_PATH)/tweak.mk

after-install::
install.exec "killall -9 SpringBoard"

这个步骤完成之后,我们就要找到锁屏界面对应的 ViewController,然后替换它的某个方法,把 UILabel 添加到它的 view 上。这个 ViewController 的名字叫 SBAwayController, SB 是SpringBoard 的缩写,不要想偏了 :). 我们要替换它的 - (void)activate 方法。SBAwayController类的头文件可以在 iOS6 的私有类的头文件 中找到。在 SBAwayController 里有个叫 _awayViewivar,获得这个 ivar 需要一个 theos 中不存在的方法,好吧,它叫 MSHookIvar, 这个方法在默认的 theos 的substrate.h 头文件里没有,可以在 GitHub 得到包含这个方法的头文件。下载到本地,覆盖 theos/include 下的同名文件(推荐将原有的 substrate.h 头文件重命名)。

OK,到这里万事具备,只欠 Coding 了。

打开 firsttweak 目录下的 Tweak.xm 文件并 清空,添加下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
%hook SBAwayController 
- (void)activate {
%orig(); //invoke the orignal method to do what should to do.
NSLog(@"=========================================================");
NSLog(@"Hello MobileSubstrate!!");
NSLog(@"=========================================================");

//get _awayView via MSHookIvar method
UIView *_awayView = MSHookIvar<*>(self, "_awayView");

//create a lable whose width = 200 and height = 100 and add to _awayView
float w = 200;
float h = 100;
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake((_awayView.frame.size.width - w)/2,100,w,h)];
label.text = @"Hello, MobileSubstate!!";
label.textAlignment = NSTextAlignmentCenter;
label.backgroundColor = [UIColor clearColor];
label.textColor = [UIColor whiteColor];
[_awayView addSubview:label];
}
%end

大概解释一下,%hook SBAwayController以及里面的 - (void)activate 方法,其实就类似 swizzling 了 SBAwayControlleractivate方法。当系统执行 SBAwayControlleractivate方法的时候会执行 tweak 里的 activate 的方法。 在这里方法里我们先执行了 %orig(),就是执行原来的activate 方法,保证原有的方法先执行,再执行我们自己的代码。

这个 activate 方法在第一次进入锁屏界面的时候会执行,在以后每次非锁屏状态下,按关机键也会执行。

接下来就是通过 MSHookIvar 获得 _awayView。然后就是我们非常熟悉的了,创建一个 UILabel,添加到_awayView 里。到这里就结束了。make package install一下(还需要先执行一下export THEOS_DEVICE_IP= 手机 IP 地址),安装到手机上,等 SpringBoard 重启完,你会看到类似下图的界面: Alt text

把手机连接到电脑上,打开 Xcode,在 Organizer 里的 Console 里能看到程序中使用 NSLog 打印的信息,用来调试很方便呢。 Alt text

总结

本文主要是讲 Mobile Substrate 的作用以及如何使用 Theos 开发一个简单的 tweak。有了这些入门的基础之后,你就可以根据自己的想法来写自己喜欢的 tweak。如果你是在 iOS7 下越狱的话,可以尝试一下把控制中心的 AirDrop 和音乐播放器给隐藏掉,让控制中心看起来更简洁。接着可以再进行改进,比如在蓝牙关闭的时候不显示 AirDrop,开启的时候依然显示,音乐正在播放的时候显示音乐播放器,否则不显示。

这个小 Demo 是前两周写的,一直没有时间整理出来,今天抽时间整理了一下文字发了出来,算是送给自己新年的一件礼物吧!

Thanks,Have Fun!

More About Substrate And Theos

Background Fetch

Background Fetch 是 iOS7 带来的非常 Cool 的新特性,开启 Background Fetch 的 App 会被系统在合适的时机执行后台任务的代码。比如这个场景:你每天晚上 10 点会通过自己的 RSS 阅读器 App 来阅读,系统可能会在 10 点之前执行 App 中设定的下载 RSS 最新资源的任务,当你打开 RSS 阅读器 App 的时候就显示出最新的内容。实现 Background Fetch 的步骤也是非常的简单,下面就来看一下。

1、开启 Background Fetch

给一个 App 开启 Background Fetch 非常的简单,可以总结为三个步骤:

#Step 1

进入 Project 设置 -> Capabilities -> 设置 Background Modes 为 ON -> 选中Background Fetch

BG_Fetch01

#Step 2

在 ApplicationDelegate 类的

1
-(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
方法中,添加下面的代码:

1
[[UIApplication sharedApplication] setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalMinimum];

MinimumBackgroundFetchInterval参数值是时间间隔的数值,系统保证两次 Fetch 的时间间隔不会小于这个值,不能保证每隔这个时间间隔都会调用。这里设置为UIApplicationBackgroundFetchIntervalMinimum,意思是告诉系统,尽可能频繁的调用我们的 Fetch 方法。

#Step 3

开始实现我们的 Fetch 方法,在 ApplicationDelegate 类中加入下面这个方法:

1
2
3
4
5
6
7
8
9
10
11
12
- (void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
SSViewController *ssVC = (SSViewController*)self.window.rootViewController;
if ([ssVC isKindOfClass:[SSViewController class]]) {
NSLog(@"is SSViewController");
ssVC.indexValue ++;
completionHandler(UIBackgroundFetchResultNewData);
} else {
NSLog(@"is not SSViewController");
completionHandler(UIBackgroundFetchResultFailed);
}

}

这个方法每次系统执行 Background Fetch 时都会被调用,可以在这里下载网络数据等。执行完下载任务之后,需要立即调用completionHandlerblock。文档中提到系统用耗时来估算这次 fetch 的电量消耗和数据消耗,如果耗时比较长,未来可能减少被调用的机会。completionHandlerblock 可以用的参数值有下面三个:

  • UIBackgroundFetchResultNewData 拉取数据 OK
  • UIBackgroundFetchResultNoData 没有新数据
  • UIBackgroundFetchResultFailed 拉取数据失败或者超时

文档中也提到,当这个方法被调用后,App 有 30s 的时间来执行下载操作,然后马上执行completionHandlerblock,就是说最好能把下载任务的耗时限制在 30s 内,超过 30s 的,App 会被系统挂起。

在刚才给出的方法中,为了方便测试只是更新了 ViewController 的一个参数值,这个参数值会直接反应到界面上,方面测试。

有个小细节是假如 Background Fetch 方法更新了 UI 的话,系统会刷新 Home 键切换 App 界面中的缩略图。

2、模拟 Background Fetch

创建了 Background Fetch 后,怎么来方面的模拟和测试呢?有两种方式,一种是在 App 被挂起后,系统执行 Background Fetch,另外一种是 App 没有在运行,被系统唤醒执行 Background Fetch 方法。

# 情况 1

直接运行程序,在 Xcode 的菜单中,选择 "Debug" -> "Simulate Background Fetch",你会发现会先打开 App,然后后台挂起,接着执行 (void)application: performFetchWithCompletionHandler 方法。

BG_Fetch02

# 情况 2

复制(Duplicate)一份当前的 Schema,在新的 Schema 的 Options 下,选中 "Launch due to a background fetch event",运行这个 Schema。

BG_Fetch03

BG_Fetch04

3、Remote Notifications & Background Transfer Service

Background Fetch 适用于定期检查更新数据,如果想从服务端推送一条消息告诉客户端来执行某些操作的话,可以使用 Remote Notifications,它和普通的 Push Notification 很相似,不同的是推送时的 Payload 不太一样以及客户端收到通知之后会执行一个的方法,和 Background Fetch 一样有 30s 的时间来做事情。你看到这里一定有个疑问,如果任务在 30s 内不能完成怎么破?比如下载音视频文件。Background Transfer Service 闪亮出场了,感兴趣的话可以参考 Ref 里的第 3、4 条链接里的内容。

Ref

完鸟,如果有写的不对的地方,欢迎小伙伴们指正,Have fun~