用Java實現(xiàn)Android多渠道打包工具

博文出處:用Java實現(xiàn)Android多渠道打包工具,歡迎大家關(guān)注我的博客,謝謝!
0001b
======
最近在公司做了一個多渠道打包的工具,趁今天有空就來講講 Android 多渠道打包這件小事。眾所周知,隨著業(yè)務(wù)的不斷增長,APP 的渠道也會越來越多,如果用 Gradle 打多渠道包的話,可能會耗費幾個小時的時間才能打出幾百個渠道包。所以就必須有一種方法能夠解決這種問題。

目前市面上比較好的解決方案就是在 apk 文件中“動手腳”,比如由一位360 Android 工程師提出的“在 apk 文件中添加 comments 多渠道打包方法”,具體的代碼在GitHub 上可以找到:MultiChannelPackageTool 。除此之外,還有美團點評技術(shù)團隊在博客上發(fā)表過一篇《美團Android自動化之旅—生成渠道包》,里面講敘了一種在 apk 文件中的 META-INF 目錄下添加渠道信息的方法,之后再在程序啟動時去動態(tài)讀取,具體的實現(xiàn)原理可以去美團博客上看,這里就不說了。

我們解壓多渠道打出來的 apk 包后,就會發(fā)現(xiàn)在 META-INF 目錄下多了一個 channel_xxxxx 文件,而這個就是我們的渠道文件:

channel文件

本文所采用的方法就是根據(jù)美團提供的思路實現(xiàn)的,當然網(wǎng)上有很多使用 Python 語言實現(xiàn)美團思路的版本,經(jīng)過測試發(fā)現(xiàn) Python 版本比 Java 版本打渠道包的速度更快一些。但是,在這里只提供 Java 版本實現(xiàn)方案,Python 版本實現(xiàn)的方案會在文末以參考鏈接的方式給出。

0010b

在這里先說明一下,Java 編寫的多渠道打包工具依賴 commons-io.jar 和 zip4j.jar 。下面我們就開始進入正題吧。

我們先規(guī)定一下,渠道文件命名為 channel.txt ,并且要打包的 apk 文件和 channel.txt 與多渠道打包工具在同一目錄下。

其中 channel.txt 的格式就是每個渠道獨占一行,如下所示:

wandoujia
googleplay
xiaomi
huawei
kumarket
anzhi

然后我們先定義幾個常量:

// 渠道文件地址
private static final String CHANNEL_FILE_PATH = "./channel.txt";

private static final String CHARSET_NAME = "UTF-8";
// 當前要打包的apk的路徑
private static final String APK_PATH = "./";
// 渠道打包后輸出的apk文件夾前綴
private static final String APK_OUT_PATH_PREFIX = "./out_apk_";

private static final String APK_SUFFIX = ".apk";

定義好之后,我們下一步就是編寫方法去讀取 channel.txt 中的渠道信息:

/**
 * 從文件中讀取channel
 * 
 * @return
 */
public static List<String> getChannel() {
    List<String> channelList = new ArrayList<>();
    InputStream inputStream = null;
    BufferedReader reader = null;
    try {
        inputStream = new FileInputStream(CHANNEL_FILE_PATH);
        reader = new BufferedReader(new InputStreamReader(inputStream,
                CHARSET_NAME));
        String buffer;
        while ((buffer = reader.readLine()) != null && buffer.length() != 0) {
            System.out.println("發(fā)現(xiàn)已有渠道 : " + buffer);
            channelList.add(buffer);
        }
    } catch (FileNotFoundException e) {
        System.out.println("當前目錄下未找到channel.txt");
        e.printStackTrace();
    } catch (UnsupportedEncodingException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            if (reader != null) {
                reader.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    return channelList;
}

上面 getChannel() 方法中都是簡單的 I/O 流操作,相信不需要解釋大家都可以看得懂吧。之后我們要做的就是去當前路徑下查找有無 apk 文件。在這里說明一下,我們這個多渠道打包小工具是支持多個 apk 文件一起打包的,所以我們要把當前目錄下所有 apk 文件的路徑存儲起來。

/**
 * 得到當前目錄下的所有apk
 * 
 * @param file
 * @return
 */
public static List<String> getApk(File file) {
    List<String> apkList = new ArrayList<>();
    File[] childFiles = file.listFiles();
    for (File childFile : childFiles) {
        if (!childFile.isDirectory()
                && childFile.getName().endsWith(APK_SUFFIX)) {
            System.out.println("發(fā)現(xiàn)已有apk : " + childFile.getName());
            apkList.add(childFile.getName());
        }
    }
    return apkList;
}

做好上面的步驟后,最后就剩下打包的代碼了,一起來看看:

/**
 * 打包apk
 */
public static void buildApk() {
    List<String> apkList = getApk(new File(APK_PATH));
    int count = apkList.size();
    if (count == 0) {
        System.out.println("當前目錄下沒有發(fā)現(xiàn)apk文件");
        return;
    }
    // 遍歷所有apk文件
    for (int i = 0; i < count; i++) {
        String name = apkList.get(i);
        // 得到文件名字
        String baseName = apkList.get(i).substring(0,
                name.lastIndexOf("."));
        // apk輸出目錄
        File dictionary = new File(APK_OUT_PATH_PREFIX + baseName);
        if (!dictionary.exists()) {
            dictionary.mkdir();
        }
        List<String> channelList = getChannel();
        if (channelList.size() == 0) {
            System.out.println("channel.txt文件中沒有多渠道信息");
            return;
        }
        // 遍歷所有渠道
        for (String channel : channelList) {
            try {
                String sourceFileName = APK_PATH + name;
                // 輸出的apk名字
                String outApkName = baseName + "_" + channel + APK_SUFFIX;
                // apk包的路徑
                String outApkFileName = dictionary.getName() + "/" + outApkName;
                // 復(fù)制要打包的apk
                copy(sourceFileName, outApkFileName);
                System.out.println("正在打 " + channel + " 的渠道包 : " + outApkName);
                ZipFile zipFile = new ZipFile(outApkFileName);
                ZipParameters parameters = new ZipParameters();
                parameters
                        .setCompressionMethod(Zip4jConstants.COMP_DEFLATE);
                parameters
                        .setCompressionLevel(Zip4jConstants.DEFLATE_LEVEL_NORMAL);
                parameters.setRootFolderInZip("META-INF/");
                // 當前目錄下創(chuàng)建一個channel_xxxxx文件
                File channelFile = new File(dictionary.getName() + "/channel_"
                        + channel);
                if (!channelFile.exists()) {
                    channelFile.createNewFile();
                }
                // 在META-INF文件夾中添加channel_xxxxx文件
                zipFile.addFile(channelFile, parameters);
                // 刪除當前目錄下的channel_xxxxx文件
                channelFile.delete();
            } catch (ZipException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

/**
 * 復(fù)制文件
 * 
 * @param sourceFilePath
 * @param copyFilePath
 */
private static void copy(String sourceFilePath, String copyFilePath){
    try {
        // 這里使用的是 common-io.jar 中的文件復(fù)制方法,比原生Java I/O API操作速度要快
        FileUtils.copyFile(new File(sourceFilePath), new File(copyFilePath));
    } catch (IOException e) {
        e.printStackTrace();
    }
}

public static void main(String[] args) {
    long preTime = System.currentTimeMillis();
    buildApk();
    System.out.println("多渠道打包完成,耗時 " + (System.currentTimeMillis() - preTime)/1000 + " s");
}

buildApk() 方法中主要做的就是兩個 for 循環(huán)嵌套。遍歷當前目錄的 apk 文件,然后遍歷渠道信息,最后打包。另外需要注意的是要復(fù)制出一個 apk 文件來進行多渠道打包,而不是在原文件的基礎(chǔ)上。

在這里打包的部分就結(jié)束了,我們還有一個步驟需要完成。那就是在應(yīng)用程序啟動時去讀取相應(yīng)的渠道,可以通過以下方法去讀取:

public static String getChannelFromMeta(Context context) {
    ApplicationInfo appinfo = context.getApplicationInfo();
    String sourceDir = appinfo.sourceDir;
    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("META-INF/channel_")) {
                ret = entryName;
                break;
            }
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (zipfile != null) {
            try {
                zipfile.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    String[] split = ret.split("_");
    if (split != null && split.length >= 2) {
        return ret.substring(split[0].length() + 1);
    } else {
        return "default";
    }
}

讀取渠道之后,我們 APP 可以把相應(yīng)的渠道號發(fā)送給服務(wù)器或者第三方統(tǒng)計平臺做統(tǒng)計。

0011b

最后,我們可以把這個多渠道打包的 Java 項目打成一個 jar 包,然后寫一個 bat 腳本,這樣就通過鼠標雙擊就可以實現(xiàn)快速打渠道包了。以下是 bat 腳本的內(nèi)容,要注意的是 bat 腳本要和 jar 包處于同一級目錄下才可以哦:

@echo off
echo 歡迎使用多渠道打包工具
echo 請確保當前目錄下有要打包的apk文件和渠道信息channel.txt
java -jar AndroidBuildApkTool.jar
echo 按任意鍵退出
pause>nul
exit

通過我們的努力 Java 版的多渠道打包工具就做好了。但是不足的是,測試后發(fā)現(xiàn) Java 版打渠道包的速度沒有 Python 版的快,主要是在 apk 文件中添加渠道信息文件這一步操作耗費的時間有點多。如果哪位小伙伴有更好的解決方案,歡迎聯(lián)系我!

附上多渠道打包工具的源碼:

MultiChannelBuildTool.rar

0100b

References:

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

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

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,094評論 25 709
  • 目錄一、Python打包及優(yōu)化(美團多渠道打包)二、Gradle打包三、其他打包方案:修改Zip文件的commen...
    守望君閱讀 5,935評論 4 17
  • Android市場的渠道分散已不是什么新鮮事,但如何高效打包仍是令許多開發(fā)者頭疼的問題。本篇文章著重介紹了目前最新...
    _曾胖子閱讀 2,011評論 1 10
  • 我 們 都 想 遇 到 一 個 人符 合 我 們 想 象 的 所 有 標 準然 而 愛 情 里 的 如 愿 以 償...
    蕊希Erin閱讀 1,873評論 0 6
  • 現(xiàn)在高考分數(shù)陸陸續(xù)續(xù)出來了,接下來就是志愿填報了。這幾天有高三的學(xué)生問我怎么填報志愿。這個學(xué)生沒有明確的喜好,看著...
    breastli閱讀 254評論 0 1

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