Android7 多用户源码解析(九)权限系统与多用户的交互机制

AOSP7 多用户源码解析(九)权限系统与多用户的交互机制

系列目录 :第一篇:功能演进与系统架构总览 | 第二篇:核心数据结构深度剖析 | 第三篇:用户创建与删除流程全链路分析 | 第四篇:用户切换机制源码剖析 | 第五篇:应用安装与管理隔离机制 | 第六篇:数据隔离与存储机制详解 | 第七篇:系统服务的多用户感知处理 | [第八篇:广播分发与 Intent 路由的用户隔离](#第八篇:广播分发与 Intent 路由的用户隔离) | 第九篇:权限系统与多用户的交互机制 | [第十篇:Work Profile 企业模式与开发实战指南](#第十篇:Work Profile 企业模式与开发实战指南)


Android 6.0 引入运行时权限之后,权限系统出现了一个新的维度:同一应用的权限授予,在不同用户下可以不同 。主用户允许了相机权限,第二个用户可以拒绝;用户 A 的授权对话框结果,不能泄漏给用户 B。此外,跨用户调用(INTERACT_ACROSS_USERS)本身也是一组权限,它们的检查又依赖权限系统,形成一圈闭环。

本篇回答三个问题:

  1. 权限授予状态在内存里如何按用户存储?为什么安装权限共享、运行时权限隔离?
  2. 一次 checkPermission 调用从 ContextImpl 到 PermissionsState 的完整链路是什么?
  3. 运行时权限按用户持久化到哪个文件?设备策略(DPM)的按用户数据又是如何组织的?

需要特别说明:权限系统在 AOSP 7 中有两套完全不同的存储结构------安装权限全局共享 (挂在 USER_ALL 桶上),运行时权限按 userId 隔离 。原稿中"位数组 1 << userId"式的描述并不存在于 AOSP 7 源码中,以下全部基于真实源码。


一、PermissionsState:按用户组织的权限状态

权限状态的载体是每个包(或共享用户)持有一个 PermissionsState 对象,挂载在 PackageSetting 上。

源码路径 :frameworks/base/services/core/java/com/android/server/pm/PermissionsState.java

java 复制代码
public final class PermissionsState {

    /** 权限名 -> 该权限在所有用户上的授予状态 */
    private ArrayMap<String, PermissionData> mPermissions;

    /** 全局 gids(与用户无关) */
    private int[] mGlobalGids = NO_GIDS;

    /** 哪些用户还需要权限审查(FLAG_PERMISSION_REVIEW_REQUIRED) */
    private SparseBooleanArray mPermissionReviewRequired;

    /**
     * 安装权限授予给所有设备用户(userId = USER_ALL)
     */
    public int grantInstallPermission(BasePermission permission) {
        return grantPermission(permission, UserHandle.USER_ALL);
    }

    /**
     * 运行时权限只授予给指定用户
     */
    public int grantRuntimePermission(BasePermission permission, int userId) {
        enforceValidUserId(userId);
        if (userId == UserHandle.USER_ALL) {
            return PERMISSION_OPERATION_FAILURE;
        }
        return grantPermission(permission, userId);
    }

    /**
     * 查询某用户对某权限是否已授予(不区分安装/运行时)
     */
    public boolean hasPermission(String name, int userId) {
        enforceValidUserId(userId);

        if (mPermissions == null) {
            return false;
        }

        PermissionData permissionData = mPermissions.get(name);
        return permissionData != null && permissionData.isGranted(userId);
    }
}

关键设计 :mPermissions 是两级结构------外层 ArrayMap<String, PermissionData> 按权限名索引,内层 PermissionData 按 userId 索引。安装权限和运行时权限共用同一套结构,区别只在于"用户键":安装权限写入 UserHandle.USER_ALL(-2)这个特殊桶,运行时权限写入具体 userId。

1.1 PermissionData:单权限的按用户状态

java 复制代码
// 源码路径同上(PermissionsState.java,内部类)
private static final class PermissionData {
    private final BasePermission mPerm;

    /**
     * 每个用户的授予状态。
     * 安装权限只会有一个条目:key 为 UserHandle.USER_ALL。
     * 运行时权限每个用户一个条目:key 为具体 userId。
     */
    private SparseArray<PermissionState> mUserStates = new SparseArray<>();

    public boolean isGranted(int userId) {
        if (isInstallPermission()) {
            userId = UserHandle.USER_ALL;
        }

        PermissionState userState = mUserStates.get(userId);
        if (userState == null) {
            return false;
        }

        return userState.mGranted;
    }

    public boolean grant(int userId) {
        if (!isCompatibleUserId(userId)) {
            return false;
        }

        if (isGranted(userId)) {
            return false;
        }

        PermissionState userState = mUserStates.get(userId);
        if (userState == null) {
            userState = new PermissionState(mPerm.name);
            mUserStates.put(userId, userState);
        }

        userState.mGranted = true;
        return true;
    }

    private boolean isInstallPermission() {
        return mUserStates.size() == 1
                && mUserStates.get(UserHandle.USER_ALL) != null;
    }
}

关键设计 :isInstallPermission() 通过"唯一条目且键为 USER_ALL"来识别安装权限。这意味着一个权限要么是全局共享的安装权限,要么是若干用户的运行时权限,二者不会混在一起。grant() 中的 isCompatibleUserId 保证不会往安装权限桶里写具体用户(反之亦然)。

PermissionState 本身非常轻量:

java 复制代码
public static final class PermissionState {
    private final String mName;
    private boolean mGranted;
    private int mFlags;

    public boolean isDefault() {
        return !mGranted && mFlags == 0;
    }

    public boolean isGranted() {
        return mGranted;
    }

    public int getFlags() {
        return mFlags;
    }
}

关键设计 :mGranted 表示是否授予,mFlags 携带 FLAG_PERMISSION_USER_SET、FLAG_PERMISSION_USER_FIXED、FLAG_PERMISSION_REVIEW_REQUIRED 等标志位。isDefault() 为 true 时该条目会被从 mUserStates 中删除,避免空桶堆积。

1.2 gids 按用户计算

权限还关联 Linux gids(多个权限共享同一 gid 组)。PermissionsState 按用户独立计算:

java 复制代码
// 源码路径同上(PermissionsState.java)
public int[] computeGids(int userId) {
    enforceValidUserId(userId);

    int[] gids = mGlobalGids;

    if (mPermissions != null) {
        final int permissionCount = mPermissions.size();
        for (int i = 0; i < permissionCount; i++) {
            String permission = mPermissions.keyAt(i);
            if (!hasPermission(permission, userId)) {
                continue;
            }
            PermissionData permissionData = mPermissions.valueAt(i);
            final int[] permGids = permissionData.computeGids(userId);
            if (permGids != NO_GIDS) {
                gids = appendInts(gids, permGids);
            }
        }
    }

    return gids;
}

关键设计:gids 不是独立存储的,而是从"该用户实际被授予的权限"即时推导。用户 A 授予了某个带 gid 的权限而用户 B 没有,两者的进程 gid 集合就会不同------这是权限隔离落到 Linux 层的方式之一。


二、运行时权限的授予与撤销

应用请求运行时权限,最终走到 PackageManagerService 的两个方法。注意 AOSP 7 中没有 grantRuntimePermissionLPw 这样的内部方法名,真正的入口就是带权限校验的公开方法。

源码路径 :frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java

java 复制代码
@Override
public void grantRuntimePermission(String packageName, String name, final int userId) {
    if (!sUserManager.exists(userId)) {
        Log.e(TAG, "No such user:" + userId);
        return;
    }

    mContext.enforceCallingOrSelfPermission(
            android.Manifest.permission.GRANT_RUNTIME_PERMISSIONS,
            "grantRuntimePermission");

    enforceCrossUserPermission(Binder.getCallingUid(), userId,
            true /* requireFullPermission */, true /* checkShell */,
            "grantRuntimePermission");

    final int uid;
    final SettingBase sb;

    synchronized (mPackages) {
        final PackageParser.Package pkg = mPackages.get(packageName);
        if (pkg == null) {
            throw new IllegalArgumentException("Unknown package: " + packageName);
        }

        final BasePermission bp = mSettings.mPermissions.get(name);
        if (bp == null) {
            throw new IllegalArgumentException("Unknown permission: " + name);
        }

        enforceDeclaredAsUsedAndRuntimeOrDevelopmentPermission(pkg, bp);

        uid = UserHandle.getUid(userId, pkg.applicationInfo.uid);
        sb = (SettingBase) pkg.mExtras;
        if (sb == null) {
            throw new IllegalArgumentException("Unknown package: " + packageName);
        }

        final PermissionsState permissionsState = sb.getPermissionsState();

        final int flags = permissionsState.getPermissionFlags(name, userId);
        if ((flags & PackageManager.FLAG_PERMISSION_SYSTEM_FIXED) != 0) {
            throw new SecurityException("Cannot grant system fixed permission "
                    + name + " for package " + packageName);
        }

        // ...development 权限走 install 桶,targetSdk < M 的 legacy 应用直接拒绝...

        final int result = permissionsState.grantRuntimePermission(bp, userId);
        switch (result) {
            case PermissionsState.PERMISSION_OPERATION_FAILURE: {
                return;
            }

            case PermissionsState.PERMISSION_OPERATION_SUCCESS_GIDS_CHANGED: {
                final int appId = UserHandle.getAppId(pkg.applicationInfo.uid);
                mHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        killUid(appId, userId, KILL_APP_REASON_GIDS_CHANGED);
                    }
                });
            }
            break;
        }

        mOnPermissionChangeListeners.onPermissionsChanged(uid);

        // 非关键路径:应用丢失后需要重新申请
        mSettings.writeRuntimePermissionsForUserLPr(userId, false);
    }
}

