Android 自动化批量打包精粹

在项目即将上线的时候,都会遇到这样的问题:
有好多个有自己 SDK 的商店,而且都要赶同一个档期上线。
个别商店可能会有一些独特的小需求,比如 icon 的角标、开机闪屏 logo 、或者独有的功能接口要调用。
当10个 SDK 接完8个了,第1个接入的有更新了,需要接入最新版本才给上线。最要命的是接口有变动。
双方联调,找问题,各种沟通不畅。
下一个项目这些问题再重演一遍。

所以这个事情如果要做的优雅,高逼格,高效简洁,我们应该做到:
一次接入可以让所有项目通用。
各个部分充分解耦(SDK、项目、服务器),单独接入单独调试单独测试。
渠道商店配置可以由非技术人员(市场、运营)来管理。
当然不可少的打包应该有工具自动化完成。

简简单单几句话来描述这个事情,看起来挺简单,但是仔细想想,都挺空的。所以,还是要细化一下具体的东西。

一、 一次接入所有项目通用
要实现通用,就需要有一个比较稳定的接口将功能性 SDK 封装起来。封装起来的 SDK 我们可以姑且命名为 SDK 插件,各个项目调用统一的接口,而不需要关心 SDK 插件之中的具体的 SDK 。

二、各个部分解耦
实际上稳定统一的接口就是完成了解耦的第一步,我们需要梳理清楚具体部分的划分。渠道商店的 SDK 往往都会有服务器端接口部分,用作用户校验和支付验证。所以大致可以分为三个部分:SDK 插件开发、渠道商店对接服务器、客户端使用的 API。
各部分详细的设计参考下面的示意图:
结构图

开发 SDK 插件的流程:
前提条件:模板工程、可用的 SDK 、测试服务器接口
流程:通过模板创建工程 → 接入 SDK → 测试通过 → 导出 SDK 插件
SDK开发模板
当完成这一部分的时候,SDK 的对接就可以完全不需要核心项目团队的资源来支持了。这样就实现了一定程度的解耦,而核心项目部分,不用关心这些 SDK 功能的具体实现,只要接入一个用于测试各种 SDK 功能的测试 SDK 就可以无忧了。
测试SDK
从上面两个图可以看到,独立出一个 Platform Server 平台服务器来做与 SDK 相关的对接。这样的好处是,服务器端也实现了解耦;一个平台服务器可以服务于多个项目;并且不需要项目团队拿出资源来维护这个部分。而区分不同项目不同商店渠道只要以自定义标准参数的形式,制定出一套自己的标准就可以通用于所有接入的项目了。

对于测试 SDK 的设计,有必要叨唠两句。这个东西的目的是把通过 SDK 获取的数据直接给客户端,所以有些东西可以更直接一些。比如,需要登录获取一个 user id ,这种需求直接做一个输入框或者下拉列表,直接让开发人员输入或者选择 user id,就可以了。没有必要还让测试人员走一个完整的登录流程是不是?并且这样可以随意调整需要登录的账号,方便测试。支付同理,客户端需要处理在成功时会怎样,失败时会怎样,那么直接用两个按钮,返回给客户端结果就可以了。真的没有必要还调个支付宝啥的真去走个第三方支付流程。

三、 由非技术人员管理
因为实际上对于渠道商店出包的需求都是由非技术人员(运营、市场等)发起的,所以最好由非技术人员负责管理渠道商店的参数、数据、出包等。这样不仅可以解放出一小部分技术人员的生产力,最关键的是减少了沟通造成的损耗,当运营取得渠道相关参数之后,直接在工具中填写并提交然后工具自动读取后将需求的包打出来,这样这个流程一个人就可以完成了。
这个工作流的设计的关键当然是非技术人员以什么形式来提交给工具这些参数?最好的方案,当然是有一个非常友好的界面,可以让用户根据提示正确的填写各种参数以及提交资源等。但是如果在开发资源不足的情况下,退而求其次可以考虑使用 excel 表格,一般运营人员经过简单培训也是完全可以 hold 住的,只要将标准制定的全面且完善。

四、 自动化打包
首先只打一包的话使用 ant 一个命令就可以实现,并且同时完成包括 编译 打包 混淆 签名 等完整的流程。然而,最重要的是我们要根据之前填好的数据自动打出所有我们需要的包。
要实现这个需求,单靠 ant 脚本就不太现实了,虽然理论上也是可以实现的。这里还是建议使用强逻辑型的语言实现这个程序。在最后打包的环节再调用 ant 脚本完成打包的最后一步。
这个程序大框架上看是一个很简单的流水线,可以看下面的图。然后有很多的细节需要处理,这其中的任何小细节被忽略都有可能无法良好的工作。
流水线

编译 C++ 我的习惯是只编译一次,所有包使用同一个 so 这样可以最大程度的保证编译速度。需要传给 C++ 部分的一些参数可以通过 JNI 传递,运行效率是会有一些小影响,几乎可以忽略不计,可以让打包部分的复杂度降低不少。不过确实有一些特殊情况需要在 so 文件中打入一些数据,那就根据自己的情况再具体解决了。

我习惯在打每个包之前都删掉生成文件夹 bin 和 gen ,因为曾经有发生过不少次因为之前的生成文件的存在导致后来的改动并没有被编译进生成文件中。并且不要忘记,如果项目有引入 Library 的话,最好递归的去清理一些。

在对工程做任何改动之前都不要忘记做现场的保护。我的做法是,如果是对一个已经存在的文件做修改,我会把那个文件做备份和操作记录。再这个包打完或者出问题被中断后可以把现场恢复到最初的状态;如果是加入一个新的文件也要记录下来,在打完这个包将其删除。这样做可以最大程度的保障不会让当前操作污染到以后的事情。

可能有人注意到了,流水线这张图并没有提到 SDK Plugin 。实际上我赞成把 SDK Plugin 看做一个实体,只是为了描述更方便,理解更直观一些,具体存在形式最好是根据自己系统的情况量身定制。它可以只是散乱的一堆代码和一堆资源,也可以是其他合适的形式甚至是代码片段或者 patches ,如何使用需要根据自己的情况具体问题具体分析,而这个流水线图展示的是最纯粹的重要工作节点。

引入 Library 只需要修改 project.properties 中的 android.library.reference.* 参数,当然不要忘了在后边添加新的之前要先解析一下旧的,取到一个当前数字再添加新的数字。然而为什么是这个文件呢?它的秘密都在 build.xml 这个默认的 ant 脚本文件里,这个文件的注释读一读可以获得好多重要的信息。

默认的 ant 构建系统是需要自己生成 local.properties 这个文件,并且设定 sdk.dir 来显示指出 Android SDK 的路径。我喜欢把 build.xml 做个小改动,让这个变量直接访问环境变量:

不管是选择使用传统的 local.properties 还是修改 build.xml ,都必须要保证所有引入的 library 都有一份。我选择修改 build.xml 这个做法就是为了在不同环境中不用再多修改一个文件。

每一个渠道商店的包都必然会有一些常量参数,而使用这些参数的大多是相关的 SDK 。所以我的方案是生成一个常量类写到代码文件中替换掉之前埋在工程中用于测试的那个类。而这里最好将生成后的类做个 log,以便在出包后可以再检查一下数据是否有误,毕竟打错参数这种事情有时候并不容易被测试发现。

有许多 SDK 都会有要求在 AndroidManifest.xml 加一些东西,比如,声明 Activity、 Receiver 、权限等等,也有包括包名、版本号这些需要修改的东西。所以读写这个 xml 也是这个工具必不可少的一部分。那么数据是从哪里来的呢?首先,参数类的数据,肯定是从配置信息里获取的(包名、版本号、其他参数等);然后,与 SDK 有关的,肯定是不能让非技术人员来配置的,这部分就要作为 SDK Plugin 的一部分,可以在 SDK Plugin 中放一个专属这个 SDK 的 xml 配置,在这一步的时候从里边读取与工程中的 AndroidManifest.xml 合并。(有人选择使用替换特殊标示的方式来操作这个文件,这样的话就需要提前在这个文件里写很多特殊标识符,其后果是这个工程可能就不能直接编译运行调试了。想运行一下还需要先替换标识符感觉就有点累了,所以我认为还是读写 xml 是最靠谱的方案)

写 AndroidManifest.xml 的时候经常会遇到命名空间不见了的情况,表现为原文件的 android:xxxx 变成了 ns0:xxxx 。原因就是原来的 xml 命名空间被丢掉了,修改这个文件一定不要忘记手动设置一下 manifest 的命名空间:
xmlns:android=”http://schemas.android.com/apk/res/android”

签名的配置修改 ant.properties 这个文件,key.store 这个变量就是签名文件的路径,不过签名文件是需要提前生成好的,这个常打包的同学应该并不陌生,这里就不展开了。

要强调的一点是 .properties 文件写中文的话是会出错的,这个文件的编码是 ascii 所以如果传入的路径或文件名有包含非 ascii 字符的话,是需要转换成 \u4e00 这种形式的编码的。