一文掌握Android app 安装原理,基于android-30的源码分析

之前通过 startActivity 我们了解到在 android-30 源码内部一个重要的系统服务 ActivityTaskManagerService,从 Android 10(api-29)开始,它就接替 ActivityManagerService(AMS) 来单独负责管理 activities 和 activity 相关容器(task,stacks,display)

接下来我们通过分析 apk 的安装过程来了解一下 Android 中另一个比较重要的系统服务 PackageManagerService(PMS)的实现原理。

apk 安装之前的信息处理

当我们点击某一个 App 安装包进行安装时,首先会弹出一个系统界面引导我们进行安装操作。

1. 不同 Android 版本安装 apk 代码不同

在 Android 7.0以前:

ini 复制代码
    public static void installAPK(Context context, File file) {
        if (file == null || !file.exists()) {
            return;
        }
        Intent intent = new Intent();
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        intent.setAction(Intent.ACTION_VIEW);
        intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive");
        context.startActivity(intent);
    }

但是如果在之后版本使用这种方式,就会抛出异常:FileUriExposeException,产生异常的原因是因为StrictMode API 政策禁止应用程序将 file:// Uri 暴露给另一个应用程序。所以在 Android 7.0 之后我们使用如下方式进行apk安装:

ini 复制代码
    private void startInstall(String apkPath) {
        File apkFile = new File(apkPath);
        Timber.i("apk大小:%s", apkFile.length());
        Intent intent = new Intent(Intent.ACTION_VIEW);
        Uri uri = AndPermission.getFileUri(getContext(), apkFile);
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
        intent.setDataAndType(uri, "application/vnd.android.package-archive");
        startActivity(intent);
    }

2. 不同 Android 版本系统的启动方式不同

之所以出现代码的变动,是因为 Android 7.0 版本之后,系统的启动界面入口发生了变化。点开 Android 的源码 PackageInstaller 包。发现从之前的 PackageInstallerActivity 变成了 InstallStart。

ini 复制代码
<application android:name=".PackageInstallerApplication"
        ...>
    <activity android:name=".InstallStart"
            android:exported="true">
        <intent-filter android:priority="1">
            <action android:name="android.intent.action.VIEW" />
            <action android:name="android.intent.action.INSTALL_PACKAGE" />
            <category android:name="android.intent.category.DEFAULT" />
            <data android:scheme="content" />
            <data android:mimeType="application/vnd.android.package-archive" />
        </intent-filter>
        ...
    </activity>
    <activity android:name=".PackageInstallerActivity"
            android:exported="false" />
    ...
</application>

我们从 InstallStart 的创建方法开始看起:

InstallStart 的 onCreate 方法

scss 复制代码
@Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...
        // 判断是否有安装权限 ,没有的话直接结束界面
        if (mAbortInstall) {
            setResult(RESULT_CANCELED);
            finish();
            return;
        }
        // 跳转参数设置
        Intent nextActivity = new Intent(intent);
        nextActivity.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
        nextActivity.putExtra(PackageInstallerActivity.EXTRA_CALLING_PACKAGE, callingPackage);
        nextActivity.putExtra(PackageInstallerActivity.EXTRA_ORIGINAL_SOURCE_INFO, sourceInfo);
        nextActivity.putExtra(Intent.EXTRA_ORIGINATING_UID, originatingUid);
        // 当前 action 是否为 PackageInstaller.ACTION_CONFIRM_PERMISSIONS
        if (isSessionInstall) {
            nextActivity.setClass(this, PackageInstallerActivity.class);
        } else {
            Uri packageUri = intent.getData();
            // 判断 Uri 的 Scheme 协议
            if (packageUri != null && packageUri.getScheme().equals(
                    ContentResolver.SCHEME_CONTENT)) {
                nextActivity.setClass(this, InstallStaging.class);
            } else if (packageUri != null && packageUri.getScheme().equals(
                    PackageInstallerActivity.SCHEME_PACKAGE)) {
                nextActivity.setClass(this, PackageInstallerActivity.class);
            } else {
                // 安装失败
                Intent result = new Intent();
                result.putExtra(Intent.EXTRA_INSTALL_RESULT,
                        PackageManager.INSTALL_FAILED_INVALID_URI);
                setResult(RESULT_FIRST_USER, result);
                nextActivity = null;
            }
        }
        // 跳转并关闭界面
        if (nextActivity != null) {
            startActivity(nextActivity);
        }
        finish();
    }

在 Android 7.0 以上,根据以上代码,当 Uri 的 Scheme 协议属于 content 协议时,跳转进入InstallStaging 界面,在这个里面主要是将传入的 content 协议转为 file 协议

scala 复制代码
public class InstallStaging extends AlertActivity {

    private final class StagingAsyncTask extends AsyncTask<Uri, Void, Boolean> {

        @Override
        protected void onPostExecute(Boolean success) {
            if (success) {
                // Now start the installation again from a file
                Intent installIntent = new Intent(getIntent());

                // Android 9.0 以下 会直接跳转 PackageInstallerActivity 
                // Android 9.0 之后 会先进 DeleteStagedFileOnResult 在并在进入 PackageInstallerActivity 之后通知它将 file 协议删除
                installIntent.setClass(InstallStaging.this, DeleteStagedFileOnResult.class);
                installIntent.setData(Uri.fromFile(mStagedFile));
                if (installIntent.getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false)) {
                    installIntent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
                }
                installIntent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
                startActivity(installIntent);
                InstallStaging.this.finish();
            } else {
                showError();
            }
        }
    }
}

可以看到不同的 Android 版本对安装 apk 的过程越来越复杂,不过最后你会发现,还是会走到 PackageInstallerActivity 这个类。

PackageInstallerActivity 的 onCreate

该界面才是真正安装的界面,在 onCreate 方法会初始化安装需要的对象信息、通过 Uri 创建一个 apk 文件、并解析 apk 文件得到包信息

ini 复制代码
    @Override
    protected void onCreate(Bundle icicle) {
        ...
        // 初始化安装需要的对象信息
        mPm = getPackageManager();
        mIpm = AppGlobals.getPackageManager();
        mAppOpsManager = (AppOpsManager) getSystemService(Context.APP_OPS_SERVICE);
        mInstaller = mPm.getPackageInstaller();
        mUserManager = (UserManager) getSystemService(Context.USER_SERVICE);

        ...

        // 根据 Uri 的 Scheme 进行预处理
        boolean wasSetUp = processPackageUri(packageUri);
        if (!wasSetUp) {
            return;
        }

        // 判断是否是未知来源的应用
        checkIfAllowedAndInitiateInstall();
    }

其中在初始化安装对象信息后,会根据根据 Uri 的 Scheme 进行预处理,在 processPackageUri 方法中主要根据 apk 路径来创建 apk 文件,并解析 apk 包的信息,并通过 PackageParser 类重新生成一个对象 PackageInfo,用来保存包信息。

最后的 checkIfAllowedAndInitiateInstall 方法主要功能是判断是否是未知来源的应用,它会根据 Intent 判断得出该 APK 不是未知来源,从而进入初始化安装方法。

在这之后会弹出安装确定的提示,点击确定按钮就会执行真正安装的方法 startInstall。启动实际安装 apk 的类 InstallInstalling

InstallInstalling 界面

在这个界面里面,主要处理安装的代码在 onResume 方法中,它会开启一个内部的异步任务

scala 复制代码
    private final class InstallingAsyncTask extends AsyncTask<Void, Void,
            PackageInstaller.Session> {
        @Override
        protected PackageInstaller.Session doInBackground(Void... params) {
            ...
            // 将 apk 的信息通过 IO 流的形式写入到 PackageInstaller.Session 中
        }

        @Override
        protected void onPostExecute(PackageInstaller.Session session) {
            if (session != null) {
                ...
                // 调用 PackageInstaller.Session 的 commit 方法,将 apk 的信息交由 PMS 处理
                PendingIntent pendingIntent = PendingIntent.getBroadcast(
                        InstallInstalling.this,
                        mInstallId,
                        broadcastIntent,
                        PendingIntent.FLAG_UPDATE_CURRENT);

                session.commit(pendingIntent.getIntentSender());
            }
        }
    }

可以看到在这 apk 的安装最终是调用 PackageInstaller.Session 的 commit 方法,将 apk 的信息交由 PMS 处理,最后安装成功后会进入回调,进入 launchSuccess 方法,进入 InstallSuccess 界面,代表安装成功。

到这就完成了有安装界面的安装方式。

到此我们可以看到 PackageInstaller.Session 将 apk 的信息交给了 PMS 处理,接下来看一下 PMS 是如何将其安装到手机设备中的。

PMS 安装过程概览

之前我们分析道当点击安装后,PackageInstaller.Session 触发 commit方法,实际是在 PackageInstallerSession 处理的,在 PackageInstallerSession 的 commit 方法中跟进,最终发现调用 handleInstall 的方法,来看这个方法内:

java 复制代码
    private void handleInstall() {
        ...
        try {
            synchronized (mLock) {
                installNonStagedLocked(childSessions);
            }
        } catch (PackageManagerException e) {
            ...
        }
    }

    @GuardedBy("mLock")
    private void installNonStagedLocked(List<PackageInstallerSession> childSessions)
            throws PackageManagerException {
        ...
        mPm.installStage(installingSession);
    }

里面的 mPm 就是系统服务 PackageManagerService。installStage 方法可以分为两大步:

  • 拷贝安装包;
  • 装载代码。

拷贝安装包

从 installStage 方法开始看起,代码如下:

java 复制代码
    void installStage(List<ActiveInstallSession> children)
            throws PackageManagerException {
        // 创建了类型为 INIT_COPY 的 Message
        final Message msg = mHandler.obtainMessage(INIT_COPY);
        // 创建 MultiPackageInstallParams,并传入安装包的相关数据
        final MultiPackageInstallParams params =
                new MultiPackageInstallParams(UserHandle.ALL, children);
        ...
        mHandler.sendMessage(msg);
    }

可以看到其中主要是创建 INIT_COPY 的消息和 MultiPackageInstallParams,它继承 HandlerParams ,主要是传入安装包的相关数据。

Message 发送出去之后,由 PMS 的内部类 PackageHandler 接收并处理,如下:

scala 复制代码
class PackageHandler extends Handler {
    void doHandleMessage(Message msg) {
        switch (msg.what) {
            case INIT_COPY: {
                HandlerParams params = (HandlerParams) msg.obj;
                if (params != null) {
                    if (DEBUG_INSTALL) Slog.i(TAG, "init_copy: " + params);
                    Trace.asyncTraceEnd(TRACE_TAG_PACKAGE_MANAGER, "queueInstall",
                            System.identityHashCode(params));
                    Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "startCopy");
                    params.startCopy();
                    Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
                }
                break;
            }
        }
    }
}

主要调用 HandlerParams 的 startCopy 方法来进行参数的拷贝。在 startCopy 方法中会继续调用一个抽象方法 handleStartCopy 处理安装请求。通过之前的分析,我们知道 HandlerParams 实际类型是 InstallParams 类型,因此最终调用的是 InstallParams 的 handlerStartCopy 方法,

InstallParams 的 handlerStartCopy 方法

这个方法是整个安装包拷贝的核心方法,具体如下:

scss 复制代码
public void handleStartCopy() {
    int ret = PackageManager.INSTALL_SUCCEEDED;
    // 设置安装标志位,决定是安装在手机内部存储空间还是 sdcard 中
    if (origin.staged) {
        if (origin.file != null) {
            installFlags |= PackageManager.INSTALL_INTERNAL;
        } else {
            throw new IllegalStateException("Invalid stage location");
        }
    }
    ...
    final InstallArgs args = createInstallArgs(this);
    ...
}

