现在是切换到 Swift 的时机吗?

在回答这个问题之前,先简单回顾一下 Swift 的发展状况。

时间回到 2014 年 6 月,那时 Swift 刚刚发布。新生事物通常是不成熟的,大部分开发者对它持谨慎态度。接下来一年,Apple 对 Swift 做了许多改进。不仅显著提升了性能,还增加了一些非常重要的语言特性,例如 Error Handling、Protocol Extension 等。紧接着,Apple 在 2015 年 8 月发布了 Swift 2,并于同年将其开源。2016 年,Swift 延续了如火如荼的发展态势,Apple 计划在今年秋季发布 Swift 3 稳定版。就在写这篇文章的时候,Swift 3 的演变已经达到了最后阶段,一切都在良好有序地进行着。

可以看出,Apple 对 Swift 非常用心。当然,除了 Apple 的投入,社区也参与了许多贡献。例如,在 Swift Evolution 中,有许多草案都是社区提交的。

从 Objective-C 切换到 Swift 有哪些优缺点呢?

先说优点吧。我能想到的优点主要有三个:

  • 类型安全。许多错误可以提前在编译时发现。
  • 语言特性丰富。Swift 吸收了很多语言的特性,例如 Optional、模式匹配等。
  • 表达能力强。Swift 不仅语法更加精炼和现代,还提供了诸如 guard 之类的特性。这让代码更具可读性。

对一个刚诞生不久的语言来说,它的缺点显而易见:

  • 开源社区有待积累。有时可能还需要依赖 Objective-C 的库。
  • 语法和 ABI 尚不稳定。未来 Swift 可能还会产生语法上的不兼容。另外,原定在 Swift 3 中实现 ABI 稳定的计划被推迟了,这意味着 Swift 对 library 或 framework 的支持还不够好。

不过 Swift 和 Objective-C 可以相互操作,在 Xcode 中,我们可以毫不费力地把它们桥接起来。因此,如果没有发布 library 或 framework 的需要,语法和 ABI 的不稳定不应该成为切换到 Swift 的阻碍。

最近我也看到有越来越多的公司开始拥抱 Swift 了,其中不乏像 Facebook 这样的大公司。Facebook 前不久发布的 Swift SDK 虽然只是 Objective-C SDK 的 wrapper,但这也标志着 Swift 日趋成熟,是时候可以委以重任了。LeanCloud 也跟随浪潮,发布了 Swift SDK

最后,我认为现在就是切换到 Swift 的绝佳时机。虽然 Swift 距离稳定还有一段路要走,但是「Swift is not dead」。

为 SocketRocket 实现对公钥的 SSL Pinning

前些天为 LeanCloud iOS SDK 的实时通信模块支持了 SSL pinning。

实时通信模块目前使用 SocketRocket 来实现长连接,它是一个 iOS 和 OS X 平台上的 WebSocket 实现。

SocketRocket 支持 pin 整个证书,但这样做存在一个问题:如果服务端更新了证书,SSL pinning 就会失败。更好的做法是 pin 证书的公钥,因为公钥可以保证长期不变。

于是,我为 SocketRocket 实现了对公钥的 SSL pinning。顺便提交了一个 pull request

让 curl 在 iOS 上支持 SSL Pinning

前些天为 LeanCloud iOS SDK 实现 SSL pinning 时遇到一个问题。

LeanCloud iOS SDK 目前使用 curl 访问 REST API,因此要使用 curl 提供的接口来实现 SSL pinning。

从 curl 的 changelog 来看,从 7.39.0 开始就支持 SSL pinning 了。但测试后发现在 iOS 上并不支持。最初,我怀疑是不是有些选项没有设置。但按照官方给出的代码和步骤进一步测试,仍然没有成功。因此,我把问题原因转移到了 curl 身上。

查看 curl 的源码后,发现 darwinssl.c 文件根本没有实现 SSL pinning 的逻辑。也就是说,在 iOS、OS X 等基于 Darwin 的平台上,的确不支持 SSL pinning。不过奇怪的是,其他 SSL backends 却是支持的,比如 OpenSSL 和 GnuTLS。

于是,我把 SSL pinning 移植到了 Darwin 平台。移植过程比较简单,因为有其他 SSL backends 的实现作为参照。

目前支持了 public key 这一种方式,已经满足需求了。

实现代码在 leancloud-curl 这个仓库中。

把 COSLayout 移植到 Cocos2D

前段时间接触了 Cocos2D-ObjC(以下简称 Cocos2D)这个 iOS 游戏框架。

在 Cocos2D 中,所有能在屏幕上绘制的对象都是 CCNode 对象。它在 Cocos2D 中的地位跟 UIView 在 Cocoa 中的地位一样。

