Android权限最佳实践和代码实例分析

一、 官方推荐的权限最佳实践

如果没有节制地频繁请求权限很容易使用户反感,如果用户发现app需要大量的敏感权限,很可能会拒绝使用甚至直接卸载。以下几点可以有效地提升用户的使用体验。

1. 考虑使用Intent

在很多情况下,你可以有两种选择实现你的操作,一种是直接app中请求比如摄像头这样的权限,然后调用摄像头APIs去控制摄像头并获取照片。这种方式可以使你对摄像头有全部的控制权,并且可以自定义相关的UI。

然而,如果你不需要完全控制,那么你只需要使用ACTION_IMAGE_CAPTURE intent来请求图片。当你发送这个intent,系统会自动询问用户打开哪个照相app(加入手机上安装不止一个照相app)。用户从选中的照相app中选择照片后,你的app就会从onActivityResult()方法中得到需要的照片。

类似的,如果你需要打电话,访问用户的通讯录等等,你也可以发送相应的intent,或者直接请求相应的权限。以下是两种方式的优缺点:

如果自己申请权限:

  • 你将可以完全控制想要的权限,但是同时也增加了复杂度,例如你要设计相应的UI界面
  • 一旦用户同意了你的权限申请,不管是在使用时还是安装阶段(根据用户手机的系统版本),你将可以一直使用该权限。但是一旦用户拒绝了你的权限申请(或者随后撤回了权限),你的app将无法使用相关的权限和功能

如果使用Intent:

  • 首先你不需要自己设计UI界面,拥有该权限的app会提供UI,那么同时也意味着你将不能控制用户的使用体验,用户将会被一个你甚至完全不知道的app所影响。
  • 如果用户有不止一个相关的app,系统将会提示用户做出选择。如果用户不勾选默认的操作,那么每次调用该权限的时候都会弹出提示框。

2. 不要同时申请大量的权限

如果用户使用的Android 6.0(API为23)及以上版本,用户需要在使用app时选择是否允许使用某权限。如果你一次性向用户申请大量的权限,用户会很反感甚至直接退出app。所以,你应该在只有用到某权限时才询问用户。

在有些情况下,你可能需要不止一个权限。你应该在启动app时请求权限,例如,你要做一个照片相关的app,你需要申请摄像头权限。当用户第一次打开app时,他们不会惊讶app询问使用摄像头的权限。同时app也需要分享照片给通讯录中好友,你不应该在app首次启动时申请READ_CONTACTS权限,而是当用户分享照片再去申请。

3. 解释为什么你需要权限

当你调用requestPermissions()方法时,系统会自动弹出权限对话框展示相应的权限描述,但是不会显示申请的原因。这会在某种程度上给用户造成困惑,所以在调用requestPermissions()前解释一下申请的原因会比较合适。

例如,一个图片应用想要地址服务以给图片标出地理标签,一个普通用户可能不明白为什么照片需要地理信息,甚至困惑app为什么要申请地址权限。因此,在调用requestPermissions()前告诉用户申请的原因就显得很有必要了。

至于如何在代码中实现显示申请原因下面的代码分析中会提及。

4. 测试权限

从Android 6.0(API为23)开始,用户可以在任意时刻同意和拒绝权限,而不是像之前版本安装时做一次决定。在Android 6.0之前,你可以假定app所有在manifest文件声明的权限是已经通过了。但是在Android 6.0及更高版本,你不能再有这样的假定。

以下这些建议将会在Android 6.0及更高版本帮你识别权限相关的代码问题:

  • 确认你的app当前需要的权限和相关的代码
  • 测试用户可以通过权限保护服务
  • 测试各种同意和拒绝的权限组合,例如,一个照相app可能会在manifest文件中罗列CAMERA, READ_CONTACTS, and ACCESS_FINE_LOCATION权限,你应该测试每个权限同意或拒绝,并且保证app可以有相应的处理。
  • 在命令行中使用adb工具管理权限:

    1. 按组罗列权限和状态:$ adb shell pm list permissions -d -g

    2. 同意和拒绝一个或者多个权限:

$ adb shell pm [grant | revoke] …

例如:

打开READ_CONTACTS权限

adb shell pm grant com.name.app android.permission.READ_CONTACTS

关闭READ_CONTACTS权限

adb shell pm revoke com.name.app android.permission.READ_CONTACTS

二、 权限代码分析

以申请摄像头为例:

首先需要判断是否要申请摄像头权限:

public void showCamera(View view) {
    // 检查摄像头权限是否已经有效
    if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
            != PackageManager.PERMISSION_GRANTED) {
        // 摄像头权限还未得到用户的同意
        requestCameraPermission();
    } else {
        // 摄像头权限以及有效,显示摄像头预览界面
        showCameraPreview();
    }
}

