我想,每當發布新版本時,大家都會對那么多發布渠道感到頭疼不已,本文主要介紹三種Android的多渠道打包方式。下面的例子我以 TalkingData 統計為例子。
1. Gradle 配置 Flavor
1.1 在 AndroidManifest.xml 中配置 meta-data 標簽:
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="your.package.name">
<application>
<meta-data android:name="TD_CHANNEL_ID" android:value="${TD_CHANNEL_VALUE}"/>
</application>
</manifest>
1.2 在項目的 build.gradle 里配置 productFlavors,定義渠道號:
productFlavors {
yyb {}
bdsjzs {}
wdj {}
xiaomi {}
sjzs360 {}
productFlavors.all {
flavor -> flavor.manifestPlaceholders = [TD_CHANNEL_VALUE: name]
}
}
上面定義了一些渠道(比如:豌豆莢,360,小米等),TD_CHANNEL_VALUE
就是你在 meta-data 標簽中配置的值。
同時,需要注意的是,你需要在 defaultConfig
中配置一個默認的渠道名稱:
manifestPlaceholders = [TD_CHANNEL_VALUE: "default_channel_name"]
該原理就是在打包過程中更換 meta-data 標簽中的內容。
優勢:方便理貨,可以根據自身的需求配置不同的渠道執行不同的邏輯(比如:小米渠道有一個獨立的方法,其他渠道沒有);
劣勢:打包速度慢(打包時是對每個渠道都進行一次完整的打包過程)。
2.第三方打包工具
第三方的打包工具原理跟 Gradle 一樣,也是去修改 AndroidManifest.xml 中的 meta-data 標簽的內容,然后執行N次打包簽名的操作實現多渠道打包的。
優勢:簡單方便,幾乎不用自身做什么工作;
劣勢:打包速度過慢;
3.美團的多渠道打包方案
多渠道打包原理可以參考美團Android自動化之旅—生成渠道包這篇文章。Demo 可以查看 快速多渠道打包。
3.1 實現原理
直接解壓apk,解壓后的根目錄會有一個META-INF
目錄,如下圖所示:
如果在META-INF目錄內添加空文件,可以不用重新簽名應用。因此,通過為不同渠道的應用添加不同的空文件,可以唯一標識一個渠道。
這樣,每打一個渠道包只需復制一個apk,在 META-INF 中添加一個使用渠道號命名的空文件即可。這種打包方式速度非??欤?00多個渠道不到一分鐘就能打完。
2.2 多渠道實現步驟
2.2.1 android 代碼中獲取渠道號
/**
* 渠道號工具類:解析壓縮包,從中獲取渠道號
*/
public class ChannelUtil {
private static final String CHANNEL_KEY = "uuchannel";
private static final String DEFAULT_CHANNEL = "internal";
private static String mChannel;
public static String getChannel(Context context) {
return getChannel(context, DEFAULT_CHANNEL);
}
public static String getChannel(Context context, String defaultChannel) {
if (!TextUtils.isEmpty(mChannel)) {
return mChannel;
}
//從apk中獲取
mChannel = getChannelFromApk(context, CHANNEL_KEY);
if (!TextUtils.isEmpty(mChannel)) {
return mChannel;
} //全部獲取失敗
return defaultChannel;
}
/**
* 從apk中獲取版本信息
*
* @param context
* @param channelKey
* @return
*/
private static String getChannelFromApk(Context context, String channelKey) {
long startTime = System.currentTimeMillis();
//從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 channel = "";
if (!TextUtils.isEmpty(ret)) {
String[] split = ret.split("_");
if (split != null && split.length >= 2) {
channel = ret.substring(split[0].length() + 1);
}
System.out.println("-----------------------------");
System.out.println("渠道號:" + channel + ",解壓獲取渠道號耗時:" + (System.currentTimeMillis() - startTime) + "ms");
System.out.println("-----------------------------");
} else {
System.out.println("未解析到相應的渠道號,使用默認內部渠道號");
channel = DEFAULT_CHANNEL;
}
return channel;
}
然后在代碼中初始化調用TalkingData設置渠道號:
String channelName = ChannelUtil.getChannel(mContext);
TCAgent.init(mContext,Constants.APP_KEY_TD,channelName );
2.2.2 編寫渠道號文件
修改 info/channel.txt
(一行代表一個渠道):
yyb
bdsjzs
wdj
xiaomi
...
2.2.3 編寫 python 腳本
實現解壓 apk 文件,遍歷渠道文件 為 META-INF 目錄添加新文件,重新壓縮 apk 文件等邏輯:
# coding=utf-8
import zipfile
import shutil
import os
def delete_file_folder(src):
'''delete files and folders'''
if os.path.isfile(src):
try:
os.remove(src)
except:
pass
elif os.path.isdir(src):
for item in os.listdir(src):
itemsrc=os.path.join(src,item)
delete_file_folder(itemsrc)
try:
os.rmdir(src)
except:
pass
# 創建一個空文件,此文件作為apk包中的空文件
src_empty_file = 'info/empty.txt'
f = open(src_empty_file,'w')
f.close()
# 在渠道號配置文件中,獲取指定的渠道號
channelFile = open('./info/channel.txt','r')
channels = channelFile.readlines()
channelFile.close()
print('-'*20,'all channels','-'*20)
print(channels)print('-'*50)
# 獲取當前目錄下所有的apk文件
src_apks = [];
for file in os.listdir('.'):
if os.path.isfile(file):
extension = os.path.splitext(file)[1][1:]
if extension in 'apk':
src_apks.append(file)
# 遍歷所以的apk文件,向其壓縮文件中添加渠道號文件
for src_apk in src_apks:
src_apk_file_name = os.path.basename(src_apk)
print('current apk name:',src_apk_file_name)
temp_list = os.path.splitext(src_apk_file_name)
src_apk_name = temp_list[0]
src_apk_extension = temp_list[1]
apk_names = src_apk_name.split('-');
output_dir = 'outputDir'+'/'
if os.path.exists(output_dir):
delete_file_folder(output_dir)
if not os.path.exists(output_dir):
os.mkdir(output_dir)
# 遍歷從文件中獲得的所以渠道號,將其寫入APK包中
for line in channels:
target_channel = line.strip()
target_apk = output_dir + apk_names[0] + "-" + target_channel+"-"+apk_names[2] + src_apk_extension
shutil.copy(src_apk, target_apk)
zipped = zipfile.ZipFile(target_apk, 'a', zipfile.ZIP_DEFLATED)
empty_channel_file = "META-INF/uuchannel_{channel}".format(channel = target_channel)
zipped.write(src_empty_file, empty_channel_file)
zipped.close()
print('-'*50)
print('repackaging is over ,total package: ',len(channels))
input('\npackage over...')
2.2.4 打包一個簽名 APK 包
將APK 包放在 python 腳本的同級目錄
2.2.5 執行python 腳步,多渠道打包
python Android.py
最后就會在 同級目錄下生成 outputDir
目錄,里面就是各個渠道的 APK 。
優勢:打包速度很快,很方便;
劣勢:不夠靈活,不能靈活的配置不同的渠道不同的業務邏輯;
總結
三種打包方式中,各有各的優勢,大家在選擇多渠道打包方式的時候可以根據自身的需求來選擇