1. 程式人生 > >詳解高速神器python腳步打包android apk,超級快!!(打包系列教程之六)

詳解高速神器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 ,上次測試完後現在的初始資料如下


我們依次安裝baidu,91market,anzhi_market,googleplay_market,yingyongbao,測試結果如下:


看來還是很靠譜的嘛,最重要的是速度,速度,速度啊。趕緊去試試吧哈。 本篇資料下載: