PackageManagerService(2)扫描目录并解析APK

系统首次开机APK扫描解析
深入探讨系统首次开机时,PackageManagerService如何扫描并解析APK,包括创建缓存目录、解析AndroidManifest.xml、处理OverlayAPK及扫描结果的保存过程。

PackageManagerService(1)扫描前的准备 文章中,分析了 PKMS ( PackageManagerService ) 在扫描之前的准备工作,这篇文章来分析扫描目录并解析APK的过程。

扫描目录并解析APK这个过程,伴随着大量升级相关的代码,但是我要提醒一句,我们现在分析的目标是系统首次开机,并不会把系统升级的代码混淆在一起分析。

扫描目录,解析APK

    public PackageManagerService(Context context, Installer installer,
                                 boolean factoryTest, boolean onlyCore) {
   
   
        synchronized (mInstallLock) {
   
   
            // writer
            synchronized (mPackages) {
   
   
                // 判断是否首次开机
                mFirstBoot = !mSettings.readLPw(sUserManager.getUsers(false));

                // 承接上文继续分析...

                // 创建缓存目录,用于缓存扫描结果
                mCacheDir = preparePackageParserCache(); 

                int scanFlags = SCAN_BOOTING | SCAN_INITIAL;

                if (mIsUpgrade || mFirstBoot) {
   
   
                    // 注意,首次开机还要加上这个扫描flag
                    scanFlags = scanFlags | SCAN_FIRST_BOOT_OR_UPGRADE;
                }                

                // 扫描目录并解析APK
                //先扫描Overlay APK
                scanDirTracedLI(new File(VENDOR_OVERLAY_DIR),
                        mDefParseFlags | PackageParser.PARSE_IS_SYSTEM_DIR,
                        scanFlags | SCAN_AS_SYSTEM | SCAN_AS_VENDOR,
                        0);

                // 还会扫描其它分区的overlay目录,例如/product/overlay, /product_services/overlay 等等。
                // ...

                // 保存静态overlay包的信息,这些信息会在后面解析APK时用到
                mParallelPackageParserCallback.findStaticOverlayPackages();

                // /system/framework目录,这个目录下只有一个apk,那就是framework-res.apk
                scanDirTracedLI(frameworkDir,
                        mDefParseFlags | PackageParser.PARSE_IS_SYSTEM_DIR,
                        scanFlags | SCAN_NO_DEX | SCAN_AS_SYSTEM | SCAN_AS_PRIVILEGED,
                        0);

                // 扫描其它分区的目录, 方法与上面的一致....

            }
        }
    }

在扫描之前,会通过preparePackageParserCache()创建一个缓存目录,用来缓存扫描APK结果。如此一来,下次开机,如果缓存有效,就可以直接从缓存中获取apk信息,而不用再扫描APK了,这样加快了不是首次开机的速度。

开始扫描APK时,首先扫描的是Overlay APK,目录有/vendor/overlay, /product/overlay, /product_services/overlay, /odm/overlay, /oem/overlay。因此如果你想生成Overlay APK,那么必须编译到这些目录下。

什么是Overlay APK?Overlay APK是用来覆盖目标APK的资源的,然而仅限于res/values目录下的资源,例如替换应用的名字,主题等等。因此也称为运行时资源覆盖(Runtime Resource Overlay)。

然而,为了保证RRO生效,需要在使用静态的RRO,例如

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.qualcomm.qti.optinoverlay">
    <overlay
        android:isStatic="true"
        android:priority="1"
        android:targetPackage="com.qualcomm.location.XT" />
</manifest>

其中isStatic属性为true表示这个RRO是静态的,它会覆盖目标应用(targetPackage属性指定的应用)的资源。

mParallelPackageParserCallback.findStaticOverlayPackages()方法保存的就是静态RRO。这个将在后面解析APK时用到。

本文的目标是PackageManagerService扫描并解析APK,至于Overlay APK的代码,还请大家自行分析。

那么现在我们来看看scanDirTracedLI()是如何扫描目录并解析APK的

    private void scanDirLI(File scanDir, int parseFlags, int scanFlags, long currentTime) {
   
   
        final File[] files = scanDir.listFiles();
        if (ArrayUtils.isEmpty(files)) {
   
   
            Log.d(TAG, "No files in app dir " + scanDir);
            return;
        }
        /**
         * mCacheDir用于缓存apk信息
         * mParallelPackageParserCallback用于在解析时提供overlay资源
         */
        try (ParallelPackageParser parallelPackageParser = new ParallelPackageParser(
                mSeparateProcesses, mOnlyCore, mMetrics, mCacheDir,
                mParallelPackageParserCallback)) {
   
   
            int fileCount = 0;
            for (File file : files) {
   
   
                // 必须是.apk或者目录
                final boolean isPackage = (isApkFile(file) || file.isDirectory())
                        && !PackageInstallerService.isStageName(file.getName());
                if (!isPackage) {
   
   
                    continue;
                }
                // 1. 提交APK文件,并行解析
                parallelPackageParser.submit(file, parseFlags);
                fileCount++;
            }

            // 2. 处理扫描结果
            for (; fileCount > 0; fileCount--) {
   
   
                // APK数据保存在ParseResult.pkg中,类型为PackageParser.Package
                ParallelPackageParser.ParseResult parseResult = parallelPackageParser.take();
                Throwable throwable = parseResult.throwable;
                int errorCode = PackageManager.INSTALL_SUCCEEDED;

                if (throwable == null) {
   
   
                    // 使用<static-library>生成的静态库,需要重新设置包名,形式为package_libVersion
                    if (parseResult.pkg.applicationInfo.isStaticSharedLibrary()) {
   
   
                        renameStaticSharedLibraryPackage(parseResult.pkg);
                    }
                    try {
   
   
                        // 扫描,并返回一个新的结果
                        // 注意最后一个参数user,值为null
                        scanPackageChildLI(parseResult.pkg, parseFlags, scanFlags,
                                currentTime, null);
                    } 
                    // ...
                } 
				// ...
            }
        }
    }

ParallelPackageParser 是一个并行的APK解析器,通过ParallelPackageParser#submit() 方法,向线程池提交待解析的APK,然后通过ParallelPackageParser#take() 来不断地获取结果,用于扫描处理。

提交并解析APK

现在来看下 ParallelPackageParser#submit() 如何提交并解析APK

    public void submit(File scanFile, int parseFlags) {
   
   
        mService.submit(() -> {
   
   
            // ParseResult只有三个变量
            // pkg变量保存扫描的结果
            // scanFile保存扫描的文件
            // throwable保存扫描出现的异常
            ParseResult pr = new ParseResult();
            Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "parallel parsePackage [" + scanFile + "]");
            try {
   
   
                PackageParser packageParser = new PackageParser();
                packageParser.setSeparateProcesses(mSeparateProcesses);
                packageParser.setOnlyCoreApps(mOnlyCore);
                packageParser.setDisplayMetrics(mMetrics);
                packageParser.setCacheDir(mCacheDir);
                packageParser.setCallback(mPackageParserCallback);
                pr.scanFile = scanFile;
                // 通过PackageParser解析APK,并返回解析结果
                // 解析结果的类型为 PackageParser.Package
                pr.pkg = parsePackage(packageParser, scanFile, parseFlags);
            } catch (Throwable e) {
   
   
                pr.throwable = e;
            } finally {
   
   
                Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
            }
            try {
   
   
                // 把解析的结果添加到队列中
                mQueue.put(pr);
            } catch (InterruptedException e) {
   
   
                Thread.currentThread().interrupt();
                mInterruptedInThread = Thread.currentThread().getName();
            }
        });
    }

这里向线程池中提交了一个请求,在这个请求中,会通过parsePackage()解析APK,然后把解析结果添加到一个类型为BlockingQueuemQueue中。后面在处理解析结果的时候,那个解析结果就是从mQueue中获取的。

现在来看下 parsePackage() 是如何解析APK的

    protected PackageParser.Package parsePackage(PackageParser packageParser, File scanFile,
            int parseFlags) throws PackageParser.PackageParserException {
   
   
        // 第三个参数表示是否使用缓存,这样会提升开机效率
        return packageParser.parsePackage(scanFile, parseFlags, true /* useCaches */);
    }

原来就是通过PackageParser#parsePackage()来解析的,注意第三个参数的值为 true ,它代表对解析结果使用缓存。

	// frameworks/base/core/java/android/content/pm/PackageParser.java
	
    public Package parsePackage(File packageFile, int flags, boolean useCaches)
            throws PackageParserException {
   
   
        // 1. 获取有效的缓存结果
        Package parsed = useCaches ? getCachedResult(packageFile, flags) : null;
        if (parsed != null) {
   
   
            return parsed;
        }

        long parseTime = LOG_PARSE_TIMINGS ? SystemClock.uptimeMillis() : 0;
        if (packageFile.isDirectory()) {
   
   
            parsed = parseClusterPackage(packageFile, flags);
        } else {
   
   
            // 2. 解析APK
            parsed = parseMonolithicPackage(packageFile, flags);
        }

        long cacheTime = LOG_PARSE_TIMINGS ? SystemClock.uptimeMillis() : 0;
        // 3. 把解析的结果进行缓存
        // 原理是把解析的数据分解为字节数组,然后写入到缓存文件中
        cacheResult(packageFile, flags, parsed);
        return parsed;
    }

从这里可以看出,整个解析过程使用了缓存,但是本文不打算分析这个缓存的机制,有兴趣的同学可以研究下,并不难。

而解析APK使用的是parseMonolithicPackage()

    public Package parseMonolithicPackage(File apkFile, int flags) throws PackageParserException {
   
   
        // 这里只是简单解析了AndroidManifest.xml的某些属性,这里主要是为了Split APK功能,本文不讨论
        final PackageLite lite = parseMonolithicPackageLite(apkFile, flags);

        // ...

        final SplitAssetLoader assetLoader = new DefaultSplitAssetLoader(lite, flags);
        try {
   
   
            final Package pkg = parseBaseApk(apkFile, assetLoader.getBaseAssetManager(), flags);
            // PackageParser.Package.codePath存储了apk的全路径
            pkg.setCodePath(apkFile.getCanonicalPath());
            pkg.setUse32bitAbi(lite.use32bitAbi);
            return pkg;
        }

        // ...
    }

这一步解析加入了Split APK解析功能,如果大家想了解Split APK功能是如何实现的,看完本文可自行分析。

那么接下来是使用parseBaseApk()来继续解析APK

    private Package parseBaseApk(File apkFile, AssetManager assets, int flags)
            throws PackageParserException {
   
   
        final String apkPath = apkFile.getAbsolutePath();

        // ...

        mParseError = PackageManager.INSTALL_SUCCEEDED;
        mArchiveSourcePath = apkFile.getAbsolutePath();

        XmlResourceParser par
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值