关键设计 :权限校验有三层------GRANT_RUNTIME_PERMISSIONS(signature|installer|verifier)、enforceCrossUserPermission(跨用户需 INTERACT_ACROSS_USERS_FULL)、FLAG_PERMISSION_SYSTEM_FIXED(策略固定的权限不可授予)。注意第三层校验用的 name 和 userId 都是显式参数:系统策略可以把某个权限对特定用户 固定为拒绝,此时即使用户在界面上点了"允许"也会被 SecurityException 拦截。

gids 变化时通过 killUid(appId, userId, ...) 杀掉该用户下对应 appId 的进程------隔离再次体现在进程维度:用户 0 的授权变更不会触发用户 10 的进程重启。

revokeRuntimePermission 结构对称:

java 复制代码
// 源码路径同上(PackageManagerService.java)
@Override
public void revokeRuntimePermission(String packageName, String name, int userId) {
    if (!sUserManager.exists(userId)) {
        Log.e(TAG, "No such user:" + userId);
        return;
    }

    mContext.enforceCallingOrSelfPermission(
            android.Manifest.permission.REVOKE_RUNTIME_PERMISSIONS,
            "revokeRuntimePermission");

    enforceCrossUserPermission(Binder.getCallingUid(), userId,
            true /* requireFullPermission */, true /* checkShell */,
            "revokeRuntimePermission");

    synchronized (mPackages) {
        // ...同样的 pkg/bp/sb 查找与 FLAG_PERMISSION_SYSTEM_FIXED 校验...
        // 撤销后若 gids 变化同样 killUid(appId, userId, KILL_APP_REASON_GIDS_CHANGED)
    }
}

关键设计 :撤销与授予共用 enforceCrossUserPermission(requireFullPermission = true)。这意味着 INTERACT_ACROSS_USERS(非 FULL)只能操作自己用户下的权限,无法跨用户改别人的授权状态。


三、checkPermission 的完整链路

3.1 客户端入口

源码路径 :frameworks/base/core/java/android/app/ContextImpl.java

java 复制代码
@Override
public int checkPermission(String permission, int pid, int uid) {
    if (permission == null) {
        throw new IllegalArgumentException("permission is null");
    }

    try {
        return ActivityManagerNative.getDefault().checkPermission(
                permission, pid, uid);
    } catch (RemoteException e) {
        throw e.rethrowFromSystemServer();
    }
}

关键设计 :客户端不解析 userId------checkPermission(permission, pid, uid) 只有三个参数,用户维度完全由服务端从 uid 推导。这保证了客户端无法通过伪造参数绕过检查。

3.2 AMS 中转

源码路径 :frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java

java 复制代码
@Override
public int checkPermission(String permission, int pid, int uid) {
    if (permission == null) {
        return PackageManager.PERMISSION_DENIED;
    }
    return checkComponentPermission(permission, pid, uid, -1, true);
}

/**
 * 这里只处理自己 pid(system_server)的快捷路径,
 * 其余委托给 ActivityManager 的静态方法
 */
int checkComponentPermission(String permission, int pid, int uid,
        int owningUid, boolean exported) {
    if (pid == MY_PID) {
        return PackageManager.PERMISSION_GRANTED;
    }
    return ActivityManager.checkComponentPermission(permission, uid,
            owningUid, exported);
}

关键设计 :AMS 自身不做权限判定,只处理 system_server(pid == MY_PID)的放行,其余全部委托给 ActivityManager.checkComponentPermission 静态方法。

3.3 ActivityManager 静态判定

源码路径 :frameworks/base/core/java/android/app/ActivityManager.java

