本章将介绍360多渠道打包的进阶方法
Android多渠道打包(一):基础多渠道打包
Android多渠道打包(二):友盟多渠道打包
Android多渠道打包(三):美团多渠道打包
Android多渠道打包(四):360多渠道打包
Android多渠道打包(五):360多渠道打包+
Android多渠道打包(六):maven&gradle
Android多渠道打包(七):系列总结及展望
本方法来自于github mcxiaoke/packer-ng-plugin,本质上和上一章Android多渠道打包(四):360多渠道打包360工程师所使用的打包方式的原理相同。
利用的是Zip文件“可以添加comment(摘要)”的数据结构特点,在文件的末尾写入任意数据,而不用重新解压zip文件(apk文件就是zip文件格式)。
实现方式有三种:python脚本、java脚本、gradle构建
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')
return
if not parse_apk(apkfile):
print('apk file', apkfile, 'is not valid apk')
return
if show:
show_market(apkfile)
return
if test > 0:
run_test(apkfile, test)
return
if 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 None
return args
if __name__ == '__main__':
args = _parse_args()
if args:
_check(**vars(args))
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 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);
}
}
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'
}
}
在 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"))
}
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 matched
return isMagicMatched(buffer);
} finally {
if (raf != null) {
raf.close();
}
}
}
为了提高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签名增强模式
}
}
}
使用APK注释保存渠道信息和MAGIC字节,从文件末尾读取渠道信息,速度飞快
实现为一个Gradle Plugin,支持定制输出APK的文件名等信息,方便CI集成
提供Java版和Python的独立命令行脚本,不依赖Gradle插件,支持独立使用
缺点
没有使用Android的productFlavors实现,无法利用flavors条件编译的功能
mcxiaoke:下一代Android渠道打包工具
mcxiaoke:github-packer-ng-plugin