查看原文
其他

Apk安装之谜

牛晓伟 郭霖
2024-08-23


/   今日科技快讯   /

《财富》世界500强榜单正式揭晓,拼多多(PDD,股价138.04美元,市值1917.1亿美元)作为中国互联网领域的新生力量,首次跻身该榜单。同时,拼多多创始人黄峥以486亿美元的身价登顶中国首富。这不仅意味着个人财富的积累,也意味着拼多多企业品牌的成功塑造。

/   作者简介   /

本篇文章来自牛晓伟的投稿,文章主要分享了Apk安装过程的源码解析,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。

原文地址:
https://mp.weixin.qq.com/s/kCsIVgorKOp-ensI27VMMQ

/   开场白   /

“大家好,我是今天的主角apk,今天的给大家带来的主题是apk安装之谜,我请到了PackageManagerService、Settings、PackageInstallerSession、PackageInstallerService、InstallPackageHelper、Installer作为嘉宾,那就让嘉宾先做下自我介绍吧。”

PackageManagerService:“大家对我一定非常的了解了,我是一个服务管理所有的已经安装的apk,运行于systemserver进程,可以称呼我PMS哦。”

Settings:“大家好啊,看到我的名字有可能会有人认为我是为设置app服务的,其实不然,我是为apk的安装服务的,哪些apk安装了、安装的时间信息等等我都保存着,并且会持久化到内部存储空间。”

PackageInstallerService:“大家好,我和PMS一样也是运行于systemserver进程的,一看我的名字就能知道我是和apk安装有关系的,如果谁有安装apk的需求,可以直接通过binder的方式'呼我哦',可以称呼我PIS哦。”

PackageInstallerSession:“大家好,我的名字和PIS是不是很像啊,可别听PIS忽悠啊,真正进行apk安装的工作都是由我完成的,可以称呼我为Session哦。”

InstallPackageHelper:“大家好,我的主要工作是负责apk安装中期的工作,后面到了我的工作内容的时候,会着重在介绍。”

Installer:“大家好,我是installd进程的代理,在java世界如果需要使用installd的能力的话,直接调用我即可,我会把相关的请求‘转告’给installd,我在apk的安装中也起了很大的作用。”

既然嘉宾都介绍完自己了,因为今天我是主角,我有主角光环,那我非常有必要隆重、浓墨重彩的介绍下我自己,让大家对我有一个非常深刻的了解。

/   我是apk   /

apk它是 Android Package 的缩写。我是一个zip格式的压缩文件,只不过为了能让大家从文件名上一眼认出我来,我的文件后缀是 .apk。

一个apk内主要包含了dex文件、so文件、res目录、resources.arsc、META-INF目录(它里面的CERT.SF、CERT.RSA文件主要是和签名证书有关的) 、AndroidManifest.xml等文件和目录,对于dex等文件大家肯定都熟悉了,我就不在这赘述了,我着重来介绍下AndroidManifest.xml(Android清单文件)。

估计会有人说AndroidManifest文件有啥好说的,我们都知道它,它会把定义了的四大组件及Application声明在内,同时声明需要用到的权限、meta-data等信息。这么简单的大家都熟知的知识就不用介绍了。但是我想要介绍的是为啥要有AndroidManifest以及它是被谁使用的?

拿你们人类去餐馆吃饭作对比,去餐馆吃饭的时候会有菜单,菜单的作用就是告诉顾客我餐馆都提供哪些菜品,对于这些菜是由哪个厨师加了哪些配料做成的,顾客都不需要关心。而AndroidManifest的作用如菜单一样,它会把自己apk声明的信息展示出来,餐馆的菜单是展示给顾客的,而AndroidManifest是展示给PMS,也就是说PMS相关的解析代码会从apk的AndroidManifest中解析出所有的信息,从而知道apk声明了哪些四大组件、apk的包名是啥、声明了哪些权限等信息。AndroidManifest的作用就是告诉PMS:如果想了解我都声明了哪些四大组件,以及声明了哪些权限、meta-data等,直接读我都可以知道。

我的“归宿”

我的“归宿”就是成功的安装到各种安卓设备上某个目录,只有在那里我才能在这台设备上充分的发挥我的价值。

像人类一样有三六九等之分,而apk也是存在不同类别划分的,大致划分为系统apk(如launcher、dialer、setting等)和普通apk(如微信、抖音)。当然系统apk还可以进一步的划分为核心系统apk、厂商apk等。不同类型的apk,它们所在的父目录也是不同的,系统apk它们的父目录是 /system/priv-app、/system/app 等,而普通apk它们的父目录是 /data/app。如下:

drwxr-xr-x 39 root root   4096 2023-08-20 21:26 system/priv-app
drwxr-xr-x 24 root root   4096 2023-08-20 21:25 system/app

/system/priv-app和/system/app目录,它们的user和group都是 root,也就是只有root用户可以对这俩目录进行读写执行操作,其他用户只有执行权限。关于/data/app目录的详细信息会在下面介绍。

你们人类有句谚语:条条大路通罗马,而有的人却生在罗马。系统apk,它们就是出生在“罗马”,一“出生“就有“归宿”。而作为普通apk的我,却是在通往“罗马”的路上,因为我一“出生“会被放置于服务器上或者电脑上的某个黑暗的目录,如果想要到达我的“罗马”就需要通过apk安装把我安装到设备上。

关于我的介绍就到此吧,进入咱们今天的正题吧。

/   安装apk这件事   /

PMI:“因为我制定了安装apk相关的大体架构和流程上的事情,因此就由我来介绍下安装apk这件事情吧,关于具体实现细节还需要Session在后面介绍。”

