From 6539878294b925c780a4e046a50546c07a67ca96 Mon Sep 17 00:00:00 2001 From: tongtongstudio Date: Wed, 12 Aug 2026 19:05:17 +0800 Subject: [PATCH] =?UTF-8?q?refactor(packages):=20=E9=87=8D=E6=9E=84AAB?= =?UTF-8?q?=E8=A7=A3=E6=9E=90=E9=80=BB=E8=BE=91=E5=B9=B6=E5=8D=87=E7=BA=A7?= =?UTF-8?q?protobuf=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 直接使用bundletool API解析AAB的Manifest和资源表,替代原有的dump命令解析方式,同时将protobuf-java版本从3.21.12升级至3.25.5以兼容新版API。 --- pom.xml | 2 +- .../app/packages/component/ApkMetaParser.java | 197 ++++++++++++++++-- 2 files changed, 186 insertions(+), 13 deletions(-) diff --git a/pom.xml b/pom.xml index 33e9097b..62598c33 100644 --- a/pom.xml +++ b/pom.xml @@ -166,7 +166,7 @@ com.google.protobuf protobuf-java - 3.21.12 + 3.25.5 diff --git a/src/main/java/com/youlai/boot/app/packages/component/ApkMetaParser.java b/src/main/java/com/youlai/boot/app/packages/component/ApkMetaParser.java index aae82cca..b5dfd1c3 100644 --- a/src/main/java/com/youlai/boot/app/packages/component/ApkMetaParser.java +++ b/src/main/java/com/youlai/boot/app/packages/component/ApkMetaParser.java @@ -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; * */ 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. 获取 根节点 + XmlProtoElement manifestElement = manifest.getManifestElement(); + + // 3.1 解析 compileSdkVersion(位于 ) +// XmlProtoElement usesSdkElement = manifestElement.getChildElement("uses-sdk"); +// if (usesSdkElement != null) { + manifestElement.getAttribute(ANDROID_NS, "compileSdkVersion") + .map(XmlProtoAttribute::getValueAsInteger) + .ifPresent(apkMetaResult::setCompileSdk); +// } + + // 3. 获取 节点 + XmlProtoElement appElement = manifestElement.getChildElement("application"); + + // 4. 解析应用名称 (android:label) + + Optional 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. 解析应用图标:从 资源引用解析 + Optional 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)。 + *

+ * 遍历 AAPT2 proto 资源表的 package/type/entry 三层结构,用资源 ID 的高 8 位 + * (packageId)、次 8 位 (typeId)、低 16 位 (entryId) 定位到目标条目,取其第一个 + * STR 类型的配置值。 + */ + private static String resolveStringResource(BundleModule baseModule, int resId) { + try { + Optional 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)。 + *

+ * 遍历 AAPT2 proto 资源表定位到目标条目后,取其第一个 FILE 类型的配置值,返回 + * FileReference 的 path(形如 res/mipmap-xxxhdpi/ic_launcher.png)。 + */ + private static String resolveFileResource(BundleModule baseModule, int resId) { + try { + Optional 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)。