原文链接:http://blog.csdn.net/javazejian/article/details/50760590

今天终于要来给大家介绍Python多渠道打包啦,我也是很激动,当初虽然有gradle这样方便的打包方式,但是一旦渠道数量多了起来,gradle打包的时间也会成为一个瓶颈,之前打20个渠道左右,用gradle打包的话大概要花上20多分钟,如果以后渠道增加到上百个那就真的呵呵了!不过现在即使再多的渠道包也没关系啦,有python在都是秒秒钟搞定的时,python打包是美团工程师的杰作,在此十分感谢哈!用python脚步打包的话,打20个渠道左右的包大概只要花上不到5分钟的时间,十分的快啊!这酸爽的感觉,太刺激了。接下来我们就开始吧,python方式的打包需要做一下准备(本节涉及的所有文件我在最后都会提供给大家下载):

python脚本多渠道打包应用

1.改动签名好的apk

这个apk里面的渠道配置信息跟gradle多渠道打包的配置有些不一样哈。之前我们是需要在AndroidManifest.xml文件中渠道信息,现在用python打包的话就不用啦,我们直接在启动的activity文件中设置就行了,设置代码如下:

[java] view plain copy
 print?
  1. //动态设置渠道信息  
  2. String channel= ChannelUtil.getChannel(this);  
  3. AnalyticsConfig.setChannel(channel);  

ChannelUtil.Java类是一个获取渠道信息的类,这个类到时会提供给大家,而AnalyticsConfig则是友盟提供的设置渠道类,我们在友盟官网可以看到这样的介绍:

python脚本多渠道打包应用

因此我们使用的就是第2种设置渠道的方式。嗯,这就是唯一与gradle打包的不同点,同时要注意,这个签名的apk不需要提前设置任何渠道,所以在gradle配置文件中无需使用productFlavors属性来设置渠道名称。

2.Python打包的实现思路详解

说完了apk的区别设置后,我们先来聊聊python打包的实现思路。我们先获取一个打包好的apk,并把后缀改为zip,解压如下:
python脚本多渠道打包应用
这是一个已经签名打包的apk,解压后我们可以看到apk中包含了一个名称为META-INF的文件夹,其实python打包的奥秘就在于此了,因为每个apk都会包含这样一个名称为META-INF文件夹,所以我们可以利用python脚步在该文件夹下创建一个空的文件,这个文件的名称就命名为channel_xxx.txt,该文件并没有任何内容,仅作为渠道标志,比如现在是华为渠道,那么该文件的名称就是channel_huawei.txt,如果现在的渠道是xiaomi,那么该文件的名称就是channel_xiaomi.txt,为了验证以上的说法我们先来看看已经利用python打包好的apk:
python脚本多渠道打包应用
然后我们打开其中几个apk看看META-INF文件夹下是否有对应的渠道文件:
python脚本多渠道打包应用

确实如我们上面所说的一样,每个apk的META-INF文件夹中都含有一个渠道名称的文件。那这个渠道名称的文件是如何创建的呢,确实我们自己通过as打包apk时是不可能含有该渠道名称文件的,而这也正是python的功劳了,我们把已经签名好的apk,通过python脚步的for循环语句去解压我们已经打包签名好的apk,然后在每个apk的META-INF文件夹中通过python脚步去创建一个渠道名称的文件,创建完成后在重新还原成apk输出,就这样渠道名称文件就被设置到apk中啦。那么这个python循环是依据什么开始的呢,还记得我们开头提到过的channel.txt文件嘛?该文件内容如下:

[java] view plain copy
 print?
  1. xiaomi  
  2. huawei  
  3. yingyongbao  
  4. 360mobile  
  5. wandoujia  
  6. anzhuo_market  
  7. baidu  
  8. 91market  
  9. anzhi_market  
  10. googleplay  
大家可能已经猜到了,没错,python脚步在开始时会去读取这个文件,根据这个文件的渠道名称去进行for循环,然后把每个渠道名称以channel_xxx结尾作为文件的名称。我们不妨看看python脚步的源码 :
[python] view plain copy
 print?
  1. #coding=utf-8  
  2. import zipfile  
  3. import shutil  
  4. import os  
  5. import sys  
  6. if __name__ == '__main__':  
  7.     apkFile = sys.argv[1]  
  8.     apk = apkFile.split('.apk')[0]  
  9.     # print apkFile  
  10.     emptyFile = 'xxx.txt'  
  11.     f = open(emptyFile, 'w')  
  12.     f.close()  
  13.     with open('./android_channels.txt''r') as f:  
  14.         contens = f.read()  
  15.     lines = contens.split('\n')  
  16.     os.mkdir('./release')  
  17.     #print lines[0]  
  18.     for line in lines:  
  19.         channel = 'channel_' + line  
  20.         destfile = './release/%s_%s.apk' % (apk, channel)  
  21.         shutil.copy(apkFile, destfile)  
  22.         zipped = zipfile.ZipFile(destfile, 'a')  
  23.         channelFile = "META-INF/{channelname}".format(channelname=channel)  
  24.         zipped.write(emptyFile, channelFile)  
  25.         zipped.close()  
  26.     os.remove('./xxx.txt')  
  27.     #mac  
  28.     os.system('chmod u+x zipalign_batch.sh')  
  29.     os.system('./zipalign_batch.sh')  
  30.     #windows  
  31.     #os.system('zipalign_batch.bat')  
