文章有点长,耐心读完哦!
https://www.jianshu.com/u/9cf1f31e1d09
/ 背景 /
- 了解APK的核心安装原理
- 了解各场景下,到达核心安装状态的应对过程原理
- AMS从PMS获取要启动的Activity的启动信息
- AMS需要确认此Activity所运行的进程有没有启动,没有则需要请求Zygote孵化
- 以上两过程正常,执行Activity启动流程
实际上,PMS加载APK主要完成三件事情:
- 解析AndroidManifest.xml,拿到构成此APP的各组件信息,以及启动信息
- 为每个APP分配UID、GID,以此创建APP运行的进程,这涉及Android的沙箱模型,可以理解为应用程序资源归属问题的解决
- 更新应用程序权限
因此,所有不同的APK安装场景,在完成了各自必要的准备后,均需完成上述三件事情。这是殊途同归的过程。先说同归,再续殊途。
note: 文章源码版本为8.0。
从APK中解析出的信息将存于Package。需要注意codePath区分分包的情况,即5.0后,为了解决65536问题,将APK拆成多个APK策略。以及Activity、Service、Provider等并不是日常所用的相应组件,而是存储了对应组件信息的信息聚合类,大概如下:
第一件事情,解析AndroidManifest.xml,直接定位 PackageParser.parsePackage()。
上面代码片段if()是针对是否使用分包策略的不同执行路径,但均通过此调用路径:
- parseBaseApk()
- parseBaseApk()
- parseBaseApkCommon()
- Settings:存储各种重要的动态配置信息或索引,如package信息索引、package.xml文件(后续会说到)、UID信息集合等
- PackageSetting: 存储APK包解析出的信息,包括第一步中解析出的数据结构Package信息,能从Setting通过pkgName找到PackageSetting
- 为pkg所描述的应用程序分配UID,并更新PackageSetting,因为APK的安装涉及到新应用安装、旧版本更新以及share user id场景。新版本情况下,要准备新的PackageSetting并分配UID;旧版本更新情况下,更新PackageSetting,UID已做过分配;share user id情况下则需考虑PackageSetting的可用性
- 将pkg所指向的Pacakge保存到PMS
- 将pkg描述的四大组件保存到PMS,以供AMS访问
常见的权限如网络权限、文件读写权限等;危险权限则如定位权限、摄像头权限等,也包括自定义权限。权限状态更新定位 PMS.grantPermissionsLPw()。
权限文件位于 /system/etc/permissions/platform.xml,而权限ID保存在PMS.mSetting.mPemissions中,默认全局可访问的权限在mGlobalGids中,有兴趣可自行查看。权限等级有三种:
- PROTECTION_NORMAL:任何应用都可以申请,在安装应用时授权,无需用户操作
- PROTECTION_DANGEROU:任何应用都可以申请,在安装应用时授权,需要用户操作
- PROTECTION_SIGNATURE:只有于声明该授权的apk使用了相同的私钥签名的应用才可以申请该权限
- GRANT_INSTALL:安装时授权,无需用户操作
- GRANT_RUNTIME:运行时授权,在6.0后加入权限动态申请,需要用户操作
- GRANT_UPGRADE:安装时提示用户授权,需要用户操作
- GRANT_DENIED:未获得授权
小结
APK安装核心步骤为:
- 从AndroidManifest中解析出应用信息、各组件信息、权限信息
- 为应用程序分配UID,并让PMS记录个组件信息,AMS启动四大组件时,需要这些信息
- 更新应用程序权限信息,授权应用程序资源访问权
/ 殊途 /
系统启动安装
系统在每次启动时,都会重新安装所有应用程序,共有四种类型的应用程序,分别存于不同文件夹之下:
- /data/app-private: 受DRM保护的程序
- /system/app-private: 系统自带程序
- /vendor/app: 手机厂商自己程序
- /data/app: 用户自行安装的程序
而 /system/framework 则是资源性应用程序,是用来打包资源文件的,不包含有执行代码。
之前说过,每一用户安装的应用程序被分配的UID是不变的,因此系统通过 /data/system/package.xml 可以到达此目的。package.xml文件保存了上一次安装应用程序的信息,其中也包括了应用程序UID,因此可以在解析出package.xml 信息后,向系统申请分配各应用程序的UID 。
紧接着,在拿到各种程序的文件夹后,通过scanDirTracedLI()进行安装,再通过updatePermissionsLPw()触发grantPermissionsLPw()更新所有应用程序权限。最后将最新的pacakge.xml写入保存。这里也就将之前所有的安装APK的核心步骤串联了起来。
Package.xml 文件的解析
package.xml 文件格式如图:
实际了解关键节点的作用,即可大致知道程序如何解析。
- package 节点:描述了某一个应用程序的安装信息,比如子节点name有包名,子节点Perms有申请的权限,自身节点属性userId即关键的UID。具体解析步骤见 Setting.readLPw()和Setting.readPackageLPw()
- shared-user 节点:有共享应用程序的信息。在package节点是可能解析出 share user id 信息的 (与 userId 互斥),当出现此场景时,会将分配进程ID的操作挂起,等解析shared-user此节点的时候验证此share user id 的有效性再进行分配,详细见Setting.readLPw().readSharedUserLPw()
在解析package节点时,解析出userId后,通过Setting.addPackageLPw() 和Setting.addUserIdLPw()让系统分配UID,并能拿到描述应用程序信息的的PackageSetting。
Package.xml文件保存
见Settings.writeLPr()。
package.xml的保存,除了根据规则生成xml外,还做了备份处理,备份文件路径为/data/system/package-backup.xml。备份文件是为了防止package.xml在写入过程中防止意外中断而做的保障。
第三方应用安装
上面的逻辑是这样的。
INIT_COPY
HandlerParams包含了安装APK必要的参数。在收到INIT_COPY信息后,会与DefaultContainerService进行连接,DefaultContainerService负责处理文件检查与拷贝等耗时操作,与PMS运行在不同进程。在与DefaultContainerService链接后,PMS获得可转为IMediaContainerService(AIDL)的Binder,可以用来与DefaultContainerService通信。如果链接已建立,将HandlerParams加入安装任务队列。连接操作见PackageHandler.connectToService。
注意在连接成功后,向PackageHandler发送了MCS_BOUND信号,并将imcs作为参数对象。
MCS_BOUND
收到此信号有两种可能,一者是与DefaultContainerService建立了连接,在此接收通信用的AIDL;二者是接收安装请求。
在接收安装请求时,如果mContainerService不为空,但mBound标志位没有正确设置,说明出现了异常,将安装请求队列清空。如果一切正常,将安装请求HandlerParams加入请求队列,取出位于0位置的进行startCopy()操作。在处理完后,将HandlerParams移除请求队列。如果队列还有任务,发送MCS_BOUND信息号继续执行下一条,否则发送MCS_UNBIND信号解决服务。
startCopy()
HandlerParams为抽象类,子类需实现以下三个方法。
作为包安装请求时,实际类为InstallParams。
HandlerParams.startCopy()
HandlerParams实际操作步骤为:
- 检查尝试安装次数,超过限制则放弃安装请求,发送MCS_GIVE_UP信号,调用handleServiceError()
- 调用子类handleStartCopy()处理具体安装
- 调用子类handleReturnCode()处理安装结果
见InstallParams.handleStartCopy()。
需要确认APK的安装位置,不同的安装位置需要构建不同的InstallArgs,InstallArgs子类有MoveInstallArgs、AsecInstallArgs、FileInstallArgs。最后通过binder让DefaultContainerService复制apk到相应位置下,具体的复制流程不进行跟踪。
直接跟进installPackageTracedLI()和installPackageLI()。
这里就触发核心的安装过程。
PMS驱动APK安装可以用上图表示:
- 通过INIT_COPY信号与DefaultContainerService进行连接,连接成功后拿到可转为IMediaContainerService的Binder,发送MCS_BOUND信号
- 通过MCS_BOUND信号接收IMediaContainerService(AIDL)
- 通过INIT_COPY接收HandlerParams请求数据,加入请求队列
- 通过MCS_BOUND新型号处理HandlerParams请求,通过startCopy()拿到合适的APK路径,通过DefaultContainerService进行复制,最后通过handleReturnCode()最终触发核心安装逻辑
前面说过第三方安装涉及到安装页面以及和用户交互,但目前为止未提及此类信息。原因是本质上是需要PMS来驱动安装,安装页面和用户交互是为了解决如何获取安装数据,并把安装请求发给PMS,这里做简单说明。
系统内置了应用程序PackageInstaller处理APK的安装和卸载,PackageInstaller涉及到的页面分别是InstallStart(入口)、InstallStaging、PackageInstallerActivity、InstallInstalling。
在7.0以下的版本能通过file:// Uri的 intent启动InstallStart。
但在7.0之后禁止将file:// Uri暴露给其它程序,因此需要FileProvider来解决。
在InstallStart.onCreate()通过协议的不同启动InstallStaging或PackageInstallerActivity。但处理逻辑在PackageInstallerActivity里。InstallStaging是为了处理7.0之后的场景,通过InstallStaging.StagingAsyncTask将content协议的Uri转换为File协议,再跳转PackageInstallerActivity。
PackageInstallerActivity在处理完pkgUri,并校验安装权限,与用户交互获取用户同意安装的操作后,启动InstallInstalling。
InstallInstalling则通过PackageInstaller.Session与PMS进行会话,调用session.commit(),最终触发installStage.installStage()发送INIT_COPY信号,进行PMS驱动APK安装过程。
ADB 安装
关键代码不多,就一次性贴出来了。PM与PMS运行在不同的进程,需要IPC,借助PackageInstaller.Session进行,调用commit()。其中关系如下图。
在 PMS.installStage() 里发送了INIT_COPY 信号, 将安装请求加入队列。因此,也是用了PMS驱动APK安装的方式。
Martket安装
从Martket安装就比较简单了。Martket应用程序在下载完APK后,与PMS进行对话,调用PMS.installPackageAsUser()。
也是发送了INIT_COPY,通过PMS驱动APK安装方式进行。
APK核心安装步骤
- 从AndroidManifest中解析出应用信息、各组件信息、权限信息,代码索引为PackageParser.parseBaseApkCommon()
- 为应用程序分配UID,并让PMS记录个组件信息,代码索引为PMS.scanPackageDirtyLI()、Setting.addUserIdLPw()
- 更新应用程序权限信息,授权应用程序资源访问权,代码索引为PMS.grantPermissionsLPw()
程序代码起点为PMS.PackageManagerService()。
- 获取应用程序文件夹,system/framework、/data/app、data/app-private、/system/app、/vendor/app
- 根据pacakge.xml文件恢复上一次保存的应用程序信息,为应用程序分配UID, 代码见Setting.ReadLPw()
- 逐步扫描程序文件夹找出各个APK并安装
- 更新所有应用程序权限
- 通过INIT_COPY信号与DefaultContainerService进行连接,连接成功后发送MCS_BOUND信号
- 通过MCS_BOUND信号接收Binder,转为AILD接口(IMediaContainerService)
- 通过INIT_COPY接收HandlerParams请求数据,加入请求队列
- 通过MCS_BOUND信号处理HandlerParams请求,通过startCopy()拿到合适的APK路径,通过DefaultContainerService进行复制,最后通过handleReturnCode()最终触发核心安装逻辑
取这个文章标题很符合学过程的心境。学习之初确实发现大家写APK安装原理的起始点均不同,并且写到如何解析APK,如何分配UID就没下文了,弄得我一脸懵,很是尴尬。因此萌生了写这一篇文章的想法。文章很长,看下来需要不少耐心。文章总体结构想了又想,省略了不少细节。
推荐阅读: 使用Flutter中实现画中画效果,这操作66的
是的,Retrofit想用得好就得这么死磕
原来这样可以优雅地解决小米手机后台弹窗权限问题
欢迎关注我的公众号 学习技术或投稿