你逃不掉的APK安装原理

背景

你是否知道APK是如何进行装载的?又是否知道APK具体的安装原理。当你以此为契机查阅各种资料的时候,发现各不相同,抓不到核心部分,找不到原因,很容易陷入萌萌哒状态。

仔细想想,平日能接触到的APK安装场景,主要有四种。针对每一种为出发点,有不同的应对策略,这也是为什么能查阅到的资料各不相同的原因。场景如下

APK安装场景 是否有安装页面
系统启动时Launcher安装
各大Market安装
下载安装(手动触发同意安装操作)
ADB 安装

因此此文章的目的有两个:
1、了解APK的核心安装原理
2、了解各场景下,到达核心安装状态的应对过程原理

运行APP的条件

当所有系统环境都准备好,安装一个APK后,运行起这个APP需要什么条件?可以借助Activity的启动过程来略窥一二,可以简述为以下过程:

  1. AMS从PMS获取要启动的Activity的启动信息
  2. AMS需要确认此Activity所运行的进程有没有启动,没有则需要请求Zygote孵化
  3. 以上两过程正常,执行Activity启动流程

Activity启动可参考

实际上,PMS加载APK主要完成三件事情:

  1. 解析AndroidManifest.xml,拿到构成此APP的各组件信息,以及启动信息
  2. 为每个APP分配UID、GID,以此创建APP运行的进程,这涉及Android的沙箱模型,可以理解为应用程序资源归属问题的解决
  3. 更新应用程序权限

沙箱模型可参考

因此,所有不同的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中。具体包含的信息大致如图:


AndroidManifest节点信息.png

图片来源

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

整段代码实际完成三件事情:

  1. 为pkg所描述的应用程序分配UID,并更新PackageSetting,因为APK的安装涉及到新应用安装、旧版本更新以及share user id场景。新版本情况下,要准备新的PackageSetting并分配UID;旧版本更新情况下,更新PackageSetting,UID已做过分配;share user id情况下则需考虑PackageSetting的可用性
  2. 将pkg所指向的Pacakge保存到PMS
  3. 将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安装核心步骤.png

APK安装核心步骤为:

  1. 从AndroidManifest中解析出应用信息、各组件信息、权限信息
  2. 为应用程序分配UID,并让PMS记录个组件信息,AMS启动四大组件时,需要这些信息
  3. 更新应用程序权限信息,授权应用程序资源访问权

如果仅对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.xml.jpg

图片来源

实际了解关键节点的作用,即可大致知道程序如何解析。

  • 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实际操作步骤为:

  1. 检查尝试安装次数,超过限制则放弃安装请求,发送MCS_GIVE_UP信号,调用handleServiceError()
  2. 调用子类handleStartCopy()处理具体安装
  3. 调用子类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驱动安装.png

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()。其中关系如下图

PackageInstaller.Session 与 PMS 通信.png

形成上述图的模版代码就不贴了。当前代码的执行路径为
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 安装

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容