
StorageStatsService 与应用目录管理
在 Android 系统中,应用的存储目录(包括私有数据目录、缓存目录、APK 安装目录等)的创建与权限管理是保障应用隔离性和数据安全的核心环节。
StorageStatsService 作为 StorageManagerService 的核心子服务,虽不直接负责目录的创建,但其通过与 PackageManagerService(PMS)、MountService 等系统服务的协作,实现了应用目录存储统计的精准性,同时参与了目录权限的校验与管理。
本文将从应用目录的创建主体、StorageStatsService 的协作角色、权限管理的底层实现、源码级的流程分析四个维度,详细解析 APK 安装后应用目录的创建逻辑与权限管控机制,并结合核心源码揭示 StorageStatsService 的核心作用。
核心认知StorageStatsService
首先需要明确的是:StorageStatsService 并不直接负责 APK 安装后应用目录的创建 ,其核心职责是统计应用目录的存储占用。
应用目录的创建主要由以下系统服务完成:
- PackageManagerService(PMS) :APK 安装的核心服务,负责创建应用的 APK 安装目录(
/data/app/<package-name>/)、lib 库目录等。 - ActivityManagerService(AMS) :应用首次启动时,负责创建应用的私有数据目录(
/data/data/<package-name>/)、缓存目录(/data/data/<package-name>/cache/)等。 - StorageManagerService(SMS):协调存储卷的挂载状态,为目录创建提供存储环境支持。
StorageStatsService 的角色:作为"存储统计的执行者",它通过扫描上述目录的文件系统信息,计算应用的存储占用,并将统计结果对外暴露。同时,它会校验应用目录的权限是否合规,确保统计数据的准确性和安全性。
应用目录的创建流
###.1 APK 安装阶段的目录创建(PMS 主导)
当 APK 通过应用市场或 pm install 命令安装时,PMS 会完成以下目录的创建:
- APK 安装目录 :
/data/app/<package-name>-<random-suffix>/(Android 7.0+ 引入的随机后缀,增强安全性),用于存放解压后的 APK 文件和 lib 库。 - ODEX 目录 :
/data/dalvik-cache/下的对应 odex 文件目录,用于存放优化后的字节码。 - 基本目录结构 :如
files、lib等基础子目录的初始化。
2 应用首次启动的目录创建(AMS 主导)
应用首次启动时,AMS 会通过 ApplicationPackageManager 触发应用私有目录的创建:
- 私有数据目录 :
/data/data/<package-name>/,这是应用的核心私有目录,其他应用无法访问。 - 缓存目录 :
/data/data/<package-name>/cache/,用于存放应用的临时缓存数据。 - 外部私有目录 :
/sdcard/Android/data/<package-name>/,用于存放应用的外部私有数据。
3 StorageStatsService 的介入时机
当应用目录创建完成后,StorageStatsService 会在以下场景介入:
- 首次统计:应用安装完成后,StorageStatsService 会被 PMS 触发,首次扫描应用目录,生成初始的存储统计数据。
- 实时统计:当应用读写数据时,StorageStatsService 会通过文件系统的事件监听(如 inotify)更新统计数据。
- 权限校验:统计前会校验应用目录的权限是否符合 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 或默认值。
- 系统通知:严重的权限异常会通知 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 采用了以下优化策略:
- 缓存机制:将计算后的应用存储统计数据缓存到内存中,避免重复扫描,缓存有效期为 5 分钟。
- 异步扫描:采用异步线程池执行目录扫描,避免阻塞主线程。
- 增量更新:通过 inotify 监听应用目录的文件变化,仅更新变化的文件大小,而非全量扫描。
- 权限过滤 :跳过无权限访问的文件,减少无效的 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. 简化输出:只提取关键数据
通过管道命令(grep、awk)可过滤掉无关信息,只保留需要的字段:
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 安装后应用目录的创建,但它是应用目录权限管控的校验者 和存储统计的执行者:
- 权限管理:通过校验应用目录的所有者、权限值等,保障应用数据的隔离性和安全性。
- 存储统计:通过递归扫描目录、累加文件大小,实现了应用级存储统计的精准性,为系统和应用提供了可靠的存储数据支撑。
- 协作机制 :与 PMS、AMS、StorageManagerService 等服务的协作,形成了应用目录从创建到统计的完整链路。