Cocos2D 不支持 Auto Layout,因为 CCNode 没有继承 UIView。当我使用代码或 SpriteBuilder 布局一些静态场景时,发现很多地方都需要手动计算 CCNode 的大小和位置。感觉突然回到了石器时代。手动计算的过程对我来说太无聊了。

我突然想到了之前创造的 COSLayout。它可以用约束的方式来布局 UIView。不过,还是因为 CCNode 没有继承 UIView 的原因,COSLayout 不兼容 Cocos2D。于是,我打算把 COSLayout 移植到 Cocos2D 上。

COSLayout 的实现依赖于 UIView 提供的下列 API:

- (void)didMoveToSuperview;
- (void)layoutSubviews;

要想把 COSLayout 移植到 Cocos2D,CCNode 必须也要有类似的 API 才能满足移植条件。幸运的是,CCNode 提供了类似的 API:

- (void)setParent:(CCNode *)parent;
- (void)contentSizeChanged;

最终,COSLayout 顺利地移植到了 Cocos2D 上,即 CCSLayout

后来,我把 CCSLayout 发到了 Cocos2D 论坛里面,Cocos2D 的作者表示很有趣。

论坛链接

COSLayout 诞生记

COSLayout 是一个基于有向无环图(DAG)的 iOS 布局库。

它的诞生背景是:接手的项目没有用 Interface Builder,项目中遍地都是手动计算 frame 的代码。我想改进这些代码。

最初,我准备用 Auto Layout 来解决问题。但引入 Auto Layout 的 iOS 6 刚发布不久,app 可能还要继续兼容 iOS 5 较长一段时间。所以暂时不能用 Auto Layout。

在 iOS 5 中,虽然可以用 Autoresizing Masks 处理一些简单的布局,但遇到稍微复杂的布局,它就显得力不从心了。于是,我萌生了一个想法:把 Auto Layout 移植到 iOS 5 中。

经过一番调研,得知 Auto Layout 使用了 Cassowary 来求解约束方程。这意味着如果要实现 Auto Layout 的完整功能,必须使用 Cassowary 或类似的约束求解器。

虽然使用约束求解器可以完整复制 Auto Layout 的功能,但我还是选择了放弃。因为学习和使用它恐怕不是一时半会儿就能完成的事。花太多时间来复制 Auto Layout 不是一件很有价值的事。我想快速寻找一个简单的实现来改进既有代码。

我的目的是改进那些手动计算 frame 的代码。因此,是不是刚好可以用手动计算 frame 来代替约束求解?直觉告诉我这是可行的。

在手动计算 frame 的过程中,一个视图的 frame 通常会依赖其他视图的 frame,并且不存在循环依赖。需要一个数据结构来描述这种依赖关系。而这正是 DAG 所擅长的。

此时,心中已经有个大致雏形:提供一个类似于 Visual Format Language 的接口,内部用 DAG 来维护视图间的依赖关系。当某个视图的 frame 发生变化时,依赖该视图的其他视图的 frame 也会更新,并沿着 DAG 一直传播下去。

COSLayout 便是上述的一个实现。它可以像 Auto Layout 那样用约束来描述布局。例如,如果想创建一个视图,使得它占父视图的上半部分,可以这样写:

UIView *subview = [[UIView alloc] init];
[subview.cosLayout addRule:@"ll = rr = bb = 0, tt = 50%"];
[view addSubview:subview];

COSLayout 的确帮我改进了手动计算 frame 的代码。消除了那些繁琐的算术运算,代码也显得更加直观。

以上就是 COSLayout 的诞生过程。

可视化抽象语法树

抽象语法树(AST)在计算机科学领域有广泛的应用。

有时,你无法直接用眼睛观察它,只能想象它的结构。如果能以可视化的方式展现出来就好了。这样不仅便于分析和调试,也能让你对程序更有信心。

上周,我就遇到了这个问题。COSStyleParser 会把 CSS 解析成一个语法树,我想看看它到底长什么样子。虽然可以遍历语法树来打印出每个节点的信息,但要观察其树形结构就比较困难了。

这时,我想到了之前使用过的 GraphViz。GraphViz 是贝尔实验室研发的图可视化软件。它接受 DOT 格式的输入,然后将其渲染成图像。用它来观察语法树的结构再合适不过了。

因此,首先得生成语法树的 DOT 表示。好在 DOT 语言非常简单,对语法树进行一次遍历就能生成。

在 COSStyleParser 中,以下几个函数会把 CSS 语法树打印成 DOT 表示:

void COSStylePrintAstAsDot(COSStyleAST *astp) {
    printf("digraph G {\n");
    printf("node[shape=rect]\n");

    COSStylePrintAstNodes(astp);

    printf("}");
}

