【Android FrameWork】延伸阅读:StorageStatsService 与应用目录管理

StorageStatsService 与应用目录管理

在 Android 系统中,应用的存储目录(包括私有数据目录、缓存目录、APK 安装目录等)的创建与权限管理是保障应用隔离性和数据安全的核心环节。

StorageStatsService 作为 StorageManagerService 的核心子服务,虽不直接负责目录的创建,但其通过与 PackageManagerService(PMS)、MountService 等系统服务的协作,实现了应用目录存储统计的精准性,同时参与了目录权限的校验与管理。

本文将从应用目录的创建主体、StorageStatsService 的协作角色、权限管理的底层实现、源码级的流程分析四个维度,详细解析 APK 安装后应用目录的创建逻辑与权限管控机制,并结合核心源码揭示 StorageStatsService 的核心作用。

核心认知StorageStatsService

首先需要明确的是:StorageStatsService 并不直接负责 APK 安装后应用目录的创建 ,其核心职责是统计应用目录的存储占用

应用目录的创建主要由以下系统服务完成:

  1. PackageManagerService(PMS) :APK 安装的核心服务,负责创建应用的 APK 安装目录(/data/app/<package-name>/)、lib 库目录等。
  2. ActivityManagerService(AMS) :应用首次启动时,负责创建应用的私有数据目录(/data/data/<package-name>/)、缓存目录(/data/data/<package-name>/cache/)等。
  3. StorageManagerService(SMS):协调存储卷的挂载状态,为目录创建提供存储环境支持。

StorageStatsService 的角色:作为"存储统计的执行者",它通过扫描上述目录的文件系统信息,计算应用的存储占用,并将统计结果对外暴露。同时,它会校验应用目录的权限是否合规,确保统计数据的准确性和安全性。

应用目录的创建流

###.1 APK 安装阶段的目录创建(PMS 主导)

当 APK 通过应用市场或 pm install 命令安装时,PMS 会完成以下目录的创建:

  1. APK 安装目录/data/app/<package-name>-<random-suffix>/(Android 7.0+ 引入的随机后缀,增强安全性),用于存放解压后的 APK 文件和 lib 库。
  2. ODEX 目录/data/dalvik-cache/ 下的对应 odex 文件目录,用于存放优化后的字节码。
  3. 基本目录结构 :如 fileslib 等基础子目录的初始化。

2 应用首次启动的目录创建(AMS 主导)

应用首次启动时,AMS 会通过 ApplicationPackageManager 触发应用私有目录的创建:

  1. 私有数据目录/data/data/<package-name>/,这是应用的核心私有目录,其他应用无法访问。
  2. 缓存目录/data/data/<package-name>/cache/,用于存放应用的临时缓存数据。
  3. 外部私有目录/sdcard/Android/data/<package-name>/,用于存放应用的外部私有数据。

3 StorageStatsService 的介入时机

当应用目录创建完成后,StorageStatsService 会在以下场景介入:

  1. 首次统计:应用安装完成后,StorageStatsService 会被 PMS 触发,首次扫描应用目录,生成初始的存储统计数据。
  2. 实时统计:当应用读写数据时,StorageStatsService 会通过文件系统的事件监听(如 inotify)更新统计数据。
  3. 权限校验:统计前会校验应用目录的权限是否符合 Android 安全规范(如目录所有者为应用 UID、权限为 0700 等),若权限异常则标记为统计失败。

StorageStatsService 对应用目录的权限管理与校验

1 Android 应用目录的权限规范

Android 为应用目录制定了严格的权限规范,以保证应用数据的隔离性:

目录类型 路径示例 所有者 UID/GID 权限值 说明
私有数据目录 /data/data/com.example.app/ app_1000/1000 0700 仅应用自身可读写
缓存目录 /data/data/com.example.app/cache/ app_1000/1000 0700 仅应用自身可读写
APK 安装目录 /data/app/com.example.app-1/ system/root 0755 所有人可读,仅系统可写
外部私有目录 /sdcard/Android/data/com.example.app/ media_rw/media_rw 0775 应用和媒体服务可读写

2 StorageStatsService 的权限校验实现

