Android批量打包提速 - 1分鍾900個市場不是夢


版權聲明:

歡迎轉載,但請保留文章原始出處

作者:GavinCT

出處:http://www.cnblogs.com/ct2011/p/4152323.html

黎明前的黑暗

使用Ant或者Gradle來給程序進行多渠道批量打包,通常都是在manifest文件中寫入一個meta標簽:

<meta-data android:name="CHANNEL" android:value="xxx" />

meta的key值固定,通過循環改變meta中的value值來實現市場渠道的寫入。

Ant批量打包實現相對麻煩,以前寫的時候多虧了謙虛的天下-《App自動化之使用Ant編譯項目多渠道打包》 。如果沒有這篇帖子,真不知道當時Ant要折騰多少回才能寫好。

Gradle作為新的安卓官方構建工具,有Google老大撐腰,它的批量打包實現會相對簡單些。可以參考《遷移到Android Studio》。當然這里面有些指令過時了,例如:runProguard已經被minifyEnabled替代了。

以上兩種都是傳統的批量打包方式,他們最大的缺點就是打包時間長。
在前期渠道很少時這種方法還可以接受,但只要渠道稍微增多該方法就不再適用了,原因是每打一個包都要執行一遍構建過程,效率太低。(電腦比較爛,以前一般打包都要花費個30-40分鍾。)

打包界的曙光

前幾天看到美團的技術分享文檔:《美團Android自動化之旅—生成渠道包》,其中第三種方式提到:

如果能直接修改apk的渠道號,而不需要再重新簽名能節省不少打包的時間。幸運的是我們找到了這種方法。直接解壓apk,解壓后的根目錄會有一個META-INF目錄,如下圖所示:
META-INF目錄
如果在META-INF目錄內添加空文件,可以不用重新簽名應用。因此,通過為不同渠道的應用添加不同的空文件,可以唯一標識一個渠道。
采用這種方式,每打一個渠道包只需復制一個apk,在META-INF中添加一個使用渠道號命名的空文件即可。
這種打包方式速度非常快,900多個渠道不到一分鍾就能打完。

OK,到這里,思路就有了。

  1. 在META-INF中放置一個類似 channel_xxx 的空文件來標識市場。
  2. 在Java代碼中解析這個文件名獲取市場xxx即可。

由於文檔中的代碼實現較少,這里我來講述一下我的實現。

我的實現

基於以上總結的美團思路,實現了一套自己的代碼,方便引入到工程后實現這種打包方式。

代碼在Github:GavinCT/AndroidMultiChannelBuildTool

Python工具實現

  1. 首先創建一個空文件,等待寫入META-INF目錄作為channel_xxx文件

    # 空文件 便於寫入此空文件到apk包中作為channel文件
    src_empty_file = 'info/czt.txt'
    # 創建一個空文件(不存在則創建)
    f = open(src_empty_file, 'w') 
    f.close()
    
  2. 獲取渠道列表。
    考慮到渠道的更新不應該是程序員來做,因此在info文件夾下放置一個channel文件,便於不懂程序的人更新渠道。(每個渠道以換行結束)

    # 獲取渠道列表
    channel_file = 'info/channel.txt'
    f = open(channel_file)
    lines = f.readlines()
    f.close()
    
  3. 找到初始apk
    考慮到現實中為了防止安裝包過大,我們通常分為arm和x86兩個版本,所以python中支持當前目錄下放多個apk來進行打包。
    當然有人會說共用了一個channel文件,多個apk會生成相同市場的對應包。
    你也可以修改一下python,使不同的apk去找不同的channel文件進行打包。
    這里由於我的業務場景這樣更方便,我就不修改了。

    # 獲取當前目錄中所有的apk源包
    src_apks = []
    # python3 : os.listdir()即可,這里使用兼容Python2的os.listdir('.')
        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)
    
  4. 遍歷渠道號並寫入apk。
    多個apk只是for循環問題,我們來看單個apk生成多市場包的代碼

    # file name (with extension)
    src_apk_file_name = os.path.basename(src_apk)
    # 分割文件名與后綴
    temp_list = os.path.splitext(src_apk_file_name)
    # name without extension
    src_apk_name = temp_list[0]
    # 后綴名,包含.   例如: ".apk "
    src_apk_extension = temp_list[1]
    
    # 創建生成目錄,與文件名相關
    output_dir = 'output_' + src_apk_name + '/'
    # 目錄不存在則創建
    if not os.path.exists(output_dir):
    os.mkdir(output_dir)
    
    # 遍歷渠道號並創建對應渠道號的apk文件
    for line in lines:
    	# 獲取當前渠道號,因為從渠道文件中獲得帶有\n,所有strip一下
    	target_channel = line.strip()
    	# 拼接對應渠道號的apk
    	target_apk = output_dir + src_apk_name + "-" + target_channel + src_apk_extension  
    	# 拷貝建立新apk
    	shutil.copy(src_apk,  target_apk)
    	# zip獲取新建立的apk文件
    	zipped = zipfile.ZipFile(target_apk, 'a', zipfile.ZIP_DEFLATED)
    	# 初始化渠道信息
    	empty_channel_file = "META-INF/cztchannel_{channel}".format(channel = target_channel)
    	# 寫入渠道信息
    	zipped.write(src_empty_file, empty_channel_file)
    	# 關閉zip流
    	zipped.close()
    

以上Python是屬於現學現寫,有什么可以優化的地方還請告知。

Java工具實現

Python幫我們向apk包中寫入了channel信息,Java端當然也需要對應更改才能使用。
由於解析channel需要去apk也就是zip中去找文件,所以相對耗時一些。
因此在ChannelUtil.java中,會將找到的channel和對應versionCode存儲在靜態變量和SharedPreference中,保證本次甚至本版本中channel只從zip中獲取一次。

在Java代碼中讀取空渠道文件名

從apk中獲取channel,美團留下的代碼if (entryName.startsWith("mtchannel"))是有問題的,應該采用if (entryName.startsWith("META-INF/mtchannel"))
我的代碼如下:

/**
 * 從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();
            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[] split = ret.split("_");
    String channel = "";
    if (split != null && split.length >= 2) {
    	channel = ret.substring(split[0].length() + 1);
    }
    return channel;
}

總結

使用這種方式打包,打包工作不再需要非得是安卓程序員。需要打包時,只要下載安裝Python環境,點擊MultiChannelBuildTool.py執行即可。

那Gradle是不是沒用了呢?

當然不是,Google老大為他做了這么多,怎么能說不用就不用呢?
他的用處在於實現訂制,比如打包出x86和arm的包,或者打出手機包和適應平板的hd包,然后借助上面的工具生成多個市場,即完成了多種適配包多個市場的任務。

Gradle渠道訂制的具體內容可以參見:《美團Android自動化之旅—適配渠道包》
還是美團的文檔,還是熟悉的味道。在此感謝美團的分享。

常見問題答疑

這部分問題是由美團大神丁志虎在微博上答復的,摘錄如下:

  • 這個方案沒法解決不同渠道使用渠道自己SDK的問題,友盟的SDK提供了在代碼中設置渠道的方式,所以再獲取到渠道號后再調用SDK相關設置渠道的方法就可以了
  • apk用的是java那一套簽名,放在META-INF文件夾里的文件原則上是不參與簽名的。如果Google修改了apk的簽名規則,這一套可能就不適用了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM