Activity源码之Android 6.0权限相关完全解析

[尊重一点点努力,转载请注明出处http://blog.csdn.net/luochoudan/article/details/71076556]

我们都知道Android6.0以前权限的申请非常简单,只需要在mainfest声明所需的权限即可。而6.0以后,Android将权限的管理进一步严格化,它要求用户在使用某些敏感权限时,必须在mainfest中先声明之后再动态申请。在一定程度上约束了应用对权限的索取,保证了用户的数据隐私;当然,反过来也增加了开发者的难度。

想必也都知道,6.0将权限粗分成了两种Normal和Dangerous。所谓的Normal就是一些不需要用户授权就直接拥有的权限,这类权限往往不涉及用户隐私数据,比如访问网络/wifi等,你从来没有见过一个应用弹出对话框让用户授权访问网络的吧;另外一种就是Dangerous,可以想象,这种权限一般都会访问到用户的私人数据,比如联系人、存储卡等;至于具体权限的分类所属,请查看相关文档,这里不再赘述。

每一个开发者在初次接触权限问题时,一定头大过,也一定都会思考我什么时间申请、我需不需要申请、怎么申请、申请结果怎么处理诸如此类问题。在Activity长达7000多行的源码中,关于权限的部分并不是很多。除去两个分发权限结果的私有函数dispatchRequestPermissionsResult和dispatchRequestPermissionsResultToFragment之外,真正与我们直接相关的api其实只有3个,在加上ContextCompat中有一个检查权限的函数,一共是4个。权限问题,归根结底就是这四个函数如何使用的问题,巧合的是,这四个函数也正好解决了前面所说的几个问题。

  • 如何检查权限?
    权限的检查很简单,ContextCompat(它的子类ActivityCompat中也有)中提供了一个静态函数checkSelfPermission(context, permission),它会返回当前Activity是否拥有相关权限,通过比对PackageManager相关常量,就可以做出判定;举个例子,存储卡权限就可以这样写:
if(ContextCompat.checkPermission(this,Manifest.permission.WRITE_EXTERNAL_STORAGE)!=PackageManager.PERMISSION_GRANTED) {
  //没有权限,需要在此处申请了
} else {
  //已经拥有权限
}
  • 什么时间申请?
    这个很好解决,就在你需要这个权限的时候申请。比如说我的应用要支持语音消息,很显然需要麦克风权限。一般的做法是,当你在Activty/Fragment发送语音时(而不是进入Activty/Fragment时),检查是否拥有麦克风权限,没有的话必须申请,否则不能正常使用。

  • 如何申请?
    申请的关键是对requestPermissions(Activity activity,String[] permissions, int requestCode)的使用,三个参数分别是申请的activity、申请的权限数组、请求码(必须>=0)。虽然Activity(Fragment)和ActivityCompat中都提供了这个函数,但前者是final型的,后者却是static型的,应用较多的是ActivityCompat中的这个(两个没什么区别,static型的用着更方便封装)。配合着上 main的权限检查,代码就变成了这样:

int requestCode = 1if(ContextCompat.checkPermission(this,Manifest.permission.WRITE_EXTERNAL_STORAGE)!=PackageManager.PERMISSION_GRANTED) {
    ActivityCompat.requestPermission(this, new String[{Manifest.permission.WRITE_EXTERNAL_STORAGE}, requestCode);
} else {
    //已经拥有权限
}
  • 申请结果怎么处理?
    权限的申请已经发起,但是否授予却掌握在用户手里。应用必须要妥善处理用户可能的两种操作:同意、拒绝。好在强大的Android已经帮我们封装好了,Activity(Fragment)中提供了onRequestPermissionsResult(int requestCode, String[] permissions,int[] grantResults)这样一个回调函数来处理用户的授权结果。3个参数分表表示请求码(就是requestPermission中传进去的那个)、一次申请的多个权限、对应的授予结果可以这样写:
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    if (requestCode == 1) {
       if (grantResults[0] == 
           PackageManager.PERMISSION_GRANTED) {
           Log.d(TAG, "onRequestPermissionsResult: 已经有权限");
       } else {
           Log.d(TAG, "onRequestPermissionsResult: 去申请");  
       }
    }
}

权限的申请及结果处理都有了,但是我们只用了3个函数,还有一个shouldShowRequestPermissionRationale(String permission)
,api中说它是检查是否该给用户一个提示,来解释一下为什么我需要这个权限。所以大多数权限相关的博客(包括鸿洋大神还有)中都这样使用它:

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
     if (requestCode == 0) {
        if (grantResults[0] ==    
            PackageManager.PERMISSION_GRANTED) {
            Log.d(TAG, "onRequestPermissionsResult: 已经有权限");
        } else {     
           if(shouldShowRequestPermissionRationale(permissions[0]){
              //弹框给用户一个解释
           } else {
              //接着申请
              requestPermission(this,
                          new String[{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0);
           }

           //if(shouldShowRequestPermissionRationale(permissions[0])) {
           //   requestPermission(this, 
           //            new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0);  
           //} else {
           //
           //}                                                   
        }
    }
}

上面提供了两种写法:注释掉的和未注释的。遗憾的是,这两种写法都让人崩溃。注释掉的写法,显然是用错了shouldShowRequestPermissionRationale,造成的后果是,如果用户如果不勾选“不再询问”,权限申请的弹框会不断弹出,让人抓狂。没注释的写法也是错的,分析之前先来看两张图。
这里写图片描述
图1 权限弹窗第一次出现的样子
这里写图片描述
图2 第一次拒绝之后权限弹窗再次出现时的样子

测试发现,不管是图1还是图2,只要只点击deny,shouldShowRequestPermissionRationale返回的都是true;如果图2中勾选了“don’t ask again”,并点击了deny时shouldShowRequestPermissionRationale才会返回false。

由上面的结论再来看未注释的代码,发现如果勾选了“don’t ask again”,并且deny了,shouldShowRequestPermissionRationale此时返回的是false。程序会进入else分支重新requestPermission,而因为勾选了“don’t ask again”导致权限弹窗不会再出现,造成的后果是权限直接获取失败,不幸的是shouldShowRequestPermissionRationale一直返回false,又会进行下一次requestPermission。就这样,不断地循环,一直到crash。

事实上,个人觉得这个函数的设计比较鸡肋,它名义上是来判断是否需要给用户一个权限申请的说明。可实际情况上,当我们动态地向用户申请某个权限时,只要用户拒绝,不管shouldShowRequestPermissionRationale返回什么,我们都必须要给出一个提醒(一般是来告诉用户为什么要开这个权限,怎样开);如果不提醒用户,用户很可能不知道自己曾经拒绝了某个权限,进而导致无法正常使用app;大多数应用包括微信等,都是采用的这种策略。那代码该如何展示呢?

public class Permission extends AppCompatActivity {
    private static final String TAG = "MainActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        if (checkPermission(this, Manifest.permission.READ_CONTACTS) != 
            PackageManager.PERMISSION_GRANTED) {
           requestPermission(this, new String[]{Manifest.permission.READ_CONTACTS}, 0);
        }
    }

    private int checkPermission(Context context, String permission) {
        return ContextCompat.checkSelfPermission(context, permission);
    }

    private void requestPermission(Context context, String[] permissions, int 
        requestCode) {
        ActivityCompat.requestPermissions((Activity) context, permissions, 
        requestCode);
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[]
        permissions,@NonNull int[] grantResults) {
        if (requestCode == 0) {
            if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                Log.d(TAG, "onRequestPermissionsResult: 已经有权限");
            } else {
                createDialog();
            }
        }
    }

    private void createDialog() {
        final TextView hint = new TextView(this);
        hint.setText("你可以在设置中打开联系人权限");
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setTitle("权限申
           请").setIcon(android.R.drawable.ic_dialog_info)
               .setView(hint)
               .setNegativeButton("取消", null);
        builder.setPositiveButton("确定", (dialog, which) -> {
            Log.d(TAG, "createDialog: 去设置");
            //一般是打开手机设置
        });
        builder.show();
    }
}

当然,AndroidMainfest中的权限声明也不能少。这里只是给了个示例,完全可以按照自己的喜好来进行相关封装;或者使用注解等方法来让代码变得更简单。最后,强力吐槽一下这个代码编辑器,已经尽力把它做到最美观了……

关于权限处理,这里总结几点:
1 权限+系统api,这两个任意一个都比较棘手;
2 需要多测试。不同手机可能对系统api进行了定制,导致手机的表现可能不尽相同,比如华为和小米。
3 权限弹窗是系统定制的,不可更改,基本每个品牌一个风格。。
4 一些重要权限,比如存储卡,一般使用了存储的应用都要满足“如果没有存储卡权限,应用就退出”的原则(微信就是这么干的),最好放在基类的onResume里申请(因为存储权限使用的地方太多了,不可能在每一处都动态申请,也是为什么“没有存储权限就退出”的原因)。别问为什么在onResume里面,因为用户可能会用的好好的,手痒去把权限给关了。。。
5 通过inetnt的方式打开手机上的某些应用,比如打开通讯录页面,不需要你自己去申请权限;因为你只是打开通讯录并没有读取通讯录数据;真正需要申请权限的是通讯录这个系统应用;
6 多动手试试,不要盲从。

有问题,欢迎留言.如果可能,将第一时间为你解答。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值