从源码分析 PackageInfo 和 LoadedApk
一、前言
之前在学习 Android 加固的时候,涉及到 classLoader、LoadedApk等的内容,感觉定义界限有点模糊不清,看着看着就晕了,后来痛定思痛觉得应该好好梳理一下这方面的内容。于是便记录下学习过程。
还是沿用之前学习 so 加载过程的思想,从最顶层,也就是我们在开发时直接接触到代码开始分析。
二、基本概念
于是写了一个简单的 demo,关键代码如下
获取 PackageInfo,并打印其参数
private String buildInfo() {
StringBuilder sb = new StringBuilder();
try {
PackageManager pm = getPackageManager();
PackageInfo pi = pm.getPackageInfo(
getPackageName(),
PackageManager.GET_ACTIVITIES
| PackageManager.GET_SERVICES
| PackageManager.GET_PROVIDERS
| PackageManager.GET_RECEIVERS
| PackageManager.GET_PERMISSIONS
| PackageManager.GET_SIGNING_CERTIFICATES
);
sb.append("PackageInfo\n");
sb.append("packageName: ").append(pi.packageName).append('\n');
sb.append("versionName: ").append(pi.versionName).append('\n');
sb.append("versionCode: ").append(pi.getLongVersionCode()).append('\n');
sb.append("firstInstallTime: ").append(pi.firstInstallTime).append('\n');
sb.append("lastUpdateTime: ").append(pi.lastUpdateTime).append('\n');
ApplicationInfo ai = pi.applicationInfo;
if (ai != null) {
sb.append("sourceDir: ").append(ai.sourceDir).append('\n');
sb.append("publicSourceDir: ").append(ai.publicSourceDir).append('\n');
sb.append("dataDir: ").append(ai.dataDir).append('\n');
sb.append("nativeLibraryDir: ").append(ai.nativeLibraryDir).append('\n');
}
SigningInfo si = pi.signingInfo;
if (si != null) {
sb.append("signing: ").append(si.hasMultipleSigners() ? "multiple" : "single").append('\n');
Signature[] signatures = si.hasMultipleSigners() ? si.getApkContentsSigners() : si.getSigningCertificateHistory();
if (signatures != null) {
for (int i = 0; i < signatures.length; i++) {
sb.append("signature[").append(i).append("] sha256: ").append(bytesToHex(signatures[i].toByteArray())).append('\n');
try {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
X509Certificate cert = (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(signatures[i].toByteArray()));
sb.append(" subject: ").append(cert.getSubjectDN()).append('\n');
sb.append(" issuer: ").append(cert.getIssuerDN()).append('\n');
sb.append(" serial: ").append(cert.getSerialNumber()).append('\n');
} catch (Throwable t) {
sb.append(" cert parse failed: ").append(t.getClass().getSimpleName()).append('\n');
}
}
}
}
} catch (Throwable e) {
sb.append("PackageInfo error: ").append(e).append('\n');
}
sb.append('\n');
sb.append("LoadedApk\n");
sb.append(getLoadedApkDetails());
return sb.toString();
}
获取 LoadedApk 并打印参数
private String getLoadedApkDetails() {
StringBuilder sb = new StringBuilder();
Object loadedApk = null;
try {
Context base = getBaseContext();
try {
// mPackageInfo 是当前Context对应的单个应用包信息
Field f = base.getClass().getDeclaredField("mPackageInfo");
f.setAccessible(true);
loadedApk = f.get(base);
} catch (Throwable ignore) { }
if (loadedApk == null) {
// 拿到ActivityThread类对象
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
// 获取当前进程的ActivityThread实例
Method currentActivityThread = activityThreadClass.getDeclaredMethod("currentActivityThread");
currentActivityThread.setAccessible(true);
Object at = currentActivityThread.invoke(null);
// 获取mPackages对象
// mPackages属于ActivityThread,是全局缓存,保存所有已加载应用包
Field mPackagesField = activityThreadClass.getDeclaredField("mPackages");
mPackagesField.setAccessible(true);
// 通过包名获取当前应用的 LoadedApk 对象
java.util.Map<?,?> packages = (java.util.Map<?,?>) mPackagesField.get(at);
Object wr = packages.get(getPackageName());
if (wr instanceof WeakReference) {
loadedApk = ((WeakReference<?>) wr).get();
}
}
if (loadedApk != null) {
sb.append("object: ").append(loadedApk.toString()).append('\n');
try {
Class<?> laClass = Class.forName("android.app.LoadedApk");
Field[] fields = laClass.getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
Object val;
try {
val = field.get(loadedApk);
} catch (Throwable t) {
val = t.getClass().getSimpleName();
}
sb.append(field.getName()).append("=").append(String.valueOf(val)).append('\n');
}
} catch (Throwable t) {
sb.append("fields reflect failed: ").append(t).append('\n');
}
} else {
sb.append("LoadedApk not found (possibly blocked by hidden API)");
}
} catch (Throwable e) {
sb.append("LoadedApk error: ").append(e);
}
return sb.toString();
}
demo 的代码逻辑很简单,就是直接 or 间接获取到,再打印所含信息。
然后接下来先来解释一下这两者的概念和关系。
PackageInfo
总的来说,PackageInfo 是应用 **安装时(parse + install 阶段)**由系统解析 AndroidManifest.xml、签名、资源等信息后生成的 结构化描述对象。它反映的是“包的静态信息”——也就是 APK 文件里声明的东西,不是运行时对象。
概念图:
PackageManagerService
└── PackageParser.parsePackage() // 解析 AndroidManifest.xml
├── 生成 PackageParser.Package 对象
├── 解析组件(Activity、Service、Provider、Receiver)
├── 解析权限声明
├── 解析签名、meta-data、sharedUserId 等
└── ...
└── generatePackageInfo() // 把 Package 转为 PackageInfo
最终会得到一个 PackageInfo,被保存到:
- 系统文件
/data/system/packages.xml - 运行时缓存(
mPackages)里也有一份。
那么 PackageInfo 都包括哪些信息呢?以下是 PackageInfo 的典型字段分类
| 分类 | 字段 | 说明 |
|---|---|---|
| 基本信息 | packageName, versionCode, versionName, firstInstallTime, lastUpdateTime |
来自 manifest 和安装时记录 |
| 应用信息 | applicationInfo(类型:ApplicationInfo) |
包含 APK 路径(sourceDir)、dataDir、nativeLibraryDir 等 |
| 组件列表 | activities, services, providers, receivers |
来自 manifest 中声明的四大组件 |
| 权限声明 | requestedPermissions, requestedPermissionsFlags |
<uses-permission> 列表和系统授予状态 |
| 签名信息 | signingInfo(或旧版 signatures) |
APK 签名证书、公钥哈希等 |
| 共享信息 | sharedUserId, sharedUserLabel |
多应用共享 UID 场景 |
| 安装来源 | installLocation, installSourceInfo |
安装路径、来源 App(如 Play Store) |
| 分包相关 | splitNames, splitCodePaths |
Android 5.0+ 的多 APK 分割机制 |
这些字段都是从磁盘上安装包解析来的静态结构,不会随着应用运行而变化。
LoadedApk
再来说说 LoadedApk。LoadedApk 是 Android 框架中的一个内部类(位于 android.app 包下),代表 一个已加载 APK 的运行时描述对象。
它内部包含了
| 字段 | 含义 |
|---|---|
ApplicationInfo mApplicationInfo |
应用的信息(包名、路径、签名等) |
ClassLoader mClassLoader |
该应用的类加载器 |
Resources mResources |
应用资源对象 |
Instrumentation mInstrumentation |
应用的测试/生命周期控制类 |
Application mApplication |
当前应用对象 |
ActivityThread mActivityThread |
所属的线程信息 |
这里面就包含了在加固时不可绕开的 ClassLoader。
而 mPackageInfo 是 Context 内部的一个隐藏字段,这个字段对应的对象类型是 LoadedApk,所以是采取反射的方式来获取。在 demo 中还写了一种备用方案,就是如果无法直接从 ContextImpl 取出 mPackageInfo,就从全局的 ActivityThread 里去拿当前应用的 LoadedApk 对象。
两种方案中又分别提到 mPackages 和 mPackageInfo,再说说这两。
| 字段 | 所属类 | 类型 | 含义 |
|---|---|---|---|
mPackages |
ActivityThread |
ArrayMap<String, WeakReference<LoadedApk>> |
全局缓存,保存所有已加载应用包(包括自身和插件) |
mPackageInfo |
ContextImpl |
LoadedApk |
当前 Context(通常是一个 Activity 或 Application)对应的单个应用包信息 |
mPackages是 全局表(所有包的映射表)mPackageInfo是 当前包的引用(指向上表中的一个值)
关系图(简化)
[ActivityThread] <-- 全局唯一
|
├── mPackages : Map<String, WeakReference<LoadedApk>>
| ├── "com.example.app" ──────▶ LoadedApk #1
| ├── "plugin.apk" ───────────▶ LoadedApk #2
| └── "systemui" ─────────────▶ LoadedApk #3
|
[ContextImpl] <-- 每个 Activity/Service/Application 都有一个
└── mPackageInfo ─────────────▶ LoadedApk #1 ←(指向上面的其中一个)
用法区别
| 场景 | 通常使用哪个 | 原因 |
|---|---|---|
| 获取当前应用的 ClassLoader / Resources | mPackageInfo |
它直接指向当前包的 LoadedApk |
| 获取其他包(如插件)的 LoadedApk | mPackages |
它存着所有包的映射关系 |
| 框架初始化时查找 Application 对象 | mPackages |
因为所有应用包都在这里注册 |
| Activity 启动时内部访问资源 | mPackageInfo |
每个 ContextImpl 都绑定自己的 LoadedApk |
那么这两者之间是怎么“沟通”的呢?答:ApplicationInfo。
ApplicationInfo 提供路径、UID等信息,安装时生成,运行时被拷贝使用,可同时出现在 PackageInfo 和 LoadedApk 中。
三、源码分析
PackageManager
首先就从最开始的 getPackageManager() 开始分析,当我们在 Android studio 中跟进时,会发现,已经看不到有效代码了

但是至少知道是在 ContextWrapper.class,去到源码中进行跟进

继续跟进

@Override
public PackageManager getPackageManager() {
if (mPackageManager != null) {
return mPackageManager;
}
final IPackageManager pm = ActivityThread.getPackageManager();
if (pm != null) {
// Doesn't matter if we make more than one instance.
return (mPackageManager = new ApplicationPackageManager(this, pm));
}
return null;
}
这段代码首先先尝试返回缓存的 mPackageManager,若没有就从 ActivityThread(应用进程的线程调度入口)拿到系统的 IPackageManager(是一个 AIDL 接口的 Binder代理,它实际代表系统进程里的 PackageManagerService),用它创建一个 ApplicationPackageManager 并缓存后返回。
如下是 ChatGPT 给出的流程图
┌──────────────────────────────────────────────┐
│ App 进程 │
│──────────────────────────────────────────────│
│ │
│ Activity / ContextImpl │
│ │ │
│ │ getPackageManager() │
│ ▼ │
│ ApplicationPackageManager (extends PackageManager)
│ │ ← 持有 IPackageManager 接口引用 (mPM)
│ │
│ │ 调用 mPM.getPackageInfo()
│ ▼
│ IPackageManager.Stub.Proxy (AIDL 客户端代理)
│ │ ↓ Binder 调用(跨进程)
│══════════════════════════════════════════════│
│ Binder 驱动层 │
│══════════════════════════════════════════════│
│ ↑ Binder 通信(内核级) │
│ │ │
└───────┼───────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ system_server 进程 │
│──────────────────────────────────────────────│
│ │
│ PackageManagerService (extends IPackageManager.Stub)
│ │
│ │ 真正实现 getPackageInfo() 等方法
│ │ 负责解析 APK、缓存包信息、检查权限
│ │
│ ▼
│ 返回结果 → Binder 驱动 → 回传给 App 进程
│ │
│ 注册时: │
│ ServiceManager.addService("package", pm);│
│ │
└──────────────────────────────────────────────┘
而 ActivityThread.getPackageManager() 方法继续跟进分析下去个人感觉意义不大了,因为这次笔者的学习目的就是理解 PackageManager 是怎么管理应用包、提供查询接口的,再往后已经不负责逻辑处理、也不解析包信息,只是一次 Binder 代理对象的获取。
在通过 getPackageManager() 方法拿到 pm 对象之后,后续调用 getPackageInfo() 方法时,虽然是调用本地方法,但实际上是一个跨进程调用。
LoadedApk
因为 loadedApk 是通过反射得到的,所以也就没什么逻辑好分析的,但是我们可以去看看源码中这些字段的定义。
首先分析的是 mPackageInfo 字段,在路径 frameworks/base/core/java/android/app/ContextImpl.java 可以找到该字段的定义。
final @NonNull LoadedApk mPackageInfo;
可以看到 mPackageInfo 是 android.app.LoadedApk 类型。LoadedApk 是一个非常核心的类,代表当前应用包在内存中的加载信息(即这个 APK 在当前进程中的运行时封装)。
ContextImpl 与 LoadedApk 的关系如下
| 类名 | 作用 | 与 mPackageInfo 关系 |
|---|---|---|
ContextImpl |
Context 的真正实现类 | 持有一个 mPackageInfo |
LoadedApk |
封装了当前 APK 的资源、ClassLoader、Application 信息 | 被 ContextImpl 引用 |
然后再跟进 LoadedApk.java。代码量很大,这里就不贴出来了,想仔细研究的直接找 frameworks/base/core/java/android/app/LoadedApk.java 路径。
通过源码进一步确认了 loadedApk 对象是一个 APK 在内存中的完整加载状态,通俗来说即这个类是 Android 应用在进程中的运行时快照。
再看 demo 里的逻辑,就更清晰了
在拿到 loadedApk 对象之后,通过反射获取 LoadedApk 类,接着遍历所有字段,然后打印。
这是第一种获取 loadedApk 对象的方法,再看看另外一种。
首先在 Android AOSP 源码中找到
frameworks/base/core/java/android/app/ActivityThread.java
然后找 mPackages 字段的定义

final ArrayMap<String, WeakReference<LoadedApk>> mPackages = new ArrayMap<>();
这是一个全局缓存,保存了当前进程内所有已加载的 APK 包。
- key:包名(String)
- value:指向 LoadedApk 的 WeakReference
再看看和该字段有关的方法。
@UnsupportedAppUsage
public final LoadedApk getPackageInfoNoCheck(ApplicationInfo ai,
CompatibilityInfo compatInfo) {
return getPackageInfo(ai, compatInfo, null, false, true, false);
}
ActivityThread 会通过它从 mPackages 获取或创建 LoadedApk,继续跟进,就可以看到
完整代码如下
private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo,
ClassLoader baseLoader, boolean securityViolation, boolean includeCode,
boolean registerPackage, boolean isSdkSandbox) {
final boolean differentUser = (UserHandle.myUserId() != UserHandle.getUserId(aInfo.uid));
synchronized (mResourcesManager) {
WeakReference<LoadedApk> ref;
if (differentUser || isSdkSandbox) {
// Caching not supported across users and for sdk sandboxes
ref = null;
} else if (includeCode) {
ref = mPackages.get(aInfo.packageName);
} else {
ref = mResourcePackages.get(aInfo.packageName);
}
LoadedApk packageInfo = ref != null ? ref.get() : null;
if (packageInfo != null) {
if (!isLoadedApkResourceDirsUpToDate(packageInfo, aInfo)) {
if (packageInfo.getApplicationInfo().createTimestamp > aInfo.createTimestamp) {
// The cached loaded apk is newer than the one passed in, we should not
// update the cached version
Slog.w(TAG, "getPackageInfo() called with an older ApplicationInfo "
+ "than the cached version for package " + aInfo.packageName);
} else {
Slog.v(TAG, "getPackageInfo() caused update to cached ApplicationInfo "
+ "for package " + aInfo.packageName);
List<String> oldPaths = new ArrayList<>();
LoadedApk.makePaths(this, aInfo, oldPaths);
packageInfo.updateApplicationInfo(aInfo, oldPaths);
}
}
return packageInfo;
}
if (localLOGV) {
Slog.v(TAG, (includeCode ? "Loading code package "
: "Loading resource-only package ") + aInfo.packageName
+ " (in " + (mBoundApplication != null
? mBoundApplication.processName : null)
+ ")");
}
packageInfo =
new LoadedApk(this, aInfo, compatInfo, baseLoader,
securityViolation, includeCode
&& (aInfo.flags & ApplicationInfo.FLAG_HAS_CODE) != 0, registerPackage);
if (mSystemThread && "android".equals(aInfo.packageName)) {
packageInfo.installSystemApplicationInfo(aInfo,
getSystemContext().mPackageInfo.getClassLoader());
}
if (differentUser || isSdkSandbox) {
// Caching not supported across users and for sdk sandboxes
} else if (includeCode) {
mPackages.put(aInfo.packageName,
new WeakReference<LoadedApk>(packageInfo));
} else {
mResourcePackages.put(aInfo.packageName,
new WeakReference<LoadedApk>(packageInfo));
}
return packageInfo;
}
}
再看 dmeo 中的代码逻辑

packages 变量指向了当前进程中 ActivityThread 维护的全局 Map,它保存了当前进程里所有已加载的 APK 包信息(LoadedApk 对象)。然后再根据包名从变量 packages 取出对应的 LoadedApk 引用。
为什么是 WeakReference?mPackages 里保存的不是直接的 LoadedApk 对象,而是 WeakReference<LoadedApk>。这是因为系统为了避免内存泄漏:
- 当某个 APK 不再引用(比如插件卸载或分进程退出)
- GC(垃圾回收器)可以自动回收对应的 LoadedApk 实例。
所以这里是弱引用。
四、最后
经过以上的梳理,对 PackageInfo 和 LoadedApk 有了较为清晰的理解。
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 1621925986@qq.com