Android 快速批量渠道打包工具packer-ng-plugin

关于Android渠道打包这个事儿:

1.gradle提供了productFlavors支持多渠道同时打包,优点是可以充分使用条件编译,比如美团的APP在一些渠道叫美团而在另一些渠道却要叫美团团购,没有任何兼容性问题,方便集成。缺点是每次都要重新编译打包,速度特别慢,现在我们公司四十多个渠道,8G内存固态硬盘的笔记本打一次差不多一两个小时,还有可能出现内存溢出。

2.友盟 打包过程:解压apk文件 -> 替换AndroidManifest.xml中的meta-data -> 压缩apk文件 ->签名 缺点是需要解压缩、压缩、重签名耗费时间较多,重签名会导致apk包在运行时有兼容性问题;

3.美团提供了一个解决方案美团Android自动化之旅—生成渠道包,它的原理是在APK文件的META-INF目里增加渠道文件,打包速度非常快,但读取时需要遍历APK文件的数据项,比较慢(需要解压缩,压缩),由于现在这一块的数据现在不参与签名校验,以后如果Google更改签名校验规则可能遇到兼容性问题。

4.packer-ng-plugin 利用的是Zip文件"可以添加comment(摘要)"的数据结构特点,在文件的末尾写入任意数据,而不用重新解压zip文件(apk文件就是zip文件格式);所以该工具不需要对apk文件解压缩和重新签名即可完成多渠道自动打包,高效速度快,无兼容性问题;

现在演示怎么集成packer-ng-plugin

具体集成过程参见https://github.com/mcxiaoke/packer-ng-plugin?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io

这儿只提及我使用的方式。 打包脚本(Python):

pack.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import print_function
import os
import sys
import struct
import shutil
import argparse
import time
import datetime
import shutil
from multiprocessing import Pool

__version__ = '1.0.1.20151201'

ZIP_SHORT = 2
MARKET_PATH = 'markets.txt'
OUTPUT_PATH = 'archives'
MAGIC = '!ZXK!'

def write_market(path, market, output):
    '''
    write market info to apk file
    write_market(apk-file-path, market-name, output-path)
    '''
    path = os.path.abspath(path)
    if not output:
        output = os.path.dirname(path)
    if not os.path.exists(output):
        os.makedirs(output)
    name,ext = os.path.splitext(os.path.basename(path))
    apk_name = name + "-" + market + ext
    apk_file = os.path.join(output,apk_name)
    shutil.copy(path,apk_file)
    # print('apkfile:',apkfile)
    index = os.stat(apk_file).st_size
    index -= ZIP_SHORT
    with open(apk_file,"r+b") as f:
        f.seek(index)
        # write comment length 
        f.write(struct.pack('<H',len(market) + ZIP_SHORT + len(MAGIC)))
        # write comment content
        # content = [market_string + market_length + magic_string]
        f.write(market)
        f.write(struct.pack('<H',len(market)))
        f.write(MAGIC)
    return apk_file

def read_market(path):
    '''
    read market info from apk file
    read_market(apk-file-path)
    '''
    index = os.stat(path).st_size
    # print('path:',path,'length:',index)
    index -= len(MAGIC)
    f = open(path,'rb')
    f.seek(index)
    # read and check magic
    magic = f.read(len(MAGIC))
    # print('magic',magic)
    if magic == MAGIC:
        index -= ZIP_SHORT
        f.seek(index)
        # read market string length
        market_length = struct.unpack('<H',f.read(ZIP_SHORT))[0]
        # print('comment length:',market_length)
        index -= market_length
        f.seek(index)
        # read market
        market = f.read(market_length)
        # print('found market:',market)
        return market
    else:
        # print('magic not matched')
        return None

def verify_market(file,market):
    '''
    verify apk market info
    verify_market(apk-file-path,market-name)
    '''
    return read_market(file) == market

def show_market(file):
    '''
    show market info for apk file
    show_market(apk-file-path)
    '''
    print('market of',file,'is',read_market(file))

def parse_markets(path):
    '''
    parse file lines to market name list
    parse_markets(market-file-path) 
    '''
    with open(path) as f:
        return filter(None,map(lambda x: x.split('#')[0].strip(), f.readlines()))

def process(file, market = MARKET_PATH,output = OUTPUT_PATH):
    '''
    process apk file to create market apk archives
    process(apk-file-path, market = MARKET_PATH, output = OUTPUT_PATH)
    '''
    print ("************START PACKING********************")
    markets = parse_markets(market)
    counter = 0
    for market in markets:
        apk_file = write_market(file, market, output)
        verified = verify_market(apk_file, market)
        if not verified:
            print('apk',apk_file,'for market',market,'verify failed')
            # break
        else:
            print('processed apk',apk_file)
            ++counter
    print('all',counter,'apks saved to',os.path.abspath(output)) 
    print ("************PACKING COMPLETE********************")

def run_test(file,times):
    '''
    run market packer performance test
    '''
    print('start to run market packaging testing...')
    t0 = time.time()
    for i in xrange(1,times):
        write_market(file,'%i Test Market' % i, 'temp')
    print('run',times,'using',(time.time() - t0), 'seconds')
    pass

def check(file):
    '''
    check apk file exists, check arguments, check market file exists
    '''
    market = MARKET_PATH
    output = OUTPUT_PATH
    if not os.path.exists(file):
        print('apk file',file,'not exists or not readable')
        return

    if not os.path.exists(market):
        print('market file',market,'not exists or not readable')
        return
    old_market = read_market(file)
    if old_market:
        print('apk file',file,'already had market:',old_market,
            'please using original release apk file')
        return
    process(file,market,output)

def getapkFilePat():
    for root, dirs, files in os.walk("./"):
        for f in files:
            try:
                if f.index('.apk') > 0:
                    print ("find apk file:", f)
                    return f
                else:
                    pass
            except:
                pass

    print ("can not find APK file")
    return None

if __name__ == '__main__':
    print("start to look for apk file  Make Sure There is only one apk file!!!!!!!")
    apkpath = getapkFilePat()
    if apkpath:
        try:
            shutil.rmtree('./archives')
            print ("delete archives doc successsful!")
        except Exception, e:
            pass
        check(apkpath)
        pass
    else:
        print("Make Sure There is only one apk file!!!!!!!")

    # print("START TIME:", datetime.datetime.now())
    # check(**vars(parse_args()))
    # print("END TIME:", datetime.datetime.now())

windows下的打包脚本 需要跟pack.py在同一目录下

python pack.py
pause

markets.txt

QIHOO360#360
CHANGHONG#changhong
XIAOMI#xiaomi

pack.py,pack.bat,markets.txt,xx.apk必须在同一目录下,apk文件保证只有一个。双击pack.bat即可批量打包。

在APP中获取渠道:

PackerNg.java文件复制到自己的工程里面

在代码中获取并设置渠道

// 如果没有使用PackerNg打包添加渠道,默认返回的是""
final String market = PackerNg.getMarket(Context)
// 或者使用 PackerNg.getMarket(Context,defaultValue)
// 之后就可以使用了,比如友盟可以这样设置
AnalyticsConfig.setChannel(market)