使用gradle进行多渠道打包
使用Android studio
基本多渠道打包
以友盟的为例
渠道信息一般在 AndroidManifest.xml
中修改以下值:
<meta-data android:name="UMENG_CHANNEL" android:value="wandoujia" />
首先你必须在AndroidManifest.xml
中的meta-data修改以下的样子:
<meta-data android:name="UMENG_CHANNEL" android:value="${UMENG_CHANNEL_VALUE}" />
其中${UMENG_CHANNEL_VALUE}
中的值就是你在gradle中自定义配置的值。
build.gradle
文件就利用productFlavors
这样写:
productFlavors {
wandoujia {}
baidu {}
c360 {}
uc {}
productFlavors.all { flavor ->
flavor.manifestPlaceholders = [UMENG_CHANNEL_VALUE: name]
}
}
其中name的值对相对应各个productFlavors
的选项值,这样就达到自动替换渠道值的目的了。
这样生成apk时,选择相应的Flavors来生成指定渠道的包就可以了,而且生成的apk会自动帮你加上相应渠道的后缀,非常方便和直观。大家可以自己反编译验证。
还可以一次生成所有渠道包.
在as底栏中有个命令行工具Terminal,打开后就就在当前项目的目录下。
输入这个命令:
gradlew assembleRelease
就可以一次性生成所有的渠道包了
所有生成的apk在项目的build\outputs\apk
下
第一次使用命令可能需要下载gradle,请自备梯子。
进阶:适配渠道包
主要是使用productFlavors这个DSL容器进行渠道包的适配,如果对gradle不够了解,可以看看这个系列的教程http://blog.csdn.net/qinxiandiqi/article/category/2394347
看下面这部分代码:
android {
....
productFlavors {
flavor1 {
minSdkVersion 14
}
}
}
上例定义了一个flavor:flavor1,并指定了应用的minSdkVersion为14(当然还可以配置更多的属性,具体可参考相关文档)。与此同时,Gradle还会为该flavor关联对应的sourceSet,默认位置为src/目录,对应到本例就是src/flavor1。
接下来,要做的就是根据具体的需求在build.gradle文件中配置flavor,并添加必要的代码和资源文件。以flavor1为例,运行gradle assembleFlavor1命令既可生成所需的适配包。
使用不同的包名
示例如下:productFlavors { test1 { applicationId "com.app.test1" } }
上面的代码添加了一个名为test1的flavor,并指定了应用的包名为com.app.test1,运行gradle assembleTest1命令即可生成test1适配包。
使用不同的应用名
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精品应用模块的渠道包了。
使用Ant多渠道打包
针对使用Eclipse的用户
跟着这个链接做(图文教程)http://blog.csdn.net/zhaokaiqiang1992/article/details/38086747
因为我的需求是多渠道,多项目打包,所以略微修改了以下,如果是单个项目,完全使用上面链接中的方式即可
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.FilenameFilter;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import org.apache.tools.ant.DefaultLogger;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.ProjectHelper;
public class AntTest {
private Project project;
private static String sourceProjectPath;// 要打包的项目根目录
private static String targetApkPath;// 保存打包之后的apk的根目录
private static String signApk;// 这里的文件名必须是准确的项目名!就是Project工程的bin目录下面的apk安装包的名字
private static String signedApkPrex;// 重命名之后的apk名称前缀(地图项目不用改)
private static String placeHolder = "@market@";// 需要修改manifest文件的地方(占位符)
public static void main(String args[]) {
// 工程目录File对象
File file = new File("..//");
// 获得本工作空间中所有完美攻略工程
String[] projects = file.list(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return name.startsWith("PerfectTools")
&& new File(dir, name).isDirectory();
}
});
for (String project : projects) {
sourceProjectPath = new File(file, project).getAbsolutePath();
targetApkPath = "E:\\apk_release" + File.separator + project;
File targetApkDir = new File(targetApkPath);
if (!targetApkDir.exists()) {
targetApkDir.mkdirs();
}
signApk = project + "-release.apk";
signedApkPrex = project + "_";
//开始打包
startPacking();
}
}
@SuppressWarnings("resource")
protected static void startPacking() {
long startTime = 0L;
long endTime = 0L;
long totalTime = 0L;
Calendar date = Calendar.getInstance();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd:HH:mm:ss");
try {
System.out.println("---------ant批量自动化打包开始----------");
startTime = System.currentTimeMillis();
date.setTimeInMillis(startTime);
System.out.println("开始时间为:" + sdf.format(date.getTime()));
BufferedReader br = new BufferedReader(new FileReader("market.txt"));
String flag = null;
while ((flag = br.readLine()) != null) {
// 先修改manifest文件:读取临时文件中的@market@修改为市场标识,然后写入manifest.xml中
String tempFilePath = sourceProjectPath + File.separator
+ "AndroidManifest.xml.temp";
String filePath = sourceProjectPath + File.separator
+ "AndroidManifest.xml";
write(filePath, read(tempFilePath, flag.trim()));
// 执行打包命令
AntTest mytest = new AntTest();
mytest.init(sourceProjectPath + File.separator + "build.xml",
sourceProjectPath);
mytest.runTarget("clean");
mytest.runTarget("release");
// 打完包后执行重命名加拷贝操作
File file = new File(sourceProjectPath + File.separator + "bin"
+ File.separator + signApk);// bin目录下签名的apk文件
File renameFile = new File(targetApkPath + File.separator
+ signedApkPrex + flag + ".apk");
// 将打包好的apk重命名后移动到copyApkPath位置
boolean renametag = file.renameTo(renameFile);
System.out.println("rename------>" + renametag);
System.out.println("file ------>" + file.getAbsolutePath());
System.out.println("rename------>"
+ renameFile.getAbsolutePath());
}
System.out.println("---------ant批量自动化打包结束----------");
endTime = System.currentTimeMillis();
date.setTimeInMillis(endTime);
System.out.println("结束时间为:" + sdf.format(date.getTime()));
totalTime = endTime - startTime;
System.out.println("耗费时间为:" + getBeapartDate(totalTime));
} catch (Exception e) {
e.printStackTrace();
System.out.println("---------ant批量自动化打包中发生异常----------");
endTime = System.currentTimeMillis();
date.setTimeInMillis(endTime);
System.out.println("发生异常时间为:" + sdf.format(date.getTime()));
totalTime = endTime - startTime;
System.out.println("耗费时间为:" + getBeapartDate(totalTime));
}
}
public void init(String _buildFile, String _baseDir) throws Exception {
project = new Project();
project.init();
DefaultLogger consoleLogger = new DefaultLogger();
consoleLogger.setErrorPrintStream(System.err);
consoleLogger.setOutputPrintStream(System.out);
consoleLogger.setMessageOutputLevel(Project.MSG_INFO);
project.addBuildListener(consoleLogger);
if (_baseDir == null)
_baseDir = new String(".");
project.setBasedir(_baseDir);
if (_buildFile == null)
_buildFile = new String(sourceProjectPath + File.separator
+ "build.xml");
ProjectHelper.configureProject(project, new File(_buildFile));
}
public void runTarget(String _target) throws Exception {
if (project == null)
throw new Exception(
"No target can be launched because the project has not been initialized. Please call the 'init' method first !");
if (_target == null)
_target = project.getDefaultTarget();
project.executeTarget(_target);
}
/**
* 根据所秒数,计算相差的时间并以**时**分**秒返回
*
* @param d1
* @param d2
* @return
*/
public static String getBeapartDate(long m) {
m = m / 1000;
String beapartdate = "";
int nDay = (int) m / (24 * 60 * 60);
int nHour = (int) (m - nDay * 24 * 60 * 60) / (60 * 60);
int nMinute = (int) (m - nDay * 24 * 60 * 60 - nHour * 60 * 60) / 60;
int nSecond = (int) m - nDay * 24 * 60 * 60 - nHour * 60 * 60 - nMinute
* 60;
beapartdate = nDay + "天" + nHour + "小时" + nMinute + "分" + nSecond + "秒";
return beapartdate;
}
public static String read(String filePath, String replaceStr) {
BufferedReader br = null;
String line = null;
StringBuffer buf = new StringBuffer();
try {
// 根据文件路径创建缓冲输入流
br = new BufferedReader(new FileReader(filePath));
// 循环读取文件的每一行, 对需要修改的行进行修改, 放入缓冲对象中
while ((line = br.readLine()) != null) {
// 此处根据实际需要修改某些行的内容
if (line.contains(placeHolder)) {
line = line.replace(placeHolder, replaceStr);
buf.append(line);
} else {
buf.append(line);
}
buf.append(System.getProperty("line.separator"));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
br = null;
}
}
}
return buf.toString();
}
/**
* 将内容回写到文件中
*
* @param filePath
* @param content
*/
public static void write(String filePath, String content) {
BufferedWriter bw = null;
try {
// 根据文件路径创建缓冲输出流
bw = new BufferedWriter(new FileWriter(filePath));
// 将内容写入文件中
bw.write(content);
} catch (Exception e) {
e.printStackTrace();
} finally {
// 关闭流
if (bw != null) {
try {
bw.close();
} catch (IOException e) {
bw = null;
}
}
}
}
}
渠道信息market.txt放在java项目的根目录中,我的渠道列表如下:
zhanwai
xiaomi
oppo
lenovo
wandoujia
meizu
huawei
hisense
tencent
liqucn
wo
eoemarket
sogou
yybei
letv
yyjia
mm
anzhi
以下三个文件拷入要打包的项目中:
ant.properties
key.store=D:\\kding.keystore //签名路径 key.store.password=525b2102 key.alias=kding.keystore key.alias.password=525b2102
build.xml(ant自动生成的文件,只需要修改project name=你的项目名)
<?xml version="1.0" encoding="UTF-8"?> <project name="PerfectToolsOfZhslm" default="help"> <property file="local.properties" /> <property file="ant.properties" /> <property environment="env" /> <condition property="sdk.dir" value="${env.ANDROID_HOME}"> <isset property="env.ANDROID_HOME" /> </condition> <loadproperties srcFile="project.properties" /> <fail message="sdk.dir is missing. Make sure to generate local.properties using 'android update project' or to inject it through the ANDROID_HOME environment variable." unless="sdk.dir" /> <import file="custom_rules.xml" optional="true" /> <import file="${sdk.dir}/tools/ant/build.xml" /> </project>
local.properties(sdk所在目录)
sdk.dir=D:\\adt-bundle-windows-x86_64-20140702\\sdk
使用python脚本进行多渠道打包
使用脚本的打包方式速度很慢,每次打包都需要重新编译,如果需要打包的渠道很多,建议采用以下
- 下载安装python环境
- 下载python脚本
AndroidMultiChannelBuildTool
https://github.com/GavinCT/AndroidMultiChannelBuildTool - 将
ChannelUtil.java
代码集成到工程里面,在app启动时获取渠道号传送给后台(如友盟:AnalyticsConfig.setChannel(ChannelUtil.getChannel(this));
- 在
PythonTool/Info/channel.txt
中编辑渠道列表,以换行隔开,工程中有示例 - 打包apk,将apk文件复制到PythonTool目录下(与py同级),运行(直接双击.py文件或者在命令行输入
python MultiChannelBuildTool.py
)py脚本即可打包完成。(生成的渠道apk包在output_** 目录下)
参考链接
Android studio 多渠道打包(超简洁版)http://xuyazhou.com/archives/461
美团Android自动化之旅—适配渠道包http://tech.meituan.com/mt-apk-adaptation.html
Android Gradle Plugin指南(五)——Build Variants(构建变种版本)http://blog.csdn.net/qinxiandiqi/article/details/37906449
美团Android自动化之旅—生成渠道包http://tech.meituan.com/mt-apk-packaging.html
Android批量打包提速 - 1分钟900个市场不是梦http://www.cnblogs.com/ct2011/p/4152323.html