void handleReturnCode() {
    if (mVerificationCompleted
            && mIntegrityVerificationCompleted && mEnableRollbackCompleted) {
        ...
        if (mRet == PackageManager.INSTALL_SUCCEEDED) {
            mRet = mArgs.copyApk();
        }
        processPendingInstall(mArgs, mRet);
    }
}

在这主要是判断 apk 安装位置,如果安装位置合法,则执行 createInstallArgs 方法,创建一个 InstallArgs,实际上是其子类 FileInstallArgs 类型,然后在 handleReturnCode 方法中调用其 FileInstallArgs 的 copyApk 方法进行安装包的拷贝操作。

FileInstallArgs 的 copyApk 方法

scala 复制代码
class FileInstallArgs extends InstallArgs {
        FileInstallArgs(InstallParams params) {
            super(params);
        }

        int copyApk() {
            try {
                return doCopyApk();
            } finally {
                Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
            }
        }

        private int doCopyApk() {
            ...
            try {
                // 创建存储安装包的目标路径,实际上是 /data/app/ 应用包名目录
                final boolean isEphemeral = (installFlags & PackageManager.INSTALL_INSTANT_APP) != 0;
                final File tempDir =
                        mInstallerService.allocateStageDirLegacy(volumeUuid, isEphemeral);
            } catch (IOException e) {
            }
            // 调用服务的 copyPackage 方法将安装包 apk 拷贝到目标路径中
            int ret = PackageManagerServiceUtils.copyPackage(
                    origin.file.getAbsolutePath(), codeFile);
            ...
            try {
                handle = NativeLibraryHelper.Handle.create(codeFile);
                // 将 apk 中的动态库 .so 文件也拷贝到目标路径中
                ret = NativeLibraryHelper.copyNativeBinariesWithOverride(handle, libraryRoot,
                        abiOverride, isIncremental);
            } catch (IOException e) {
            } 
            return ret;
        }

可以看出在 copyApk 方法中调用了 doCopyApk 方法,doCopyAPk 方法中主要做了 3 件事情:

  • 创建存储安装包的目标路径,实际上是 /data/app/ 应用包名目录;
  • 调用服务的 copyPackage 方法将安装包 apk 拷贝到目标路径中;
  • 将 apk 中的动态库 .so 文件也拷贝到目标路径中。

而 copyPackage 方法本质上就是执行 IO 流操作,具体如下:

arduino 复制代码
    public static int copyPackage(String packagePath, File targetDir) {
        try {
            ...
            copyFile(pkg.baseCodePath, targetDir, "base.apk");
        } catch (PackageParserException | IOException | ErrnoException e) {
        }
    }

    private static void copyFile(String sourcePath, File targetDir, String targetName)
            throws ErrnoException, IOException {
        ...
        try {
            source = new FileInputStream(sourcePath);
            FileUtils.copy(source.getFD(), targetFd);
        } finally {
            IoUtils.closeQuietly(source);
        }
    }

最终安装包在 data/app 目录下以 base.apk 的方式保存,至此安装包拷贝工作就已经完成。

装载代码

代码拷贝结束之后,就开始进入真正的安装步骤。

代码回到上述的 HandlerParams 中的 handleReturnCode 方法:

可以看出当安装包拷贝操作结束之后,继续调用 processPendingInstall 方法处理安装过程,具体如下:

scss 复制代码
    private void processPendingInstall(final InstallArgs args, final int currentStatus) {
        ...
            processInstallRequestsAsync(
                    res.returnCode == PackageManager.INSTALL_SUCCEEDED,
                    Collections.singletonList(new InstallRequest(args, res)));
    }

private void processInstallRequestsAsync(boolean success,
            List<InstallRequest> installRequests) {
        mHandler.post(() -> {
            if (success) {
                // 执行预安装操作,主要是检查安装包的状态,确保安装环境正常,如果安装环境有问题会清理拷贝文件。
                for (InstallRequest request : installRequests) {
                    request.args.doPreInstall(request.installResult.returnCode);
                }
                // 真正的安装阶段,installPackageTraceLI 方法中添加跟踪 Trace,然后调用 installPackageLI 方法进行安装。
                synchronized (mInstallLock) {
                    installPackagesTracedLI(installRequests);
                }
                // 处理安装完成之后的操作
                for (InstallRequest request : installRequests) {
                    request.args.doPostInstall(
                            request.installResult.returnCode, request.installResult.uid);
                }
            }
            for (InstallRequest request : installRequests) {
                restoreAndPostInstall(request.args.user.getIdentifier(), request.installResult,
                        new PostInstallData(request.args, request.installResult, null));
            }
        });
    }

    private void installPackagesTracedLI(List<InstallRequest> requests) {
        try {
            installPackagesLI(requests);
        } finally {
            Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
        }
    }

上述代码详细说明了 processInstallRequestsAsync 异步处理安装的操作,其中 installPackagesLI 是 apk 安装阶段的核心代码,核心代码如下:

java 复制代码
private void installPackagesLI(List<InstallRequest> requests) {
    try {
        for (InstallRequest request : requests) {
            final PrepareResult prepareResult;
            try {
                prepareResult =
                        preparePackageLI(request.args, request.installResult);
            } catch (PrepareFailure prepareFailure) {
            }
            try {
                final ScanResult result = scanPackageTracedLI(
                        prepareResult.packageToScan, prepareResult.parseFlags,
                        prepareResult.scanFlags, System.currentTimeMillis(),
                        request.args.user, request.args.abiOverride);
                ...
            } catch (PackageManagerException e) {
            }
        }
        // 如果安装成功,更新系统设置中的应用信息,比如应用的权限信息。
        executePostCommitSteps(commitRequest);
        
    }
}

这里面就两个核心方法,preparePackageLI 和 scanPackageTracedLI

  • preparePackageLI 方法调用 PackageParser 的 parsePackage 方法解析 apk 文件,主要是解析 AndroidManifest.xml 文件,将结果记录在 PackageParser.Package 中。我们在清单文件中声明的 Activity、Service 等组件就是在这一步中被记录到系统 Framework 中,后续才可以通过 startActivity 或者 startService 启动相应的活动或者服务。
  • scanPackageTracedLI 方法主要是扫描解析 apk 安装包文件,保存 apk 相关信息到 PMS 中,并创建 apk 的 data 目录,具体路径为 /data/data/应用包名

至此整个 apk 的安装过程结束。

总结

总的来说,Android 点击 apk 安装时,最后都会启动 PackageInstallerActivity 来进行信息的处理以及将 apk 的信息传递给 PackageManagerService(PMS),之后 PMS 通过执行拷贝安装包、拷贝安装包和其他异常处理来保证安装 apk 时将 apk 文件正确的拆解并放在对应的目录。

  • data/app/包名
    • 安装时把 apk 文件复制到此目录。
  • data/data/包名
    • 开辟存放应用程序的文件数据的文件夹,包括应用的so库,缓存文件等。
  • data/dalvik-cache
    • 将apk中的dex文件安装到此处。
相关推荐
拭心9 小时前
Google 提供的 Android 端上大模型组件:MediaPipe LLM 介绍
android
带电的小王11 小时前
WhisperKit: Android 端测试 Whisper -- Android手机(Qualcomm GPU)部署音频大模型
android·智能手机·whisper·qualcomm
梦想平凡12 小时前
PHP 微信棋牌开发全解析:高级教程
android·数据库·oracle
元争栈道12 小时前
webview和H5来实现的android短视频(短剧)音视频播放依赖控件
android·音视频
阿甘知识库13 小时前
宝塔面板跨服务器数据同步教程:双机备份零停机
android·运维·服务器·备份·同步·宝塔面板·建站
元争栈道13 小时前
webview+H5来实现的android短视频(短剧)音视频播放依赖控件资源
android·音视频
MuYe14 小时前
Android Hook - 动态加载so库
android
居居飒14 小时前
Android学习(四)-Kotlin编程语言-for循环
android·学习·kotlin
Henry_He17 小时前
桌面列表小部件不能点击的问题分析
android
工程师老罗17 小时前
Android笔试面试题AI答之Android基础(1)
android