前言
StorageStatsService磁盤統(tǒng)計(jì)服務(wù):提供了相關(guān)應(yīng)用程序、用戶以及外部/共享存儲如何利用磁盤空間的摘要。在Android Framework中提供了StorageStatsManager供應(yīng)用調(diào)用。
StorageStatsManager
權(quán)限聲明
- 當(dāng)為您自己的程序包或UID調(diào)用StorageStatsManager API時(shí),不需要聲明權(quán)限。
- 請求其他任何軟件包的詳細(xì)信息都需要android.Manifest.permission#PACKAGE_USAGE_STATS權(quán)限,這是系統(tǒng)級權(quán)限,不會授予普通應(yīng)用程序。聲明權(quán)限表示您打算使用此API,最終用戶可以選擇通過“設(shè)置”應(yīng)用程序授予此權(quán)限。
接口說明
https://developer.android.google.cn/reference/android/app/usage/StorageStatsManager
StorageStatsManager提供的能力總結(jié)
- 獲取目標(biāo)卷的可用空間和總空間
- 返回請求的存儲卷上特定UserHandle的共享/外部存儲統(tǒng)計(jì)信息
- 返回請求存儲卷上特定軟件包的存儲統(tǒng)計(jì)信息
- 返回請求的存儲卷上特定UID的存儲統(tǒng)計(jì)信息
- 返回請求的存儲卷上特定UserHandle的存儲統(tǒng)計(jì)信息
StorageStatsService
啟動流程
StorageStatsService同其他系統(tǒng)服務(wù)一樣,是從SystemServer中啟動。
他的啟動順序是在StartStorageManagerService之后。
frameworks/base/services/java/com/android/server/SystemServer.java
private void startOtherServices(@NonNull TimingsTraceAndSlog t) {
...
t.traceBegin("StartStorageManagerService");
try {
/*
* NotificationManagerService is dependant on StorageManagerService,
* (for media / usb notifications) so we must start StorageManagerService first.
*/
// 啟動存儲服務(wù)
mSystemServiceManager.startService(STORAGE_MANAGER_SERVICE_CLASS);
storageManager = IStorageManager.Stub.asInterface(
ServiceManager.getService("mount"));
} catch (Throwable e) {
reportWtf("starting StorageManagerService", e);
}
t.traceEnd();
t.traceBegin("StartStorageStatsService");
try {
// 啟動磁盤統(tǒng)計(jì)服務(wù)
mSystemServiceManager.startService(STORAGE_STATS_SERVICE_CLASS);
} catch (Throwable e) {
reportWtf("starting StorageStatsService", e);
}
t.traceEnd();
...
}
SystemServiceManager通過反射構(gòu)建com.android.server.usage.StorageStatsService$Lifecycle對象,并調(diào)用他的onStart方法進(jìn)一步構(gòu)建并啟動StorageStatsService,然后publishBinderService將服務(wù)發(fā)布到ServiceManager
public static class Lifecycle extends SystemService {
private StorageStatsService mService;
public Lifecycle(Context context) {
super(context);
}
@Override
public void onStart() {
mService = new StorageStatsService(getContext());
publishBinderService(Context.STORAGE_STATS_SERVICE, mService);
}
}
構(gòu)造方法
public StorageStatsService(Context context) {
mContext = Preconditions.checkNotNull(context);
// 獲取權(quán)限管理服務(wù)
mAppOps = Preconditions.checkNotNull(context.getSystemService(AppOpsManager.class));
// 獲取用戶管理服務(wù)
mUser = Preconditions.checkNotNull(context.getSystemService(UserManager.class));
// 獲取包管理服務(wù)
mPackage = Preconditions.checkNotNull(context.getPackageManager());
// 獲取存儲管理服務(wù)
mStorage = Preconditions.checkNotNull(context.getSystemService(StorageManager.class));
// 緩存配額
mCacheQuotas = new ArrayMap<>();
// Installer背后是installd服務(wù)
mInstaller = new Installer(context);
mInstaller.onStart();
// 調(diào)用installd守護(hù)進(jìn)程的invalidateMounts
invalidateMounts();
// “android.io”線程
mHandler = new H(IoThread.get().getLooper());
mHandler.sendEmptyMessage(H.MSG_LOAD_CACHED_QUOTAS_FROM_FILE);
// 存儲服務(wù)監(jiān)聽各存儲卷狀態(tài)變化
mStorage.registerListener(new StorageEventListener() {
@Override
public void onVolumeStateChanged(VolumeInfo vol, int oldState, int newState) {
switch (vol.type) {
case VolumeInfo.TYPE_PUBLIC:
case VolumeInfo.TYPE_PRIVATE:
case VolumeInfo.TYPE_EMULATED:
if (newState == VolumeInfo.STATE_MOUNTED) {
invalidateMounts();
}
}
}
});
LocalServices.addService(StorageStatsManagerInternal.class, new LocalService());
}
Quota配額功能
https://source.android.com/devices/storage/faster-stats?hl=zh-cn
為了更快地獲得存儲統(tǒng)計(jì)信息,Android 8.0 開始會詢問是否利用 ext4 文件系統(tǒng)的“配額”支持來幾乎即時(shí)地返回磁盤使用情況統(tǒng)計(jì)信息。此配額功能還可以防止任何單個應(yīng)用使用超過 90% 的磁盤空間或 50% 的索引節(jié)點(diǎn),從而提高系統(tǒng)的穩(wěn)定性。
配額功能是 installd 默認(rèn)實(shí)現(xiàn)的一部分。在特定文件系統(tǒng)上啟用配額功能后,installd 會自動使用該功能。如果在所測量的塊存儲設(shè)備上未啟用或不支持配額功能,則系統(tǒng)將自動且透明地恢復(fù)手動計(jì)算方式。
獲取應(yīng)用磁盤占用
queryStatsForPackage
@Override
public StorageStats queryStatsForPackage(String volumeUuid, String packageName, int userId,
String callingPackage) {
if (userId != UserHandle.getCallingUserId()) {
mContext.enforceCallingOrSelfPermission(
android.Manifest.permission.INTERACT_ACROSS_USERS, TAG);
}
// 獲取對應(yīng)包名的ApplicationInfo
final ApplicationInfo appInfo;
try {
appInfo = mPackage.getApplicationInfoAsUser(packageName,
PackageManager.MATCH_UNINSTALLED_PACKAGES, userId);
} catch (NameNotFoundException e) {
throw new ParcelableException(e);
}
final boolean callerHasStatsPermission;
// 權(quán)限檢查
if (Binder.getCallingUid() == appInfo.uid) {
// No permissions required when asking about themselves. We still check since it is
// needed later on but don't throw if caller doesn't have the permission.
callerHasStatsPermission = checkStatsPermission(
Binder.getCallingUid(), callingPackage, false) == null;
} else {
enforceStatsPermission(Binder.getCallingUid(), callingPackage);
callerHasStatsPermission = true;
}
// 如果uid僅對應(yīng)一個包名,則直接調(diào)用queryStatsForUid
if (defeatNullable(mPackage.getPackagesForUid(appInfo.uid)).length == 1) {
// Only one package inside UID means we can fast-path
return queryStatsForUid(volumeUuid, appInfo.uid, callingPackage);
} else {
// 如果uid對應(yīng)多個包名(通過sharedUserId),需要mInstaller.getAppSize計(jì)算
// Multiple packages means we need to go manual
final int appId = UserHandle.getUserId(appInfo.uid);
final String[] packageNames = new String[] { packageName };
final long[] ceDataInodes = new long[1];
String[] codePaths = new String[0];
// 系統(tǒng)鏡像中的系統(tǒng)應(yīng)用codePath不計(jì)入
if (appInfo.isSystemApp() && !appInfo.isUpdatedSystemApp()) {
// We don't count code baked into system image
} else {
codePaths = ArrayUtils.appendElement(String.class, codePaths,
appInfo.getCodePath());
}
final PackageStats stats = new PackageStats(TAG);
try {
mInstaller.getAppSize(volumeUuid, packageNames, userId, 0,
appId, ceDataInodes, codePaths, stats);
} catch (InstallerException e) {
throw new ParcelableException(new IOException(e.getMessage()));
}
if (volumeUuid == StorageManager.UUID_PRIVATE_INTERNAL) {
forEachStorageStatsAugmenter((storageStatsAugmenter) -> {
storageStatsAugmenter.augmentStatsForPackage(stats,
packageName, userId, callerHasStatsPermission);
}, "queryStatsForPackage");
}
// PackageStats轉(zhuǎn)換為StorageStats
return translate(stats);
}
}
queryStatsForUid
可以看到queryStatsForUid邏輯大體上與queryStatsForPackage是一致的
最終都是使用mInstaller.getAppSize獲取應(yīng)用的各項(xiàng)磁盤占用。
@Override
public StorageStats queryStatsForUid(String volumeUuid, int uid, String callingPackage) {
final int userId = UserHandle.getUserId(uid);
final int appId = UserHandle.getAppId(uid);
if (userId != UserHandle.getCallingUserId()) {
mContext.enforceCallingOrSelfPermission(
android.Manifest.permission.INTERACT_ACROSS_USERS, TAG);
}
final boolean callerHasStatsPermission;
if (Binder.getCallingUid() == uid) {
// No permissions required when asking about themselves. We still check since it is
// needed later on but don't throw if caller doesn't have the permission.
callerHasStatsPermission = checkStatsPermission(
Binder.getCallingUid(), callingPackage, false) == null;
} else {
enforceStatsPermission(Binder.getCallingUid(), callingPackage);
callerHasStatsPermission = true;
}
final String[] packageNames = defeatNullable(mPackage.getPackagesForUid(uid));
final long[] ceDataInodes = new long[packageNames.length];
String[] codePaths = new String[0];
for (int i = 0; i < packageNames.length; i++) {
try {
final ApplicationInfo appInfo = mPackage.getApplicationInfoAsUser(packageNames[i],
PackageManager.MATCH_UNINSTALLED_PACKAGES, userId);
if (appInfo.isSystemApp() && !appInfo.isUpdatedSystemApp()) {
// We don't count code baked into system image
} else {
codePaths = ArrayUtils.appendElement(String.class, codePaths,
appInfo.getCodePath());
}
} catch (NameNotFoundException e) {
throw new ParcelableException(e);
}
}
final PackageStats stats = new PackageStats(TAG);
try {
mInstaller.getAppSize(volumeUuid, packageNames, userId, getDefaultFlags(),
appId, ceDataInodes, codePaths, stats);
if (SystemProperties.getBoolean(PROP_VERIFY_STORAGE, false)) {
final PackageStats manualStats = new PackageStats(TAG);
mInstaller.getAppSize(volumeUuid, packageNames, userId, 0,
appId, ceDataInodes, codePaths, manualStats);
checkEquals("UID " + uid, manualStats, stats);
}
} catch (InstallerException e) {
throw new ParcelableException(new IOException(e.getMessage()));
}
if (volumeUuid == StorageManager.UUID_PRIVATE_INTERNAL) {
forEachStorageStatsAugmenter((storageStatsAugmenter) -> {
storageStatsAugmenter.augmentStatsForUid(stats, uid, callerHasStatsPermission);
}, "queryStatsForUid");
}
return translate(stats);
}
Installer
Installer調(diào)用mInstalld, 他的最終實(shí)現(xiàn)是在InstalldNativeService.cpp
public void getAppSize(String uuid, String[] packageNames, int userId, int flags, int appId,
long[] ceDataInodes, String[] codePaths, PackageStats stats)
throws InstallerException {
if (!checkBeforeRemote()) return;
if (codePaths != null) {
for (String codePath : codePaths) {
BlockGuard.getVmPolicy().onPathAccess(codePath);
}
}
try {
final long[] res = mInstalld.getAppSize(uuid, packageNames, userId, flags,
appId, ceDataInodes, codePaths);
stats.codeSize += res[0];
stats.dataSize += res[1];
stats.cacheSize += res[2];
stats.externalCodeSize += res[3];
stats.externalDataSize += res[4];
stats.externalCacheSize += res[5];
} catch (Exception e) {
throw InstallerException.from(e);
}
}
應(yīng)用內(nèi)存占用包括dataBytes和codeBytes
dataBytes包含的主要路徑如下:
- Context#getDataDir()----------------------------------------/data/user/0/<app>
- Context#getCacheDir()------------------------------------/data/user/0/<app>/cache
- Context#getCodeCacheDir()---------------------------/data/user/0/<app>/code_cache
- Context#getExternalFilesDir(String)-------------<sdcard>/Android/data/\app>/files
- Context#getExternalCacheDir()-------------------<sdcard>/Android/data/<app>/cache
- Context#getExternalMediaDirs()----------------------<sdcard>/Android/media/<app>
codeBytes包含的主要路徑如下:
- Context#getObbDir()--------------/storage/emulated/0/Android/obb/<app>
cePath和dePath
在啟用了 FBE 的設(shè)備上,每位用戶均有兩個可供應(yīng)用使用的存儲位置:
- 憑據(jù)加密 (CE) 存儲空間:這是默認(rèn)存儲位置,只有在用戶解鎖設(shè)備后才可用。
- 設(shè)備加密 (DE) 存儲空間:在直接啟動模式期間以及用戶解鎖設(shè)備后均可用。
InstalldNativeService
getAppSize計(jì)算
- 首先將obb計(jì)入extStats.codeSize,calculate_tree_size函數(shù)最終是作累加處理
- 如果支持quota:
- 累加codePath,計(jì)入stats.codeSize
- 如果支持quota,則用quota計(jì)算
- 不支持quota:
- 累加codePath,計(jì)入stats.codeSize
- 計(jì)算cePath 和 dePath計(jì)入stats.dataSize
- profiles計(jì)入stats.dataSize
- external路徑計(jì)入extStats.dataSize
- dalvik_cache_path計(jì)入stats.codeSize
各模塊對應(yīng)的具體路徑請查看源碼:
frameworks/native/cmds/installd/utils.cpp
binder::Status InstalldNativeService::getAppSize(const std::unique_ptr<std::string>& uuid,
const std::vector<std::string>& packageNames, int32_t userId, int32_t flags,
int32_t appId, const std::vector<int64_t>& ceDataInodes,
const std::vector<std::string>& codePaths, std::vector<int64_t>* _aidl_return) {
ENFORCE_UID(AID_SYSTEM);
CHECK_ARGUMENT_UUID(uuid);
for (const auto& packageName : packageNames) {
CHECK_ARGUMENT_PACKAGE_NAME(packageName);
}
for (const auto& codePath : codePaths) {
CHECK_ARGUMENT_PATH(codePath);
}
// NOTE: Locking is relaxed on this method, since it's limited to
// read-only measurements without mutation.
// When modifying this logic, always verify using tests:
// runtest -x frameworks/base/services/tests/servicestests/src/com/android/server/pm/InstallerTest.java -m testGetAppSize
#if MEASURE_DEBUG
LOG(INFO) << "Measuring user " << userId << " app " << appId;
#endif
// Here's a summary of the common storage locations across the platform,
// and how they're each tagged:
//
// /data/app/com.example UID system
// /data/app/com.example/oat UID system
// /data/user/0/com.example UID u0_a10 GID u0_a10
// /data/user/0/com.example/cache UID u0_a10 GID u0_a10_cache
// /data/media/0/foo.txt UID u0_media_rw
// /data/media/0/bar.jpg UID u0_media_rw GID u0_media_image
// /data/media/0/Android/data/com.example UID u0_media_rw GID u0_a10_ext
// /data/media/0/Android/data/com.example/cache UID u0_media_rw GID u0_a10_ext_cache
// /data/media/obb/com.example UID system
struct stats stats;
struct stats extStats;
memset(&stats, 0, sizeof(stats));
memset(&extStats, 0, sizeof(extStats));
auto uuidString = uuid ? *uuid : "";
const char* uuid_ = uuid ? uuid->c_str() : nullptr;
if (!IsQuotaSupported(uuidString)) {
flags &= ~FLAG_USE_QUOTA;
}
// 將obb計(jì)入extStats.codeSize,calculate_tree_size函數(shù)最終是作累加處理
ATRACE_BEGIN("obb");
for (const auto& packageName : packageNames) {
auto obbCodePath = create_data_media_package_path(uuid_, userId,
"obb", packageName.c_str());
calculate_tree_size(obbCodePath, &extStats.codeSize);
}
ATRACE_END();
if (flags & FLAG_USE_QUOTA && appId >= AID_APP_START) {
ATRACE_BEGIN("code");
// 累加codePath,計(jì)入stats.codeSize
for (const auto& codePath : codePaths) {
calculate_tree_size(codePath, &stats.codeSize, -1,
multiuser_get_shared_gid(0, appId));
}
ATRACE_END();
// 如果支持quota,則用quota計(jì)算
ATRACE_BEGIN("quota");
collectQuotaStats(uuidString, userId, appId, &stats, &extStats);
ATRACE_END();
} else {
ATRACE_BEGIN("code");
for (const auto& codePath : codePaths) {
calculate_tree_size(codePath, &stats.codeSize);
}
ATRACE_END();
for (size_t i = 0; i < packageNames.size(); i++) {
const char* pkgname = packageNames[i].c_str();
// 計(jì)算cePath 和 dePath計(jì)入stats.dataSize
ATRACE_BEGIN("data");
auto cePath = create_data_user_ce_package_path(uuid_, userId, pkgname, ceDataInodes[i]);
collectManualStats(cePath, &stats);各模塊對應(yīng)的具體路徑請查看源碼:
`frameworks/native/cmds/installd/utils.cpp`
auto dePath = create_data_user_de_package_path(uuid_, userId, pkgname);
collectManualStats(dePath, &stats);
ATRACE_END();
// profiles計(jì)入stats.dataSize
if (!uuid) {
ATRACE_BEGIN("profiles");
calculate_tree_size(
create_primary_current_profile_package_dir_path(userId, pkgname),
&stats.dataSize);
calculate_tree_size(
create_primary_reference_profile_package_dir_path(pkgname),
&stats.codeSize);
ATRACE_END();
}
// external路徑計(jì)入extStats.dataSize
ATRACE_BEGIN("external");
auto extPath = create_data_media_package_path(uuid_, userId, "data", pkgname);
collectManualStats(extPath, &extStats);各模塊對應(yīng)的具體路徑請查看源碼:
`frameworks/native/cmds/installd/utils.cpp`
auto mediaPath = create_data_media_package_path(uuid_, userId, "media", pkgname);
calculate_tree_size(mediaPath, &extStats.dataSize);
ATRACE_END();
}
if (!uuid) {
// dalvik_cache_path計(jì)入stats.codeSize
ATRACE_BEGIN("dalvik");
int32_t sharedGid = multiuser_get_shared_gid(0, appId);
if (sharedGid != -1) {
calculate_tree_size(create_data_dalvik_cache_path(), &stats.codeSize,
sharedGid, -1);
}
ATRACE_END();
}
}
std::vector<int64_t> ret;
ret.push_back(stats.codeSize);
ret.push_back(stats.dataSize);
ret.push_back(stats.cacheSize);
ret.push_back(extStats.codeSize);
ret.push_back(extStats.dataSize);
ret.push_back(extStats.cacheSize);
#if MEASURE_DEBUG
LOG(DEBUG) << "Final result " << toString(ret);
#endif
*_aidl_return = ret;
return ok();
}