Android Marshmallow 的权限模型

叁叁肆2018-10-18 14:45

此文已由作者王宇飞授权网易云社区发布。

欢迎访问网易云社区,了解更多网易技术产品运营经验。



在 Android 6.0 之前,用户安装一个 APK 之前必须要允许这个应用请求的所有权限,否则应用就无法安装。这个用户体验是很不好的,如果用户只是不希望应用使用某个特殊权限(比如拍照),同时又想正常使用应用的其他功能,这在 6.0 之前是很难实现的。部分第三方 ROM 或者安全软件可能会提供一些方法,这里就不做扩展。在 Android Marshmallow 中 Google 引入了新的权限模型,允许用户像使用 iOS 一般“选择性”地允许或禁止某些权限。

首先需要升级应用的 targetSdkVersion 到 23+。

1 APIs

1.1 检查是否拥有权限

在需要请求权限的地方,比如 “进入扫一扫界面” 之前,我们需要检查用户是否授权了使用相机的权限。 在 API 23,Context 引入了一个名为 int checkSelfPermission (String permission) 的方法用于检测权限授权情况。返回值通常有两种:

  • PackageManager.PERMISSION_GRANTED  对应 int 值0,表示已经拥有此权限

  • PackageManager.PERMISSION_DENIED  对应 int 值 -1,表示尚未拥有此权限

传入的参数为权限对应的字符串,定义在 Manifest.permission 中,例如 照相权限 对应 Manifest.permission.CAMERA 。需要注意的是, 请使用 android.Manifest 下的权限字符串,而非本应用自己的 com.netease.xxx.Manifest ,否则得到的是自定义权限(这里当然是不支持的)。

前面有说到, checkSelfPermission 方法是 API 23 才加入的,所以如果应用的 minSdkVersion 小于 23 是无法使用此方法的。当然,我们可以手动判定系统版本然后区别处理(如, Build.VERSION.SDK_INT >= 23), 但 Google 在 support library 中为我们提供了更方便更完善的处理方案:
ContextCompat.checkSelfPermission(Context context, String permission)  
这个方法会自动处理掉系统版本号等问题,如果小于 23,直接返回 PERMISSION_GRANTED,反之才会进行真正的权限判断。

1.2 请求权限

向用户请求权限就比较简单了,使用如下方法:
ActivityCompat.requestPermissions(Context context, String[] premissions, int requestCode) 
其中 permissions 可以包含一个或者多个权限,不同的权限数量在 请求对话框 的显示上存在细微的差异,如下图:

请求对话框的样式由系统控制,应用开发者是无法自定义的。
requestCode 唯一标识此次请求,主要用于之后对于权限请求结果回调的处理。需要注意的是,requestCode 存在长度限制,过长的 requestCode 会导致如下异常:
Can only use lower 8 bits for requestCode

1.3 解释权限

有的时候用户对于请求的权限由于不理解或者习惯选择了拒绝,系统提供了相关的方法来判断是否应该给用户一个解释:
ActivityCompat.shouldShowRequestPermissionRationale(Context context, String permission) 
这个方法在用户 上一次拒绝了此权限的请求 后调用时返回 true,此外,如果用户选择了 “不再询问” 并拒绝 后调用会返回 false,其他情况都返回 false。这种设计既保证了能够在恰当的时候给用户一个解释又避免了对用户的骚扰。这个解释的形式开发者可以自主选择,对话框或者 Toast 或者其他形式,系统没有强制要求。需要注意的是,在解释完之后最好再次调用 requestPermissions 来让用户批准请求。

1.4 处理结果回调

权限的请求是异步的,所以处理权限请求需要相应的回调方法:
onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) 
这个方法定义于 v4 支持包的 FragmentActivity 以及 Fragment 中(需要 support library 23+),所以相关的子类例如 AppCompactActivity 也可以使用。
参数中, requestCode 即为请求权限时的唯一标识;permissions 为请求的权限;grantResults 为请求的结果,与上一个参数一一对应,值只有 PackageManager.PERMISSION_GRANTED 与 PackageManager.PERMISSION_DENIED 两种。
对于多个权限的请求(1.2 右边那个图),只有当所有的权限都请求完之后才会回调结果。
如果用户勾选了 “不再询问”并拒绝,该如何区分 仅仅拒绝 与 永久拒绝 呢?1.3 中有提到 shouldShowRequestPermissionRationale 在用户选择了 “不再询问” 并拒绝 后调用会返回 false。因此,可以在回调中的 PERMISSION_DENIED 分之获取 shouldShowRequestPermissionRationale 的返回值,true 为仅仅拒绝,false 为勾选了 “不再询问”。