java 复制代码
public static int checkComponentPermission(String permission, int uid,
        int owningUid, boolean exported) {
    // Root、system_server 放行
    final int appId = UserHandle.getAppId(uid);
    if (appId == Process.ROOT_UID || appId == Process.SYSTEM_UID) {
        return PackageManager.PERMISSION_GRANTED;
    }
    // Isolated 进程没有任何权限
    if (UserHandle.isIsolated(uid)) {
        return PackageManager.PERMISSION_DENIED;
    }
    // 同一个 app(owningUid 与 uid 同 app)放行
    if (owningUid >= 0 && UserHandle.isSameApp(uid, owningUid)) {
        return PackageManager.PERMISSION_GRANTED;
    }
    // 非 exported 直接拒绝
    if (!exported) {
        return PackageManager.PERMISSION_DENIED;
    }
    if (permission == null) {
        return PackageManager.PERMISSION_GRANTED;
    }
    try {
        return AppGlobals.getPackageManager()
                .checkUidPermission(permission, uid);
    } catch (RemoteException e) {
        throw e.rethrowFromSystemServer();
    }
}

关键设计 :判定顺序是"系统身份 → isolated 进程 → 同 app → exported → 查权限表"。UserHandle.isSameApp 比较的是 appId(uid / 100000 无关,appId = uid % 100000),即跨用户但同 appId 的进程视为同一个应用------这是共享 UID(sharedUserId)跨用户场景的基础。

3.4 PMS 终端:从 uid 推导 userId

源码路径 :frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java

java 复制代码
@Override
public int checkUidPermission(String permName, int uid) {
    final int userId = UserHandle.getUserId(uid);

    if (!sUserManager.exists(userId)) {
        return PackageManager.PERMISSION_DENIED;
    }

    synchronized (mPackages) {
        Object obj = mSettings.getUserIdLPr(UserHandle.getAppId(uid));
        if (obj != null) {
            final SettingBase ps = (SettingBase) obj;
            final PermissionsState permissionsState = ps.getPermissionsState();
            if (permissionsState.hasPermission(permName, userId)) {
                return PackageManager.PERMISSION_GRANTED;
            }
            // 特例:ACCESS_FINE_LOCATION 隐含 ACCESS_COARSE_LOCATION
            if (Manifest.permission.ACCESS_COARSE_LOCATION.equals(permName)
                    && permissionsState.hasPermission(
                            Manifest.permission.ACCESS_FINE_LOCATION, userId)) {
                return PackageManager.PERMISSION_GRANTED;
            }
        } else {
            ArraySet<String> perms = mSystemPermissions.get(uid);
            if (perms != null) {
                if (perms.contains(permName)) {
                    return PackageManager.PERMISSION_GRANTED;
                }
                // ...同样有 FINE 隐含 COARSE 的特例...
            }
        }
    }

    return PackageManager.PERMISSION_DENIED;
}

关键设计 :UserHandle.getUserId(uid) 是整条链路中唯一一次"用户推导"------userId = uid / 100000,appId = uid % 100000。之后所有判定都按这个 userId 查 PermissionsState。mSystemPermissions 是给平台组件(如 shell)用的旁路表,与包无关。

注意 hasPermission 内部会区分安装权限和运行时权限(见第一节):安装权限查 USER_ALL 桶,运行时权限查具体 userId 桶。两个桶都查不到就是拒绝。

3.5 带包名的 checkPermission

PMS 还有一个不走 uid 而直接按包名 + userId 查询的重载,供系统内部(如 ContentProvider 权限判定)使用:

java 复制代码
// 源码路径同上(PackageManagerService.java)
@Override
public int checkPermission(String permName, String pkgName, int userId) {
    if (!sUserManager.exists(userId)) {
        return PackageManager.PERMISSION_DENIED;
    }

    synchronized (mPackages) {
        final PackageParser.Package p = mPackages.get(pkgName);
        if (p != null && p.mExtras != null) {
            final PackageSetting ps = (PackageSetting) p.mExtras;
            final PermissionsState permissionsState = ps.getPermissionsState();
            if (permissionsState.hasPermission(permName, userId)) {
                return PackageManager.PERMISSION_GRANTED;
            }
            // 同样的 FINE 隐含 COARSE 特例
            if (Manifest.permission.ACCESS_COARSE_LOCATION.equals(permName)
                    && permissionsState.hasPermission(
                            Manifest.permission.ACCESS_FINE_LOCATION, userId)) {
                return PackageManager.PERMISSION_GRANTED;
            }
        }
    }

    return PackageManager.PERMISSION_DENIED;
}

关键设计 :这个重载不做跨用户权限检查 ,因为调用方(系统进程内部)已经持有正确的上下文;外部 Binder 调用一律走 checkUidPermission 那条链路,由 AMS 中转。


