refactor(packages): 重构 APKS 解析逻辑并优化图标选择

- 移除 parseApks 的 @Deprecated 标记,改为直接提取 Base/Universal APK 解析
- 使用 BinaryXmlParser 解码二进制 AndroidManifest.xml
- 图标选择改为取 density 最大的候选图标
- 调整依赖版本并更新测试用例
This commit is contained in:
2026-08-15 13:12:13 +08:00
parent a3a34326e2
commit 4a38c1c1a4
3 changed files with 146 additions and 17 deletions

14
pom.xml
View File

@@ -151,12 +151,6 @@
<version>1.18.2</version>
</dependency>
<dependency>
<groupId>com.android.tools</groupId>
<artifactId>sdk-common</artifactId>
<version>32.3.1</version>
</dependency>
<dependency>
<groupId>com.android.tools.build</groupId>
<artifactId>bundletool</artifactId>
@@ -175,6 +169,12 @@
<version>9.3.1-15703166</version>
</dependency>
<dependency>
<groupId>com.android.tools</groupId>
<artifactId>sdk-common</artifactId>
<version>32.3.1</version>
</dependency>
<dependency>
<groupId>com.android.tools.apkparser</groupId>
<artifactId>apkanalyzer</artifactId>
@@ -184,7 +184,7 @@
<dependency>
<groupId>com.android.tools.apkparser</groupId>
<artifactId>binary-resources</artifactId>
<version>31.2.0</version>
<version>32.3.1</version>
</dependency>
<dependency>

View File

@@ -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 {
* <p>
* 直接使用 bundletool 依赖(非命令行、非 jar的 {@link DumpCommand} 输出
* base 模块的 AndroidManifest.xml 文本,再提取关键字段。
* <p>
* 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<String> 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<String> 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<Icon> 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) {

View File

@@ -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("!! 跳过:文件不存在或不是普通文件");