多渠道打包的进化史

阅读时间 约 2 分钟

Android开发比iOS麻烦的地方在于,应用市场和发布渠道众多。

为了统计和区分各个渠道包的效果和数据,就需要有一种方法来标识它们。

石器时代

为了区分不同的渠道包,最直观也是最原始的方案,

就是使用一个变量来表示渠道名,

在Release时,每修改一次变量打一次包,

最后就能在代码中读取这个变量的值,判断属于哪个渠道了。

.

在这其中,以「友盟+ProductFlavors」的方案最具有代表性:

友盟需要在Manifest中使用meta-data标签定义一个UMENG_CHANNEL表示渠道,

而这种方案,则将UMENG_CHANNEL的值使用占位符代替,

在build.gradle中定义不同的ProductFlavors,

然后利用Gradle脚本在打包时动态替换AndroidManifest中占位符的值

<application
    android:name=".base.App"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name">

    <!-- 友盟统计SDK组件 -->
    <meta-data android:name="UMENG_APPKEY" android:value="********************"/>
    <meta-data android:name="UMENG_CHANNEL" android:value="${UMENG_CHANNEL_VALUE}"/>

...
productFlavors {
    local_test {
        dimension "default"
        manifestPlaceholders = [UMENG_CHANNEL_VALUE: "local_test"]
    }
    baidu {
        dimension "default"
        manifestPlaceholders = [UMENG_CHANNEL_VALUE: "baidu"]
    }
    qh360 {
        dimension "default"
        manifestPlaceholders = [UMENG_CHANNEL_VALUE: "360"]
    }
    qq {
        dimension "default"
        manifestPlaceholders = [UMENG_CHANNEL_VALUE: "qq"]
    }
}

基于同样的原理,还有使用Gradle插件来实现字符串替换的方案,如「Gradle-Packer」。

但是无论采用哪种替换方案,本质都是需要多少个渠道包就Build多少次,

在项目较大、渠道数量较多时,全部打一遍渠道包可能需要十几个小时,这是不可忍受的。

青铜时代

上面的打包过程中,耗时最长的就是Build了,

有没有办法只替换字符串,不用重新Build呢?

答案是有的,例如可以利用apktool修改资源文件不影响dex二进制文件的特性,

先Build一次生成待处理的apk包,然后复制一份然后使用apktool工具对其进行解包,

修改完AndroidManifest文件的内容后,再重新进行封包和签名即可。

.

这种方案将多次Build的过程压缩成一次,大幅降低了生成渠道包的时间,

然而使用apktool修改apk和重新签名依然是有时间成本的,以美团的实际应用为例[1]

近900个渠道,使用这种方案全部打完需要约3个小时,还是有点慢。

铁器时代

既然签名也很慢,那能不能无需重新签名就能在apk的某个位置写入信息呢?

.

现在通用的Android的签名方式是使用Java对jar文件签名的方案jarsigner,

这种方案的原理,是在目录下建立一个META-INF文件夹,然后对所有文件进行遍历,

使用SHA1摘要算法对每一个文件进行计算,然后保存进MANIFEST.MF文件,

为了防止MANIFEST.MF被篡改,再对它进行摘要生成CERT.SF文件,

最后对SF文件使用RSA非对称加密算法加密,生成CERT.RSA文件。

Name: res/anim/abc_fade_in.xml
SHA1-Digest: ohPEA4mboaFUu9LZMUwk7FmjbPI=
Name: res/anim/abc_fade_out.xml
SHA1-Digest: MTJWZc22b5LNeBboqBhxcQh5xHQ=

这里可以看到,签名过程用到了SHA1和RSA算法,看上去无懈可击,

但是仔细想想,整个签名算法都只针对根目录中的其它目录和文件,

对于META-INF目录中除了上述三个签名算法生成的文件外,并无其它校验机制。

也就是说,在已签名的apk文件的META-INF目录下添加文件,是不会触发校验失败的。

.

美团的方案就是利用这个特性,在打好的Release包的META-INF目录下添加一个空文件,

利用文件名来标示渠道,然后在代码中去读取这个文件的文件名。

这种方案的渠道包生成速度非常快,900个渠道不到一分钟就能打完。

蒸汽时代

美团的方案对于多渠道打包需求来说,在当时几乎已经非常完美了。

不过由于文件名的字符和长度有限制,并且从zip包中解压读取文件在运行时有性能损耗,

想利用这个特性来做复杂一点需求还是有点力不从心,典型的例如动态跳转需求:

  1. 小说下载:用户在Web页面点击下载阅读小说的广告,第一步下载App,第二部用户打开该App直接打开该小说开始阅读
  2. 应用下载:用户搜索某应用后点击下载按钮,先下载打开应用市场App后打开市场App,然后开始自动下载用户真正要下载的应用

更进一步,产生了一种使用zip-comment的方案,

即利用了apk文件使用zip格式进行压缩的特性。

.

zip文件格式中定义了comment-length和comment字段,

length字段长度为两个字节,也即comment中最多能写入2^16=65535字节。

apk文件默认的comment-length为0,也就是说,

我们可以通过修改这两个字节的内容来向comment字节写入自定义内容

(例如一段scheme)

这种方案的打包速度比美团的META-INF方案略快一点,更重要是,

由于是直接读取文件的二进制内容,不需要zip解压缩,读取效率非常高(10ms级别)

目前市面上绝大多数动态跳转的解决方案,都是使用zip-comment实现的。

电气时代

如果签名算法到此为止的话,似乎也不需要更快更好的解决方案了,

但是,在多渠道打包技术蓬勃发展的时候,Google也及时看到了apk签名算法的漏洞,

能往已经签名过的apk文件中添加任意文件(META-INF方案),

能写入几乎任意长字符串(Zip-Comment方案)

这个算法的安全性已经不能用糟糕来形容了

.

因此,Android 7.0推出了新的应用签名方案:APK Signature Scheme v2

v2方案是一种全文件签名方案,针对整个zip包而不只是其中的文件进行校验。

在上图中,EOCD块中定义了Central Directory起始位置的偏移值,

默认情况下它是紧跟着Contents of ZIP entries的,

Google利用这一特性,人为增大了offset值,

在Contents和Central Directory之间强行插入了一块用于存储文件的签名信息。

.

上图红色部分中的签名信息会对其它三块内容进行保护,

因此如果要写入渠道信息,就只能在这个Signing Block中做文章了

根据Google的说明文档,APK Signing Block的格式如下:

  1. size of block,以字节数(不含此字段)计 (uint64)
  2. 带 uint64 长度前缀的“ID-值”对序列:
    • ID (uint32)
    • value(可变长度:“ID-值”对的长度 - 4 个字节)
  3. size of block,以字节数计 - 与第一个字段相同 (uint64)
  4. magic APK 签名分块 42(16 个字节)

上面的第二部分是由「ID-值」组成的一组序列,

v2的签名信息是其中的一项,其ID为「0x7109871a」

不过可能是为了以后方便扩展,Google在文档中明确说明了,

在解译该分块时会忽略 ID 未知的「ID-值」对,

这样一来,就给了开发者写入渠道或者其它信息的机会,

美团的新一代多渠道打包方案「Walle」就是基于这个原理。

.

这种方案虽然是基于Android 7.0才支持的APK Signature Scheme v2,

但是由于v1和v2签名相互兼容,这种方式打出来的包,

即使在低版本中也可以直接读取二进制文件获取写入的信息,

而不影响签名验证和安装过程。

.

以美团的实际应用为例[2],对一个30M大小的APK包,

只需要100多毫秒(包含文件复制时间)就能生成一个渠道包,

而且由于都是直接解析二进制文件格式,

读取信息的性能与Zip-Comment方案相当,都在10毫秒级别。

暂时…没有更给力的方案了

参考资料

  1. 美团Android自动化之旅—生成渠道包
  2. 新一代开源Android渠道包生成工具Walle
  3. 一种动态为apk写入信息的方案
  4. Android APK签名原理及方法
  5. APK 签名方案 v2
  6. Android7.0新签名对多渠道打包的影响
  7. https://github.com/mcxiaoke/gradle-packer-plugin
  8. https://github.com/mcxiaoke/packer-ng-plugin