关于多渠道打包的最强攻略--总结版

来源:互联网 发布:美国人知乎 编辑:程序博客网 时间:2024/05/17 01:51
作开发工程师发布产品时多渠道打包是个必要的过程,此文可以对产品打包及上线不太熟悉的人提供了解及建议:

原始多渠道打包

原始多渠道打包的方式,指的是每次打包的时候在代码中设置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 redef 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/bashpackage(){    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 argsif __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的签名校验。


0 0