Android批量打包提速 - 1分钟900个市场不是梦

来源:互联网 发布:数据机房建设方案 编辑:程序博客网 时间:2024/04/30 08:51

Android批量打包提速 - 1分钟900个市场不是梦

黎明前的黑暗

使用Ant或者Gradle来给程序进行多渠道批量打包,通常都是在manifest文件中写入一个meta标签:

[xml] view plaincopy
  1. <code class="hljs"><span class="hljs-tag"><<span class="hljs-title">meta-data</span> <span class="hljs-attribute">android:name</span>=<span class="hljs-value">"CHANNEL"</span> <span class="hljs-attribute">android:value</span>=<span class="hljs-value">"xxx"</span> /></span></code>  

meta的key值固定,通过循环改变meta中的value值来实现市场渠道的写入。

Ant批量打包实现相对麻烦,以前写的时候多亏了谦虚的天下-《App自动化之使用Ant编译项目多渠道打包》 。如果没有这篇帖子,真不知道当时Ant要折腾多少回才能写好。

Gradle作为新的安卓官方构建工具,有Google老大撑腰,它的批量打包实现会相对简单些。可以参考《迁移到Android Studio》。当然这里面有些指令过时了,例如:runProguard已经被minifyEnabled替代了。

以上两种都是传统的批量打包方式,他们最大的缺点就是打包时间长。
在前期渠道很少时这种方法还可以接受,但只要渠道稍微增多该方法就不再适用了,原因是每打一个包都要执行一遍构建过程,效率太低。(电脑比较烂,以前一般打包都要花费个30-40分钟。)

打包界的曙光

前几天看到美团的技术分享文档:《美团Android自动化之旅—生成渠道包》,其中第三种方式提到:

如果能直接修改apk的渠道号,而不需要再重新签名能节省不少打包的时间。幸运的是我们找到了这种方法。直接解压apk,解压后的根目录会有一个META-INF目录,如下图所示:
META-INF目录
如果在META-INF目录内添加空文件,可以不用重新签名应用。因此,通过为不同渠道的应用添加不同的空文件,可以唯一标识一个渠道。
采用这种方式,每打一个渠道包只需复制一个apk,在META-INF中添加一个使用渠道号命名的空文件即可。
这种打包方式速度非常快,900多个渠道不到一分钟就能打完。

OK,到这里,思路就有了。

  1. 在META-INF中放置一个类似 channel_xxx 的空文件来标识市场。
  2. 在Java代码中解析这个文件名获取市场xxx即可。

由于文档中的代码实现较少,这里我来讲述一下我的实现。

我的实现

基于以上总结的美团思路,实现了一套自己的代码,方便引入到工程后实现这种打包方式。

代码在Github:GavinCT/AndroidMultiChannelBuildTool

