android 优化system分区空间小方法

9 篇文章 6 订阅
7 篇文章 0 订阅

背景

为何需要采用此种方式 ,随着android系统大版本的升级,系统本身的体积越来越大,对于必须要内置GMS包的升级项目,system分区的大小因为之前在低版本时,给得不够大,为了OTA升级,又不能修改分区的大小,那就只能各种裁剪,尝试各种减少system分区占用的方法(裁剪app及so、关闭部分app的预编译等). 经历各种折腾后,还没有达到预期的效果,最后找到了apk gz压缩编译的方式节省分区空间.

原理

此方案android源码很早就已经支持了,猜想此方案并未被广泛应用的原因,一方面,增加分区大小比较稳妥,另一方面,大部分场景下,大家以裁剪为主,不太会考虑此种方式;补充一点,apk最终解压缩安装到data分区,会占用userdata的空间.

通过对代码的追踪和实践,归纳为两个方面:

1. 压缩编译apk为.gz,达到减少apk体积的目的;

2.开机启动时,解压缩apk,安装到data分区.

方案

提到具体的方案,因为android已经实现,并未有多少难点,还是直接贴出代码位置,便于大家实操,另外,调试中也遇到一些问题.

  • 压缩编译apk代码

build/make/core/app_prebuilt_internal.mk

...

ifdef LOCAL_COMPRESSED_MODULE
  ifneq (true,$(LOCAL_COMPRESSED_MODULE))
    $(call pretty-error, Unknown value for LOCAL_COMPRESSED_MODULE $(LOCAL_COMPRESSED_MODULE))
  endif
  LOCAL_BUILT_MODULE_STEM := package.apk.gz
  ifndef LOCAL_INSTALLED_MODULE_STEM
    PACKAGES.$(LOCAL_MODULE).COMPRESSED := gz
    LOCAL_INSTALLED_MODULE_STEM := $(LOCAL_MODULE).apk.gz
  endif
else  # LOCAL_COMPRESSED_MODULE
  LOCAL_BUILT_MODULE_STEM := package.apk
  ifndef LOCAL_INSTALLED_MODULE_STEM
    LOCAL_INSTALLED_MODULE_STEM := $(LOCAL_MODULE).apk
  endif
endif  # LOCAL_COMPRESSED_MODULE
...

针对内置的app,无论是打包好的apk或者是源码编译,mk中添加LOCAL_COMPRESSED_MODULE := true即可编译为package.apk.gz包,另外,当搜索LOCAL_COMPRESSED_MODULE关键字时,发现必须关闭apk预编译.

# If the module is a compressed module, we don't pre-opt it because its final
# installation location will be the data partition.
# 启用压缩编译apk时,必须关闭apk的预编译
ifdef LOCAL_COMPRESSED_MODULE
LOCAL_DEX_PREOPT := false
endif

内置apk,Android.mk添加

LOCAL_COMPRESSED_MODULE := true

LOCAL_DEX_PREOPT := false

  • 内置stub app并解压缩apk.gz安装到data分区

做壳Stub apk比较关键,apk需要安装成功,就必定要走PKMS的流程,此处不对安装apk的正常流程做解析,只对安装stub app做些流程上的梳理.

直观的PackageManagerService中的构造方法中调用

1. installStubPackageLI(pkg, 0, scanFlags);

            // install the package to replace the stub on /system
            try {
                installStubPackageLI(pkg, 0, scanFlags);
                ps.setEnabled(PackageManager.COMPONENT_ENABLED_STATE_DEFAULT,
                        UserHandle.USER_SYSTEM, "android");
                systemStubPackageNames.remove(i);
            } catch (PackageManagerException e) {
                Slog.e(TAG, "Failed to parse uncompressed system package: " + e.getMessage());
            }

 2.通过遍历包名,得到哪些是stub app的包,然后执行stub app的安装;

/** 
* 解压安装stub app就是为了节省system分区的空间,并且注释告知我们stub app可以用一个仅仅只拥* *  
* 有AndroidManifest的app即可,如果stub app有问题,将不会使能该app并防止解压缩
*/

    private void installSystemStubPackages(@NonNull List<String> systemStubPackageNames,
            @ScanFlags int scanFlags) {
        for (int i = systemStubPackageNames.size() - 1; i >= 0; --i) {
            final String packageName = systemStubPackageNames.get(i);
            // skip if the system package is already disabled
            if (mSettings.isDisabledSystemPackageLPr(packageName)) {
                systemStubPackageNames.remove(i);
                continue;
            }
            // skip if the package isn't installed (?!); this should never happen
            final AndroidPackage pkg = mPackages.get(packageName);
            if (pkg == null) {
                systemStubPackageNames.remove(i);
                continue;
            }
            // skip if the package has been disabled by the user
            final PackageSetting ps = mSettings.mPackages.get(packageName);
            if (ps != null) {
                final int enabledState = ps.getEnabled(UserHandle.USER_SYSTEM);
                if (enabledState == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER) {
                    systemStubPackageNames.remove(i);
                    continue;
                }
            }

            // install the package to replace the stub on /system
            try {
                installStubPackageLI(pkg, 0, scanFlags);
                ps.setEnabled(PackageManager.COMPONENT_ENABLED_STATE_DEFAULT,
                        UserHandle.USER_SYSTEM, "android");
                systemStubPackageNames.remove(i);
            } catch (PackageManagerException e) {
                Slog.e(TAG, "Failed to parse uncompressed system package: " + e.getMessage());
            }

            // any failed attempt to install the package will be cleaned up later
        }

        // disable any stub still left; these failed to install the full application
        for (int i = systemStubPackageNames.size() - 1; i >= 0; --i) {
            final String pkgName = systemStubPackageNames.get(i);
            final PackageSetting ps = mSettings.mPackages.get(pkgName);
            ps.setEnabled(PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
                    UserHandle.USER_SYSTEM, "android");
            logCriticalInfo(Log.ERROR, "Stub disabled; pkg: " + pkgName);
        }
    }

3.扫描安装,如果package为stub app,即可解压缩目录;

/*****************************************************************************
 *  扫描安装Stubpackage的方法,直接完成调用decompressPackage方法对stub app解压缩
 *
 ****************************************************************************/   
 private AndroidPackage installStubPackageLI(AndroidPackage stubPkg,
            @ParseFlags int parseFlags, @ScanFlags int scanFlags)
                    throws PackageManagerException {
        if (DEBUG_COMPRESSION) {
            Slog.i(TAG, "Uncompressing system stub; pkg: " + stubPkg.getPackageName());
        }
        // uncompress the binary to its eventual destination on /data
        final File scanFile = decompressPackage(stubPkg.getPackageName(), stubPkg.getCodePath());
        if (scanFile == null) {
            throw new PackageManagerException(
                    "Unable to decompress stub at " + stubPkg.getCodePath());
        }
        synchronized (mLock) {
            mSettings.disableSystemPackageLPw(stubPkg.getPackageName(), true /*replaced*/);
        }
        removePackageLI(stubPkg, true /*chatty*/);
        try {
            return scanPackageTracedLI(scanFile, parseFlags, scanFlags, 0, null);
        } catch (PackageManagerException e) {
            Slog.w(TAG, "Failed to install compressed system package:" + stubPkg.getPackageName(),
                    e);
            // Remove the failed install
            removeCodePathLI(scanFile);
            throw e;
        }
    }

