From 4a38c1c1a475f8feb0cffdb855b4aefef7aa80b4 Mon Sep 17 00:00:00 2001 From: TongTongStudio Date: Sat, 15 Aug 2026 13:12:13 +0800 Subject: [PATCH] =?UTF-8?q?refactor(packages):=20=E9=87=8D=E6=9E=84=20APKS?= =?UTF-8?q?=20=E8=A7=A3=E6=9E=90=E9=80=BB=E8=BE=91=E5=B9=B6=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E5=9B=BE=E6=A0=87=E9=80=89=E6=8B=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除 parseApks 的 @Deprecated 标记,改为直接提取 Base/Universal APK 解析 - 使用 BinaryXmlParser 解码二进制 AndroidManifest.xml - 图标选择改为取 density 最大的候选图标 - 调整依赖版本并更新测试用例 --- pom.xml | 14 +- .../app/packages/component/ApkMetaParser.java | 142 +++++++++++++++++- .../packages/component/ApkMetaParserTest.java | 7 +- 3 files changed, 146 insertions(+), 17 deletions(-) diff --git a/pom.xml b/pom.xml index 62598c33..c56f38d9 100644 --- a/pom.xml +++ b/pom.xml @@ -151,12 +151,6 @@ 1.18.2 - - com.android.tools - sdk-common - 32.3.1 - - com.android.tools.build bundletool @@ -175,6 +169,12 @@ 9.3.1-15703166 + + com.android.tools + sdk-common + 32.3.1 + + com.android.tools.apkparser apkanalyzer @@ -184,7 +184,7 @@ com.android.tools.apkparser binary-resources - 31.2.0 + 32.3.1 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 64511263..a3c9dc91 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,6 +1,8 @@ package com.youlai.boot.app.packages.component; import com.android.aapt.Resources; +import com.android.bundle.Commands; +import com.android.tools.apk.analyzer.BinaryXmlParser; import com.android.tools.build.bundletool.commands.DumpCommand; import com.android.tools.build.bundletool.model.AndroidManifest; import com.android.tools.build.bundletool.model.AppBundle; @@ -23,6 +25,8 @@ import org.zeroturnaround.zip.ZipUtil; import java.io.*; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import java.util.*; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -96,6 +100,9 @@ public class ApkMetaParser { *

* 直接使用 bundletool 依赖(非命令行、非 jar)的 {@link DumpCommand} 输出 * base 模块的 AndroidManifest.xml 文本,再提取关键字段。 + *

+ * splits + * toc.pb */ public static ApkMetaResult parseAab(File aabFile) throws IOException { ApkMetaResult apkMetaResult = new ApkMetaResult(); @@ -118,7 +125,7 @@ public class ApkMetaParser { String packageName = manifest.getPackageName(); apkMetaResult.setPackageName(packageName); int versionCode = manifest.getVersionCode().orElse(-1); - apkMetaResult.setVersionCode(Long.valueOf(versionCode)); + apkMetaResult.setVersionCode((long) versionCode); String versionName = manifest.getVersionName().orElse(""); apkMetaResult.setVersionName(versionName); apkMetaResult.setMinSdk(manifest.getMinSdkVersion().orElse(-1)); @@ -317,13 +324,128 @@ public class ApkMetaParser { * bundletool 依赖可直接解析 .apks 整包并 dump 其 Manifest;同时枚举 ZIP 内 * 的 apk 条目记录 split 列表。 */ - @Deprecated public static ApkMetaResult parseApks(File apksFile) throws IOException { - ApkMetaResult result = parseByBundleTool(apksFile); - result.setSplitApkList(collectApkEntries(apksFile)); - // 图标位于内部各 split apk 的 res/mipmap-* 或 res/drawable-*,逐包扫描 - fillBundleIcon(result, apksFile, false); - return result; + List baseApkSuffixList = new ArrayList<>(Arrays.asList("base-master.apk", "universal.apk")); + + ApkMetaResult apkMetaResult = new ApkMetaResult(); + + try (ZipFile apksZip = new ZipFile(apksFile)) { + ZipEntry tocEntry = apksZip.getEntry("toc.pb"); + if (tocEntry == null) { + LOGGER.warn("非标准的 .apks 文件,缺失 toc.pb"); + baseApkSuffixList.add("base.apk"); + } else { +// try (InputStream tocStream = apksZip.getInputStream(tocEntry)) { +// Commands.BuildApksResult apksResult = Commands.BuildApksResult.parseFrom(tocStream); +// // System.out.println("=== 基础信息 (来自 toc.pb) ===" ); +// // System.out.println("Package Name: " + apksResult.getPackageName()); +// // +// for (Commands.Variant variant : apksResult.getVariantList()) { +// System.out.println("Variant Number: " + variant.getVariantNumber()); +// for (Commands.ApkSet apkSet : variant.getApkSetList()) { +// for (Commands.ApkDescription apkDesc : apkSet.getApkDescriptionList()) { +// System.out.println(" - 包含 APK: " + apkDesc.getPath()); +// } +// } +// } +// } + } + + // 补全 split 列表:读取 .apks 容器内所有 *.apk 条目 + List splitApkList = apksZip.stream() + .map(ZipEntry::getName) + .filter(name -> name.toLowerCase(Locale.ROOT).endsWith(".apk")) + .collect(Collectors.toList()); + apkMetaResult.setSplitApkList(splitApkList); + + // ========================================== + // 步骤 1:定位并提取 .apks 中的 Base APK 或 Universal APK + // ========================================== + ZipEntry baseApkEntry = apksZip.stream() + .filter(e -> baseApkSuffixList.stream().anyMatch(e.getName()::endsWith)) + .findFirst() + .orElseThrow(() -> new RuntimeException("在 .apks 中未找到有效的 Base APK 或 Universal APK")); + + Path tempBaseApkPath = Files.createTempFile("optimized_base_", ".apk"); + + try { + try (InputStream is = apksZip.getInputStream(baseApkEntry)) { + Files.copy(is, tempBaseApkPath, StandardCopyOption.REPLACE_EXISTING); + } + + try (ApkFile apkFileParser = new ApkFile(tempBaseApkPath.toFile())) { + apkFileParser.setPreferredLocale(Locale.SIMPLIFIED_CHINESE); + ApkMeta meta = apkFileParser.getApkMeta(); + + fillFromApkMeta(apkMetaResult, meta); + apkMetaResult.setAbiList(extractAbiList(tempBaseApkPath.toFile())); + fillIcon(apkMetaResult, apkFileParser); + } + + // ========================================== + // 步骤 2:读取 Base APK 内部的二进制 AndroidManifest.xml + // ========================================== + try (ZipFile baseApkZip = new ZipFile(tempBaseApkPath.toFile())) { + ZipEntry manifestEntry = baseApkZip.getEntry("AndroidManifest.xml"); + if (manifestEntry == null) { + throw new RuntimeException("Base APK 中缺失 AndroidManifest.xml 文件"); + } + + byte[] binaryManifestBytes; + try (InputStream manifestIs = baseApkZip.getInputStream(manifestEntry); + ByteArrayOutputStream buffer = new ByteArrayOutputStream()) { + byte[] data = new byte[4096]; + int nRead; + while ((nRead = manifestIs.read(data, 0, data.length)) != -1) { + buffer.write(data, 0, nRead); + } + binaryManifestBytes = buffer.toByteArray(); + } + + // ========================================== + // 步骤 3:使用 apkanalyzer 的 BinaryXmlParser 解码二进制 XML + // ========================================== + byte[] decodedXmlBytes = BinaryXmlParser.decodeXml(binaryManifestBytes); + String manifestXml = new String(decodedXmlBytes, StandardCharsets.UTF_8); + + // ========================================== + // 步骤 4:提取包名与版本详情 + // ========================================== + String label = extractXmlAttribute(manifestXml, "android:label"); + String packageName = extractXmlAttribute(manifestXml, "package"); + String versionName = extractXmlAttribute(manifestXml, "android:versionName"); +// long versionCode = extractXmlAttribute(manifestXml, "android:versionCode"); +// int compileSdkVersion = extractXmlAttribute(manifestXml, "android:compileSdkVersion"); +// int minSdkVersion = extractXmlAttribute(manifestXml, "android:minSdkVersion"); +// int targetSdkVersion = extractXmlAttribute(manifestXml, "android:targetSdkVersion"); + +// apkMetaResult.setAppLabel(label); +// apkMetaResult.setPackageName(packageName); +// apkMetaResult.setVersionName(versionName); + + } + + } finally { + // 安全清理临时生成的 APK 文件 + Files.deleteIfExists(tempBaseApkPath); + } + } catch (Exception e) { + LOGGER.error("解析 APKS 失败", e); + } + return apkMetaResult; + } + + + /** + * 辅助方法:从 XML 字符串中极简提取属性(仅作演示,实战可用 JDK org.w3c.dom 库) + */ + private static String extractXmlAttribute(String xml, String attributeName) { + String regex = attributeName + "=\"([^\"]+)\""; + java.util.regex.Matcher matcher = java.util.regex.Pattern.compile(regex).matcher(xml); + if (matcher.find()) { + return matcher.group(1); + } + return ""; } // ============================ APKM ============================ @@ -558,6 +680,7 @@ public class ApkMetaParser { @Deprecated private static ApkMetaResult parseSapk(File xapkFile) { ApkMetaResult apkMetaResult = new ApkMetaResult(); + // todo 暂未实现,不知道哪里有这种格式 return apkMetaResult; } @@ -822,7 +945,10 @@ public class ApkMetaParser { try { List icons = apkFileParser.getIconFiles(); if (icons != null && !icons.isEmpty()) { - Icon icon = icons.get(0); + // 从所有候选图标中选取 density 最大(分辨率最高)的一个 + Icon icon = icons.stream() + .max(Comparator.comparingInt(Icon::getDensity)) + .orElse(icons.get(0)); result.setIconPath(icon.getPath()); byte[] data = icon.getData(); if (data != null && data.length > 0) { diff --git a/src/test/java/com/youlai/boot/app/packages/component/ApkMetaParserTest.java b/src/test/java/com/youlai/boot/app/packages/component/ApkMetaParserTest.java index 62c1b667..d7bad0cb 100644 --- a/src/test/java/com/youlai/boot/app/packages/component/ApkMetaParserTest.java +++ b/src/test/java/com/youlai/boot/app/packages/component/ApkMetaParserTest.java @@ -33,6 +33,8 @@ class ApkMetaParserTest { "D:\\apkTest\\XwadOSHonor_V1.4.7.aab", "D:\\apkTest\\XwadOSHonor_V1.4.7.apks", "D:\\apkTest\\Twitter_v8.95.0-release.00_apkpure.com.xapk", + "D:\\apkTest\\MIR4_0.488243_APKPure.xapk", + "D:\\apkTest\\GitHub_1.271.1_APKPure.xapk", "D:\\apkTest\\com.instagram.android_440.1.0.46.86-384611414_1dpi_c0d8039a2b3d4f69b31e6cbb1c611c5a_apkmirror.com.apkm", "D:\\apkTest\\抖音V39.1.2谷歌版.apks" ); @@ -70,10 +72,11 @@ class ApkMetaParserTest { int total = paths.size(); int success = 0; - for (String path : paths) { + for (int i = 0; i < paths.size(); i++) { + String path = paths.get(i); File apkFile = new File(path); System.out.println(); - System.out.println(">>>>>> [" + (success + 1) + "/" + total + "] " + apkFile.getAbsolutePath()); + System.out.println(">>>>>> [" + (i + 1) + "/" + total + "] " + apkFile.getAbsolutePath()); if (!apkFile.exists() || !apkFile.isFile()) { System.out.println("!! 跳过:文件不存在或不是普通文件");