博文出處:用Java實現(xiàn)Android多渠道打包工具,歡迎大家關(guān)注我的博客,謝謝!
0001b
======
最近在公司做了一個多渠道打包的工具,趁今天有空就來講講 Android 多渠道打包這件小事。眾所周知,隨著業(yè)務(wù)的不斷增長,APP 的渠道也會越來越多,如果用 Gradle 打多渠道包的話,可能會耗費幾個小時的時間才能打出幾百個渠道包。所以就必須有一種方法能夠解決這種問題。
目前市面上比較好的解決方案就是在 apk 文件中“動手腳”,比如由一位360 Android 工程師提出的“在 apk 文件中添加 comments 多渠道打包方法”,具體的代碼在GitHub 上可以找到:MultiChannelPackageTool 。除此之外,還有美團點評技術(shù)團隊在博客上發(fā)表過一篇《美團Android自動化之旅—生成渠道包》,里面講敘了一種在 apk 文件中的 META-INF 目錄下添加渠道信息的方法,之后再在程序啟動時去動態(tài)讀取,具體的實現(xiàn)原理可以去美團博客上看,這里就不說了。
我們解壓多渠道打出來的 apk 包后,就會發(fā)現(xiàn)在 META-INF 目錄下多了一個 channel_xxxxx 文件,而這個就是我們的渠道文件:

本文所采用的方法就是根據(jù)美團提供的思路實現(xiàn)的,當然網(wǎng)上有很多使用 Python 語言實現(xiàn)美團思路的版本,經(jīng)過測試發(fā)現(xiàn) Python 版本比 Java 版本打渠道包的速度更快一些。但是,在這里只提供 Java 版本實現(xiàn)方案,Python 版本實現(xiàn)的方案會在文末以參考鏈接的方式給出。
0010b
在這里先說明一下,Java 編寫的多渠道打包工具依賴 commons-io.jar 和 zip4j.jar 。下面我們就開始進入正題吧。
我們先規(guī)定一下,渠道文件命名為 channel.txt ,并且要打包的 apk 文件和 channel.txt 與多渠道打包工具在同一目錄下。
其中 channel.txt 的格式就是每個渠道獨占一行,如下所示:
wandoujia
googleplay
xiaomi
huawei
kumarket
anzhi
然后我們先定義幾個常量:
// 渠道文件地址
private static final String CHANNEL_FILE_PATH = "./channel.txt";
private static final String CHARSET_NAME = "UTF-8";
// 當前要打包的apk的路徑
private static final String APK_PATH = "./";
// 渠道打包后輸出的apk文件夾前綴
private static final String APK_OUT_PATH_PREFIX = "./out_apk_";
private static final String APK_SUFFIX = ".apk";
定義好之后,我們下一步就是編寫方法去讀取 channel.txt 中的渠道信息:
/**
* 從文件中讀取channel
*
* @return
*/
public static List<String> getChannel() {
List<String> channelList = new ArrayList<>();
InputStream inputStream = null;
BufferedReader reader = null;
try {
inputStream = new FileInputStream(CHANNEL_FILE_PATH);
reader = new BufferedReader(new InputStreamReader(inputStream,
CHARSET_NAME));
String buffer;
while ((buffer = reader.readLine()) != null && buffer.length() != 0) {
System.out.println("發(fā)現(xiàn)已有渠道 : " + buffer);
channelList.add(buffer);
}
} catch (FileNotFoundException e) {
System.out.println("當前目錄下未找到channel.txt");
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (reader != null) {
reader.close();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
return channelList;
}
上面 getChannel() 方法中都是簡單的 I/O 流操作,相信不需要解釋大家都可以看得懂吧。之后我們要做的就是去當前路徑下查找有無 apk 文件。在這里說明一下,我們這個多渠道打包小工具是支持多個 apk 文件一起打包的,所以我們要把當前目錄下所有 apk 文件的路徑存儲起來。
/**
* 得到當前目錄下的所有apk
*
* @param file
* @return
*/
public static List<String> getApk(File file) {
List<String> apkList = new ArrayList<>();
File[] childFiles = file.listFiles();
for (File childFile : childFiles) {
if (!childFile.isDirectory()
&& childFile.getName().endsWith(APK_SUFFIX)) {
System.out.println("發(fā)現(xiàn)已有apk : " + childFile.getName());
apkList.add(childFile.getName());
}
}
return apkList;
}
做好上面的步驟后,最后就剩下打包的代碼了,一起來看看:
/**
* 打包apk
*/
public static void buildApk() {
List<String> apkList = getApk(new File(APK_PATH));
int count = apkList.size();
if (count == 0) {
System.out.println("當前目錄下沒有發(fā)現(xiàn)apk文件");
return;
}
// 遍歷所有apk文件
for (int i = 0; i < count; i++) {
String name = apkList.get(i);
// 得到文件名字
String baseName = apkList.get(i).substring(0,
name.lastIndexOf("."));
// apk輸出目錄
File dictionary = new File(APK_OUT_PATH_PREFIX + baseName);
if (!dictionary.exists()) {
dictionary.mkdir();
}
List<String> channelList = getChannel();
if (channelList.size() == 0) {
System.out.println("channel.txt文件中沒有多渠道信息");
return;
}
// 遍歷所有渠道
for (String channel : channelList) {
try {
String sourceFileName = APK_PATH + name;
// 輸出的apk名字
String outApkName = baseName + "_" + channel + APK_SUFFIX;
// apk包的路徑
String outApkFileName = dictionary.getName() + "/" + outApkName;
// 復(fù)制要打包的apk
copy(sourceFileName, outApkFileName);
System.out.println("正在打 " + channel + " 的渠道包 : " + outApkName);
ZipFile zipFile = new ZipFile(outApkFileName);
ZipParameters parameters = new ZipParameters();
parameters
.setCompressionMethod(Zip4jConstants.COMP_DEFLATE);
parameters
.setCompressionLevel(Zip4jConstants.DEFLATE_LEVEL_NORMAL);
parameters.setRootFolderInZip("META-INF/");
// 當前目錄下創(chuàng)建一個channel_xxxxx文件
File channelFile = new File(dictionary.getName() + "/channel_"
+ channel);
if (!channelFile.exists()) {
channelFile.createNewFile();
}
// 在META-INF文件夾中添加channel_xxxxx文件
zipFile.addFile(channelFile, parameters);
// 刪除當前目錄下的channel_xxxxx文件
channelFile.delete();
} catch (ZipException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 復(fù)制文件
*
* @param sourceFilePath
* @param copyFilePath
*/
private static void copy(String sourceFilePath, String copyFilePath){
try {
// 這里使用的是 common-io.jar 中的文件復(fù)制方法,比原生Java I/O API操作速度要快
FileUtils.copyFile(new File(sourceFilePath), new File(copyFilePath));
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
long preTime = System.currentTimeMillis();
buildApk();
System.out.println("多渠道打包完成,耗時 " + (System.currentTimeMillis() - preTime)/1000 + " s");
}
buildApk() 方法中主要做的就是兩個 for 循環(huán)嵌套。遍歷當前目錄的 apk 文件,然后遍歷渠道信息,最后打包。另外需要注意的是要復(fù)制出一個 apk 文件來進行多渠道打包,而不是在原文件的基礎(chǔ)上。
在這里打包的部分就結(jié)束了,我們還有一個步驟需要完成。那就是在應(yīng)用程序啟動時去讀取相應(yīng)的渠道,可以通過以下方法去讀取:
public static String getChannelFromMeta(Context context) {
ApplicationInfo appinfo = context.getApplicationInfo();
String sourceDir = appinfo.sourceDir;
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("META-INF/channel_")) {
ret = entryName;
break;
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (zipfile != null) {
try {
zipfile.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
String[] split = ret.split("_");
if (split != null && split.length >= 2) {
return ret.substring(split[0].length() + 1);
} else {
return "default";
}
}
讀取渠道之后,我們 APP 可以把相應(yīng)的渠道號發(fā)送給服務(wù)器或者第三方統(tǒng)計平臺做統(tǒng)計。
0011b
最后,我們可以把這個多渠道打包的 Java 項目打成一個 jar 包,然后寫一個 bat 腳本,這樣就通過鼠標雙擊就可以實現(xiàn)快速打渠道包了。以下是 bat 腳本的內(nèi)容,要注意的是 bat 腳本要和 jar 包處于同一級目錄下才可以哦:
@echo off
echo 歡迎使用多渠道打包工具
echo 請確保當前目錄下有要打包的apk文件和渠道信息channel.txt
java -jar AndroidBuildApkTool.jar
echo 按任意鍵退出
pause>nul
exit
通過我們的努力 Java 版的多渠道打包工具就做好了。但是不足的是,測試后發(fā)現(xiàn) Java 版打渠道包的速度沒有 Python 版的快,主要是在 apk 文件中添加渠道信息文件這一步操作耗費的時間有點多。如果哪位小伙伴有更好的解決方案,歡迎聯(lián)系我!
附上多渠道打包工具的源碼:
0100b
References: