Android 6.0 的权限模型

最近做 app 时遇到了一个问题,原本在 Android 5.1 上运行得很好的应用在 Android 6.0 直接提示权限不足或者直接崩溃。这是由于 Google 从 Android Marshmallow 开始修改了系统的权限模型。为了更大地兼容老版本的应用,Google 在设计上做出了很多妥协。目前看来有些机制并不好,可是估计这也是没办法的办法,只有等应用慢慢跟上了然后逐步改善。

这里要马克一下我总结出的一些改变,以便以后写 app 时照搬。


一、编译时目标 SDK 在 22 或更低版本时,新的权限模型不起作用。

如果在开发时,编译时指定的 Target SDK 不是 API 23 (Android 6.0 Marshmallow)或以上,在安装时系统会授予应用所有请求的权限。在运行应用时,系统会自动开启旧版本的兼容模式,保证旧版本的应用不会出现问题。

但是有一个例外,如果用户在设置里面关闭了任意一项权限组里的权限(官方称之为 dangerous permission),那么应用在访问响应 API 时会被系统拒绝(可能直接抛异常、返回空数据集或者常量值)。如果应用没有捕获异常的话,应用就会直接崩溃。因此,用户在收回权限时系统会提示这样操作可能会导致应用无法运行。


二、编译时 SDK 在 23 版本(或以上)时,新的权限模型适用。

此时,开发者必须要遵循 Google 的规范来请求权限。如果像以前那样编码,系统会直接拒绝抛出 SecurityException。

有一个特别蛋疼的地方在于:如果你的应用需要集成一些第三方的垃圾 SDK (比如我最近集成的微信、微博等像翔一样的 API),那么你也需要为他们事先请求权限。因为这些辣鸡 SDK 到现在还没有跟进 Android 系统的更新,而他们需要一些相关权限的 API 时会被系统毫不犹豫地拒绝掉,就像下面这样:

所以,不论是自己使用,还是集成第三方SDK,务必要遵循以下三部曲:检查→请求→访问。

比如需要把图片保存在外部存储中,需要一个 WRITE_EXTERNAL_STORAGE 的权限,在保存之前,需要调用 ContextCompat 的 checkSelfPermission 方法检查一下应用当前是否有这项权限,如果有,那么说明用户之前做过授权,可以很安全地访问相关的 API。

如果该方法没有返回 GRANTED,那么需要调用 ActivityCompat 的 shouldShowRequestPermissionRationale 方法得知一下原因。没有权限的原因无非是:

  • 还没有请求用户授权,此时直接 request 相关的 permission。
  • 用户曾经拒绝过此权限:此时如果照旧 request,系统会不询问用户直接拒绝掉。Google 的做法是在此时像用户解释为何需要这项权限,以便获得用户的同意。

因此代码就像下面这样:

        int permissionCheck = ContextCompat.checkSelfPermission(this, android.Manifest.permission.WRITE_EXTERNAL_STORAGE);

        if (permissionCheck != PackageManager.PERMISSION_GRANTED) {
            if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
                new AlertDialog.Builder(this)
                        .setMessage(R.string.no_permission_storage)
                        .setTitle(R.string.app_name)
                        .setPositiveButton(R.string.ok, null)
                        .show();
            } else {
                ActivityCompat.requestPermissions(this,
                        new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE},
                        PERMISSION_SAVE_REQUEST_CODE);
            }
        } else {
            savePictureWithPermission();
        }

requestPermissions 可以一次性请求多项权限,所以可以传一个 String[] 数组。调用 requestPermissions 方法时系统会弹出一个类似 iOS 系统那样的对话框询问用户是否授予权限(用户无法在这个对话框上显示任何自定义的信息)。

这个对话框不像 AlertDialog 那样有回调方法,用户在操作后系统会调用 Activity 的 onRequestPermissionsResult 方法,因此我们需要覆盖掉这个方法。使用之前传入的 requestCode 判断要执行什么样的操作(因为你可能会在一个 Activity 里面)。

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        if (requestCode == PERMISSION_SAVE_REQUEST_CODE) {
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                savePictureWithPermission();
            } else {
                new AlertDialog.Builder(this)
                        .setMessage(R.string.no_permission_storage)
                        .setTitle(R.string.app_name)
                        .setPositiveButton(R.string.ok, null)
                        .show();
            }

        }
    }

至此,我们可以看到申请权限是如此的曲折,本来一个用户确认→执行的简单操作要分这么多步来进行。就好比把同步的请求写成异步那样要拆好几步,如果以后能够有 .NET 4.5 那样的 async-await 模式就好了,不过按照 Java 的惯例来看似乎是不太可能了。

对于新的权限模型有很多弊端,最主要体现在:只有9个权限组中的权限才使用这种授权的方式(包括电话、短信、存储、位置、传感器等隐私相关的信息),而对于其他权限还是必须在安装时一次性授予,且不能收回。

由此可见 Google 这样做的目的还是保护用户的隐私,而不是像我所希望的那样对付 BAT 360 之类的流氓应用。Google 本身可能意识不到国内这些辣鸡 APP 丛生的问题,就好比我一直在 Google Play 上购买游戏不会知道其他玩家玩盗版时遇到的各种各样的问题。所以,Google 今后收紧 app 权限的可能性并不大,有这方面需求的还是得靠国内各种定制的 UI (虽然相当多的一部分都是负优化),当然最靠谱的还是自己动手用 Privacy 之类的神器对付之。

✏️ 有任何想法?欢迎发邮件告诉老夫:daozhihun@outlook.com