Android多渠道打包(五):360多渠道打包+

本章将介绍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脚本的方式

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"))
}

着重几点

  • 改善了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 matched
                return isMagicMatched(buffer);
            } finally {
                if (raf != null) {
                    raf.close();
                }
            }
        }
  • 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签名增强模式
      }
    }
  }

优缺点

使用APK注释保存渠道信息和MAGIC字节,从文件末尾读取渠道信息,速度飞快

实现为一个Gradle Plugin,支持定制输出APK的文件名等信息,方便CI集成

提供Java版和Python的独立命令行脚本,不依赖Gradle插件,支持独立使用
缺点

没有使用Android的productFlavors实现,无法利用flavors条件编译的功能

引用

mcxiaoke:下一代Android渠道打包工具

mcxiaoke:github-packer-ng-plugin


你可能感兴趣的:(Android多渠道打包,Android多渠道打包)