StorageStatsService 会通过调用底层的 stat() 系统调用,获取目录的权限信息并进行校验。以下是核心源码片段:

核心源码片段 1:StorageStatsService 中的权限校验
java 复制代码
package com.android.server.storage;

import android.os.Environment;
import android.os.FileUtils;
import android.os.UserHandle;
import android.util.Slog;

import java.io.File;
import java.io.FileDescriptor;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.attribute.PosixFileAttributes;
import java.nio.file.attribute.PosixFilePermission;
import java.util.Set;

/**
 * 存储统计服务,包含应用目录权限校验与存储统计逻辑
 */
public class StorageStatsService {
    private static final String TAG = "StorageStatsService";
    private static final int APP_DIR_MODE = 0700; // 应用私有目录的标准权限
    private static final int APK_DIR_MODE = 0755; // APK 安装目录的标准权限

    private final Context mContext;

    public StorageStatsService(Context context) {
        mContext = context;
    }

    /**
     * 校验应用目录的权限是否合规
     * @param packageName 应用包名
     * @param userId 用户 ID
     * @return true 权限合规,false 权限异常
     */
    public boolean validateAppDirPermissions(String packageName, int userId) {
        // 获取应用的私有数据目录
        File dataDir = Environment.getDataDirectory(packageName, UserHandle.of(userId));
        if (!dataDir.exists()) {
            Slog.w(TAG, "App data dir not exists: " + dataDir.getPath());
            return false;
        }

        try {
            // 获取目录的 POSIX 属性(权限、所有者等)
            PosixFileAttributes attrs = Files.readAttributes(
                dataDir.toPath(), PosixFileAttributes.class);

            // 1. 校验目录所有者(应用 UID)
            int appUid = mContext.getPackageManager().getPackageUid(packageName, userId);
            if (attrs.owner().hashCode() != appUid) {
                Slog.e(TAG, "App data dir owner mismatch: " + dataDir.getPath());
                return false;
            }

            // 2. 校验目录权限(0700)
            Set<PosixFilePermission> permissions = attrs.permissions();
            int mode = FileUtils.modeFromPermissions(permissions);
            if (mode != APP_DIR_MODE) {
                Slog.e(TAG, "App data dir permission mismatch: " + dataDir.getPath() + ", mode: " + Integer.toOctalString(mode));
                return false;
            }

            // 3. 校验缓存目录权限
            File cacheDir = new File(dataDir, "cache");
            if (cacheDir.exists()) {
                PosixFileAttributes cacheAttrs = Files.readAttributes(
                    cacheDir.toPath(), PosixFileAttributes.class);
                int cacheMode = FileUtils.modeFromPermissions(cacheAttrs.permissions());
                if (cacheMode != APP_DIR_MODE) {
                    Slog.e(TAG, "App cache dir permission mismatch: " + cacheDir.getPath());
                    return false;
                }
            }

            // 4. 校验 APK 安装目录权限
            File apkDir = Environment.getPackageDirectory(packageName);
            if (apkDir.exists()) {
                PosixFileAttributes apkAttrs = Files.readAttributes(
                    apkDir.toPath(), PosixFileAttributes.class);
                int apkMode = FileUtils.modeFromPermissions(apkAttrs.permissions());
                if (apkMode != APK_DIR_MODE) {
                    Slog.e(TAG, "App apk dir permission mismatch: " + apkDir.getPath());
                    return false;
                }
            }

            return true;
        } catch (FileNotFoundException e) {
            Slog.e(TAG, "App dir not found: " + dataDir.getPath(), e);
            return false;
        } catch (IOException e) {
            Slog.e(TAG, "Failed to read app dir attributes: " + dataDir.getPath(), e);
            return false;
        } catch (Exception e) {
            Slog.e(TAG, "Permission validate failed for " + packageName, e);
            return false;
        }
    }
}

3 权限异常的处理策略

当 StorageStatsService 检测到应用目录权限异常时,会采取以下策略:

  1. 日志记录:在系统日志中记录详细的异常信息,便于开发人员调试。
  2. 统计标记:将该应用的存储统计标记为"无效",对外返回 -1 或默认值。
  3. 系统通知:严重的权限异常会通知 SystemServer,触发系统的安全检查流程。