正如代码中注释那样,需要先判断摄像头权限是否有限,如果权限还未有效,需要调用申请权限方法,如果已经有效,则直接显示摄像头预览界面。

打开摄像头预览界面不是本文的重点,所以相关的代码就不关注了。那么接下来看一下requestCameraPermission方法。

 private void requestCameraPermission() {
        // 摄像头权限已经被拒绝
        if (ActivityCompat.shouldShowRequestPermissionRationale(this,
                Manifest.permission.CAMERA)) {
            // 如果用户已经拒绝劝降,那么提供额外的权限说明           
            Snackbar.make(mLayout, R.string.permission_camera_rationale,
                    Snackbar.LENGTH_INDEFINITE)
                    .setAction(R.string.ok, new View.OnClickListener() {
                        @Override
                        public void onClick(View view) {
                            ActivityCompat.requestPermissions(MainActivity.this,
                                    new String[]{Manifest.permission.CAMERA},
                                    REQUEST_CAMERA);
                        }
                    })
                    .show();
        } else {
            // 摄像头还没有被拒绝,直接申请
            ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA},
                    REQUEST_CAMERA);
        }
    }

上述代码先是判断权限是否已经被拒绝,如果被拒绝则通过Snackbar展示权限申请的原因,如果用户同意将会再次申请权限。权限申请的结果是在onRequestPermissionsResult返回的,下面来看这部分代码:

/**
 * Callback received when a permissions request has been completed.
 */
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
        @NonNull int[] grantResults) {
    if (requestCode == REQUEST_CAMERA) {
        // BEGIN_INCLUDE(permission_result)
        // 收到摄像头权限申请的结果
        // 检查摄像头权限是否已经通过
        if (grantResults.length == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            // 摄像头权限已经申请成功,可以展示摄像预览界面了
            Snackbar.make(mLayout, R.string.permision_available_camera,
                    Snackbar.LENGTH_SHORT).show();
            showCameraPreview();
        } else {
            // 摄像头权限申请失败
            Snackbar.make(mLayout, R.string.permissions_not_granted,
                    Snackbar.LENGTH_SHORT).show();
        }
    } else {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }
}

三、第三方SDK:EasyPermissions

Github上有一个比较火且简单易懂的第三方SDK可以简化权限管理,EasyPermissions可以详见github地址。接下来会大致介绍一下其用法,最后会发现其实和系统提供的权限管理很相似,理解起来也很简单。

1. 简单用法

首先需要在build.gradle文件中引入包,操作如下:

dependencies {
  compile 'pub.devrel:easypermissions:0.1.7'
}

还以申请摄像头权限为例:

public class MainActivity extends AppCompatActivity
    implements EasyPermissions.PermissionCallbacks {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);

        // 调用EasyPermissions的onRequestPermissionsResult方法,参数和系统方法保持一致,然后就不要关心具体的权限申请代码了
        EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this);
    }
    @Override
    public void onPermissionsGranted(int requestCode, List<String> list) {
        // 此处表示权限申请已经成功,可以使用该权限完成app的相应的操作了
        // ...
    }
    @Override
    public void onPermissionsDenied(int requestCode, List<String> list) {
        // 此处表示权限申请被用户拒绝了,此处可以通过弹框等方式展示申请该权限的原因,以使用户允许使用该权限
        // ...
    }
}

首先需要在Activity实现EasyPermissions.PermissionCallbacks接口,该接口提供了onPermissionsGranted和onPermissionsDenied两个方法,也即权限申请成功和失败的回调方法,而EasyPermissions.PermissionCallbacks又实现了ActivityCompat.OnRequestPermissionsResultCallback,该接口提供了onRequestPermissionsResult方法,相当于EasyPermissions将系统的权限申请结果回调方法又进行了二次封装,同时提供了权限申请成功和失败的回调方法。

同时触发摄像头权限申请方法如下:

@AfterPermissionGranted(RC_CAMERA_PERM)
public void cameraTask() {
    if (EasyPermissions.hasPermissions(this, Manifest.permission.CAMERA)) {
        // 已经有摄像头权限了,可以使用该权限完成app的相应的操作了
        Toast.makeText(this, "TODO: Camera things", Toast.LENGTH_LONG).show();
    } else {
        // app还没有使用摄像头的权限,调用该方法进行申请,同时给出了相应的说明文案,提高用户同意的可能性
        EasyPermissions.requestPermissions(this, getString(R.string.rationale_camera),
                RC_CAMERA_PERM, Manifest.permission.CAMERA);
    }
}

此处会先调用EasyPermissions.hasPermissions方法判断是否允许使用该权限,如果返回值为ture表示已经申请成功过该权限,则直接使用即可,如果返回值为false表示还没有申请过该权限,那么可以通过EasyPermissions.requestPermissions方法进行申请,同时给出申请原因文案。

