Android多用户的一些坑

最近关于多用户功能报了几个bug,我觉得蛮有意思的这里记录一下。

起因是是测试报了打开了多用户功能并且切到另外一个用户之后,系统功能异常。调试发现我们的中间层服务启动了两个进程:

system         6074   2524 14649520 96244 SyS_epoll_wait      0 S me.linjw.demo.multiuser
u10_system     7991   2524 14582664 94148 SyS_epoll_wait      0 S me.linjw.demo.multiuser

从上面可以看出me.linjw.demo.multiuser这个应用分别在USER为system和u10_system各起了一个进程。查找了下资料发现正常情况下一个应用进程的确是不能跨用户访问的,会在不同的用户下启动新的进程。

android:singleUser配置

由于历史代码原因,我们系统上的硬件操控接口的确不支持多个进程访问,也不好修改。只能靠我们应用做规避。使用ps命令查看了下,发现像system_server这样的系统服务在多用户下也只有一个进程。谷歌应该会考虑到这种多用户共用一个进程的场景,于是在开发者文档中找到多用户相关文档:

如需将应用识别为单例,请将 android:singleUser=”true” 添加至 Android 清单中的服务、接收器或提供程序。

由于现象是多个进程,我下意识认为这个singleUser配置是针对应用的,所以在AndroidManifest.xml的application标签中配置上去,但是发现没有作用:

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:singleUser="true"
        android:theme="@style/Theme.MultiuserDemo">

由于文档里面没有其他信息,从网上搜索找到的类似需要系统签名、android:persistent需要为true这样的信息也确认过没有效果。本来还怀疑是我们framework里面做了什么修改导把这块改坏了。

于是去看这部分相关的framework源码,主要逻辑在ActiveServices.retrieveServiceLocked里面,子用户里启动服务的时候会去通过isSingleton判断是否使用主用户的进程,如果是的话就使用主用户的进程,不需要新启动一个进程::

// frameworks/base/services/core/java/com/android/server/am/ActiveServices.java
private ServiceLookupResult retrieveServiceLocked(Intent service,
        String instanceName, String resolvedType, String callingPackage,
        int callingPid, int callingUid, int userId,
        boolean createIfNeeded, boolean callingFromFg, boolean isBindExternal,
        boolean allowInstant) {
    ...
    // 这里在不同userId下查询出来的rInfo就是不一样的
    ResolveInfo rInfo = mAm.getPackageManagerInternalLocked().resolveService(service,
            resolvedType, flags, userId, callingUid);
    ServiceInfo sInfo = rInfo != null ? rInfo.serviceInfo : null;
    ...
    // userId不为0代表子用户下运行
    if (userId > 0) {
        if (mAm.isSingleton(sInfo.processName, sInfo.applicationInfo,
                sInfo.name, sInfo.flags)
                && mAm.isValidSingletonCall(callingUid, sInfo.applicationInfo.uid)) {
            // 如果组件isSingleton判断为true
            // 则将userId改成0,并使用clearCallingIdentity清除调用进程的用户信息,重新查询
            // 则查出来的rInfoForUserId0为主用户的缓存  
            userId = 0;
            smap = getServiceMapLocked(0);
            // Bypass INTERACT_ACROSS_USERS permission check
            final long token = Binder.clearCallingIdentity();
            try {
                ResolveInfo rInfoForUserId0 = mAm.getPackageManagerInternalLocked().resolveService(service,
                                resolvedType, flags, userId, callingUid);
                if (rInfoForUserId0 == null) {
                    Slog.w(TAG_SERVICE,
                            "Unable to resolve service " + service + " U=" + userId
                                    + ": not found");
                    return null;
                }
                // 然后用这个rInfoForUserId0.serviceInfo去替换之前查出来的rInfo.serviceInfo,保证多用户下都用主用户下的同一个进程
                sInfo = rInfoForUserId0.serviceInfo;
            } finally {
                Binder.restoreCallingIdentity(token);
            }
        }
        sInfo = new ServiceInfo(sInfo);
        sInfo.applicationInfo = mAm.getAppInfoForUser(sInfo.applicationInfo, userId);
    }
    ...
}

判断是否在多用户下只启动单个进程主要靠isSingleton这个方法:

// frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
boolean isSingleton(String componentProcessName, ApplicationInfo aInfo,
        String className, int flags) {
    boolean result = false;
    // For apps that don't have pre-defined UIDs, check for permission
    if (UserHandle.getAppId(aInfo.uid) >= FIRST_APPLICATION_UID) {
        if ((flags & ServiceInfo.FLAG_SINGLE_USER) != 0) {
            if (ActivityManager.checkUidPermission(
                    INTERACT_ACROSS_USERS,
                    aInfo.uid) != PackageManager.PERMISSION_GRANTED) {
                ComponentName comp = new ComponentName(aInfo.packageName, className);
                String msg = "Permission Denial: Component " + comp.flattenToShortString()
                        + " requests FLAG_SINGLE_USER, but app does not hold "
                        + INTERACT_ACROSS_USERS;
                Slog.w(TAG, msg);
                throw new SecurityException(msg);
            }
            // Permission passed
            result = true;
        }
    } else if ("system".equals(componentProcessName)) {
        result = true;
    } else if ((flags & ServiceInfo.FLAG_SINGLE_USER) != 0) {
        // Phone app and persistent apps are allowed to export singleuser providers.
        result = UserHandle.isSameApp(aInfo.uid, PHONE_UID)
                || (aInfo.flags & ApplicationInfo.FLAG_PERSISTENT) != 0;
    }
    if (DEBUG_MU) Slog.v(TAG_MU,
            "isSingleton(" + componentProcessName + ", " + aInfo + ", " + className + ", 0x"
            + Integer.toHexString(flags) + ") = " + result);
    return result;
}

打开DEBUG_MU之后查看打印,发现singleUser是按组件来配置的:

08-03 13:45:20.092  3289  4023 V ActivityManager_MU: isSingleton(me.linjw.demo.multiuser, ApplicationInfo{417a0ea me.linjw.demo.multiuser}, me.linjw.demo.multiuser.TestService, 0x0) = false

所以应该在service里面配置:

<service
    android:name=".TestService"
    android:exported="true"
    android:singleUser="true">

实际上如果我一开始看到是英文文档,应该就不会出现这样的误解了:

To identify an app as a singleton, add android:singleUser=”true” to your service, receiver, or provider in the Android manifest.

android:exported被自动关闭

修改完成自检通过,开开心心上传代码原本以为问题已经解决。没想到一天之后另外一个客户的软件报了连接不上我们的Service的问题:

08-03 12:13:22.108  3185  3557 W ActivityManager: Permission Denial: Accessing service me.linjw.demo.multiuser/.TestService from pid=5994, uid=10055 that is not exported from uid 1000
08-03 12:13:22.112  5994  5994 E AndroidRuntime: Caused by: java.lang.SecurityException: Not allowed to bind to service Intent { act=me.linjw.multiuser.service pkg=me.linjw.demo.multiuser }

从日志上来看TestService没有export,但是从AndroidManifest.xml上看android:exported的确设置成true了。而且尝试把android:singleUser改成fasle又能连上。这就意味着android:singleUser陪着会影响到android:exported。

但这里又有个问题,当初我修改完android:singleUser="true"之后是有自检通过的,如果exported为false,那自检为什么能通过?

最终排查发现我们的应用设置了sharedUserId声明为系统进程:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="me.linjw.demo.multiuser"
    android:sharedUserId="android.uid.system">

当初自检的时候的那个应用的sharedUserId也是android.uid.system,所以相当于他们是同一个应用,可以相互访问exported为false的组件。

原因排查清楚了,那要怎么解决呢?还是只能从framework源码开始翻起,先去报错的地方看起,找找为什么exported会被自动改成false:

// frameworks/base/services/core/java/com/android/server/am/ActiveServices.java
private ServiceLookupResult retrieveServiceLocked(Intent service,
          String instanceName, String resolvedType, String callingPackage,
          int callingPid, int callingUid, int userId,
          boolean createIfNeeded, boolean callingFromFg, boolean isBindExternal,
          boolean allowInstant) {
      ServiceRecord r = null;
      ...
      if (mAm.checkComponentPermission(r.permission,
              callingPid, callingUid, r.appInfo.uid, r.exported) != PERMISSION_GRANTED) {
          if (!r.exported) {
              Slog.w(TAG, "Permission Denial: Accessing service " + r.shortInstanceName
                      + " from pid=" + callingPid
                      + ", uid=" + callingUid
                      + " that is not exported from uid " + r.appInfo.uid);
              return new ServiceLookupResult(null, "not exported from uid "
                      + r.appInfo.uid);
          }
          Slog.w(TAG, "Permission Denial: Accessing service " + r.shortInstanceName
                  + " from pid=" + callingPid
                  + ", uid=" + callingUid
                  + " requires " + r.permission);
          return new ServiceLookupResult(null, r.permission);
      }
      ...
}

从这里看r的exported为false导致了这个异常,我们需要在retrieveServiceLocked里面一路追踪r的exported是怎么被singleUser影响的,由于这部分代码比较曲折我也找了很久才找到关键代码。

