9 min read

Maria 开发记录

Maria Logo

Maria 是为 aria2 这款命令行下载软件定制的 Native App,开发 Maria 本身是不用写下载核心代码的。

这篇文章主要目的是分享我在开发过程中的一些经验以及遇到的一些坑,顺便期望着有人能快速看懂我的代码提出意见。

Aria2

我们先来说一下 Aria2 是什么。Aria2 是一款命令行下载软件,你可以在终端输入简单命令直接下载文件,而不用再非常麻烦的打开其他下载工具了。~~(其实我觉得一点也不麻烦)~~比如快速下载某个文件:

$ aria2c "https://moeoverflow.com/moe.gif"

我们通常所说的 Aria2,其实是指的 Aria2 RPC 模式,一种可以在后台运行并通过 WebSocket 通信的模式。由于有了这么一个特性,很多开发者开发出了各个网盘的浏览器插件,可以一键式的快速下载网盘内容,比如下载百度云盘的内容,不用再下载云管家(Mac 甚至没有客户端)来下载网盘内容。(这才是用这个软件的原因)

WebSocket

WebSocketHTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。WebSocket通讯协议于2011年被IETF定为标准RFC 6455,WebSocket APIW3C 定为标准。

在 WebSocket API中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。

原文链接:https://zh.wikipedia.org/wiki/WebSocket

Maria 目前的工作原理便是通过 WebSocket 来对 Aria2 RPC 进行操作。

Aria2 RPC

文档吐槽时间

讲到 Aria2 的 API,我必须要先吐槽一下官方的那一套文档了,比如这么一个 API

aria2.addUri([secret, ]uris[, options[, position]])

excuse me ?? 嵌套中括号??逗号??

这奇葩的中括号以及里面的逗号,浪费了我不少的时间。

API 方法里面有 pause , unpause(喵喵喵????) 方法,这两个方法只能暂停下载和继续下载,但是我却没找到当下载为 Error 或者 Stopped 状态时候的重新下载方法??

WTF ?? 然后我去问了一下作者,他就这么很淡定的告诉我不能重启已停止的下载任务???

API and Framework

好吧我还是正经的讲讲这套 API 的使用吧。

首先用 Swift Enum 映射 Method List

public enum Aria2Method: String {
    case addUri
    case getGlobalStat
    case tellActive
    ... // 省略 N 行
    case onBtDownloadComplete
    case onDownloadError
}

可以通过 AddUri 添加 HTTP(s) 下载、磁力链接下载或者是 ed2k 下载,官方文档说的是可以一次传入多个链接同时创建多个任务,但是我实际测试发现似乎这种方式无效,Aria2 只会对第一个链接生效,所以想要同时下载多个链接需要同时发多次请求。

通过 AddTorrent 添加 BT 下载。

Aria2 把当前有的任务分为了 activewaitingpausederrorcompleteremoved 6 种状态;
通过 tellActive 获取 处于 active 状态的任务,(可通过 pausepauseAll 方法暂停)
通过 tellWaiting 获取 处于 pausedwaiting 状态的任务,(可通过 unpauseunpauseAll 方法继续下载)
通过 tellStopped 获取 处于 errorcompleteremoved状态的任务。

如我之前吐槽的那样,如果任务出错或者停止了,将没有方便的办法重启下载。因此直到 Maria 0.8.3 都还没有重启已停止任务的功能。

还有一组通知方法,当 Aria2 的下载任务更改状态的时候,就会自动向 Client 发送通知。

onDownloadStart onDownloadPause onDownloadStopped onDownloadComplete onDownloadError onBtDownloadComplete

Maria 的系统通知就是靠的这一组 API 工作的。

private func request(method method: Aria2Method, params: String) {
    let socketString = "{\"jsonrpc\": \"2.0\", \"id\": \"\(method.rawValue)\", \"method\":\"aria2.\(method.rawValue)\",\"params\":[\"token:\(secret)\", \(params)]}"
    let data: NSData = socketString.dataUsingEncoding(NSUTF8StringEncoding)!
    self.socket.writeData(data)
}

在 WebSocket 通信中,是通过参数 id 来判断 Request 和 Response 的对应关系的,因为可能会出现这样的情况:

几乎同时的发出两个 Request ,但是收到 Response 的时候也许后一个请求的 Response 先返回,这样就会造成混乱。

理论上来讲,每次发送请求的时候都需要随机生成一串唯一识别码,附带到 id 参数里面,这样才能区分不同请求。

但是再开发 Maria 的过程中,我并没有加上唯一识别码,因为考虑到每次获取的任务状态并不需要考虑对应关系,而实际的开发测试也并没有遇到类似问题。

Maria Today Widget

一开始其实是只准备做一个通知中心小插件来着,结果一不小心把整个软件写出来了。 (:3_ヽ)_

当你在 Xcode 创建好一个 Today Widget Target 的时候,会默认提供一个方法

