【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 等服务的协作,形成了应用目录从创建到统计的完整链路。
相关推荐
阿巴斯甜1 天前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker1 天前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq95271 天前
Andorid Google 登录接入文档
android
黄林晴1 天前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab2 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿2 天前
Android MediaPlayer 笔记
android
Jony_2 天前
Android 启动优化方案
android
阿巴斯甜2 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇2 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_2 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android