void COSStylePrintAstNodes(COSStyleAST *astp) {
    if (astp == NULL) return;

    printf("_%p[label=%s]\n", astp, COSStyleNodeTypeToStr(astp->nodeType));

    COSStyleAST *l = astp->l;
    COSStyleAST *r = astp->r;

    if (l != NULL) printf("_%p -> _%p\n", astp, l);
    if (r != NULL) printf("_%p -> _%p\n", astp, r);

    COSStylePrintAstNodes(l);
    COSStylePrintAstNodes(r);
}

char *COSStyleNodeTypeToStr(COSStyleNodeType nodeType) {
    switch (nodeType) {
    case COSStyleNodeTypeVal      : return "val";
    case COSStyleNodeTypeProp     : return "prop";
    case COSStyleNodeTypeDecl     : return "decl";
    case COSStyleNodeTypeDeclList : return "decllist";
    case COSStyleNodeTypeCls      : return "cls";
    case COSStyleNodeTypeClsList  : return "clslist";
    case COSStyleNodeTypeSel      : return "sel";
    case COSStyleNodeTypeSelList  : return "sellist";
    case COSStyleNodeTypeRule     : return "rule";
    case COSStyleNodeTypeRuleList : return "rulelist";
    case COSStyleNodeTypeSheet    : return "sheet";
    default: break;
    }

    return "undefined";
}

假设解析的 CSS 如下:

.foo.bar, .baz {
    left: 10;
    width: 100% - 20;
    background-color: red;
}

函数 COSStylePrintAstAsDot 的打印结果是:

digraph G {
node[shape=rect]
_0x7fb18277a050[label=sheet]
_0x7fb18277a050 -> _0x7fb18277a020
_0x7fb18277a020[label=rulelist]
_0x7fb18277a020 -> _0x7fb182779ff0
_0x7fb182779ff0[label=rule]
_0x7fb182779ff0 -> _0x7fb182779d30
_0x7fb182779ff0 -> _0x7fb182779fc0
_0x7fb182779d30[label=sellist]
_0x7fb182779d30 -> _0x7fb182779c70
_0x7fb182779d30 -> _0x7fb182779d00
_0x7fb182779c70[label=sellist]
_0x7fb182779c70 -> _0x7fb182779c40
_0x7fb182779c40[label=sel]
_0x7fb182779c40 -> _0x7fb182779c10
_0x7fb182779c10[label=clslist]
_0x7fb182779c10 -> _0x7fb182779bb0
_0x7fb182779c10 -> _0x7fb182779be0
_0x7fb182779bb0[label=clslist]
_0x7fb182779bb0 -> _0x7fb182779b80
_0x7fb182779b80[label=cls]
_0x7fb182779be0[label=cls]
_0x7fb182779d00[label=sel]
_0x7fb182779d00 -> _0x7fb182779cd0
_0x7fb182779cd0[label=clslist]
_0x7fb182779cd0 -> _0x7fb182779ca0
_0x7fb182779ca0[label=cls]
_0x7fb182779fc0[label=decllist]
_0x7fb182779fc0 -> _0x7fb182779f00
_0x7fb182779fc0 -> _0x7fb182779f90
_0x7fb182779f00[label=decllist]
_0x7fb182779f00 -> _0x7fb182779df0
_0x7fb182779f00 -> _0x7fb182779e90
_0x7fb182779df0[label=decllist]
_0x7fb182779df0 -> _0x7fb182779d90
_0x7fb182779d90[label=decl]
_0x7fb182779d90 -> _0x7fb182779d60
_0x7fb182779d90 -> _0x7fb182779dc0
_0x7fb182779d60[label=prop]
_0x7fb182779dc0[label=val]
_0x7fb182779e90[label=decl]
_0x7fb182779e90 -> _0x7fb182779e20
_0x7fb182779e90 -> _0x7fb182779ed0
_0x7fb182779e20[label=prop]
_0x7fb182779ed0[label=val]
_0x7fb182779f90[label=decl]
_0x7fb182779f90 -> _0x7fb182779f30
_0x7fb182779f90 -> _0x7fb182779f60
_0x7fb182779f30[label=prop]
_0x7fb182779f60[label=val]
}

最后,用 GraphViz 渲染以上 DOT 表示,即可得到下面这张图:

CSS-AST

使用 Lemon 实现 CSS 语法分析器

前段时间 CocoaSugar 项目需要一个 CSS 解析器。

我参考了一些开源实现,但都不尽人意。主要原因有:

  1. 项目太重,CocoaSugar 只要求支持 CSS 语法的一个子集;
  2. 命名空间,为了防止跟用户的依赖产生符号冲突,要做一些重命名工作。

