Android 6.0运行时权限完全解析

1. 引子

Android自诞生以来,其安全性一直被人们所诟病。加之,国内安卓App生态环境恶劣,只要是个应用,恨不得申请所有权限。谷歌或许也意识到这个问题,因此在Marshmallow包含了期待已久的运行时权限管理。

2. M时代之前

在Android 6.0之前,一个应用所需要的权限会在安装的时候列出,如果用户想安装这个应用,只能全盘接受。国内应用一般都是权限毒瘤,这一直以来都是安卓用户的痛中之痛。主要体现在两点:

  • 无法动态授权
  • 无法知道应用什么时候使用了权限

如果M之前的代码不加修改,直接跑在M手机上,可能导致应用崩溃。

3. M时代

Android中的权限分为两大类:普通权限和危险权限,具体可以参考开发文档。在M手机上,对于敏感权限,需要在程序运行时进行动态申请。对于非敏感权限,即Normal Permissions,和M之前的使用相同。

3.1 场景拆分

首先,上效果图:

Runtime gif

场景拆解:

  • 应用首次请求权限,系统弹出授权对话框(系统级,不可定制),包含允许拒绝两个按钮。如果用户点击允许,应用拿到相应权限,如果点击拒绝,则进入下一个场景。
  • 当上次授权用户点击拒绝,应用再次申请权限时,可以定制一个提示信息给用户,引导用户授权。例如效果图中的对话框,当用户点击确定,应用再次发起授权;若点击取消,授权中断。
  • 从应用请求第二次授权开始,授权对话框会多出一个不再弹出的复选框。如果用户选中复选框,应用每次请求授权得到的授权结果都是拒绝

接下来,让我们看看具体的实现细节。

3.2 请求权限

这里以最简单的电话权限举例。首先,需要在AndroidManifest.xml中声明android.permission.CALL_PHONE权限:

1
<uses-permission android:name="android.permission.CALL_PHONE"/>

接下来,在Activity中,使用ContextCompat.checkSelfPermission())检查该权限是否授权,如果没有被授权,使用requestPermissions())进行权限申请。需要为每一个权限指定一个id,当系统返回授权结果时,应用根据id拿到授权结果。

1
2
3
4
5
6
7
8
if (ContextCompat.checkSelfPermission(this, android.Manifest.permission
.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(MainActivity.this, new
String[]{android.Manifest.permission.CALL_PHONE}, REQUEST_PERMISSION_CALL_PHONE);
} else {
Log.d(TAG, "call phone permission granted.");
startActivity(intent);
}

3.3 授权回调

当应用申请权限后,Activity将触发一个回调onRequestPermissionsResult()),告诉应用用户的授权结果。

1
2
3
4
5
6
7
8
9
10
11
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
if (requestCode == REQUEST_PERMISSION_CALL_PHONE) {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
call10086();
} else {
Toast.makeText(this, "Permission denied.", Toast.LENGTH_SHORT).show();
}
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}

3.4 授权提示

当应用首次申请权限时,如果用户点击拒绝,下次再申请权限,Android允许你提示用户,你为什么需要这个权限,好引导用户是否授权。这个功能通过shouldShowRequestPermissionRationale())实现。例如,可以弹出一个对话框提示用户:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest
.permission.CALL_PHONE)) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("说明")
.setMessage("需要使用电话权限,进行电话测试")
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
ActivityCompat.requestPermissions(MainActivity.this, new
String[]{android.Manifest.permission.CALL_PHONE}, REQUEST_PERMISSION_CALL_PHONE);
}
})
.setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
return;
}
})
.create()
.show();
} else {
ActivityCompat.requestPermissions(this, new String[]{android
.Manifest.permission.CALL_PHONE}, REQUEST_PERMISSION_CALL_PHONE);
}

如果用户点击对话框取消按钮,授权结果;如果点击确定按钮,再次申请权限。具体交互可以看效果图。完整代码请戳这里

4 轮子

通过上面讲解,大家也可以看出,虽然动态权限的编码逻辑简单,涉及的Api也就几个。但由于申请权限的位置和授权结果回调分别在两个地方,给人的感觉就一个字;并且,如果Activity规模较大、需要申请权限较多时,代码就会变得混乱。针对这些,前辈们封装了许多动态权限第三方库,这里拿PermissionsDispatcher进行说明。PermissionsDispatcher具有如下优点:

  • 采用注解,代码形式简洁;
  • PermissionsDispatcher采用编译时生成代理类,让Activity/Fragment调用。因此,在效率上和官方写法没有区别。

4.1 配置依赖

在工程build.gradle文件加入依赖:

1
2
3
4
5
buildscript {
dependencies {
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
}

app/build.gradle加入依赖:

1
2
3
4
5
6
apply plugin: 'android-apt'

dependencies {
compile 'com.github.hotchemi:permissionsdispatcher:2.0.7'
apt 'com.github.hotchemi:permissionsdispatcher-processor:2.0.7'
}

4.2 使用

给需要动态权限的Activity或者Fragment加上注解RuntimePermissions

1
2
3
4
@RuntimePermissions
public class MainActivity extends AppCompatActivity {
// ...
}

给涉及到动态权限的方法加上注解@NeedsPermission

1
2
3
4
5
6
7
@RuntimePermissions
public class MainActivity extends AppCompatActivity {
@NeedsPermission(Manifest.permission.CALL_PHONE)
void callPhone() {
// Trigger the calling of a number here
}
}

重新编译工程后,将产生一个以PermissionsDispatcher为后缀的代理类,例如这里是MainActivityPermissionsDispatcher

如果提示找不到MainActivityPermissionsDispatcher类,说明apt代码生成失败,重启Android Studio试试。

代理类生成后,重写权限申请回调,让代理类来处理:

1
2
3
4
5
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
MainActivityPermissionsDispatcher.onRequestPermissionsResult(this, REQUEST_PERMISSION_CALL_PHONE, grantResults);
}

4.3 处理授权结果

应用首次申请权限,用户拒绝,使用@OnPermissionDenied标识的方法将作为回调:

1
2
3
4
@OnPermissionDenied(Manifest.permission.CALL_PHONE)
void showDeniedForCallPhone() {
Toast.makeText(this, "未授权", Toast.LENGTH_SHORT).show();
}

应用首次申请权限被拒绝,再次申请权限时,给出提示信息,使用@OnShowRationale注解标识:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@OnShowRationale(Manifest.permission.CALL_PHONE)
void showRationaleForCallPhone(final PermissionRequest request) {
new AlertDialog.Builder(this)
.setTitle("提示")
.setMessage("需要授权电话权限")
.setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
request.cancel();
}
})
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
request.proceed();
}
})
.create()
.show();
}

应用非首次申请权限时,授权对话框会多出一个复选框不再询问,相应回调方法用@OnNeverAskAgain标识:

1
2
3
4
@OnNeverAskAgain(Manifest.permission.CALL_PHONE)
void showNeverAskForPhoneCall() {
Toast.makeText(this, "不再询问", Toast.LENGTH_SHORT).show();
}

完整代码请戳这里

4.4 原理

PermissionsDispatcher在编译时,会为需要申请运行时权限的每个Activity或者Fragment都生成一个相应的代理类。这个代理类非常简单,这里贴一下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// This file was generated by PermissionsDispatcher. Do not modify!
package com.aaron.androidruntimepermissions;

import android.support.v4.app.ActivityCompat;
import java.lang.Override;
import java.lang.String;
import java.lang.ref.WeakReference;
import permissions.dispatcher.PermissionRequest;
import permissions.dispatcher.PermissionUtils;

final class MainActivityPermissionsDispatcher {
private static final int REQUEST_CALL10086 = 0;

private static final String[] PERMISSION_CALL10086 = new String[] {"android.permission.CALL_PHONE"};

private MainActivityPermissionsDispatcher() {
}

static void call10086WithCheck(MainActivity target) {
if (PermissionUtils.hasSelfPermissions(target, PERMISSION_CALL10086)) {
target.call10086();
} else {
if (PermissionUtils.shouldShowRequestPermissionRationale(target, PERMISSION_CALL10086)) {
target.showRationaleForCallPhone(new Call10086PermissionRequest(target));
} else {
ActivityCompat.requestPermissions(target, PERMISSION_CALL10086, REQUEST_CALL10086);
}
}
}

static void onRequestPermissionsResult(MainActivity target, int requestCode, int[] grantResults) {
switch (requestCode) {
case REQUEST_CALL10086:
if (PermissionUtils.getTargetSdkVersion(target) < 23 && !PermissionUtils.hasSelfPermissions(target, PERMISSION_CALL10086)) {
target.showDeniedForCallPhone();
return;
}
if (PermissionUtils.verifyPermissions(grantResults)) {
target.call10086();
} else {
if (!PermissionUtils.shouldShowRequestPermissionRationale(target, PERMISSION_CALL10086)) {
target.showNeverAskForPhoneCall();
} else {
target.showDeniedForCallPhone();
}
}
break;
default:
break;
}
}

private static final class Call10086PermissionRequest implements PermissionRequest {
private final WeakReference<MainActivity> weakTarget;

private Call10086PermissionRequest(MainActivity target) {
this.weakTarget = new WeakReference<>(target);
}

@Override
public void proceed() {
MainActivity target = weakTarget.get();
if (target == null) return;
ActivityCompat.requestPermissions(target, PERMISSION_CALL10086, REQUEST_CALL10086);
}

@Override
public void cancel() {
MainActivity target = weakTarget.get();
if (target == null) return;
target.showDeniedForCallPhone();
}
}
}

总结

  • 动态授权,对于一些流氓应用是十分有必要的。但是,有些超级流氓,如果你不授权就直接退出应用,也是无奈,比如某宝…
  • 如果Activity规模较大、需要申请权限较多时,代码就会变得混乱。

针对第二点,前辈们封装了不少库,大家可以根据项目需要进行选择。

完整示例代码,请移步Github

参考