Android多用戶下數(shù)據(jù)隔離方案與常見問題解決思路##
1.Android 多用戶概述###
Android從4.2開始支持多用戶模式鱼冀,不同的用戶運(yùn)行在不同的用戶空間糕非,相關(guān)的系統(tǒng)設(shè)置是各不相同而且不同用戶安裝的應(yīng)用和應(yīng)用數(shù)據(jù)也是不一樣的,但是系統(tǒng)中和硬件相關(guān)的設(shè)計(jì)則是共用的站叼。
Android的多用戶實(shí)現(xiàn)主要通過UserManagerService(UMS)對(duì)多用戶進(jìn)行創(chuàng)建娃兽、刪除等操作。
相關(guān)的引入類是
framework/base/services/core/java/com/android/server/pm/UserManagerService.java
framework/base/core/java/android/os/UserManager.java
framework/base/core/java/android/content/pm/UserInfo.java
framework/base/core/java/android/os/UserHandle.java
我們知道尽楔,Linux有原生的用戶管理系統(tǒng)投储。作為一個(gè)多用戶多任務(wù)的分時(shí)操作系統(tǒng),它通過uid和gid來進(jìn)行用戶管理阔馋。
但是Android將Linux的多用戶管理玛荞,移植到了apk管理上。Android給每個(gè)APK進(jìn)程分配一個(gè)單獨(dú)的空間,manifest中的userid就是對(duì)應(yīng)一個(gè)分配的Linux用戶ID呕寝,并且為它創(chuàng)建一個(gè)沙箱勋眯,以防止影響其他應(yīng)用程序(或者其他應(yīng)用程序影響它)。
用戶ID 在應(yīng)用程序安裝到設(shè)備中時(shí)被分配下梢,并且在這個(gè)設(shè)備中保持它的永久性客蹋。也就是說,原生的Linux用戶id孽江,被用在了apk做自己的標(biāo)識(shí)讶坯。所以,需要有管理當(dāng)前所有app的
用戶
的概念竟坛,Android引入了UserHandle
闽巩。
2.UserHandle,userId, uid, gid###
Android對(duì)于不同UserHandler担汤,認(rèn)為數(shù)據(jù)是相互隔離的涎跨。有不同的用戶數(shù)據(jù),權(quán)限等崭歧。每個(gè)UserHandle有它的userId隅很,主用戶是0。默認(rèn)新創(chuàng)建一個(gè)用戶率碾,userId加1叔营。
在 Android 上,一個(gè)用戶 UID 標(biāo)示一個(gè)應(yīng)用程序所宰。應(yīng)用程序在安裝時(shí)被分配用戶 UID绒尊,應(yīng)用程序在設(shè)備上的存續(xù)期間內(nèi),用戶 UID 保持不變仔粥。對(duì)于普通的應(yīng)用程序婴谱,GID即等于UID蟹但。
GIDS 是由框架在 Application 安裝過程中生成,與 Application 申請的具體權(quán)限相關(guān)谭羔。 如果 Application 申請的相應(yīng)的 permission 被 granted 华糖,而且有對(duì)應(yīng)的GIDS, 那么 這個(gè)Application 的 gids 中將 包含這個(gè) gids瘟裸。記住權(quán)限(GIDS)是關(guān)于允許或限制應(yīng)用程序(而不是用戶)訪問設(shè)備資源客叉。
轉(zhuǎn)換關(guān)系為:
uid = userId * 100000 + appId
3.多用戶下數(shù)據(jù)隔離架構(gòu)###
不同用戶下,應(yīng)用的數(shù)據(jù)需要隔離话告。所以Android通過不同的數(shù)據(jù)路徑來達(dá)成數(shù)據(jù)隔離的效果兼搏。
普通應(yīng)用數(shù)據(jù)的保存路徑為/data/user/userId/packageName
以日歷的數(shù)據(jù)庫的數(shù)據(jù)為例,假設(shè)手機(jī)有userId 0
和userId 11
的數(shù)據(jù)超棺,那么它不同用戶下的存儲(chǔ)路徑為:
user11:/data/user/11/com.android.providers.calendar/databases
user 0:/data/user/0/com.android.providers.calendar/databases
常規(guī)的數(shù)據(jù)差異問題向族,可以導(dǎo)出對(duì)應(yīng)路徑下的數(shù)據(jù)進(jìn)行分析呵燕。
4.Settings多用戶下數(shù)據(jù)隔離架構(gòu)與常見問題###
作為Android設(shè)置項(xiàng)的數(shù)據(jù)庫棠绘,android.provider.Settings是被其他應(yīng)用使用得最多,問題也出現(xiàn)最多的數(shù)據(jù)庫再扭。
android.provider.Settings的操作類有三個(gè)氧苍,System,Secure,Global
。他們通過key-value的方式進(jìn)行保存泛范,user0對(duì)應(yīng)的存儲(chǔ)實(shí)體文件為:
/data/system/users/0/settings_system.xml
/data/system/users/0/settings_secure.xml
/data/system/users/0/settings_global.xml
user11對(duì)應(yīng)的存儲(chǔ)實(shí)體文件為:
/data/system/users/11/settings_system.xml
/data/system/users/11/settings_secure.xml
4.1全局變量多用戶隔離問題####
Global作為全局的變量表让虐,多個(gè)用戶只存在一個(gè)文件。也就意味著罢荡,一個(gè)用戶修改會(huì)影響到其他用戶赡突。如果要考慮多用戶下隔離global的開關(guān)項(xiàng),而不能影響原有的業(yè)務(wù)邏輯区赵。只能是考慮在System惭缰,或是Secure下新增key保存當(dāng)前用戶下Global的開關(guān)狀態(tài)。然后在用戶切換的時(shí)候笼才,用System/Secure的狀態(tài)更新Global的狀態(tài)漱受。
用戶切換時(shí)系統(tǒng)有原生的廣播發(fā)出來,需要注意的是骡送,這個(gè)廣播只能是動(dòng)態(tài)注冊的receiver
能夠接收昂羡。所以需要考慮數(shù)據(jù)場景。像設(shè)置就不適宜做用戶切換時(shí)修改數(shù)據(jù)的操作摔踱。只能由其他app進(jìn)行操作虐先。
/**
* Broadcast sent to the system when the user switches. Carries an extra EXTRA_USER_HANDLE that has
* the userHandle of the user to become the current one. This is only sent to
* registered receivers, not manifest receivers. It is sent to all running users.
* You must hold
* {@link android.Manifest.permission#MANAGE_USERS} to receive this broadcast.
* @hide
*/
public static final String ACTION_USER_SWITCHED =
"android.intent.action.USER_SWITCHED";
4.2當(dāng)前用戶數(shù)據(jù)異常問題####
從其他app反饋給設(shè)置這邊最多的,就是讀取到的值不對(duì)派敷,需要設(shè)置進(jìn)行分析蛹批。他們使用方式為getInt(cr,name,def);
/**
* Convenience function for retrieving a single secure settings value
* as an integer. Note that internally setting values are always
* stored as strings; this function converts the string to an integer
* for you. The default value will be returned if the setting is
* not defined or not an integer.
*
* @param cr The ContentResolver to access.
* @param name The name of the setting to retrieve.
* @param def Value to return if the setting is not defined.
*
* @return The setting's current value, or 'def' if it is not defined
* or not a valid integer.
*/
public static int getInt(ContentResolver cr, String name, int def) {
return getIntForUser(cr, name, def, cr.getUserId());
}
/** @hide */
public static int getIntForUser(ContentResolver cr, String name, int def, int userHandle) {
if (LOCATION_MODE.equals(name)) {
// Map from to underlying location provider storage API to location mode
return getLocationModeForUser(cr, userHandle);
}
String v = getStringForUser(cr, name, userHandle);
try {
return v != null ? Integer.parseInt(v) : def;
} catch (NumberFormatException e) {
return def;
}
}
從上面代碼可以看到,這個(gè)方法會(huì)跳轉(zhuǎn)到getIntForUser,此時(shí)的userHandler是通過ContentResolver的getUserId獲取到的。但是常駐進(jìn)程在用戶切換的時(shí)候般眉,上下文是否正常切換了赵?如systemUI,就將處理邏輯都在user0創(chuàng)建的進(jìn)程里處理甸赃。用戶切換時(shí)柿汛,并沒有切換到其他用戶創(chuàng)建的進(jìn)程,context也就不會(huì)切換埠对。那么在其他用戶(如當(dāng)前處于userId 11)時(shí)络断,狀態(tài)欄一直獲取到user0的值,自然導(dǎo)致異常项玛。那么對(duì)應(yīng)的解決方案貌笨,可以使用
getIntForUser(cr, name, def, ActivityManager.getCurrentUser());
ActivityManager.getCurrentUser()
通過IPC獲取當(dāng)前前臺(tái)活躍的userId
。這樣的話可以獲取到前臺(tái)用戶的值襟沮,而不是一直獲取user 0的锥惋。
4.3Android O上非主用戶監(jiān)聽Global失效問題####
在android O上,其他用戶監(jiān)聽Global里面值的修改开伏,發(fā)現(xiàn)ContentObserver的onChange()不會(huì)被回調(diào)膀跌。
SettingsProvider.call()
->insertGlobalSetting(args...)
->mutateGlobalSetting(args...)
在做global值修改的時(shí)候,可以看到insertSettingLocked
第二個(gè)參數(shù)固灵,userId的信息被USER_SYSTEM(也就是user 0)擦除了捅伤。等到修改結(jié)束notifyForSettingsChange
的時(shí)候,無論是哪個(gè)用戶修改了global巫玻,onChange都被傳遞給了user 0丛忆。
mSettingsRegistry.insertSettingLocked(SETTINGS_TYPE_GLOBAL,
UserHandle.USER_SYSTEM, name, value, tag, makeDefault,
callingPkg, forceNotify, CRITICAL_GLOBAL_SETTINGS);
針對(duì)這種情況,需要將修改同步給當(dāng)前用戶仍秤。
//#ifdef VENDOR_EDIT
//xionglong@Apps.Settings, 2019/03/07, Add for Ali Dual System ,to solve Global's ContentObserver not transfer in work mode . . .
if (isGlobalSettingsKey(key)) {
final long token = Binder.clearCallingIdentity();
try {
notifyGlobalSettingChangeForRunningUsers(key, name);
} finally {
Binder.restoreCallingIdentity(token);
}
}
//#endif /* VENDOR_EDIT */
//#ifdef VENDOR_EDIT
//xionglong@Apps.Settings, 2019/03/07, Add for Ali Dual System ,to solve Global's ContentObserver not transfer in work mode . . .
private void notifyGlobalSettingChangeForRunningUsers(int key, String name) {
// Important: No need to update generation for each user as there
// is a singleton generation entry for the global settings which
// is already incremented be the caller.
final Uri uri = getNotificationUriFor(key, name);
final List<UserInfo> users = mUserManager.getUsers(/*excludeDying*/ true);
for (int i = 0; i < users.size(); i++) {
final int userId = users.get(i).id;
if (mUserManager.isUserRunning(UserHandle.of(userId))) {
mHandler.obtainMessage(MyHandler.MSG_NOTIFY_URI_CHANGED,
userId, 0, uri).sendToTarget();
}
}
}
//#endif /* VENDOR_EDIT */
5.多用戶下界面啟動(dòng)異常問題###
在多用戶場景下熄诡,有出現(xiàn)主用戶界面正常切換,但是在其他用戶下缺拉不起對(duì)應(yīng)Activity的問題徒扶。此類問題分兩種:
- 拉起一個(gè)常駐進(jìn)程的界面粮彤。如嘗試打開
com.android.phone
的一個(gè)界面。因?yàn)閜hone進(jìn)程并不會(huì)在其他用戶創(chuàng)建的時(shí)候創(chuàng)建一個(gè)新的進(jìn)程姜骡。所以在其他用戶活躍的時(shí)候导坟,隸屬于user0的phone進(jìn)程的界面無法顯示在上層。相應(yīng)的解決方案為:
(1).在Activity里加上android:showForAllUsers="true";
(2).用startActivityAsUser的方式打開界面圈澈。(注意system app和權(quán)限)
/**
* Version of {@link #startActivity(Intent)} that allows you to specify the
* user the activity will be started for. This is not available to applications
* that are not pre-installed on the system image.
* @param intent The description of the activity to start.
* @param user The UserHandle of the user to start this activity for.
* @throws ActivityNotFoundException
* @hide
*/
@RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL)
public void startActivityAsUser(@RequiresPermission Intent intent, UserHandle user) {
throw new RuntimeException("Not implemented. Must override in a subclass.");
}
需要注意的是惫周,此時(shí)用user0打開界面,界面里的數(shù)據(jù)都是user0的康栈。在其他用戶界面展示user0的界面递递,需要和產(chǎn)品確認(rèn)是否滿足要求喷橙。
- 其他用戶拉起界面失敗。主用戶下無異常登舞,其他用戶異常的話贰逾,需要確認(rèn)是否是拉起正確userId的界面。如上面有描述到的菠秒,systemUI并沒有切換到對(duì)應(yīng)的用戶疙剑,那么它去打開界面是,AMS會(huì)嘗試用user0去打開界面践叠。前臺(tái)活躍的是其他user,所以打開失敗言缤。如關(guān)鍵字
ActivityManager: START u0 {flg=0x10808000...
此時(shí)可以考慮用startActivityAsUser。
startActivityAsUser需要傳入U(xiǎn)serHandle對(duì)象禁灼」苄可以通過
/** @hide */
@SystemApi
public static UserHandle of(@UserIdInt int userId) {
return userId == USER_SYSTEM ? SYSTEM : new UserHandle(userId);
}
獲取到。
6.Context切換問題###
如果有一些場景弄捕,需要用到其他userId的context僻孝,該怎么辦呢?
有一個(gè)方法察藐,可以獲取到對(duì)應(yīng)userId的Context皮璧。
/**
* Similar to {@link #createPackageContext(String, int)}, but with a
* different {@link UserHandle}. For example, {@link #getContentResolver()}
* will open any {@link Uri} as the given user.
*
* @hide
*/
@SystemApi
public Context createPackageContextAsUser(
String packageName, @CreatePackageOptions int flags, UserHandle user)
throws PackageManager.NameNotFoundException {
if (Build.IS_ENG) {
throw new IllegalStateException("createPackageContextAsUser not overridden!");
}
return this;
}
這個(gè)Context沒有Theme舟扎,但是應(yīng)對(duì)一些數(shù)據(jù)場景分飞,如Service,Broadcast,ContentResolver這種場景,再試用不過了睹限。
6.多用戶數(shù)據(jù)問題的常見分析解決思路###
1.確定是否是數(shù)據(jù)不一致導(dǎo)致問題譬猫。從實(shí)際數(shù)據(jù)的存儲(chǔ)位置和代碼里獲取的數(shù)據(jù)進(jìn)行對(duì)比,確認(rèn)數(shù)據(jù)是否存在不一致羡疗。
2.判斷上下文的userId是否發(fā)生變化染服。對(duì)應(yīng)的方法有Context.getUserId,ContentResolver.getUserId,ActivityManagetr.getUserId∵逗蓿活躍的userId以ActivityManagetr.getUserId結(jié)果為準(zhǔn)柳刮。界面切換則搜索關(guān)鍵字ActivityManager: START u...
。
3.確認(rèn)是上下文userId導(dǎo)致的問題痒钝,根據(jù)場景選擇合適的...AsUser
方法秉颗。
參考文檔:
1.多用戶管理UserManager
2.Android逆向之旅---Android中的sharedUserId屬性詳解
3.Android 安全機(jī)制(1)uid 、 gid 與 pid