背景
你是否知道APK是如何进行装载的?又是否知道APK具体的安装原理。当你以此为契机查阅各种资料的时候,发现各不相同,抓不到核心部分,找不到原因,很容易陷入萌萌哒状态。
仔细想想,平日能接触到的APK安装场景,主要有四种。针对每一种为出发点,有不同的应对策略,这也是为什么能查阅到的资料各不相同的原因。场景如下
APK安装场景 | 是否有安装页面 |
---|---|
系统启动时Launcher安装 | 无 |
各大Market安装 | 无 |
下载安装(手动触发同意安装操作) | 有 |
ADB 安装 | 无 |
因此此文章的目的有两个:
1、了解APK的核心安装原理
2、了解各场景下,到达核心安装状态的应对过程原理
运行APP的条件
当所有系统环境都准备好,安装一个APK后,运行起这个APP需要什么条件?可以借助Activity的启动过程来略窥一二,可以简述为以下过程:
- AMS从PMS获取要启动的Activity的启动信息
- AMS需要确认此Activity所运行的进程有没有启动,没有则需要请求Zygote孵化
- 以上两过程正常,执行Activity启动流程
实际上,PMS加载APK主要完成三件事情:
- 解析AndroidManifest.xml,拿到构成此APP的各组件信息,以及启动信息
- 为每个APP分配UID、GID,以此创建APP运行的进程,这涉及Android的沙箱模型,可以理解为应用程序资源归属问题的解决
- 更新应用程序权限
因此,所有不同的APK安装场景,在完成了各自必要的准备后,均需完成上述三件事情。这是殊途同归的过程。
先说同归,再续殊途。
note: 文章源码版本为8.0
同归
先了解Package类
public final static class Package implements Parcelable {
public String packageName;
public String manifestPackageName;
// 分包策略下个各个APK名
public String[] splitNames;
// APK 存储路径,分包策略下为文件夹
public String codePath;
/**
* 权限信息和组件信息
*/
public final ArrayList<Permission> permissions = new ArrayList<Permission>(0);
public final ArrayList<PermissionGroup> permissionGroups = new ArrayList<PermissionGroup>(0);
public final ArrayList<Activity> activities = new ArrayList<Activity>(0);
public final ArrayList<Activity> receivers = new ArrayList<Activity>(0);
public final ArrayList<Provider> providers = new ArrayList<Provider>(0);
public final ArrayList<Service> services = new ArrayList<Service>(0);
public final ArrayList<Instrumentation> instrumentation = new ArrayList<Instrumentation>(0);
......
}
从APK中解析出的信息将存于Package。需要注意codePath区分分包的情况,即5.0后,为了解决65536问题,将APK拆成多个APK策略。以及Activity、Service、Provider等并不是日常所用的相应组件,而是存储了对应组件信息的信息聚合类,大概如下
public final static class Activity extends Component<ActivityIntentInfo> implements Parcelable
第一步 解析AndroidManifest.xml
第一件事情,解析AndroidManifest.xml,直接定位 PackageParser.parsePackage()
public Package parsePackage(File packageFile, int flags, boolean useCaches)
throws PackageParserException {
......
if (packageFile.isDirectory()) {
parsed = parseClusterPackage(packageFile, flags);
} else {
parsed = parseMonolithicPackage(packageFile, flags);
}
.....
return parsed;
}
Android中存在各种包、如APK、Jar、so等均以静态文件的形式存在,需要对各样的包进行包管理。但包管理在内存中进行,因此需要PackageParser将各种包转换为内存中的数据结构。
上面代码片段if()是针对是否使用分包策略的不同执行路径,但均通过此调用路径
-> parseBaseApk()
-> parseBaseApk()
-> parseBaseApkCommon()
private Package parseBaseApkCommon(Package pkg, Set<String> acceptedTags, Resources res,
XmlResourceParser parser, int flags, String[] outError) throws XmlPullParserException,
IOException {
......
// 解析<manifest>节点
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
&& (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
......
if (tagName.equals(TAG_APPLICATION)) {
......
// 解析application标签
if (!parseBaseApplication(pkg, res, parser, flags, outError)) {
return null;
}
}
......
} else if (tagName.equals(TAG_USES_PERMISSION)) {
// 解析 uses-permission标签
if (!parseUsesPermission(pkg, res, parser)) {
return null;
}
}
......
}
......
}
上述通过XmlResourceParser逐步解析AndroidManifest.xml文件中 Manifest 节点个字节点信息,如parseBaseApplication()解析 application 节点信息等,解析出的数据存于pkg中。具体包含的信息大致如图:
xml具体解析的细枝末节就不再深入。在parseBaseApkCommon()执行完后,能拿到最关键的信息分别是 application 节点下的各组成APP的组件信息,以及 uses-permission 和 manifest 下拿到的UID、GID、GIDS、share uid信息,这将决定能访问的资源
第二步,分配进程UID
定位PMS.scanPackageDirtyLI()
private PackageParser.Package scanPackageDirtyLI(PackageParser.Package pkg,
final int policyFlags, final int scanFlags, long currentTime, @Nullable UserHandle user)
throws PackageManagerException {
......
PackageSetting pkgSetting = null;
......
synchronized (mPackages) {
if (pkg.mSharedUserId != null) {
// SIDE EFFECTS; may potentially allocate a new shared user
// 如果应用程序与其它应用程序共享一个UID,找到这个被共享的UID
suid = mSettings.getSharedUserLPw(
pkg.mSharedUserId, 0 /*pkgFlags*/, 0 /*pkgPrivateFlags*/, true /*create*/);
}
......
pkgSetting = mSettings.getPackageLPr(pkg.packageName);
if (pkgSetting != null && pkgSetting.sharedUser != suid) {
/**
* 走到这说明,PS用来描述一个与其它应用程序共享同一个UID的应用程序应用信息
* 如果 shareUser 不同,说明这个PS不能用来描述当前应用信息,置空,让后面创建
*/
......
pkgSetting = null;
}
......
if (pkgSetting == null) {
final String parentPackageName = (pkg.parentPackage != null)
? pkg.parentPackage.packageName : null;
final boolean instantApp = (scanFlags & SCAN_AS_INSTANT_APP) != 0;
// 创建PackageSetting
pkgSetting = Settings.createNewSetting(pkg.packageName, origPackage,
disabledPkgSetting, realName, suid, destCodeFile, destResourceFile,
pkg.applicationInfo.nativeLibraryRootDir, pkg.applicationInfo.primaryCpuAbi,
pkg.applicationInfo.secondaryCpuAbi, pkg.mVersionCode,
pkg.applicationInfo.flags, pkg.applicationInfo.privateFlags, user,
true /*allowInstall*/, instantApp, parentPackageName,
pkg.getChildPackageNames(), UserManagerService.getInstance(),
usesStaticLibraries, pkg.usesStaticLibrariesVersions);
if (origPackage != null) {
// 更新rename表
mSettings.addRenamedPackageLPw(pkg.packageName, origPackage.name);
}
// 分配 uid
mSettings.addUserToSettingLPw(pkgSetting);
} else {
/**
走到这里,说明是旧版本升级,更新信息
*/
Settings.updatePackageSetting(pkgSetting, disabledPkgSetting, suid, destCodeFile,
pkg.applicationInfo.nativeLibraryDir, pkg.applicationInfo.primaryCpuAbi,
pkg.applicationInfo.secondaryCpuAbi, pkg.applicationInfo.flags,
pkg.applicationInfo.privateFlags, pkg.getChildPackageNames(),
UserManagerService.getInstance(), usesStaticLibraries,
pkg.usesStaticLibrariesVersions);
}
......
}
......
else {
final int userId = user == null ? 0 : user.getIdentifier();
// 更新信息,也就是将四大组件信息、权限信息、uid、gids等交给PMS
commitPackageSettings(pkg, pkgSetting, user, scanFlags,
(policyFlags & PackageParser.PARSE_CHATTY) != 0 /*chatty*/);
if (pkgSetting.getInstantApp(userId)) {
mInstantAppRegistry.addInstantAppLPw(userId, pkgSetting.appId);
}
}
return pkg;
}
上述代码有两个类做简要了解:
- 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访问
(根据沙盒模型,share user id 可以理解为两个应用程序公用UID,因此可以共享资源)
接下来仅对分配UID进行追踪。见
Setting.addUserToSettingLPw()
Setting.newUserIdLPw()
private int newUserIdLPw(Object obj) {
// 分配uid
final int N = mUserIds.size();
for (int i = mFirstAvailableUid; i < N; i++) {
if (mUserIds.get(i) == null) {
mUserIds.set(i, obj);
return Process.FIRST_APPLICATION_UID + I;
}
}
// None left?
if (N > (Process.LAST_APPLICATION_UID-Process.FIRST_APPLICATION_UID)) {
return -1;
}
mUserIds.add(obj);
return Process.FIRST_APPLICATION_UID + N;
}
一般情况,为应用程序分配LAST_APPLICATION_UID - FIRST_APPLICATION_UID之间的UID,小于LAST_APPLICATION_UID的UID给特权用户使用,能以共享的方式被应用程序使用。应用进程安装后,uid是不变的,可以通过adb命令查看:
adb shell ps | grap packageName
第三步 权限更新
APP在运行过程中,需要不断地访问系统资源以及使用资源。在第一步解析AndroidManifest.xml时,uses-permission、permission 节点下申请的权限已做记录但还未授权,需要对权限状态进行更新。常见的权限如网络权限、文件读写权限等;危险权限则如定位权限、摄像头权限等,也包括自定义权限。
权限状态更新定位 PMS.grantPermissionsLPw()
private void grantPermissionsLPw(PackageParser.Package pkg, boolean replace,
String packageOfInterest) {
final PackageSetting ps = (PackageSetting) pkg.mExtras;
if (ps == null) {
return;
}
PermissionsState permissionsState = ps.getPermissionsState();
......
// 设置全局能具有的资源访问权限
permissionsState.setGlobalGids(mGlobalGids);
final int N = pkg.requestedPermissions.size();
for (int i=0; i<N; i++) {
......
// SDK23才支持权限动态申请
final boolean appSupportsRuntimePermissions = pkg.applicationInfo.targetSdkVersion
>= Build.VERSION_CODES.M;
......
// 权限等级
final int level = bp.protectionLevel & PermissionInfo.PROTECTION_MASK_BASE;
switch (level) {
// normal 等级通过
case PermissionInfo.PROTECTION_NORMAL: {
grant = GRANT_INSTALL;
} break;
// dangerous等级,
case PermissionInfo.PROTECTION_DANGEROUS: {
if (!appSupportsRuntimePermissions && !mPermissionReviewRequired) {
grant = GRANT_INSTALL;
} else if (origPermissions.hasInstallPermission(bp.name)) {
grant = GRANT_UPGRADE;
} else if (mPromoteSystemApps
&& isSystemApp(ps)
&& mExistingSystemPackages.contains(ps.name)) {
grant = GRANT_UPGRADE;
} else {
grant = GRANT_RUNTIME;
}
} break;
// 签名等级,需要看应用程序的签名来看非法性
case PermissionInfo.PROTECTION_SIGNATURE: {
allowedSig = grantSignaturePermission(perm, pkg, bp, origPermissions);
if (allowedSig) {
grant = GRANT_INSTALL;
}
} break;
}
......
if (grant != GRANT_DENIED) {
......
// 根据grant来更新授权状态
switch (grant) {
.....
}
}
}
权限文件位于 /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:未获得授权
上面代码可以简述为,根据所申请的等级以及系统状态,来确认授权策略。当然,在得知授权策略后,还要进行记录更新。在上述代码switch (grant)里,会调用PermissionsState.updatePermissionFlags()进行更新记录, 这里不做展开。
小结
APK安装核心步骤为:
- 从AndroidManifest中解析出应用信息、各组件信息、权限信息
- 为应用程序分配UID,并让PMS记录个组件信息,AMS启动四大组件时,需要这些信息
- 更新应用程序权限信息,授权应用程序资源访问权
如果仅对APK安装核心部分感兴趣,看到这里已经可以结束。
殊途
当前遗留的问题时,APK安装的核心步骤,是散落的,缺少控制逻辑。而之后的文章篇幅,则是针对各种APK安装场景进行跟踪阐述,了解在进行核心步骤时,都经历了怎样的过程。
场景顺序为系统启动安装、第三方应用安装、ABD安装、Martket安装,每一个场景可以视为独立章节。
系统启动安装
System进程在启动时,会初始化系统运行时的各种环境参数、并启动各种辅助。在启动Boot服务时,激活PMS.main()创建PMS,并注册入ServiceManager中。
public PackageManagerService(Context context, Installer installer,
boolean factoryTest, boolean onlyCore) {
......
/**
* 系统每次启动时,都会重新安装一遍系统中的应用程序,但是有些应用程序信息每次安装都需要保持一致,
* 如UID
* 因此需要Setting来保存
*/
mSettings = new Settings(mPackages);
......
synchronized (mPackages) {
......
// /data目录
// 获取数据目录 /data
File dataDir = Environment.getDataDirectory();
// 获取用户自己安装的应用程序目录 /data/app
mAppInstallDir = new File(dataDir, "app");
// 获取受DRM保护的私有应用程序目录 /data/app-private
mDrmAppPrivateInstallDir = new File(dataDir, "app-private");
......
// 读取恢复上一次安装应用程序信息
mFirstBoot = !mSettings.readLPw(sUserManager.getUsers(false));
// 加载FrameWork资源,即资源文件,不包含执行代码
// 先获取系统目录/system , 拿到路径/system/framework
File frameworkDir = new File(Environment.getRootDirectory(), "framework");
......
// 资源文件
scanDirTracedLI(frameworkDir, mDefParseFlags
| PackageParser.PARSE_IS_SYSTEM
| PackageParser.PARSE_IS_SYSTEM_DIR
| PackageParser.PARSE_IS_PRIVILEGED,
scanFlags | SCAN_NO_DEX, 0);
// 安装受DRM保护的私有程序
final File privilegedAppDir = new File(Environment.getRootDirectory(), "priv-app");
scanDirTracedLI(privilegedAppDir, mDefParseFlags
| PackageParser.PARSE_IS_SYSTEM
| PackageParser.PARSE_IS_SYSTEM_DIR
| PackageParser.PARSE_IS_PRIVILEGED, scanFlags, 0);
// 安装系统自带程序
final File systemAppDir = new File(Environment.getRootDirectory(), "app");
scanDirTracedLI(systemAppDir, mDefParseFlags
| PackageParser.PARSE_IS_SYSTEM
| PackageParser.PARSE_IS_SYSTEM_DIR, scanFlags, 0);
// 保存的是设备厂商提供的应用程序
File vendorAppDir = new File("/vendor/app");
......
// 安装厂商自带程序
scanDirTracedLI(vendorAppDir, mDefParseFlags
| PackageParser.PARSE_IS_SYSTEM
| PackageParser.PARSE_IS_SYSTEM_DIR, scanFlags, 0);
......
if (!mOnlyCore) {
......
// 安装用户程序
scanDirTracedLI(mAppInstallDir, 0, scanFlags | SCAN_REQUIRE_KNOWN, 0);
......
}
......
// 更新所有应用权限,根据 updateFlags = UPDATE_PERMISSIONS_ALL 来标识更新所有
updatePermissionsLPw(null, null, StorageManager.UUID_PRIVATE_INTERNAL, updateFlags);
......
// 保存应用程序的安装信息
mSettings.writeLPr();
......
}
系统在每次启动时,都会重新安装所有应用程序,共有四种类型的应用程序,分别存于不同文件夹之下:
- /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。
private void readPackageLPw(XmlPullParser parser) throws XmlPullParserException, IOException {
......
else if (userId > 0) {
packageSetting = addPackageLPw(name.intern(), realName, new File(codePathStr),
new File(resourcePathStr), legacyNativeLibraryPathStr, primaryCpuAbiString,
secondaryCpuAbiString, cpuAbiOverrideString, userId, versionCode, pkgFlags,
pkgPrivateFlags, parentPackageName, null /*childPackageNames*/,
null /*usesStaticLibraries*/, null /*usesStaticLibraryVersions*/);
......
}
......
}
PackageSetting addPackageLPw(String name, String realName, File codePath, File resourcePath,
String legacyNativeLibraryPathString, String primaryCpuAbiString,
String secondaryCpuAbiString, String cpuAbiOverrideString, int uid, int vc, int
pkgFlags, int pkgPrivateFlags, String parentPackageName,
List<String> childPackageNames, String[] usesStaticLibraries,
int[] usesStaticLibraryNames) {
// key value形式,能通过name获取到 PackageSetting
PackageSetting p = mPackages.get(name);
if (p != null) {
if (p.appId == uid) {
return p;
}
PackageManagerService.reportSettingsProblem(Log.ERROR,
"Adding duplicate package, keeping first: " + name);
return null;
}
p = new PackageSetting(name, realName, codePath, resourcePath,
legacyNativeLibraryPathString, primaryCpuAbiString, secondaryCpuAbiString,
cpuAbiOverrideString, vc, pkgFlags, pkgPrivateFlags, parentPackageName,
childPackageNames, 0 /*userId*/, usesStaticLibraries, usesStaticLibraryNames);
p.appId = uid;
// 分配 uid
if (addUserIdLPw(uid, p, name)) {
mPackages.put(name, p);
return p;
}
return null;
}
Package.xml文件保存
见Settings.writeLPr()
void writeLPr() {
/**
* 如果mSettingsFilename存在,并且备份文件不存在,将其作为备份
*
* 否认则删除,新建
*/
if (mSettingsFilename.exists()) {
// package.xml存在
if (!mBackupSettingsFilename.exists()) {
// package-backup.xml不存在,用当前的package.xml作为备份
if (!mSettingsFilename.renameTo(mBackupSettingsFilename)) {
Slog.wtf(PackageManagerService.TAG,
"Unable to backup package manager settings, "
+ " current changes will be lost at reboot");
return;
}
} else {
// 删除,后面会重写
mSettingsFilename.delete();
Slog.w(PackageManagerService.TAG, "Preserving older settings backup");
}
}
......
package.xml的保存,除了根据规则生成xml外,还做了备份处理,备份文件路径为/data/system/package-backup.xml。备份文件是为了防止package.xml在写入过程中防止意外中断而做的保障。
scanDirTracedLI()
解析安装各类型的应用程序就比较好理解了,当前拿到的是Director,因此逐个分出安装文件进行安装。见
PMS.scanDirTracedLI()
-> scanDirLI()
private void scanDirLI(File dir, int parseFlags, int scanFlags, long currentTime) {
......
// 找出所用应用程序文件,如.apk
int fileCount = 0;
for (File file : files) {
final boolean isPackage = (isApkFile(file) || file.isDirectory())
&& !PackageInstallerService.isStageName(file.getName());
if (!isPackage) {
continue;
}
// 这里最终触发PackageParser.parseBaseApkCommon解析AndroidManifest.xm;
parallelPackageParser.submit(file, parseFlags);
fileCount++;
}
for (; fileCount > 0; fileCount--) {
......
if (errorCode == PackageManager.INSTALL_SUCCEEDED) {
// 这里就是走scanPacakgeDirtyLI() 把组件信息交给PMS的逻辑
scanPackageLI(parseResult.pkg, parseResult.scanFile, parseFlags, scanFlags,
currentTime, null);
}
......
}
// 删除无效非非系统目录下的应用程序安装文件
if ((parseFlags & PackageParser.PARSE_IS_SYSTEM) == 0 &&
errorCode == PackageManager.INSTALL_FAILED_INVALID_APK) {
logCriticalInfo(Log.WARN,
"Deleting invalid package at " + parseResult.scanFile);
removeCodePathLI(parseResult.scanFile);
}
......
}
第三方应用安装
第三方应用需要PMS向PackageHandler发送信息来驱动安装,见PMS.PackageHandler.doHandleMessage()
void doHandleMessage(Message msg) {
switch (msg.what) {
case INIT_COPY: {
HandlerParams params = (HandlerParams) msg.obj;
int idx = mPendingInstalls.size();
if (DEBUG_INSTALL) Slog.i(TAG, "init_copy idx=" + idx + ": " + params);
// 此标志位表示是否绑定了服务,默认为false
if (!mBound) {
.....
// 与DefaultContainerService连接
if (!connectToService()) {
......
// 到这里表示绑定服务失败
return;
} else {
// 加入请求到安装队列
mPendingInstalls.add(idx, params);
}
} else {
// 已绑定服务成功,添加请求,等待处理
mPendingInstalls.add(idx, params);
if (idx == 0) {
mHandler.sendEmptyMessage(MCS_BOUND);
}
}
break;
}
case MCS_BOUND: {
......
if (msg.obj != null) {
mContainerService = (IMediaContainerService) msg.obj;
......
}
if (mContainerService == null) {
if (!mBound) {
// 进到这里是不正常的状态
......
// 绑定失败,清空请求队列
mPendingInstalls.clear();
} else {
// 继续等待服务
......
}
} else if (mPendingInstalls.size() > 0) {
HandlerParams params = mPendingInstalls.get(0);
if (params != null) {
......
// 复制APK
if (params.startCopy()) {
......
// 安装成功,移除此次安装请求
if (mPendingInstalls.size() > 0) {
mPendingInstalls.remove(0);
}
// 没有安装请求了,解绑服务连接
if (mPendingInstalls.size() == 0) {
if (mBound) {
......
}
} else {
// 发送信息处理接下来的安装请求
mHandler.sendEmptyMessage(MCS_BOUND);
}
}
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
}
}
......
break;
......
}
}
上面的逻辑是这样的
INIT_COPY
HandlerParams包含了安装APK必要的参数。在收到INIT_COPY信息后,会与DefaultContainerService进行连接,DefaultContainerService负责处理文件检查与拷贝等耗时操作,与PMS运行在不同进程。在与DefaultContainerService链接后,PMS获得可转为IMediaContainerService(AIDL)的Binder,可以用来与DefaultContainerService通信。如果链接已建立,将HandlerParams加入安装任务队列。连接操作见PackageHandler.connectToService
private boolean connectToService() {
// 此String为com.android.defcontainer.DefaultContainerService
Intent service = new Intent().setComponent(DEFAULT_CONTAINER_COMPONENT);
Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT);
// mDefContainerConn为DefaultContainerConnection
if (mContext.bindServiceAsUser(service, mDefContainerConn,
Context.BIND_AUTO_CREATE, UserHandle.SYSTEM)) {
......
mBound = true;
return true;
}
......
}
class DefaultContainerConnection implements ServiceConnection {
public void onServiceConnected(ComponentName name, IBinder service) {
if (DEBUG_SD_INSTALL) Log.i(TAG, "onServiceConnected");
final IMediaContainerService imcs = IMediaContainerService.Stub
.asInterface(Binder.allowBlocking(service));
// 发送MCS_BOUND信号
mHandler.sendMessage(mHandler.obtainMessage(MCS_BOUND, imcs));
}
......
}
注意在连接成功后,向PackageHandler发送了MCS_BOUND信号,并将imcs作为参数对象。
MCS_BOUND
收到此信号有两种可能,一者是与DefaultContainerService建立了连接,在此接收通信用的AIDL;二者是接收安装请求。
在接收安装请求时,如果mContainerService不为空,但mBound标志位没有正确设置,说明出现了异常,将安装请求队列清空。如果一切正常,将安装请求HandlerParams加入请求队列,取出位于0位置的进行startCopy()操作。在处理完后,将HandlerParams移除请求队列。如果队列还有任务,发送MCS_BOUND信息号继续执行下一条,否则发送MCS_UNBIND信号解决服务。
startCopy()
HandlerParams为抽象类,子类需实现以下三个方法
abstract void handleStartCopy() throws RemoteException;
abstract void handleServiceError();
abstract void handleReturnCode();
作为包安装请求时,实际类为InstallParams。
HandlerParams.startCopy()
final boolean startCopy() {
boolean res;
try {
// 尝试次数超限,放弃这个安装请求
if (++mRetries > MAX_RETRIES) {
.....
mHandler.sendEmptyMessage(MCS_GIVE_UP);
handleServiceError();
return false;
} else {
// 处理安装
handleStartCopy();
res = true;
}
} catch (RemoteException e) {
......
mHandler.sendEmptyMessage(MCS_RECONNECT);
res = false;
}
// 处理安装结果
handleReturnCode();
return res;
}
HandlerParams实际操作步骤为:
- 检查尝试安装次数,超过限制则放弃安装请求,发送MCS_GIVE_UP信号,调用handleServiceError()
- 调用子类handleStartCopy()处理具体安装
- 调用子类handleReturnCode()处理安装结果
复制APK
见InstallParams.handleStartCopy()
public void handleStartCopy() throws RemoteException {
......
/**
* 确定APK的安装位置
* onSd : SD卡
* onInt: 内部存储Data分区
* ephemeral: 安装到临时存储(Instant Apps 安装)
*/
final boolean onSd = (installFlags & PackageManager.INSTALL_EXTERNAL) != 0;
final boolean onInt = (installFlags & PackageManager.INSTALL_INTERNAL) != 0;
final boolean ephemeral = (installFlags & PackageManager.INSTALL_INSTANT_APP) != 0;
......
if (onInt && onSd) {
// apk 不能同时安装在SD卡和Data分区
// Check if both bits are set.
Slog.w(TAG, "Conflicting flags specified for installing on both internal and external");
ret = PackageManager.INSTALL_FAILED_INVALID_INSTALL_LOCATION;
} else if (onSd && ephemeral) {
// 冲突,Instant Apps 不能安装到SD卡中
Slog.w(TAG, "Conflicting flags specified for installing ephemeral on external");
ret = PackageManager.INSTALL_FAILED_INVALID_INSTALL_LOCATION;
} else {
// 获取少量apk信息
pkgLite = mContainerService.getMinimalPackageInfo(origin.resolvedPath, installFlags,
packageAbiOverride);
......
}
// 创建 InstallArgs
final InstallArgs args = createInstallArgs(this);
// 记住此 InstallArgs
mArgs = args;
......
else {
// 通过DefaultContainerService复制apk
ret = args.copyApk(mContainerService, true);
}
}
需要确认APK的安装位置,不同的安装位置需要构建不同的InstallArgs,InstallArgs子类有MoveInstallArgs、AsecInstallArgs、FileInstallArgs。最后通过binder让DefaultContainerService复制apk到相应位置下,具体的复制流程不进行跟踪。
apk复制后,再由InstallParams处理结果,见InstallParams.handleStartCopyhandleReturnCode()
-> processPendingInstall()
private void processPendingInstall(final InstallArgs args, final int currentStatus) {
......
if (res.returnCode == PackageManager.INSTALL_SUCCEEDED) {
// 安装前处理 , 主要是检查复制的APK状态,确保可用
args.doPreInstall(res.returnCode);
synchronized (mInstallLock) {
// 触发apk安装逻辑
installPackageTracedLI(args, res);
}
// 安装后处理
args.doPostInstall(res.returnCode, res.uid);
}
......
}
直接跟进
installPackageTracedLI()
-> installPackageLI()
private void installPackageLI(InstallArgs args, PackageInstalledInfo res){
......
// 解析apk
pkg = pp.parsePackage(tmpPackageFile, parseFlags);
......
// 安装新的APK,并更新应用权限
installNewPackageLIF(pkg, parseFlags, scanFlags | SCAN_DELETE_DATA_ON_FAILURES,
args.user, installerPackageName, volumeUuid, res, args.installReason);
......
}
这里就触发核心的安装过程。
第三方安装小结
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
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setDataAndType(Uri.parse("file://" + path),"application/vnd.android.package-archive");
context.startActivity(intent);
但在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安装过程。
ABD 安装
通过ADB 命令
adb install packagePath
可将APK安装入手机。
首先需要简单了解PM。PM全名为 package manager,是包管理工具。可以使用PM来执行应用程序的安装和查询应用包的信息、系统权限、控制应用等。PM接收到的各种各样的操作命令,大多通过PMS完成。以上的ABD命令会由PM接收。
见PM.run()
public int run(String[] args) throws RemoteException {
if (args.length < 1) {
return showUsage();
}
......
// PMS
mPm = IPackageManager.Stub.asInterface(ServiceManager.getService("package"));
......
// adb install 命令执行到这里
if ("install".equals(op)) {
return runInstall();
}
......
}
private int runInstall() throws RemoteException {
long startedTime = SystemClock.elapsedRealtime();
// 解析安装参数, InstallParams是不是很眼熟
final InstallParams params = makeInstallParams();
......
// 获取会话id
final int sessionId = doCreateSession(params.sessionParams,
params.installerPackageName, params.userId);
......
// 进行会话
Pair<String, Integer> status = doCommitSession(sessionId, false /*logSuccess*/);
//
}
private Pair<String, Integer> doCommitSession(int sessionId, boolean logSuccess){
// 创建Session
session = new PackageInstaller.Session(
mInstaller.openSession(sessionId));
// 提交安装请求到PMS
session.commit(receiver.getIntentSender());
......
}
关键代码不多,就一次性贴出来了。PM与PMS运行在不同的进程,需要IPC,借助PackageInstaller.Session进行,调用commit()。其中关系如下图
形成上述图的模版代码就不贴了。当前代码的执行路径为
Session.commit()
-> PacakgeInstallerSession.mHandler 发送 MSG_COMMIT 信号
-> PacakgeInstallerSession.mHandlerCallback 执行 commitLocked()
private void commitLocked(PackageInfo pkgInfo, ApplicationInfo appInfo){
......
// mPm 为 PMS
mPm.installStage(mPackageName, stageDir, stageCid, localObserver, params,
installerPackageName, installerUid, user, mCertificates);
}
在 PMS.installStage() 里发送了INIT_COPY 信号, 将安装请求加入队列。因此,也是用了PMS驱动APK安装的方式。
Martket安装
从Martket安装就比较简单了。Martket应用程序在下载完APK后,与PMS进行对话,调用PMS.installPackageAsUser()
public void installPackageAsUser(String originPath, IPackageInstallObserver2 observer,
int installFlags, String installerPackageName, int userId) {
......
// apk 文件
final File originFile = new File(originPath);
......
// INIT_COPY 信号
final Message msg = mHandler.obtainMessage(INIT_COPY);
final VerificationInfo verificationInfo = new VerificationInfo(
null /*originatingUri*/, null /*referrer*/, -1 /*originatingUid*/, callingUid);
// 安装请求参数
final InstallParams params = new InstallParams(origin, null /*moveInfo*/, observer,
installFlags, installerPackageName, null /*volumeUuid*/, verificationInfo, user,
null /*packageAbiOverride*/, null /*grantedPermissions*/,
null /*certificates*/, PackageManager.INSTALL_REASON_UNKNOWN);
params.setTraceMethod("installAsUser").setTraceCookie(System.identityHashCode(params));
msg.obj = params;
......
// 发送信号
mHandler.sendMessage(msg);
}
也是发送了INIT_COPY, 通过PMS驱动APK安装方式进行。
总结
通过以上分析,实际上真正的APK安装方式为两大方式,第一种方式为系统启动时安装,第二种为PMS驱动安装。
敲黑板,再做个总结
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并安装
- 更新所有应用程序权限
PMS驱动安装
- 通过INIT_COPY信号与DefaultContainerService进行连接,连接成功后发送MCS_BOUND信号
- 通过MCS_BOUND信号接收Binder,转为AILD接口(IMediaContainerService)
- 通过INIT_COPY接收HandlerParams请求数据,加入请求队列
- 通过MCS_BOUND信号处理HandlerParams请求,通过startCopy()拿到合适的APK路径,通过DefaultContainerService进行复制,最后通过handleReturnCode()最终触发核心安装逻辑
题外话
取这个文章标题很符合学过程的心境。学习之初确实发现大家写APK安装原理的起始点均不同,并且写到如何解析APK,如何分配UID就没下文了,弄得我一脸懵,很是尴尬。因此萌生了写这一篇文章的想法。
文章很长,看下来需要不少耐心。文章总体结构想了又想,省略了不少细节。
拙拙之笔,行文简陋,错误之处,函请不吝赐教
参考
《Android 系统源代码情景分析》第16章
《深入理解Android 卷2》 第4章 4.4小节
刘望舒--包管理机制
Market 安装