从源码分析 PackageInfo 和 LoadedApk

  1. 从源码分析 PackageInfo 和 LoadedApk
    1. 一、前言
    2. 二、基本概念
      1. PackageInfo
      2. LoadedApk
    3. 三、源码分析
      1. PackageManager
      2. LoadedApk
    4. 四、最后

从源码分析 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)、dataDirnativeLibraryDir
组件列表 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

​ 而 mPackageInfoContext 内部的一个隐藏字段,这个字段对应的对象类型是 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 中跟进时,会发现,已经看不到有效代码了

image-20251011101749709

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

image-20251011101855497

​ 继续跟进

image-20251011101959824

@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 里的逻辑,就更清晰了
image-20251011114625817

​ 在拿到 loadedApk 对象之后,通过反射获取 LoadedApk 类,接着遍历所有字段,然后打印。

​ 这是第一种获取 loadedApk 对象的方法,再看看另外一种。

​ 首先在 Android AOSP 源码中找到

frameworks/base/core/java/android/app/ActivityThread.java

​ 然后找 mPackages 字段的定义

image-20251011115201100

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,继续跟进,就可以看到

image-20251011115802492 完整代码如下

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 中的代码逻辑

image-20251011141239807

​ packages 变量指向了当前进程中 ActivityThread 维护的全局 Map,它保存了当前进程里所有已加载的 APK 包信息(LoadedApk 对象)。然后再根据包名从变量 packages 取出对应的 LoadedApk 引用。

​ 为什么是 WeakReference?mPackages 里保存的不是直接的 LoadedApk 对象,而是 WeakReference<LoadedApk>。这是因为系统为了避免内存泄漏:

  • 当某个 APK 不再引用(比如插件卸载或分进程退出)
  • GC(垃圾回收器)可以自动回收对应的 LoadedApk 实例。

所以这里是弱引用。

四、最后

​ 经过以上的梳理,对 PackageInfo 和 LoadedApk 有了较为清晰的理解。


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 1621925986@qq.com

💰

×

Help us with donation