APK的安装流程

2023-11-14

文章目录

我们来思考一下Android系统是如何安装一个APK文件的,从直观的流程上,当我们点击一个APK文件或者从应用商店下载一个APK文件,会弹起一个安装对话框,点击安装就可以安装应用。

那么这里面的流程是什么样的呢?

首先很容易想到的是,Android根据文件类型MimeType来判断是否弹起安装页面,就行点击一个视频会调起视频播放器一样。

Android系统常见的文件类型如下所示:

  • add("application/zip", "zip");
  • add("application/vnd.android.package-archive", "apk");
  • add("video/mp4", "mp4");
  • add("video/3gpp", "3gpp");
  • add("text/plain", "txt");
  • add("image/gif", "gif");
  • add("image/ico", "ico");
  • add("image/jpeg", "jpeg");
  • add("image/jpeg", "jpg");

这里面就有我们今天聊的APK文件,当点击APK文件时会调起安装界面,这个安装界面其实就是PackageInstallerActivity

//点击APK文件,弹起对话框,询问是否安装此应用。
File apkFile;
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setDataAndType(Uri.fromFile(apkFile), "application/vnd.android.package-archive");
context.startActivity(intent);

PackageInstallerActivity显示的是个对话框,当点击确定安装以后,会启动真正的安装界面,这个界面就是InstallAppProgress,它也是一个Activity,会显示安装的进度,

整个APK的安装流程如下所示:

  1. 复制APK到/data/app目录下,解压并扫描安装包。
  2. 资源管理器解析APK里的资源文件。
  3. 解析AndroidManifest文件,并在/data/data/目录下创建对应的应用数据目录。
  4. 然后对dex文件进行优化,并保存在dalvik-cache目录下。
  5. 将AndroidManifest文件解析出的四大组件信息注册到PackageManagerService中。
  6. 安装完成后,发送广播。

总体说来就两件事情拷贝APK和解析APK,解析APK主要是解析APK的应用配置文件AndroidManifest.xml,以便获得它的安装信息。在安装的过程中还会这个应用分配Linux用 户ID和Linux用户组ID(以便它可以在系统中获取合适的运行权限)。

关于Linux用户ID与Linux用户组ID

Linux用户ID与Linux用户组ID的分配与管理是由Settings类来完成的。

Settings:该类用来管理应用程序的安装信息(APK包信息、Linux用户ID、Linux用户组ID等),Android系统在每次重启是都会将应用程序重新安装一遍,Settings就是保证在重新安装应用时可以恢复应用的信息。

在Android系统中,用户ID可以划分为以下层次:

  • 小于FIRST_APPLICATION_UID:特权用户ID,用户应用程序不能直接使用,但是可以以一种sharedUserId的方式共享使用,例如想要修改系统时间就设置android:sharedUserId = "android.uid.system"。
  • FIRST_APPLICATION_UID 至 FIRST_APPLICATION_UID + MAX_APPLICATION_UIDS:给用户应用程序使用,共有1000个。
  • 大于FIRST_APPLICATION_UID + MAX_APPLICATION_UIDS:非法的Linxu用户ID。

以上便是对APK安装流程的整体概述,有了整体的把握,我们接着来看实现细节。APK安装流程序列图如下所示:


嗯,看起来有点长,但只要我们掌握核心逻辑和原理,再长的函数调用链都是纸老虎。

整个序列图按照颜色划分为三个进程:

  • PackageInstaller进程:PackageInstaller事实上是一个应用,它负责APK安装以及卸载过程中与用户的交互流程。
  • SystemServer进程:该进程主要运行的是系统服务,APK的安装、卸载和查询都由PackageManagerService负责,它也是Android核心系统服务的一种,在SystemServer里初始化系统服务的时候被启动。
  • DefaultContainerService进程:DefaultContainerService也是一个单独的进程,它主要负责检查和复制设备上的文件,APK的复制就是由DefaultContainerService来完成的。

PackageInstaller和DefaultContainerService都比较简单,我们重点关注的是Android的包管理服务PackageManagerService。

一 APK解析流程

Android的应用程序是一个以".apk"为后缀名的归档文件,它在安装之前会先DefaultContainerService将自己复制到/data/app目录中去,拷贝完成以后 便开始解析APK。

这里提一下/data/app这个目录,Android不同的目录存放不同类型的应用,如下所示:

  • /system/framwork:保存的是资源型的应用程序,它们用来打包资源文件。
  • /system/app:保存系统自带的应用程序。
  • /data/app:保存用户安装的应用程序。
  • /data/app-private:保存受DRM保护的私有应用程序。
  • /vendor/app:保存设备厂商提供的应用程序。

APK文件里包含了一个配置文件AndroidManifest.xml,Android应用程序的解析过程就是解析这个xml文件的过程。

从上面的序列图我们可以看出,APK解析是从PackageManagerService的scanPackageLI开始的,而该方法内部又调用的是scanPackageDirtyLI()方法,我们来看一下这个方法的实现。

public class PackageManagerService extends IPackageManager.Stub {
    
       private PackageParser.Package scanPackageDirtyLI(PackageParser.Package pkg, int parseFlags,
               int scanFlags, long currentTime, UserHandle user) throws PackageManagerException {
           //...
           // writer
           synchronized (mPackages) {
               // 验证已注册的ContentProvider是否有其他同名,做冲突检测。
               if ((scanFlags & SCAN_NEW_INSTALL) != 0) {
                   final int N = pkg.providers.size();
                   int i;
                   for (i=0; i<N; i++) {
                       PackageParser.Provider p = pkg.providers.get(i);
                       if (p.info.authority != null) {
                           String names[] = p.info.authority.split(";");
                           for (int j = 0; j < names.length; j++) {
                               if (mProvidersByAuthority.containsKey(names[j])) {
                                   PackageParser.Provider other = mProvidersByAuthority.get(names[j]);
                                   final String otherPackageName =
                                           ((other != null && other.getComponentName() != null) ?
                                                   other.getComponentName().getPackageName() : "?");
                                   throw new PackageManagerException(
                                           INSTALL_FAILED_CONFLICTING_PROVIDER,
                                                   "Can't install because provider name " + names[j]
                                                   + " (in package " + pkg.applicationInfo.packageName
                                                   + ") is already used by " + otherPackageName);
                               }
                           }
                       }
                   }
               }
           }
         
           if (mPlatformPackage == pkg) {
              //...
           } else {
               // This is a normal package, need to make its data directory.
               dataPath = getDataPathForPackage(pkg.packageName, 0);
               if (dataPath.exists()) {
                   //...
               } else {
                   //invoke installer to do the actual installation
                   //这里创建了应用数据目录,用于存放用户数据
                   int ret = createDataDirsLI(pkgName, pkg.applicationInfo.uid,
                                              pkg.applicationInfo.seinfo);
                   //...
               }
             
           }
         
           // We also need to dexopt any apps that are dependent on this library.  Note that
           // if these fail, we should abort the install since installing the library will
           // result in some apps being broken.
           if (clientLibPkgs != null) {
               if ((scanFlags & SCAN_NO_DEX) == 0) {
                   for (int i = 0; i < clientLibPkgs.size(); i++) {
                       PackageParser.Package clientPkg = clientLibPkgs.get(i);
                       if (performDexOptLI(clientPkg, null /* instruction sets */, forceDex,
                               (scanFlags & SCAN_DEFER_DEX) != 0, false) == DEX_OPT_FAILED) {
                           throw new PackageManagerException(INSTALL_FAILED_DEXOPT,
                                   "scanPackageLI failed to dexopt clientLibPkgs");
                       }
                   }
               }
           }
         
           // writer
           synchronized (mPackages) {
               //...
               // 以下对四大组件进行注册
               int N = pkg.providers.size();
               StringBuilder r = null;
               int i;
               for (i=0; i<N; i++) {
                   PackageParser.Provider p = pkg.providers.get(i);
                   p.info.processName = fixProcessName(pkg.applicationInfo.processName,
                           p.info.processName, pkg.applicationInfo.uid);
                   //注册Content Provider
                   mProviders.addProvider(p);
                   //...
               }
               //...
           }
       }
       //...
   }
}

scanPackageDirtyLI是一个上千行的函数,它主要完成的工作如下所示:

  1. 调用PackageParser的parsePackage()方法解析AndroidMainfest.xml文件,主要包括四大组件、权限信息、用户ID,其他use-feature、shared-userId、use-library等 信息,并保存到PackageManagerService相应的成员变量中。
  2. 调用签名验证方法verifySignaturesLP()进行签名验证,验证失败的无法进行安装。
  3. 调用createDataDirsDirtyLI()方法创建应用目录/data/data/package,同时将APK中提取的DEX文件保存到/data/dalvik-cache中。
  4. 调用performDexOptLI()方法执行dexopt操作。

我们接着来看看APK里的信息是如何被解析出来的。

Apk的解析是PackageParser的parsePackage()函数来完成的,我们来看看它的实现。

public class PackageParser {
    
     public Package parsePackage(File packageFile, int flags) throws PackageParserException {
         if (packageFile.isDirectory()) {
             return parseClusterPackage(packageFile, flags);
         } else {
             return parseMonolithicPackage(packageFile, flags);
         }
     }
     
     private Package parseClusterPackage(File packageDir, int flags) throws PackageParserException {
             //...
             
             //初始化AssetManager
             final AssetManager assets = new AssetManager();
             try {
                 //...
                 //解析Base APk,解析AndroidManifest.xml
                 final Package pkg = parseBaseApk(baseApk, assets, flags);
                 if (pkg == null) {
                     throw new PackageParserException(INSTALL_PARSE_FAILED_NOT_APK,
                             "Failed to parse base APK: " + baseApk);
                 }
     
                 //如果splitName不为空,则循环解析Split Apk
                 if (!ArrayUtils.isEmpty(lite.splitNames)) {
                     final int num = lite.splitNames.length;
                     pkg.splitNames = lite.splitNames;
                     pkg.splitCodePaths = lite.splitCodePaths;
                     pkg.splitRevisionCodes = lite.splitRevisionCodes;
                     pkg.splitFlags = new int[num];
                     pkg.splitPrivateFlags = new int[num];
     
                     for (int i = 0; i < num; i++) {
                         //解析
                         parseSplitApk(pkg, i, assets, flags);
                     }
                 }
     
                 pkg.setCodePath(packageDir.getAbsolutePath());
                 pkg.setUse32bitAbi(lite.use32bitAbi);
                 return pkg;
             } finally {
                 IoUtils.closeQuietly(assets);
             }
         }
}

注:Split APK是Google为解决65535上限以及APK越来越大的问题而提出的一种方案,它可以将一个庞大的APK按照屏幕密度、ABI等形式拆分成多个独立的APK,这些APK共享相同的data、cache目录。 共享相同的进程,共享相同的包名。它们还可以使用各自的资源,并且继承了Base APK里的资源。更多细节可以查阅官方文档Build Multiple APKs

该方法调用parseBaseApk()去解析AndroidManifest.xml,AndroidManifest.xml也是xml文件,当然也使用XmlResourceParser来解析。这个解析相应标签并保存到PackageManagerService对应的成员变量中去。 此处就不再展开。

通过上面的讲解,我们理解了APK的计息流程,APK解包以后,里面有个DEX文件,我们前面也说过PackageManagerService的performPackageLI()方法去执行dexopt操作 ,我们接着来分析它是如何实现的。

二 DEX的dexopt流程

我们先来了解一下什么dexopt操作,dexopt操作实际上对DEX文件在执行前进行一些优化,但是不同的虚拟机操作有所不同。

  • Davlik:将dex文件优化生成odex文件,这个odex文件的后缀也是dex,保存在/data/dalvik-cache目录下。
  • ART:将dex文件翻译生成oat文件

从上面的序列图我们可以看出,Installer.java通过Socket方式请求守护进程installd完成dexopt操作。

public final class Installer {  
    public int dexopt(String apkPath, int uid, boolean isPublic) {  
        StringBuilder builder = new StringBuilder("dexopt");  
        builder.append(' ');  
        builder.append(apkPath);  
        builder.append(' ');  
        builder.append(uid);  
        builder.append(isPublic ? " 1" : " 0");  
        return execute(builder.toString());  
    }  
}

守护进程调用Command.c里的dexopt()方法执行dexopt操作,如果你对dexopt的C++层的实现感兴趣可以异步:

 Android ART运行时无缝替换Dalvik虚拟机的过程分析

APK安装完成以后会在桌面生成一个快捷图标,点击图标就可以启动应用了。


本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

APK的安装流程 的相关文章

  • Android应用程序开发中的EditText警告

    在 xml 文件中声明 EditText 时 我收到了如下警告 没有标签视图通过 android labelFor id id start 属性指向此文本字段 编辑文本代码是
  • Android 应用程序因 Firebase 电话身份验证而崩溃

    我正在使用 firebase 进行电话号码身份验证 当我使用我的电话号码时 它会自动验证它 但是当我使用另一个电话号码时 我得到类转换异常 它说 getGoogleApiForMethod 返回 Gms stackoverflow上也有类似
  • onBackPressed 仅关闭 ProgressDialog

    我意识到我的异步任务有一个小问题 我意识到 当我按 Android 设备上的后退按钮来关闭进度对话框和异步任务时 只有我的进度对话框被关闭 而我的异步任务仍在执行 我真的不知道为什么会发生这种情况 所以我只是希望有人能让我回到正确的轨道并帮
  • Android Wear:在手持设备上启动服务

    我正在构建一个 Wear 应用程序 它将与手持设备上的 WearableListenerService 进行通信 但是 我想确保当应用程序在手表上启动时服务已启动并运行 我最初的想法是发送意图或广播消息来启动服务 但是 我一直无法弄清楚如何
  • 如何在android中将多个图像合并为一个图像?

    我正在开发 android 的分布式应用程序 我已将单个图像分成 4 个部分 然后对其进行处理 现在我想将 4 个位图图像组合成一个图像 我怎样才能做到这一点 Bitmap parts new Bitmap 4 Bitmap result
  • AdMob 广告未显示

    因此 我使用 Play Services SDK 实施了 AdMob 广告 我已经 按照书本 做了所有事情 但广告不会显示 如果我将 AdView 背景设置为白色 它会显示空白 但不显示广告 我正在使用 Fragments 但我将 AdVi
  • SQLite 中的 NOT NULL 列和错误捕获

    我有一个由用户定义的 EditText 填充的数据库 所有编辑文本都不允许有空字段 我知道我可以通过几个简单的 if 语句来检查这一点 if myEditText getText toString equals display error
  • 在styles.xml中设置所有按钮样式

    我想让我的应用程序有多种样式 样式更改按钮样式 文本视图颜色和布局背景颜色 我的按钮样式位于 xml 文件中 这是我的代码 style xml v21
  • cordova - 删除不必要的权限

    我需要在游戏中播放声音 因此我将 org apache cordova media 插件添加到我的应用程序中 现在platforms android AndroidManifest xml包含2个我不需要的条目
  • 如何使用 Retrofit 2 和 RxJava 处理分页

    我知道如何处理 Retrofit 响应 但在使用 rx java 处理来自 REST API 的分页时遇到问题 背景 我使用的其余 api 为我提供了以下响应 并在标题中提供了下一页的链接 HTTP 200 OK Allow GET HEA
  • Android“权限拒绝:无法使用相机”

    我正在学习有关在 Android 应用程序中使用相机的教程 我收到错误 权限被拒绝 无法使用相机 在模拟器和物理设备上运行调试时 我在清单文件中尝试了各种权限 似乎大多数遇到此错误的人都遇到了拼写错误 缺少权限或权限不在清单中的正确位置 这
  • 如果从超链接打开,应用程序将启动两次

    我正在开发一个应用程序 可以从多个地方启动 例如日历中的超链接 我在以下场景中面临问题 如果应用程序已启动并在后台运行 并且用户单击本机日历中的事件 超链接来启动应用程序 我的应用程序作为新实例启动两次 在正在运行的应用程序列表中 我可以看
  • Android图层列表不显示比例可绘制项目?

    使用下面的图层列表 我的比例绘制从未显示 这是为什么 菜单 对话框 标题 xml
  • 从我自己的网站而不是市场安装(和更新)Android应用程序[关闭]

    Closed 这个问题需要多问focused help closed questions 目前不接受答案 我可以在自己的网站上发布 Android 应用程序 而不使用 Android 市场 该应用程序与我们的互联网软件服务一起使用 因此仅符
  • Android:了解 OnDrawFrame、FPS 和 VSync (OpenGL ES 2.0)

    一段时间以来 我在 Android 游戏中遇到了运动精灵间歇性 卡顿 的情况 这是一个非常简单的 2D OpenGL ES 2 0 游戏 这是一个持续存在的问题 我已经多次重新访问过 在我的游戏循环中 我有 2 个 计时器 一个用于记录前一
  • 在 NumberPicker 中显示更多数字

    我有两个问题 第一个问题是删除 NumberPicker 中的分隔线 我在 Android 中扩展 NumberPicker 来解决这个问题 如下所示 import android content Context import androi
  • Android Studio:src/androidTest 和 src/main 文件夹之间的区别?

    我是 Android Studio 的新手 我的问题是 src androidTest 和 src main 文件夹有什么区别 我所有的课程应该放在哪里 Refer Android Studio 概述 http developer andr
  • 尝试在后台使用 AsyncTask 解析 JSON 时强制关闭

    我是 Android 开发新手 正在研究 json 数据 我设法让解析工作 我想显示一个 ProgressDialog 我读到我需要使用 AsyncTask 但由于某种原因 一旦我将相同的工作代码放入 doInBackground 中 即使
  • 如何在清单文件中添加符合我意图的标志

    我们知道 我们可以使用 java 代码中的 addFlags 方法将一些标志添加到我们的意图中 有什么方法可以将这些标志添加到清单文件本身中 而不是用 java 代码编写 我需要为清单中的一项活动添加 REORDER TO FRONT 标志
  • 切换按钮形状不变

    我正在尝试制作一个带有绿色背景的圆形切换按钮 我用了