PackageManagerService扫描应用信息的时候,会判断SCAN_AS_PRIVILEGED这个flag,如果没有设置就会执行markNotActivitiesAsNotExportedIfSingleUser

// frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java
if ((scanFlags & SCAN_AS_PRIVILEGED) == 0) {
    parsedPackage
            .markNotActivitiesAsNotExportedIfSingleUser();
}

markNotActivitiesAsNotExportedIfSingleUser顾名思义,就会在配置了SingleUser的时候去修改exported,实际上它里面除了Activity不修改,其他的三个组件都修改了:

public PackageImpl markNotActivitiesAsNotExportedIfSingleUser() {
    // ignore export request for single user receivers
    int receiversSize = receivers.size();
    for (int index = 0; index < receiversSize; index++) {
        ParsedActivity receiver = receivers.get(index);
        if ((receiver.getFlags() & ActivityInfo.FLAG_SINGLE_USER) != 0) {
            receiver.setExported(false);
        }
    }

    // ignore export request for single user services
    int servicesSize = services.size();
    for (int index = 0; index < servicesSize; index++) {
        ParsedService service = services.get(index);
        if ((service.getFlags() & ActivityInfo.FLAG_SINGLE_USER) != 0) {
            service.setExported(false);
        }
    }

    // ignore export request for single user providers
    int providersSize = providers.size();
    for (int index = 0; index < providersSize; index++) {
        ParsedProvider provider = providers.get(index);
        if ((provider.getFlags() & ActivityInfo.FLAG_SINGLE_USER) != 0) {
            provider.setExported(false);
        }
    }

    return this;
}

那问题就在于我们的应用没有携带SCAN_AS_PRIVILEGED,所以在singleUser为true的时候exported会被改成false。那我们要怎么带上这个flag呢?搜索了下资料发现这个flag代表着特权应用,只要预装到下面目录的就能成为特权应用

/system/framework
/system/priv-app
/vendor/priv-app
/odm/priv-app
/product/priv-app
/system_ext/priv-app

最终将预装路径从/syste/app改到/syste/priv-app解决问题。

全局浮动框在子用户不显示

没想到过了两天又报了另外一个问题,我们通过WindowManager.addView添加的全局浮动框在子用户不显示,在主用户是好的。又踩了一个隐藏的坑。

既然不显示,那么首先考虑是不是addView失败了,于是用dumpsys window看看有没有add成功:

console:/ # dumpsys window | grep me.linjw.demo.multiuser
    mPackageName=me.linjw.demo.multiuser
  Window #0 Window{a03fb0 u0 me.linjw.demo.multiuser}:
    mOwnerUid=1000 showForAllUsers=false package=me.linjw.demo.multiuser appop=SYSTEM_ALERT_WINDOW

从打印上来看是add成功的,但是里面有个showForAllUsers引起了我的注意,大概猜测是addView的时候有个showForAllUsers的flag没有设置,于是在源码里面搜索还真找到了:

// android/view/WindowManager.java
@SystemApi
@RequiresPermission(permission.INTERNAL_SYSTEM_WINDOW)
public static final int SYSTEM_FLAG_SHOW_FOR_ALL_USERS = 0x00000010;

但是它的值和FLAG_NOT_TOUCHABLE重复了:

public static final int FLAG_NOT_TOUCHABLE      = 0x00000010;

于是从搜索了下它,发现需要设置到WindowManager.LayoutParams.privateFlags而不是WindowManager.LayoutParams.flags

可惜的是无论是privateFlags还是SYSTEM_FLAG_SHOW_FOR_ALL_USERS都是系统api,所以只能用反射去设置:

private int SYSTEM_FLAG_SHOW_FOR_ALL_USERS = 0x00000010;

// 多用户下需要设置这个flag才能在其他用户显示
Field privateFlags = null;
try {
    privateFlags = WindowManager.LayoutParams.class.getDeclaredField("privateFlags");
    privateFlags.set(wmParams, SYSTEM_FLAG_SHOW_FOR_ALL_USERS);
} catch (Exception e) {
    Log.e("testtest", "err", e);
}

设置之后的确在子用户下也显示成功了,用dumpsys window查看showForAllUsers也变成了true:

console:/ # dumpsys window | grep me.linjw.demo.multiuser
    mPackageName=me.linjw.demo.multiuser
  Window #0 Window{3655208 u0 me.linjw.demo.multiuser}:
    mOwnerUid=1000 showForAllUsers=true package=me.linjw.demo.multiuser appop=SYSTEM_ALERT_WINDOW

结尾吐槽

这系列问题前前后后差不多一个月才弄完,framework部分源码的源码看得人都晕了,也不知道还会不会有其他意料之外的坑。当个安卓应用开发太难了...

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

推荐阅读更多精彩内容