Android 多渠道打包

来源:互联网 发布:我国粮食产量 知乎 编辑:程序博客网 时间:2024/05/17 02:25

Android 的Gradle多渠道打包

配置AndroidMainfest.xml

以友盟渠道为例,渠道信息一般都是写在 AndroidManifest.xml文件中,代码大约如下:

<meta-data android:name="UMENG_CHANNEL" android:value="xiaomi" />

如果不使用多渠道打包方法,那就需要我们手动一个一个去修改value中的值,xiaomi,360,qq,wandoujia等等。
使用多渠道打包的方式,就需要把上面的value配置成下面的方式:

<meta-data    android:name="UMENG_CHANNEL"                                              android:value="${UMENG_CHANNEL_VALUE}" />

其中${UMENG_CHANNEL_VALUE}中的值就是你在gradle中自定义配置的值。

在build.gradle中配置productFlavors

productFlavors {     wandoujia {          manifestPlaceholders = [UMENG_CHANNEL_VALUE: "wandoujia"]     }     xiaomi{          manifestPlaceholders = [UMENG_CHANNEL_VALUE: "xiaomi"]     }     qq {          manifestPlaceholders = [UMENG_CHANNEL_VALUE: "qq"]     }     _360 {          manifestPlaceholders = [UMENG_CHANNEL_VALUE: "360"]     }}

其中[UMENG_CHANNEL_VALUE: “wandoujia”]就是对应${UMENG_CHANNEL_VALUE}的值。
我们可以发现,按照上面的方式写,比较繁琐,其实还有更简洁的方式去写,方法如下:

android {     productFlavors {        wandoujia{}        xiaomi{}        qq{}        _360 {}    }     productFlavors.all {         flavor -> flavor.manifestPlaceholders = [UMENG_CHANNEL_VALUE: name]         }}

其中name的值对相对应各个productFlavors的选项值,这样就达到自动替换渠道值的目的了。
这样生成apk时,选择相应的Flavors来生成指定渠道的包就可以了,而且生成的apk会自动帮你加上相应渠道的后缀,非常方便和直观。大家可以自己反编译验证。

配置签名信息

//签名signingConfigs{     appsign{          storeFile file("keystore路径")          storePassword "***"          keyAlias "***"          keyPassword "***"     }}buildTypes {        release {            runProguard false            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'            signingConfig signingConfigs.appsign        }}

注意:signingConfig signingConfigs.appsign:这段代码不可少,作用是打包的时候连带签名信息一起打进去APK。否则在安装生成的APK的时候会出现下面这个错误信息:
install_parse_failed_no_certificates
这里写图片描述

修改导出APK的名称

我们可以根据渠道自定义apk的名称

android {    applicationVariants.all { variant ->        variant.outputs.each { output ->            output.outputFile = new File(                    output.outputFile.parent,                    "xxxx(apk的名字)-${variant.buildType.name}-${defaultConfig.versionName}-${variant.productFlavors[0].name}.apk".toLowerCase())        }    }}

最后打包完成之后,apk文件就会生成在项目的build\outputs\apk下。

配置Gradle的环境变量

(一)Windows平台下配置Gradle:

我们可以使用CMD命令,进入到项目所在的目录,直接输入命令:

gradle assembleRelease
就开始打包了,如果渠道很多的话,时间可能会很长。或者,当然Android Studio中的下方底栏中有个命令行工具Terminal,你也可以直接打开,输入上面的命令:

gradle assembleRelease

用CMD进入到项目所在目录执行,或者用AS中自带的命令行工具Terminal其实性质都是一样的。
注意:如果没有对gradle配置的话,可能输入上面的命令,会提示“不是内部或者外部命令”,不要着急,我们只需要找到gradle的目录,把它配置到电脑中的环境变量中去即可。
配置方式如下:

1)先找到gralde的根目录,在系统变量里添加两个环境变量:

变量名为:GRADLE_HOME,变量值就为gradle的根目录;
所以变量值为:D:\android\android-studio-ide-143.2739321-windows\android-studio\gradle\gradle-2.10

2)还有一个在系统变量里PATH里面添加gradle的bin目录
D:\android\android-studio-ide-143.2739321-windows\android-studio\gradle\gradle-2.10\bin
这样就配置完了,,执行以下这个命令:gradle assembleRelease。

