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~

轻量级 KVO[译]

在这篇文章中,我会实现一个自己用的简单 KVO 类,我认为 KVO 非常棒,然而对于我大部分的使用场景来说,有这两个问题:

  1. 我不喜欢在 observeValueForKeyPath:ofObject:change:context: 方法里通过 keyPath 值来做调度,当 Observe 比较多的对象时,会使得代码变得杂乱和迷惑。
  2. 必须手动的来注册和删除一个观察者,如果能自动做就好了。

So,我们开始这个实现。这个技巧我第一次是在 THObserversAndBinders 项目中见到,本篇内容也仅仅描述了一下里面的做法,同时做了简化。

首先,我们定义一下我们的这个类,我们这个帮助类的类名是Observer:

1
2
3
4
5
6
@interface Observer : NSObject
+ (instancetype)observerWithObject:(id)object
keyPath:(NSString*)keyPath
target:(id)target
selector:(SEL)selector;
@end

Observer 类的这个类方法有四个参数,每个参数都是自解释的,我选择使用 target/action 模式,当然也可以使用 block,但是那样的话需要做 weakSelf/strongSelf 的转换,你懂的,通常来说分来来做比较好。

我们做的是在初始化方法中设置 KVO,并在 dealloc 方法中移除。这意味着一旦 Observer 对象被 retain,我们就有了一个观察者,下面这段代码是从我的一个 ViewCOntroller 中拿来的:

1
2
3
4
self.usernameObserver = [Observer observerWithObject:self.user
keyPath:@"name"
target:self
selector:@selector(usernameChanged)];

把这个 Observer 对象作为一个属性放在 ViewController 中来保证被 retain,一旦我们的 Viewcontroller 被释放,就会设置它为 nil,observer 就停止观察了。

在这个实现中,使用一个 weak 引用指向被观察对象和观察者 (target) 是很重要的,如果两个中的其中一个是 nil,我们就停止向观察者发送消息。

1
2
3
4
5
6
@interface Observer ()
@property (nonatomic, weak) id target;
@property (nonatomic) SEL selector;
@property (nonatomic, weak) id observedObject;
@property (nonatomic, copy) NSString* keyPath;
@end

初始化器里设置 KVO 通知,使用 self 作为 context,如果我们会有一个子类也添加类似的观察者时就很有必要了。

1
2
3
4
5
6
7
8
9
10
11
- (id)initWithObject:(id)object keyPath:(NSString*)keyPath target:(id)target selector:(SEL)selector
{
if (self) {
self.target = target;
self.selector = selector;
self.observedObject = object;
self.keyPath = keyPath;
[object addObserver:self forKeyPath:keyPath options:0 context:self];
}
return self;
}

一旦被观察者发生变化,我们就通知观察者(target),如果它还存在的话:

1
2
3
4
5
6
7
8
9
10
11
12
- (void)observeValueForKeyPath:(NSString*)keyPath ofObject:(id)object change:(NSDictionary*)change context:(void*)context
{
if (context == self) {
id strongTarget = self.target;
if ([strongTarget respondsToSelector:self.selector]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[strongTarget performSelector:self.selector];
#pragma clang diagnostic pop
}
}
}

最后在 dealloc 方法中移除观察者对象:

1
2
3
4
5
6
7
- (void)dealloc
{
id strongObservedObject = self.observedObject;
if (strongObservedObject) {
[strongObservedObject removeObserver:self forKeyPath:self.keyPath];
}
}

这就是全部内容了。还有很多可以扩展的地方,比如增加 block 的支持,或者我比较喜欢的 trick:再增加爱一个方便的构造方法用来第一次直接调用 action。然而,我想的是展现出这个技术的核心部分,你可以根据自己的需求来调整它。

这个技术的优点是在使用 KVO 的时候不需要记住太多东西,仅仅 retain 住 Observer 对象,然后在完成的试试置为 nil 即可,剩下的会自动完成。

原文作者是 Chris Eidhofobjc.io 的创办者
原文地址:Lightweight Key-Value Observing

AppCode JVM 参数优化

昨晚花了 2 个小时熟悉了一下 AppCode, 和 IDEA 系列给人的感觉一样:很卡很强大。就打算优化一下 JVM 的设置,AppCode 的 JVM 参数配置文件在 /Applications/AppCode EAP.app/bin/idea.vmoptions

使用默认的参数,用一段 AppCode,观察了一下 GC 的情况:

➜  ~  jstat -gcutil 50991 1s
  S0     S1     E      O      P     YGC     YGCT    FGC    FGCT     GCT
 79.31   0.00  37.61  88.64  60.84   6654   57.031   137    3.017   60.048
 79.31   0.00  37.63  88.64  60.84   6654   57.031   137    3.017   60.048
 79.31   0.00  37.65  88.64  60.84   6654   57.031   137    3.017   60.048
 79.31   0.00  37.66  88.64  60.84   6654   57.031   137    3.017   60.048
 79.31   0.00  37.67  88.64  60.84   6654   57.031   137    3.017   60.048
 79.31   0.00  37.69  88.64  60.84   6654   57.031   137    3.017   60.048
 79.31   0.00  37.70  88.64  60.84   6654   57.031   137    3.017   60.048

发现 YoungGC 有 6654 次,耗时 57s,FullGC 有 137 次,3s 多,花在 GC 上的总时间有 60s,按每次卡一次 1s 来算,单是 GC 就让人感觉到 60 次明显卡顿,确实让人受不了。

查了一下默认的参数,内存设置的太保守,所以我改成了下面这个方案:
我的机子是 8G 内存,给 AppCode 分配 1500M,如果你的是 4G 内存,建议把 -Xms1500m-Xmx1500m都调成 1000m,-XX:NewSize=600m-XX:MaxNewSize=600m 改为 400M。修改之前把 idea.vmoptions 文件备份一下,以防万一。

-Xms1500m
-Xmx1500m
-XX:NewSize=600m  
-XX:MaxNewSize=600m
-XX:SurvivorRatio=8
-XX:PermSize=200m
-XX:MaxPermSize=400m
-XX:ReservedCodeCacheSize=96m
-XX:+UseCompressedOops
-XX:+DisableExplicitGC

使用后:

➜  jstat -gcutil 58835 1s

  S0     S1     E      O      P     YGC     YGCT    FGC    FGCT     GCT
 61.70   0.00  48.84  15.60  52.92     12    1.066     0    0.000    1.066
 61.70   0.00  48.84  15.60  52.92     12    1.066     0    0.000    1.066
 61.70   0.00  48.84  15.60  52.92     12    1.066     0    0.000    1.066
 61.70   0.00  48.84  15.60  52.92     12    1.066     0    0.000    1.066
 61.70   0.00  48.84  15.60  52.92     12    1.066     0    0.000    1.066
 61.70   0.00  48.84  15.60  52.92     12    1.066     0    0.000    1.066

YGC 降低到了 12 次,GC 时间是 1s,没有 FullGC, 没有感觉到卡顿的情况。

这个主要是从内存分配方面优化,GC 算法上也可以优化,但是需要多测试每种 GC 算法的情况,也可能会因人而异,等我慢慢找到一个不错的方案再分享出来。

至于上面参数的意思,可以查看我在 iteye 上以前的一篇 Blog:10s 启动 MyEclipse/Eclipse 的 JVM 参数(含 Mac 下)

如何在一个设备上安装一个 App 的两个不同版本

最近干了件蠢事,事情是这样的,我们 App 有 2 套图标,一套是测试版图标用于发布 OTA 的内部测试版,一套是正式版用于发布到 AppStore,每次打包,我都会检查图标,结果上次粗心搞错了,把测试版的图标打包发布到 AppStore 了,发现之后想死的心都有了。马上修改了一版,申请紧急审核,结果你可能猜到了,没有通过。这是个很大的教训,像这一类的手动来改都不靠谱,毕竟有忘掉的概率存在,能不能自动处理呢? 在 这篇 Blog上找到了答案, 我大概的翻译一下。

