refactor(packages): 重构 APKS 解析逻辑并优化图标选择
- 移除 parseApks 的 @Deprecated 标记,改为直接提取 Base/Universal APK 解析 - 使用 BinaryXmlParser 解码二进制 AndroidManifest.xml - 图标选择改为取 density 最大的候选图标 - 调整依赖版本并更新测试用例
This commit is contained in:
14
pom.xml
14
pom.xml
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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("!! 跳过:文件不存在或不是普通文件");
|
||||
|
||||
Reference in New Issue
Block a user