四、跨用户权限的定义与校验

4.1 权限定义

源码路径 :frameworks/base/core/res/AndroidManifest.xml

xml 复制代码
<permission android:name="android.permission.INTERACT_ACROSS_USERS"
    android:protectionLevel="signature|privileged|development" />

<!-- @SystemApi Fuller form of INTERACT_ACROSS_USERS
     that removes restrictions on where broadcasts can be sent -->
<permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL"
    android:protectionLevel="signature|installer" />

<permission android:name="android.permission.MANAGE_USERS"
    android:protectionLevel="signature|privileged" />

<!-- @hide 允许创建、删除用户并获取用户列表 -->
<permission android:name="android.permission.CREATE_USERS"
    android:protectionLevel="signature" />

<permission android:name="android.permission.GRANT_RUNTIME_PERMISSIONS"
    android:protectionLevel="signature|installer|verifier" />

关键设计 :INTERACT_ACROSS_USERS 是 signature|privileged|development,INTERACT_ACROSS_USERS_FULL 是 signature|installer,MANAGE_USERS 是 signature|privileged。GRANT_RUNTIME_PERMISSIONS 的持有者是权限授予应用(Permission Controller)和包安装验证器(verifier)。这些权限本身的授予状态同样落在 PermissionsState 里,受同一套机制管理。

4.2 enforceCrossUserPermission

第七篇已经分析过 PMS 的这个私有方法,此处补充它在权限链路中的位置------它被 grantRuntimePermission、revokeRuntimePermission、getApplicationEnabledSetting 等所有"带 userId 参数"的 PMS API 调用:

java 复制代码
// 源码路径同上(PackageManagerService.java)
void enforceCrossUserPermission(int callingUid, int userId, boolean requireFullPermission,
        boolean checkShell, String message) {
    if (userId < 0) {
        throw new IllegalArgumentException("Invalid userId " + userId);
    }
    if (checkShell) {
        enforceShellRestriction(UserManager.DISALLOW_DEBUGGING_FEATURES, callingUid, userId);
    }
    if (userId == UserHandle.getUserId(callingUid)) return;
    if (callingUid != Process.SYSTEM_UID && callingUid != 0) {
        if (requireFullPermission) {
            mContext.enforceCallingOrSelfPermission(
                    android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, message);
        } else {
            mContext.enforceCallingOrSelfPermission(
                    android.Manifest.permission.INTERACT_ACROSS_USERS, message);
        }
    }
}

关键设计 :判定逻辑是"同用户放行 → 系统 uid 放行 → 否则要求跨用户权限"。requireFullPermission 决定要 FULL 还是普通版本。权限检查本身是递归安全的:enforceCallingOrSelfPermission 走 ContextImpl.checkPermission → AMS → checkUidPermission 链路,查的是调用者 uid 对应的权限状态,不存在循环。


五、运行时权限的按用户持久化

5.1 文件位置与写入入口

运行时权限不写 packages.xml,而是每个用户单独一个文件。

源码路径 :frameworks/base/services/core/java/com/android/server/pm/Settings.java

java 复制代码
private File getUserRuntimePermissionsFile(int userId) {
    // 而非 Environment.getUserSystemDirectory(userId),为测试方便
    File userDir = new File(new File(mSystemDir, "users"), Integer.toString(userId));
    return new File(userDir, RUNTIME_PERMISSIONS_FILE_NAME);
}

public void writeRuntimePermissionsForUserLPr(int userId, boolean sync) {
    if (sync) {
        mRuntimePermissionsPersistence.writePermissionsForUserSyncLPr(userId);
    } else {
        mRuntimePermissionsPersistence.writePermissionsForUserAsyncLPr(userId);
    }
}

关键设计 :文件路径为 /data/system/users/<userId>/runtime-permissions.xml(RUNTIME_PERMISSIONS_FILE_NAME 常量值)。按用户分文件意味着删除用户 10 时只需删掉 users/10/ 整个目录,权限数据随之消失,无需清理 packages.xml。

5.2 写入逻辑

java 复制代码
// 源码路径同上(Settings.java,内部类 RuntimePermissionPersistence)
private void writePermissionsSync(int userId) {
    AtomicFile destination = new AtomicFile(getUserRuntimePermissionsFile(userId));

    ArrayMap<String, List<PermissionState>> permissionsForPackage = new ArrayMap<>();
    ArrayMap<String, List<PermissionState>> permissionsForSharedUser = new ArrayMap<>();

    synchronized (mLock) {
        mWriteScheduled.delete(userId);

        final int packageCount = mPackages.size();
        for (int i = 0; i < packageCount; i++) {
            String packageName = mPackages.keyAt(i);
            PackageSetting packageSetting = mPackages.valueAt(i);
            if (packageSetting.sharedUser == null) {
                PermissionsState permissionsState = packageSetting.getPermissionsState();
                List<PermissionState> permissionsStates = permissionsState
                        .getRuntimePermissionStates(userId);
                if (!permissionsStates.isEmpty()) {
                    permissionsForPackage.put(packageName, permissionsStates);
                }
            }
        }

        // sharedUser 同理,写入 TAG_SHARED_USER 块
    }

    FileOutputStream out = null;
    try {
        out = destination.startWrite();

        XmlSerializer serializer = Xml.newSerializer();
        serializer.setOutput(out, StandardCharsets.UTF_8.name());
        serializer.setFeature(
                "http://xmlpull.org/v1/doc/features.html#indent-output", true);
        serializer.startDocument(null, true);

        serializer.startTag(null, TAG_RUNTIME_PERMISSIONS);

        String fingerprint = mFingerprints.get(userId);
        if (fingerprint != null) {
            serializer.attribute(null, ATTR_FINGERPRINT, fingerprint);
        }

        final int packageCount = permissionsForPackage.size();
        for (int i = 0; i < packageCount; i++) {
            String packageName = permissionsForPackage.keyAt(i);
            List<PermissionState> permissionStates = permissionsForPackage.valueAt(i);
            serializer.startTag(null, TAG_PACKAGE);
            serializer.attribute(null, ATTR_NAME, packageName);
            writePermissions(serializer, permissionStates);
            serializer.endTag(null, TAG_PACKAGE);
        }

        // sharedUser 块同理
        // restored 权限块(TAG_RESTORED_RUNTIME_PERMISSIONS)同理

        serializer.endTag(null, TAG_RUNTIME_PERMISSIONS);
        serializer.endDocument();
        destination.finishWrite(out);
    }
}

关键设计 :写入的是 getRuntimePermissionStates(userId) 的运行时权限 列表,安装权限不写这个文件(它们写在 packages.xml 的包定义里,全局共享)。fingerprint 属性记录当前构建指纹,用于 OTA 升级后判断"默认权限是否已授予"(mDefaultPermissionsGranted)。

5.3 写入调度

java 复制代码
// 源码路径同上(Settings.java)
public void writePermissionsForUserAsyncLPr(int userId) {
    final long currentTimeMillis = SystemClock.uptimeMillis();

    if (mWriteScheduled.get(userId)) {
        mHandler.removeMessages(userId);

        // 距上次未写变更若超过 2 秒,立即落盘
        final long lastNotWrittenMutationTimeMillis = mLastNotWrittenMutationTimesMillis
                .get(userId);
        final long timeSinceLastNotWrittenMutationMillis = currentTimeMillis
                - lastNotWrittenMutationTimeMillis;
        if (timeSinceLastNotWrittenMutationMillis >= MAX_WRITE_PERMISSIONS_DELAY_MILLIS) {
            mHandler.obtainMessage(userId).sendToTarget();
            return;
        }

        final long maxDelayMillis = Math.max(lastNotWrittenMutationTimeMillis
                + MAX_WRITE_PERMISSIONS_DELAY_MILLIS - currentTimeMillis, 0);
        final long writeDelayMillis = Math.min(WRITE_PERMISSIONS_DELAY_MILLIS, maxDelayMillis);
        Message message = mHandler.obtainMessage(userId);
        mHandler.sendMessageDelayed(message, writeDelayMillis);
    } else {
        mLastNotWrittenMutationTimesMillis.put(userId, currentTimeMillis);
        Message message = mHandler.obtainMessage(userId);
        mHandler.sendMessageDelayed(message, WRITE_PERMISSIONS_DELAY_MILLIS);
        mWriteScheduled.put(userId, true);
    }
}

关键设计 :防抖写入------首次变更延迟 200ms,连续变更最长 2s 内必须落盘。调度以 userId 为键 (SparseBooleanArray mWriteScheduled、SparseLongArray mLastNotWrittenMutationTimesMillis),各用户独立计时,互不阻塞。


六、设备策略(DPM)的按用户数据

6.1 数据结构

源码路径 :frameworks/base/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java

java 复制代码
final class DevicePolicyData {
    final int mUserHandle;

    // 密码策略、锁屏、用户配置完成状态等(按用户一份)
    int mPasswordOwner = -1;
    long mLastMaximumTimeToLock = -1;
    boolean mUserSetupComplete = false;
    int mUserProvisioningState;
    int mPermissionPolicy;

    // 该用户下的所有激活的 admin
    final ArrayMap<ComponentName, ActiveAdmin> mAdminMap = new ArrayMap<>();
    final ArrayList<ActiveAdmin> mAdminList = new ArrayList<>();
    final ArrayList<ComponentName> mRemovingAdmins = new ArrayList<>();

    // 各策略字段(证书、lock task 包、代理排除列表等)...
}

final SparseArray<DevicePolicyData> mUserData = new SparseArray<>();

关键设计 :mUserData 是 SparseArray<DevicePolicyData>,以 userId 为键,每个用户一份完整的策略数据。注意 AOSP 7 中没有 DeviceAdminData 这个类名------admin 状态类叫 ActiveAdmin,且 admin 是挂到 DevicePolicyData.mAdminMap(ArrayMap<ComponentName, ActiveAdmin>)里,不是单独的 per-user 字典。

6.2 ActiveAdmin:单 admin 的策略字段

java 复制代码
// 源码路径同上(DevicePolicyManagerService.java)
static class ActiveAdmin {
    final DeviceAdminInfo info;

    int passwordQuality = DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
    int minimumPasswordLength = DEF_MINIMUM_PASSWORD_LENGTH;
    int minimumPasswordUpperCase = DEF_MINIMUM_PASSWORD_UPPER_CASE;
    int minimumPasswordLowerCase = DEF_MINIMUM_PASSWORD_LOWER_CASE;
    int minimumPasswordLetters = DEF_MINIMUM_PASSWORD_LETTERS;
    int minimumPasswordNumeric = DEF_MINIMUM_PASSWORD_NUMERIC;
    int minimumPasswordSymbols = DEF_MINIMUM_PASSWORD_SYMBOLS;
    int minimumPasswordNonLetter = DEF_MINIMUM_PASSWORD_NON_LETTER;
    long maximumTimeToUnlock = DEF_MAXIMUM_TIME_TO_UNLOCK;
    int maximumFailedPasswordsForWipe = DEF_MAXIMUM_FAILED_PASSWORDS_FOR_WIPE;
    long passwordExpirationTimeout = DEF_PASSWORD_EXPIRATION_TIMEOUT;
    long passwordExpirationDate = DEF_PASSWORD_EXPIRATION_DATE;
    int disabledKeyguardFeatures = DEF_KEYGUARD_FEATURES_DISABLED;

    boolean encryptionRequested = false;
    boolean disableCamera = false;
    boolean disableCallerId = false;
    boolean disableScreenCapture = false;   // 只能由 device/profile owner 设置
    boolean requireAutoTime = false;        // 只能由 device owner 设置
    boolean forceEphemeralUsers = false;    // 只能由 device owner 设置

    ActiveAdmin parentAdmin;
    final boolean isParent;
}

关键设计 :ActiveAdmin 直接持有策略字段(密码质量、禁用相机、禁用截屏等),isParent 标记是否为"父 admin"(Work Profile 场景下 profile owner 的 admin 会引用 parent admin)。getProfileOwnerAdminLocked(userId)、getDeviceOwnerAdminLocked() 等查询方法都按 userId 定位到对应的 DevicePolicyData 再取 admin。

6.3 Work Profile 创建的用户侧入口

UserManager 提供的 profile 创建 API(DPM 侧最终调用它):

源码路径 :frameworks/base/core/java/android/os/UserManager.java

java 复制代码
public UserInfo createProfileForUser(String name, int flags, @UserIdInt int userHandle) {
    try {
        return mService.createProfileForUser(name, flags, userHandle);
    } catch (RemoteException e) {
        throw e.rethrowFromSystemServer();
    }
}

源码路径 :frameworks/base/services/core/java/com/android/server/pm/UserManagerService.java

java 复制代码
@Override
public UserInfo createProfileForUser(String name, int flags, int userId) {
    // ...权限校验(CREATE_USERS / MANAGE_USERS)与参数校验...
    // 调用 createUserInternal 走与创建普通用户相同的初始化链路
}

关键设计 :createProfileForUser(name, flags, userHandle) 的第三个参数是父用户的 userId ,不是 ComponentName。flags 传入 UserInfo.FLAG_MANAGED_PROFILE 表示工作配置。DPM 侧(DevicePolicyManager)没有 createAndManageUser 这样的公开方法------创建 profile 并设为 profile owner 的流程由 Provisioning 相关代码和 setProfileOwner 串联完成,具体见第十篇。


七、调试技巧

7.1 查看某包在某用户下的权限状态