iOS 系统区分两个 App 是否相同的根据是 App 的 Bundle ID 是否相同,在安装一个程序时,系统是根据 Bundle ID 来判断是全新安装还是升级。那想在一个系统上安装一个 App 的两个不同版本,其实是需要两个不同的 Bundle ID。就是说正式版一个 Bundle ID,OTA 版本 /Debug 版本用一个 Bundle ID,假设 AppStore 版的 ID 是 com.mycompany.myapp,OTA 版的是com.mycompany.myapp-beta。同时为了直观的区分两个 App,一般也会使用两套图标, 假设 AppStore 版的图标名称为Icon.png, Icon@2x.png, OTA 版是Icon-beta.png, Icon-beta@2x.png. 那如果做到自动化的配置呢?答案在 Build 设置(Build Setting) 里。

默认 Xcode 会提供 2 个 Build 配置 (Build Configuration):DebugRelease,我们再加一个AppStore, 这样来用:

  • Debug: 用来直接连机调试
  • Release:用于发布 OTA 的测试版
  • AppStore:用户提交到 AppStore

下一步我们来在项目的 Build Setting 里添加两个自定义的设置,一个命名为BUNDLE_IDENTIFIER, 另一个命名为APP_ICON_NAME,如下图这样设置:

add_user_define_setting

这两个值分别定义个 Bundle ID 和图标的名称,下一步需要在 Info.plist(名字格式是 YourAppName-Info.plist)中修改 BundleId 和 Icon 图标名称,把 bundle identifier 值设置为${BUNDLE_IDENTIFIER},把图标值设置为${APP_ICON_NAME}@2x.png${APP_ICON_NAME}.png,如果提供了 72px 和 144px 等图标也类似这样。

${xxx}语法是预处理语法,都会被替换为 xxx 对应的真实值,在刚才的设置的基础上,在 Debug 的时候,实际的 Bundle ID 会替换为 com.mycompany.myapp-beta, 图标对应的为Icon-beta.pngIcon-beta@2x.png,Cooool

实际上我自己实践的时候,新建了一个叫 myApp-AppStoreSchema,在不同的 Schema 里的 Archive 里是用不同的 Build 配置,myApp-AppStore的 Schema 里 Archive 的 Build 配置为 "AppStore",原来的 myApp 这个 Schema 的 Build 配置为 Release,这样当我想发布 OTA 的时候,选择 myApp-AppStore 这个 Schema,然后 Archive,就能使用 AppStore 的自定义的配置来打包,用来提交 AppStore;当选择 myApp 这个 Schema 的时候,Archive 得到的是使用 Release 的自定义配置来打包的,用来上传到 OTA 测试。整个过程是自动化的,包括 BundleId 和图标文件的名称,如果你有别的类似的需要,也可以参考着来。

总之,麻麻再也不用担心我的图标会搞错了。

这篇文章编译自:How to Have Two Versions of the Same App on Your Device ,原作者 Blog 上还有其他精彩的文章等你发现。

NSOperation

<div style="text-align:center; margin-bottom:10px;"> <img src="/assets/nsoperation.png"
height="400" width="600"> </div>

几乎每个开发者都知道,让 App 快速响应的秘诀是把耗时的计算丢到后台线异步去做。于是,Modern Objective-C 开发者有两个选择:GCDNSOperation.

由于 GCD 已经发展的比较主流了,我们稍后再说它,先说说面向对象的 NSOperation.

NSOperation 表示一个单独的计算单元,它是一个抽象类(很类似 Java 里的 Runnable 接口),给子类提供了一些非常有用且线程安全的特性,比如 状态 (state), 优先级 (priority), 依赖 (dependencies) 以及 取消(cancellation). 如果你不想子类化 NSOperation,可以选择使用 NSBlockOperation 这个 NSOperation 的子类,它可以把一个 block 包装成为一个 NSOperation.

非常适合使用 NSOperation 的任务例子包括network requests, 图片的缩放,语言处理或者其他一些重复的、结构化的以及需要运行较长时间来处理数据的任务。

但是,仅仅把计算包装成一个对象,没有一些监管也不会非常的有用,这时 NSOperationQueue 就出现了。

NSOperationQueue 控制各个 operation 的并发执行. 它像是一个优先级队列,operation 大致的会按 FIFO 的方式被执行,不过带有高优先级的会跳到低优先级前面被执行(用 NSOperation 的 queuePriority 方法来设置优先级)。 NSOperationQueue 支持并发的执行 operations,通过 maxConcurrentOperationCount 来指定最大并发数,就是同时有最多有多少个 operation 同时被运行。

可以通过调用 -start 方法来启动一个 NSOperation,或者把它放到 NSOperationQueue 里,当到达队列最前端时也会被自动的执行。

现在来看看 NSOperation 的几个不同的特性,以及如何如果使用和子类化它:

状态 State

NSOperation 构建了一个非常优雅的状态机来描述一个 operation 的执行过程:

isReady -> isExecuting -> isFinished

State 是通过这些 keypath 的 KVO 通知来隐式的得到,而不是显式的通过一个 state 的属性。就是说,当一个 operation 已经准备就绪,将要被执行时,它会为isReadykeyPath 发送一个 KVO 的通知,对应的属性值也会变为 YES.

为了构造一致的状态,下面每个属性都与其他属性相互排斥:

  • isReady: 如果 operation 已经做好了执行的准备返回 YES,如果它所依赖的操作存在一些未完成的初始化步骤则返回 NO。
  • isExecuting: 如果 operation 正在执行它的任务返回 YES,否则返回 NO。
  • isFinished: 任务成功的完成了执行,或者中途被 Cancel,返回 YES。NSOperationQueue 只会把 isFinished 为 YES 的 operation 踢出队列,isFinished 为 NO 的永远不会被移除,所以实现时一定要保证其正确性,避免死锁的情况发生。

取消 Cancellation

如果正在进行的 operation 所做的工作不再有意义,尽早的取消掉是非常有必要的。取消一个 operation 可以是显式的调用 cancel 方法,也可以是 operation 依赖的其他 operation 执行失败。

和 state 类似,当 NSOperation 的被取消,是通过isCancelledkeypath 的 KVO 来获得。当 NSOperation 的子类覆写 cancel 方法时,注意清理掉内部分配的资源。特别注意的是,这时 isCancelled 和 isFinished 的值都变为了 YES,isExecuting 为值变为 NO。

一个需要格外注意的地方是和单词“cancel”有关的两个词:

  • cancel : 带一个 "l" 表示方法 (动词)
  • isCancelled : 带两个 "l" 表示属性(形容词)

优先级 Priority

所有的 operation 在 NSOperationQueue 中未必都是一样的重要,设置 queuePriority 属性就可以提升和降低 operation 的优先级,queuePriority属性可选的值如下:

  • NSOperationQueuePriorityVeryHigh
  • NSOperationQueuePriorityHigh
  • NSOperationQueuePriorityNormal
  • NSOperationQueuePriorityLow
  • NSOperationQueuePriorityVeryLow

另外,operation 可以指定一个 threadPriority 值,它的取值范围是 0.0 到 1.0,1.0 代表最高的优先级。queuePriority决定执行顺序的优先级,threadPriority决定当 operation 开始执行之后分配的计算资源的多少。

依赖 Dependencies

取决于你的 App 的复杂性,可能会需要把一个大的任务分成多个子任务,这时 NSOperation 依赖就排上用场了。

比如从服务器上下载和缩放图片的过程,你可能会想把下载图片作为一个 operation,缩放作为另外一个(这样也可以复用下载图片和缩放图片的代码)。然后,一个图片在从服务器上下载下来之前是没有办法缩放的,于是我们说缩放图片的 operation 依赖从服务器上下载图片的 operation,后者必须先完成,前者才能开始执行。用代码表示是这样的:

1
2
3
[resizingOperation addDependency:networkingOperation];
[operationQueue addOperation:networkingOperation];
[operationQueue addOperation:resizingOperation];
一个 operation 只有在它依赖的所有的 operation 的 isFinished 都为 YES 的时候才会开始执行。要记住添加到 queue 里的所有的 operation 的依赖关系,并避免循环依赖,比如 A 依赖 B,B 依赖 A,这样会产生死锁。

completionBlock

completionBlock是在 iOS4 和 Snow Leopard 中添加的一个非常有用的特性。当一个 NSOperation 完成之后,就会精确地只执行一次 completionBlock。我们需要在 operation 完成之后想做点什么的时候这个属性就会非常有用。比如当一个网络请求结束之后,可以在completionBlock 里处理返回的数据。

总结

NSOperation 依然是 Modern Objective-C 程序员杀手锏里的重要工具。相对于 GCD 非常适用于 in-line 的异步处理,NSOperation 提供了更综合的、面向对象的计算模型,非常适用于封装结构化的数据,重复性的任务。把它加到你的下个项目中,给你的用户和你自己都带来乐趣吧!

译者注

本文编译自 NSHipster 里的 NSOperation 一文,感谢作者Mattt Thompson, 来头很大,这是他的简介:

Mattt Thompson is the Mobile Lead at Heroku, and the creator & maintainer of AFNetworking and other popular open-source projects, including Postgres.app & Induction. He also writes about obscure & overlooked parts of Cocoa on NSHipster.

最上面的图片是来自于 WWDC2013 中的“Hidden Gems in Cocoa and Cocoa Touch”(228)中 Mattt 讲 NSOperation 时的截图,这个视频一共有 30 个 tips,这是第 8 个 tip,大部分的内容我是第一次知道,非常值得看,而且如果有条件的话,建议下载 HD 版本的视频来看,效果比 SD 好太多。字幕文件在我的这个 repo 里, :)

如有文中有不准确的地方,欢迎留言指正 :)

Enjoy!

写个自己的 Xcode4 插件

刚写 iOS 程序的时候就知道 Xcode 支持第三方插件,比如 ColorSense 等很实用的插件,但 Xcode 的插件开发没有官方的文档支持,一直觉得很神秘,那今天就来揭开它的面纱。

在 Xcode 启动的时候,它会检查插件目录 (~/Library/Application Support/Developer/Shared/Xcode/Plug-ins) 下所有的插件 (扩展名为.xcplugin 的 bundle 文件)并加载他们。其实到这里我们就猜到了,我们做的插件最终会是一个扩展名为 .xcplugin 的 bundle 文件,放在插件目录下供 Xcode 加载。

OK,我们先做一个简单的插件,需要很简单的几个步骤即可完成,我的环境是 Xcode 4.6.3 (4H1503)。

1. 新创建一个 Xcode Project

Xcode 插件其实就是一个 Mac OS X bundle,所以可以参考下图创建一个 Bundle。 Image1 icon

给 Project 起个名字,并确保 不要 勾选Use automatic reference counting,因为 Xcode 是使用 GC 来管理内存的,所以 Xcode 的插件也需要是用 GC 来管理内存的。Framework 选择Cocoa

Image2 icon

2. 设置 Target Info

像下图一样设置这些信息

  • XC4Compatible = YES
  • XCPluginHasUI = NO
  • XCGCReady = YES
  • Principal Class = Plugin (这个设置为你 插件的名字,本例中命名为Plugin)

前三个可能 Info 里缺省没有,可以自己添加,都选 Boolean 类型,最后一个 Principal ClassString类型。 Image3 icon

3. 设置 Build Settings

然后打开 Build Setting Tab,设置这些:

  • 设置 Installation Build Products Location${HOME},Xcode 会自动转换为你当前用户的 Home 路径
  • 设置 Installation Directory/Library/Application Support/Developer/Shared/Xcode/Plug-ins, Xcode 会把拼接Installation Build Products LocationInstallation Directory为一个绝对路径来查找你的插件
  • 设置Deployment LocationYES
  • 设置Set Wrapper extensionxcplugin

Image4 icon Image5 icon

4. 添加 User-Defined 设置

  • 设置GCC_ENABLE_OBJC_GCsupported
  • 设置GCC_MODEL_TUNINGG5

Image6 icon

有了这些设置,每次 build 这个 Projct 的时候,Xcode 就会把 build 后的插件 copy 到 plugin 文件夹下,然后我们需要重启 Xcode 来重新加载新 build 的插件。开发插件相对来说简单一些,调试插件就比较纠结了,唯一的办法就是 build 之后,重启 Xcode,来加载最新 build 的插件。

准备工作已经结束,下面开始实现我们的插件。

5. 实现我们的插件

在第二步的时候我们设置了一个 Principal Class,那么在 Xcode 里新建 Objective-C 类,名字和Principal Class 设置的值保持一致。在实现文件中添加上 + (void) pluginDidLoad: (NSBundle*) plugin 方法。 该方法会在 Xcode 加载插件的时候被调用,可以用来做一些初始化的操作。通常这个类是一个单例,并 Observe 了NSApplicationDidFinishLaunchingNotification,用来获得 Xcode 加载完毕的通知。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
+ (void) pluginDidLoad: (NSBundle*) plugin {
static id sharedPlugin = nil;
static dispatch_once_t once;
dispatch_once(&once, ^{
sharedPlugin = [[self alloc] init];
});
}

- (id)init {
if (self = [super init]) {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationDidFinishLaunching:)
name:NSApplicationDidFinishLaunchingNotification
object:nil];
}
return self;
}
一旦接收到 Xcode 加载完毕的通知,就可以 Observe 需要的其他 notification 或者在菜单中添加菜单项或者访问 Code Editor 之类的 UI 组件。

在我们的这个简单例子中,我们就在 Edit 下添加一个叫做 Custom Plugin 的菜单项,并设置一个 ⌥ + c 快捷键。它的功能是使用 NSAlert 显示出我们在代码编辑器中选中的文本。我们需要通过观察 NSTextViewDidChangeSelectionNotification 并访问接收参数中的NSTextView,来获得被选中的文本。

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
- (void) applicationDidFinishLaunching: (NSNotification*) notification {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(selectionDidChange:)
name:NSTextViewDidChangeSelectionNotification
object:nil];

NSMenuItem* editMenuItem = [[NSApp mainMenu] itemWithTitle:@"Edit"];
if (editMenuItem) {
[[editMenuItem submenu] addItem:[NSMenuItem separatorItem]];

NSMenuItem* newMenuItem = [[NSMenuItem alloc] initWithTitle:@"Custom Plugin"
action:@selector(showMessageBox:)
keyEquivalent:@"c"];
[newMenuItem setTarget:self];
[newMenuItem setKeyEquivalentModifierMask: NSAlternateKeyMask];
[[editMenuItem submenu] addItem:newMenuItem];
[newMenuItem release];
}
}

- (void) selectionDidChange: (NSNotification*) notification {
if ([[notification object] isKindOfClass:[NSTextView class]]) {
NSTextView* textView = (NSTextView *)[notification object];

NSArray* selectedRanges = [textView selectedRanges];
if (selectedRanges.count==0) {
return;
}

NSRange selectedRange = [[selectedRanges objectAtIndex:0] rangeValue];
NSString* text = textView.textStorage.string;
selectedText = [text substringWithRange:selectedRange];
}
}

- (void) showMessageBox: (id) origin {
NSAlert *alert = [[[NSAlert alloc] init] autorelease];
[alert setMessageText: selectedText];
[alert runModal];
}

你会发现在出现 selectedText 的地方会报错,在实现里添加上 NSString *selectedText 即可。

1
2
3
@implementation Plugin {
NSString *selectedText;
}

最终效果:
Image7 icon

6. 需要注意的

  • Plugin 不能使用 ARC,需要手动管理好内存(谢谢 @onevcat 的提醒,因为是用 GC,不需要手动管理内存了)
  • 不能直接 Debug,不过可以在程序里通过 NSLog 打印出日志,并通过tail -f /var/log/system.log 命令来查看输出的日志
  • 如果 Xcode 突然启动不起来了,可能是插件有问题,跑去 ~/Library/Application Support/Developer/Shared/Xcode/Plug-ins 目录下,把插件删掉,restart Xcode,查找问题在哪
  • 如果 1-4 步骤的各种设置你比较讨厌的话,可以直接用这个 Xcode4 Plugin Template 来搞定, 怎么使用在它的 Readme 中有详细的说明,:)

