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 = 1;if(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中传进去的那个)、一次申请的多个权限、对应的授予结果可以这样写:
@Overridepublic 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中说它是检查是否该给用户一个提示,来解释一下为什么我需要这个权限。所以大多数权限相关的博客(包括鸿洋大神、还有)中都这样使用它:
@Overridepublic 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 多动手试试,不要盲从。
有问题,欢迎留言.如果可能,将第一时间为你解答。
更多相关文章
- android实现仿QQ登陆界面的多账号保存
- 移动平台前端开发总结(针对iphone,Android等手机)
- Android(安卓)JNI配置及入门
- Android(安卓)init 启动过程分析
- [置顶] Android(安卓)进程常驻(5)----开机广播的简单守护以及总结
- android利用数据库实现搜索联想功能
- Android应用开发实例篇(2)-----挂接电震动
- Looper中的消息队列处理机制
- TopGeek:移动互联网时代,选择Android还是iOS?