(二)Linux平台下配置Gradle:
1)配置profile

$ sudo vim /etc/profile

在文件末尾添加:

export GRADLE_HOME=/XX/XXX/gradle-2.10export PATH=$GRADLE_HOME/bin:$PATH

2)重启
重启机器,然后就可以运行 gradle

$ sudo reboot$ gradle 

运行完Gradle会出现如图所示的信息:

这里写图片描述

如果不配置Gradle会出现的问题:

1)

这里写图片描述
2)

这里写图片描述

如果配置完Gradle仍然出现上图的错误,则需要删除工程下面的.gradle和gradle文件,然后重新导入工程即可。其他的操作和Windows平台下一致

打包

当然Android Studio中的下方底栏中有个命令行工具Terminal,你也可以直接打开,输入上面的命令:

gradle assembleRelease

备注:Gradle方式打包的缺点是如果需要渠道包特别多的时候,则会非常的慢。耗费大量的时间。另外使用Gradle还可以适配不同的渠道包(如果有需求参考第二章)。

参考文档:

http://mp.weixin.qq.com/s?__biz=MjM5NDkxMTgyNw==&mid=2653057549&idx=1&sn=456fa138f2fd307a3ff94eddc5ff2e73&scene=21#wechat_redirect

Gradle适配多渠道包(来自美团技术分享(原文))

概述

随着渠道越来越多,不同渠道对应用的要求也不尽相同。例如,有的渠道要求美团客户端的应用名为美团,有的渠道要求应用名为美团团购。又比如,有些渠道要求应用不能使用第三方统计工具(如flurry)。总之,每次打包都需要对这些渠道进行适配。

之前的做法是为每个需要适配的渠道创建一个Git分支,发版时再切换到相应的分支,并合并主分支的代码。适配的渠道比较少的话这种方式还可以接受,如果分支比较多,对开发人员来说简直就是噩梦。还好,自从有了Gradle flavor,一切都变得简单了。本文假定读者使用过Gradle,如果还不了解建议先阅读相关文档。

flavor的配置

先来看build.gradle文件中的一段代码:

android {    ....    productFlavors {        flavor1 {            minSdkVersion 14        }    }}

上例定义了一个flavor:flavor1,并指定了应用的minSdkVersion为14(当然还可以配置更多的属性,具体可参考相关文档)。与此同时,Gradle还会为该flavor关联对应的sourceSet,默认位置为src/目录,对应到本例就是src/flavor1。

接下来,要做的就是根据具体的需求在build.gradle文件中配置flavor,并添加必要的代码和资源文件。以flavor1为例,运行gradle assembleFlavor1命令既可生成所需的适配包。下面主要介绍美团团购Android客户端的一些适配案例。

案例

使用不同的包名

使用不同的包名,美团团购Android客户端之前有两个版本:手机版(com.meituan.group)和hd版(com.meituan.group.hd),两个版本使用了不同的代码。目前hd版对应的代码已不再维护,希望能直接使用手机版的代码。解决该问题可以有多种方法,不过使用flavor相对比较简单,示例如下:

productFlavors {    hd {        applicationId "com.meituan.group.hd"    }}

上面的代码添加了一个名为hd的flavor,并指定了应用的包名为com.meituan.group.hd,运行gradle assembleHd命令即可生成hd适配包

控制是否自动更新

美团团购Android客户端在启动时会默认检查客户端是否有更新,如果有更新就会提示用户下载。但是有些渠道和应用市场不允许这种默认行为,所以在适配这些渠道时需要禁止自动更新功能。

解决的思路是提供一个配置字段,应用启动的时候检查该字段的值以决定是否开启自动更新功能。使用flavor可以完美的解决这类问题。

Gradle会在generateSources阶段为flavor生成一个BuildConfig.java文件。BuildConfig类默认提供了一些常量字段,比如应用的版本名(VERSION_NAME),应用的包名(PACKAGE_NAME)等。更强大的是,开发者还可以添加自定义的一些字段。下面的示例假设wandoujia市场默认禁止自动更新功能:

android {    defaultConfig {        buildConfigField "boolean", "AUTO_UPDATES", "true"    }    productFlavors {        wandoujia {            buildConfigField "boolean", "AUTO_UPDATES", "false"        }            }}

