refactor(packages): 重构AAB解析逻辑并升级protobuf依赖
直接使用bundletool API解析AAB的Manifest和资源表,替代原有的dump命令解析方式,同时将protobuf-java版本从3.21.12升级至3.25.5以兼容新版API。
This commit is contained in:
@@ -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;
|
||||
* </ul>
|
||||
*/
|
||||
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. 获取 <manifest> 根节点
|
||||
XmlProtoElement manifestElement = manifest.getManifestElement();
|
||||
|
||||
// 3.1 解析 compileSdkVersion(位于 <uses-sdk android:compileSdkVersion=".."/>)
|
||||
// XmlProtoElement usesSdkElement = manifestElement.getChildElement("uses-sdk");
|
||||
// if (usesSdkElement != null) {
|
||||
manifestElement.getAttribute(ANDROID_NS, "compileSdkVersion")
|
||||
.map(XmlProtoAttribute::getValueAsInteger)
|
||||
.ifPresent(apkMetaResult::setCompileSdk);
|
||||
// }
|
||||
|
||||
// 3. 获取 <application> 节点
|
||||
XmlProtoElement appElement = manifestElement.getChildElement("application");
|
||||
|
||||
// 4. 解析应用名称 (android:label)
|
||||
|
||||
Optional<XmlProtoAttribute> 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. 解析应用图标:从 <application android:icon="@mipmap/.."> 资源引用解析
|
||||
Optional<XmlProtoAttribute> 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)。
|
||||
* <p>
|
||||
* 遍历 AAPT2 proto 资源表的 package/type/entry 三层结构,用资源 ID 的高 8 位
|
||||
* (packageId)、次 8 位 (typeId)、低 16 位 (entryId) 定位到目标条目,取其第一个
|
||||
* STR 类型的配置值。
|
||||
*/
|
||||
private static String resolveStringResource(BundleModule baseModule, int resId) {
|
||||
try {
|
||||
Optional<com.android.aapt.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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// 资源解析失败不影响其它字段
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 AAB base 模块的资源表中,按资源 ID 解析文件资源路径(如 @mipmap/ic_launcher)。
|
||||
* <p>
|
||||
* 遍历 AAPT2 proto 资源表定位到目标条目后,取其第一个 FILE 类型的配置值,返回
|
||||
* FileReference 的 path(形如 res/mipmap-xxxhdpi/ic_launcher.png)。
|
||||
*/
|
||||
private static String resolveFileResource(BundleModule baseModule, int resId) {
|
||||
try {
|
||||
Optional<com.android.aapt.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.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)。
|
||||
|
||||
Reference in New Issue
Block a user