Android多渠道打包-Fast

實(shí)現(xiàn)原理

Android的安裝包中有一個(gè)META-INF目錄。如果在META-INF目錄內(nèi)添加空文件,可以不用重新簽名應(yīng)用。因此,通過(guò)為不同渠道的應(yīng)用添加不同的空文件,可以唯一標(biāo)識(shí)一個(gè)渠道。然后可以通過(guò)java代碼獲取到這個(gè)文件名從而獲取到渠道號(hào)。

使用這種方式,每個(gè)渠道包只要復(fù)制一個(gè)apk,然后在META-INF中創(chuàng)建一個(gè)渠道號(hào)命名的空文件即可,這種打包方式速度非常快,900多個(gè)渠道不到一分鐘就能打完。

打包腳本

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

# 空文件 便于寫(xiě)入此空文件到apk包中作為channel文件
src_empty_file = 'empty.txt'
# 創(chuàng)建一個(gè)空文件(不存在則創(chuàng)建)
f = open(src_empty_file, 'w')
f.close()

# 獲取當(dāng)前目錄中所有的apk源包
src_apks = []
# python3 : os.listdir()即可,這里使用兼容Python2的os.listdir('.')
#篩選apk后綴的文件,過(guò)濾unaligned的和debug的apk
path = os.path.join(os.path.dirname(__file__), "build", "outputs", "apk")
for file in os.listdir(path):
   if ('unaligned' not in file) and ('debug' not in file):
        file = os.path.join(path, file)
        if os.path.isfile(file):
            extension = os.path.splitext(file)[1][1:]
            if extension in 'apk':
                print(file)
                src_apks.append(file)

# 獲取渠道列表
channel_file = os.path.join(os.path.dirname(__file__), "channel.txt")
f = open(channel_file)
lines = f.readlines()
f.close()

for src_apk in src_apks:
    # 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]
    # src_apk_name = "iShowDubbing.apk"
    # 后綴名,包含.   例如: ".apk "
    src_apk_extension = temp_list[1]

    # 創(chuàng)建生成目錄,與文件名相關(guān)
    output_dir = os.path.join(path, 'output_' + src_apk_name + '/')
    # 目錄不存在則創(chuàng)建
    if not os.path.exists(output_dir):
        os.mkdir(output_dir)

    # 遍歷渠道號(hào)并創(chuàng)建對(duì)應(yīng)渠道號(hào)的apk文件
    for line in lines:
        # 獲取當(dāng)前渠道號(hào),因?yàn)閺那牢募蝎@得帶有\(zhòng)n,所有strip一下
        target_channel = line.strip()
        # 拼接對(duì)應(yīng)渠道號(hào)的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/channel_{channel}".format(channel = target_channel)
        # 寫(xiě)入渠道信息
        zipped.write(src_empty_file, empty_channel_file)
        # 關(guān)閉zip流
        zipped.close()

此腳本用python編寫(xiě),主要是獲取渠道號(hào)列表,然后復(fù)制已經(jīng)簽名的apk,在apk中的META-INF目錄下創(chuàng)建一個(gè)以channel_渠道號(hào)命名的空文件。渠道列表寫(xiě)在channel.txt中,多個(gè)渠道用換行隔開(kāi)。例如:

ying_yong_bao
wandoujia
huawei
jifeng
Ucanzhuo
meizu
anzhuoshichang
m91
baidushoujizhushou
tianyidianxin
yingyonghui
shougoushoujizhushou
lianxiang
anzhi
yiyonghui
oppo
xiaomi
m360
mumayi
qiniu
guan_wang
qita
souhu
fensitong
guangdiantong
jiaoyuyingyongbao
jinritoutiao
sanxing
google_play
beiyong1
beiyong2
liujianghua
woyaodangxueba
mayigongshe
tianwen

最后將python腳本和channel.txt放到主工程目錄下即可。

獲取渠道號(hào)

package com.ishowedu.peiyin.util;

import java.io.IOException;
import java.util.Enumeration;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.preference.PreferenceManager;
import android.text.TextUtils;

public class ChannelUtil {

    private static final String TAG = "ChannelUtil";
    private static final String CHANNEL_KEY = "channel";
    private static final String CHANNEL_VERSION_KEY = "channel_version";
    private static String mChannel;
    /**
     * 返回市場(chǎng)。  如果獲取失敗返回""
     * @param context
     * @return
     */
    public static String getChannel(Context context){
        return getChannel(context, "");
    }
    /**
     * 返回市場(chǎng)。  如果獲取失敗返回defaultChannel
     * @param context
     * @param defaultChannel
     * @return
     */
    public static String getChannel(Context context, String defaultChannel) {
        //內(nèi)存中獲取
        if(!TextUtils.isEmpty(mChannel)){
            return mChannel;
        }
        //sp中獲取
        mChannel = getChannelBySharedPreferences(context);
        if(!TextUtils.isEmpty(mChannel)){
            return mChannel;
        }
        //從apk中獲取
        mChannel = getChannelFromApk(context, CHANNEL_KEY);
        if(!TextUtils.isEmpty(mChannel)){
            //保存sp中備用
            saveChannelBySharedPreferences(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;
        //默認(rèn)放在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;
    }
    /**
     * 本地保存channel & 對(duì)應(yīng)版本號(hào)
     * @param context
     * @param channel
     */
    private static void saveChannelBySharedPreferences(Context context, String channel){
        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
        Editor editor = sp.edit();
        editor.putString(CHANNEL_KEY, channel);
        editor.putInt(CHANNEL_VERSION_KEY, getVersionCode(context));
        editor.apply();
    }
    /**
     * 從sp中獲取channel
     * @param context
     * @return 為空表示獲取異常、sp中的值已經(jīng)失效、sp中沒(méi)有此值
     */
    private static String getChannelBySharedPreferences(Context context){
        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
        int currentVersionCode = getVersionCode(context);
        if(currentVersionCode == -1){
            //獲取錯(cuò)誤
            return "";
        }
        int versionCodeSaved = sp.getInt(CHANNEL_VERSION_KEY, -1);
        if(versionCodeSaved == -1){
            //本地沒(méi)有存儲(chǔ)的channel對(duì)應(yīng)的版本號(hào)
            //第一次使用  或者 原先存儲(chǔ)版本號(hào)異常
            return "";
        }
        if(currentVersionCode != versionCodeSaved){
            return "";
        }
        return sp.getString(CHANNEL_KEY, "");
    }
    /**
     * 從包信息中獲取版本號(hào)
     * @param context
     * @return
     */
    private static int getVersionCode(Context context){
        try{
            return context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionCode;
        }catch(NameNotFoundException e) {
            e.printStackTrace();
        }
        return -1;
    }
}

通過(guò)此java工具類就可以獲取到渠道號(hào)。
如果使用友盟的統(tǒng)計(jì),在程序啟動(dòng)時(shí),可以通過(guò)此方法設(shè)置渠道號(hào)

AnalyticsConfig.setChannel(ChannelUtil.getChannel(context));

PS: apk用的是java那一套簽名,放在META-INF文件夾里的文件原則上是不參與簽名的。如果Google修改了apk的簽名規(guī)則,這一套可能就不適用了。

參考地址

美團(tuán)Android自動(dòng)化之旅:http://tech.meituan.com/mt-apk-packaging.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android多渠道打包 概述 每當(dāng)發(fā)新版本時(shí),Android客戶端會(huì)被分發(fā)到各個(gè)應(yīng)用市場(chǎng),比如豌豆莢,360手機(jī)...
    礪雪凝霜閱讀 2,234評(píng)論 2 11
  • 目錄一、Python打包及優(yōu)化(美團(tuán)多渠道打包)二、Gradle打包三、其他打包方案:修改Zip文件的commen...
    守望君閱讀 5,931評(píng)論 4 17
  • Android市場(chǎng)的渠道分散已不是什么新鮮事,但如何高效打包仍是令許多開(kāi)發(fā)者頭疼的問(wèn)題。本篇文章著重介紹了目前最新...
    _曾胖子閱讀 2,009評(píng)論 1 10
  • 最近在項(xiàng)目中遇到需要實(shí)現(xiàn) Apk 多渠道、定制化打包, Google 、百度查找了一些資料,成功實(shí)現(xiàn)了上述功能,在...
    看一季殘花落幕閱讀 2,645評(píng)論 1 8
  • 參考:https://segmentfault.com/a/1190000003763833 生成keystore...
    小面包屑閱讀 2,362評(píng)論 0 2

友情鏈接更多精彩內(nèi)容