Apk安装之谜

用心坚持输出易读、有趣、有深度、高质量、体系化的技术文章

本文概要

本文同样还是延续自述、对话这种轻松的方式,带您揭开apk安装的谜底,apk它到底是如何安装的。(为什么没有继续写系统native进程的文章,而来写apk安装的文章,主要原因是在看installd代码的时候也结合了apk安装的代码,如果放到后面写apk安装的文章,怕时间久了很多就忘记了,因此才有了此篇文章。文中的代码是基于android13

Android系统进程系列的前五篇文章如下:
Android系统native进程之我是init进程
Android系统native进程之属性能力的设计魅力
Android系统native进程之进程杀手--lmkd
Android系统native进程之日志系统--logd、logcat
Android系统native进程之我是installd进程

开场白

"大家好,我是今天的主角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.arscMETA-INF目录(它里面的CERT.SFCERT.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。如下:

yaml 复制代码
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,首先需要调用我的PackageInstallerServicecreateSession 方法创建一个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对象会在后面的安装流程用到。

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

less 复制代码
//文件路径: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类的关键属性如下,有兴趣可以看下

arduino 复制代码
//文件路径: 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库的所处的目录如下:

bash 复制代码
//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/ **目录下面。下一步就来看下版本号验证吧。

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

scss 复制代码
//文件路径: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拿到。"

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

java 复制代码
//文件路径: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父目录的相关代码,有兴趣可以看下

arduino 复制代码
//文件路径: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(它在后面的安装流程会用到),咱们进入扫描阶段。

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

java 复制代码
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的代码如下,有兴趣看下:

java 复制代码
//文件路径: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的信息删除。

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

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

            省略代码......
}

提交 (Commit)

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

Settings :"首先由我来介绍吧,调用我的insertPackageSettingLPw 方法可以把PackageSettingParsedPackage 更新我的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 对象会传递给SettingsSettings把它加入内存并且持久化到packages.xml文件中。"

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

dart 复制代码
//文件: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优化的源代码如下,有兴趣可以看下

ini 复制代码
//文件: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。

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

arduino 复制代码
//文件: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的状态。

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

ini 复制代码
//文件: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中的内容为ParsedPackageParsedPackage 中的权限等信息经过验证通过后,ParsedPackage 传递给PMS,这样其他使用者比如ActivityManagerService就可以从PMS获取刚安装apk的信息了。
  4. 刚安装的apk的安装信息比如包名、版本、签名证书、安装时间等会存储到PackageSettingPackageSetting 会传递给SettingsSettings 会把它持久化到packages.xml文件。
  5. 创建app data根目录,app data根目录是apk运行期间数据存储的根目录,并且app data根目录只有当前apk程序有读写执行权,其他不用没有任何权限。
  6. 对apk的dex进行优化,优化即使不成功也不影响apk的安装,dex优化可以保证app运行性能上的提升。
  7. 发送安装成功广播。

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

欢迎关注我的公众号 --牛晓伟(搜索或者点击牛晓伟链接)

相关推荐
阿巴斯甜15 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker15 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq952716 小时前
Andorid Google 登录接入文档
android
黄林晴18 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab1 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿1 天前
Android MediaPlayer 笔记
android
Jony_1 天前
Android 启动优化方案
android
阿巴斯甜1 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇1 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_2 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android