通过查看EasyPermissions.hasPermissions的源码,可以看到该方法可以接收多个参数,即可以同时检查多个权限。

AfterPermissionGranted注解是可选的,如果有该注解的话,那么当request值对应的权限申请通过的话会自动调用该方法。

需要特别说明的是,当用户在系统弹出的权限申请对话框中拒绝权限并且勾选不再询问,那么下次系统讲不会自动尝试申请,但是可以在onPermissionsDenied方法中通过弹框的方式解释app需要该权限的理由,如果用户同意的话会再次尝试请求。

2. 源码分析

首先来看EasyPermissions.hasPermissions方法,可以看到先是有一个版本检查,因为Android 6.0之前是不需要在运行时检查权限的,然后就是调用系统提供的ContextCompat.checkSelfPermission方法,所以这个方法好理解的。

public static boolean hasPermissions(Context context, String... perms) {
        // Always return true for SDK < M, let the system deal with the permissions 
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
            Log.w(TAG, "hasPermissions: API version < M, returning true by default");
            return true;
        }
        for (String perm : perms) {
            boolean hasPerm = (ContextCompat.checkSelfPermission(context, perm) ==
                    PackageManager.PERMISSION_GRANTED);
            if (!hasPerm) {
                return false;
            }
        }
        return true;
    }

然后是权限申请requestPermissions方法,首先是通过checkCallingObjectSuitability判断版本号和是否是Acticity或Fragment调用,然后是根据入参拼接权限申请解释文案并通过对话框显示给用户,如果用户点击同意将会调用系统提供的requestPermissions方法,如果用户点击取消,只直接返回权限申请拒绝的回调方法onPermissionsDenied。

 public static void requestPermissions(final Object object, String rationale,
                                          @StringRes int positiveButton,
                                          @StringRes int negativeButton,
                                          final int requestCode, final String... perms) {

    checkCallingObjectSuitability(object);
    final PermissionCallbacks callbacks = (PermissionCallbacks) object;

    boolean shouldShowRationale = false;
    for (String perm : perms) {
        shouldShowRationale =
                shouldShowRationale || shouldShowRequestPermissionRationale(object, perm);
    }

    if (shouldShowRationale) {          // permission has ever denied
        Activity activity = getActivity(object);
        if (null == activity) {
            return;
        }

        AlertDialog dialog = new AlertDialog.Builder(activity)
                .setMessage(rationale)
                .setPositiveButton(positiveButton, new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        executePermissionsRequest(object, perms, requestCode);
                    }
                })
                .setNegativeButton(negativeButton, new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        // act as if the permissions were denied
                        callbacks.onPermissionsDenied(requestCode, Arrays.asList(perms));
                    }
                }).create();
        dialog.show();
    } else {
        executePermissionsRequest(object, perms, requestCode);
    }
}

最后是onRequestPermissionsResult方法,该方法接收系统的权限申请结果方法,并做统一的处理。可以看到也是先通过checkCallingObjectSuitability判断版本号和是否是Acticity或Fragment调用,然后根据不同权限申请结果分别放置到通过和拒绝列表,可以看到如果拒绝列表不为空直接返回申请失败的回调,当成功列表不为空调用之前包含AfterPermissionGranted注解的方法,完成后续的业务动作。

public static void onRequestPermissionsResult(int requestCode, String[] permissions,
                                                  int[] grantResults, Object object) {

    checkCallingObjectSuitability(object);
    PermissionCallbacks callbacks = (PermissionCallbacks) object;

    // Make a collection of granted and denied permissions from the request.
    ArrayList<String> granted = new ArrayList<>();
    ArrayList<String> denied = new ArrayList<>();
    for (int i = 0; i < permissions.length; i++) {
        String perm = permissions[i];
        if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
            granted.add(perm);
        } else {
            denied.add(perm);
        }
    }

    // Report granted permissions, if any.
    if (!granted.isEmpty()) {
        // Notify callbacks
        callbacks.onPermissionsGranted(requestCode, granted);
    }

    // Report denied permissions, if any.
    if (!denied.isEmpty()) {
        callbacks.onPermissionsDenied(requestCode, denied);
    }

    // If 100% successful, call annotated methods
    if (!granted.isEmpty() && denied.isEmpty()) {
        runAnnotatedMethods(object, requestCode);
    }
}

结语

至此算是讲完了Android权限管理最佳实践的所有内容,第一部分是官方的操作建议,第二部分是系统提供的权限管理方案,第三部分是Github上比较成熟的SDK的简单用法和源码分析,希望对各位读者有帮助。


版权声明:本文为duanyy1990原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。