Python工具实现

  1. 首先创建一个空文件,等待写入META-INF目录作为channel_xxx文件

    [python] view plaincopy
    1. <code class="hljs"><span class="hljs-comment"># 空文件 便于写入此空文件到apk包中作为channel文件</span>  
    2. src_empty_file = <span class="hljs-string">'info/czt.txt'</span>  
    3. <span class="hljs-comment"># 创建一个空文件(不存在则创建)</span>  
    4. f = open(src_empty_file, <span class="hljs-string">'w'</span>)   
    5. f.close()</code>  
  2. 获取渠道列表。
    考虑到渠道的更新不应该是程序员来做,因此在info文件夹下放置一个channel文件,便于不懂程序的人更新渠道。(每个渠道以换行结束)

    [python] view plaincopy
    1. <code class="hljs"><span class="hljs-comment"># 获取渠道列表</span>  
    2. channel_file = <span class="hljs-string">'info/channel.txt'</span>  
    3. f = open(channel_file)  
    4. lines = f.readlines()  
    5. f.close()</code>  
  3. 找到初始apk
    考虑到现实中为了防止安装包过大,我们通常分为arm和x86两个版本,所以python中支持当前目录下放多个apk来进行打包。
    当然有人会说共用了一个channel文件,多个apk会生成相同市场的对应包。
    你也可以修改一下python,使不同的apk去找不同的channel文件进行打包。
    这里由于我的业务场景这样更方便,我就不修改了。

    [python] view plaincopy
    1. <code class="hljs"><span class="hljs-comment"># 获取当前目录中所有的apk源包</span>  
    2. src_apks = []  
    3. <span class="hljs-comment"># python3 : os.listdir()即可,这里使用兼容Python2的os.listdir('.')</span>  
    4.     <span class="hljs-keyword">for</span> file <span class="hljs-keyword">in</span> os.listdir(<span class="hljs-string">'.'</span>):  
    5.     <span class="hljs-keyword">if</span> os.path.isfile(file):  
    6.         extension = os.path.splitext(file)[<span class="hljs-number">1</span>][<span class="hljs-number">1</span>:]  
    7.         <span class="hljs-keyword">if</span> extension <span class="hljs-keyword">in</span> <span class="hljs-string">'apk'</span>:  
    8.             src_apks.append(file)</code>  
  4. 遍历渠道号并写入apk。
    多个apk只是for循环问题,我们来看单个apk生成多市场包的代码

    [python] view plaincopy
    1. <code class="hljs"><span class="hljs-comment"># file name (with extension)</span>  
    2. src_apk_file_name = os.path.basename(src_apk)  
    3. <span class="hljs-comment"># 分割文件名与后缀</span>  
    4. temp_list = os.path.splitext(src_apk_file_name)  
    5. <span class="hljs-comment"># name without extension</span>  
    6. src_apk_name = temp_list[<span class="hljs-number">0</span>]  
    7. <span class="hljs-comment"># 后缀名,包含.   例如: ".apk "</span>  
    8. src_apk_extension = temp_list[<span class="hljs-number">1</span>]  
    9.   
    10. <span class="hljs-comment"># 创建生成目录,与文件名相关</span>  
    11. output_dir = <span class="hljs-string">'output_'</span> + src_apk_name + <span class="hljs-string">'/'</span>  
    12. <span class="hljs-comment"># 目录不存在则创建</span>  
    13. <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> os.path.exists(output_dir):  
    14. os.mkdir(output_dir)  
    15.   
    16. <span class="hljs-comment"># 遍历渠道号并创建对应渠道号的apk文件</span>  
    17. <span class="hljs-keyword">for</span> line <span class="hljs-keyword">in</span> lines:  
    18.     <span class="hljs-comment"># 获取当前渠道号,因为从渠道文件中获得带有\n,所有strip一下</span>  
    19.     target_channel = line.strip()  
    20.     <span class="hljs-comment"># 拼接对应渠道号的apk</span>  
    21.     target_apk = output_dir + src_apk_name + <span class="hljs-string">"-"</span> + target_channel + src_apk_extension    
    22.     <span class="hljs-comment"># 拷贝建立新apk</span>  
    23.     shutil.copy(src_apk,  target_apk)  
    24.     <span class="hljs-comment"># zip获取新建立的apk文件</span>  
    25.     zipped = zipfile.ZipFile(target_apk, <span class="hljs-string">'a'</span>, zipfile.ZIP_DEFLATED)  
    26.     <span class="hljs-comment"># 初始化渠道信息</span>  
    27.     empty_channel_file = <span class="hljs-string">"META-INF/cztchannel_{channel}"</span>.format(channel = target_channel)  
    28.     <span class="hljs-comment"># 写入渠道信息</span>  
    29.     zipped.write(src_empty_file, empty_channel_file)  
    30.     <span class="hljs-comment"># 关闭zip流</span>  
    31.     zipped.close()</code>  

以上Python是属于现学现写,有什么可以优化的地方还请告知。

Java工具实现

Python帮我们向apk包中写入了channel信息,Java端当然也需要对应更改才能使用。
由于解析channel需要去apk也就是zip中去找文件,所以相对耗时一些。
因此在ChannelUtil.java中,会将找到的channel和对应versionCode存储在静态变量和SharedPreference中,保证本次甚至本版本中channel只从zip中获取一次。

在Java代码中读取空渠道文件名

从apk中获取channel,美团留下的代码if (entryName.startsWith("mtchannel"))是有问题的,应该采用if (entryName.startsWith("META-INF/mtchannel"))
我的代码如下:

[java] view plaincopy
  1. <code class="hljs"><span class="hljs-javadoc">/** 
  2.  * 从apk中获取版本信息 
  3.  *<span class="hljs-javadoctag"> @param</span> context 
  4.  *<span class="hljs-javadoctag"> @param</span> channelKey 
  5.  *<span class="hljs-javadoctag"> @return</span> 
  6.  */</span>  
  7. <span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> String <span class="hljs-title">getChannelFromApk</span><span class="hljs-params">(Context context, String channelKey)</span> </span>{  
  8.     <span class="hljs-comment">//从apk包中获取</span>  
  9.     ApplicationInfo appinfo = context.getApplicationInfo();  
  10.     String sourceDir = appinfo.sourceDir;  
  11.     <span class="hljs-comment">//注意这里:默认放在meta-inf/里, 所以需要再拼接一下</span>  
  12.     String key = <span class="hljs-string">"META-INF/"</span> + channelKey;  
  13.     String ret = <span class="hljs-string">""</span>;  
  14.     ZipFile zipfile = <span class="hljs-keyword">null</span>;  
  15.     <span class="hljs-keyword">try</span> {  
  16.         zipfile = <span class="hljs-keyword">new</span> ZipFile(sourceDir);  
  17.         Enumeration<?> entries = zipfile.entries();  
  18.         <span class="hljs-keyword">while</span> (entries.hasMoreElements()) {  
  19.             ZipEntry entry = ((ZipEntry) entries.nextElement());  
  20.             String entryName = entry.getName();  
  21.             <span class="hljs-keyword">if</span> (entryName.startsWith(key)) {  
  22.                 ret = entryName;  
  23.                 <span class="hljs-keyword">break</span>;  
  24.             }  
  25.         }  
  26.     } <span class="hljs-keyword">catch</span> (IOException e) {  
  27.         e.printStackTrace();  
  28.     } <span class="hljs-keyword">finally</span> {  
  29.         <span class="hljs-keyword">if</span> (zipfile != <span class="hljs-keyword">null</span>) {  
  30.             <span class="hljs-keyword">try</span> {  
  31.                 zipfile.close();  
  32.             } <span class="hljs-keyword">catch</span> (IOException e) {  
  33.                 e.printStackTrace();  
  34.             }  
  35.         }  
  36.     }  
  37.     String[] split = ret.split(<span class="hljs-string">"_"</span>);  
  38.     String channel = <span class="hljs-string">""</span>;  
  39.     <span class="hljs-keyword">if</span> (split != <span class="hljs-keyword">null</span> && split.length >= <span class="hljs-number">2</span>) {  
  40.         channel = ret.substring(split[<span class="hljs-number">0</span>].length() + <span class="hljs-number">1</span>);  
  41.     }  
  42.     <span class="hljs-keyword">return</span> channel;  
  43. }</code>  

总结

使用这种方式打包,打包工作不再需要非得是安卓程序员。需要打包时,只要下载安装Python环境,点击MultiChannelBuildTool.py执行即可。

那Gradle是不是没用了呢?

当然不是,Google老大为他做了这么多,怎么能说不用就不用呢?
他的用处在于实现订制,比如打包出x86和arm的包,或者打出手机包和适应平板的hd包,然后借助上面的工具生成多个市场,即完成了多种适配包多个市场的任务。

Gradle渠道订制的具体内容可以参见:《美团Android自动化之旅—适配渠道包》 。
还是美团的文档,还是熟悉的味道。在此感谢美团的分享。

常见问题答疑

这部分问题是由美团大神丁志虎在微博上答复的,摘录如下:

  • 这个方案没法解决不同渠道使用渠道自己SDK的问题,友盟的SDK提供了在代码中设置渠道的方式,所以再获取到渠道号后再调用SDK相关设置渠道的方法就可以了
  • apk用的是java那一套签名,放在META-INF文件夹里的文件原则上是不参与签名的。如果Google修改了apk的签名规则,这一套可能就不适用了。

声明

欢迎转载,但请保留文章原始出处
作者:GavinCT
出处:http://www.cnblogs.com/ct2011/
0 0
原创粉丝点击