随机推荐

  • 半导体创业

    synosis系列 芯耀辉 芯华章 芯原 dsp 壁仞科技 主要负责人华为mobile gp ps 华为升腾的大佬是liaoheng和tujiajun Mikehong在MobileGpu oppo 哲库科技 GPU摩尔线程 NB 芯翼信息
  • Android中apk的名称被Module下相同的app_name替换时,正确的更改方式

    错误产生原因 android 中 寻找资源文件 首先会寻找本机语言下的资源文件 例如 如果手机是中文版 则会优先选择res下面values有中文资源的进行匹配 这也是导致我的app name被module下的中文app name替换的原因
  • Python:Anaconda安装&常用库(selenium,pymysql)离线安装

    因为网络限制 所以用很多库用pip安装不成功 只能采用离线安装了 方法也简单 按照下面步骤来就好了 1 Anaconda下载安装 下载地址 https www anaconda com products individual 下载后 傻瓜式
  • ES6(这是我见过写的最好的)!推荐

    文章目录 ES6总结 var let const的区别 箭头函数和function的区别 结构赋值 原型 原型链 继承 1 原型链继承 2 构造函数继承 3 组合式继承 4 class类继承 Promise async和await Gene
  • iOS 使用蓝牙耳机的mic作为输入源

    1 首先采样率的设置必须与蓝牙耳机设备的采样率相同 2 然后通过 setPreferredInput 方法从可用的输入设备的数组中选取蓝牙耳机
  • linux应用程序core dump处理

    默认编译出来的程序在出现Segmentation fault 时并没有生成core崩溃文件 可以在gcc g 编译时增加 g选项 如果仍然没有生成core文件 则可能是因为系统设置了core文件大小为0 可以通过 ulimit a 查询得知
  • 实现游戏结束时显示GameOver界面。(Unity)

    在Canvas画板里添加Text文本组件 修改名字为GameOver 修改名字是为了让我们以后更改时更容易找到对应的组件 请名字时尽量和代码一样需要见名知义 并且通过锚点修改他的位置 在位置里修改他需要显示的大小 并且在Text文本组件中修
  • python画笑脸-如何用Python画滑稽笑脸

    Linux编程 点击右侧关注 免费入门到精通 作者丨Saltwater Room https blog csdn net Saltwater Room article details 829 用turtle画滑稽 fromturtle im
  • Android ListView默认选中第一项或第N项

    大体上从查阅的资料和自己的实践一共可以分为以下几种方法 一 重写Adapter 在getView里进行自己的操作 选中 变色等等 class MyAdapter extends BaseAdapter Override public Vie
  • 垂直网络广告

    垂直网站 英文名 Vertical website 注意力集中在某些特定的领域或某种特定的需求 提供有关这个领域或需求的全部深度信息和相关服务 作为互联网的亮点 垂直网站正引起越来越多人的关注 垂直网络广告是指广告发布主体利用网络广告投放平
  • 两个二叉树的合并

    将给定两个二叉树 想象当你将它们中的一个覆盖到另一个上时 两个二叉树的一些节点便会重叠 你需要将他们合并为一个新的二叉树 合并的规则是如果两个节点重叠 那么将他们的值相加作为节点合并后的新值 否则不为 NULL 的节点将直接作为新二叉树的节
  • 手把手教你打造自己的弱口令扫描工具(系统弱口令篇)

    0x01 前言 在渗透测试过程中 弱口令检测是必要的一个环节 选择一个好用的弱口令扫描工具 尤为重要 类似的弱口令检测工具如 Hydra Hscan X Scan 很多时候满足不了自己的需求 通过Python打造自己的弱口令扫描工具 集成在
  • 01FFMPEG的AVFormatContext结构体分析和输出时AVFormatContext的初始化(包含有输入和无输入的AVFormatContext)

    01FFMPEG的AVFormatContext结构体分析和输出时AVFormatContext的初始化 包含有输入和无输入的AVFormatContext 提醒 接下来对所有源码的分析都是针对于目前最新版本的avformat5 8源码 概
  • 五个实施环节

    定级 定级流程 在 信息系统安全等级保护定级指南 中 说明了定级的一般流程 1 确定作为定级对象的信息系统 2 确定业务信息安全受到破坏时所侵害的客体 3 根据不同的受侵害客体 从多个方面综合评定业务信息安全被破坏对客体的侵害程度 4 根据
  • 专项-弱网络测试

    弱网络 简单理解 网络不好 网络环境复杂 使用场景多变 异常逻辑检查 弱网络测什么 测试标准 客户端的核心场景必须有断线重连机制 并在有网络抖动 延时 丢包的网络场景下 客户端需达到以下要求 一 不能出现以下现象 1 游戏中不能出现收支不等
  • Lloyd-Max条件、DPCM系统最佳预测系数推导以及最小二乘:梯度下降、牛顿法、高斯牛顿法总结

    文章目录 1 Lloyd Max条件推导 2 DPCM系统最佳预测系数推导 3 最小二乘 梯度下降 牛顿法 高斯牛顿法总结 3 1 梯度下降 3 2 牛顿法 3 3 高斯牛顿法 1 Lloyd Max条件推导 2 DPCM系统最佳预测系数推
  • Kimera-VIO-ROS,Kimera-Semantic源码运行结果及问题解决

    前期条件准备 1 VMware Ubuntu18 04 2 ROS rviz melodic 3 Eigen3 3 7 一 Kimera VIO ROS 1 源码运行 Polygon mode solid Polygon mode wire
  • vue+springboot中使用ueditor,路由跳转后再进入ueditor,ueditor无法加载出来

    问题描述 已经在标题上了 解决办法 1 在页面销毁时删除ueditor实例 mounted时创建实例 这样做的目的是再次进来时重新加载ueditor mounted 自定义上传路径 const baseUrl http localhost
  • deebot扫地机器人响四声_科沃斯扫地机器人故障声叫4声什么故障_科沃斯扫地机器人故障...

    科沃斯扫地机器人初次使用时 请多多陪同 这样能及时发现问题 解决问题 常见故障二 地宝出现语音提示 运行困难 请检查驱动轮 解决方法 关机后滑动驱动轮 检查其能否转动 回弹 并清理干净 然后把地宝放回原地 点击主机启动键即可继续工作 常见
  • APK的安装流程

    文章目录 我们来思考一下Android系统是如何安装一个APK文件的 从直观的流程上 当我们点击一个APK文件或者从应用商店下载一个APK文件 会弹起一个安装对话框 点击安装就可以安装应用 那么这里面的流程是什么样的呢 首先很容易想到的是