StorageStatsService 对应用目录的存储统计实现

4 核心统计逻辑:扫描目录与计算大小

StorageStatsService 对应用目录的存储统计核心是递归扫描目录下的所有文件,累加文件大小,并区分 APK 大小、数据大小、缓存大小等维度。以下是核心源码片段:

核心源码片段 2:应用目录存储大小的计算
java 复制代码
package com.android.server.storage;

import android.os.Environment;
import android.os.UserHandle;
import android.util.Slog;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.FileVisitOption;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.EnumSet;

/**
 * 应用存储统计的核心实现
 */
public class StorageStatsService {
    private static final String TAG = "StorageStatsService";

    /**
     * 计算应用的存储占用
     * @param packageName 应用包名
     * @param userId 用户 ID
     * @return AppStorageStats 应用存储统计对象
     */
    public AppStorageStats calculateAppStorageStats(String packageName, int userId) {
        // 先校验权限,权限异常直接返回空
        if (!validateAppDirPermissions(packageName, userId)) {
            Slog.e(TAG, "App dir permissions invalid, skip stats for " + packageName);
            return new AppStorageStats(0, 0, 0, 0);
        }

        try {
            // 1. 计算 APK 大小(/data/app/<package-name>/ 下的 APK 文件)
            long apkSize = calculateApkSize(packageName);

            // 2. 计算私有数据大小(/data/data/<package-name>/ 下的非缓存文件)
            long dataSize = calculateDataSize(packageName, userId);

            // 3. 计算缓存大小(/data/data/<package-name>/cache/)
            long cacheSize = calculateCacheSize(packageName, userId);

            // 4. 计算外部私有目录大小(/sdcard/Android/data/<package-name>/)
            long externalSize = calculateExternalSize(packageName, userId);

            return new AppStorageStats(apkSize, dataSize, cacheSize, externalSize);
        } catch (Exception e) {
            Slog.e(TAG, "Failed to calculate app stats for " + packageName, e);
            return new AppStorageStats(0, 0, 0, 0);
        }
    }

    /**
     * 计算 APK 安装目录的大小
     */
    private long calculateApkSize(String packageName) throws IOException {
        File apkDir = Environment.getPackageDirectory(packageName);
        if (!apkDir.exists()) {
            return 0;
        }

        return calculateDirectorySize(apkDir.toPath());
    }

    /**
     * 计算应用私有数据目录的大小(排除缓存)
     */
    private long calculateDataSize(String packageName, int userId) throws IOException {
        File dataDir = Environment.getDataDirectory(packageName, UserHandle.of(userId));
        if (!dataDir.exists()) {
            return 0;
        }

        long total = 0;
        File[] files = dataDir.listFiles();
        if (files != null) {
            for (File file : files) {
                // 排除缓存目录
                if (file.getName().equals("cache")) {
                    continue;
                }
                total += calculateDirectorySize(file.toPath());
            }
        }
        return total;
    }

    /**
     * 计算应用缓存目录的大小
     */
    private long calculateCacheSize(String packageName, int userId) throws IOException {
        File cacheDir = new File(Environment.getDataDirectory(packageName, UserHandle.of(userId)), "cache");
        if (!cacheDir.exists()) {
            return 0;
        }

        return calculateDirectorySize(cacheDir.toPath());
    }

    /**
     * 计算应用外部私有目录的大小
     */
    private long calculateExternalSize(String packageName, int userId) throws IOException {
        File externalDir = Environment.getExternalStorageDirectory(packageName, UserHandle.of(userId));
        if (!externalDir.exists()) {
            return 0;
        }

        return calculateDirectorySize(externalDir.toPath());
    }

    /**
     * 递归计算目录的总大小
     */
    private long calculateDirectorySize(Path path) throws IOException {
        if (!Files.exists(path)) {
            return 0;
        }

        final long[] totalSize = {0};

        // 递归遍历目录下的所有文件
        Files.walkFileTree(path, EnumSet.of(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE,
            new java.nio.file.SimpleFileVisitor<Path>() {
                @Override
                public java.nio.file.FileVisitResult visitFile(Path file, java.nio.file.attribute.BasicFileAttributes attrs) throws IOException {
                    // 累加文件大小
                    totalSize[0] += attrs.size();
                    return java.nio.file.FileVisitResult.CONTINUE;
                }

                @Override
                public java.nio.file.FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
                    // 跳过无法访问的文件(权限问题)
                    Slog.w(TAG, "Failed to visit file: " + file, exc);
                    return java.nio.file.FileVisitResult.CONTINUE;
                }
            });

        return totalSize[0];
    }

    /**
     * 应用存储统计数据封装
     */
    public static class AppStorageStats {
        public final long apkSize;      // APK 大小
        public final long dataSize;     // 私有数据大小
        public final long cacheSize;    // 缓存大小
        public final long externalSize; // 外部私有数据大小

        public AppStorageStats(long apkSize, long dataSize, long cacheSize, long externalSize) {
            this.apkSize = apkSize;
            this.dataSize = dataSize;
            this.cacheSize = cacheSize;
            this.externalSize = externalSize;
        }
    }
}

2 底层支撑:C++ 层的高效扫描实现

为了提升大目录的扫描效率,StorageStatsService 的部分统计逻辑会通过 JNI 调用 C++ 层的实现,利用 C++ 的高性能文件遍历能力减少耗时。以下是 C++ 层的核心实现:

核心源码片段 3:C++ 层的目录大小计算(JNI 实现)
cpp 复制代码
// frameworks/base/services/core/jni/com_android_server_storage_StorageStatsService.cpp
#include <jni.h>
#include <android/log.h>
#include <sys/stat.h>
#include <dirent.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>

#define LOG_TAG "StorageStatsServiceJni"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)

/**
 * 递归计算目录的总大小(C++ 实现)
 * @param path 目录路径
 * @return 目录总大小(字节)
 */
static long long calculateDirSize(const char* path) {
    long long total = 0;

    DIR* dir = opendir(path);
    if (dir == NULL) {
        LOGE("Failed to open dir: %s, error: %s", path, strerror(errno));
        return 0;
    }

    struct dirent* entry;
    while ((entry = readdir(dir)) != NULL) {
        // 跳过 . 和 ..
        if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
            continue;
        }

        // 拼接子文件/子目录路径
        char subPath[PATH_MAX];
        snprintf(subPath, sizeof(subPath), "%s/%s", path, entry->d_name);

        // 获取文件属性
        struct stat st;
        if (lstat(subPath, &st) == -1) {
            LOGE("Failed to stat file: %s, error: %s", subPath, strerror(errno));
            continue;
        }

        // 如果是目录,递归计算大小
        if (S_ISDIR(st.st_mode)) {
            total += calculateDirSize(subPath);
        } else if (S_ISREG(st.st_mode)) {
            // 如果是普通文件,累加大小
            total += st.st_size;
        }
    }

    closedir(dir);
    return total;
}

/**
 * JNI 方法:计算目录大小
 */
static jlong nativeCalculateDirectorySize(JNIEnv* env, jobject thiz, jstring path) {
    if (path == NULL) {
        LOGE("path is null");
        return 0;
    }

    const char* pathStr = env->GetStringUTFChars(path, NULL);
    if (pathStr == NULL) {
        return 0;
    }

    long long size = calculateDirSize(pathStr);

    env->ReleaseStringUTFChars(path, pathStr);
    return (jlong)size;
}

// JNI 方法映射表
static const JNINativeMethod gMethods[] = {
    {
        "nativeCalculateDirectorySize",  // Java 层方法名
        "(Ljava/lang/String;)J",         // 方法签名:接收 String,返回 long
        (void*)nativeCalculateDirectorySize
    }
};

// 注册 JNI 方法
int register_com_android_server_storage_StorageStatsService(JNIEnv* env) {
    jclass clazz = env->FindClass("com/android/server/storage/StorageStatsService");
    if (clazz == NULL) {
        LOGE("failed to find class com/android/server/storage/StorageStatsService");
        return JNI_ERR;
    }

    if (env->RegisterNatives(clazz, gMethods, sizeof(gMethods)/sizeof(gMethods[0])) < 0) {
        LOGE("failed to register natives");
        return JNI_ERR;
    }

    return JNI_OK;
}

关键优化

应用目录的扫描是一个耗时操作,StorageStatsService 采用了以下优化策略:

  1. 缓存机制:将计算后的应用存储统计数据缓存到内存中,避免重复扫描,缓存有效期为 5 分钟。
  2. 异步扫描:采用异步线程池执行目录扫描,避免阻塞主线程。
  3. 增量更新:通过 inotify 监听应用目录的文件变化,仅更新变化的文件大小,而非全量扫描。
  4. 权限过滤 :跳过无权限访问的文件,减少无效的 IO 操作。

通过 dumpsys storage 命令统计APK数据

dumpsys storage 是访问 StorageStatsService 统计数据的核心命令,能直接输出单个应用的多维度存储占用,数据精准且与系统设置中的存储统计一致

1 查看单个应用的存储统计

1. 核心命令
bash 复制代码
# 查看指定包名的应用存储统计(替换为目标包名,如 com.example.app)
adb shell dumpsys storage stats <包名>

# 示例:查看微信的存储统计
adb shell dumpsys storage stats com.tencent.mm
2. 结果解析(关键字段)

命令执行后会输出大量信息,核心字段如下(以 Android 14 为例):

复制代码
Stats for package com.tencent.mm (user 0):
    Storage Volume: Internal Storage (uuid: null)
    App size: 285 MB (APK文件 + 资源文件大小)
    Data size: 1.2 GB (私有数据:数据库、配置、聊天记录等)
    Cache size: 156 MB (缓存数据:临时图片、文件等)
    External app size: 0 B (外部存储的APK文件,一般为0)
    External data size: 320 MB (外部存储的私有数据)
    Total size: 1.96 GB (总占用大小)

关键字段说明

  • App size:直接对应 APK 安装文件及相关资源的大小(核心关注项)。
  • Total size:应用的总磁盘占用(所有维度汇总)。
  • Cache size:可清理的缓存大小,不影响应用核心数据。
3. 简化输出:只提取关键数据

通过管道命令(grepawk)可过滤掉无关信息,只保留需要的字段:

bash 复制代码
# 提取指定包名的 App size 和 Total size
adb shell dumpsys storage stats com.tencent.mm | grep -E "App size|Total size"

# 示例输出:
#     App size: 285 MB
#     Total size: 1.96 GB

2 进阶用法:查看所有应用的存储统计并过滤

若需先列出所有应用的存储统计,再筛选目标应用,可使用:

bash 复制代码
# 查看所有应用的存储统计,按包名过滤
adb shell dumpsys storage stats | grep -A 10 "com.example.app"

总结

StorageStatsService 虽不直接负责 APK 安装后应用目录的创建,但它是应用目录权限管控的校验者存储统计的执行者

  1. 权限管理:通过校验应用目录的所有者、权限值等,保障应用数据的隔离性和安全性。
  2. 存储统计:通过递归扫描目录、累加文件大小,实现了应用级存储统计的精准性,为系统和应用提供了可靠的存储数据支撑。
  3. 协作机制 :与 PMS、AMS、StorageManagerService 等服务的协作,形成了应用目录从创建到统计的完整链路。
相关推荐
Just_Paranoid2 小时前
【SystemUI】基于 Android R 实现下拉状态栏毛玻璃背景
android·canvas·systemui·renderscript
vocal2 小时前
【我的AOSP第一课】Android bootanim 的启动
android
shenshizhong2 小时前
Compose + Mvi 架构的玩android 项目,请尝鲜
android·架构·android jetpack
Chuck_Chan2 小时前
Launcher3模块化-组件化
android
xuyin12042 小时前
Android内存优化
android
jzlhll1233 小时前
android kotlinx.serialization用法和封装全解
android
龚子亦3 小时前
【Unity开发】安卓应用开发中,用户进行权限请求
android·unity·安卓权限
共享家95273 小时前
MySQL-基础查询(下)
android·mysql
查克陈Chuck3 小时前
Launcher3模块化-组件化
android·launcher开发