码个蛋(codeegg)第 610 次推文
作者:FeelsChaotic
原文:https://www.jianshu.com/p/a17c8bed79d9
前言
Android将安全设计贯穿系统架构的各个层面,覆盖系统内核、虚拟机、应用程序框架层以及应用层各个环节,力求在开放的同时,也最大程度地保护用户的数据、应用程序和设备的安全。Android安全模型主要提供以下几种安全机制:
从技术架构角度来看,Android安全模型基于Linux操作系统内核安全性,有基本的用户和文件访问隔离,在这个基础上辅以内存管理技术和进程间通信机制,来适应移动端处理器性能与内存容量的限制。在应用层面,使用显式定义且经用户授权的权限控制机制,系统化地规范并强制各类应用程序的行为准则与权限许可,可以看到权限机制在安全模型中所处的位置。
那么为什么有权限机制?
我们知道 Android 应用程序是沙箱隔离的,每个应用都有一个只有自己具有读写权限的专用数据目录,应用只能访问自己的文件和一些设备上全局可访问的资源。
那如果我需要访问系统服务呢?这就有了 Android 的权限机制。所以根本原因一是沙箱隔离,二是服务支持的需求。
在本文开始之前,先抛出几个问题思考,什么是权限?权限是怎么进行赋予的?怎么判断一个组件是否拥有特定的权限?权限维护在哪?Android 6.0 运行时权限是什么原理?权限赋予后还能再更改吗?
带着问题和结构图,我们一一解开疑惑。
权限的本质
什么是权限?在 Android 中,一个权限,本质上是一个字符串,一个可以表示执行特定操作的能力的字符串。比如说:访问 SD 卡的能力,访问通讯录的能力,启动或访问一个第三方应用中的组件的能力。
使用 pm list permissions -f 命令可以详细查看 Android 所有预定义的权限。我们挑一个权限出来:
+ permission:android.permission.DELETE_PACKAGES
package:android
label:null
description:null
protectionLevel:signature|privileged
可以看到,一个权限的信息包括:定义的包名、标签、描述和保护级别,保护级别,嗯,这个我们需要详细讲讲。因为不是应用声明了权限,就一定会全部被自动赋予的,保护级别决定了包管理器是否应该赋予组件所申请的权限。
权限的级别
normal 级别。权限保护级别的默认值,无须用户确认,只要声明了,就自动默默授权。如:ACCESS_NETWORK_STATE。
dangerous 级别。赋予权限前,会弹出对话框,显式请求权限。如:READ_SMS。因为 Android 需要在安装时赋予权限,所以安装的确认对话框,也会显示列出权限清单。
signature 级别。signature 级别的权限是最严格的权限,只会赋予与声明权限使用相同证书的应用程序。
以系统内置 signature 级别权限为例,Android 系统应用的签名由平台密钥签发,默认情况下源码树里有 4 个不同的密钥文件:platform、shared、media 和 testkey。所有核心平台的包(如:设置、电话、蓝牙)均使用 platform 密钥签发;搜索和通讯录相关的包使用 shared 签发;图库和媒体相关的包使用 media 密钥签发;其他的应用使用 testkey 签发。定义系统内置权限的 framework-res.apk 文件是使用平台密钥签发的,因此任何试图请求 signature 级别内置权限的应用程序,需要使用与框架资源包相同的密钥进行签名。
signatureOrSystem 级别。可以看做是一种折中的级别,可被赋予与声明权限具有相同签名证书密钥的应用程序(同 signature 级别)或者系统镜像的部分应用,也就是说这允许厂商无须共享签名密钥。Android 4.3 之前,安装在 system 分区下的应用会被自动赋予该保护级别的权限,而 Android 4.4 之后,只允许安装在 system/priv-app/ 目录下的应用才能被主动赋予。
权限的管理
那么,系统内置权限,自定义权限,是怎么维护和管理的?
在每个应用安装时,权限就已经赋予了,系统使用包管理服务来管理权限。打开我们系统目录下的 /data/system/packages.xml,可以看到文件包含了所有已定义的权限列表和所有 apk 的包信息,这可以看做是包管理服务维护的一个已安装程序的核心数据库,这个数据库,随着每次应用安装、升级或卸载而进行更新。
主要属性如图:
标签内,定义了目前系统中的所有权限,分为系统内置的(package 属性为 android 的)和 apk 自定义的(package 属性为 apk 的包名)。根元素 含有每个 apk 的核心属性,以一个应用程序的条目为例:
Android 6.0 以下 packages.xml
<package name="com.feelschaotic.demo" codePath="/data/app/com.feelschaotic.demo-1" nativeLibraryPath="/data/app/com.feelschaotic.demo-1/lib" primaryCpuAbi="x86" flags="5783366" ft="16349bcc4d0" it="16349bcc752" ut="16349bcc752" version="8220" userId="10097">
<sigs count="1">
<cert index="7" />
sigs>
<perms>
<item name="android.permission.READ_SMS" />
<item name="android.permission.ACCESS_FINE_LOCATION" />
<item name="android.permission.CHANGE_NETWORK_STATE" />
<item name="android.permission.INTERNET" />
<item name="android.permission.READ_EXTERNAL_STORAGE" />
<item name="android.permission.ACCESS_COARSE_LOCATION" />
<item name="android.permission.READ_PHONE_STATE" />
<item name="android.permission.CALL_PHONE" />
<item name="android.permission.CHANGE_WIFI_STATE" />
<item name="android.permission.ACCESS_NETWORK_STATE" />
<item name="android.permission.CAMERA" />
<item name="android.permission.WRITE_EXTERNAL_STORAGE" />
<item name="android.permission.READ_CONTACTS" />
perms>
<proper-signing-keyset identifier="1686" />
<signing-keyset identifier="1686" />
package>
Android 6.0 及以上 packages.xml
<package name="com.feelschaotic.demo" codePath="/data/app/com.feelschaotic.demo-Gi5ksdF6mUDLakfOugCcwQ==" nativeLibraryPath="/data/app/com.feelschaotic.demo-Gi5ksdF6mUDLakfOugCcwQ==/lib" primaryCpuAbi="x86" publicFlags="945307462" privateFlags="0" ft="16348dc3870" it="16343f1d6aa" ut="16348dc4c4d" version="8220" userId="10102">
<sigs count="1">
<cert index="20" key="..." />
sigs>
<perms>
<item name="android.permission.CHANGE_NETWORK_STATE" granted="true" flags="0" />
<item name="android.permission.INTERNET" granted="true" flags="0" />
<item name="android.permission.CHANGE_WIFI_STATE" granted="true" flags="0" />
<item name="android.permission.ACCESS_NETWORK_STATE" granted="true" flags="0" />
perms>
<proper-signing-keyset identifier="48" />
package>
可以发现,其他信息没有太大差异,但是权限列表中,部分在 Android 6.0 被标记为高危的权限都不在 里了,如:READ_SMS、READ_EXTERNAL_STORAGE、CALL_PHONE 等。这是怎么一回事呢?
Android 6.0 之前,权限都是在安装时自动赋予的,不卸载应用的情况下,不能更改或撤销(实际上有些厂商打开了appOps,使得 6.0 以下也能更改权限,这种特殊情况我们不讨论)。而 Android 6.0 版本对 permission 的管理做了部分改动,针对 dangerous 级别,不再安装的时候赋予权限,而是在运行时动态申请。
我们大胆地推断下,packages.xml 里保留的是不会再变更的权限,运行时权限一定是另外单独地维护。我们在 data/system 目录下来了个全盘搜索,找到了 /data/system/users/0/runtime-permissions.xml。
<pkg name="com.feelschaotic.demo">
<item name="android.permission.ACCESS_FINE_LOCATION" granted="false" flags="1" />
<item name="android.permission.READ_EXTERNAL_STORAGE" granted="true" flags="0" />
<item name="android.permission.ACCESS_COARSE_LOCATION" granted="false" flags="1" />
<item name="android.permission.READ_PHONE_STATE" granted="true" flags="0" />
<item name="android.permission.WRITE_EXTERNAL_STORAGE" granted="true" flags="0" />
...
pkg>
没错了,就是这个文件。里面就记录着运行时权限的授予和拒绝状态。申请时,申请的结果会动态修改 granted 值。管理权限的仍然是 PMS,只不过 Android 6.0 之后新增了 runtime-permissions.xml 数据库。
权限的赋予
我们知道,Android 应用安装时,会被分配一个唯一的 UID,应用启动时,包管理器会设置新建进程的 UID 和 GID 为应用程序的 UID。如果应用已经被赋予了额外的权限,就把这些权限映射成一组 GID,作为补充 GID 分配给进程。低层就可以依赖于进程的 UID、GID 和补充 GID 来决定是否赋予权限了。
那么,上面流程的重点:
权限是如何映射到 OS 层的 UID、GID 上的呢?
映射完是怎么分配给进程的?
低层是怎么判断是否赋予权限的?
我们一个个来看,首先第一个,权限是如何映射的?内置权限到 GID 的映射是定义在 /etc/permission/platform.xml 中。
<permissions>
···
<permission name="android.permission.READ_EXTERNAL_STORAGE" >
<group gid="sdcard_r" />
permission>
<permission name="android.permission.WRITE_EXTERNAL_STORAGE" >
<group gid="sdcard_r" />
<group gid="sdcard_rw" />
permission>
<permission name="android.permission.INTERNET" >
<group gid="inet" />
permission>
···
permissions>
值得注意的是:READ_EXTERNAL_STORAGE 这种运行时权限,在 Android 6.0 之后已经不会映射到 gid 了。动态赋予,动态申请,也就不需要映射了。
<permission name="android.permission.READ_EXTERNAL_STORAGE" />
<permission name="android.permission.WRITE_EXTERNAL_STORAGE" />
包管理器在启动时读取 platform.xml,并维护「权限-GID」对应的列表。当它给安装中的包授权时,会把权限对应的 GID 加入到该应用进程的补充 GID 中。我们举个例子,看下进程的属性:adb shell ps 拿到想要查找的进程的 PID。
USER PID PPID VSZ RSS WCHAN ADDR S NAME
...
u0_a88 2149 1624 1929916 112340 SyS_epoll_wait 0 S com.feelschaotic.demo
接着 adb shell -> cd proc -> cd 2149 (2149 为进程 PID,不固定) -> cat status
我们关注如下两行:
Gid: 10050 10050 10050 10050
Groups: 1006 1015 1028 3002 3003 50050
这里我们便看到了系统进程的权限配置信息,这里的数字具体代表意义,可以在Android
\system\core\include\private\android_filesystem_config.h 里面看到,其部分内容如下:
#define AID_CAMERA 1006 /* camera devices */
#define AID_SDCARD_RW 1015 /* external storage write access */
#define AID_SDCARD_R 1028 /* external storage read access */
···
static const struct android_id_info android_ids[] = {
{ "camera", AID_CAMERA, },
{ "sdcard_r", AID_SDCARD_R, },
{ "sdcard_rw", AID_SDCARD_RW, },
···
};
有没有发现什么?前文所述 sdcard_r 和 android.permission.READ_EXTERNAL_STORAGE 的映射已经定义在 /etc/permission/platform.xml 中了,此处 sdcard_r 映射 AID_SDCARD_R 对应 gid = 1015,这就是权限映射到 gid 的整个关系。总共分为三层:
那么第二个问题,当我们安装应用完成,启动应用,应用的进程是如何启动并被赋予进程属性的呢?
每个应用都会运行在自己的 Dalvik 虚拟机进程中,但是为了提高启动效率,Android 不会为每个应用都新建一个 Dalvik 进程,而是采用 fork 的形式。每个进程都 fork form zygote 进程。那么 fork 比起 new ,效率提高在哪里?
因为 zygote 进程已经预加载了大部分核心和 Java 应用框架库,fork 的子进程会继承 zygote 的进程空间,也就是说,fork 的子进程,可以共享这些预加载的副本(记住,是副本,不是直接共享。fork 时,会 copy-on-write 预加载的内容),减少了重新加载核心库的时间。
当 zygote 收到启动新进程的请求时,它会 fork 自身出一个子进程,并对该子进程做特殊化处理。其源代码位于 dalvik/vm/native/dalvik_system_Zygote.c 中。forkAndSpecializeCommon() 的主要代码如下:
static pid_t forkAndSpecializeCommon(const u4* args, boolisSystemServer) {
...
pid = fork(); //创建新进程
if (pid == 0) //判断是否是root,有没有权限修改自己的进程属性
{
setgroupsIntarray(gids); //设置进程的所有组
setrlimitsFromArray(rlimits);
setgid(gid); //设置进程的组ID
setuid(uid); //设置进程的用户ID
}
...
}
如上所示:这里设置进程的组 ID 和用户 ID,通过 fork 创建的子进程调用 setgroups Intarray 设置该进程所属的组,这样应用程序就拥有了该组的权限,并且可以通过 setgid() 及 setuid() 确定应用程序的 GID 及 UID 值。刚刚开始 fork 时,子进程是以 root 执行的,所以它可以更改自己的进程属性,当属性都设置完成,子进程就以分配的 GID 和 UID 执行,此时,子进程无法再更改自己的进程属性了,因为用户 ID 已经不是 root 即 ! = 0 了,没有修改自己进程属性的权限了。
adb shell ps,看下进程列表:
USER PID PPID VSZ RSS WCHAN ADDR S NAME
root 1 0 10456 2352 SyS_epoll_wait 0 S init
...
root 1620 1 1614572 20312 poll_schedule_timeout 0 S zygote
...
u0_a88 3468 1620 1929916 112340 SyS_epoll_wait 0 S com.feelschaotic.demo
u0_a90 3574 1620 1696012 24176 SyS_epoll_wait 0 S com.demo.pushdemo
u0_a90 3607 1620 1708844 26748 SyS_epoll_wait 0 S com.demo.pushdemo:mult
u0_i0 3741 1879 1557800 11720 SyS_epoll_wait 0 S com.android.chrome:sandboxed
u0_a15 3774 1620 1690604 22056 SyS_epoll_wait 0 S com.google.android.ext.services
system 3805 1620 1687840 19688 SyS_epoll_wait 0 S com.android.keychain
u0_a42 3831 1620 1834408 45660 SyS_epoll_wait 0 S com.android.chrome
...
PID 表示应用的进程 ID,PPID 表示父进程 ID,NAME 表示进程名称(一般情况下 NAME 是应用包名)。可以看到 zygote 进程是由 init 进程启动,所有的应用进程的父进程都是 zygote。USER 表示的是进程的专有用户,这个我们下次再详细讲讲 Android 的用户管理机制。
好了,既然如此,每个应用进程都分配好自己的 GID、UID和补充 GID,系统内核和守护进程就可以用这些标识来决定,是否要赋予进程权限。
权限的检查
1. 系统内核层权限检查
思考一下:如果我们的应用没有在 AndroidManifest.xml 中申请 android.permission.INTERNET 权限就进行网络请求,是不是会报 Permission denied 错误。这个权限,是谁来检查?其他进程来检查吗?明显不是,网络访问权限是由低层来进行控制的。
Android 的访问控制,和 Linux 是一样的,但 Android 增加了个特有的网络访问安全控制机制,也就是说,创建网络套接字的进程,必须属于 inet 组。
如上内核代码,current_has_network(void) 方法检查了进程的所在组。如果不在 inet 组,则直接返回错误。所以为了使我们的应用具有访问网络的能力,我们需要在 AndroidManifest.xml 中申请 INTERNET 权限,经过解析,逐步映射到内核层的组 ID 和用户 ID,最终才能通过内核层的检查。
你可能会有疑问,那非内核层的其他 C/C++ 层,要怎么拿到进程的所在组信息呢?在 PMS 初始化所有包信息之后,就会调用 mSettings.writeLPr()。
这段代码的任务就是将mPackages 中保存的所有包的信息保存到 /data/system/packages.list。所以,packages.list中保存了所有应用申请的权限,C代码只要读这个文件就能判断某个应用是否申请了我们要求的权限。
2. 框架层
因为 Android 6.0 之前组件不能在运行时改变权限,所以系统的权限检查执行过程是静态的。这个情况下,组件的角色和权限的等安全属性会被放置在元数据中,即 AndroidManifest.xml 文件中,而不是组件的本身。系统包管理器会负责记录组件的权限,所以静态权限检查可以从包管理器拿到权限,由运行环境或容器来执行权限检查,这样子可以把业务逻辑和安全决策分离开来,但是灵活性不足。
那 Android 组件可不可以不预先声明权限在 AndroidManifest.xml 中呢?答案是:可以的。Android 的动态权限执行,可以让组件自身执行权限检查,而不是运行环境。
所以接下来我们将深入了解框架层的动态和静态权限执行的原理。
动态权限执行
动态权限执行,最典型的场景,就是 IPC。Android 的核心系统服务统一会注册到服务管理器,任何应用,只要知道服务的注册名称,就可以拿到对应的 Binder引用,就可使用 Binder IPC 机制调用服务。因为 Binder 没有内置的访问控制机制,所以每个系统服务需要自己实现访问控制机制。
系统服务可以直接检查调用者的 UID,通过限定 UID 来控制访问权限,这种方式简单直接,但是对于非固定UID的应用,就比较棘手了。而且大部分服务,并不关心调用者的 UID,只需要检查调用者是否被赋予特定的权限即可。所以这种方式,比较适合只允许以 root(UID:0) 或 system(UID:1000) 运行的进程访问的服务检查。
那换一种方式,服务怎么拿到调用者的权限列表?我们知道,大部分 UID 都是和包一一对应的,除了共享 UID。(共享 UID 后面再详细解释)
使用 Binder.getCallingUid() 和 Binder.getCallingPid() 获取调用者的 UID 和 PID,通过 UID 在包管理器中查询到对应应用的权限。android.content.Context 类中就有 checkPermission(String permission, int pid, int uid) 方法。实质上会调用到 PMS 中的 checkUidPermission(String perName, int uid),如下:
Android 6.0 以下 PMS 中的 checkUidPermission(String perName, int uid)
public int checkUidPermission(String permName, int uid) {
synchronized (mPackages) {
Object obj = mSettings.getUserIdLPr(UserHandle.getAppId(uid));
if (obj != null) {
GrantedPermissions gp = (GrantedPermissions)obj;
if (gp.grantedPermissions.contains(permName)) {
return PackageManager.PERMISSION_GRANTED;
}
} else {
HashSet perms = mSystemPermissions.get(uid);if (perms != null && perms.contains(permName)) {return PackageManager.PERMISSION_GRANTED;
}
}
}return PackageManager.PERMISSION_DENIED;
}
Android 6.0 以下的 checkUidPermission() 方法比较简单,首先,基于入参 uid 获取应用的 appId,拿到权限列表对象(也就是 packages.xml 里的 映射),如果 GrantedPermissions 类中的 grantedPermissions 集合包含目标权限,则检查通过。
如果没有该 GrantedPermissions 对象,则检查目标权限是否可以被自动授予,实际上 mSystemPermissions 就是 platform.xml 文件中的 标签映射缓存,记录了一些系统级应用的 uid 对应的 permission。例:
...
<assign-permission name="android.permission.MODIFY_AUDIO_SETTINGS" uid="cameraserver" />
<assign-permission name="android.permission.ACCESS_SURFACE_FLINGER" uid="cameraserver" />
<assign-permission name="android.permission.UPDATE_DEVICE_STATS" uid="cameraserver" />
<assign-permission name="android.permission.ACCESS_SURFACE_FLINGER" uid="graphics" />
...
Android 6.0 及以上 PMS 中的 checkUidPermission(String perName, int uid)
@Override
public int checkUidPermission(String permName, int uid) {
final int callingUid = Binder.getCallingUid();
final int callingUserId = UserHandle.getUserId(callingUid);
final boolean isCallerInstantApp = getInstantAppPackageName(callingUid) != null;
final boolean isUidInstantApp = getInstantAppPackageName(uid) != null;
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 settingBase = (SettingBase) obj;
final PermissionsState permissionsState = settingBase.getPermissionsState();
if (permissionsState.hasPermission(permName, userId)) {
if (isUidInstantApp) {
BasePermission bp = mSettings.mPermissions.get(permName);
if (bp != null && bp.isInstant()) {
return PackageManager.PERMISSION_GRANTED;
}
} else {
return PackageManager.PERMISSION_GRANTED;
}
}
...
} else {
ArraySet perms = mSystemPermissions.get(uid);if (perms != null) {if (perms.contains(permName)) {return PackageManager.PERMISSION_GRANTED;
}
...
}
}
}return PackageManager.PERMISSION_DENIED;
}
可以注意到,6.0 之后 checkPermission() 方法有所改变。多了从 mSettings.mPermissions 去查询权限列表。关键就在于这个 mSettings 里面保存的这个 SettingBase 对象,它记录了 PermissionsState 也就是权限的授予情况。
// PermissionsState.java
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);
}
所以检查权限的流程是本来就有的,6.0 之后差异仅在于:危险级别权限可以动态修改授权情况,也就是修改 PermissionState 的 mGranted 值,所以每次权限执行,都会查询下 mGranted 值。
静态权限执行
静态权限执行的典型场景,是跨应用组件交互。我们使用隐式 Intent 来表达意图,搜索匹配的组件,如果有多个,弹出选择框,目标组件被选定后,会由 ActivityManagerService 执行权限检查,检查目标组件是否有相应的权限要求,如果有,则把权限检查的工作交给 PMS,去检查调用者有没有被授权这些权限。
接下来的总体的流程和动态执行流程大致相同:Binder.getCallingUid()和Binder.getCallingPid()获取调用者的 UID 和 PID,然后利用 UID 映射包名,再获得相关权限集合。如果权限集合中含有所需权限即启动,否则抛出 SecurityException 异常。静态权限执行这里,我们可以详细了解下,每种组件的权限检查时机和具体顺序是怎么样的。
组件权限执行
思考一下,什么时候会执行对调用者的权限检查?那肯定是在目标组件被调用的时候,去解析目标组件声明的权限,如果有,就执行权限检查。
Activity 和 Service。Activity 显而易见,会在 startActivity() 和 startActivityForResult() 里解析到声明权限的 Activity 时,就执行权限检查。而 Service startService()、stopService() 和 bindService(),这 3 个方法被调用时都会进行权限检查。
广播。我们注意到,发送广播除了常用的 sendBroadcast(Intent intent),还有个 sendBroadcast(Intent intent, String receiverPermission),该方法可以要求广播接受者具备特定的权限,但是,调用 sendBroadcast 是不会进行权限检查的,因为广播是异步的,所以权限检查会在 intent 传递到已注册的广播接受者时进行,如果接收者不具备特定的权限,则不会接收到该广播,也不会收到 SecurityException 异常。
反过来,接收者可以要求广播发送者必须具备的权限,所要求的权限在 manifest 文件中设置 标签的 permission 属性,或者动态注册时指定 registerReceiver(BroadcastReceiver receiver, IntentFilter filter, String broadcastPermission, Handler scheduler),权限检查也是在广播传递时执行。
所以,收发广播可以分开指定权限。值得一提的是,一些系统广播被声明为 protected,并且只能由系统进程发送,比如 PACKAGE_INSTALLED。只能由系统进程发送,这个限制会在内核层进行检查,对调用者的 UID 进行匹配,只能是 SYSTEM_UID、PHONE_UID、SHELL_UID、BLUETOOTH_UID 或 root。如果其他 UID 的进程试图发送系统广播,则会收到 SecurityException 异常。
想了解所有的系统广播,可以打开/system/framework/framework-res.apk 中的 AndroidManifest.xml 标签详细了解。
ContentProvider。ContentProvider 可以为读写分别指定不同的权限,即:调用目标 provider、query() 方法 和 insert()、update()、delete() 都会进行权限检查。
总结
综上所述,Android 的权限的检查会在各个层次上实施。
高层的组件,例如应用和系统服务,通过包管理器查询应用程序被赋予的权限,并决定是否准予访问。
低层的组件,通常不访问包管理器,比如本地守护进程,依赖于进程的 UID、GID 和补充 GID 来决定赋予。
访问系统资源时,如设备文件、UNIX 域套接字和网络套接字,则由内核根据所有者、目标资源的访问权限和访问进程的进程属性或者 packages.list 来进行控制。
共享 UID
最后简单说下共享 UID,填一下前面挖的坑。虽说 Android 会为每一个应用分配唯一的 UID,但如果应用使用相同的密钥签发,就可以使用相同 UID 运行,也就是运行在同一个进程中。
这个特性被系统应用和核心框架服务广泛使用,比如:Google Play 和 Google 定位服务,请求同一进程内的 Google 登录服务,从而达到静默自动同步用户数据的体验。
值得注意的是:Android 不支持将一个已安装的应用,从非共享 UID 切换到共享状态,因为改变了已安装应用的 UID,会导致应用失去对自己文件的访问权限(在一些早期 Android 版本中),所以如果使用共享 UID 必须从一开始就设计好。
近期文章:
10段奇葩的代码注释
Android studio 3.4 稳定版来了
Flutter 入门指北之弹窗和提示(干货)
今日问题:
权限超出了你的想象了吧?
在码个蛋(codeegg)后台回复「社群」即可加入学习群