bash 复制代码
# dumpsys package 会输出每个用户的 grantedPermissions
adb shell dumpsys package com.example.app | grep -A 30 "User 0:"
adb shell dumpsys package com.example.app | grep -A 30 "User 10:"

# 对比两个用户的 grantedPermissions 列表,即可看到运行时权限的差异

7.2 按用户授予 / 撤销运行时权限

bash 复制代码
# 给用户 10 授予相机权限
adb shell pm grant --user 10 com.example.app android.permission.CAMERA

# 撤销
adb shell pm revoke --user 10 com.example.app android.permission.CAMERA

# pm 内部调用 PackageManagerService.grantRuntimePermission / revokeRuntimePermission

7.3 查看权限持久化文件

bash 复制代码
# 每个用户独立的运行时权限文件
adb shell ls /data/system/users/
adb shell cat /data/system/users/10/runtime-permissions.xml

7.4 权限日志

bash 复制代码
# PMS 权限操作日志
adb logcat -s PackageManager

# 权限被拒绝时的异常栈
adb logcat | grep -E "SecurityException|PERMISSION_DENIED"

八、本篇总结

  1. 两级权限存储 :PermissionsState 外层按权限名(ArrayMap<String, PermissionData>)、内层按 userId(SparseArray<PermissionState>)。安装权限占 USER_ALL 桶全局共享,运行时权限按具体 userId 隔离。
  2. checkPermission 链路 :ContextImpl.checkPermission(permission, pid, uid) → AMS.checkPermission → ActivityManager.checkComponentPermission(系统身份/isolated/同 app/exported 判定)→ PMS.checkUidPermission(UserHandle.getUserId(uid) 推导用户,查 PermissionsState)。
  3. 运行时权限持久化 :每个用户独立文件 /data/system/users/<id>/runtime-permissions.xml,由 RuntimePermissionPersistence 按 userId 防抖写入;安装权限则写在 packages.xml。
  4. 跨用户权限 :INTERACT_ACROSS_USERS(signature|privileged|development)/ INTERACT_ACROSS_USERS_FULL(signature|installer)/ MANAGE_USERS(signature|privileged),统一由 enforceCrossUserPermission 在 PMS 各带 userId 的 API 中校验。
  5. DPM 按用户组织 :mUserData(SparseArray<DevicePolicyData>)每用户一份,内含 mAdminMap(ArrayMap<ComponentName, ActiveAdmin>);admin 策略字段直接挂在 ActiveAdmin 上。

关键源码路径:

复制代码
frameworks/base/services/core/java/com/android/server/pm/PermissionsState.java
frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java
frameworks/base/services/core/java/com/android/server/pm/Settings.java
frameworks/base/services/core/java/com/android/server/pm/UserManagerService.java
frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
frameworks/base/core/java/android/app/ActivityManager.java
frameworks/base/core/java/android/app/ContextImpl.java
frameworks/base/core/java/android/os/UserManager.java
frameworks/base/core/res/AndroidManifest.xml
frameworks/base/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java

下一篇 :[第十篇:Work Profile 企业模式与开发实战指南](#第十篇:Work Profile 企业模式与开发实战指南) ------ 全系列最后一篇,深入 Work Profile 的完整创建链路、跨 profile 交互(startActivityAsUser + INTERACT_ACROSS_USERS)与实战开发要点。

相关推荐
数字新视界3 小时前
国产动环系统技术原理与应用实践的深入剖析
嵌入式硬件·物联网·系统架构·机房管理·动力与环境监控系统
m0_5873830014 小时前
深圳24小时自助健身房解决方案实战:从系统架构到部署指南
java·人工智能·spring boot·spring·系统架构·需求分析
m0_5873830017 小时前
西安同城拼车软件开发实战指南:从零搭建高效系统
人工智能·小程序·数据挖掘·系统架构·需求分析
励志不掉头发的内向程序员21 小时前
【从零写一个CAD 02】画完第一条线之后:实体容器、Esc 取消,和按了没反应的键盘
开发语言·c++·qt·学习·系统架构
Cicada1287 天前
库存消息消费的正确性设计——从幂等窗口到批量流水线
分布式·系统架构
彧azz7 天前
操作系统时间管理与系统核心板块学习总结
c语言·笔记·学习·系统架构
风123456789~7 天前
【架构专栏】第15章 面向服务架构设计 2/3
系统架构
风123456789~7 天前
【架构专栏】第15章 面向服务架构设计 1/3
系统架构
智慧物业老杨7 天前
物业日常巡查的数智化重构:从“打卡式巡检“到“闭环式风控“
android·java·人工智能·系统架构·rxjava