Android 多渠道打包

Posted android小菜比

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 多渠道打包相关的知识,希望对你有一定的参考价值。

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.10
export 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放在上述目录里面。这个是一个样例:

samsungapps
hiapk
anzhi
360cn
xiaomi
myapp
91com
gfan
appchina
nduoa
3gcn
mumayi
10086com
wostore
189store
lenovomm
hicloud
meizu
baidu
googleplay
wandou

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

#coding=utf-8
#!/usr/bin/python
import zipfile
import shutil
import 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

以上是关于Android 多渠道打包的主要内容,如果未能解决你的问题,请参考以下文章

Android 多渠道打包

android 多渠道打包

Android Studio多渠道打包和代码混淆教程 亲测可用

Android grade语法,多渠道打包

Android 进行友盟多渠道打包步骤详解

借腾讯开源 VasDolly,谈谈 Android 签名和多渠道打包的原理!