代码并不太复杂(我也只是懂一些python的基础哈,也还在慢慢学习中),我们可以看到一开始会去读取android_channels.txt的渠道文件,然后创建一个release的文件夹,然后就进入for循环了,后面我们就不过多讨论了,大概明白意思就行。通过上面的分析我们也大概明白了channel_xxx.txt文件是如何被写入每个apk的,同时也知道了android_channels.txt这个文件的作用。但是在apk中写入channel_xxx.txt渠道名称的文件有什么用呢,这个就是ChannelUtil.java工具类的作用了,还记得我们的渠道是怎么设置的嘛?
[java] view plain copy
 print?
  1. //动态设置渠道信息  
  2. String channel= ChannelUtil.getChannel(this);  
  3. AnalyticsConfig.setChannel(channel);  
没错,在应用启动时,ChannelUtil.java工具类会去读取META-INF文件下的channel_xxx.txt文件的名称,并通过拆分去掉channel_字符串,从而获取到渠道名称,最后就可以通过友盟api的接口发送友盟服务器了。ChannelUtil.java源码如下:
[java] view plain copy
 print?
  1. package com.zejian.application.utils;  
  2. import android.content.Context;  
  3. import android.content.pm.ApplicationInfo;  
  4. import android.content.pm.PackageManager.NameNotFoundException;  
  5. import android.text.TextUtils;  
  6. import java.io.IOException;  
  7. import java.util.Enumeration;  
  8. import java.util.zip.ZipEntry;  
  9. import java.util.zip.ZipFile;  
  10. /** 
  11.  * Created by WuZeJian 
  12.  * Time:2015/12/1 17:57 
  13.  * Email:[email protected] 
  14.  * Description:打包渠道工具类 
  15.  */  
  16. public class ChannelUtil {  
  17.       
  18.     private static final String CHANNEL_KEY = "channel";  
  19.     private static final String CHANNEL_DEFAULT = "offical";  
  20.       
  21.     private static final String PREF_KEY_CHANNEL = "pref_key_channel";  
  22.     private static final String PREF_KEY_CHANNEL_VERSION = "pref_key_channel_version";  
  23.       
  24.     private static String mChannel;  
  25.       
  26.     /** 
  27.      * 返回市场。  如果获取失败返回"" 
  28.      * @param context 
  29.      * @return 
  30.      */  
  31.     public static String getChannel(Context context){  
  32.         return getChannel(context, CHANNEL_DEFAULT);  
  33.     }  
  34.       
  35.     /** 
  36.      * 返回市场。  如果获取失败返回defaultChannel 
  37.      * @param context 
  38.      * @param defaultChannel 
  39.      * @return 
  40.      */  
  41.     public static String getChannel(Context context, String defaultChannel) {  
  42.         //内存中获取  
  43.         if(!TextUtils.isEmpty(mChannel)){  
  44.             return mChannel;  
  45.         }  
  46.         //sp中获取  
  47.         mChannel = getChannelFromSP(context);  
  48.         if(!TextUtils.isEmpty(mChannel)){  
  49.             return mChannel;  
  50.         }  
  51.         //从apk中获取  
  52.         mChannel = getChannelFromApk(context, CHANNEL_KEY);  
  53.         if(!TextUtils.isEmpty(mChannel)){  
  54.             //保存sp中备用  
  55.             saveChannelInSP(context, mChannel);  
  56.             return mChannel;  
  57.         }  
  58.         //全部获取失败  
  59.         return defaultChannel;  
  60.     }  
  61.       
  62.     /** 
  63.      * 从apk中获取版本信息 
  64.      * @param context 
  65.      * @param channelKey 
  66.      * @return 
  67.      */  
  68.     private static String getChannelFromApk(Context context, String channelKey) {  
  69.         //从apk包中获取  
  70.         ApplicationInfo appinfo = context.getApplicationInfo();  
  71.         String sourceDir = appinfo.sourceDir;  
  72.         //默认放在meta-inf/里, 所以需要再拼接一下  
  73.         String key = "META-INF/" + channelKey;  
  74.         String ret = "";  
  75.         ZipFile zipfile = null;  
  76.         try {  
  77.             zipfile = new ZipFile(sourceDir);  
  78.             Enumeration<?> entries = zipfile.entries();  
  79.             while (entries.hasMoreElements()) {  
  80.                 ZipEntry entry = ((ZipEntry) entries.nextElement());  
  81.                 String entryName = entry.getName();  
  82.                 LogUtils.d("APK entry name --------> " + entryName);  
  83.                 if (entryName.startsWith(key)) {  
  84.                     ret = entryName;  
  85.                     break;  
  86.                 }  
  87.             }  
  88.         } catch (IOException e) {  
  89.             LogUtils.e(e);  
  90.         } finally {  
  91.             if (zipfile != null) {  
  92.                 try {  
  93.                     zipfile.close();  
  94.                 } catch (IOException e) {  
  95.                         LogUtils.e(e);  
  96.                 }  
  97.             }  
  98.         }  
  99.         String[] split = ret.split("_");  
  100.         String channel = "";  
  101.         if (split != null && split.length >= 2) {  
  102.                 channel = ret.substring(split[0].length() + 1);  
  103.         }  
  104.         return channel;  
  105.     }  
  106.       
  107.     /** 
  108.      * 本地保存channel & 对应版本号 
  109.      * @param context 
  110.      * @param channel 
  111.      */  
  112.     private static void saveChannelInSP(Context context, String channel){  
  113.         SharedPreferencedUtils.setString(context, PREF_KEY_CHANNEL, channel);  
  114.         SharedPreferencedUtils.getInteger(context, PREF_KEY_CHANNEL_VERSION, getVersionCode(context));  
  115.     }  
  116.       
  117.     /** 
  118.      * 从sp中获取channel 
  119.      * @param context 
  120.      * @return 为空表示获取异常、sp中的值已经失效、sp中没有此值 
  121.      */  
  122.     private static String getChannelFromSP(Context context){  
  123.         int currentVersionCode = getVersionCode(context);  
  124.         if(currentVersionCode == -1){  
  125.             //获取错误  
  126.             return "";  
  127.         }  
  128.         int versionCodeSaved = SharedPreferencedUtils.getInteger(context, PREF_KEY_CHANNEL_VERSION, -1);  
  129.         if(versionCodeSaved == -1){  
  130.             //本地没有存储的channel对应的版本号  
  131.             //第一次使用  或者 原先存储版本号异常  
  132.             return "";  
  133.         }  
  134.         if(currentVersionCode != versionCodeSaved){  
  135.             return "";  
  136.         }  
  137.         return SharedPreferencedUtils.getString(context, PREF_KEY_CHANNEL, "");  
  138.     }  
  139.       
  140.     public static int getVersionCode(Context context) {  
  141.         try{  
  142.             return context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionCode;  
  143.         }catch(NameNotFoundException e) {  
  144.             LogUtils.e(e);  
  145.         }  
  146.         return -1;  
  147.     }  
  148.       
  149. }  
