1. 程式人生 > >Android產品研發(五)-->多渠道打包

Android產品研發(五)-->多渠道打包

國內的Android開發者還是很苦逼的,由於眾所周知的原因,google play無法在國內開啟(翻牆的就不在考慮之內了),所以Android系的應用市場,群雄爭霸。後果就是國記憶體在著有眾多的應用市場,產品在不同的渠道可能有這不同的統計需求,為此Android開發人員需要為每個應用市場釋出一個安裝包,這裡就引出了Android的多渠道打包。

首先我們說明一下什麼是多渠道打包?

國記憶體在著眾多的Android應用市場,為了統計不同安卓應用市場的下載量一個個性化統計需求,需要為每個應用市場的Android包設定一個可以區分應用市場的標識,這個為Android包設定應用市場標識的過程就是多渠道打包。

幾種主流的多渠道打包方式,以及其優劣勢

  • 通過配置gradle指令碼實現多渠道打包

這種打包方式是使用Android Studio的編譯工具gradle配合使用的,其核心原理就是通過指令碼修改AndroidManifest.xml中的mate-date內容,執行N次打包簽名操作實現多渠道打包的需求,具體實現如下。

(一)在Androidmanifest.xml中定義mate-data標籤

<manifest xmlns:Android="http://schemas.Android.com/apk/res/Android"    
    package="your.package.name"
>
<application> <meta-data Android:name="UMENG_CHANNEL" Android:value="{UMENG}"/> </application> </manifest>

這裡需要注意的是:上面的value的值要和渠道名所對應,比如wandoujia裡面要對應為你豌豆莢的渠道名稱

(二)在build.gradle下的productFlavors定義渠道號:

productFlavors {  

        internal {}  

        /*InHouse
{} pcguanwang {} h5guanwang {} hiapk {} m91 {} appchina {} baidu {} qq {} jifeng {} anzhi {} mumayi {} m360 {} youyi {} wandoujia {} xiaomi {} sougou {} leshangdian {} huawei {} uc {} oppo {} flyme {} jinli {} letv {}*/ productFlavors.all { flavor -> flavor.manifestPlaceholders = [UMENG_CHANNEL_VALUE: name] } }

同時需要注意的是,這裡需要在defaultConfig中配置一個預設的渠道名稱

manifestPlaceholders = [UMENG_CHANNEL_VALUE: "channel_name"]  

實現多渠道打包更換mate-data標籤中的內容

優勢:方便靈活,可以根據自身的需求配置不同的渠道執行不同的邏輯;
劣勢:打包速度過慢;

  • 使用第三方打包工具

這種方式就是使用第三方的服務,比如360,百度,友盟等,其原理也是通過修改AndroidManifest.xml中的mate-data標籤內容,然後執行N次打包簽名的操作實現多渠道打包的。這裡就不在做具體解釋說明,免得又做廣告的嫌疑,O(∩_∩)O哈哈~。

優勢:簡單方便,幾乎不用自身做什麼工作;
劣勢:打包速度過慢;

  • 使用美團多渠道打包方式

而這裡主要是根據美團客戶端打包經驗(詳見:美團Android自動化之旅—生成渠道包
主要是介紹利用在META-INF目錄內新增空檔案的方式,實現批量快速打包Android應用。

實現原理

Android應用安裝包apk檔案其實是一個壓縮檔案,可以將字尾修改為zip直接解壓。解壓安裝檔案後會發現在根目錄有一個META-INF目錄。如果在META-INF目錄內新增空檔案,可以不用重新簽名應用。因此,通過為不同渠道的應用新增不同的空檔案,可以唯一標識一個渠道。
“採用這種方式,每打一個渠道包只需複製一個apk,在META-INF中新增一個使用渠道號命名的空檔案即可。這種打包方式速度非常快,900多個渠道不到一分鐘就能打完。”

實現步驟

(一)編寫渠道號檔案

(二)編寫python指令碼,實現解壓縮apk檔案,為META-INF目錄新增檔案,重新壓縮apk檔案等邏輯:

# coding=utf-8
import zipfile
import shutil
import os

def delete_file_folder(src):  
    '''delete files and folders''' 
    if os.path.isfile(src):  
        try:  
            os.remove(src)  
        except:  
            pass 
    elif os.path.isdir(src):  
        for item in os.listdir(src):  
            itemsrc=os.path.join(src,item)  
            delete_file_folder(itemsrc)  
        try:  
            os.rmdir(src)  
        except:  
            pass

# 建立一個空檔案,此檔案作為apk包中的空檔案
src_empty_file = 'info/empty.txt'
f = open(src_empty_file,'w')
f.close()

# 在渠道號配置檔案中,獲取指定的渠道號
channelFile = open('./info/channel.txt','r')
channels = channelFile.readlines()
channelFile.close()
print('-'*20,'all channels','-'*20)
print(channels)
print('-'*50)

# 獲取當前目錄下所有的apk檔案
src_apks = [];
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)

# 遍歷所以的apk檔案,向其壓縮檔案中新增渠道號檔案
for src_apk in src_apks:
    src_apk_file_name = os.path.basename(src_apk)
    print('current apk name:',src_apk_file_name)
    temp_list = os.path.splitext(src_apk_file_name)
    src_apk_name = temp_list[0]
    src_apk_extension = temp_list[1]

    apk_names = src_apk_name.split('-');

    output_dir = 'outputDir'+'/'
    if os.path.exists(output_dir):
        delete_file_folder(output_dir)
    if not os.path.exists(output_dir):
        os.mkdir(output_dir)

    # 遍歷從檔案中獲得的所以渠道號,將其寫入APK包中
    for line in channels:
        target_channel = line.strip()
        target_apk = output_dir + apk_names[0] + "-" + target_channel+"-"+apk_names[2] + src_apk_extension
        shutil.copy(src_apk,  target_apk)
        zipped = zipfile.ZipFile(target_apk, 'a', zipfile.ZIP_DEFLATED)
        empty_channel_file = "META-INF/uuchannel_{channel}".format(channel = target_channel)
        zipped.write(src_empty_file, empty_channel_file)
        zipped.close()

print('-'*50)
print('repackaging is over ,total package: ',len(channels))
input('\npackage over...')

(三)打包一個正常的apk包
(四)執行python指令碼,多渠道打包
(五)Android程式碼中獲取渠道號

/**
 * 渠道號工具類:解析壓縮包,從中獲取渠道號
 */
public class ChannelUtil {
    private static final String CHANNEL_KEY = "uuchannel";
    private static final String DEFAULT_CHANNEL = "internal";
    private static String mChannel;

    public static String getChannel(Context context) {
        return getChannel(context, DEFAULT_CHANNEL);
    }

    public static String getChannel(Context context, String defaultChannel) {
        if (!TextUtils.isEmpty(mChannel)) {
            return mChannel;
        }
        //從apk中獲取
        mChannel = getChannelFromApk(context, CHANNEL_KEY);
        if (!TextUtils.isEmpty(mChannel)) {
            return mChannel;
        }
        //全部獲取失敗
        return defaultChannel;
    }
    /**
     * 從apk中獲取版本資訊
     *
     * @param context
     * @param channelKey
     * @return
     */
    private static String getChannelFromApk(Context context, String channelKey) {
        long startTime = System.currentTimeMillis();
        //從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 channel = "";
        if (!TextUtils.isEmpty(ret)) {
            String[] split = ret.split("_");
            if (split != null && split.length >= 2) {
                channel = ret.substring(split[0].length() + 1);
            }
            System.out.println("-----------------------------");
            System.out.println("渠道號:" + channel + ",解壓獲取渠道號耗時:" + (System.currentTimeMillis() - startTime) + "ms");
            System.out.println("-----------------------------");
        } else {
            System.out.println("未解析到相應的渠道號,使用預設內部渠道號");
            channel = DEFAULT_CHANNEL;
        }
        return channel;
    }
}

整個打包的流程就是這樣了,打工工具可參考github上的專案:多渠道打包實現

優勢:打包速度很快,很方便;
劣勢:不夠靈活,不能靈活的配置不同的渠道不同的業務邏輯;

問題:
專案中由於使用了友盟統計,以前是在meta-data中儲存渠道資訊,現在更改了方式之後需要手動執行渠道號的設定程式碼:

String channel = ChannelUtil.getChannel(mContext);
        System.out.println("啟動頁獲取到的渠道號為:" + channel);
        // 設定友盟統計的渠道號,原來是在Manifest檔案中設定的meta-data,現在啟動頁中設定
        AnalyticsConfig.setChannel(channel);

通過這種打包方式以前需要一個小時的打包工作現在只需要一分鐘即可,極大的提高了效率,目前在實際的應用中尚未發現有什麼問題,有這種需求的童鞋可以嘗試一下。

總結

雖說我們總結了三種打包方式,但是其實通過gradle打包和使用第三方服務打包都是執行了N次的打包簽名操作,時間上耗費太多,因此不太推薦,而美團的方式在效率上提高了很多,但是對於那種不同的渠道包執行不同的業務邏輯的需求就無能為例了,只能通過gradle配置,因此大家在選擇多渠道打包方式的時候可以根據自身的需求來選擇。