關於多渠道打包的最強攻略--總結版

作開發工程師發佈產品時多渠道打包是個必要的過程,此文可以對產品打包及上線不太熟悉的人提供瞭解及建議:

原始多渠道打包

原始多渠道打包的方式,指的是每次打包的時候在代碼中設置channelId,打包完這個渠道的apk包後,需要重新設置channelId再進行打包,如此反覆。該方式多出現在android早期的時候,多被一些剛入行的android工程師使用,或者是一些公司面對較少渠道的時候使用。

原理

原始多渠道打包就是個體力活,在較少渠道的時候可以使用,但是面對上千的渠道的時候,使用這種方式你會後悔當一名android開發工程師。它的原理是在應用代碼中設置渠道ID,使用的時候將渠道ID設置給數據分析接口,數據分析平臺通過該渠道ID分析之。其實後面多渠道方式的本質原理都是這樣的,但是具體擴展方式不同而已,將在後面的分析的時候介紹。

實現

  • 第一步:設置渠道id

方式一 在代碼中直接設置channelId

String channelId="channel1";
  • 1
  • 1

方式二 在AndroidMainfest.xml中application中設置meta-data

<manifest ...><application ...><meta-data
            android:name="CHANNEL_NAME"android:value="channel1" />
        ...
    </application></manifest>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

在代碼中獲取channelId

ApplicationInfo appInfo = this.getPackageManager()
                              .getApplicationInfo(getPackageName()
                              ,PackageManager.GET_META_DATA);
String channelId = appInfo.metaData.getString("CHANNEL_NAME");
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4
  • 第二步:集成到sdk中,比如友盟sdk
MobclickAgent. startWithConfigure(UMAnalyticsConfig config)
UMAnalyticsConfig(Context context, String appkey, String channelId)
UMAnalyticsConfig(Context context, String appkey, String channelId, EScenarioType eType)
UMAnalyticsConfig(Context context, String appkey, String channelId, EScenarioType eType,Boolean isCrashEnable)
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

優缺點

在渠道較少(個位數)的時候可以使用,但對於多渠道的時候太耗時耗力了。

友盟多渠道打包

該方法是友盟幾年前公佈的多渠道打包方式,並且在github開源了打包工具,友盟多渠道打包方式經歷了多次迭代,主要有兩種方式,一種是通過反編譯apk修改渠道信息,另一種是通過AXML解析器編輯修改渠道信息。

原理

  • 第一種方法: 
    通過ApkTool進行解包,然後修改AndroidManifest中修改渠道標示,最後再通過ApkTool進行打包、簽名。

  • 第二種方法: 
    使用AXML解析器axmleditor.jar,擁有很弱的編輯功能,工程中用來編輯二進制格式的 AndroidManifest.xml 文件.

實現

  • 第一步 apktool解包apk

apktool是一個逆向工程工具,可以用它解碼(decode)並修改apk中的資源。接下來詳細介紹如何使用apktool生成渠道包。

Android多渠道打包(一)介紹過,同樣需要在AndroidManifest.xml文件中定義元素,並在應用啓動的時候讀取清單文件中的渠道號。打包時,只需構建一次生成一個apk,然後在該apk的基礎上生成其他渠道包即可。

首先,使用apktool decode應用程序,在終端中輸入如下命令:

apktool d your_unsigned.apk build

解包後生成如下圖片的文件 

  • 第二步 使用python腳本修改AndroidManifest.xml中的渠道號

AndroidManifest.xml文件內容

<manifest ...><application ...><meta-data
            android:name="CHANNEL_NAME"android:value="channel" />
        ...
    </application></manifest>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

Python腳本

import re

def replace_channel(channel, manifest):
    pattern = r'(<meta-data\s+android:name="CHANNEL_NAME"\s+android:value=")(\S+)("\s+/>)'
    replacement = r"\g<1>channel\g<3>".format(channel=channel1)
    return re.sub(pattern, replacement, manifest)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

或者使用AXML解析器直接編輯修改AndroidManifest.xml中的渠道號

  • 第三步 使用apktool重新構建未簽名的apk

apktool b build your_unsigned_apk

  • 第四步 使用jarsigner重新簽名apk

jarsigner -sigalg MD5withRSA -digestalg SHA1 -keystore your_keystore_path -storepass your_storepass -signedjar your_signed_apk, your_unsigned_apk, your_alias

  • 另在代碼中集成,比如友盟sdk
MobclickAgent. startWithConfigure(UMAnalyticsConfig config)
UMAnalyticsConfig(Context context, String appkey, String channelId)
UMAnalyticsConfig(Context context, String appkey, String channelId, EScenarioType eType)
UMAnalyticsConfig(Context context, String appkey, String channelId, EScenarioType eType,Boolean isCrashEnable)
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

官方說明

  • 最近更新

友盟本次更新最大的改變是放棄了 V2.x 版本中通過 Apktool 反編譯apk文件打包的方式,這種打包方式會對開發的apk文件做出大幅度的修改,可能會產生許多不兼容的問題,比如對jar包中包含資源的情況無法支持,對包含 .so 文件的apk兼容性也不好,而且在打包時 AndroidManifest.xml 文件中的特殊標籤會丟失。爲了解決這些問題減少對開發者apk文件的修改, 我們決定放棄這種方式,而採用直接編輯二進制的AndroidManifest.xml 文件的方式。這種方式只會修改 AndroidManifest.xml 文件,對於apk包中的資源文件和代碼文件都不會做任何改變。如果打包不成功,生成的apk文件有問題,在測試階段也可以快速發現,因爲修改只會影響AndroidManifest.xml 相關的少量的設置。

  • 工具使用

axmleditor.jar 一個AXML解析器,擁有很弱的編輯功能,工程中用來編輯二進制格式的 AndroidManifest.xml 文件. 
JarSigner.jar 給 Apk 簽名, SignApk.jar 文件是我們修改過的 apk 簽名工具,實現了和 ADT 中一樣的簽名方式. 
這些Java工具都是使用java7編譯的,如果您還在使用Java 1.6 請留下issue。 
DotNetZip 解壓縮和壓縮文件使用的是DotNetZip(Ionic.Zip.dll), 運行源碼需要加入這個庫.

優缺點

對比之前的老方法大大節省了構建時間,因爲該方法只需構建一次,然後通過腳本修改渠道並簽名就可。 
但是對於三位數以上的渠道還是有點力不從心,另外該方法需要解壓縮、壓縮、重簽名耗費時間較多,重簽名可能會導致apk包在運行時有兼容性問題。

引用

友盟github

maven&gradle打包

原理

都是採用在AndroidManifest.xml的節點中添加如下元素,構建時替換value值得方式。

實現

  • Maven

Maven是一個軟件項目管理和自動構建工具,配合使用Android-maven-plugin插件,以及maven-resources-plugin插件可以很方便的生成渠道包,下面簡要介紹下打包過程,更多Maven以及插件的使用方法請參考相關文檔。

首先,在AndroidManifest.xml的節點中添加如下元素,用來定義渠道的來源:

<!-- 使用Maven打包時會用具體的渠道號替換掉${channel} --><meta-data
        android:name="channel"android:value="${channel}" />
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

定義好渠道來源後,接下來就可以在程序啓動時讀取渠道號了:

private String getChannel(Context context) {
        try {
            PackageManager pm = context.getPackageManager();
            ApplicationInfo appInfo = pm.getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA);
            return appInfo.metaData.getString("channel");
        } catch (PackageManager.NameNotFoundException ignored) {
        }
        return "";
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

要替換AndroidManifest.xml文件定義的渠道號,還需要在pom.xml文件中配置Resources插件:

<resources><resource><directory>${project.basedir}</directory><filtering>true</filtering><targetPath>${project.build.directory}/filtered-manifest</targetPath><includes><include>AndroidManifest.xml</include></includes></resource></resources>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

準備工作已經完成,現在需要的就是實際的渠道號了。下面的腳本會遍歷渠道列表,逐個替換並打包:

#!/bin/bash
package(){
    while read line
    do
        mvn clean
        mvn  -Dchannel=$line package
    done < $1
}

package $1
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

在前期渠道很少時這種方法還可以接受,但只要渠道稍微增多該方法就不再適用了,原因是每打一個包都要執行一遍構建過程,效率太低。

  • gradle

以友盟的渠道統計爲例,渠道信息一般在 AndroidManifest.xml中修改以下值:

<meta-dataandroid:name="UMENG_CHANNEL"android:value="wandoujia" />
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

首先你必須在AndroidManifest.xml中的meta-data修改以下的樣子:

<meta-dataandroid:name="UMENG_CHANNEL"android:value="${UMENG_CHANNEL_VALUE}" />
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

其中${UMENG_CHANNEL_VALUE}中的值就是你在gradle中自定義配置的值。 
build.gradle文件就利用productFlavors這樣寫

productFlavors { 
    wandoujia {
        manifestPlaceholders = [UMENG_CHANNEL_VALUE: "wandoujia"]
    }
    baidu {
        manifestPlaceholders = [UMENG_CHANNEL_VALUE: "baidu"]
    } 
    c360 {
        manifestPlaceholders = [UMENG_CHANNEL_VALUE: "c360"]
    } 
    uc {
        manifestPlaceholders = [UMENG_CHANNEL_VALUE: "uc"]
    } 
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

其中[UMENG_CHANNEL_VALUE: "wandoujia"]就是對應${UMENG_CHANNEL_VALUE}的值。

不過現在有個更加簡潔的寫法

productFlavors {

    wandoujia {...}//支持在{}定義屬性
    baidu {...}
    c360 {...}
    uc {...}

productFlavors.all { flavor ->
    flavor.manifestPlaceholders = [UMENG_CHANNEL_VALUE: name]
}

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

在android studio中sync gradle在build下可以看到

 
直接在gradle中點擊assemble可構建所有渠道的包 
單獨點擊對應渠道的assemble 比如assembleC360可以單獨構建出C360渠道的包 
代碼中獲取渠道值如下代碼

private String getChannel(Context context) {
        try {
            PackageManager pm = context.getPackageManager();
            ApplicationInfo appInfo = pm.getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA);
            return appInfo.metaData.getString("channel");
        } catch (PackageManager.NameNotFoundException ignored) {
        }
        return "";
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

優缺點

maven&gradle對於每個渠道都會單獨構建一次,比較耗時,但是可以對各個渠道更加細化的定製


樣例參考:

Gradle多渠道打包
由於國內Android市場衆多渠道,爲了統計每個渠道的下載及其它數據統計,就需要我們針對每個渠道單獨打包,如果讓你打幾十個市場的包豈不煩死了,不過有了Gradle,這再也不是事了。 以友盟統計爲例,在AndroidManifest.xml裏面會有這麼一段:
<meta-data
android:name="UMENG_CHANNEL"
android:value="Channel_ID" />
裏面的Channel_ID就是渠道標示。我們的目標就是在編譯的時候這個值能夠自動變化。 * 第一步 AndroidManifest.xml裏配置PlaceHolder
<meta-data
android:name="UMENG_CHANNEL"
android:value="${UMENG_CHANNEL_VALUE}" />
第二步 build.gradle 設置productFlavors
android {
productFlavors {
xiaomi {}
_360 {}
baidu {}
wandoujia {}
}
productFlavors.all {
flavor -> flavor.manifestPlaceholders = [UMENG_CHANNEL_VALUE: name]
}
}
然後直接執行 ./gradlew assembleRelease
然後就等待打包完成吧。
assemble 這個命令,會結合 Build Type 創建自己的task,如:
./gradlew assembleDebug
./gradlew assembleRelease
常用命令如下:(linux下是./gradlew,該腳本在項目下,windows直接gradlew即可)
 
./gradlew -v 版本號,首次運行,沒有gradle的要下載的哦。
 
./gradlew clean 刪除HelloWord/app目錄下的build文件夾
 
./gradlew build 檢查依賴並編譯打包
這裏注意的是 ./gradlew build 命令把debugrelease環境的包都打出來,生成的包在目錄HelloWord/app/build/outputs/apk/下。如果正式發佈只需要打release的包,該怎麼辦呢,下面介紹一個很有用的命令 assemble,
 
./gradlew assembleDebug 編譯並打Debug
 
./gradlew assemblexiaomiDebug 編譯並打xiaomidebug包,其他類似
 
./gradlew assembleRelease 編譯並打Release的包
 
./gradlew assemblexiaomiRelease 編譯並打xiaomiRelease包,其他類似
 
./gradlew installRelease Release模式打包並安裝
 
./gradlew uninstallRelease 卸載Release模式包
http://www.jianshu.com/p/44d40f8e67c9 git自動獲取包名打包

360多渠道打包

來源

這個打包方法是由奇虎360的工程師開源出來的,這位大神在github的id是seven456

原理

利用的是Zip文件“可以添加comment(摘要)”的數據結構特點,在文件的末尾寫入任意數據,而不用重新解壓zip文件(apk文件就是zip文件格式);所以該工具不需要對apk文件解壓縮和重新簽名即可完成多渠道自動打包,高效速度快,無兼容性問題;

實現方式

  • java源碼
/*關鍵代碼*/public static void main(String[] args) throws Exception {
//      寫入渠道號//      args = "-path D:/111.apk -outdir D:/111/ -contents googleplay;m360; -password 12345678".split(" ");//      查看工具程序版本號//      args = "-version".split(" ");//      讀取渠道號//      args = "-path D:/111_m360.apk -password 12345678".split(" ");long time = System.currentTimeMillis();
        String cmdPath  = "-path";
        String cmdOutdir  = "-outdir";
        String cmdContents  = "-contents";
        String cmdPassword  = "-password";
        String cmdVersion  = "-version";
        String help = "用法:java -jar MCPTool.jar [" + cmdPath + "] [arg0] [" + cmdOutdir + "] [arg1] [" + cmdContents + "] [arg2] [" + cmdPassword + "] [arg3]"
                + "\n" + cmdPath + "        APK文件路徑"
                + "\n" + cmdOutdir + "      輸出路徑(可選),默認輸出到APK文件同一級目錄"
                + "\n" + cmdContents + "    寫入內容集合,多個內容之間用“;”分割(linux平臺請在“;”前加“\\”轉義符),如:googleplay;m360; 當沒有" + cmdContents + "”參數時輸出已有文件中的contents"
                + "\n" + cmdPassword + "    加密密鑰(可選),長度8位以上,如果沒有該參數,不加密"
                + "\n" + cmdVersion + " 顯示MCPTool版本號"
                + "\n例如:"
                + "\n寫入:java -jar MCPTool.jar -path D:/test.apk -outdir ./ -contents googleplay;m360; -password 12345678"
                + "\n讀取:java -jar MCPTool.jar -path D:/test.apk -password 12345678";

        if (args.length == 0 || args[0] == null || args[0].trim().length() == 0) {
            System.out.println(help);
        } else {
            if (args.length > 0) {
                if (args.length == 1 && cmdVersion.equals(args[0])) {
                    System.out.println("version: " + VERSION_1_1);
                } else {
                    Map<String, String> argsMap = new LinkedHashMap<String, String>();
                    for (int i = 0; i < args.length; i += 2) {
                        if (i + 1 < args.length) {
                            if (args[i + 1].startsWith("-")) {
                                throw new IllegalStateException("args is error, help: \n" + help);
                            } else {
                                argsMap.put(args[i], args[i + 1]);
                            }
                        }
                    }
                    System.out.println("argsMap = " + argsMap);
                    File path = argsMap.containsKey(cmdPath) ? new File(argsMap.get(cmdPath)) : null;
                    String parent = path == null? null : (path.getParent() == null ? "./" : path.getParent());
                    File outdir = parent == null ? null : new File(argsMap.containsKey(cmdOutdir) ? argsMap.get(cmdOutdir) : parent);
                    String[] contents = argsMap.containsKey(cmdContents) ? argsMap.get(cmdContents).split(";") : null;
                    String password = argsMap.get(cmdPassword);
                    if (path != null) {
                        System.out.println("path: " + path);
                        System.out.println("outdir: " + outdir);
                        if (contents != null && contents.length > 0) {
                            System.out.println("contents: " + Arrays.toString(contents));
                        }
                        System.out.println("password: " + password);
                        if (contents == null || contents.length == 0) { // 讀取數據;
                            System.out.println("content: " + readContent(path, password));
                        } else { // 寫入數據;
                            String fileName = path.getName();
                            int dot = fileName.lastIndexOf(".");
                            String prefix = fileName.substring(0, dot);
                            String suffix = fileName.substring(dot);
                            for (String content : contents) {
                                File target = new File(outdir, prefix + "_" + content + suffix);
                                if (nioTransferCopy(path, target)) {
                                    write(target, content, password);
                                }
                            }
                        }
                    }
                }
            }
        }
        System.out.println("time:" + (System.currentTimeMillis() - time));
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 使用方法

1、命令行使用說明: 
用法:Java -jar MCPTool.jar [-path] [arg] [-contents] [arg] [-password] [arg] 
-path APK文件路徑 
-outdir 輸出路徑(可選),默認輸出到APK文件同一目錄 
-contents 寫入內容集合,多個內容之間用“;”分割,如:googleplay;m360; 當沒有“-contents”參數時輸出已有文件中的content 
-password 加密密鑰(可選),長度8位以上,如果沒有該參數,不加密 
-version 顯示版本號 
例如:

寫入: 
java -jar MCPTool.jar -path D:/test.apk -outdir ./ -contents googleplay;m360; -password 12345678 
讀取: 
java -jar MCPTool.jar -path D:/test.apk -password 12345678

2、Android代碼中讀取寫入的渠道號: 
導入MCPTool.jar中的MCPTool類,MCPTool.getChannelId(context, mcptoolPassword, defValue)讀出寫入的渠道號;

3、jenkins、hudson、ant使用說明: 
請看MultiChannelPackageTool\build-ant\MCPTool\build.xml文件;

4、Windows下bat腳本運行說明: 
拖拽文件即可完成多渠道打包:MultiChannelPackageTool\build-ant\MCPTool\MCPTool.bat; 
拖拽文件檢查渠道號是否寫入成功:MultiChannelPackageTool\build-ant\MCPTool\MCPTool-check.bat;

  • 獲取渠道號
/**
     * Android平臺讀取渠道號
     * @param context Android中的android.content.Context對象
     * @param mcptoolPassword mcptool解密密鑰
     * @param defValue 讀取不到時用該值作爲默認值
     * @return
     */public static String getChannelId(Object context, String mcptoolPassword, String defValue) {
        String content = MCPTool.readContent(new File(getPackageCodePath(context)), mcptoolPassword);
        return content == null || content.length() == 0 ? defValue : content;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
String channelId = MCPTool.getChannelId(context,password,default);
  • 1
  • 1

優缺點

沒有解壓縮、壓縮、重簽名,沒有兼容性問題,速度最快;寫入的渠道號數據支持加密,安全可靠;

由於速度極快,我還可以作爲服務器端下載apk時動態寫入“特定數據”,用戶下載到apk後安裝啓動,讀取“特定數據”完成特定的操作; 
如:加好友功能,下載前寫入用戶ID,用戶下載後啓動apk,讀取寫入的用戶ID,完成加好友操作,用戶體驗大大提升,沒有斷裂感; 
當然,也可以寫入JSON數據,想做什麼就做什麼;

引用

seven456:MultiChannelPackageTool

360多渠道打包升級版:

原理

利用的是Zip文件“可以添加comment(摘要)”的數據結構特點,在文件的末尾寫入任意數據,而不用重新解壓zip文件(apk文件就是zip文件格式)。

實現

實現方式有三種:Python腳本、Java腳本、gradle構建

  • 方法一:python腳本的方式

python源碼

'''關鍵代碼'''def _check(apkfile, marketfile=MARKET_PATH, output=OUTPUT_PATH, format=ARCHIVE_FORMAT, show=False, test=0):'''
    check apk file exists, check apk valid, check arguments, check market file exists
    '''if not os.path.exists(apkfile):
        print('apk file', apkfile, 'not exists or not readable')
        returnif not parse_apk(apkfile):
        print('apk file', apkfile, 'is not valid apk')
        returnif show:
        show_market(apkfile)
        returnif test > 0:
        run_test(apkfile, test)
        returnif not os.path.exists(marketfile):
        print('marketfile file', marketfile, 'not exists or not readable.')
        return
    old_market = read_market(apkfile)
    if old_market:
        print('apk file', apkfile, 'already had market:', old_market,
              'please using original release apk file')
        return
    process(apkfile, marketfile, output, format)


def _parse_args():'''
    parse command line arguments
    '''
    parser = argparse.ArgumentParser(
        formatter_class=argparse.RawDescriptionHelpFormatter,
        description='PackerNg v{0} created by mcxiaoke.\n {1}'.format(__version__, INTRO_TEXT),
        epilog='')
    parser.add_argument('apkfile', nargs='?',
                        help='original release apk file path (required)')
    parser.add_argument('marketfile', nargs='?', default=MARKET_PATH,
                        help='markets file path [default: ./markets.txt]')
    parser.add_argument('output', nargs='?', default=OUTPUT_PATH,
                        help='archives output path [default: ./archives]')
    parser.add_argument('-f', '--format', nargs='?', default=ARCHIVE_FORMAT, const=True,
                        help="archive format [default:'${name}-${package}-v${vname}-${vcode}-${market}${ext}']")
    parser.add_argument('-s', '--show', action='store_const', const=True,
                        help='show apk file info (pkg/market/version)')
    parser.add_argument('-t', '--test', default=0, type=int,
                        help='perform serval times packer-ng test')
    args = parser.parse_args()
    if len(sys.argv) == 1:
        parser.print_help()
        return Nonereturn args

if __name__ == '__main__':
    args = _parse_args()
    if args:
        _check(**vars(args))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59

python腳本

python PackerNg.py [file] [market] [output] [-h] [-s] [-t TEST]

方法二:java腳本的方式

/*關鍵代碼*//*java 腳本程序入口*/public static void main(String[] args) {
        if (args.length < 2) {
            Helper.println(USAGE_TEXT);
            Helper.println(INTRO_TEXT);
            System.exit(1);
        }
        File apkFile = new File(args[0]);
        File marketFile = new File(args[1]);
        File outputDir = new File(args.length >= 3 ? args[2] : "apks");
        if (!apkFile.exists()) {
            Helper.printErr("Apk file '" + apkFile.getAbsolutePath() +
                    "' is not exists or not readable.");
            Helper.println(USAGE_TEXT);
            System.exit(1);
            return;
        }
        if (!marketFile.exists()) {
            Helper.printErr("Market file '" + marketFile.getAbsolutePath() +
                    "' is not exists or not readable.");
            Helper.println(USAGE_TEXT);
            System.exit(1);
            return;
        }
        if (!outputDir.exists()) {
            outputDir.mkdirs();
        }
        Helper.println("Apk File: " + apkFile.getAbsolutePath());
        Helper.println("Market File: " + marketFile.getAbsolutePath());
        Helper.println("Output Dir: " + outputDir.getAbsolutePath());
        List<String> markets = null;
        try {
            markets = Helper.parseMarkets(marketFile);
        } catch (IOException e) {
            Helper.printErr("Market file parse failed.");
            System.exit(1);
        }
        if (markets == null || markets.isEmpty()) {
            Helper.printErr("No markets found.");
            System.exit(1);
            return;
        }
        final String baseName = Helper.getBaseName(apkFile.getName());
        final String extName = Helper.getExtension(apkFile.getName());
        int processed = 0;
        try {
            for (final String market : markets) {
                final String apkName = baseName + "-" + market + "." + extName;
                File destFile = new File(outputDir, apkName);
                Helper.copyFile(apkFile, destFile);
                Helper.writeMarket(destFile, market);
                if (Helper.verifyMarket(destFile, market)) {
                    ++processed;
                    Helper.println("Generating apk " + apkName);
                } else {
                    destFile.delete();
                    Helper.printErr("Failed to generate " + apkName);
                }
            }
            Helper.println("[Success] All " + processed
                    + " apks saved to " + outputDir.getAbsolutePath());
            Helper.println(INTRO_TEXT);
        } catch (MarketExistsException ex) {
            Helper.printErr("Market info exists in '" + apkFile
                    + "', please using a clean apk.");
            System.exit(1);
        } catch (IOException ex) {
            Helper.printErr("" + ex);
            System.exit(1);
        }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73

java腳本

java -jar PackerNg.jar apkFile marketFile outputDir

方法三:gradle構建

在項目top level build.gradle中添加

buildscript {
    ......
    dependencies{
    // add packer-ng
        classpath 'com.mcxiaoke.gradle:packer-ng:1.0.7'
    }
}  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

在 app level build.gradle中添加

apply plugin: 'packer'
packer {
    checkSigningConfig = true
    checkZipAlign = true
    archiveNameFormat = '${appPkg}-${flavorName}-${buildType}-v${versionName}-${versionCode}-${fileMD5}'
    archiveOutput = file(new File(project.rootProject.buildDir.path, "apks"))
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

着重幾點

  • 改善了360多渠道打包方式中api兼容性的問題

ZipFile.getComment是ZIP文件註釋寫入,使用Java會導致APK文件被破壞,無法安裝。這裏是讀取ZIP文件註釋的問題,Java 7裏可以使用zipFile.getComment()方法直接讀取註釋,非常方便。但是Android系統直到API 19,也就是4.4以上的版本才支持 ZipFile.getComment() 方法。由於要兼容之前的版本,所以這個方法也不能使用。改爲:

public static boolean hasZipCommentMagic(File file) throws IOException {
            RandomAccessFile raf = null;
            try {
                raf = new RandomAccessFile(file, "r");
                long index = raf.length();
                byte[] buffer = new byte[MAGIC.length];
                index -= MAGIC.length;
                // read magic bytes
                raf.seek(index);
                raf.readFully(buffer);
                // check magic bytes matchedreturn isMagicMatched(buffer);
            } finally {
                if (raf != null) {
                    raf.close();
                }
            }
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • Android 7.0簽名校驗引起的安裝失敗

爲了提高Android系統的安全性,Google從Android 7.0開始增加一種新的增強簽名模式,從Android Gradle Plugin 2.2.0開始,構建系統在打包應用後簽名時默認使用APK signature scheme v2,該模式在原有的簽名模式上,增加校驗APK的SHA256哈希值,如果簽名後對APK作了任何修改,安裝時會校驗失敗,提示沒有簽名無法安裝,使用本工具修改的APK會無法安裝,解決辦法是在 signingConfigs 裏增加 v2SigningEnabled false ,禁用新版簽名模式,技術細節請看官方文檔:APK signature scheme v2

android {
    ...
    defaultConfig { ... }
    signingConfigs {
      release {
        storeFile file("myreleasekey.keystore")
        storePassword "password"
        keyAlias "MyReleaseKey"
        keyPassword "password"
        v2SigningEnabled false //禁用v2簽名增強模式
      }
    }
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

優缺點

使用APK註釋保存渠道信息和MAGIC字節,從文件末尾讀取渠道信息,速度飛快

實現爲一個Gradle Plugin,支持定製輸出APK的文件名等信息,方便CI集成

提供Java版和Python的獨立命令行腳本,不依賴Gradle插件,支持獨立使用 
缺點

沒有使用Android的productFlavors實現,無法利用flavors條件編譯的功能


現360推出加固寶,方便進行簽名與打包以及軟件加固防解密,操作比較簡便易上手,有興趣可以用一下

總結


原始多渠道打包:

渠道較少的情況下使用,每設置一次渠道id需要構建一次,完全是個體力活。
  • 1
  • 1

友盟多渠道打包:

打包:解壓apk文件 -> 替換AndroidManifest.xml中的meta-data -> 壓縮apk文件 -> 簽名
讀取渠道號:直接通過Android的API讀取meta-data
特點:需要解壓縮、壓縮、重簽名耗費時間較多,重簽名會導致apk包在運行時有兼容性問題。
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

360多渠道打包

打包:直接寫入渠道號到apk文件的末尾
讀取渠道號:直接讀取data/app/<package>.apk文件末尾的渠道號
特點:沒有解壓縮、壓縮、重簽名,沒有兼容性問題,速度最快;寫入的渠道號數據支持加密,安全可靠。
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

360多渠道打包plus(進階版)

改善了360多渠道打包受android api19的影響,並擴展了java、python、gradle插件版,是目前第三方多渠道打包中速度最快最靈活的。
  • 1
  • 1

maven、gradle版

對於android studio而言基本上拋棄了maven的方式,那麼對於gradle版我們可以通過productFlavors通過更加細膩的定製,不過打包構建過程還是比較耗時。
  • 1
  • 1

那麼我們選擇時可以按實際情況使用360多渠道打包plus,或者android studio gradle多渠道打包。需注意的是360多渠道打包plus無法通過android7.0簽名校驗,當然只要是通過後期修改apk文件的方式都不能通過android7.0的簽名校驗。


發佈了77 篇原創文章 · 獲贊 242 · 訪問量 73萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章