上面的代码会在BuildConfig类中生成AUTO_UPDATES布尔常量,默认值为true,在使用wandoujia flavor时,该值会被设置成false。接下来就可以在代码中使用AUTO_UPDATES常量来判断是否开启自动更新功能了。最后,运行gradle assembleWandoujia命令即可生成默认不开启自动升级功能的渠道包,是不是很简单。

使用不同的应用名

最常见的一类适配是修改应用的资源。例如,美团团购Android客户端的应用名是美团,但有的渠道需要把应用名修改为美团团购;还有,客户端经常会和一些应用分发市场合作,需要在应用的启动界面中加上第三方市场的Logo,类似这类适配形式还有很多。
Gradle在构建应用时,会优先使用flavor所属dataSet中的同名资源。所以,解决思路就是在flavor的dataSet中添加同名的字符串资源,以覆盖默认的资源。下面以适配wandoujia渠道的应用名为美团团购为例进行介绍。

首先,在build.gradle配置文件中添加如下flavor:

android {    productFlavors {        wandoujia {         }    }}

上面的配置会默认src/wandoujia目录为wandoujia flavor的dataSet。

接下来,在src目录内创建wandoujia目录,并添加如下应用名字符串资源(src/wandoujia/res/values/appname.xml):

<resources>    <string name="app_name">美团团购</string></resources>

默认的应用名字符串资源如下(src/main/res/values/strings.xml):

<resources>    <string name="app_name">美团</string></resources>

最后,运行gradle assembleWandoujia命令即可生成应用名为美团团购的应用了。

使用第三方SDK

某些渠道会要求客户端嵌入第三方SDK来满足特定的适配需求。比如360应用市场要求美团团购Android客户端的精品应用模块使用他们提供的SDK。问题的难点在于如何只为特定的渠道添加SDK,其他渠道不引入该SDK。使用flavor可以很好的解决这个问题,下面以为qihu360 flavor引入com.qihoo360.union.sdk:union:1.0 SDK为例进行说明:

android {    productFlavors {        qihu360 {        }    }}...dependencies {    provided 'com.qihoo360.union.sdk:union:1.0'    qihu360Compile 'com.qihoo360.union.sdk:union:1.0'}

上例添加了名为qihu360的flavor,并且指定编译和运行时都依赖com.qihoo360.union.sdk:union:1.0。而其他渠道只是在构建的时候依赖该SDK,打包的时候并不会添加它。

接下来,需要在代码中使用反射技术判断应用程序是否添加了该SDK,从而决定是否要显示360 SDK提供的精品应用。部分代码如下:

class MyActivity extends Activity {    private boolean useQihuSdk;    @override    public void onCreate(Bundle savedInstanceState) {        try {            Class.forName("com.qihoo360.union.sdk.UnionManager");            useQihuSdk = true;        } catch (ClassNotFoundException ignored) {        }    }}

最后,运行gradle assembleQihu360命令即可生成包含360精品应用模块的渠道包了。

参考文档:

http://tech.meituan.com/mt-apk-adaptation.html

Android的META_INF多渠道打包(来自美团技术分享)

原理介绍

美团高效的多渠道打包方案是把一个Android应用程序包当作一个zip文件包进行解压,然后发现在签名生成的目录下添加一个空文件,空文件用渠道名来命名,而且不需要重新签名。这种方式不需要重新签名,编译等步骤,使得这种方法非常高效。
如果能直接修改apk的渠道号,而不需要再重新签名能节省不少打包的时间。幸运的是我们找到了这种方法。直接解压apk,解压后的根目录会有一个META-INF目录,如下图所示:

这里写图片描述

如果在META-INF目录内添加空文件,可以不用重新签名应用。因此,通过为不同渠道的应用添加不同的空文件,可以唯一标识一个渠道。

下面的python代码用来给apk添加空的渠道文件,渠道名的前缀为cztchannel_:

用python脚本向apk文件中添加空渠道文件

假定目录是:/home/XXX/XXXX/multibuildtool
1)配置python环境
Windows下需要配置python开发环境。Linux默认有python开发环境
2)配置渠道列表
将渠道包列表文件channel.txt放在上述目录里面。这个是一个样例:

samsungappshiapkanzhi360cnxiaomimyapp91comgfanappchinanduoa3gcnmumayi10086comwostore189storelenovommhicloudmeizubaidugoogleplaywandou

3)编写脚本
将multiChannelBuildTool.py也放在这个目录下面。这个是python代码:

#coding=utf-8#!/usr/bin/pythonimport zipfileimport shutilimport os# 空文件 便于写入此空文件到apk包中作为channel文件(指定特定目录文件)src_empty_file = '/home/XXX/XXXX/XXXX/czt.txt'# 创建一个空文件(不存在则创建)f = open(src_empty_file, 'w') f.close()# 获取当前目录中所有的apk源包src_apks = []# python3 : os.listdir()即可,这里使用兼容Python2的os.listdir('.')directs = '/home/XXXX/XXXX/XXXXX/'for file in os.listdir('/XXXX/XXXX/XXXX/XXXX'):    #######打印出拼接的字符串的结果    print os.path.join(directs,file)        if os.path.isfile(os.path.join(directs,file)):        extension = os.path.splitext(file)[1][1:]    print extension####不加上not in 的条件判断会出现如果没有后缀的文件依旧会执行append方法        if extension in 'apk' and extension not in "":            src_apks.append(file)        print "apk"print len(src_apks)# 获取渠道列表channel_file = '/XXXX/XXXX/XXXX/XXXX/channel.txt'f = open(channel_file)lines = f.readlines()f.close()for src_apk in src_apks:    # file name (with extension)    print src_apk    src_apk_file_name = os.path.basename(src_apk)    # 分割文件名与后缀    temp_list = os.path.splitext(src_apk_file_name)    # name without extension   Apk的文件名称    src_apk_name = temp_list[0]    # 后缀名,包含.   例如: ".apk "    src_apk_extension = temp_list[1]    # 创建生成目录,与文件名相关    output_dir = 'output_' + src_apk_name + '/'    # 目录不存在则创建    if not os.path.exists(output_dir):        os.mkdir(output_dir)    # 遍历渠道号并创建对应渠道号的apk文件    for line in lines:        # 获取当前渠道号,因为从渠道文件中获得带有\n,所有strip一下        target_channel = line.strip()        # 拼接对应渠道号的apk        target_apk = output_dir + src_apk_name + "-" + target_channel + src_apk_extension          # 拷贝建立新apk        shutil.copy(src_apk,  target_apk)        # zip获取新建立的apk文件        zipped = zipfile.ZipFile(target_apk, 'a', zipfile.ZIP_DEFLATED)        # 初始化渠道信息        empty_channel_file = "META-INF/cztchannel_{channel}".format(channel = target_channel)        # 写入渠道信息        zipped.write(src_empty_file, empty_channel_file)        # 关闭zip流    print target_channel        zipped.close()

复制签好名的包,运行脚本

选择一个之前打好的APK,也放在同一个目录下面。然后执行python脚本。
命令如下:python /home/dq/桌面/multibuildtool/multiChannelBuildTool.py.
执行完Python脚本以后会在这个目录下面,生成output_(APK名称)的文件夹,里面有相关的APK文件
3.3用java代码读取渠道名,并动态设置渠道名
我们用脚本生成了文件之后,文件的名字是用渠道名来命名的,所以我们在启动程序的时候,可以用java代码动态读取渠道名,并动态的去设置。
java代码读取渠道名的方法:

package XXXXXXX.utils;import android.content.Context;import android.content.SharedPreferences;import android.content.pm.ApplicationInfo;import android.content.pm.PackageManager;import android.preference.PreferenceManager;import android.text.TextUtils;import java.io.IOException;import java.util.Enumeration;import java.util.zip.ZipEntry;import java.util.zip.ZipFile;/** * 获取渠道包的类(如果使用了umneg的数据统计可以直接将结果用字setChannel()umeng的一个方法) * Createod by dengquan on 16-6-7. */public class ChannelUtil{    private static final String CHANNEL_KEY = "cztchannel";    private static final String CHANNEL_VERSION_KEY = "cztchannel_version";    private static String mChannel;    /**     * 返回市场。  如果获取失败返回""     * @param context     * @return     */    public static String getChannel(Context context){        return getChannel(context, "");    }    /**     * 返回市场。  如果获取失败返回defaultChannel     * @param context     * @param defaultChannel     * @return     */    public static String getChannel(Context context, String defaultChannel) {        //内存中获取        if(!TextUtils.isEmpty(mChannel)){            return mChannel;        }        //sp中获取        mChannel = getChannelBySharedPreferences(context);        if(!TextUtils.isEmpty(mChannel)){            return mChannel;        }        //从apk中获取        mChannel = getChannelFromApk(context, CHANNEL_KEY);        if(!TextUtils.isEmpty(mChannel)){            //保存sp中备用            saveChannelBySharedPreferences(context, mChannel);            return mChannel;        }        //全部获取失败        return defaultChannel;    }    /**     * 从apk中获取版本信息     * @param context     * @param channelKey     * @return     */    private static String getChannelFromApk(Context context, String channelKey) {        //从apk包中获取        ApplicationInfo appinfo = context.getApplicationInfo();        String sourceDir = appinfo.sourceDir;        //默认放在meta-inf/里, 所以需要再拼接一下        String key = "META-INF/" + channelKey;        String ret = "";        ZipFile zipfile = null;        try {            zipfile = new ZipFile(sourceDir);            Enumeration<?> entries = zipfile.entries();            while (entries.hasMoreElements()) {                ZipEntry entry = ((ZipEntry) entries.nextElement());                String entryName = entry.getName();                if (entryName.startsWith(key)) {                    ret = entryName;                    break;                }            }        } catch (IOException e) {            e.printStackTrace();        } finally {            if (zipfile != null) {                try {                    zipfile.close();                } catch (IOException e) {                    e.printStackTrace();                }            }        }        String[] split = ret.split("_");        String channel = "";        if (split != null && split.length >= 2) {            channel = ret.substring(split[0].length() + 1);        }        return channel;    }    /**     * 本地保存channel & 对应版本号     * @param context     * @param channel     */    private static void saveChannelBySharedPreferences(Context context, String channel){        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);        SharedPreferences.Editor editor = sp.edit();        editor.putString(CHANNEL_KEY, channel);        editor.putInt(CHANNEL_VERSION_KEY, getVersionCode(context));        editor.commit();    }    /**     * 从sp中获取channel     * @param context     * @return 为空表示获取异常、sp中的值已经失效、sp中没有此值     */    private static String getChannelBySharedPreferences(Context context){        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);        int currentVersionCode = getVersionCode(context);        if(currentVersionCode == -1){            //获取错误            return "";        }        int versionCodeSaved = sp.getInt(CHANNEL_VERSION_KEY, -1);        if(versionCodeSaved == -1){            //本地没有存储的channel对应的版本号            //第一次使用  或者 原先存储版本号异常            return "";        }        if(currentVersionCode != versionCodeSaved){            return "";        }        return sp.getString(CHANNEL_KEY, "");    }    /**     * 从包信息中获取版本号     * @param context     * @return     */    private static int getVersionCode(Context context){        try{            return context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionCode;        }catch(PackageManager.NameNotFoundException e) {            e.printStackTrace();        }        return -1;    }}

读取到了渠道名,我们就可以动态的设置了,比如友盟渠道的动态设置方法是:AnalyticsConfig.setChannel(getChannel(Context context) );这样就好了。这种方式每打一个渠道包只需复制一个apk,在META-INF中添加一个使用渠道号命名的空文件即可。这种打包方式速度非常快,据说900多个渠道不到一分钟就能打完。我亲测的是我用了10秒钟打了32个渠道包,是不是很快。

友盟设置渠道包代码:

private void initUmneg()    {        MobclickAgent.UMAnalyticsConfig config = new MobclickAgent.UMAnalyticsConfig(this, Constant.UMeng.APP_KEY,getChannel());        MobclickAgent.startWithConfigure(config);    }

相关问题参考这个Github的文档介绍。

参考文档:
http://mp.weixin.qq.com/s?__biz=MjM5NDkxMTgyNw==&mid=2653057569&idx=1&sn=0fa214999538a7ae8e5964d729377827#wechat_redirect

http://tech.meituan.com/mt-apk-packaging.html

https://github.com/GavinCT/AndroidMultiChannelBuildTool

其他参考文档(Android打包流程介绍):

http://blog.csdn.net/jason0539/article/details/44917745

http://blog.csdn.net/luoshengyang/article/details/8744683

http://www.cnblogs.com/royi123/p/3576746.html

1 0