apk的安装方式有 adb install命令、应用商店、安装apk的程序。它们的区别首先在于apk的来源不同:adb install的apk来源于电脑、应用商店的apk是应用商店从服务器下载成功后进行安装、通过安装apk的程序的apk来源于设备上已经存在的apk;其次是是否提供友好的交互界面。不管是哪种方式它们的安装流程基本上是一致的。

我把安装apk总结为三阶段:前期准备、安装、后期收尾。

前期准备的工作有拷贝、完整性校验、解析apk、提取native libs、版本号验证;安装的工作有准备 (Prepare) 、扫描 (Scan) 、调和 (Reconcile) 、提交 (Commit) ;后期收尾的工作有创建app data根目录、dex优化、移除已有apk、发送安装成功广播。

那我们就按上面的三阶段来给大家揭开apk安装的谜底吧。

/   前期准备   /

PIS:“谁要想安装apk,首先需要调用我的PackageInstallerService的createSession方法创建一个PackageInstallerSession,一次安装会对应唯一的一个PackageInstallerSession,PackageInstallerSession才是真正干活的主力,后面的安装环节就由PackageInstallerSession来给大家介绍了,PackageInstallerSession简称Session。”

Session:“我会给使用者一个sessionId,通过这个sessionId可以从PIS找到我,拷贝是安装apk的第一步,那我们就从拷贝开始。”

拷贝

Session:“安装apk第一步是需要把apk(不管apk来源于哪)进行拷贝,拷贝到 /data/app/xxxx.tmp(xxxx是一个随机的字符串)目录下面,拷贝的apk的名字一般被命名为:base.apk,拷贝完后的apk文件的路径是 /data/app/xxxx.tmp/base.apk 这样的。”

apk:“一上来就要拷贝,这一下子把我搞懵逼了,能说说为啥要拷贝吗?我的理解是拷贝会增加apk的安装时长,如果apk特别大,安装时长更会加长,不拷贝不行吗?”

Session:“不拷贝还真不行,那我就来说下原因。“

拿adb install或者应用市场安装apk的方式来说明问题吧,Session我是运行于systemserver进程。通过adb install安装的话,apk是位于pc上,pc上的apk对于Session是肯定不能拿来直接用的;通过应用市场安装的话,apk是被应用市场进程所存储的,而Session我也是基本不可以访问的(除非apk被下载到可共享的目录)。因此我需要先把apk拷贝到我可以访问的目录下面,这样我就可以直接操作apk了。

apk:“我同意你的说法,对于adb install安装确实需要拷贝,因为apk是存储于pc上。但是通过应用市场安装是不是可以这样做:就是应用市场在从服务器下载apk的时候直接下载到一个约定好的目录中,Session你可以直接从这个目录来操作apk了,这样就不需要拷贝的过程了,安装速度肯定可以提升。”

Session:“对于你的提议是存在几个问题,约定好的目录这个目录应该是一个共享目录吧,第一个问题是:怎么样做到只有我和应用市场进程才能访问这个目录?第二个问题是:即使可以做到共享还需要对该目录进行保护,在安装期间应用市场进程是不可以对该目录进行任何修改的,也就是在安装期间只有Session我才可以操作这个共享目录。解决上面的两个问题是不是比较麻烦啊。”

apk:“说的极是,我确实没想到你说的这些问题。”

还有非常重要的一点,拷贝到的 /data/app/xxxx.tmp 目录,这个目录有一个非常重要的特性,这个目录的user和group都是system,也就是只有systemserver进程对此目录具有读写执行权限,而其他进程只有读权限,这样就可以保证被拷贝apk的安全性了,只有Session我才可以访问、修改该目录。(如下图)


对apk拷贝可以调用我的write方法,调用我所有的方法都需要进行binder跨进程调用。

该步的产物是 /data/app/xxxx.tmp/base.apk ,后面的安装流程都基于此apk进行。关于拷贝就介绍到此,咱们接着介绍完整性验证。

完整性验证

Session:“第二步是对 /data/app/xxxx.tmp/base.apk 进行完整性验证。完整性验证用一句话概括就是:验证apk有没有被改过。这一步肯定是要最先进行的,只有我先确认apk是一个完整的apk才有必要进行后面的安装流程。“

apk问到:“apk被改动会存在哪些危害?”

Session:“比如有个高人下载了微信的apk,抛开加固等黑科技,这位高人解压了微信apk,并且在其中插入了自己的代码(比如把聊天信息上传到自己的服务器上)在重新打包成微信apk,那这个时候的微信apk被用户安装上的话,你可以想想这有多危险,用户和别人的聊天信息他都可以知道了。apk完整性验证就是要验证apk有没有被改过,改过的话那就完全认为这个apk是被动过手脚的,肯定不允许安装的。”

apk:“那又是如何能验证apk没有被改动过呢?”

Session:"我先从雏形说起,这样可以更容易理解从雏形到最终方案是如何一步一步形成的。刚开始的验证雏形是这样的:我Session需要从apk内拿到一个信息,这个信息是与apk是一一对应关系,也就是apk内不管发生任何变化,那这个信息也需要发生变化,并且我需要根据apk能推导或计算出这个信息,如果推导或计算的信息与apk内拿到的信息一致就可以证明apk是没有被修改过的。那怎么样可以做到呢?答案是使用摘要算法。“

apk迷惑的问到:“摘要算法,这又是啥子嘛?”

举个例子人类读完一篇文章后,这篇文章总会有个中心思想之类的总结,那这个总结就是一个摘要。

摘要算法:会接受一个输入,不论输入的内容是多长都会输出一个固定长度的内容,输入内容一样才会有一样的输出,输入内容不一样输出内容也不会一样,并且这个输出内容是不可逆的。

可以使用摘要算法对apk的各种文件生成摘要,这些摘要信息会写入apk。验证apk完整性的进一步思路是这样的:使用摘要算法对apk的各种文件生成摘要,如果生成的摘要与apk内存的摘要信息一致则证明apk是没有被修改过的。

Session:"apk老兄,你觉得上面的的思路有啥问题吗?“

apk:“我想想啊,想到了,这些摘要信息没有加密,如若改动了apk内的内容,则也可以重新把摘要内容改了,重新打包到apk内。因此需要对摘要信息进行加密。”

你说的非常的对,对摘要信息进行加密需要用到非对称加密(https中就用到它),非对称加密是一种加密算法,分为公钥和私钥,公钥是可以公开的,私钥是不能公开的,用私钥对信息加密,是可以用公钥解密的。需要用私钥对摘要信息进行加密,把加密后的摘要信息和证书(证书存储了公钥和开发者的一些信息)一同打包到apk内。我把这个过程起了一个很好听的名字apk签名,就如人类在合同上签名一样,每个apk也是需要签名的,签了名后这个apk就和开发者绑定了。

总结下apk签名的过程:首先用摘要算法对apk内的各种文件生成摘要;其次使用非对称加密的私钥对这些摘要信息加密;最后把加密的摘要信息和证书(公钥和开发者信息)写入apk内。这只是对apk v1签名算法的一个简单总结,签名算法有v1、v2、v3、v4四个版本,每个版本都是为了解决前者存在的问题而诞生的。

基于apk签名,终极apk完整性验证流程如下(下面主要介绍的是签名v1版本的验证流程):

  1. 从apk中拿到证书信息,拿到加密的摘要信息
  2. 从证书中用公钥对加密的摘要信息解密,解密出摘要信息
  3. 对apk的各文件用摘要算法生成摘要,并与解密出的摘要信息进行对比,如若一致则证明没有被改动,否则发生了改动。

除了验证apk的完整性外,还会从apk中的提取签名信息,签名信息保存在SigningDetails对象中,在后面的安装流程中是要用到SigningDetails信息的,如果apk没有获取到签名信息,则会停止安装(正常咱们开发的debug版的app是已经默认进行了签名)。

apk完整性验证是安装的必要环节,如果apk完整性验证失败,则停止安装;否则继续下一步的安装流程。该步的产物是SigningDetails对象以及apk是否完整的,SigningDetails对象会在后面的安装流程用到。

完整性验证的部分代码如下,有兴趣可以看下:

drwxr-xr-x 39 root root   4096 2023-08-20 21:26 system/priv-app
//文件路径:frameworks/base/core/java/android/content/pm/parsing/FrameworkParsingPackageUtils.java

//获取签名信息,签名信息存储在SigningDetails
public static ParseResult<SigningDetails> getSigningDetails(ParseInput input,
            String baseCodePath, boolean skipVerify, boolean isStaticSharedLibrary,
            @NonNull SigningDetails existingSigningDetails, int targetSdk) {

        省略代码......

        //跳过验证,走这
        if (skipVerify) {
            省略代码......
        } else {
            //验证并且返回签名信息,会对apk的完整性进行校验,并且返回签名信息
            verified = ApkSignatureVerifier.verify(input, baseCodePath, minSignatureScheme);
        }

        ......省略代码
    }

解析apk

Session:“有没有发现,前两步我对于安装的apk是知之甚少的,我不知道安装apk的包名、它的名字、版本号等基础信息。但是这些信息是非常非常重要的,因此这一步需要把这些信息解析出来,为后面的安装流程做准备。我把这一步称为解析apk,解析apk说的更具体点就是解析apk中的AndroidManifest(清单文件),从AndroidManifest文件中把包名、版本号、安装路径、是否是debug、是否是多架构、是否提取native libs等信息提取出来放入PackageLite对象。并不会提取四大组件信息、权限等信息,因为还暂时用不到这些信息,多解析这些信息就需要多花时间,我秉持一个用时才去解析的原则。”

该步的产物是PackageLite对象,该步的产物会在后面的安装流程用到,下一步就需要做提取native libs的操作。

PackageLite类的关键属性如下,有兴趣可以看下:

//文件路径: frameworks/base/core/java/android/content/pm/parsing/PackageLite.java
public class PackageLite {
    //包名
    private final @NonNull String mPackageName;

    //base apk的路径
    private final @NonNull String mBaseApkPath;

    //版本号
    private final int mVersionCode;

    //app是否是debug版本
    private final boolean mDebuggable;

    //是否是多架构(32位和64位)
    private final boolean mMultiArch;

    //是否提取native libs
    private final boolean mExtractNativeLibs;


    ......省略其他属性
}

提取native libs

Session:“这一步所要做的事情是提取native libs(native libs指的是apk中的so库),提取native libs:也就是把apk中的so库提取到 /data/app/xxxx.tmp/lib/cpuabi/ (cpuabi是当前设备的cpu架构比如arm、arm64)目录下面。abi是Application Binary Interface的缩写,应用程序二进制接口。但是并不是所有的apk都包含了so,如果没有包含则不会执行此步。提取native libs会用到解析apk这一步解析出的PackageLite信息。”

apk中so库的所处的目录如下:

//base.apk,该apk中包含了两个abi:arm、arm64,(为了减小apk的大小,现在的apk都只保留一个abi)
lib/arm/xx.so
lib/arm/xxx.so
lib/arm/xxxx.so

lib/arm64/xx.so
lib/arm64/xxx.so
lib/arm64/xxxx.so

在提取native libs的时候,会检测apk中的cpu abi是否与当前设备的cpu abi是否匹配,如果不匹配比如当前设备cpu abi是x86_64的,而apk中的cpu abi只有arm、arm64,那这种情况肯定是不能继续安装的,因为so库是与cpu abi强相关的,arm下面的so库在x86_64上面运行肯定是出问题的。为了考虑性能和方便性,整个提取native libs都是委托给native的代码执行的。