2 通用的请求流程

下图以请求单一权限为例简单地描述了一个通用的请求权限的过程:

相关代码:

...
if (ContextCompat.checkSelfPermission(ActivityTest.this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
    if (ActivityCompat.shouldShowRequestPermissionRationale(ActivityTest.this, Manifest.permission.CAMERA)) {
            Toast.makeText(ActivityTest.this, "这个权限是用来...的, 请选择允许", Toast.LENGTH_SHORT).show();
            ActivityCompat.requestPermissions(ActivityTest.this, new String[]{Manifest.permission.CAMERA}, PERMISSION_CODE_QR_CODE_CAMERA);
     } else {
            ActivityCompat.requestPermissions(ActivityTest.this, new String[]{Manifest.permission.CAMERA}, PERMISSION_CODE_QR_CODE_CAMERA);
     }
 } else {
      doQRCodeScan();
}
...
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
    switch (requestCode) {
        case PERMISSION_CODE_QR_CODE_CAMERA:
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                doQRCodeScan();                break;
            } else if (grantResults[0] == PackageManager.PERMISSION_DENIED) {
                if (!ActivityCompat.shouldShowRequestPermissionRationale(ActivityTest.this, Manifest.permission.CAMERA)) {
                    Toast.makeText(ActivityTest.this, "请到系统设置中打开..后再使用此功能", Toast.LENGTH_SHORT).show();
                }
            }
    }
 }
...

3 普通权限与危险权限

不是所有的权限都会弹框请求, Android 定义了普通权限与危险权限,只有危险权限才会弹出请求,普通权限只需在 Manifest 中声明即可,当然  危险权限也须在 Manifest 中声明 。
例如访问网络、振动、获取网络状态等都属于普通权限,危险权限并不多,常见的主要有如下这些:
CAMERA、READ_CONTACTS、ACCESS_FINE_LOCATION、RECORD_AUDIO、READ_PHONE_STATE、WRITE_EXTERNAL_STORAGE
更多的可以参考官方文档

4 应用升级

Android M 由于升级了权限模型,所以老版本(targetSdkVersion 小于 23)升级会存在一些特别情况。
Android M 在系统设置中存在可配置的权限开关,所以即便是面向 API 23- 的应用,用户也可以手动关闭一些权限,而这些权限在面向 API 23- 的 APK 安装成功时时自动批准的。
下面分别列举一些新版本 APK 安装后会出现的情况:

  • 【设备为 Android 6.0 以上】+【老版本(22及以下) APK 安装好后没有手动关闭过权限】+ 【安装新的面向 API 23 的版本】
    --> 权限默认自动批准,不会弹出请求框

  • 【设备为 Android 6.0 以上】+【老版本(22及以下) APK 安装好后用户手动关闭了权限】+【安装新的面向 API 23 的版本】
    --> 权限默认拒绝,会弹出请求框

  • 【设备为 Android 6.0以下】
    -->系统 不存在权限管理,所以,所有权限均批准,绝对不会出现请求框

当然,还存在一些涉及到权限组以及新增权限等更复杂的情况,组合很多,在开发过程中可以自己通过尝试得到结果。

5 其他

新的权限模型的引入是 Android  系统在安全方面的一个重大革新,现在很多的主流应用都已经引入了这套机制(支付宝、微信...)。如果你希望将 targetApiVersion 升至 23 并利用随之带来的新特性,请务必对权限进行相关处理(除非这个应用压根不面向 Android 6.0+ 用户)。 Android L 以及 Android M 对于传统面向 4.x 甚至更低版本的代码造成的冲击还是不小的,除了权限之外,还有诸如 Notification 通知栏图标、Apache Http Client 等,在开发过程中都需要特别注意。

时间仓促加之水平有限,如果有任何错误欢迎大家批评指正。


网易云免费体验馆,0成本体验20+款云产品! 

更多网易技术、产品、运营经验分享请点击


相关文章:
【推荐】 接口文档神器Swagger(下篇)
【推荐】 Android事件分发机制浅析(1)