好了,到此python打包的思路基本讲完了,我们先来小结:首先我们先准备一个打包签名好的apk,然后通过python脚步去解压该apk,并在apk的META-INF目录下创建一个名称channel_xxx.txt的渠道名称文件,然后再重新打包成apk,这个channel_xxx.txt的渠道名称文件在apk应用启动时会被一个名称为ChannelUtil.java的工具类读取,该工具通过META-INF目录下的channel_xxx.txt获取到渠道名称并设置给友盟,这样就完成了友盟渠道信息的记录。

3.Python打包实战记录(记得集成友盟统计哈)

说了这么多还是赶紧来实战吧,首先我们要配置key,以及相应的工具类,我们使用的项目结构如下:

python脚本多渠道打包应用

然后我们打包出一个签名好的apk,并把它放到和python脚步同一个目录下:

python脚本多渠道打包应用

然后打开我们的命令终端,cd到该目录下,输入如下命令,回车。

[html] view plain copy
 print?
  1. python channel.py app-debug4zj.apk  
python脚本多渠道打包应用
成功后我们在看看该目录下多出一个release的文件夹(没有经过zipalign优化的apk),里面还有一个zipalign的文件夹(经过zipalign优化的apk),如下:

python脚本多渠道打包应用

就这样我们的apk都打包好啦,至于zipalign优化有什么作用,我在第一篇文章有详细的说明,大家可以移步看看哈。这里我就不重复了。还有等会我会把工具提供给大家如果是mac系统的话,python已经默认安装好的了哈,如果是window系统就先要安装python环境哈。还有这里提供两种zipalign_batch.bat(window平台用)和zipalign_batch.sh(mac平台使用)脚本。同时要记得配置好zipalign的环境变量哈(该工具在androidSdk/build-tools目录下)。最后还有一点要注意的是channel.py脚本文件最后的代码设置如下:

python脚本多渠道打包应用

4.使用友盟渠道统计验证一下打包结果

说了这么多,操作也讲解了,最后到底靠不靠谱,还是得用友盟来检测一下对吧。我们还是使用前篇文章的测试设备vivoxplay3s ,上次测试完后现在的初始数据如下

python脚本多渠道打包应用

我们依次安装baidu,91market,anzhi_market,googleplay_market,yingyongbao,测试结果如下:

python脚本多渠道打包应用

看来还是很靠谱的嘛,最重要的是速度,速度,速度啊。赶紧去试试吧哈。 本篇资料下载:

http://download.csdn.net/detail/javazejian/9446843

相关文章: