android 优化system分区空间小方法
背景为何需要采用此种方式 ,随着android系统大版本的升级,系统本身的体积越来越大,对于必须要内置GMS包的升级项目,system分区的大小因为之前在低版本时,给得不够大,为了OTA升级,又不能修改分区的大小,那就只能各种裁剪,尝试各种减少system分区占用的方法(裁剪app及so、关闭部分app的预编译等). 经历各种折腾后,还没有达到预期的效果,最后找到了apk gz压缩编译的方式节省分
背景
为何需要采用此种方式 ,随着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,这个也是需要考虑到的状态,本人就有遇到,采取绕过签名的方式处理,具体细节就不在此赘述. 如果有朋友有需要,可以私聊,这个稍微看下代码即可搞定.
更多推荐
所有评论(0)