Android 多渠道打包方式詳解
面試的時候,如果面試官突然問到:你們渠道包是怎么打的?如果你說是用gradle一個一個編譯的,然后他很鄙視的說這個效率太低啦,你們寫過什么腳本自己打渠道包沒?你肯定心里想,臥槽,這么狂炫吊炸天,自己寫腳本打包?!其實這個根本也不是太難啦!!今天就來聊聊多渠道打包的原理以及如何自己DIY多渠道打包的工具!
渠道包出現
當一個產品到發版的時候,我們搞Android的就會面臨一個超級尷尬的問題:國內這么多的渠道,渠道統計是必須做滴,那么十多個主要渠道再加無限量的地推渠道包就成了一個巨坑了!這一塊耗費的時間是一個無底洞啊!!!
方式一覽
這里一共會介紹三種渠道包的實現方式,分別是:
1、使用gradle配置直接編譯出不同的渠道包。
2、通過反編譯修改對應的渠道號。
3、META-INF里面新加一個文件。
Gradle方式
不管是用友盟統計還是其他什么的,首先肯定都是要有一些準備工作的,由于本人就比較了解友盟的,所以就用友盟統計來舉例啦!
友盟統計提供了兩種渠道統計策略,其實就是一個自動擋的一個手動擋的。
<meta-data
android:name="UMENG_APPKEY"
android:value="xxxxxxxx"/>
<meta-data
android:name="UMENG_CHANNEL"
android:value="${GRADLE_CHANNEL_VALUE}"/>
在對應的build.gradle里面配置對應的信息:
productFlavors.all { flavor ->
flavor.manifestPlaceholders = [GRADLE_CHANNEL_VALUE: name]
}
productFlavors {
dev {
}
baidu {
minSdkVersion 18
applicationId "com.test.michat"
}
}
如果手動去設置對應的渠道號的話,就在程序入口處調用以下方法:
MobclickAgent. startWithConfigure(UMAnalyticsConfig config)
UMAnalyticsConfig(Context context, String appkey, String channelId)
UMAnalyticsConfig(Context context, String appkey, String channelId, EScenarioType eType)
UMAnalyticsConfig(Context context, String appkey, String channelId, EScenarioType eType,Boolean isCrashEnable)
那么怎么獲取到對應的渠道號呢?!這個方法在之后的所有方式中都要使用滴,其實不管是哪種方式,最后都會調用這個方法去讀相關數據的!!
private String getChannel(Context context) {
try {
PackageManager pm = context.getPackageManager();
ApplicationInfo appInfo = pm.getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA);
return appInfo.metaData.getString("CHANNEL_VALUE");
} catch (PackageManager.NameNotFoundException ignored) {
}
return "";
}
buildVariants.png
當然你也可以使用命令行:gradlew assemble 組裝出所有的渠道包!!
反編譯方式
gradle方式用著也挺不錯的,為什么還要去搞什么反編譯這么麻煩的東西呢?因為它有一個很大的問題,那就是每一個包都是要去編譯打包的! 這是相當的耗時!time is 加班啊!誰也不想加班打渠道包咯!! 反編譯的方式就是節省了每個渠道包都去編譯的時間,而是編譯好一個渠道包之后就使用該渠道包,通過反編譯動態修改 AndroidManifest.xml 里面的信息,然后再重新打包簽名!
說到反編譯,那么這里就不得不提大名鼎鼎的 apktool.jar 了!納尼,你說你從未聽說過?!沒事兒,以前沒有聽過,現在會用了就行了!!
然后總結一下接下來的一系列套路:
解包->修改相關參數->打包->簽名->Zipalign優化
-
1、解包
apktool d your_original_apk build
你沒有看錯,就是這樣的!因為我們是站在巨人的肩膀上工作的嘛,所以好多工作就不同自己搞了!
執行以上命令之后,如果不出什么意外,你就會得到一個文件夾:
反編譯.png
相關代碼:
try {
brut.apktool.Main.main(new String[]{"d", "-f", apkFilePath, "-o", outPath});
return true;
} catch (Exception e) {
e.printStackTrace();
callback("解包失敗 !!!!!\r\n" + e.getMessage());
}
-
2、修改對應的參數
打開對應的 AndroidManifest.xml ,你沒有看錯,什么都在里面,直接修改就好了!等等,xml解析你不會?!沒有關系,這里有dom4j.jar給你使用啦!!
修改反編譯之后的AndroidManifest文件相關代碼
try {
File androidManifestFile = new File(appFolderName + File.separator + "AndroidManifest.xml");
Document document = new SAXReader().read(androidManifestFile);//使用dom4j的sax解析
Element element = document.getRootElement().element("application");
List<Element> list = element.elements("meta-data");//獲取到所有的“meta-data”
List<MetaData> metaData = manifest.getMetaData();
boolean isUpdate = false;
for (MetaData data : metaData) {
String name = data.getName();
String value = data.getValue();
callback(" meta-data name='" + name + "' value='" + value + "'");
for (Element s : list) {
Attribute attribute = s.attribute("name");
//更新相關渠道號
if ( "UMENG_CHANNEL".equals(name)&&"UMENG_CHANNEL".equals(attribute.getValue())) {//更換相關的渠道號
s.attribute("value").setValue(value);
isUpdate = true;
callback("更新1 AndroidManifest.xml meta-data name='" + attribute.getValue() + "' value='" + value + "'");
break;
}
}
}
if(isUpdate){//更新后重新寫入
XMLWriter writer = new XMLWriter(new FileOutputStream(androidManifestFile));
writer.write(document);
writer.close();
callback("更新 AndroidManifest.xml 完成 ~ ");
}
} catch (Exception e) {
e.printStackTrace();
return false;
}
-
3、打包
apktool b build your_unsigned_apk
還是這么簡單:
try {
brut.apktool.Main.main(new String[]{"b", buildApkFolderPath, "-o", buildApkOutPath});
return true;
} catch (Exception e) {
e.printStackTrace();
callback("打包失敗 !!!!!\r\n" + e.getMessage());
}
-
4、簽名
jarsigner -sigalg MD5withRSA -digestalg SHA1 -keystore your_keystore_path -storepass your_storepass -signedjar your_signed_apk, your_unsigned_apk, your_alias
這個是jdk里面直接提供了的,只要你的環境變量配置好了的,就沒有什么問題啦!
重新簽名相關代碼
executeCommand("jarsigner", "-verbose", "-sigalg", "SHA1withRSA", "-digestalg", "SHA1", "-keystore", keystoreFilePath, apkFilePath, alias, "-storepass", password);
/**
* 執行命令
*
* @param command 命令
*/
private synchronized boolean executeCommand(String... command) {
Process process = null;
BufferedReader reader = null;
try {
ProcessBuilder builder = new ProcessBuilder();
builder.command(command);
builder.redirectErrorStream(true);
process = builder.start();
reader = new BufferedReader(new InputStreamReader(process.getInputStream(),"UTF-8"));
String line;
while ((line = reader.readLine()) != null) {
callback(line);
if (line.contains("Exception") || line.contains("Unable to open")) {
return false;
}
}
return true;
} catch (IOException e) {
e.printStackTrace();
callback(e.getMessage());
} finally {
close(reader);
if (process != null) {
process.destroy();
}
}
return false;
}
- 5、Zipalign優化
Zipalign.png
如圖所示,sdk/build-tools里面每個版本都是有這個東西的,加到環境變量中就好了!!!
zipalign 優化處理相關代碼
* 需要安裝并Android SDK并配置環境變量Build Tools路徑
* 優化apk文件,這個需要Android Build Tools 中的zipalign程序文件
*
* @param apkFilePath 要優化的apk文件路徑
* @param outFilePath 優化后的apk存放文件路徑
*/
public boolean zipalign(String apkFilePath, String outFilePath) {
return executeCommand("zipalign", "-f", "-v", "4", apkFilePath, outFilePath);
}
美團方式
上面說的反編譯要各種解包,打包,簽名,相對也比較繁瑣,然后我們可以發現,apk其實都是一個壓縮包,我們直接在這個壓縮包里添加對應的文件作為渠道號標記是不是又能省去上面繁瑣的步奏呢?!打開一個APK文件之后你會看到 META-INF 這個文件夾!
apk壓縮包.png
META-INF.png
美團的方式就是在這里面直接再添加一個文件,然后通過這個文件的名稱來指定對應的渠道號!
話不多說,直接上代碼!!
public static void addUmengChannel(String filepath, String channel) {
String channel_title = "umengchannel_";
if(filepath.substring(filepath.lastIndexOf(".") + 1).toLowerCase().equals("apk")) {
String path2 = "";
if(filepath.lastIndexOf(File.separator) >= 0) {
path2 = filepath.substring(0, filepath.lastIndexOf(File.separator) + 1);//得到父路徑
}
if(path2.length() != 0) {
File s = new File(filepath);//原始的apk
File t = new File(filepath.substring(0, filepath.lastIndexOf(".")) + "_" + channel + ".apk");//目標apk
if(!t.exists()) {//不存在就創建
try {
t.createNewFile();
} catch (IOException var12) {
var12.printStackTrace();
}
}
Utils.fileChannelCopy(s, t);//拷貝原始apk到目標apk
File addFile = new File(path2 + channel_title + channel);//需要添加的渠道文件
if(!addFile.exists()) {
try {
addFile.createNewFile();
} catch (IOException var11) {
var11.printStackTrace();
}
}
try {
Utils.addFileToExistingZip(t, addFile);//將新加的渠道文件添加到目標apk文件中
addFile.delete();
} catch (IOException var10) {
var10.printStackTrace();
}
}
}
}
public static void addFileToExistingZip(File zipFile, File file) throws IOException {
File tempFile = File.createTempFile(zipFile.getName(), (String)null);
tempFile.delete();
boolean renameOk = zipFile.renameTo(tempFile);//拷貝
if(!renameOk) {
throw new RuntimeException("could not rename the file " + zipFile.getAbsolutePath() + " to " + tempFile.getAbsolutePath());
} else {
byte[] buf = new byte[1024];
ZipInputStream zin = new ZipInputStream(new FileInputStream(tempFile));
ZipOutputStream out = new ZipOutputStream(new FileOutputStream(zipFile));
for(ZipEntry entry = zin.getNextEntry(); entry != null; entry = zin.getNextEntry()) {
String in = entry.getName();
if(in.contains("umengchannel")) {//如果有重復的就不復制回去了!
continue;
}
out.putNextEntry(new ZipEntry(in));
int len1;
while((len1 = zin.read(buf)) > 0) {
out.write(buf, 0, len1);
}
}
zin.close();
FileInputStream in1 = new FileInputStream(file);
out.putNextEntry(new ZipEntry("META-INF/" + file.getName()));//創建對應的渠道文件
int len2;
while((len2 = in1.read(buf)) > 0) {
out.write(buf, 0, len2);
}
out.closeEntry();
in1.close();
out.close();
tempFile.delete();
}
}
渠道包完成.png
最后送上讀取相關的方法:
public static String getChannel(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();
//這里需要替換成你的那個key
if (entryName.startsWith(YOUR_CHNANNEL_NAME)) {
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 "";
}
}
當然你肯定要手動設置了,這個沒法直接在清單文件中去配置了!!
小結
主要的方式就是這三種了!可以說一個比一個快,一個比一個的定制也要高,效率提高了,靈活性似乎就會下降的,至于到底使用哪種方式,還是根據實際情況靈活選擇吧,反正到現在,這些方案都是很成熟的,沒有什么坑!一不小心又說了幾句廢話啊!
來自:http://www.jianshu.com/p/f3f930fd4f6a