总结

这只是一个简单的 Xcode 插件的入门编写示例,不过“麻雀虽小,五脏俱全”,可以了解到 Xcode 的插件一些东西,比如 Xcode 插件本质上其实就是一个 Mac OS X bundle 等等,而且因为没有 Apple 官方的文档的支持,很多东西只能去 Google,或者参考别人插件的一些实现。

REF

本文主要参考和编译自WRITING YOUR OWN XCODE 4 PLUGINS,感谢原作者Blacksmith Software


另: 前两天我们的小伙伴 @onevcat 写了一个 Xcode 插件VVDocumenter,作用是在方法、类等前面输入三个 / 就会自动生成规范的 JavaDoc 文档(Xcode5 中将支持 JavaDoc 类型的文档,对于我这样从 Java 转过来的来说是真是雪中送炭),赶紧 clone 了一个,用起来很方便,很好很强大,强烈推荐! 赶紧把我们的项目代码文档化起来,迎接 Xcode5 的到来吧,:)

Enjoy!!!

Xcode 自定义 Eclipse 中常用的快捷键

之前在用 Eclipse 写 Java 的时候,有几个常用的快捷键,比如删除当前行,在当前行下面插入空行,向上 / 下移动当前行等等,到了 Xcode 里怎么也找不到这些快捷键,一直觉得 Xcode 自带的快捷键不够强大,直到今天才知道不借助第三方的插件,在 Xcode 下完全也可以实现这些功能,下面就说一下如何来做。

首先找到 Xcode 中的自带的配置文件
/Applications/Xcode.app/Contents/Frameworks/IDEKit.framework/Versions/A/Resources/IDETextKeyBindingSet.plist 这个文件里配置了一些可以设置快捷键的操作, 使用常用的编辑器打开它(需要 root 权限)。

然后看看下面这段配置, (来自gist, 感谢作者@gdavis )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<key>GDI Commands</key>
<dict>
<key>GDI Duplicate Current Line</key>
<string>selectLine:, copy:, moveToEndOfLine:, insertNewline:, paste:, deleteBackward:</string>
<key>GDI Delete Current Line</key>
<string>moveToEndOfLine:, deleteToBeginningOfLine:, deleteBackward:, moveDown:, moveToEndOfLine:</string>
<key>GDI Move Current Line Up</key>
<string>selectLine:, cut:, moveUp:, moveToBeginningOfLine:, insertNewLine:, paste:, moveBackward:</string>
<key>GDI Move Current Line Down</key>
<string>selectLine:, cut:, moveDown:, moveToBeginningOfLine:, insertNewLine:, paste:, moveBackward:</string>
<key>GDI Insert Line Above</key>
<string>moveUp:, moveToEndOfLine:, insertNewline:</string>
<key>GDI Insert Line Below</key>
<string>moveToEndOfLine:, insertNewline:</string>
</dict>

这个 dict 是一组可以设置快捷键的操作,里面的 key 是名称,对应的 string 是对应的一组操作,从名字本身也可以看出是什么意思,而且也可以根据这些自由装配成自己的别的快捷操作。

  • GDI Duplicate Current Line 复制当前行到下面一行
  • GDI Delete Current Line 删除当前行
  • GDI Move Current Line Up 把当前行往上移动一行
  • GDI Move Current Line Down 把当前行往下移动一行
  • GDI Insert Line Above 在当前行上面增加一空行
  • GDI Insert Line Below 在当前行下面增加一空行(不管光标是否在行尾)

把这段配置放到上面提到的 IDETextKeyBindingSet.plist 里,放在文件的最后的这两行之前:

1
2
	</dict>
</plist>
重启 Xcode,在 Xcode 菜单中,打开Preferences,选中Key Binding,在右上方搜索GDI, 会出现类似下图的显示,如果没有的话,请检查上面的每步操作。

img

双击右边的空白处,就可以为每个功能设置不同的快捷键,我设置和 Eclipse 里的一致,感受了下,非常爽,Cooool

Have fun!~