func widgetPerformUpdateWithCompletionHandler(completionHandler: ((NCUpdateResult) -> Void)!) {
}

系统会定期自动的更新 Today Widget Content,但是 Maria 需要的是实时显示上传下载速度以及任务进度信息,所以我直接开了一个时间间隔为 (+)1s 的循环 NSTimer 来实时的显示信息。

Main App

项目结构

这个项目里把 aria2 部分全部放进了一个 Framework,这样可供 Maria 和 Today Widget 共同使用,减少重复代码。

但是也带来一个问题,似乎 Framework 太大了导致 Widget 启动缓慢?

NSToolbar

开发这个 Maria 的时候我是第一次接触 Cocoa,当时做这个 NSToolbar 的时候浪费了不少时间。

并没有什么 Button Toolbar Item ,直接就可以把 NSButton 拖放到 工具栏里面,当时才开始开发 Cocoa 的时候我一度以为例如 Pages 或者 Keynotes 工具栏上面的按钮全部是通过 Image Toolbar Item

NSToolbar 一个是可以用做工具栏,另一个用法是可以作为 TabViewController 的类型。(详见 Settings.storyboard

还有一点需要注意的是,如果你以 Modal Segue 的方式显示一个 Window,是不会显示 Toolbar 的,必须是 Show Segue 类型才行。

NSTableView

NSTableView 由 Content Mode 来确定显示内容的方式,不仅可以像 UITableView 一样显示 Custom Cell View View Based,也可以以表格的形式显示简单类型的数据**Cell Based**。

假如你有一个 TableCellView.xib 文件,就可以通过以下方式在 TableView 中使用:

let nib = NSNib(nibNamed: "TaskCellView", bundle: NSBundle.mainBundle())
// 为 TableView 注册 NIB 文件
tableView.registerNib(nib!, forIdentifier: "TaskCell")

func tableView(tableView: NSTableView, viewForTableColumn tableColumn: NSTableColumn?, row: Int) -> NSView? {
	let cell = tableView.makeViewWithIdentifier("TaskCell", owner: self) as! TaskCellView
	// cell data
    return cell
}

再说一个疑似 Cocoa 的 Bug:

scrollview 如果关掉了 H/V Scroller,然后 tableview cell 的总高度如果超过了窗口的高度,这时候就无法用触摸板向下滑动了,开启 Vertical Scroller 之后就正常了。

NSTask - Shell

其实是已经做好了 aria2 随 Maria 启动运行的,但是似乎不太稳定?暂时关掉了这部分功能。

详见 Cocoa dev 运行 shell 命令

App Groups

因为 Maria 是通过 NSUserDefaults 来存储配置信息的,所以 App 和 Widget 共用一个 App Groups 来同步配置信息。

使用方法很简单,在两个 Targets Capabilities 里面都打开 App Groups 并设置 ID,然后代码引用就是了:

let defaults = NSUserDefaults(suiteName: "group.<bundle identifier>.<bundle name>")!

图标素材

矢量图 PDF

这个项目的大部分图标素材都是使用的矢量图(PDF 格式)(虽然到最后 Xcode 还是会自动转格式),当然我也推荐大家都尽量的使用矢量图

imageassets 添加一个 PDF 文件即可,不需要再手动的去生成三种尺寸的图片。

在 Xcode 使用 PDF 需要注意的是,图片尺寸需和显示尺寸一致。矢量图虽说可以无级缩放,但是如果你在 ImageView 里引用一个 1000px * 1000px 的 PDF ,你会发现显示会有问题的,也就是说,你如果要在一个 30px * 30pxImageView 里显示一个图标,那么向 imageassets 里添加的 PDF 文件尺寸也必须是 30px * 30px

我个人猜测 Xcode 使用 PDF 矢量图只是会帮你把这个矢量图图标自动转为 @1x@2x@3x 三种尺寸的像素图。

应该是在编译的时候就自动裁切好需要用到的像素图,而后不管你用什么 ImageView ,不管你怎么改变 View 的大小,引用实际上还是同一个图片。

也不知道什么时候 iOS 能支持直接 SVG 动态解析。

关于图标

Maria Logo

已经有不下 5 个人在吐槽我自己画的这个图标了,我就想问,这图标就这么丑吗?(:3_ヽ)_

因为还没有更合适的图标,所以会暂时用着这个图标。(嗯,我觉得挺好看的呀) (´・ω・`)

有人说,你就随便画个圆里面加上一个字母 M ,然后 Duang Duang 地加上渐变背景阴影不就好啦。

NO!!!!!! 我个人觉得,这样的图标不如不要。

素材源

至于图标素材的来源,我个人推荐一个站 FLATICON ,有很多的矢量图素材,而且是不强制收费模式。(虽然大部分都不是彩色的,不过也足够用了)

[持续更新]

执一

2016 年 5 月 30 日