0%

这篇 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

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

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 是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~