4. 解压缩.gz 的apk,创建相应的目录,是stub app安装到data分区,直接调用了PackageManagerServiceUtils.getCompressedFiles(codePath);

    /**
     * Decompresses the given package on the system image onto
     * the /data partition.
     * @return The directory the package was decompressed into. Otherwise, {@code null}.
     */

    /** 解压压缩编译apk后,安装到data分区下的*/

    private File decompressPackage(String packageName, String codePath) {
        final File[] compressedFiles = getCompressedFiles(codePath);
        if (compressedFiles == null || compressedFiles.length == 0) {
            if (DEBUG_COMPRESSION) {
                Slog.i(TAG, "No files to decompress: " + codePath);
            }
            return null;
        }
        final File dstCodePath =
                getNextCodePath(Environment.getDataAppDirectory(null), packageName);
        int ret = PackageManager.INSTALL_SUCCEEDED;
        try {
            makeDirRecursive(dstCodePath, 0755);
            for (File srcFile : compressedFiles) {
                final String srcFileName = srcFile.getName();
                final String dstFileName = srcFileName.substring(
                        0, srcFileName.length() - COMPRESSED_EXTENSION.length());
                final File dstFile = new File(dstCodePath, dstFileName);
                ret = decompressFile(srcFile, dstFile);
                if (ret != PackageManager.INSTALL_SUCCEEDED) {
                    logCriticalInfo(Log.ERROR, "Failed to decompress"
                            + "; pkg: " + packageName
                            + ", file: " + dstFileName);
                    break;
                }
            }
        } catch (ErrnoException e) {
            logCriticalInfo(Log.ERROR, "Failed to decompress"
                    + "; pkg: " + packageName
                    + ", err: " + e.errno);
        }
        if (ret == PackageManager.INSTALL_SUCCEEDED) {
            final File libraryRoot = new File(dstCodePath, LIB_DIR_NAME);
            NativeLibraryHelper.Handle handle = null;
            try {
                handle = NativeLibraryHelper.Handle.create(dstCodePath);
                ret = NativeLibraryHelper.copyNativeBinariesWithOverride(handle, libraryRoot,
                        null /*abiOverride*/, false /*isIncremental*/);
            } catch (IOException e) {
                logCriticalInfo(Log.ERROR, "Failed to extract native libraries"
                        + "; pkg: " + packageName);
                ret = PackageManager.INSTALL_FAILED_INTERNAL_ERROR;
            } finally {
                IoUtils.closeQuietly(handle);
            }
        }
        if (ret != PackageManager.INSTALL_SUCCEEDED) {
            if (!dstCodePath.exists()) {
                return null;
            }
            removeCodePathLI(dstCodePath);
            return null;
        }

        return dstCodePath;
    }

5.系统如何判定stub app的条件,相当于告知我们如何创建一个stub app.

    public static File[] getCompressedFiles(String codePath) {
        final File stubCodePath = new File(codePath);
        final String stubName = stubCodePath.getName();

        // The layout of a compressed package on a given partition is as follows :
        //
        // Compressed artifacts:
        //
        // /partition/ModuleName/foo.gz
        // /partation/ModuleName/bar.gz
        //
        // Stub artifact:
        //
        // /partition/ModuleName-Stub/ModuleName-Stub.apk
        //
        // In other words, stub is on the same partition as the compressed artifacts
        // and in a directory that's suffixed with "-Stub".

       //上面的注释很关键,告知我们如何创建一个stub app的方法
        int idx = stubName.lastIndexOf(STUB_SUFFIX);
        if (idx < 0 || (stubName.length() != (idx + STUB_SUFFIX.length()))) {
            return null;
        }

        final File stubParentDir = stubCodePath.getParentFile();
        if (stubParentDir == null) {
            Slog.e(TAG, "Unable to determine stub parent dir for codePath: " + codePath);
            return null;
        }

        final File compressedPath = new File(stubParentDir, stubName.substring(0, idx));
        final File[] files = compressedPath.listFiles(new FilenameFilter() {
            @Override
            public boolean accept(File dir, String name) {
                return name.toLowerCase().endsWith(COMPRESSED_EXTENSION);
            }
        });

        if (DEBUG_COMPRESSION && files != null && files.length > 0) {
            Slog.i(TAG, "getCompressedFiles[" + codePath + "]: " + Arrays.toString(files));
        }

        return files;
    }

以上,基本思路是很简单的,主要是Google都已经给实现了,只不过很少被人这样来操作罢了.

总结

思路不难,但是因为开发过程中,思考的方式或者说习惯以增加system分区来解决空间吃紧问题,google大版本升级,他们自然也考虑到了这一点,包括android10上启用的动态分区,都是体现.当然,如果预置签名的app,这个也是需要考虑到的状态,本人就有遇到,采取绕过签名的方式处理,具体细节就不在此赘述. 如果有朋友有需要,可以私聊,这个稍微看下代码即可搞定.

  • 4
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 8
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值