Android 9.0 PM机制系列(四) APK安装需要空间分析
前言
在PM机制系列前三篇,我们着重分析了安装的整个流程,没有具体到很多细节问题。
这一篇文章我们就会具体到很多细节问题。本篇主要就是围绕一个问题展开:
安装APK到底需要多少空间不会报错INSTALL_FAILED_INSUFFICIENT_STORAGE?可以提高我们的安装成功率。
1. 分析结果时序图
DCS: DefaultContainerService
SMS: StorageManagerService
SM: StorageManager
调用关系图如下所示,后面会进行具体分析。
2. 开始安装
经过前三篇的安装流程,可以清楚的知道安装核心代码开始处就在handleStartCopy方法,代码如下:
/frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java
public void handleStartCopy() throws RemoteException {...// If we're already staged, we've firmly committed to an install locationif (origin.staged) {//安装时候,传参进来的origin.staged是为true,file != nullif (origin.file != null) {installFlags |= PackageManager.INSTALL_INTERNAL; //这样就会走到这里给installFlags赋值,installFlags &= ~PackageManager.INSTALL_EXTERNAL;//走内部安装逻辑} else {throw new IllegalStateException("Invalid stage location");}}/*确定APK的安装位置。onSd:安装到SD卡, onInt:内部存储即Data分区,ephemeral:安装到临时存储(Instant Apps安装)这里根据上面installFlags的赋值可知onSd=false;onInt=true;ephemeral=false;*/ 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;PackageInfoLite pkgLite = null;if (onInt && onSd) {// APK不能同时安装在SD卡和Data分区Slog.w(TAG, "Conflicting flags specified for installing on both internal and external");ret = PackageManager.INSTALL_FAILED_INVALID_INSTALL_LOCATION;//安装标志冲突,Instant Apps不能安装到SD卡中} else if (onSd && ephemeral) {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);//1if (DEBUG_EPHEMERAL && ephemeral) {Slog.v(TAG, "pkgLite for install: " + pkgLite);}...if (ret == PackageManager.INSTALL_SUCCEEDED) {//判断安装的位置int loc = pkgLite.recommendedInstallLocation;if (loc == PackageHelper.RECOMMEND_FAILED_INVALID_LOCATION) {ret = PackageManager.INSTALL_FAILED_INVALID_INSTALL_LOCATION;} else if (loc == PackageHelper.RECOMMEND_FAILED_ALREADY_EXISTS) {ret = PackageManager.INSTALL_FAILED_ALREADY_EXISTS;} else if (loc == PackageHelper.RECOMMEND_FAILED_INSUFFICIENT_STORAGE) {ret = PackageManager.INSTALL_FAILED_INSUFFICIENT_STORAGE; //2} ...}else{loc = installLocationPolicy(pkgLite);...}}//根据InstallParams创建InstallArgs对象final InstallArgs args = createInstallArgs(this);mArgs = args;if (ret == PackageManager.INSTALL_SUCCEEDED) {...if (!origin.existing && requiredUid != -1&& isVerificationEnabled(verifierUser.getIdentifier(), installFlags, installerUid)) {...} else{ret = args.copyApk(mContainerService, true);}}mRet = ret;}
handleStartCopy方法的代码很多,这里截取关键的部分。
- 注释1处通过IMediaContainerService跨进程调用DefaultContainerService的getMinimalPackageInfo方法,该方法轻量解析APK并得到APK的少量信息,轻量解析的原因是这里不需要得到APK的全部信息,APK的少量信息会封装到PackageInfoLite中。在上一篇中我们一笔带过,这里信息量很多,里面就有我们这篇文章所涉及到的安装空间判断。
- 注释2处,如果loc == PackageHelper.RECOMMEND_FAILED_INSUFFICIENT_STORAGE,就会返回INSTALL_FAILED_INSUFFICIENT_STORAGE。所以要从loc的返回值分析。下面看一下getMinimalPackageInfo方法。
/frameworks/base/packages/DefaultContainerService/src/com/android/defcontainer/DefaultContainerService.java
/*** Parse given package and return minimal details.** @param packagePath absolute path to the package to be copied. Can be* a single monolithic APK file or a cluster directory* containing one or more APKs.*/
@Override
public PackageInfoLite getMinimalPackageInfo(String packagePath, int flags,String abiOverride) {final Context context = DefaultContainerService.this;PackageInfoLite ret = new PackageInfoLite();if (packagePath == null) {Slog.i(TAG, "Invalid package file " + packagePath);ret.recommendedInstallLocation = PackageHelper.RECOMMEND_FAILED_INVALID_APK;return ret;}final File packageFile = new File(packagePath);final PackageParser.PackageLite pkg;final long sizeBytes;try {pkg = PackageParser.parsePackageLite(packageFile, 0);sizeBytes = PackageHelper.calculateInstalledSize(pkg, abiOverride); //1} catch (PackageParserException | IOException e) {Slog.w(TAG, "Failed to parse package at " + packagePath + ": " + e);if (!packageFile.exists()) {ret.recommendedInstallLocation = PackageHelper.RECOMMEND_FAILED_INVALID_URI;} else {ret.recommendedInstallLocation = PackageHelper.RECOMMEND_FAILED_INVALID_APK;}return ret;}final int recommendedInstallLocation;final long token = Binder.clearCallingIdentity();try {recommendedInstallLocation = PackageHelper.resolveInstallLocation(context,pkg.packageName, pkg.installLocation, sizeBytes, flags);//2} finally {Binder.restoreCallingIdentity(token);}ret.packageName = pkg.packageName;ret.splitNames = pkg.splitNames;ret.versionCode = pkg.versionCode;ret.versionCodeMajor = pkg.versionCodeMajor;ret.baseRevisionCode = pkg.baseRevisionCode;ret.splitRevisionCodes = pkg.splitRevisionCodes;ret.installLocation = pkg.installLocation;ret.verifiers = pkg.verifiers;ret.recommendedInstallLocation = recommendedInstallLocation;ret.multiArch = pkg.multiArch;return ret;
}
- 注释1处计算安装apk需要的空间大小。
- 注释2处是核心代码,根据apk需要的空间大小计算推荐存储位置,参数flags就是就是我们之前赋值的内部存储。
下面看一下resolveInstallLocation方法
/frameworks/base/core/java/com/android/internal/content/PackageHelper.java
@Deprecated
public static int resolveInstallLocation(Context context, String packageName,int installLocation, long sizeBytes, int installFlags) {final SessionParams params = new SessionParams(SessionParams.MODE_INVALID);params.appPackageName = packageName;params.installLocation = installLocation;params.sizeBytes = sizeBytes;params.installFlags = installFlags;try {return resolveInstallLocation(context, params); //1} catch (IOException e) {throw new IllegalStateException(e);}
}/*** Given a requested {@link PackageInfo#installLocation} and calculated* install size, pick the actual location to install the app.*/
public static int resolveInstallLocation(Context context, SessionParams params)throws IOException {ApplicationInfo existingInfo = null;try {existingInfo = context.getPackageManager().getApplicationInfo(params.appPackageName,PackageManager.MATCH_ANY_USER);} catch (NameNotFoundException ignored) {}final int prefer;final boolean checkBoth;boolean ephemeral = false;if ((params.installFlags & PackageManager.INSTALL_INSTANT_APP) != 0) {prefer = RECOMMEND_INSTALL_INTERNAL;ephemeral = true;checkBoth = false;} else if ((params.installFlags & PackageManager.INSTALL_INTERNAL) != 0) { //2prefer = RECOMMEND_INSTALL_INTERNAL;checkBoth = false;} else if ((params.installFlags & PackageManager.INSTALL_EXTERNAL) != 0) {prefer = RECOMMEND_INSTALL_EXTERNAL;checkBoth = false;} else if (params.installLocation == PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY) {prefer = RECOMMEND_INSTALL_INTERNAL;checkBoth = false;} else if (params.installLocation == PackageInfo.INSTALL_LOCATION_PREFER_EXTERNAL) {prefer = RECOMMEND_INSTALL_EXTERNAL;checkBoth = true;} else if (params.installLocation == PackageInfo.INSTALL_LOCATION_AUTO) {// When app is already installed, prefer same mediumif (existingInfo != null) {// TODO: distinguish if this is external ASECif ((existingInfo.flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE) != 0) {prefer = RECOMMEND_INSTALL_EXTERNAL;} else {prefer = RECOMMEND_INSTALL_INTERNAL;}} else {prefer = RECOMMEND_INSTALL_INTERNAL;}checkBoth = true;} else {prefer = RECOMMEND_INSTALL_INTERNAL;checkBoth = false;}boolean fitsOnInternal = false;if (checkBoth || prefer == RECOMMEND_INSTALL_INTERNAL) {fitsOnInternal = fitsOnInternal(context, params); //3}boolean fitsOnExternal = false;if (checkBoth || prefer == RECOMMEND_INSTALL_EXTERNAL) {fitsOnExternal = fitsOnExternal(context, params);}if (prefer == RECOMMEND_INSTALL_INTERNAL) { //4// The ephemeral case will either fit and return EPHEMERAL, or will not fit// and will fall through to return INSUFFICIENT_STORAGEif (fitsOnInternal) {return (ephemeral)? PackageHelper.RECOMMEND_INSTALL_EPHEMERAL: PackageHelper.RECOMMEND_INSTALL_INTERNAL; //5}} else if (prefer == RECOMMEND_INSTALL_EXTERNAL) {if (fitsOnExternal) {return PackageHelper.RECOMMEND_INSTALL_EXTERNAL;}}if (checkBoth) {if (fitsOnInternal) {return PackageHelper.RECOMMEND_INSTALL_INTERNAL;} else if (fitsOnExternal) {return PackageHelper.RECOMMEND_INSTALL_EXTERNAL;}}return PackageHelper.RECOMMEND_FAILED_INSUFFICIENT_STORAGE; //6
}
- 注释1处将我们传入的参数全部都放入SessionParams内,执行resolveInstallLocation
- 根据之前传入的flags值代码会运营到注释2处。此处赋值:
prefer = RECOMMEND_INSTALL_INTERNAL;
checkBoth = false; - 根据之前的赋值,会走到注释3处,调用fitsOnInternal()方法。
如果fitsOnInternal为true,就会走到注释5处,返回RECOMMEND_INSTALL_INTERNAL
如果fitsOnInternal为false,就会走到注释6处,返回RECOMMEND_FAILED_INSUFFICIENT_STORAGE。
下面我们看一下fitsOnInternal()方法
/frameworks/base/core/java/com/android/internal/content/PackageHelper.java
public static boolean fitsOnInternal(Context context, SessionParams params) throws IOException {final StorageManager storage = context.getSystemService(StorageManager.class);final UUID target = storage.getUuidForPath(Environment.getDataDirectory());return (params.sizeBytes <= storage.getAllocatableBytes(target,translateAllocateFlags(params.installFlags))); //1
}
- 就是判断安装需要的空间大小是否小于系统能分配的存储大小。
- translateAllocateFlags方法如下,由于installFlags并没有设置INSTALL_ALLOCATE_AGGRESSIVE标识符,所以返回0。
public static int translateAllocateFlags(int installFlags) {if ((installFlags & PackageManager.INSTALL_ALLOCATE_AGGRESSIVE) != 0) {return StorageManager.FLAG_ALLOCATE_AGGRESSIVE;} else {return 0;} }
下面就会调用到StorageManager的getAllocatableBytes方法
/frameworks/base/core/java/android/os/storage/StorageManager.java
public long getAllocatableBytes(@NonNull UUID storageUuid,@RequiresPermission @AllocateFlags int flags) throws IOException {try {return mStorageManager.getAllocatableBytes(convert(storageUuid), flags,mContext.getOpPackageName()); //1} catch (ParcelableException e) {e.maybeRethrow(IOException.class);throw new RuntimeException(e);} catch (RemoteException e) {throw e.rethrowFromSystemServer();}
}
- 注释1继续调用StorageManagerService的getAllocatableBytes方法
/frameworks/base/services/core/java/com/android/server/StorageManagerService.java
@Override
public long getAllocatableBytes(String volumeUuid, int flags, String callingPackage) {flags = adjustAllocateFlags(flags, Binder.getCallingUid(), callingPackage);final StorageManager storage = mContext.getSystemService(StorageManager.class);final StorageStatsManager stats = mContext.getSystemService(StorageStatsManager.class);final long token = Binder.clearCallingIdentity();try {// In general, apps can allocate as much space as they want, except// we never let them eat into either the minimum cache space or into// the low disk warning space. To avoid user confusion, this logic// should be kept in sync with getFreeBytes().final File path = storage.findPathForUuid(volumeUuid);final long usable = path.getUsableSpace();//1final long lowReserved = storage.getStorageLowBytes(path);//2final long fullReserved = storage.getStorageFullBytes(path);if (stats.isQuotaSupported(volumeUuid)) { //3final long cacheTotal = stats.getCacheBytes(volumeUuid);final long cacheReserved = storage.getStorageCacheBytes(path, flags);final long cacheClearable = Math.max(0, cacheTotal - cacheReserved);if ((flags & StorageManager.FLAG_ALLOCATE_AGGRESSIVE) != 0) {return Math.max(0, (usable + cacheClearable) - fullReserved);} else {return Math.max(0, (usable + cacheClearable) - lowReserved); //4}} else {// When we don't have fast quota information, we ignore cached// data and only consider unused bytes.if ((flags & StorageManager.FLAG_ALLOCATE_AGGRESSIVE) != 0) {return Math.max(0, usable - fullReserved);} else {return Math.max(0, usable - lowReserved);//5}}} catch (IOException e) {throw new ParcelableException(e);} finally {Binder.restoreCallingIdentity(token);}
}
- 注释1处会先求出分区可用的空间
- 注释2处会求出系统运行需要的最低存储
- 注释3处会 判断是否支持Quota,在9.0虚拟机上和真机上测试都是支持的。
- 注释4如果支持,就会计算cacheTotal(总共的cache大小),cacheReserved(需要保留的cache大小),求出可以清理的cache大小。返回getTotalSpace + 可清理cache大小-需要保留的
- 注释5如果不支持返回getTotalSpace的5%和500M之间的最小值
下面我们看一下getStorageLowBytes方法
/frameworks/base/core/java/android/os/storage/StorageManager.java
private static final int DEFAULT_THRESHOLD_PERCENTAGE = 5;
private static final long DEFAULT_THRESHOLD_MAX_BYTES = DataUnit.MEBIBYTES.toBytes(500);public long getStorageLowBytes(File path) {final long lowPercent = Settings.Global.getInt(mResolver,Settings.Global.SYS_STORAGE_THRESHOLD_PERCENTAGE, DEFAULT_THRESHOLD_PERCENTAGE);final long lowBytes = (path.getTotalSpace() * lowPercent) / 100;final long maxLowBytes = Settings.Global.getLong(mResolver,Settings.Global.SYS_STORAGE_THRESHOLD_MAX_BYTES, DEFAULT_THRESHOLD_MAX_BYTES);return Math.min(lowBytes, maxLowBytes);
}
我们发现。系统设置的默认阈值是5%,最终返回值为getTotalSpace的5%和500M之间的最小值。
总结
至此,算是全部结束了,我们发现只要系统空间小于Math.min(getTotalSpace的5%,500M)+ PackageHelper.calculateInstalledSize(pkg, abiOverride),系统就会报空间不足。
Android 9.0 PM机制系列(四) APK安装需要空间分析相关推荐
- Android 6.0 PM机制系列(四) APK安装需要空间分析
前言 在Android 9.0 PM机制系列(四) APK安装需要空间分析文章中,我们重点分析了Android9.0需要的最小APK安装存储空间大小.结论就是:只要系统空间小于Math.min(get ...
- Android 10.0 PackageManagerService(三)APK扫描-[Android取经之路]
摘要:上一节讲解了PKMS的 权限扫描,扫描/system/etc/permissions中的xml,存入相应的结构体中,供之后权限管理使用. 这一节主要来讲讲APK的扫描. 阅读本文大约需要花费15 ...
- Android 4.0.4系统下实现apk的静默安装和启动
转自http://www.linuxidc.com/Linux/2013-02/79403.htm 未亲测 最近在Android 4.0.4系统下实现apk的静默安装和启动的功能,这里和大家分享一下, ...
- 深入理解 Android 9.0 Crash 机制(二)
极力推荐Android 开发大总结文章:欢迎收藏 Android 开发技术文章大总结 本篇文章主要介绍 Android 开发中的部分知识点,通过阅读本篇文章,您将收获以下内容: 九. AppError ...
- Docker系列四~docker安装mysql
Docker系列四 docker安装mysql 搜索mysql版本 [root@localhost conf.d]# docker search mysql INDEX NAME DESCRIPTIO ...
- Android 8.0 VDEX机制简介
背景 Android 8.0在odex的基础上又引入了vdex机制,目的是为了降低dex2oat时间. 因为当系统ota后,用户自己安装的应用是不会发生任何变化的,但framework代码已经发生了变 ...
- android应用是非正式版本,Androidstudio 打包apk安装失败 应用是非正式发布版本,当前设备不支持安装...
Androidstudio 打包apk安装失败 应用是非正式发布版本,当前设备不支持安装 环境:Androidstudio 4.0 华为手机版本 10.0 android { compileSdkV ...
- Android 6.0 BluetoothAdapter.startDiscovery()扫描不到蓝牙的问题分析及解决
一.问题描述 最近在做蓝牙相关的项目,用 BluetoothAdapter.startDiscovery() 这个方法在Android 6.0 的机子上扫描不到对设备可见的蓝牙,但在 Android ...
- android 6.0 logcat机制(一)java层写log,logd接受log
第一篇博客,讲的主要是c++,java中打印log,然后通过socket传给logd,然后logd是如何处理接受log的. 一.logcat常用命令 logcat -c 清除已有log信息 logca ...
最新文章
- 看动画轻松理解「链表」实现「LRU缓存淘汰算法」
- chm文件打不开问题
- 【LoadRunner】OSGI性能测试实例
- ubuntu mysql5.6 编译安装_Ubuntu14.04编译安装mysql5.6.26
- IE下checkbox或radio隐藏bug
- lisp标注界址点号_(IP服务年终大盘点第二期)协会理事单位湖北高韬律师事务所完成韩国商标注册优先审查...
- C# 强制关闭当前程序进程(完全Kill掉不留痕迹)
- 如何优雅的移植JavaScript组件到Blazor
- 电脑睡眠快捷键_电脑快速进入睡眠的快捷键是什么?
- python创建sqlite3 unicode error_python/sqlite3:发生异常:sqlite3.operationalerror
- 电动汽车真的省钱吗?
- C++并发与多线程(三)单例设计模式与共享数据分析、call_once、condition_variable使用
- 数据恢复哪家强?四大数据恢复类软件评测
- [小o地图-数据] - 获取全国行政区划轮廓数据(上)
- 断层成像CT和ET重建算法
- amd处理器更新zen4服务器芯片,AMD将推出64 核心Zen 4处理器,整体性能提升了40%...
- 『C语言从入门到进阶』第 ⑥ 期 - 初识指针
- intellij IDE 快捷键(windows)
- 微信商户发放红包接口调试经验总结
- 最简洁的百度图片爬虫
热门文章
- 量化交易-回测调参-神龙摆尾
- 2172php,MAX2172 直接变频至低IF的调谐器,用于数字音频广播
- TokenGazer《一问到底》第53期:研究员 vs Cocos-BCX
- 解决下载github-production-release-asset-2e65be.s3.amazonaws.com上release文件慢的问题
- 李云大连理工计算机系2015级,大连理工大学考研研究生导师简介-李秀英
- 有时候,我们曾经有着同样的困扰。
- DOTA2无法找到有效的direct 3D
- mysql批量删除5000条数据_mysql批量删除大量数据
- SQL server 复杂查询
- 【已补蓝奏云链接】PyTorch中MNIST数据集(附datasets.MNIST离线包)下载慢/安装慢的解决方案