轻量级 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!~

WWDC 2013 视频英文字幕下载

不卖关子,这是一个git repo ,可以从这里下载到 WWDC 2013 公开的 100 个视频的英文字幕。 如果觉得有用的话,不妨 star 一下,或者在微博上 @我 满足一下我的虚荣心 :-),这都不重要,重要的是一定要 坚持看完这 100 个视频

我发起这个项目以及抓取到这些字幕的的原因是这样的,一个是英语的听力太差,基本上听不懂苹果的传道士们在视频中说的是什么,没有字幕真是很难受,然后是发现在 iPad 上使用 WWDC 这个 App 看视频的时候是有字幕的,但在 iPad 上看屏幕不够大,看起来也很费劲。就想既然在 iPad 上有字幕,一定有办法抓取出来,于是就开工,用 burpsuite 之类的抓 Http 请求包的 App 很容易就能探测到字幕文件的地址,在准备写代码的时候,Google 了一下,发现一个 python 写的 gist 正是做这个的,于是就用这个脚本把一部分视频的字幕下载下来,自己又现学了点 ruby 写了个 gist 脚本来把分散的字幕文件按照顺序合并起来。 刚开始下载的比较慢,因为这个脚本是单线程的,后来自己改了一下,分 10 个线程,每个线程下载 10 个视频的字幕,这样就快很多,这个代码因为比较简单,就没放出来,有兴趣的童鞋自己也可以实现。

另外 @lexrus 同学的这个 gist 里提供了所有视频的 HD 和 SD 的版本,以及文件序号和视频名称的对应关系,可以直接放在迅雷里下载,完了再配上字幕,可以像欣赏好莱坞大片一样的欣赏 WWDC2013 带来的新技术盛宴了!

关于 revoke certificate 和备份的这点事

事情是这样的,前几天电脑崩溃,硬盘数据全部丢失,重装系统和 Xcode 之后,从 Develop Center 的 Certificates 里重新下载证书,安装到新电脑上,在真机上运行时,提示报错:"A valid signing identity matching this profile could not be found in your keychain", 按照字面意思查了一下,是因为本地 KeyChain 里丢失了 private key 的缘故,得到的结论是要么从之前的备份中恢复,或者重新生成新的证书。遗憾的是我之前并没有保存备份,无奈只好重新生成。一个问题马上出现在脑海,就是重新生成新的证书是否会对线上的 App 产生影响。查了一下官方的文档,发现有这部分详细的说明,消除了我的顾虑,内容如下(参考 2):

Important: Members of the Standard iOS Developer Program can be assured that replacing either your developer or distribution certificate will not affect any existing apps that you've published in the iOS App Store, nor will it affect your ability to update those apps.

Notes before beginning:

Replacing your distribution certificate won't affect your developer certificate or development profiles.

Similarly, replacing your developer certificate won't affect your distribution certificate or distribution profiles.

Replace only your development certificate if you are troubleshooting an issue running your app on device through Xcode.

Replace only your distribution certificate if you are troubleshooting an issue creating, submitting or installing a distribution build.

After replacing your certificate(s) you are required to update and reinstall any provisioning profiles that were bound to the old certificate.

搞清楚问题和解决办法,就开工:

  1. 首先在 iOS Provisioning Portal 里 revoke 掉当前失效的 Certificates,并创建一个新的 Certificates(参考[2])
  2. 通过 import 和 export Developer Profile 备份和恢复 (参考[3] 和[4])

Links:

  1. iOS Provisioning Portal
  2. How do I delete/revoke my certificates and start over fresh?
  3. Exporting and Importing Developer Profile
  4. Transferring Your Identities

Xcode 的 iOS 项目的版本号设置

Version & Build 号

Image1 icon

今天对 Xcode 里 iOS 的版本号又有了新的认识,一个叫做 Version,一个叫做 Build,这两个值都可以在 Xcode 中选中 target,点击“Summary”后看到。 Version 在 plist 文件中的 key 是“CFBundleShortVersionString”,和 AppStore 上的版本号保持一致,Build 在 plist 中的 key 是“CFBundleVersion”,代表 build 的版本号,该值每次 build 之后都应该增加 1。这两个值都可以在程序中通过下面的代码获得:

1
[[[NSBundle mainBundle] infoDictionary] valueForKey:@"key"]
<br /> <br />

Archive 后自动增长 build 号

除此之外,如果我们想在 Archive 后 build 号自动增长,就可以使用到 Xcode 的 run script 来实现,步骤是

  1. 选中项目的 target,点击“Build Phases“
  2. 点击右下角的”Add Build Phrase“,选择”Add run script“,会产生一个新的 Run Script 项
  3. 拖拽新生成的 Run Script 项到最上面
  4. 点开该项,copy 下面的 shell 代码进去,代码来自 [这里](http://stackoverflow.com/questions/9855955/xcode-increment- build-number-only-during-archive?answertab=active#tab-top),如下图所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if [$CONFIGURATION == Release ]; then
echo "Bumping build number..."
plist=${PROJECT_DIR}/${INFOPLIST_FILE}

#increment the build number (ie 115 to 116)
buildnum=$(/usr/libexec/PlistBuddy -c "Print CFBundleVersion" "${plist}")
if [["${buildnum}" == "" ]]; then
echo "No build number in $plist"
exit 2
fi

buildnum=$(expr $buildnum + 1)
/usr/libexec/Plistbuddy -c "Set CFBundleVersion $buildnum" "${plist}"
echo "Bumped build number to $buildnum"

else
echo $CONFIGURATION " build - Not bumping build number."
fi

这段 shell 脚本的意思就是说,如果当前的配置是 Release(Archive 时该值为 Release,直接在模拟器上运行是 Debug),就设置 build 值为当前 build 值 +1, 否则什么都不干。

这样在 build 的时候就会看到 build 号会自动加 1 的,想看 build 时输出的信息,可以通过 "View -> Navigators -> Log" 来查看最新的 build 时产生的 log。 <br /><br />

Ref:

  1. Concurrent Debug, Beta and App Store Builds
  2. stackoverflow: Xcode-Increment build number only during ARCHIVE?

Have fun!

UINavigationController 的 setViewControllers 方法

在 iOS 开发中,UINavigationController 是很常用的 Controller,对它的一般操作就像操作一个栈,push 和 pop。但也经常会遇到 pop 和 push 无法优雅的完成的操作,比如退回到中间的某个 VC 上,或者在第一个 VC 之前添加一个 VC 等,更甚者要重新构造整个 VC 的顺序,这时候 setViewControllers 方法就排上用场了,它使对 VC 栈的操作不再局限于 push 和 pop,而是构造整个 VC 栈并应用到当前的 UINavigationController 中,这个方法支持 iOS3.0+,放心使用。
<br /> #Sample

1
2
3
4
5
6
NSMutableArray * viewControllers = [self.navigationController.viewControllers mutableCopy];
[viewControllers removeLastObject];
[viewControllers addObject:newController];

[self.navigationController setViewControllers:viewControllers animated:YES];
// [viewControllers relase] // if non-arc

感谢 Allen(Weibo) 提供的代码和思路 <br /> <br /> #说明
下面这段摘自 Api 文档

You can use this method to update or replace the current view controller stack without pushing or popping each controller explicitly. In addition, this method lets you update the set of controllers without animating the changes, which might be appropriate at launch time when you want to return the navigation controller to a previous state.

If animations are enabled, this method decides which type of transition to perform based on whether the last item in the items array is already in the navigation stack. 

1.If the view controller is currently in the stack, but is not the topmost item, this method uses a pop transition; 
2.if it is the topmost item, no transition is performed. 
3.If the view controller is not on the stack, this method uses a push transition. 

Only one transition is performed, but when that transition finishes, the entire contents of the stack are replaced with the new view controllers. For example, if controllers A, B, and C are on the stack and you set controllers D, A, and B, this method uses a pop transition and the resulting stack contains the controllers D, A, and B.

<br /> Have fun! <br /> <br />