深入理解 iOS App 开发过程中的数字证书和授权文件以及签名过程

在做 iOS 开发的过程中,我们不免都碰到过跟证书相关的种种问题,而这些问题我相信很多人都是比较懵的,即便我们很多时候知道了具体怎么去做可以让我们能成功的编译某个工程并且打包发布到 AppStore 上去,但是我想其实我们还是有很多的东西需要去挖一挖的。

为什么要数字证书?

做个 App 又不是念大学或者考职称对吧,要啥证书?你咋不要自行车呢?不过我们回过头来看看,证书这个东西实际上并非什么新鲜的东西。我想大家在 Windows 上都安装过很多应用程序对吧,大概是从 Windows 7 开始,有些安装程序在开始安装之前,我们会看到系统会弹出一个提示框,提示这个应用程序是哪家公司开发的,我们是否确定要安装,点击确定之后才会开始真正的程序安装,对吧。

那么 Windows 怎么知道这个应用是谁开发的呢?难道我们自己在安装程序里头直接写明是谁开发的就好了吗?是这么回事,也不是这么回事。如果大家随便说这个程序是谁谁谁开发的,那么做恶意软件的人就可以随意将自己的木马程序伪装成任何一家有公信力的大公司了,例如微软自己等等。这显然是不可接受的,为了让这个机制正常运作起来,就有了一个公开的可信的签名和验证机制,大家按照这个机制的规则来,就可以避免出现伪装的这种情况了。

这个数字签名的机制保证了 Windows 可以在完全不知晓你要安装的具体是什么软件,即便这个软件是在我们使用的 Windows 系统发布好多年之后才发布的一款软件,只要这款软件的安装程序按照这个签名的规则对其自身的二进制安装文件进行签名,那么 Windows 就可以根据规则将其签名信息读出来,然后展示给最终用户也就是我们,然后再将判断的主动权交给我们,让我们自行判断你要安装的这款软件是否是出自这家公司的,如果是当然最好了,果断确认安装即可,如果不是,那么就要小心这个是不是恶意软件开发商搞的鬼了。搞得这么麻烦,究竟是为了神马捏?这个成本显然还是蛮高的嘛。

一切都是为了信息安全

在信息社会里头,数字安全一直都是一个很严肃但又时常被轻视的话题,由于数字信息传播的高速性,所以一个小小的安全问题往往可以被放大很多很多倍。为了安全,就会有各种加密手段来帮助我们达成信息安全的这个目的。在这里我们用人与人之间通信的这个过程来作为例子来简单地看看我们是如何通过加密来保证我们之间传送的信息是安全的。可以先从最简单最原始的人与人之间的通信开始,例如小明给小黄发了一封信,内容是「xxxxxx」,小黄收到的信内容也就是「xxxxxx」,这就是最原始的明文通信了。任何接触到这封信的人都是可以偷偷地阅读这封信的内容的,如下图:

原始的通信方式

原始的通信方式

那么为了防止别人随意偷看信件的内容,或者即便被人偷看了,也让人压根看不懂,就有了加密这种手段啊来确保只有通信双方能看明白信件的内容,其他的人即便看到了加密之后的信件内容也不会造成信息泄漏,最简单的加密情况是这样的,也称之为对称加密(相对于后面我们后头会讲到的非对称加密来说)。

对称加密的通信方式

对称加密的通信方式

在这种通信方式中的通信双方协商好一种加密和解密方法,并且必须持有同一个密钥才可以完成整个加解密的流程,而现实中这种方式会带来各种不便和其他的安全隐患问题,例如密码又该如何先给到通信的目标方,密码传送的过程中又如何保证其安全性,通信双方都持有同一个密码后无法区分双方的身份等等。所以就又了更为高级和方便的加密方法,就是非对称加密。

非对称加密的通信方式

非对称加密的通信方式

在这个通信方式里头的非对称加密方法已经可以做到让加密和解密的密钥是分离的,这样一来在发送方保存好自己的私钥不被泄漏的情况下,可以把公钥给到所有需要与他通信的目标方手里,就不会出现对称加密中收信方可以直接拿着与发信方共享的密钥去伪装成发信方的情况了。但是这个方案中还存在其他的问题,例如如何获取发信方的公钥以及如何确认获取到的发信方的公钥是可信的。这个时候数字证书就应运而生了。

关于密码学相关的内容,建议大家参考一下阮一峰写的几篇博客:

密码学笔记

RSA算法原理(一)

RSA算法原理(二)

图解SSL/TLS协议

数字签名是什么?

数字证书和数字签名是怎么运作的?

那么这个数字签名机制究竟是怎么运作的呢?下图就是一个证书用户如何创建并应用数字证书的流程:

数字证书的应用流程

数字证书的应用流程

图中的数字证书的申请和应用流程大体上就是:数字证书用户(开发者)创建密钥对 => 生成 CSR 文件 => 向 CA 申请数字证书文件 => CA 审核证书申请者提供的各项信息合法性 => CA 使用自身的私钥生成一个加密的数字证书文件 => 将数字证书发送给证书申请者(开发者)=> 证书申请者(开发者)收到证书之后,使用证书对需要分发的文件进行签名 => 将签名后的文件分发给最终用户。

我们还可以看看一个数字证书里头都包含着哪些信息和数据:

数字证书内容结构图

数字证书内容结构图

从上图中我们看到证书文件中包含了证书拥有者的公钥信息和 CA 机构的签名信息,以及相关的身份标识信息,通过这些信息我们就可以达成通过数字证书来验证被签名的内容是否可信了。

那么证书申请者在获得到证书之后又怎么做数字签名呢?下图就是一个对数字内容进行数字签名的流程:

生成数字签名的流程图

生成数字签名的流程图

那么数字签名生成之后,附加到了某个文件上分发给了最终的接收者,收到这个文件的人又是如何验证这个数字签名的合法性呢?下图就是一个数字签名验证的流程:

验证数字签名的流程图

验证数字签名的流程图

在了解完数字证书相关的基础知识之后,我们终于可以去揭开苹果开发中这个繁复的证书创建和应用的流程的面纱了,看看这货究竟要怎么搞才是对的,以及为什么需要这么搞才是对的。

iOS 应用的整个签名和验证的流程

既然我们已经铺垫了这么多,那么 iOS 应用开发涉及到的应用的签名,以及 iOS 设备又是如何验证设备上安装的应用的签名是否可信,然后允许安装这些 iOS 应用的呢?通过下面这张流程图,我们来看看 Apple 是如何设计其应用签名流程的。

iOS 应用签名的流程示意图

iOS 应用签名的流程示意图

在整个开发 APP 签名的流程中,我们只需要抓住三个文件,就可以完全 Hold 住整个签名打包的过程了:

  1. 创建 CSR 文件,在 macOS 上通过 Keychain Access 创建 CSR 文件的同时,实际上这货会同时帮你创建好对应该 CSR 的 RSA Key Pair 文件(这些密钥文件 Keychain Access 会自动帮你保存,后续编译打包的 Xcode 也都是直接通过 Keychain Access 来获取相关的密钥信息的);
  2. 上传 CSR 文件,创建 Certificate 文件,下载 Certificate 文件并导入到 Keychain Access 中,针对指定的 Bundle Identifier 和 Certificate 文件创建 Provisioning Profile 文件,下载并导入;
  3. 使用 Certificate 文件对应用进行签名,将 Provisioning Profile 文件打包到应用中。

然后我们针对以上这三个文件来看看它们是如何与上面我们叨逼叨了一大堆的数字签名流程对应的,这样我们在了解了数字签名的通用流程之后,再来看看 Apple 是如何在 iOS 应用的开发流程中应用这一通用的应用签名和验证技术的,如此印证一番,更能加深我们对数字签名远离的理解。

  • 创建 RSA Key Pair 和 CSR 文件(Keychain Access 自动完成了创建 RSA Key Pair 文件的过程);
  • 通过 CSR 文件请求 CA 颁发 Certificate 文件(由于 Apple 的整个生态系统相对封闭,整个申请证书的流程相对就简单并高效,只需要通过 Apple 提供的一系列工具和 Developer 后台即可完成,这个对于安全性的掌控非常的重要,对 Apple、开发者和用户三方来说,都尤为重要。正因为有了这一个封闭而又完整的系统,开发者可以将精力放在研发优质内容上毋需为了内容被盗版多费心力,用户也大可放心的安装 App Store 中的应用毋需过于担心被恶意软件或病毒骚扰,Apple 对于开发者和 APP 拥有绝对的生杀大权使得开发者不敢轻易挑战其底线。当然这个话题有点大,不展开了,况且咱对安全也不太专业,这仅仅是 Apple 对 iOS 系统安全控制所做的努力中极为基础和微小的一部分,咱别夸大了,😄);
  • 成功编译 APP 的工程后,使用 Certificate 文件对 APP 的文件进行数字签名;

    在这个过程中,Xcode 会在 APP 最终的目录下创建一个名为 _CodeSignature 的目录,在该目录下会有一个名为 CodeResources 的 plist 文件:

    iOS APP 数字签名文件目录

    iOS APP 数字签名文件目录

    这个 CodeResources 文件的内容如下:

    这个 plist 文件中会将整个 APP 中使用到的可执行文件以及资源文件的签名 Hash 值都罗列出来(当然有些文件是可以配置为忽略签名验证的,例如 CodeResources 文件,防止出现鸡生蛋蛋生鸡死循环的验证链死锁问题),文件中的 Hash 值就是使用我们在创建 CSR 文件时自动创建的 RSA Key Pair 中的私钥,通过某个 Hash 算法计算出来的。iOS 设备在安装一个 APP 的时候,会自动读取 APP 中 _CodeSignature 目录下 CodeResources 文件中的所有内容,然后使用随 APP 一并打包到 APP 内的 Provisioning Profile 文件中的 Certificate 通过同一个指定的 Hash 算法对 APP 包内所有文件进行 Hash 计算,然后与 CodeResources 文件中的 Hash 值进行比对,如果出现 Hash 值不一致的,签名验证失败,该 APP 肯定是无法正确安装运行的。

  • 基于已经创建的 Certificate 文件生成 Provisioning Profile 文件,这是 iOS 开发中特有的一个文件,该文件针对 iOS 应用的分发方式不同而有所区别,一般有三种类型:
    1. Development,用于开发者将正在开发中 APP 安装到在 Apple Developer 中注册的 iOS 测试设备上;
    2. Ad Hoc,这个实际上跟 Development 没有太大的区别,也是用于将 APP 部署到已注册的 iOS 测试设备上,不过使用这个 Provisioning Profile 打包的 ipa 可以通过 OTA 方式直接安装,不需要从物理层面上拿着测试设备找开发者来安装 APP(实际上使用 Development 授权文件打包的 ipa 也可以安装到 iOS 设备上,不过得先拿到 ipa 文件);
    3. App Store,这就是最终提交 APP 到 App Store 中时打包必须使用的 Provisioning Profile 了,在我们上传 APP 到 App Store 的过程中,Apple 会自动验证当前上传的 APP 是否使用了正确的 Provisioning Profile 打包的。

      Apple 通过这一额外的 Provisioning Profile(授权文件)机制很好地实现了在不同场景下的应用分发控制,由于 Development 和 Ad Hoc 都有明显的安装设备数限制(100台设备),而且需要事先将目标安装设备的 Device Identifier 注册到 Apple Developer,这就意味着开发者如果想让自己的 APP 安装到大量用户的 iOS 设备上,只能选择使用 App Store Provisioning Profile 打包自己的 APP 然后上传到 App Store(当然实际上绝大部分用户也都是通过 App Store 来寻找并安装 APP,这两者并没有什么直接的因果关系,只是 Apple 的设计使然)。

      所有的授权文件都包含了以下的信息:

      1. 用于签名的证书,授权文件中包含有用来给该 APP 签名的数字证书的内容,如下图:

      iOS Provisioning Profile 文件中包含了数字证书的内容

      iOS Provisioning Profile 文件中包含了数字证书的内容

      2. APP 的 Bundle Identifier(该授权文件只能随指定包名的 APP 一起打包进行分发);

      3. 分发方式:Development、Ad Hoc、App Store、Enterprise 中的某一个(指明该 APP 的分发方式,iOS 设备在安装和启动时会根据授权文件声明的不同分发方式,进行不同的处理,例如使用 Enterprise 方式分发的应用,Apple 可以随时吊销该应用的授权文件,授权文件被吊销之后就无法再正常启动了,正常情况下,这类 APP 启动之前,iOS 会弹出一个非常明确的提示框告知用户当前正在使用一个由开发者 XX 开发并使用企业授权文件分发的 APP,用户可以选择信任该开发者或者不信任,用户确认信任该开发者的授权文件后,APP 才能正常启动,如果是 Development 和 Ad Hoc 的授权文件,那么在安装的时候 iOS 系统就会先确认该 APP 中的授权文件中是否包含当前安装的目标设备的 Identifier,如果不在其列,该 APP 是无法成功安装到目标设备上的);

      4. 应用的能力(Entitlements),声明该 APP 能否使用 Apple Push Service 等 Apple 提供的公共服务,以及该 APP 与同一开发者发布的 APP 直接协作的权限等等;

      5. 应用安装的有效日期,声明使用该授权文件打包的 APP 的过期时间,过期后该 APP 无法再被安装到 iOS 设备上。

iOS 开发中的数字证书配置流程和使用方法

iOS 开发过程中大家都绕不过去的就是这个证书和授权文件的配置,生成和使用问题,我们希望能在这片文章里头把这件事情给搞清楚了。

通常我们会怎们做?

相信接触过iOS开发的童鞋们都已经很了解以下的这些步骤了,不过我们既然要把这个问题搞透彻一些,那么我们就还是假装我们自己神马都不会吧,从零开始,我们一步步地操作一下,看看究竟如何才能编译一个应用并且能把这应用装到我们的设备上呢。

创建一个 App 工程

关于如何创建一个工程,这个不是我们这篇文章所要聊的,所以这里不展开聊如何使用 Xcode 进行 App 的开发了,这也不是我所擅长的更不能瞎说。

好了,言归正传,我们先看看下面这两个截图。
这是未设置开发者账号的 Xcode 工程的基本设置信息页面:

未设置开发者账号的 Xcode 工程基本设置页面

未设置开发者账号的 Xcode 工程基本设置页面

这是设置为我个人账号的 Xcode 工程基本设置信息页面:

设置为不可用的个人账号的 Xcode 工程基本设置信息页面

设置为不可用的个人账号的 Xcode 工程基本设置信息页面

这是设置为可用的企业开发者账号的 Xcode 工程基本设置信息页面:

正确设置了开发者账号的 Xcode 工程基本设置页面

正确设置了开发者账号的 Xcode 工程基本设置页面

大家可以看到第二个截图中,我使用的 Team 账号是我个人的帐号,而第三个截图中我使用的是公司的开发者帐号,这有什么区别呢?

  1. 帐号的主体不一样,申请的流程也不一样,如果是个人帐号的话,通常我们只需要有一个 Apple Id,然后登录 http://developer.apple.com 网站注册开发者服务即可,然后选择 iOS Developer Program 进行付费购买即可,而企业帐号就必须有一个企业组织的邓白氏码(这货是有一个专门的第三方机构在做,目前国内已经有官方代理商,通常办理都是免费的,只需要提交信息就可以获得一个邓白氏码,一般是申请成功后,15 个工作日后方可在苹果的 Developer 网站使用,不过这个数值不太准确,这个应该是最保守的估计。),有了可用的邓白氏码之后就可以直接付费开通 iOS Developer Program 了,开通服务器以后,实际上在 Developer 站点上个人帐号跟企业帐号之间应该是没有什么太大区别的,主要都是用来管理 App 的 Bunder Indentifier,开发者的证书文件和开发设备列表,以及各种授权文件了,当然还有用户管理啦,毕竟咱们现在开发任何一个 App 通常都是需要多人协作来完成的,所以在这个里头也可以完成开发者的邀请,开发者权限的管理等等操作。
  2. 实际上在完成了开发阶段之后,我们还需要使用这个开发者帐号登录到一个叫 iTunes Connect 的站点中,进行 App 的发布前的提交工作,提交 App 的名字、描述、截图、售价以及内购支付相关的诸多选项,所有提交需要的信息都准备完毕之后,我们就可以着手在 Xcode 中将打包好的 App 提交到 iTunes Connect 上了,提交完毕之后就等审核了,审核通过之后就可以在 App Store 中正式发布我们的 App 了,这样用户就能下载到我们的 App 了。那么在支付相关的选项上,个人帐号和企业帐号之间应该是有区别的,个人帐号我们只需要提供一个个人的银行卡即可,而企业可能需要提供的信息就更多了,包括银行开户信息,企业工商和税务的信息可能都需要酌情提交,由于这个事情之前处理的事情有点久远了,已经忘得差不多了,有机会再补充吧。本质上结算应该也不会有什么区别,最终都是苹果按照一定账期定期会结算并汇款到指定的银行账户,记得应该是两个月的账期。

好了,不扯远了,既然第一个 App 没有正确地设置授权文件,那么我们尝试着点一下那个 Fix Issue 按钮看看行不行呗,遗憾的是不好使,因为我这个个人帐号是没有购买 iOS Developer Program 服务的,也就是说我只能开发 App,但是不能发布 App,如果需要发布的话,还是必须购买 iOS Developer Program的,之前有看到报道说苹果开放了 Developer 的资源,貌似现在不需要购买 iOS Developer Program 也能开发 App 并且可以安装到自己的设备上,但是实际测试的结果并非如此,从👆上面第二张截图中来看,这个方法貌似并不可行。

请注意:以上三张截图中,我们都可以清楚地看到勾选了一个 Automatically manage signing 选项,勾选这个选项之后,我们就可以完全将创建 Bundle Identifier,生成 Certificate 文件,创建 Provisioning Profile 文件这些无聊繁琐而又很容易把人搞懵逼的事情委托给 Xcode 了,Xcode 会在 Build 之前帮我们把所有需要做好的事情都做好。在 Xcode 8 某个版本之前(原谅我无法记住这些玩意儿),Automatically manage signing 还不存在的时候,实际上是有一个叫 Fix Issue 的按钮的,那个时候如果为 Xcode 工程设置好了某个 Development Team 账号之后,Xcode 无法在本机找到可用的对应当前 Xcode 工程的 Bundle Identifier 的相关的数字证书(Certificate)和授权文件(Provisioning Profile )的话,会提供一个快捷修复的按钮给开发者的。咱们来看个之前版本的项目设置页面的截图吧:

Xcode未提供自动托管APP签名之前的设置页面

Xcode未提供自动托管APP签名之前的设置页面

虽然现在不再有 Fix Issue 按钮了,但是我们也可以了解一下,这个 Fix Issue 按钮究竟会干些什么。

按照直觉来讲,Xcode 是不是应该先在 Developer 中新建一个 App Bundle Indentifier,正如我们截图中的这个 App 的 Bundle Indentifier —— “com.laputa.playcard”,然后再在 Developer 站点上创建数字证书(如果需要的话),生成授权文件呢?嗯,我们的直觉非常的准确。作为一个好程序员,我一直认为这种直觉来源于不断地进行逻辑和程序化的思维训练,这种逻辑和程序化的思维是很容易训练和形成的,而且非常必要,思考问题时我们要先站到设计者或者实现者的角度上去看,看看如果是我们自己来设计或者实现的时候会按照什么样的套路来做,最后再跟对方的设计和实现验证一番,慢慢地我们会发现实际上大部分的设计和实现真的跟我们之前的直觉是一致的,或者说大体上会保持一致,这是一种很好的思维训练,而且会很利于我们更深入地去理解对方的设计和实现的意图和思路。

实际上点击 Fix Issue 按钮之后,Xcode 会默默地帮我们做以下的一些事情:

Xcode Fix Issue 流程示意图

Xcode Fix Issue 流程示意图

不过这个 Fix Issue 按钮已经不见了,所以现在我们没法到 Apple Developer 后台一一再次验证一下是否会创建对应的文件了,有点遗憾,所以我们可以暂时先把它给忘了。在最新的 Xcode 8 中,Xcode 虽说会自动帮我们管理签名相关的东西,但实际上原理应该是一致的,只是 Xcode 与 Apple Developer 后台服务交互的所有过程,以及整个过程中创建的所有文件都给隐藏起来了(我想这可能是防止有人在 Developer 后台误操作导致出现一些不必要的问题而设计的),Xcode 依然会通过上面截图中的流程在后台默默地帮我们把这些事情给做了。

那我们到 Apple Developer 后台去看一看吧。
Developer 中关于 App IDs 设置的页面:

Developer 中关于 App IDs 设置的页面

Developer 中关于 App IDs 设置的页面

Developer 中关于证书的设置页面:

Developer 中关于证书的设置页面

Developer 中关于证书的设置页面

Developer 中关于授权文件的设置页面:

Developer 中关于授权文件的设置页面

Developer 中关于授权文件的设置页面

从这三个截图中来看,实际上我们并没有看到任何跟 “com.laputa.playcard” 相关的东西,Xcode 8 的 Automatically manage signing 真的是完全不留痕迹,有木有?

有了 Xcode 8 的 Automatically manage signing 功能选项,实际上这篇文章看起来就有点多余了。你说你讲这么多对我有个毛的用捏,是伐?伦家 Xcode 已经帮你搞得妥妥的了,干嘛要给自己找不开森呢?你死磕这玩意儿有意思吗?

有,太有了。如果我们只需要使用 Xcode 进行简单的编码和编译打包的话,那么止步于此并没有啥问题,但是我们是程序猿啊,有木有?我们是一群懒人啊,有木有?我们肯定会利用持续集成服务来帮我们省去每次需要自己手动编译打包这种无聊重复工作浪费的时间啊,有木有?而且还有 Unity 3D 这个奇怪的,每次打包 iOS 包都需要借助 Xcode 中间工程的鬼噢,难道说每次等待 Unity Editor 编译出 iOS 工程,再手动打包 APP 不蛋疼吗?

好吧,那么我们需要持续集成来帮我们缓解这个蛋疼,有木有?而 iOS 工程的持续集成就需要咱们尽可能多地去了解 iOS APP 整个的签名和打包的机制了,掌握了整个 iOS APP 签名打包的机制和流程之后,在配置 iOS 工程持续集成时就能做到游刃有余。

下一次,我们再来好好分享一下,如果使用 Jenkins 持续集成 Unity3D 工程编译打包 Android 和 iOS 平台的安装包。

参考资料

密码学笔记

RSA算法原理(一)

RSA算法原理(二)

图解SSL/TLS协议

数字签名是什么?

Migrating Code Signing Configurations to Xcode 8

macOS 安装配置 Jenkins 持续集成 Unity 项目指南

安装 Java 和 JDK

由于 Jenkins 是一个基于 Java 实现的项目,所以我们需要先确保我们的 macOS 上安装有 Java,直接从 Oracle 的官方网站上下载安装即可。

安装完成之后,理论上咱们就可以安装 Jenkins 了,但是考虑到咱们实际上使用 Jenkins 来进行持续集成的目的是让其自动将 Unity 工程打包成 Android 平台和 iOS 平台的安装包,而 Android 工程的编译又依赖于 JDK,所以我们还需要安装一个 JDK,同理也可以直接从 Orcale 的官方网站上下载后安装。

安装完成之后在命令行终端中输入一下 java -version 命令,输出内容类似:

java version “1.8.0_91”
Java(TM) SE Runtime Environment (build 1.8.0_91-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.91-b14, mixed mode)

安装 Jenkins

从 Jenkins 官网下载适用 macOS(截止写这篇文章的时候,Jenkins 官网上依然写的是 Mac OS X)的 pkg 文件,我下载的是 LTS 版本 2.19.2。双击一步一步安装完成即可,安装完成之后执行一下 defaults read /Library/Preferences/org.jenkins-ci 命令,输出内容如下:

{
“JENKINS_HOME” = “/Users/Shared/Jenkins/Home”;
heapSize = 512m;
httpListenAddress = “192.168.200.99”;
httpPort = 8080;
minHeapSize = 256m;
minPermGen = 256m;
permGen = 512m;
tmpdir = “/Users/Shared/Jenkins/tmp”;
}

这个就是 Jenkins 的各项启动参数,从以上输出的参数就得到了 Jenkins 的后台管理页面的地址:http://192.168.200.99:8080 ,在浏览器中输入该地址之后应该能看到如下的页面:
jenkins_dashboard

当然默认肯定是没有任何构建项目的,截图中已有的 3 个项目是我添加的,到这一步就意味着我们已经将 Jenkins 安装好了,而且使用 pkg 文件安装的方式会帮我们把 macOS 开机自启动 Jenkins 服务的工作都做好,省去了自己去折腾 LaunchDaemons 相关的各种配置的麻烦,不过也有其他的问题,咱们一个个来解决。接下来,我们需要安装一些插件,或者更新一些插件。

安装 Git 插件

通过 Jenkins Dashboard 页面中系统管理菜单项进入 Jenkins 管理页面,点击管理插件选项即可进入插件管理页面,找到在右上方的过滤输入栏中输入 Git 筛选一下跟 Git 相关的插件,找到自己需要的插件勾选,然后点击安装即可,等插件下载安装完成之后,按照页面的提示可以重启一下 Jenkins 让这些新安装的插件生效。

设置 Credentials

由于我们团队使用 Git 对 Unity 项目进行版本管理,所以我们需要设置一个 Credentials 能让 Jenkins 中的 Git 插件可以用来与 Git 服务器进行验证和交互。还是通过 Jenkins Dashboard 页面的 Credentials 菜单项进入管理页面,然后新建一个 Credentials,我选择的是 SSH Username with private key 的方式,输入 Username(例如安装 Jenkins 的机器当前登录的用户名为 developer,那么就填入 developer),Private Key 选择Enter directly(因为其他的两个方式不知道 Jenkins master 具体指的是啥,也没有帮助没有设置成功,最终只能选择直接输入 private key 的方式了),确保当前登录的用户目录下有 .ssh 目录并且已经有 id_rsa 和 id_rsa.pub 文件(如果没有的话,通过 ssh-keygen 生成一个 SSH 的密钥对吧),然后在终端中执行一下 cat ~/.ssh/id_rsa 命令,把输出的内容全部拷贝,粘贴到 Jenkins 的 Private Key 设置输入框中,保存即可。

注意:~/.ssh 目录下的 id_rsa.pub 应该已经被添加到 Git 服务器信任的公钥列表中,例如我们团队中使用的是自己搭建的 GitLab 服务,那么就需要将 id_rsa.pub 中的内容通过 Profile Settings -> SSH Keys 设置页面,将这个 SSH Key 添加到账户的 Profile 中,这样 Jenkins 在使用 Git 拉取代码的时候才能验证成功。

修改用于 Jenkins 服务启动的用户和组

由于 Unity Editor 的限制,如果要在 Jenkins 中调用 Unity Editor 提供的命令行执行 BuildPlayer 方法来完成构建任务的话,必须要通过用户名和密码登入 macOS 系统后再执行构建任务才能正常调用 Unity Editor 的命令行,否则整个构建任务会一直 Hang 住。

这个是在经历了各种各样的坑之后才总结出来的,例如我曾经经历过 Mac 电脑自动更新后重启,未主动输入用户密码登录系统,通过浏览器可以其他电脑上访问 Jenkins Dashboard 并且能查看到所有构建任务的状态,可是所有的构建任务永远都不会完成。这个就是因为 Jenkins 默认在系统启动之后就自动启动了,感谢 LaunchDaemons,所以可以访问 Jenkins Dashboard 页面。但是由于未成功登录账号,Unity Editor 无法正常执行其提供的命令行,最终所有的构建任务全 Hang 住了,通过 GUI 登入用户账号之后恢复正常(通过 Remote Desktop 输入用户的密码登入也行,但是必须登入用户账号)。

这个问题我的理解是这样的,由于 Unity Editor 是在当前登入的用户(developer:wheel)下安装的,所以通过 Jenkins 调用 Unity Editor 的命令行也需要使用当前登入的用户来调用,才有权限能执行 Untiy Editor 的命令行。然而通过 pkg 文件的方式安装的 Jenkins 会自动创建一个名为 jenkins 的用户,所在组也是 jenkins。在 jenkins 用户空间中执行的 Jenkins 服务再调用 Unity Editor 的命令行肯定也是在 jenkins:jenkins 用户空间下执行的,而 jenkins 用户组应该是没有权限调用 Unity Editor 的命令行,所以出现了上面提到的整个构建任务 Hang 住的情况。

如果在通过 pkg 文件安装了 Jenkins 之后不做任何调整,直接配置好构建任务,然后点击 Build Now 直接开始构建,我们会发现前面拉取代码的流程都是正常的,但是 Unity 的图标却一直不显示在 macOS 的桌面 Docker 栏上(我们知道虽然调用 Unity Editor 的命令行不会显示 Unity Editor 的窗口,但是在 Docker 栏上还是会显示 Unity 的图标的,这个我们可以直接执行一下 /Applications/Unity/Unity.app/Contents/MacOS/Unity -batchmode -quit -projectPath [Unity工程路径] -executeMethod [public 的 static 无参方法] 命令来验证一下,详细可以参考 Unity 的命令行使用方法官方文档),所以我们可以确认 Unity Editor 的命令行压根就没有执行,接下来我们就要做些调整来让 Jenkins 成功构建 Unity 工程。

1.修改 LaunchDaemons 目录下 Jenkins 启动配置 plist 文件

执行 sudo launchctl unload /Library/LaunchDaemons/org.jenkins-ci.plist 命令,先将安装完之后默认自动启动的 Jenkins 服务关掉,然后使用文本编辑器打开 /Library/LaunchDaemons/org.jenkins-ci.plist 文件,找到 <key>GroupName</key> 将其下方一行中的 <string>daemon</string> 修改为 <string>wheel</string>,找到 <key>UserName</key> 将其下方一行中的 <string>jenkins</string> 修改为 <string>developer</string>,然后保存该文件。

由于我们没有修改 /Library/LaunchDaemons/org.jenkins-ci.plist 文件中 JENKINS_HOME 的配置,也没有修改 /Library/Preferences/org.jenkins-ci.plist 文件中 JENKINS_HOME 的默认配置,所以目前 Jenkins 还是会使用 /Users/Shared/Jenkins/Home 作为其主目录,使用 /Users/Shared/Jenkins/tmp 作为其临时目录,Jenkins 的日志将会输出到 /var/log/jenkins/jenkins.log 文件。

这些目录和文件在 Jenkins 安装成功之后就都创建出来了,并且都是使用 jenkins:jenkins 用户创建的,但是由于上面我们提到的问题,我们现在需要使用 developer:wheel 这个用户来启动 Jenkins,也就意味着我们需要把 Jenkins 可能用到的所有目录和文件的所有者修改为 developer:wheel 用户,这样 Jenkins 启动的时候才不会出现问题。所以我们需要依次执行以下命令:
sudo chown -R developer:wheel /Users/Shared/Jenkins
sudo chown -R developer:wheel /var/log/jenkins
将相关的目录和文件的归属修改为 developer:wheel 用户,然后再执行一下 sudo launchctl load /Library/LaunchDaemons/org.jenkins-ci.plist 命令,启动 Jenkins 服务,然后在浏览器中输入一下 http://192.168.200.99:8080 测试一下。

2.疑难杂症

如果按照以上的流程进行操作出现了问题,这当然是很有可能的,毕竟每个人的电脑的软硬件环境各不相同,这里就把我自己遇到的一些问题罗列一下,以供参考。

  • 用于启动 Jenkins 的用户跟安装 Unity 的用户不是同一个,导致在 Jenkins 中正确配置了所有的参数后,无法正常编译 Unity 工程,这个问题需要我们保证 Jenkins 跟 Unity 是在同一个用户空间下运行的,详细方法见上一步;
  • 按照上述的步骤安装和设置好了 Jenkins 之后依然无法启动,那么可以尝试查看一下 /var/log/jenkins/jenkins.log 文件中的日志信息,如果启动出错的话,通常错误日志以及相关的异常信息都会输出到这里的,但是如果你看到 jenkins.log 文件中有类似 logfile turned over 之类的字眼的时候,先把 Jenkins 服务关闭,然后把整个 /var/log/jenkins 目录删了再自己手动创建一个 jenkins 目录,在新建的 jenkins 目录下创建一个 jenkins.log 文件,再重新启动 Jenkins 服务就好了;
  • 如果想调整 Jenkins 启动的参数,例如日志文件的位置和 Jenkins 的 Home 目录等等,咱们可以通过文本编辑器编辑 /Library/LaunchDaemons/org.jenkins-ci.plist 文本文件,或者通过 defaults 命令修改 /Library/Preferences/org.jenkins-ci.plist 这个加密的配置文件,详细的教程可以参考 Jenkins 的官方 Wiki。记住这两个文件是有区别的哦,别使用 defaults 命令去修改 /Library/LaunchDaemons/org.jenkins-ci.plist 这个文件哦,否则这个文件被改成加密的 plist 文件后(使用 defaults 命令行貌似编辑后的 plist 文件貌似都是加密的),launchctl 不认识了就得重装 jenkins 了,如果怕操作出错可以在编辑前先备份一下这个文件。

如何正确地使用 Unity3D 中的 Bounds.Encapsulate 方法

在我们的游戏中有个很简单的需求,有一个物体 A 挂有所有控制的脚本,所有点击的处理逻辑代码都在这些脚本里头,在某个时机需要在物体 A 上再挂上一个武器物体 B,武器物体 B 有自己的 BoxCollider,原有的物体 A 也有自己的 BoxCollider,为了简单起见我的想法是当我把物体物体 B 挂到 物体 A 上的同时,我就将 A 物体的 BoxCollider 的 bounds 调整一下尺寸,让其能将物体物体 B 的 BoxCollider 给包含进来就好了,然后我再将武器物体 B 的 BoxCollider 给关闭。

看起来这个需求一点都不复杂呢,对伐?所以我就简单的这样写了以下的代码:

baseCollider.bounds.Encapsulate (skillCollider.bounds);

然而毛用都木有呢,mBaseCollider 在调用了 Encapsulate 方法之后,自身的尺寸并没有发生任何变化,当然前提肯定是 mSkillCollider 自身的 bounds 肯定不在 mBaseCollider 的 bounds 范围内的,那么问题是什么呢?

一开始,我以为是 mSkillCollider 自身的 GameObject 当前不是 Active 的导致了获取到的 bounds 是空的,毕竟在 Unity3D 的官方文档里头有这么一段嘛:

Collider.bounds

public Bounds bounds; 

Description
The world space bounding volume of the collider.

Note that this will be an empty bounding box if the collider is disabled or the game object is inactive.

可是试了很多次依然不行,最后发现 Bounds 这货只是一个结构体,而且 Unity3D 对它的处理跟对 Transform 的 Position 的处理类似,而且更严格。首先我们不能直接修改 Bounds 结构体里头的 center 和 size 属性都不能直接设置,而且 BoxCollider 的 bounds 属性也不能直接设置。

由此我们可以推断 BoxCollider 的 bounds 属性每次调用都是复制了一个 Bounds 结构体出来,所以后续我们调用 Encapsulate 方法实际上操作的 Bounds 结构体已经跟 BoxCollider 自身没有关系了,这样就造成了我们即便调用了 Encapsulate 方法,BoxCollider 自身的 Bounds 尺寸并无任何变化的原因。如果我们想修改 BoxCollider 的 Bounds 尺寸,那么我们就应该是先获取 BoxCollider 的 Bounds 存起来,然后针对这个 Bounds 结构体进行操作,操作结束后,将该 Bounds 的数值反算回去,将反算出来的数值设置给 BoxCollider。

最终的代码就是这个样子的

Bounds bounds = baseCollider.bounds;
bounds.Encapsulate (skillCollider.bounds);
baseCollider.bounds.SetMinMax (bounds.min, bounds.max);
BoxCollider boxCollider = baseCollider as BoxCollider;
boxCollider.center = boxCollider.transform.InverseTransformPoint (bounds.center);
boxCollider.size = bounds.size;

测试一切正常了,BoxCollider 如我所愿地调整了自身的尺寸,并将 skillCollider 的 bounds 也给包围住了。

Linode 上 VPS 重启后 MySQL 服务器挂了的恢复方法

做个备忘,省得每次遇见都得重新 Google 了。

这次 Linode 整体更新 XEN 的安全漏洞补丁之后,我的 VPS 重启之后,自己也没有注意,今天打开一下自己的博客站点,发现 WordPress 报了了一个「连接数据库出错」的问题,首页无法正常打开了。

之前也碰到过类似的问题,但是之前每次都是直接丢给我们可爱的耀华同学来搞定的,现在耀华同学不在身边,有的时候用起来就没有那么顺手了,所以捏,咱们还是得学会自己动手,丰衣足食啊。在询问了耀华同学咱们 MySQL 的错误日志在哪里无果之后,本想尝试直接使用耀华同学提供的通过 find 命令暴力查找了(事实证明这样可能可以找到,但是会比较麻烦),刚好看到网上有个默认地址提供了,直接 cd 到那个目录之后,看到一个长得很像的 error log 文件,打开一看,果了个然啊。打开看看错误日志里头的错误是啥,原来是 mysql 依赖的临时文件创建不成功,文件权限有问题,那么修改一下文件权限吧,修改好了文件权限之后,重启 mysql 服务,一切 OK。

那么我们来回顾一下整个流程是怎么样的。

  1. 发现 WordPress 报告 MySQL 数据库连接出错了,那么 ssh 到 VPS 上,执行一下 ps aux | grep mysql 命令,发现并没有 mysql 服务进程,可以肯定 mysql 服务没有成功启动;
  2. 那么接下来我们就要看看 mysql 服务为啥没有在此次 XEN 补丁升级后自动启动成功呢,找 error 日志来看吧,最终我们发现 rpm 默认安装的 mysql 错误日志路径在 /var/lib/mysql 目录下,跟 mysql 的核心数据库文件就在同一个目录,建议不要这么干,还是在 my.cnf 中好好地配置一下 mysql 的日志文件存放路径吧(由于我并非一个合格的服务器开发人员或维护人员,原谅我使用了默认配置);
  3. 找到这个名称为 li402-16.err(文件名格式为:[host-name].err) 的错误日志文件,滚到最下面一行,看看错误是啥吧,/usr/libexec/mysqld: Can’t create/write to file ‘/dev/shm/ibinQuSV’ (Errcode: 13);
  4. 这下可以确认是因为不能往 /dev/shm 这个目录里头写入文件了,如果不是磁盘满了那么肯定就是权限不足,所以无法写入文件,但是这个目录其实是一个内存里头的临时文件目录,所以不会出现满了的情况,那么肯定的权限的问题;
  5. 接下来我们就应该给 mysql 用户分配 /dev/shm 目录的写权限就好了,由于我比较懒(实际上是不太会,这个会有安全隐患的),我直接把 /dev/shm 的权限修改为 777 了,建议还是自行严格地将写入权限分配给 mysql 用户吧;
  6. 然后通过 /etc/init.d/mysqld start 启动 mysql 服务;
  7. 再次打开博客站点首页,测试通过,OK 搞定了。

此事的原因很有可能是因为 VPS 重启之后,对于某些敏感文件目录,XEN 容器管理的原则就是将其恢复到默认的文件权限吧,不是很了解,但是直觉会是这样,也就是说以后碰到重启这样的情况再次出现 MySQL 启动错误,都应该先检查一下是否为临时文件写入权限的问题导致的。


喵的,这次 Linode VPS 硬件问题之后,机器再次重启了,泥煤的啊,突然发现,在这里的备忘压根达不到备忘的效果啊,VPS 挂了,博客打不开啊,你上哪儿看去呢?好像是个问题呢。

星巴克贩卖的是什么?

我一直都是一个愿意喝咖啡的人,虽然可能喝得最多的无非就是雀巢速溶三合一,剩下的也就是什么星巴克和 Costa(貌似中文名字叫咖世家)了,当然肯德基和麦当劳的早餐中的咖啡也是很重要的构成部分啦。

以我自己的口味和经历来讲的话,确实星巴克和 Costa 的咖啡比速溶的口味要好一些,但是前两者跟肯德基或麦当劳早餐中搭配的咖啡之间的区别就很小了。

速溶咖啡的那种浓重的香味和绵密的甜感对于任何喜欢甜食的人来说,我觉得都不是很好拒绝的东西,确实不难喝挺好喝的,如果非得要做个类比的话,我觉得可以把雀巢速溶三合一经典跟康师傅的红烧牛肉面做个类比。我个人喝完雀巢速溶经典三合一或者淘宝上卖得极好的越南中原 G7 速溶三合一之后,我在小便的时候都还能闻到咖啡的香精味道(也许有人会说我该去看医生了,好吧,谢谢好心人了。我在吃完烤羊肉的第二天小便时都能闻到羊骚味,所以咱们不要纠结这个问题了,好伐?),另外喝完之后呢,喉咙处会有一股甜腻的黏黏感。

那么星巴克或者 Costa 们呢?他们的咖啡就很好喝吗?我倒是并没有觉得有多么好喝,这些饮料类的东西对于我来说,我只是觉得它们比白水好喝,所以我更愿意喝罢了,其实我也很喜欢喝绿茶和红茶的,最次的茶叶都可以的,只要不是白水,我都挺能喝的。但是星巴克和 Costa 卖的咖啡香味确实更简单和真实,而且一般也不会给加那么重的糖和奶(而且还可以要求不加,当然速溶也有不带糖和不带奶的款)。我喝得最多的就是两款,美式和拿铁,美式就是最普通的咖啡粉冲水之后的形态,拿铁就是往美式里头加了奶和糖,很无聊的两款咖啡对吧。不过喝过之后确实不会出现上面我说到的小便还尼玛能感觉到味道的情况,也不会出现喉咙处那种黏黏的甜腻感,很直接的说法就是「这货可以当水喝能解渴,而速溶不能解渴,喝完了之后我还得喝水漱口」,差不多就是这个意思。

好吧,说完了这些屁话,感觉咖啡店里头卖的现磨鲜煮的咖啡确实好喝一些,品质稍高一些。但是我觉得这个不是星巴克和 Costa 们在中国城市中成功的主要因素,更不是广大消费者愿意去咖啡馆消费的主因。那么是什么让我们愿意去星巴克呢?

  1. 舒适感,当大家已经能够很轻松的吃饱穿暖了之后,很自然的就会要求吃得健康和穿得有格调,咖啡馆向来卖的就不只是一种饮品,更多的是售卖了一种舒适的体验。例如,我们随时走进营业的咖啡馆,随便找个座位坐下,通常咖啡馆里头的工作人员都不会主动过来问你需要喝点什么,你完全可以自己带瓶水坐在咖啡馆里头待上大半天甚至更长时间,这里是一个很轻松的环境,消费者与服务者之间更多的是一种相对来说更对等的朋友之间的对话,而不只是简单的服务与被服务的关系。这也就能解释为什么总有人会在咖啡馆里头一坐就是一整天,占着一张桌子就不走了,因为双方对于这种行为是默许的,甚至咖啡馆是更希望有一拨这样常驻在咖啡馆里头的人。这样才会让咖啡馆区别于卖冰淇淋和冷饮的小铺子,这样的咖啡馆有「人味」更能让人感到轻松自然;
  2. 自我满足,现代社会更多强调个人的满足和享受,前面提到的两点其实都是某种层次上的满足。在我们选择进入星巴克去买一杯咖啡,堂食或者外带,当我们走进了咖啡馆,自然就与没有走进咖啡馆的人分成了两类。鉴于星巴克的价格在中国的情况,并不是大部分人都能很轻松地每天进去喝一杯的,那么是不是有这种可能「进入咖啡馆的人通过买咖啡或者喝咖啡这件事情,从某种角度上能获得社会地位的一种认同」,好比有人会将「星巴克早餐」称为「你国中产早餐」一样的,这实际上确实从某种程度上很直接地说明了消费者的经济能力,这跟所有的游戏里头那个排行榜带给人们的快乐是一样的,人人都想比别人强一些,在咖啡馆里的人很自然地会获得一种简单又直接的超群的快感;
  3. 文化裹挟,咖啡是现代都市流行文化的一个重要组成部分,现代文艺作品里面咖啡馆的出场率实在是太高了,我们随便找两个现代背景的电视剧或电影,不论是美剧、韩剧、港剧、台剧还是大陆电视剧,不论是主流商业、喜剧、动作、罪案还是文艺电影,几乎随处可见的咖啡馆里主人翁闲着喝咖啡、分手喝咖啡、聚会喝咖啡、装逼喝咖啡,甚至在家早上起床之后第一件事情就是冲咖啡等等。社交媒体上那些达人们 Po 的那些美美哒的照片,简单粗暴一些的话,可以分为几大类:景区旅游风景修改自拍、商场百货 Shopping 自拍、家里头无聊素颜自拍、夜店或者 LiveHouse 演出现场自拍、户外大型活动自拍、咖啡馆闲趣自拍等等。看吧,咖啡已经不只是一个纯粹的简单的饮料了,对于现代都市人来说,这货扮演了很多的角色,而且几乎无孔不入。主流的平面媒体和电视媒体上,其实我们已经很少会看到关于咖啡品牌的广告了,为啥呢?这就是因为它真的已经是个普及率极高的东西了,大家已经不需要广告来告诉我们有这么个东西了,我们除了在电视或者网络视频上偶尔能看到类似于雀巢这种国际性品牌的战略性品牌广告以外,我们很少看到哪个咖啡品牌的广告了。而咖啡这个东西又被打上各种文艺的标签和印记,所以即便宣传和广告也只会在咖啡的个性和情调上进行挖掘,而咖啡馆这种有很强烈的店主个人审美和咖啡品味印记与之刚好形成极佳的互补,虽然星巴克和 Costa 并未呈现出很强烈的文艺风格,但是其整体的格调还是较一般的快餐店还是高出不少的。

想喝一杯好喝的咖啡,对于任何一个能喝出咖啡好坏的人来说,如果需要自己动手制作的话,需要耗费的时间和精力都是不容小觑的,特别对于都市一族来说时间成本更是难以承受。进入咖啡馆,享受一下咖啡馆里那种氛围,等待个几分钟,鲜煮的咖啡端上来,水温刚刚好,烘焙的咖啡香味浓而不腻,试问谁又不愿意为此掏点钱呢?谁还没个满足一下自己的小欲望?