refactor(packages): 重构 APKM/XAPK 元数据解析逻辑

This commit is contained in:
2026-08-14 22:34:30 +08:00
parent baa2d8d2e3
commit 9d55739955
4 changed files with 260 additions and 252 deletions

View File

@@ -1,8 +1,6 @@
package com.youlai.boot.app.packages.component;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.android.aapt.Resources;
import com.android.tools.build.bundletool.commands.DumpCommand;
import com.android.tools.build.bundletool.model.AndroidManifest;
import com.android.tools.build.bundletool.model.AppBundle;
@@ -11,6 +9,8 @@ import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoAttribute
import com.android.tools.build.bundletool.model.utils.xmlproto.XmlProtoElement;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.youlai.boot.app.packages.model.dto.ApkmJsonInfo;
import com.youlai.boot.app.packages.model.dto.SplitApks;
import com.youlai.boot.app.packages.model.dto.XapkJsonInfo;
import net.dongliu.apk.parser.ApkFile;
import net.dongliu.apk.parser.bean.ApkMeta;
import net.dongliu.apk.parser.bean.Icon;
@@ -24,6 +24,7 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
@@ -44,7 +45,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);
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private static final Logger LOGGER = LoggerFactory.getLogger(ApkMetaParser.class);
/**
* 统一解析入口:根据文件扩展名分发到对应解析策略。
@@ -189,6 +191,32 @@ public class ApkMetaParser {
return apkMetaResult;
}
/**
* 在资源表中按资源 IDpackageId/typeId/entryId定位到目标条目。
*/
private static Resources.Entry findResourceEntry(
Resources.ResourceTable table, int resId) {
int packageId = (resId >>> 24) & 0xFF;
int typeId = (resId >>> 16) & 0xFF;
int entryId = resId & 0xFFFF;
for (Resources.Package pkg : table.getPackageList()) {
if (pkg.getPackageId().getId() != packageId) {
continue;
}
for (Resources.Type type : pkg.getTypeList()) {
if (type.getTypeId().getId() != typeId) {
continue;
}
for (Resources.Entry entry : type.getEntryList()) {
if (entry.getEntryId().getId() == entryId) {
return entry;
}
}
}
}
return null;
}
/**
* 从 AAB base 模块的资源表中,按资源 ID 解析字符串资源(如 @string/app_name
* <p>
@@ -198,34 +226,20 @@ public class ApkMetaParser {
*/
private static String resolveStringResource(BundleModule baseModule, int resId) {
try {
Optional<com.android.aapt.Resources.ResourceTable> tableOpt =
Optional<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();
}
}
}
Resources.ResourceTable table = tableOpt.get();
Resources.Entry entry = findResourceEntry(table, resId);
if (entry == null) {
return null;
}
for (Resources.ConfigValue cv : entry.getConfigValueList()) {
Resources.Item item = cv.getValue().getItem();
if (item != null && item.getValueCase() == Resources.Item.ValueCase.STR) {
return item.getStr().getValue();
}
}
} catch (Exception ignored) {
@@ -243,61 +257,47 @@ public class ApkMetaParser {
*/
private static String resolveFileResource(BundleModule baseModule, int resId) {
try {
Optional<com.android.aapt.Resources.ResourceTable> tableOpt =
Optional<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) {
Resources.ResourceTable table = tableOpt.get();
Resources.Entry entry = findResourceEntry(table, resId);
if (entry == null) {
return null;
}
String bestPath = null;
int bestDensity = -1;
boolean hasDensity = false;
for (Resources.ConfigValue cv : entry.getConfigValueList()) {
Resources.Item item = cv.getValue().getItem();
if (item == null
|| item.getValueCase() != Resources.Item.ValueCase.FILE) {
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;
}
String bestPath = null;
int bestDensity = -1;
boolean hasDensity = false;
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) {
continue;
}
String path = item.getFile().getPath();
if (path == null || path.isBlank()) {
continue;
}
int density = cv.getConfig().getDensity();
if (density > 0) {
hasDensity = true;
if (density > bestDensity) {
bestDensity = density;
bestPath = path;
}
} else if (bestPath == null) {
// 无 density 信息的配置值,作为兜底
bestPath = path;
}
}
if (hasDensity && bestPath != null) {
return bestPath;
}
// 存在带 density 的配置时走上面的分支;否则返回无 density 的第一个兜底路径
if (bestPath != null) {
return bestPath;
}
}
String path = item.getFile().getPath();
if (path == null || path.isBlank()) {
continue;
}
int density = cv.getConfig().getDensity();
if (density > 0) {
hasDensity = true;
if (density > bestDensity) {
bestDensity = density;
bestPath = path;
}
} else if (bestPath == null) {
// 无 density 信息的配置值,作为兜底
bestPath = path;
}
}
if (hasDensity && bestPath != null) {
return bestPath;
}
// 存在带 density 的配置时走上面的分支;否则返回无 density 的第一个兜底路径
if (bestPath != null) {
return bestPath;
}
} catch (Exception ignored) {
// 资源解析失败不影响其它字段
@@ -355,113 +355,70 @@ public class ApkMetaParser {
//以上3个应该都存在,否则解析失败
byte[] infoData;
byte[] iconBytes;
ApkmContent content = readApkmContent(apkmFile);
byte[] infoData = content.infoJson;
List<ContainerEntry> apks = content.apks;
List<String> splitApkList = apks.stream().map(apk -> apk.name).collect(Collectors.toList());
apkMetaResult.setSplitApkList(splitApkList);
if (infoExists) {
infoData = ZipUtil.unpackEntry(apkmFile, "info.json");
ObjectMapper objectMapper = new ObjectMapper();
ApkmJsonInfo info = objectMapper.readValue(infoData, ApkmJsonInfo.class);
apkMetaResult.setAppLabel(info.getApp_name());
apkMetaResult.setPackageName(info.getPname());
apkMetaResult.setVersionName(info.getRelease_version());
apkMetaResult.setVersionCode(info.getVersioncode());
apkMetaResult.setMinSdk(info.getMin_api());
apkMetaResult.setAbiList(info.getArches());
ApkmJsonInfo info = OBJECT_MAPPER.readValue(infoData, ApkmJsonInfo.class);
apkMetaResult.setAppLabel(info.getApp_name());
apkMetaResult.setPackageName(info.getPname());
apkMetaResult.setVersionName(info.getRelease_version());
apkMetaResult.setVersionCode(info.getVersioncode());
apkMetaResult.setMinSdk(info.getMin_api());
apkMetaResult.setAbiList(info.getArches());
//info.json 无法获取到 targetSdk,compileSdk,permissions,iconPath
//先加载icon.png兜底,后续再加载base.apk,有些base.apk无法获取到图标
iconBytes = ZipUtil.unpackEntry(apkmFile, "icon.png");
apkMetaResult.setIconBytes(iconBytes);
apkMetaResult.setIconPath("icon.png");
//info.json 无法获取到 targetSdk,compileSdk,permissions,iconPath
//先加载icon.png兜底,后续再加载base.apk,有些base.apk无法获取到图标
byte[] iconBytes = ZipUtil.unpackEntry(apkmFile, "icon.png");
apkMetaResult.setIconPath("icon.png");
apkMetaResult.setIconBytes(iconBytes);
byte[] baseApkbytes = ZipUtil.unpackEntry(apkmFile, "base.apk");
ApkMetaResult result = tryParseApkBytes(baseApkbytes);
if (result != null) {
if (!Objects.equals(apkMetaResult.getAppLabel(),result.getAppLabel())){
apkMetaResult.setAppLabel(result.getAppLabel());
}
if (!Objects.equals(apkMetaResult.getPackageName(),result.getPackageName())){
apkMetaResult.setPackageName(result.getPackageName());
}
if (!Objects.equals(apkMetaResult.getVersionName(),result.getVersionName())){
apkMetaResult.setVersionName(result.getVersionName());
}
if (!Objects.equals(apkMetaResult.getVersionCode(), result.getVersionCode())){
apkMetaResult.setVersionCode(result.getVersionCode());
}
byte[] baseApkbytes = ZipUtil.unpackEntry(apkmFile, "base.apk");
ApkMetaResult result = tryParseApkBytes(baseApkbytes);
//这三个属性无法从info.json中获取,只能从base.apk中获取
apkMetaResult.setTargetSdk(result.getTargetSdk());
apkMetaResult.setCompileSdk(result.getCompileSdk());
apkMetaResult.setPermissions(result.getPermissions());
if (result.getAbiList()!= null && !result.getAbiList().isEmpty()){
apkMetaResult.setAbiList(result.getAbiList());
}
if(result.getSplitApkList()!= null && !result.getSplitApkList().isEmpty()){
apkMetaResult.setSplitApkList(result.getSplitApkList());
}
if (StringUtils.isNotBlank(result.getIconPath())) {
apkMetaResult.setIconPath(result.getIconPath());
}
if (result.getIconBytes() != null) {
apkMetaResult.setIconBytes(result.getIconBytes());
}
if (result != null) {
if (!Objects.equals(apkMetaResult.getAppLabel(), result.getAppLabel())) {
apkMetaResult.setAppLabel(result.getAppLabel());
}
if (!Objects.equals(apkMetaResult.getPackageName(), result.getPackageName())) {
apkMetaResult.setPackageName(result.getPackageName());
}
if (!Objects.equals(apkMetaResult.getVersionName(), result.getVersionName())) {
apkMetaResult.setVersionName(result.getVersionName());
}
if (!Objects.equals(apkMetaResult.getVersionCode(), result.getVersionCode())) {
apkMetaResult.setVersionCode(result.getVersionCode());
}
//这三个属性无法从info.json中获取,只能从base.apk中获取
apkMetaResult.setTargetSdk(result.getTargetSdk());
apkMetaResult.setCompileSdk(result.getCompileSdk());
apkMetaResult.setPermissions(result.getPermissions());
return apkMetaResult;
} else {
ApkmContent content = readApkmContent(apkmFile);
List<ContainerEntry> apks = content.apks;
byte[] infoJson = content.infoJson;
if (apks.isEmpty()) {
throw new IOException("容器内未找到任何 .apk 文件: " + apkmFile.getName());
if (isNotEmpty(result.getAbiList())) {
apkMetaResult.setAbiList(result.getAbiList());
}
if (isNotEmpty(result.getSplitApkList())) {
apkMetaResult.setSplitApkList(result.getSplitApkList());
}
byte[] baseApkbytes = ZipUtil.unpackEntry(apkmFile, "base.apk");
// 1) 按经验法则优先定位主 APK
ContainerEntry mainApk = locateApkmMainApk(apks);
// 2) 若经验法则未命中,兜底:逐个尝试解析,第一个拿到 packageName 的即为主包
ApkMetaResult result = tryParseApkBytes(baseApkbytes);
if (result == null) {
for (ContainerEntry entry : apks) {
ApkMetaResult r = tryParseApkBytes(entry.data);
if (r != null && r.getPackageName() != null && !r.getPackageName().isBlank()) {
result = r;
mainApk = entry;
break;
}
}
if (StringUtils.isNotBlank(result.getIconPath())) {
apkMetaResult.setIconPath(result.getIconPath());
}
if (result == null) {
throw new IOException("容器内所有 apk 均无法解析出有效包名: " + apkmFile.getName());
if (result.getIconBytes() != null) {
apkMetaResult.setIconBytes(result.getIconBytes());
}
// 3) 记录 split apk 列表
for (ContainerEntry entry : apks) {
result.getSplitApkList().add(entry.name);
}
if (infoJson != null) {
supplementFromJson(result, infoJson);
}
return result;
}
return apkMetaResult;
}
/**
* 解包 ZIP 容器,收集内部所有 .apk 及描述文件info.json
* APKM固定为info.json
* <p>
* 只获取splitApkList和info.json信息
*
*/
private static ApkmContent readApkmContent(File apkmFile) throws IOException {
@@ -475,7 +432,7 @@ public class ApkMetaParser {
continue;
}
String entryName = entry.getName();
if (entryName.toLowerCase(Locale.ROOT).endsWith(".apk")) {
if (entryName.toLowerCase(Locale.ROOT).endsWith(".apk") && !entryName.equalsIgnoreCase("base.apk")) {
try (InputStream in = zf.getInputStream(entry)) {
apks.add(new ContainerEntry(entryName, in.readAllBytes()));
}
@@ -505,44 +462,63 @@ public class ApkMetaParser {
* 可能额外包含大型 obb 扩展文件(非 apk直接忽略
*/
public static ApkMetaResult parseXapk(File xapkFile) throws IOException {
ApkMetaResult apkMetaResult = new ApkMetaResult();
boolean manifestExists = ZipUtil.containsEntry(xapkFile, "manifest.json");
if (!manifestExists) {
throw new IOException("容器内未找到 manifest.json 文件: " + xapkFile.getName());
}
XapkContent content = readXapkContent(xapkFile);
List<ContainerEntry> apks = content.apks;
byte[] manifestJson = content.manifestJson;
if (apks.isEmpty()) {
throw new IOException("容器内未找到任何 .apk 文件: " + xapkFile.getName());
XapkJsonInfo xapkJsonInfo = OBJECT_MAPPER.readValue(manifestJson, XapkJsonInfo.class);
apkMetaResult.setAppLabel(xapkJsonInfo.getName());
apkMetaResult.setPackageName(xapkJsonInfo.getPackage_name());
apkMetaResult.setVersionName(xapkJsonInfo.getVersion_name());
apkMetaResult.setVersionCode(xapkJsonInfo.getVersion_code());
apkMetaResult.setMinSdk(xapkJsonInfo.getMin_sdk_version());
apkMetaResult.setTargetSdk(xapkJsonInfo.getTarget_sdk_version());
apkMetaResult.setPermissions(xapkJsonInfo.getPermissions());
byte[] iconBytes = ZipUtil.unpackEntry(xapkFile, xapkJsonInfo.getIcon());
apkMetaResult.setIconPath(xapkJsonInfo.getIcon());
apkMetaResult.setIconBytes(iconBytes);
List<String> splitConfigs = xapkJsonInfo.getSplit_configs();
List<SplitApks> splitApksList = xapkJsonInfo.getSplit_apks();
Set<String> splitConfigSet = new HashSet<>(splitConfigs);
List<String> fileList = xapkJsonInfo.getSplit_apks().stream()
.filter(apk -> splitConfigSet.contains(apk.getId()))
.map(SplitApks::getFile)
.collect(Collectors.toList());
apkMetaResult.setSplitApkList(fileList);
SplitApks baseApk = splitApksList.stream().filter(splitApks -> splitApks.getId().equals("base")).findFirst().orElse(null);
if (baseApk == null) {
throw new IOException("容器内未找到 base apk 文件: " + xapkFile.getName());
}
// 1) XAPK 主包优先按 manifest.json 的 split_apks(id == "base") 定位,失败再走经验法则
ContainerEntry mainApk = locateXapkMainApk(apks, manifestJson);
ContainerEntry baseApkEntry = apks.stream().filter(containerEntry -> containerEntry.name.equals(baseApk.getFile())).findFirst().orElse(null);
if (baseApkEntry == null) {
throw new IOException("容器内未找到 base apk 文件: " + xapkFile.getName());
}
// 2) 若经验法则未命中,兜底:逐个尝试解析,第一个拿到 packageName 的即为主包
ApkMetaResult result = mainApk != null ? tryParseApkBytes(mainApk.data) : null;
ApkMetaResult result = tryParseApkBytes(baseApkEntry.data);
if (result == null) {
for (ContainerEntry entry : apks) {
ApkMetaResult r = tryParseApkBytes(entry.data);
if (r != null && r.getPackageName() != null && !r.getPackageName().isBlank()) {
result = r;
mainApk = entry;
break;
}
}
}
if (result == null) {
throw new IOException("容器内所有 apk 均无法解析出有效包名: " + xapkFile.getName());
throw new IOException("容器内未找到 base apk 文件: " + xapkFile.getName());
}
// 3) 记录 split apk 列表
for (ContainerEntry entry : apks) {
result.getSplitApkList().add(entry.name);
}
// 主 APK 解析出的字段优先(覆盖容器 JSON 中的值)
overrideIfDifferent(apkMetaResult, result);
apkMetaResult.setCompileSdk(result.getCompileSdk());
apkMetaResult.setAbiList(result.getAbiList());
// 4) 补充 JSON 描述信息XAPK 通常只有 manifest.json
if (manifestJson != null) {
supplementFromJson(result, manifestJson);
}
return result;
return apkMetaResult;
}
private static XapkContent readXapkContent(File xapkFile) throws IOException {
@@ -570,64 +546,6 @@ public class ApkMetaParser {
return new XapkContent(apks, manifestJson);
}
/**
* 经验法则定位主 APK
* <ul>
* <li>APKMAPKMirrorinfo.json 描述的 base.apk</li>
* </ul>
*/
private static ContainerEntry locateApkmMainApk(List<ContainerEntry> apks) {
// 优先 base-master.apk / base.apk
for (ContainerEntry e : apks) {
if (e.name.toLowerCase(Locale.ROOT).equalsIgnoreCase("base.apk")) {
return e;
}
}
// 再找不含 split / config / density 等关键字的主包
for (ContainerEntry e : apks) {
String lower = e.name.toLowerCase(Locale.ROOT);
if (!lower.contains("split") && !lower.contains("config") && !lower.contains("density")) {
return e;
}
}
return null;
}
/**
* XAPK 专属定位:解析 manifest.json 的 {@code split_apks} 数组,返回
* {@code id == "base"} 项对应的 apk 条目(即主包)。
*/
private static ContainerEntry locateXapkMainApk(List<ContainerEntry> apks, byte[] manifestJson) {
if (manifestJson == null) {
return null;
}
String baseFile = null;
try {
JSONArray splits = JSONUtil.parseObj(new String(manifestJson, StandardCharsets.UTF_8))
.getJSONArray("split_apks");
if (splits != null) {
for (int i = 0; i < splits.size(); i++) {
JSONObject split = splits.getJSONObject(i);
if ("base".equals(split.getStr("id"))) {
baseFile = split.getStr("file");
break;
}
}
}
} catch (Exception ignored) {
// JSON 解析失败则回退到经验法则
}
if (baseFile == null) {
return null;
}
for (ContainerEntry e : apks) {
if (e.name.equalsIgnoreCase(baseFile)) {
return e;
}
}
return null;
}
private record XapkContent(List<ContainerEntry> apks, byte[] manifestJson) {
}
@@ -870,6 +788,34 @@ public class ApkMetaParser {
}
}
/**
* 将主 APK 解析结果覆盖到容器解析结果上,仅当两者不一致时才覆盖。
*/
private static void overrideIfDifferent(ApkMetaResult target, ApkMetaResult source) {
if (!Objects.equals(target.getAppLabel(), source.getAppLabel())) {
target.setAppLabel(source.getAppLabel());
}
if (!Objects.equals(target.getPackageName(), source.getPackageName())) {
target.setPackageName(source.getPackageName());
}
if (!Objects.equals(target.getVersionName(), source.getVersionName())) {
target.setVersionName(source.getVersionName());
}
if (!Objects.equals(target.getVersionCode(), source.getVersionCode())) {
target.setVersionCode(source.getVersionCode());
}
if (!Objects.equals(target.getMinSdk(), source.getMinSdk())) {
target.setMinSdk(source.getMinSdk());
}
if (!Objects.equals(target.getTargetSdk(), source.getTargetSdk())) {
target.setTargetSdk(source.getTargetSdk());
}
}
private static boolean isNotEmpty(List<?> list) {
return list != null && !list.isEmpty();
}
private static void fillIcon(ApkMetaResult result, ApkFile apkFileParser) {
try {
List<Icon> icons = apkFileParser.getIconFiles();

View File

@@ -0,0 +1,25 @@
package com.youlai.boot.app.packages.model.dto;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Data;
import java.io.Serializable;
@Data
public class SplitApks implements Serializable {
private static final long serialVersionUID = 4207274362982520838L;
private String file;
private String id;
@Override
public String toString() {
try {
return new ObjectMapper().writeValueAsString(this);
} catch (JsonProcessingException e) {
return super.toString();
}
}
}

View File

@@ -0,0 +1,37 @@
package com.youlai.boot.app.packages.model.dto;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
@Data
public class XapkJsonInfo implements Serializable {
private static final long serialVersionUID = -5080591968988248631L;
private int xapk_version;
private String package_name;
private String name;
private long version_code;
private String version_name;
private int min_sdk_version;
private int target_sdk_version;
private List<String> permissions;
private List<String> split_configs;
private long total_size;
private String icon;
private List<SplitApks> split_apks;
@Override
public String toString() {
try {
return new ObjectMapper().writeValueAsString(this);
} catch (JsonProcessingException e) {
return super.toString();
}
}
}

View File

@@ -44,7 +44,7 @@ public class AppPackageVO {
private String versionName;
@Schema(description = "版本号")
private Integer versionCode;
private Long versionCode;
@Schema(description = "最低SDK版本")
private Integer minSdk;