Android磁盘统计服务:StorageStatsService

前言

StorageStatsService磁盘统计服务:提供了相关应用程序、用户以及外部/共享存储如何利用磁盘空间的摘要。在Android Framework中提供了StorageStatsManager供应用调用。

StorageStatsManager

权限声明

  1. 当为您自己的程序包或UID调用StorageStatsManager API时,不需要声明权限。
  2. 请求其他任何软件包的详细信息都需要android.Manifest.permission#PACKAGE_USAGE_STATS权限,这是系统级权限,不会授予普通应用程序。声明权限表示您打算使用此API,最终用户可以选择通过“设置”应用程序授予此权限。

接口说明

https://developer.android.google.cn/reference/android/app/usage/StorageStatsManager

image.png

StorageStatsManager提供的能力总结

  • 获取目标卷的可用空间和总空间
  • 返回请求的存储卷上特定UserHandle的共享/外部存储统计信息
  • 返回请求存储卷上特定软件包的存储统计信息
  • 返回请求的存储卷上特定UID的存储统计信息
  • 返回请求的存储卷上特定UserHandle的存储统计信息

StorageStatsService

启动流程

StorageStatsService同其他系统服务一样,是从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.
                     */
                    // 启动存储服务
                    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 {
                    // 启动磁盘统计服务
                    mSystemServiceManager.startService(STORAGE_STATS_SERVICE_CLASS);
                } catch (Throwable e) {
                    reportWtf("starting StorageStatsService", e);
                }
                t.traceEnd();
    ...
    }

SystemServiceManager通过反射构建com.android.server.usage.StorageStatsService$Lifecycle对象,并调用他的onStart方法进一步构建并启动StorageStatsService,然后publishBinderService将服务发布到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);
        }
    }

构造方法

    public StorageStatsService(Context context) {
        mContext = Preconditions.checkNotNull(context);
        // 获取权限管理服务
        mAppOps = Preconditions.checkNotNull(context.getSystemService(AppOpsManager.class));
        // 获取用户管理服务
        mUser = Preconditions.checkNotNull(context.getSystemService(UserManager.class));
        // 获取包管理服务
        mPackage = Preconditions.checkNotNull(context.getPackageManager());
        // 获取存储管理服务
        mStorage = Preconditions.checkNotNull(context.getSystemService(StorageManager.class));
        // 缓存配额
        mCacheQuotas = new ArrayMap<>();
        // Installer背后是installd服务
        mInstaller = new Installer(context);
        mInstaller.onStart();
        // 调用installd守护进程的invalidateMounts
        invalidateMounts();

        // “android.io”线程
        mHandler = new H(IoThread.get().getLooper());
        mHandler.sendEmptyMessage(H.MSG_LOAD_CACHED_QUOTAS_FROM_FILE);
        // 存储服务监听各存储卷状态变化
        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

为了更快地获得存储统计信息,Android 8.0 开始会询问是否利用 ext4 文件系统的“配额”支持来几乎即时地返回磁盘使用情况统计信息。此配额功能还可以防止任何单个应用使用超过 90% 的磁盘空间或 50% 的索引节点,从而提高系统的稳定性。
配额功能是 installd 默认实现的一部分。在特定文件系统上启用配额功能后,installd 会自动使用该功能。如果在所测量的块存储设备上未启用或不支持配额功能,则系统将自动且透明地恢复手动计算方式。

获取应用磁盘占用

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);
        }

        // 获取对应包名的ApplicationInfo
        final ApplicationInfo appInfo;
        try {
            appInfo = mPackage.getApplicationInfoAsUser(packageName,
                    PackageManager.MATCH_UNINSTALLED_PACKAGES, userId);
        } catch (NameNotFoundException e) {
            throw new ParcelableException(e);
        }

        final boolean callerHasStatsPermission;
        // 权限检查
        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仅对应一个包名,则直接调用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对应多个包名(通过sharedUserId),需要mInstaller.getAppSize计算
            // 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];

            // 系统镜像中的系统应用codePath不计入
            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转换为StorageStats
            return translate(stats);
        }
    }

queryStatsForUid

可以看到queryStatsForUid逻辑大体上与queryStatsForPackage是一致的
最终都是使用mInstaller.getAppSize获取应用的各项磁盘占用。

    @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调用mInstalld, 他的最终实现是在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);
        }
    }

应用内存占用包括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 的设备上,每位用户均有两个可供应用使用的存储位置:

  • 凭据加密 (CE) 存储空间:这是默认存储位置,只有在用户解锁设备后才可用。
  • 设备加密 (DE) 存储空间:在直接启动模式期间以及用户解锁设备后均可用。

InstalldNativeService

getAppSize计算

  • 首先将obb计入extStats.codeSize,calculate_tree_size函数最终是作累加处理
  • 如果支持quota:
    1. 累加codePath,计入stats.codeSize
    2. 如果支持quota,则用quota计算
  • 不支持quota:
    1. 累加codePath,计入stats.codeSize
    2. 计算cePath 和 dePath计入stats.dataSize
    3. profiles计入stats.dataSize
    4. external路径计入extStats.dataSize
    5. dalvik_cache_path计入stats.codeSize

各模块对应的具体路径请查看源码:

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计入extStats.codeSize,calculate_tree_size函数最终是作累加处理
    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,计入stats.codeSize
        for (const auto& codePath : codePaths) {
            calculate_tree_size(codePath, &stats.codeSize, -1,
                    multiuser_get_shared_gid(0, appId));
        }
        ATRACE_END();
        // 如果支持quota,则用quota计算
        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();
            
            // 计算cePath 和 dePath计入stats.dataSize
            ATRACE_BEGIN("data");
            auto cePath = create_data_user_ce_package_path(uuid_, userId, pkgname, ceDataInodes[i]);
            collectManualStats(cePath, &stats);各模块对应的具体路径请查看源码: 

`frameworks/native/cmds/installd/utils.cpp`
            auto dePath = create_data_user_de_package_path(uuid_, userId, pkgname);
            collectManualStats(dePath, &stats);
            ATRACE_END();
            
            // profiles计入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路径计入extStats.dataSize
            ATRACE_BEGIN("external");
            auto extPath = create_data_media_package_path(uuid_, userId, "data", pkgname);
            collectManualStats(extPath, &extStats);各模块对应的具体路径请查看源码: 

`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计入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();
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,951评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,606评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,601评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,478评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,565评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,587评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,590评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,337评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,785评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,096评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,273评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,935评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,578评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,199评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,440评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,163评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,133评论 2 352

推荐阅读更多精彩内容