提前native libs可以提前检测当前设备的cpu abi是否与apk中的so库匹配,不匹配则不安装,并且还可以提升app的启动速度,如果不提取的话,每次app启动都需要从apk中解析出这些so库,这速度肯定慢啊,该步的产物是apk中的so库提取到 /data/app/xxxx.tmp/lib/cpuabi/ 目录下面。下一步就来看下版本号验证吧。

对应的代码如下,有兴趣可以看下:

//文件路径:frameworks/base/services/core/java/com/android/server/pm/PackageInstallerSession.java

private void parseApkAndExtractNativeLibraries() throws PackageManagerException {
        synchronized (mLock) {
            省略代码......

            final PackageLite result;
            if (!isApexSession()) {
                //走这,解析apk信息
                result = getOrParsePackageLiteLocked(stageDir, /* flags */ 0);
            } else {
                result = getOrParsePackageLiteLocked(mResolvedBaseFile, /* flags */ 0);
            }
            if (result != null) {
                mPackageLite = result;
                if (!isApexSession()) {
                    省略代码......
                    //提取so库
                    extractNativeLibraries(
                            mPackageLite, stageDir, params.abiOverride, mayInheritNativeLibs());
                }
            }
        }
    }

版本号验证

Session:“到了版本号验证这一步了,但是这步不是必须,如果设备上已经安装了相同包名的apk,则该步是必须的,版本号验证所要做的事情非常简单:正在安装的apk的版本号是否比已经安装的apk的版本号小,小的话就停止安装。正在安装的apk的版本号从解析apk中的PackageLite拿到。”

对应的代码如下,有兴趣可以看下:

//文件路径:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java
Pair<Integer, String> verifyReplacingVersionCode(PackageInfoLite pkgLite,
            long requiredInstalledVersionCode, int installFlags) {
        if ((installFlags & PackageManager.INSTALL_APEX) != 0) {
            return verifyReplacingVersionCodeForApex(
                    pkgLite, requiredInstalledVersionCode, installFlags);
        }

        String packageName = pkgLite.packageName;
        synchronized (mPm.mLock) {

            省略代码......

            //dataOwnerPkg代表设备已经安装对应的apk了
            if (dataOwnerPkg != null && !dataOwnerPkg.isSdkLibrary()) {
                //只有debug版本才允许版本降级
                if (!PackageManagerServiceUtils.isDowngradePermitted(installFlags,
                        dataOwnerPkg.isDebuggable())) {
                    try {
                        //检测是否存在版本降级,是的话会报错
                        PackageManagerServiceUtils.checkDowngrade(dataOwnerPkg, pkgLite);
                    } catch (PackageManagerException e) {
                        String errorMsg = "Downgrade detected: " + e.getMessage();
                        Slog.w(TAG, errorMsg);
                        return Pair.create(
                                PackageManager.INSTALL_FAILED_VERSION_DOWNGRADE, errorMsg);
                    }
                }
            }
        }
        return Pair.create(PackageManager.INSTALL_SUCCEEDED, null);
    }

总结

前期准备阶段又划分为拷贝、完整性验证、解析apk、提取native libs、版本号验证五步,每一步都在为后一步做准备。

拷贝会把安装的apk拷贝到/data/app/xxxx.tmp/base.apk。

完整性验证会对/data/app/xxxx.tmp/base.apk进行验证,如果修改过则停止安装,同时还会提取签名信息到SigningDetails对象,如果apk没有签名信息则会停止安装,SigningDetails对象会在后面的安装流程用到。

解析apk会从/data/app/xxxx.tmp/base.apk的AndroidManifest中把包名、版本号、安装路径、是否是debug等信息提取出来放入PackageLite对象,若解析中发生错误也会停止安装。

提取native libs的时候会用到 PackageLite对象,会把/data/app/xxxx.tmp/base.apk中的so库提取到 /data/app/xxxx.tmp/lib/cpuabi/ 目录(若apk存在so库),若发生错误则也会停止安装。

版本号验证的工作内容是正在安装的apk的版本号是否比已经安装的apk的版本号小,小的话就停止安装。这步不是必须的,只有设备上已经安装了相同包名的apk才执行。

前期准备的各步都能正常执行的话,就进入正式的安装阶段,那我们来看下安装阶段的内容。

/   安装   /

Session:“具体安装阶段的工作内容由InstallPackageHelper来完成的,那就有请它来给大家介绍。”

InstallPackageHelper:“大家好啊,终于轮到我出场了,安装阶段也可以称为正式安装,在这阶段才真正开始apk的安装工作。那我就来介绍下吧。”

安装阶段可以分为四步:准备 (Prepare) 、扫描 (Scan)、调和 (Reconcile)、提交 (Commit),这四步整体是原子化操作,也就是只要有一个出问题,整体的安装就停止,下面就来介绍下这四步。

准备 (Prepare)

完全解析apk

还记得解析apk那步会把apk的基础信息存放到PackageLite对象吗,这只是解析了比较少的基础信息。完全解析apk就是从/data/app/xxxx.tmp/base.apk的AndroidManifest中把所有的信息都解析出来,包含了声明的四大组件、权限、meta-data、shareLibs等,这些信息会存放在ParsedPackage对象中,如果解析发生错误,则停止安装。解析出ParsedPackage后,后面的工作都是围绕ParsedPackage展开的。

保存签名

在完整性验证那步是保存了签名信息到SigningDetails对象的,如果SigningDetails不为null的话会把SigningDetails存入ParsedPackage中;否则从apk中解析出SigningDetails存入ParsedPackage。

签名验证

签名验证的工作内容是对正在安装的apk的证书信息与设备上已经安装的相同包名的apk的证书信息进行对比,如果不一致,则停止安装。如果设备上不存在相同包名的apk则这一步是不会进行的。比如设备上安装了微信,如果有一个apk它的包名与微信一样,签名肯定不一样的情况下。这时候往设备上安装此apk肯定是安装不上的。

