refactor(packages): 重构AAB解析逻辑并升级protobuf依赖

直接使用bundletool API解析AAB的Manifest和资源表,替代原有的dump命令解析方式,同时将protobuf-java版本从3.21.12升级至3.25.5以兼容新版API。
This commit is contained in:
2026-08-12 19:05:17 +08:00
parent 3db6c17511
commit 6539878294
2 changed files with 186 additions and 13 deletions

View File

@@ -1,9 +1,15 @@
package com.youlai.boot.app.packages.component;
import com.android.tools.build.bundletool.commands.DumpCommand;
import com.android.tools.build.bundletool.model.AndroidManifest;
import com.android.tools.build.bundletool.model.AppBundle;
import com.android.tools.build.bundletool.model.BundleModule;
import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoAttribute;
import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoElement;
import net.dongliu.apk.parser.ApkFile;
import net.dongliu.apk.parser.bean.ApkMeta;
import net.dongliu.apk.parser.bean.Icon;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.io.File;
@@ -12,13 +18,9 @@ import java.io.InputStream;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.*;
import java.util.function.Predicate;
import org.slf4j.Logger;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
@@ -38,6 +40,8 @@ import java.util.zip.ZipInputStream;
* </ul>
*/
public class ApkMetaParser {
private static final String ANDROID_NS = "http://schemas.android.com/apk/res/android";
private Logger logger = LoggerFactory.getLogger(ApkMetaParser.class);
/**
* 统一解析入口:根据文件扩展名分发到对应解析策略。
@@ -83,12 +87,179 @@ public class ApkMetaParser {
* base 模块的 AndroidManifest.xml 文本,再提取关键字段。
*/
public static ApkMetaResult parseAab(File aabFile) throws IOException {
ApkMetaResult result = parseByBundleTool(aabFile);
// 从 AAB 的 base 模块资源目录res/mipmap-*、res/drawable-*)提取应用图标
fillBundleIcon(result, aabFile, true);
return result;
ApkMetaResult apkMetaResult = new ApkMetaResult();
// 使用 java.util.zip.ZipFile 加载 AAB 文件构建 AppBundle 对象
try (ZipFile zipFile = new ZipFile(aabFile)) {
AppBundle appBundle = AppBundle.buildFromZip(zipFile);
// 1. 获取 Base Module (基础模块)
BundleModule baseModule = appBundle.getBaseModule();
// 2. 获取 Manifest 信息
AndroidManifest manifest = baseModule.getAndroidManifest();
String packageName = manifest.getPackageName();
apkMetaResult.setPackageName(packageName);
int versionCode = manifest.getVersionCode().orElse(-1);
apkMetaResult.setVersionCode(versionCode);
String versionName = manifest.getVersionName().orElse("");
apkMetaResult.setVersionName(versionName);
apkMetaResult.setMinSdk(manifest.getMinSdkVersion().orElse(-1));
apkMetaResult.setTargetSdk(manifest.getTargetSdkVersion().orElse(-1));
// 3. 获取 <manifest> 根节点
XmlProtoElement manifestElement = manifest.getManifestElement();
// 3.1 解析 compileSdkVersion位于 <uses-sdk android:compileSdkVersion=".."/>
// XmlProtoElement usesSdkElement = manifestElement.getChildElement("uses-sdk");
// if (usesSdkElement != null) {
manifestElement.getAttribute(ANDROID_NS, "compileSdkVersion")
.map(XmlProtoAttribute::getValueAsInteger)
.ifPresent(apkMetaResult::setCompileSdk);
// }
// 3. 获取 <application> 节点
XmlProtoElement appElement = manifestElement.getChildElement("application");
// 4. 解析应用名称 (android:label)
Optional<XmlProtoAttribute> labelOptional = appElement.getAttribute(ANDROID_NS, "label");
if (labelOptional.isPresent()) {
XmlProtoAttribute labelAttr = labelOptional.get();
// 若 android:label 是资源引用 (@string/app_name),则从资源表解析字符串;
// 否则直接取 manifest 中写死的文本值
if (labelAttr.hasRefIdValue()) {
int resId = labelAttr.getValueAsRefId();
String appName = resolveStringResource(baseModule, resId);
if (appName != null && !appName.isBlank()) {
apkMetaResult.setAppLabel(appName);
}
} else {
apkMetaResult.setAppLabel(labelAttr.getValueAsString());
}
}
// 5. 解析应用图标:从 <application android:icon="@mipmap/.."> 资源引用解析
Optional<XmlProtoAttribute> iconOptional = appElement.getAttribute(ANDROID_NS, "icon");
if (iconOptional.isPresent()) {
XmlProtoAttribute iconAttr = iconOptional.get();
if (iconAttr.hasRefIdValue()) {
// 资源引用,如 @mipmap/ic_launcher先从资源表解析出资源文件路径
int iconResId = iconAttr.getValueAsRefId();
String iconPath = resolveFileResource(baseModule, iconResId);
if (iconPath != null) {
// 资源表返回的路径以 res/ 开头AAB 内实际位于 base/ 前缀下
String entryName = "base/" + iconPath;
ZipEntry iconEntry = zipFile.getEntry(entryName);
if (iconEntry != null && !iconEntry.isDirectory()) {
try (InputStream in = zipFile.getInputStream(iconEntry)) {
byte[] data = in.readAllBytes();
if (data != null && data.length > 0) {
apkMetaResult.setIconPath(iconPath);
apkMetaResult.setIconBytes(data);
}
}
}
}
}
}
String appName = apkMetaResult.getAppLabel();
}
return apkMetaResult;
}
/**
* 从 AAB base 模块的资源表中,按资源 ID 解析字符串资源(如 @string/app_name
* <p>
* 遍历 AAPT2 proto 资源表的 package/type/entry 三层结构,用资源 ID 的高 8 位
* (packageId)、次 8 位 (typeId)、低 16 位 (entryId) 定位到目标条目,取其第一个
* STR 类型的配置值。
*/
private static String resolveStringResource(BundleModule baseModule, int resId) {
try {
Optional<com.android.aapt.Resources.ResourceTable> tableOpt =
baseModule.getResourceTable();
if (!tableOpt.isPresent()) {
return null;
}
com.android.aapt.Resources.ResourceTable table = tableOpt.get();
int packageId = (resId >>> 24) & 0xFF;
int typeId = (resId >>> 16) & 0xFF;
int entryId = resId & 0xFFFF;
for (com.android.aapt.Resources.Package pkg : table.getPackageList()) {
if (pkg.getPackageId().getId() != packageId) {
continue;
}
for (com.android.aapt.Resources.Type type : pkg.getTypeList()) {
if (type.getTypeId().getId() != typeId) {
continue;
}
for (com.android.aapt.Resources.Entry entry : type.getEntryList()) {
if (entry.getEntryId().getId() != entryId) {
continue;
}
for (com.android.aapt.Resources.ConfigValue cv : entry.getConfigValueList()) {
com.android.aapt.Resources.Item item = cv.getValue().getItem();
if (item != null && item.getValueCase() == com.android.aapt.Resources.Item.ValueCase.STR) {
return item.getStr().getValue();
}
}
}
}
}
} catch (Exception ignored) {
// 资源解析失败不影响其它字段
}
return null;
}
/**
* 从 AAB base 模块的资源表中,按资源 ID 解析文件资源路径(如 @mipmap/ic_launcher
* <p>
* 遍历 AAPT2 proto 资源表定位到目标条目后,取其第一个 FILE 类型的配置值,返回
* FileReference 的 path形如 res/mipmap-xxxhdpi/ic_launcher.png
*/
private static String resolveFileResource(BundleModule baseModule, int resId) {
try {
Optional<com.android.aapt.Resources.ResourceTable> tableOpt =
baseModule.getResourceTable();
if (!tableOpt.isPresent()) {
return null;
}
com.android.aapt.Resources.ResourceTable table = tableOpt.get();
int packageId = (resId >>> 24) & 0xFF;
int typeId = (resId >>> 16) & 0xFF;
int entryId = resId & 0xFFFF;
for (com.android.aapt.Resources.Package pkg : table.getPackageList()) {
if (pkg.getPackageId().getId() != packageId) {
continue;
}
for (com.android.aapt.Resources.Type type : pkg.getTypeList()) {
if (type.getTypeId().getId() != typeId) {
continue;
}
for (com.android.aapt.Resources.Entry entry : type.getEntryList()) {
if (entry.getEntryId().getId() != entryId) {
continue;
}
for (com.android.aapt.Resources.ConfigValue cv : entry.getConfigValueList()) {
com.android.aapt.Resources.Item item = cv.getValue().getItem();
if (item != null && item.getValueCase() == com.android.aapt.Resources.Item.ValueCase.FILE) {
return item.getFile().getPath();
}
}
}
}
}
} catch (Exception ignored) {
// 资源解析失败不影响其它字段
}
return null;
}
// ============================ APKS ============================
/**
@@ -395,11 +566,13 @@ public class ApkMetaParser {
return (isMipmap ? 1000 : 0) + density;
}
private record IconCandidate(String name, int rank, byte[] data) {}
private record IconCandidate(String name, int rank, byte[] data) {
}
// ============================ 通用工具 ============================
private record ContainerEntry(String name, byte[] data) {}
private record ContainerEntry(String name, byte[] data) {
}
/**
* 从字节数组解析 APK用于容器内的主 APK