因此,我只好自己实现一个 CSS 解析器。恰好前不久刚使用 FlexBison 实现过 SLL,自然想到用它们实现 CSS 解析器。

在好奇心的驱使下,我想看看有没有比 Flex 和 Bison 更好的替代品。搜索一阵发现了 Lemon。简单对比后,我决定用 Lemon 代替 Bison 来实现语法分析,词法分析则继续用 Flex。

坦白说,选择 Lemon 的主要原因是想尝试新的东西。不过,Lemon 的确有一些优点:首先,它生成的解析器是可重入的;而 Bison 默认生成的解析器是不可重入的,除非显式指定。另外,Lemon 还支持为 action 中的 token 指定占位符;而 Bison 只支持 $$$1 这样的位置参数,容易出错。

W3C 给出了 CSS 的语法,实现起来就非常轻松了。

代码放在了 COSStyleParser 这个仓库中。

让 CocoaPods 支持 Subproject

CocoaPods 是 Objective-C 项目的依赖管理器。

之前准备为一个现有的项目引入 CocoaPods 来管理第三方库,发现 CocoaPods 必须使用 workspace 管理依赖。但我想继续使用 xcodeproj,不想引入新的项目文件。

于是,我花了些时间让 CocoaPods 支持了 subproject。具体实现是 SubPod

扩展 Zen Coding 的 $ 表达式

Zen Coding 是一个可以帮助你快速编辑结构化代码(例如 HTML)的插件。

它支持许多文本编辑器和 IDE。但这里要说的是它的 Vim 插件,其 GitHub 地址是 zencoding-vim

我已经使用它一段时间了。它的确让我感觉到在 Vim 中编辑 HTML 不再是一件痛苦的事(当然,觉得痛苦可能是因为 Vim 水平有限)。

它提供了一个强大的文本替换引擎,能让你以最少的按键次数完成复杂的编辑任务。例如,如果想写一个 3 行 4 列的表格,只需输入 table>tr*3>td*4,再按一个快捷键让 Zen Coding 展开,就可以得到想要的结果了。

随着使用的增加,我发现它不仅仅擅长处理 HTML,同时还可以作为一个通用的 snippet 插件来使用,甚至取代 UltiSnips 的位置。因此,我创建了一些 Ruby、JavaScript 等语言的 snippets 来快速输入样板代码。

在创建 snippet 的过程中,我希望把缩进单元提取成一个变量。这样,调整缩进时只需修改这个变量就可以了,而不用单独修改各个 snippet。

然而,Vim 插件还不支持这个功能。之前使用过 Notepad++ 的插件,它支持通过 $ 表达式来引用一个变量。因此我就为 Vim 插件支持了这个功能。

由于忘记提 pull request,这个功能一直躺在我的 fork 中。直到有一天,Vim 插件的作者看到了我的 fork,终于合并了。相关链接有:

博客的备份与恢复

周末为博客增加了备份与恢复的功能,以后就不用担心数据丢失了。

有许多 WordPress 插件可以做这件事。起初,我使用了评价和下载量较高的 BackWPup。它可以把源文件和数据库 dump 文件打包成一个压缩文件,并上传到 FTP、Dropbox 等指定的地方。此外,它还支持自动备份。

虽然 BackWPup 把备份做得很出色,但遗憾的是,目前它只支持备份,不支持恢复。若要进行恢复,必须自行下载并解压备份文件,然后手动恢复源文件和数据库。操作非常繁琐。

于是,我打算写个脚本替我做这些繁琐的恢复工作。源文件的恢复很简单,就是简单的文件覆盖;数据库的恢复也不难,数据库的用户名和密码可以从 wp-config.php 文件中找到,这样就能轻松地把数据库 dump 文件导入到数据库中了。

当恢复脚本写完,我发现基于这个脚本,可以很快实现备份功能。因为备份只是把恢复反过来做一遍:首先生成数据库 dump 文件,然后把它和源文件放在一起打包即可。因此,我决定自己写脚本来做备份。这样一来,BackWPup 就没有用武之地了。

当备份脚本写完,又发现了新的问题:每次备份都会产生一个整站的拷贝。拷贝那些没有发生变化的源文件是没有意义的,这样不仅浪费磁盘空间,还不利于记录变更历史。而这些问题,正是版本控制所能解决的。

因此,我决定用 Git 管理源文件和数据库 dump 文件。备份时,先让脚本生成数据库 dump 文件,然后把它和源文件放在一起提交;恢复时,先 checkout 到某个 commit,然后用脚本把数据库 dump 文件导入到数据库中即可。

若想实现自动备份,可以添加一个 cron 定时任务。不过我还是喜欢手动备份,因为这样可以产生更有意义的 commit message。

以上就是目前本站的备份与恢复方案。