Hi,各位老铁,欢迎走进自动化打包系列的最后一篇,上文已整体分析并构建了自动化打包项目,下面跟大家伙唠唠打包的实战,打包过程遇到的各位问题及处理方式。
在写之前,大家伙先来看看之前的打包过程图解:
通过这张图可以看到,整个打包的过程准确地来讲就是资源合并的过程,自动化打包踩坑记录篇也就是资源合并的坑点。
合并Assets目录及apktool.yml实战踩坑记
通常在合并游戏和渠道的assets目录的时候,先处理渠道的特殊配置,然后将渠道assets目录资源拷贝覆盖到游戏资源目录就可以了。但是这里会遇到两个坑点:
坑点一:assets目录的资源怎么缺失了。
以打YSDK渠道包为例,当你美滋滋的打出来的包体运行的时候。。。bangbangbangbang ,哈哈,点击YSDK渠道登录没反应。问题在哪里呢?不着急哈,首先你找一个可以正常运行的包体,反编译后跟你打出来的包体,你会发现assets目录缺少了一堆文件。
那这些文件都是哪里来的?最后你会发现在YSDK的.jar包里面,当打包将.jar转化为smail的时候只是将源码进行了转化,没有处理资源文件,导致资源的丢失。
所以在合并libs资源的时候需要将jar里面的非class资源抽出来处理,后面代码有说明
坑点二:unknown目录的生成
细心的朋友可能通过上面的截图就可以看到,jar文件里面除了assets目录下的资源以外还多了BeaconVersion.txt文件,而且这个文件在最开始的时候也没有打到游戏里面去,这个文件又要怎么处理呢?
其实,在回编译生成apk文件的时候,apktool工具会将非.apk文件打包放到unknown的目录下。这里可以参考下apktool源码分析。
Apktool源码解析——第一篇
Apktool源码解析——第二篇
public void decodeUnknownFiles(ExtFile apkFile, File outDir, ResTable resTable) throws AndrolibException {
LOGGER.info("Copying unknown files...");
File unknownOut = new File(outDir, UNK_DIRNAME);
ZipEntry invZipFile;
// have to use container of ZipFile to help identify compression type
// with regular looping of apkFile for easy copy
try {
Directory unk = apkFile.getDirectory();
ZipExtFile apkZipFile = new ZipExtFile(apkFile.getAbsolutePath());
// loop all items in container recursively, ignoring any that are pre-defined by aapt
Set<String> files = unk.getFiles(true);
for (String file : files) {//取出apk内所有文件名
if (!isAPKFileNames(file) && !file.endsWith(".dex")) {//不是常规文件也不是.dex文件
// copy file out of archive into special "unknown" folder
unk.copyToDir(unknownOut, file);//拷贝至unknown目录
try {
// ignore encryption
apkZipFile.getEntry(file).getGeneralPurposeBit().useEncryption(false);
invZipFile = apkZipFile.getEntry(file);
// lets record the name of the file, and its compression type
// so that we may re-include it the same way
if (invZipFile != null) {//这里把他们收集起来,如果需要回编译还可以原封不动的塞回去
mResUnknownFiles.addUnknownFileInfo(invZipFile.getName(), String.valueOf(invZipFile.getMethod()));
}
} catch (NullPointerException ignored) { }
}
}
apkZipFile.close();
} catch (DirectoryException | IOException ex) {
throw new AndrolibException(ex);
}
}
当你处理了前面两个坑点,将资源文件都拷贝到对应的目录的时候,你以为美滋滋的打包了, 你会发现你处理的unknown目录的资源在回编译的时候没有打到Apk里面。为什么?
大家请看这句:
// lets record the name of the file, and its compression type
// so that we may re-include it the same way
if (invZipFile != null) {//这里把他们收集起来,如果需要回编译还可以原封不动的塞回去
mResUnknownFiles.addUnknownFileInfo(invZipFile.getName(), String.valueOf(invZipFile.getMethod()));
}
apktool工具在反编译apk时,会把编译过程的资源信息写到apktool.yml文件中,然后在回编译的时候读取apktool.yml的配置信息找对应的资源文件
public void writeMetaFile(File mOutDir, Map<String, Object> meta)//键值对信息
throws AndrolibException {
DumperOptions options = new DumperOptions();
options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
Yaml yaml = new Yaml(options);
try (
Writer writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(
new File(mOutDir, "apktool.yml")), "UTF-8"));//输出目录
) {
yaml.dump(meta, writer);
} catch (IOException ex) {
throw new AndrolibException(ex);
}
}
因此,咱们在合并资源到unknown目录的时候,需要修改下apktool.yml信息
坑点三:!!brut.androlib.meta.MetaInfo
这个坑点,比较隐秘,是不同版本apktool工具发现的,是我在排查问题时遇到的,打包工具用的是2.3.3版本的,但是电脑配置环境变量apktool版本是2.0.1的版本,然后手动回编译时,老是报错:“could not determine a constructor for the tag 'tag:yaml.org,2002:brut.androlib.meta.MetaInfo'”。才发现高版本的的apktool工具在生成apktool.yml文件时,自动在第一行上添加了这个字段,再用版本回编译的时候报错。如果反编译和回编译用的版本都是一样的就没有这个问题。
坑点四:brut.androlib.AndrolibException: brut.common.BrutException: could not exec
这个问题排查了好久,最终发现是游戏包体资源放到assets目录太多,在回编译成apk包体时,无法编译。在apktool.yml处理下doNotCompress 字段下的字符串数字限制导致的(windows 命令行支持的字符串长度有限制,不超过8191个字符)。
针对这个问题处理,还需要兼容apktool这个工具的版本。
apktool 2.0.2下以下版本,没有做这个不压缩机制处理,可以正常回编译。
2.0.2到2.3.2版本新增不压缩的过滤规则
“jpg|jpeg|png|gif|wav|mp2|mp3|ogg|aac|mpg|mpeg|mid|midi|smf|jet|rtttl|imy|xmf|mp4|m4a|m4v|3gp|3gpp|3g2|3gpp2|amr|awb|wma|wmv|webm|mkv”
但是在2.3.3以上版本版本又将不过滤规则去掉了。
为兼容apktool版本,在回编译时修改设置过滤规则修改apktool.yml文件下的doNotCompress字段。
针对上述的坑点问题,可查看PackageApkTool/MergeLibUtils.py中的modify_jars具体实现
合并libs目录实战踩坑记
通常在合并游戏和渠道的libs目录的时候,这里需要处理.jar和.so两个类型的文件处理。
1、提取.jar文件里面非class资源,拷贝到对应的目录下
2、保证渠道提供的.so资源文件在游戏包体对应的armeabi/armeabi-v7a/x86等目录下都保持一致。不然会导致部分机型crash
3、需要额外处理微信相关功能的.java文件转化为对应的jar文件,方便在后续jar自动转化为smali代码统一处理
针对上述的坑点问题,可查看PackageApkTool/MergeResource.py中的merge_libs_resource具体实现
合并res目录实战踩坑记
通常在合并游戏和渠道的libs目录的时候,会直接将渠道的资源拷贝覆盖到游戏目录里面。
一般渠道的res目录资源都是有前缀来区分的,但是需要考虑的是,游戏资源和渠道资源同名文件的覆盖问题。
坑点一:res目录下-V4目录问题
渠道提供的资源目录结构与游戏反编译后的目录结构不一致问题。通常在新建安卓工程的时候,res目录的资源包大多是drawable、drawable-hdpi、drawable-xhdpi、values、values-hdpi等形式。但是反编译之后你就发现游戏的目录格式不一样:
如果游戏资源已经存在了资源文件,当拷贝渠道的资源后,回编译时,会提示资源重复,为了处理这个问题,在合并资源文件的时候,就需提前判断反编译后的目录结构。拷贝到对应的资源文件中。
坑点二:res目录下values目录下arrays.xml/colors.xml/demens.xml/strings.xml资源丢失问题:
拷贝完渠道res资源之后,渠道的资源同名资源会覆盖掉游戏反编译后原有的资源来保证资源是渠道的最新版本,但是需要注意的是values目录下arrays.xml/colors.xml/demens.xml/strings.xml的资源丢失问题,因为游戏apk包反编译后的资源信息都打包到这几个文件里面了,覆盖后,找不到对应的资源会导致各种问题,所以需要针对这几个资源文件做合并处理。
坑点三:res目录下values/values-hdpi等目录下values.xml、values-hdpi.xml等资源冲突问题:
个别渠道的res/values目录下提供的是values.xml、values-hdpi.xml的复合资源文件,将资源属性都定义到里面,values.xml准确来说就是arrays.xml/colors.xml/demens.xml/strings.xml等的结合体:
其实在处理arrays.xml/colors.xml/demens.xml/strings.xml资源的时候,并没有处理掉values.xml资源,后面在生成R文件的时候就会提示资源冲突了。需要先将values.xml转化为arrays.xml/colors.xml/demens.xml/strings.xml等文件。
坑点四:Error parsing XML: duplicate attribute 资源属性名称重复定义问题:
res目录下values/values-hdpi等目录下,渠道的资源属性和游戏的资源属性重复定义,比如,游戏已定义了app_name这个字段,但是渠道资源里面也定义了这个字段。这类的处理是将渠道定义的属性去掉。
针对上述的坑点问题,可查看PackageApkTool/MergeResUtils.py中的handle_res_dirs具体实现
合并Manifest文件实战踩坑记
通常在合并游戏和渠道的Manifest文件的时候,先修改渠道的差异化参数配置后,直接将对应的节点属性拷贝到游戏的Manifest文件上面就可以了。
1、这里需要额外处理游戏的启动入口和SDK的闪屏逻辑处理
2、个别渠道需要特殊处理游戏的主Activity问题
坑点一:apktool无法识别compileSdkVersion 、compileSdkVersionCodename
在实际操作过程中发现个别游戏包体反编译后,Manifest文件中有compileSdkVersion 、compileSdkVersionCodename这两个字段,即使下载了最新版本的apktool工具也无法识别。这个是因为游戏编译生成apk包时设置编译版本为compileSdkVersion = 28 才会生成的。这里需要额外处理下将compileSdkVersion 、compileSdkVersionCodename字段去掉。
但是在个别包体发现,单单去掉还不行,需将targetSdkVersion设置为23以上才不会异常。
针对上述的坑点问题,可查看PackageApkTool/MergeManifesUtils.py中的merger_manifest_config具体实现
生成R文件实战踩坑记
坑点一:aapt停止运行
其实在生成R文件时导致aapt停止运行的原因有很多,但是基本上一般是资源文件或是xml文件中有错误造成的。具体错误的原因需要慢慢找。
这个问题发现在反编译包体后,生成的资源文件有问题,发现在布局xml中都自动生成了n1这个字段。
最终定位的原因是模拟游戏母包的apk包是Android Studio3.0 Build Apks 生成包体,classpath 'com.android.tools.build:gradle:3.0.0' 生成的包体反编译后都会这样,可以通过将gradle版本设置:classpath 'com.android.tools.build:gradle:2.3.3' 就没有该问题出现
坑点二:第三方库通过R.xxx引用资源导致资源找不到问题。
这个问题很典型的案例就是个别第三方渠道引用了v7库,v7库里面的res资源是通过R.xxx引用的。在最终打出来的包体资源只会跟当前包名有关,为解决类似这种需要指定包名的问题,可以生成多个R文件的思路解决。
PS:但是这里有个雷点:生成的R文件除了包名不一致外,资源的ID都是一样的。不知道会不会有问题。
针对上述的坑点问题,可查看PackageApkTool/MergeRFileUtils.py中的create_r_files具体实现
渠道差异化处理实战踩坑记
坑点一:YSDK渠道ysdkconf.ini文件解析
文件不是标准的.ini的标准格式,需要额外处理,同时文件的字符集是utf-8-sig,读取后第一行老是多一个空格,用.ini格式读写时,老报错,需先处理下字符集。
针对上述的坑点问题,可查看PackageApkTool/YsdkChannel.py中的modify_assets_resource具体实现
坑点二:xml.etree import ElementTree as ET 解析xml文件出现 ns0:xxx错误
这个问题很好解决,在解析文件之前需将在parse前一定要设置namespace,类似Manifest需添加下面代码。
ET.register_namespace('android', "http://schemas.android.com/apk/res/android")
结语:
坑点记录的详细代码,可到开源项目打包工具下载:PackageApkTool
该工具后续持续完善,欢迎大家star
如果觉得我的文章对你有帮助,请随意赞赏。您的支持将鼓励我继续创作!