权限验证

权限验证就是根据ParsedPackage里的getPermissions()方法获取的权限,来判断哪些权限是存在问题的,比如声明了只有系统app才能使用的权限,如果存在问题则停止安装。

重命名

还记得拷贝第一步的时候生成的临时目录 /data/app/xxxx.tmp/ 吗?这毕竟是个临时目录,是有必要给它一个正式的名字的,那重命名所做的事情就是把 /data/app/xxxx.tmp/ 重命名为 /data/app/~~[randomStrA]/[packageName]-[randomStrB](其中randomStrA、randomStrB是随机生成的字符串,packageName是包名),这个名字看上去确实不是很正规,但是它确实是一个非常正式的名字。

apk:“那我有个问题啊,为什么重命名的名字没有用包名,而是用一个随机字符串呢?”

InstallPackageHelper:“用随机字符串的原因是,在 /data/app/ 目录下面会存在两个同一包名apk的情况,如果用包名的话会出现问题。比如当前设备上已经安装了一个微信apk,则在 /data/app/com.weixin/ 目录下会存在微信的apk。这时候安装一个高版本的微信apk的,这时候重命名的话就出现问题,因为已经有com.weixin目录存在了。”

如果重命名失败也会停止安装。下面是重命名的例子,可以看到它们的user、group都是system。


如下正式apk父目录的相关代码,有兴趣可以看下。

//文件路径:services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
/**
    * 返回的目录结构样子:targetDir/~~[randomStrA]/[packageName]-[randomStrB]
     */
    public static File getNextCodePath(File targetDir, String packageName) {
        SecureRandom random = new SecureRandom();
        byte[] bytes = new byte[16];
        File firstLevelDir;
        do {
            random.nextBytes(bytes);
            String firstLevelDirName = RANDOM_DIR_PREFIX
                    + Base64.encodeToString(bytes, Base64.URL_SAFE | Base64.NO_WRAP);
            firstLevelDir = new File(targetDir, firstLevelDirName);
        } while (firstLevelDir.exists());

        random.nextBytes(bytes);
        String dirName = packageName + RANDOM_CODEPATH_PREFIX + Base64.encodeToString(bytes,
                Base64.URL_SAFE | Base64.NO_WRAP);
        final File result = new File(firstLevelDir, dirName);
        if (DEBUG && !Objects.equals(tryParsePackageName(result.getName()), packageName)) {
            throw new RuntimeException(
                    "codepath is off: " + result.getName() + " (" + packageName + ")");
        }
        return result;
    }

总结

当然除了上面的这些工作外,还做了尝试杀死当前同包名的app进程(如果设备上已经有相同包名的apk并且处于运行状态),构造需要移除的信息PackageRemovedInfo对象(如果设备上已经有相同包名的apk,则需要把它的信息在后面的流程中移除掉,因为这些信息毕竟是老apk的信息)。

准备阶段所做的主要事情有:把/data/app/xxxx.tmp/base.apk的AndroidManifest中把所有的信息都解析出来,存在ParsedPackage对象中,进行了签名、权限等验证,把/data/app/xxxx.tmp/目录重命名为 /data/app/~~[randomStrA]/[packageName]-[randomStrB] 目录。若准备阶段发生了错误,则会停止安装。准备阶段的产物是ParsedPackage(它在后面的安装流程会用到),咱们进入扫描阶段。

准备阶段对应的一部分源码如下(源码实在是太多了,只列出一部分),有兴趣可以看下:

private PrepareResult preparePackageLI(InstallArgs args, PackageInstalledInfo res)
            throws PrepareFailure {

   省略代码......
   final ParsedPackage parsedPackage;
   try (PackageParser2 pp = mPm.mInjector.getPreparingPackageParser()) {
        //完全解析apk
        parsedPackage = pp.parsePackage(tmpPackageFile, parseFlags, false); //niu 解析apk中更具体的信息 放入ParsedPackage
        AndroidPackageUtils.validatePackageDexMetadata(parsedPackage);
   } catch (PackageManagerException e) {
        throw new PrepareFailure("Failed parse during installPackageLI", e);
   } finally {
        Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
   }

   省略代码......

   //设置签名信息
   if (args.mSigningDetails != SigningDetails.UNKNOWN) {
        parsedPackage.setSigningDetails(args.mSigningDetails);
   } else {
        final ParseTypeImpl input = ParseTypeImpl.forDefaultParsing();
        final ParseResult<SigningDetails> result = ParsingPackageUtils.getSigningDetails(
                    input, parsedPackage, false /*skipVerify*/);
        if (result.isError()) {
           throw new PrepareFailure("Failed collect during installPackageLI",
                        result.getException());
           }
        parsedPackage.setSigningDetails(result.getResult());
    }
    省略代码......

}

扫描 (Scan)

InstallPackageHelper:“扫描这步主要的作用就是完善ParsedPackage的信息,同时用ParsedPackage的信息创建或者更新已有的PackageSetting。关于PackageSetting还需要有请Settings来介绍下。”

Settings:“在安装apk后,肯定需要把安装的apk相关的信息记录下来,这些信息比如包名、版本号、apk路径、native code路径、appid、签名相关信息等,这些信息都是与安装的apk是一一对应并且不会变化的。而还有一些信息是与当前设备的用户有关的(比如当前设备存在多用户),则是需要记录下每个用户是否安装了这个apk、安装apk的时间等信息。上面的这些信息肯定是需要记录并且需要持久化到内部存储空间的。这些信息被放在PackageSetting对象中,一个已安装的apk会对应自己的PackageSetting,也就是说PackageSetting存储了已安装apk相关的信息。而这些信息会最终持久化到packages.xml文件中。”

生成appId

每个被安装的apk都会有自己的appId,appId它是一个整数,如果在AndroidManifest中配置了android:sharedUserId则配置了相同sharedUserId的apk的appId是一样的。扫描的最后一步是为apk生成它的appId,这样被安装的apk就有了“正式身份”。

生成appId的代码如下,有兴趣看下:

//文件路径:Settings.java
boolean registerAppIdLPw(PackageSetting p, boolean forceNew) throws PackageManagerException {
        final boolean createdNew;
        Slog.i(TAG, "niulog install registerAppIdLPw p:" + p + " forceNew:" + forceNew + " appid:" + p.getAppId());
        if (p.getAppId() == 0 || forceNew) {
            // Assign new user ID
            p.setAppId(mAppIds.acquireAndRegisterNewAppId(p));
            createdNew = true;
        } else {
            // Add new setting to list of user IDs
            createdNew = mAppIds.registerExistingAppId(p.getAppId(), p, p.getPackageName());
        }
        if (p.getAppId() < 0) {
            PackageManagerService.reportSettingsProblem(Log.WARN,
                    "Package " + p.getPackageName() + " could not be assigned a valid UID");
            throw new PackageManagerException(INSTALL_FAILED_INSUFFICIENT_STORAGE,
                    "Package " + p.getPackageName() + " could not be assigned a valid UID");
        }
        return createdNew;
    }

扫描这步会为apk生成appId,同时会完善ParsedPackage的信息,扫描过程如果发生错误也会停止安装。这一步的产物是PackageSetting,它会被后面的安装流程用到。下面来介绍下调和这步。

调和 (Reconcile)

调和这步主要是利用准备、扫描的产物来验证当前apk使用到的shared libs是否存在、真实有效、是否重复申请等,如果验证失败则停止安装,比如在apk的AndroidManifest文件中用申明了一个不存在的lib则肯定是不能安装的;同时还会创建DeletePackageAction(它会把设备上相同包名的apk(称老apk)信息包含进来)如果设备上存在老apk,创建DeletePackageAction的目的是为了在后面的安装阶段可以把老apk的信息删除。

下面是它的源码(由于源码篇幅太长,只把方法名展示出来),有兴趣可以看下:

//文件名:ReconcilePackageUtils.java
public static Map<String, ReconciledPackage> reconcilePackages(
            final ReconcileRequest request, SharedLibrariesImpl sharedLibraries,
            KeySetManagerService ksms, Settings settings)
            throws ReconcileFailure {

            省略代码......
}

提交 (Commit)

InstallPackageHelper:“提交是安装的最后一步了,提交的主要工作内容就是对上面准备、扫描、调和的产物PackageSetting和ParsedPackage提交给Settings和PMS,让它们把各自更新自己的状态。那就由它们来介绍吧。”

Settings:“首先由我来介绍吧,调用我的insertPackageSettingLPw方法可以把PackageSetting和ParsedPackage更新我的mPackages属性 (它以包名为key,PackageSetting为value存放所有的已安装apk)。并且会把它们持久化到packages.xml文件,这样当下次设备重新启动的时候,就可以从packages.xml中把所有已安装apk的信息都读取到,每个已安装apk对应自己的PackageSetting,如果想知道当前设备已安装了哪些apk,可以‘呼我哦’。”

PMS:“该轮到我了,我有个非常重要的属性mPackages (它同样以包名为key,AndroidPackage为value存放所有已安装的apk,ParsedPackage是AndroidPackage的子类) ,我会把ParsedPackage添加到mPackages属性中。同时我还有个属性mComponentResolver,它可以把ParsedPackage中的四大组件‘收拢’起来。只有经过这些操作,在运行该apk的时候才能从我这检索到对应apk里面的四大组件信息,进而apk才能运行。”

总结

InstallPackageHelper郑重的对apk说:“恭喜你啊,经过安装阶段,你终于找到了你的‘归宿’ /data/app/~~[randomStrA]/[packageName]-[randomStrB] 目录,从此你就可以在这台设备上发挥你的价值了。这个目录它的user和group都是system,也就是说只有systemserver进程才有权读写执行该目录,而其他用户只能读的权限,这样就可以保证该目录的安全性。这也就是为啥在apk运行时候,是可以把该目录下的apk文件和lib下的各种so文件加载到自己进程的ClassLoader的原因。”

InstallPackageHelper又说:“虽然apk你找到了自己的‘归宿’,但是你的AndroidManifest声明的各种数据还没有传递给PMS,因为PMS是包管理者它管理着系统里的所有的apk信息,系统中谁想知道哪个apk安装了?哪个apk都声明了哪些组件?哪个apk声明了哪些权限等等这些信息都需要向PMS来要。因此需要把从apk中的AndroidManifest中解析出来的ParsedPackage信息传递给PMS,这样其他查询者比如ActivityManagerService就可以从PMS查到这些信息了。”

InstallPackageHelper:“系统里面安装了哪些apk,这都是需要记录并且持久化到内部存储空间的,而Settings就负责这个事件,新安装的apk会生成一个PackageSetting对象(它记录了apk的包名、版本号、签名信息、apk路径、哪些user安装了、安装时间等信息),PackageSetting对象会传递给Settings,Settings把它加入内存并且持久化到packages.xml文件中。”

下面是安装阶段的代码,有兴趣看下:

//文件:InstallPackageHelper.java
private void installPackagesLI(List<InstallRequest> requests) {
        final Map<String, ScanResult> preparedScans = new ArrayMap<>(requests.size());
        final Map<String, InstallArgs> installArgs = new ArrayMap<>(requests.size());
        final Map<String, PackageInstalledInfo> installResults = new ArrayMap<>(requests.size());
        final Map<String, PrepareResult> prepareResults = new ArrayMap<>(requests.size());
        final Map<String, Settings.VersionInfo> versionInfos = new ArrayMap<>(requests.size());
        final Map<String, Boolean> createdAppId = new ArrayMap<>(requests.size());
        boolean success = false;
        try {
            Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "installPackagesLI");
            for (InstallRequest request : requests) {
                // TODO(b/109941548): remove this once we've pulled everything from it and into
                //                    scan, reconcile or commit.
                final PrepareResult prepareResult;
                try {
                    Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "preparePackage");
                    //1.prepare阶段,会解析apk中的信息主要是AndroidManifest,解析出来的实体是ParsedPackage(解析的信息更全包含了四大组件等),若不是一个正确的apk则不会继续下面的步骤;若是正确的apk,则会对apk的签名、shareuserid以及是替换老apk还是新apk做处理
                    prepareResult =
                            preparePackageLI(request.mArgs, request.mInstallResult);
                } catch (PrepareFailure prepareFailure) {
                    request.mInstallResult.setError(prepareFailure.error,
                            prepareFailure.getMessage());
                    request.mInstallResult.mOrigPackage = prepareFailure.mConflictingPackage;
                    request.mInstallResult.mOrigPermission = prepareFailure.mConflictingPermission;
                    return;
                } finally {
                    Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
                }
                request.mInstallResult.setReturnCode(PackageManager.INSTALL_SUCCEEDED);
                request.mInstallResult.mInstallerPackageName =
                        request.mArgs.mInstallSource.installerPackageName;

                final String packageName = prepareResult.mPackageToScan.getPackageName();
                Slog.i(TAG,"niulog install installPackagesLI prepare request = "+request+" packageName = "+packageName);
                prepareResults.put(packageName, prepareResult);
                installResults.put(packageName, request.mInstallResult);
                installArgs.put(packageName, request.mArgs);
                try {
                    // 2.扫描阶段,扫描阶段主要是构造或者使用原有的PkgSetting
                    final ScanResult result = scanPackageTracedLI(
                            prepareResult.mPackageToScan, prepareResult.mParseFlags,
                            prepareResult.mScanFlags, System.currentTimeMillis(),
                            request.mArgs.mUser, request.mArgs.mAbiOverride);
                    if (null != preparedScans.put(result.mPkgSetting.getPkg().getPackageName(),
                            result)) {
                        request.mInstallResult.setError(
                                PackageManager.INSTALL_FAILED_DUPLICATE_PACKAGE,
                                "Duplicate package "
                                        + result.mPkgSetting.getPkg().getPackageName()
                                        + " in multi-package install request.");
                        return;
                    }
                    if (!checkNoAppStorageIsConsistent(
                            result.mRequest.mOldPkg, result.mPkgSetting.getPkg())) {
                        // TODO: INSTALL_FAILED_UPDATE_INCOMPATIBLE is about incomptabible
                        //  signatures. Is there a better error code?
                        request.mInstallResult.setError(
                                INSTALL_FAILED_UPDATE_INCOMPATIBLE,
                                "Update attempted to change value of "
                                        + PackageManager.PROPERTY_NO_APP_DATA_STORAGE);
                        return;
                    }
                    createdAppId.put(packageName, optimisticallyRegisterAppId(result)); //niu 生成或者使用原有appid
                    versionInfos.put(result.mPkgSetting.getPkg().getPackageName(),
                            mPm.getSettingsVersionForPackage(result.mPkgSetting.getPkg()));
                    Slog.i(TAG,"niulog install installPackagesLI scan  request = "+request+" ScanResult.result = "+result+" versionInfo:"+mPm.getSettingsVersionForPackage(result.mPkgSetting.getPkg()));
                } catch (PackageManagerException e) {
                    request.mInstallResult.setError("Scanning Failed.", e);
                    return;
                }
            }
            ReconcileRequest reconcileRequest = new ReconcileRequest(preparedScans, installArgs,
                    installResults, prepareResults,
                    Collections.unmodifiableMap(mPm.mPackages), versionInfos); //niu 用prepare和scan阶段的数据构造ReconcileRequest
            CommitRequest commitRequest = null;
            synchronized (mPm.mLock) {
                Map<String, ReconciledPackage> reconciledPackages;
                try {
                    Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "reconcilePackages");
                    // 调和阶段
                    reconciledPackages = ReconcilePackageUtils.reconcilePackages(
                            reconcileRequest, mSharedLibraries,
                            mPm.mSettings.getKeySetManagerService(), mPm.mSettings);
                    printPkg(reconciledPackages,"niulog install installPackagesLI reconcile");
                } catch (ReconcileFailure e) {
                    for (InstallRequest request : requests) {
                        request.mInstallResult.setError("Reconciliation failed...", e);
                    }
                    return;
                } finally {
                    Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
                }
                try {
                    Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "commitPackages");
                    commitRequest = new CommitRequest(reconciledPackages,
                            mPm.mUserManager.getUserIds()); //niu 构建CommitRequest(把前面各种阶段的信息都收集起来)
                    //进入commit阶段
                    commitPackagesLocked(commitRequest);
                    success = true;
                } finally {
                    Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
                }
            }

        } finally {
            省略代码......
        }
    }

/   后期收尾   /

终于到了后期收尾阶段,为啥要叫后期收尾呢?是因为这一阶段所做的事情即使出现了错误也不会影响上面apk安装成功的结果,那就来看下后期收尾都做了哪些事情。

创建app data根目录

关于为什么创建app data根目录以及都创建了哪些目录可以参考installd进程,在这篇就不赘述了。创建app data根目录是委托了Installer,Installer在通过binder通信的方式让installd进程帮忙创建的。只有创建app data根目录成功后,apk才可以运行起来。

dex优化

关于dex优化可以参考installd进程,同样dex优化也是委托Installer实现的,最终也是转交由installd进程帮忙实现的。dex优化即使不成功也不会影响apk的运行,但是会影响apk的运行速度。

创建app data根目录和dex优化的源代码如下,有兴趣可以看下:

//文件:InstallPackageHelper.java
private void executePostCommitSteps(CommitRequest commitRequest) {
        final ArraySet<IncrementalStorage> incrementalStorages = new ArraySet<>();
        for (ReconciledPackage reconciledPkg : commitRequest.mReconciledPackages.values()) {
            final boolean instantApp = ((reconciledPkg.mScanResult.mRequest.mScanFlags
                    & SCAN_AS_INSTANT_APP) != 0);
            final AndroidPackage pkg = reconciledPkg.mPkgSetting.getPkg();
            final String packageName = pkg.getPackageName();
            final String codePath = pkg.getPath();
            final boolean onIncremental = mIncrementalManager != null
                    && isIncrementalPath(codePath);

            省略代码......

            //创建app data根目录
            mAppDataHelper.prepareAppDataPostCommitLIF(pkg, 0); //niu 创建 data目录

            省略代码......

            final boolean performDexopt =
                    (!instantApp || android.provider.Settings.Global.getInt(
                            mContext.getContentResolver(),
                            android.provider.Settings.Global.INSTANT_APP_DEXOPT_ENABLED, 0) != 0)
                            && !pkg.isDebuggable()
                            && (!onIncremental)
                            && dexoptOptions.isCompilationEnabled();

           //并不是所有的apk都需要dex优化,如果需要优化,进入下面逻辑
           if (performDexopt) {
                省略代码......

                //开始优化
                mPackageDexOptimizer.performDexOpt(pkg, realPkgSetting,
                        null /* instructionSets */,
                        mPm.getOrCreateCompilerPackageStats(pkg),
                        mDexManager.getPackageUseInfoOrDefault(packageName),
                        dexoptOptions);
                Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
            }

            省略代码......
        }
        PackageManagerServiceUtils.waitForNativeBinariesExtractionForIncremental(
                incrementalStorages);
    }

移除已有apk

如果设备上已经安装了相同包名的apk(称它为老apk),则在新apk安装成功后是需要把老apk删除的,删除过程也同样是委托Installer,最终转交由installd进程来实现。即使老apk删除失败也不会影响新apk。

下面是对应源码,有兴趣看下:

//文件:Installer.java
public void rmPackageDir(String packageName, String packageDir) throws InstallerException {
        if (!checkBeforeRemote()) return;
        BlockGuard.getVmPolicy().onPathAccess(packageDir);
        try {
            mInstalld.rmPackageDir(packageName, packageDir);
        } catch (Exception e) {
            throw InstallerException.from(e);
        }
    }

发送安装成功广播

既然一个apk安装成功了,那肯定是需要通知关注者的,采用的方式是发广播,比如桌面在收到安装成功的广播后,修改正在安装apk的状态。

下面是发送广播源码,有兴趣看下:

//文件:PackageInstallerSession.java
private void dispatchSessionFinished(int returnCode, String msg, Bundle extras) {
        sendUpdateToRemoteStatusReceiver(returnCode, msg, extras);

        synchronized (mLock) {
            mFinalStatus = returnCode;
            mFinalMessage = msg;
        }

        final boolean success = (returnCode == INSTALL_SUCCEEDED);


        final boolean isNewInstall = extras == null || !extras.getBoolean(Intent.EXTRA_REPLACING);
        if (success && isNewInstall && mPm.mInstallerService.okToSendBroadcasts()) {
            //收集apk的信息,把这些信息通过广播发送出去
            mPm.sendSessionCommitBroadcast(generateInfoScrubbed(true /*icon*/), userId);
        }

        mCallback.onSessionFinished(this, success);
        if (isDataLoaderInstallation()) {
            logDataLoaderInstallationSession(returnCode);
        }
    }

/   总结   /

到此apk的安装之谜算是揭开了,apk的安装会经过前期准备、安装、后期收尾这三个阶段,前期准备成功后才会进入安装阶段,安装阶段成功后才会进入后期收尾阶段。除了后期收尾外,前两个阶段只要发生错误就会停止apk的安装。

apk的安装可以总结为下面几步:

  1. 不管apk是通过adb安装的(apk存储于PC的磁盘)还是应用市场安装的(apk存储于设备),首先apk会被拷贝到 /data/app/xxx.tmp目录下面(xxx是一个随机生成的字符串)
  2. 在经过重重的验证、校验(签名、版本号),/data/app/xxx.tmp 目录会重命名为 /data/app/[randomStrA]/[packageName]-[randomStrB] 目录,也就是被拷贝的apk最终路径是 /data/app/[randomStrA]/[packageName]-[randomStrB]/base.apk 。同时会为apk生成一个唯一的id又称appid
  3. 解析apk的AndroidManifest中的内容为ParsedPackage,ParsedPackage中的权限等信息经过验证通过后,ParsedPackage传递给PMS,这样其他使用者比如ActivityManagerService就可以从PMS获取刚安装apk的信息了。
  4. 刚安装的apk的安装信息比如包名、版本、签名证书、安装时间等会存储到PackageSetting,PackageSetting会传递给Settings,Settings会把它持久化到packages.xml文件。
  5. 创建app data根目录,app data根目录是apk运行期间数据存储的根目录,并且app data根目录只有当前apk程序有读写执行权,其他不用没有任何权限。
  6. 对apk的dex进行优化,优化即使不成功也不影响apk的安装,dex优化可以保证app运行性能上的提升。
  7. 发送安装成功广播。

apk越大包含的so越多,安装apk的时间越长。主要时长体现在拷贝、提取native libs、dex优化这几项工作。

推荐阅读:
我的新书,《第一行代码 第3版》已出版!
原创:写给初学者的Jetpack Compose教程,高级Layout
官方支持Compose轮播图组件啦~

欢迎关注我的公众号
学习技术或投稿


长按上图,识别图中二维码即可关注
继续滑动看下一个
郭霖
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存