詳解高速神器python腳步打包android apk,超級快!!(打包系列教程之六)
打包系列教程目錄:
今天終於要來給大家介紹python多渠道打包啦,我也是很激動,當初雖然有gradle這樣方便的打包方式,但是一旦渠道數量多了起來,gradle打包的時間也會成為一個瓶頸,之前打20個渠道左右,用gradle打包的話大概要花上20多分鐘,如果以後渠道增加到上百個那就真的呵呵了!不過現在即使再多的渠道包也沒關係啦,有python在都是秒秒鐘搞定的時,python打包是美團工程師的傑作,在此十分感謝哈!用python腳步打包的話,打20個渠道左右的包大概只要花上不到5分鐘的時間,十分的快啊!這酸爽的感覺,太刺激了。接下來我們就開始吧,python方式的打包需要做一下準備(本節涉及的所有檔案我在最後都會提供給大家下載):
1.改動簽名好的apk
這個apk裡面的渠道配置資訊跟gradle多渠道打包的配置有些不一樣哈。之前我們是需要在AndroidManifest.xml檔案中渠道資訊,現在用python打包的話就不用啦,我們直接在啟動的activity檔案中設定就行了,設定程式碼如下:
//動態設定渠道資訊
String channel= ChannelUtil.getChannel(this);
AnalyticsConfig.setChannel(channel);
ChannelUtil.java類是一個獲取渠道資訊的類,這個類到時會提供給大家,而AnalyticsConfig則是友盟提供的設定渠道類,我們在友盟官網可以看到這樣的介紹:
因此我們使用的就是第2種設定渠道的方式。嗯,這就是唯一與gradle打包的不同點,同時要注意,這個簽名的apk不需要提前設定任何渠道,所以在gradle配置檔案中無需使用productFlavors屬性來設定渠道名稱。
2.Python打包的實現思路詳解
說完了apk的區別設定後,我們先來聊聊python打包的實現思路。我們先獲取一個打包好的apk,並把字尾改為zip,解壓如下:這是一個已經簽名打包的apk,解壓後我們可以看到apk中包含了一個名稱為META-INF的資料夾,其實python打包的奧祕就在於此了,因為每個apk都會包含這樣一個名稱為META-INF資料夾,所以我們可以利用python腳步在該資料夾下建立一個空的檔案,這個檔案的名稱就命名為channel_xxx.txt,該檔案並沒有任何內容,僅作為渠道標誌,比如現在是華為渠道,那麼該檔案的名稱就是channel_huawei.txt,如果現在的渠道是xiaomi,那麼該檔案的名稱就是channel_xiaomi.txt,為了驗證以上的說法我們先來看看已經利用python打包好的apk:
然後我們開啟其中幾個apk看看META-INF資料夾下是否有對應的渠道檔案:
確實如我們上面所說的一樣,每個apk的META-INF資料夾中都含有一個渠道名稱的檔案。那這個渠道名稱的檔案是如何建立的呢,確實我們自己通過as打包apk時是不可能含有該渠道名稱檔案的,而這也正是python的功勞了,我們把已經簽名好的apk,通過python腳步的for迴圈語句去解壓我們已經打包簽名好的apk,然後在每個apk的META-INF資料夾中通過python腳步去建立一個渠道名稱的檔案,建立完成後在重新還原成apk輸出,就這樣渠道名稱檔案就被設定到apk中啦。那麼這個python迴圈是依據什麼開始的呢,還記得我們開頭提到過的channel.txt檔案嘛?該檔案內容如下:
xiaomi
huawei
yingyongbao
360mobile
wandoujia
anzhuo_market
baidu
91market
anzhi_market
googleplay
大家可能已經猜到了,沒錯,python腳步在開始時會去讀取這個檔案,根據這個檔案的渠道名稱去進行for迴圈,然後把每個渠道名稱以channel_xxx結尾作為檔案的名稱。我們不妨看看python腳步的原始碼 :#coding=utf-8
import zipfile
import shutil
import os
import sys
if __name__ == '__main__':
apkFile = sys.argv[1]
apk = apkFile.split('.apk')[0]
# print apkFile
emptyFile = 'xxx.txt'
f = open(emptyFile, 'w')
f.close()
with open('./android_channels.txt', 'r') as f:
contens = f.read()
lines = contens.split('\n')
os.mkdir('./release')
#print lines[0]
for line in lines:
channel = 'channel_' + line
destfile = './release/%s_%s.apk' % (apk, channel)
shutil.copy(apkFile, destfile)
zipped = zipfile.ZipFile(destfile, 'a')
channelFile = "META-INF/{channelname}".format(channelname=channel)
zipped.write(emptyFile, channelFile)
zipped.close()
os.remove('./xxx.txt')
#mac
os.system('chmod u+x zipalign_batch.sh')
os.system('./zipalign_batch.sh')
#windows
#os.system('zipalign_batch.bat')
程式碼並不太複雜(我也只是懂一些python的基礎哈,也還在慢慢學習中),我們可以看到一開始會去讀取android_channels.txt的渠道檔案,然後建立一個release的資料夾,然後就進入for迴圈了,後面我們就不過多討論了,大概明白意思就行。通過上面的分析我們也大概明白了channel_xxx.txt檔案是如何被寫入每個apk的,同時也知道了android_channels.txt這個檔案的作用。但是在apk中寫入channel_xxx.txt渠道名稱的檔案有什麼用呢,這個就是ChannelUtil.java工具類的作用了,還記得我們的渠道是怎麼設定的嘛?//動態設定渠道資訊
String channel= ChannelUtil.getChannel(this);
AnalyticsConfig.setChannel(channel);
沒錯,在應用啟動時,ChannelUtil.java工具類會去讀取META-INF檔案下的channel_xxx.txt檔案的名稱,並通過拆分去掉channel_字串,從而獲取到渠道名稱,最後就可以通過友盟api的介面傳送友盟伺服器了。ChannelUtil.java原始碼如下:package com.zejian.application.utils;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.text.TextUtils;
import java.io.IOException;
import java.util.Enumeration;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
/**
* Created by WuZeJian
* Time:2015/12/1 17:57
* Email:[email protected]
* Description:打包渠道工具類
*/
public class ChannelUtil {
private static final String CHANNEL_KEY = "channel";
private static final String CHANNEL_DEFAULT = "offical";
private static final String PREF_KEY_CHANNEL = "pref_key_channel";
private static final String PREF_KEY_CHANNEL_VERSION = "pref_key_channel_version";
private static String mChannel;
/**
* 返回市場。 如果獲取失敗返回""
* @param context
* @return
*/
public static String getChannel(Context context){
return getChannel(context, CHANNEL_DEFAULT);
}
/**
* 返回市場。 如果獲取失敗返回defaultChannel
* @param context
* @param defaultChannel
* @return
*/
public static String getChannel(Context context, String defaultChannel) {
//記憶體中獲取
if(!TextUtils.isEmpty(mChannel)){
return mChannel;
}
//sp中獲取
mChannel = getChannelFromSP(context);
if(!TextUtils.isEmpty(mChannel)){
return mChannel;
}
//從apk中獲取
mChannel = getChannelFromApk(context, CHANNEL_KEY);
if(!TextUtils.isEmpty(mChannel)){
//儲存sp中備用
saveChannelInSP(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();
LogUtils.d("APK entry name --------> " + entryName);
if (entryName.startsWith(key)) {
ret = entryName;
break;
}
}
} catch (IOException e) {
LogUtils.e(e);
} finally {
if (zipfile != null) {
try {
zipfile.close();
} catch (IOException e) {
LogUtils.e(e);
}
}
}
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 saveChannelInSP(Context context, String channel){
SharedPreferencedUtils.setString(context, PREF_KEY_CHANNEL, channel);
SharedPreferencedUtils.getInteger(context, PREF_KEY_CHANNEL_VERSION, getVersionCode(context));
}
/**
* 從sp中獲取channel
* @param context
* @return 為空表示獲取異常、sp中的值已經失效、sp中沒有此值
*/
private static String getChannelFromSP(Context context){
int currentVersionCode = getVersionCode(context);
if(currentVersionCode == -1){
//獲取錯誤
return "";
}
int versionCodeSaved = SharedPreferencedUtils.getInteger(context, PREF_KEY_CHANNEL_VERSION, -1);
if(versionCodeSaved == -1){
//本地沒有儲存的channel對應的版本號
//第一次使用 或者 原先儲存版本號異常
return "";
}
if(currentVersionCode != versionCodeSaved){
return "";
}
return SharedPreferencedUtils.getString(context, PREF_KEY_CHANNEL, "");
}
public static int getVersionCode(Context context) {
try{
return context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionCode;
}catch(NameNotFoundException e) {
LogUtils.e(e);
}
return -1;
}
}
好了,到此python打包的思路基本講完了,我們先來小結:首先我們先準備一個打包簽名好的apk,然後通過python腳步去解壓該apk,並在apk的META-INF目錄下建立一個名稱channel_xxx.txt的渠道名稱檔案,然後再重新打包成apk,這個channel_xxx.txt的渠道名稱檔案在apk應用啟動時會被一個名稱為ChannelUtil.java的工具類讀取,該工具通過META-INF目錄下的channel_xxx.txt獲取到渠道名稱並設定給友盟,這樣就完成了友盟渠道資訊的記錄。
3.Python打包實戰記錄(記得整合友盟統計哈)
說了這麼多還是趕緊來實戰吧,首先我們要配置key,以及相應的工具類,我們使用的專案結構如下:
然後我們打包出一個簽名好的apk,並把它放到和python腳步同一個目錄下:
然後開啟我們的命令終端,cd到該目錄下,輸入如下命令,回車。
python channel.py app-debug4zj.apk
成功後我們在看看該目錄下多出一個release的資料夾(沒有經過zipalign優化的apk),裡面還有一個zipalign的資料夾(經過zipalign優化的apk),如下:
就這樣我們的apk都打包好啦,至於zipalign優化有什麼作用,我在第一篇文章有詳細的說明,大家可以移步看看哈。這裡我就不重複了。還有等會我會把工具提供給大家如果是mac系統的話,python已經預設安裝好的了哈,如果是window系統就先要安裝python環境哈。還有這裡提供兩種zipalign_batch.bat(window平臺用)和zipalign_batch.sh(mac平臺使用)指令碼。同時要記得配置好zipalign的環境變數哈(該工具在androidSdk/build-tools目錄下)。最後還有一點要注意的是channel.py指令碼檔案最後的程式碼設定如下:
4.使用友盟渠道統計驗證一下打包結果
說了這麼多,操作也講解了,最後到底靠不靠譜,還是得用友盟來檢測一下對吧。我們還是使用前篇文章的測試裝置vivoxplay3s ,上次測試完後現在的初始資料如下
看來還是很靠譜的嘛,最重要的是速度,速度,速度啊。趕緊去試試吧哈。 本篇資料下載: