该篇文章基于 apktool 源码版本 2.6.1
1. apktool.yml 是什么
是 apktool 这个开源工具反编译 apk 生成的一个 配置文件,用来记录这个 apk 的基础信息
源码对应的bean文件是 brut.androlib.meta.MetaInfo.java;
源码里对应的 apktool.yml的生成逻辑是在
brut.androlib.ApkDecoder.java ,ApkDecoder 中的 writeMetaFile 逻辑我复制在了下面,文章会逐个对每个 key的生成原理 去分析
private void writeMetaFile() throws AndrolibException {
MetaInfo meta = new MetaInfo();
meta.version = Androlib.getVersion();
meta.apkFileName = mApkFile.getName();
if (mResTable != null) {
meta.isFrameworkApk = mAndrolib.isFrameworkApk(mResTable);
putUsesFramework(meta);
putSdkInfo(meta);
putPackageInfo(meta);
putVersionInfo(meta);
putSharedLibraryInfo(meta);
putSparseResourcesInfo(meta);
} else {
putMinSdkInfo(meta);
}
putUnknownInfo(meta);
putFileCompressionInfo(meta);
mAndrolib.writeMetaFile(mOutDir, meta);
}
2. apktool.yml 节点分析
!!brut.androlib.meta.MetaInfo
apkFileName: filename.apk
compressionType: false
doNotCompress:
- arsc
- png
isFrameworkApk: false
packageInfo:
forcedPackageId: '127'
renameManifestPackage: null
sdkInfo:
minSdkVersion: '19'
targetSdkVersion: '26'
sharedLibrary: false
sparseResources: false
unknownFiles:
sentry-build.properties: '0'
usesFramework:
ids:
- 1
tag: null
version: 2.4.1-2264e6-SNAPSHOT
versionInfo:
versionCode: '20220314'
versionName: 1.14.2
1. apkFileName
反编译的文件名
生成逻辑对应 第一段落源码中的
meta.apkFileName = mApkFile.getName();
具体逻辑不用分析,比较简单,就是反编译 目标apk 的文件名
2. isFrameworkApk
判断是否是反编译 apktool 工具 的 framework apk
public boolean isFrameworkApk(ResTable resTable) {
for (ResPackage pkg : resTable.listMainPackages()) {
if (pkg.getId() < 64) {
return true;
}
}
return false;
}
入参 ResTable 是 通过 getResTable生成的
public ResTable getResTable() throws AndrolibException {
if (mResTable == null) {
boolean hasResources = hasResources();
boolean hasManifest = hasManifest();
if (! (hasManifest || hasResources)) {
throw new AndrolibException(
"Apk doesn't contain either AndroidManifest.xml file or resources.arsc file");
}
mResTable = mAndrolib.getResTable(mApkFile, hasResources);
mResTable.setAnalysisMode(mAnalysisMode);
}
return mResTable;
}
一路往下走 在 brut.androlib.res.AndrolibResources 调用 getResTable , new 了一个 restable 然后返回回去, 这个 restable 就是 apk 读取 AndroidManifest.xml 和 res 内容后的 一个实体类
public ResTable getResTable(ExtFile apkFile, boolean loadMainPkg)
throws AndrolibException {
ResTable resTable = new ResTable(this);
if (loadMainPkg) {
loadMainPkg(resTable, apkFile);
}
return resTable;
}
在 loadMainPkg 函数中传给 让 resTable 读取到对应的属性,在 brut.androlib.res.decoder.ARSCDecoder
给 resTable 赋予读取 apk 信息后的属性,在 readTablePackage 中给 mResId 这个属性赋值是 id 的
mResId = id << 24; 代码如下
private ResPackage readTablePackage() throws IOException, AndrolibException {
checkChunkType(Header.TYPE_PACKAGE);
int id = mIn.readInt();
if (id == 0) {
// This means we are dealing with a Library Package, we should just temporarily
// set the packageId to the next available id . This will be set at runtime regardless, but
// for Apktool's use we need a non-zero packageId.
// AOSP indicates 0x02 is next, as 0x01 is system and 0x7F is private.
id = 2;
if (mResTable.getPackageOriginal() == null && mResTable.getPackageRenamed() == null) {
mResTable.setSharedLibrary(true);
}
}
String name = mIn.readNullEndedString(128, true);
/* typeStrings */mIn.skipInt();
/* lastPublicType */mIn.skipInt();
/* keyStrings */mIn.skipInt();
/* lastPublicKey */mIn.skipInt();
// TypeIdOffset was added platform_frameworks_base/@f90f2f8dc36e7243b85e0b6a7fd5a590893c827e
// which is only in split/new applications.
int splitHeaderSize = (2 + 2 + 4 + 4 + (2 * 128) + (4 * 5)); // short, short, int, int, char[128], int * 4
if (mHeader.headerSize == splitHeaderSize) {
mTypeIdOffset = mIn.readInt();
}
if (mTypeIdOffset > 0) {
LOGGER.warning("Please report this application to Apktool for a fix: https://github.com/iBotPeaches/Apktool/issues/1728");
}
mTypeNames = StringBlock.read(mIn);
mSpecNames = StringBlock.read(mIn);
mResId = id << 24;
mPkg = new ResPackage(mResTable, id, name);
nextChunk();
boolean flag = true;
while (flag) {
switch (mHeader.type) {
case Header.TYPE_LIBRARY:
readLibraryType();
break;
case Header.TYPE_SPEC_TYPE:
readTableTypeSpec();
break;
default:
flag = false;
break;
}
}
return mPkg;
}
3. usesFramework
不太清楚这个标记是用来做什么,会把 包的 id 按顺序输出出来,然后读取 buildOptions 里面的 frameworkTag
private void putUsesFramework(MetaInfo meta) {
Set<ResPackage> pkgs = mResTable.listFramePackages();
if (pkgs.isEmpty()) {
return;
}
Integer[] ids = new Integer[pkgs.size()];
int i = 0;
for (ResPackage pkg : pkgs) {
ids[i++] = pkg.getId();
}
Arrays.sort(ids);
meta.usesFramework = new UsesFramework();
meta.usesFramework.ids = Arrays.asList(ids);
if (mAndrolib.buildOptions.frameworkTag != null) {
meta.usesFramework.tag = mAndrolib.buildOptions.frameworkTag;
}
}
4. sdkInfo
应用的 sdk 版本信息
private void putSdkInfo(MetaInfo meta) {
Map<String, String> info = mResTable.getSdkInfo();
if (info.size() > 0) {
String refValue;
if (info.get("minSdkVersion") != null) {
refValue = ResXmlPatcher.pullValueFromIntegers(mOutDir, info.get("minSdkVersion"));
if (refValue != null) {
info.put("minSdkVersion", refValue);
}
}
if (info.get("targetSdkVersion") != null) {
refValue = ResXmlPatcher.pullValueFromIntegers(mOutDir, info.get("targetSdkVersion"));
if (refValue != null) {
info.put("targetSdkVersion", refValue);
}
}
if (info.get("maxSdkVersion") != null) {
refValue = ResXmlPatcher.pullValueFromIntegers(mOutDir, info.get("maxSdkVersion"));
if (refValue != null) {
info.put("maxSdkVersion", refValue);
}
}
meta.sdkInfo = info;
}
}
靠工具类 brut.androlib.res.xml.ResXmlPatcher 来完成 对 sdk 版本的读取,只不过读取路径是在
/res/values/integers.xml ,有点费解,因为实际看文件内容是没有 sdkverison 相关的内容,后续还需要单步调试看看获取原理
/**
* Finds key in integers.xml file and returns text value
*
* @param directory Root directory of apk
* @param key Integer reference (ie @integer/foo)
* @return String|null
*/
public static String pullValueFromIntegers(File directory, String key) {
if (key == null || ! key.contains("@")) {
return null;
}
File file = new File(directory, "/res/values/integers.xml");
key = key.replace("@integer/", "");
if (file.exists()) {
try {
Document doc = loadDocument(file);
XPath xPath = XPathFactory.newInstance().newXPath();
XPathExpression expression = xPath.compile("/resources/integer[@name=" + '"' + key + "\"]/text()");
Object result = expression.evaluate(doc, XPathConstants.STRING);
if (result != null) {
return (String) result;
}
} catch (SAXException | ParserConfigurationException | IOException | XPathExpressionException ignored) {
}
}
return null;
}
5. unknownFiles
下面的代码是 apktool 对unknow 文件的识别逻辑,读取 zip 解压后的所有不在 APK_STANDARD_ALL_FILENAMES 文件列表里的文件 ,并且也不是dex 文件,那么就归属于 unknow 文件,但是 unknow文件 可不是 可有可无的,只是 apktool 对这些文件不知道如何处理,但是如果你删掉了,那么回编译代码肯定会有问题
public void decodeUnknownFiles(ExtFile apkFile, File outDir)
throws AndrolibException {
LOGGER.info("Copying unknown files...");
File unknownOut = new File(outDir, UNK_DIRNAME);
try {
Directory unk = apkFile.getDirectory();
// loop all items in container recursively, ignoring any that are pre-defined by aapt
Set<String> files = unk.getFiles(true);
for (String file : files) {
if (!isAPKFileNames(file) && !file.endsWith(".dex")) {
// copy file out of archive into special "unknown" folder
unk.copyToDir(unknownOut, file);
// lets record the name of the file, and its compression type
// so that we may re-include it the same way
mResUnknownFiles.addUnknownFileInfo(file, String.valueOf(unk.getCompressionLevel(file)));
}
}
} catch (DirectoryException ex) {
throw new AndrolibException(ex);
}
} public void decodeUnknownFiles(ExtFile apkFile, File outDir)
throws AndrolibException {
LOGGER.info("Copying unknown files...");
File unknownOut = new File(outDir, UNK_DIRNAME);
try {
Directory unk = apkFile.getDirectory();
// loop all items in container recursively, ignoring any that are pre-defined by aapt
Set<String> files = unk.getFiles(true);
for (String file : files) {
if (!isAPKFileNames(file) && !file.endsWith(".dex")) {
// copy file out of archive into special "unknown" folder
unk.copyToDir(unknownOut, file);
// lets record the name of the file, and its compression type
// so that we may re-include it the same way
mResUnknownFiles.addUnknownFileInfo(file, String.valueOf(unk.getCompressionLevel(file)));
}
}
} catch (DirectoryException ex) {
throw new AndrolibException(ex);
}
}
private final static String[] APK_STANDARD_ALL_FILENAMES = new String[] {
"classes.dex", "AndroidManifest.xml", "resources.arsc", "res", "r", "R",
"lib", "libs", "assets", "META-INF", "kotlin" };
6. doNotCompress
apktool 源码中对这个节点的定义是列出 不需要压缩的文件
下面是 apktool 对 必须要压缩的文件的定义
private final static Pattern NO_COMPRESS_PATTERN = Pattern.compile("(" +
"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|webp|mkv)$");
整体判断文件是否不需要压缩,就是读取文件后缀,不匹配必须压缩的文件后缀,那就加入不压缩的行列
public void recordUncompressedFiles(ExtFile apkFile, Collection<String> uncompressedFilesOrExts) throws AndrolibException {
try {
Directory unk = apkFile.getDirectory();
Set<String> files = unk.getFiles(true);
for (String file : files) {
if (isAPKFileNames(file) && unk.getCompressionLevel(file) == 0) {
String ext = "";
if (unk.getSize(file) != 0) {
ext = FilenameUtils.getExtension(file);
}
if (ext.isEmpty() || !NO_COMPRESS_PATTERN.matcher(ext).find()) {
ext = file;
}
if (!uncompressedFilesOrExts.contains(ext)) {
uncompressedFilesOrExts.add(ext);
}
}
}
} catch (DirectoryException ex) {
throw new AndrolibException(ex);
}
}
整个 apktool 反编译生成的 apktool.yml 的逻辑我已经讲了比较重要的几个 key ,还有 诸如 version
verisonInfo ,这些就可以从我开头写的源码入口自己去看看对应的生成逻辑,相信会受益匪浅