周末参加了东莞和深圳的两场GDG,因为都是在线的,所以没赶时间,只能坐在家里等活动开始。等待期间心血来潮,突然想写一篇原创文章,谈谈自己在编写Android权限请求代码时的一些技术心得。正如这篇文章的标题所暗示的,在Android中请求权限从来都不是一件容易的事。为什么?我认为谷歌在设计运行时权限功能时,充分考虑了用户体验,而没有充分考虑开发者编码体验。之前在公众号的留言区和大家讨论的时候,有小伙伴说:我觉得Android提供的运行时权限API很好用,觉得用起来不麻烦。真的是这样吗?让我们看一个具体的例子。假设我正在开发一个相机功能。相机功能通常需要相机权限和位置权限。也就是说这两个权限是我实现相机功能的前提。在我继续之前,用户必须同意这两个权限。拍个照。那么如何申请这两个权限呢?Android提供的运行时权限API相信大家都不陌生。我们可以很自然地写出如下代码:Manifest.permission.ACCESS_FINE_LOCATION),1)}overridefunonRequestPermissionsResult(requestCode:Int,permissions:Array,grantResults:IntArray){super.onRequestPermissionsResult(requestCode,permissions,grantResults)当(requestCode){1->{varallGranted=truefor(resultinggrantResults){if(result!=PackageManager.PERMISSION_GRANTED){allGranted=false}}if(allGranted){takePicture()}else{Toast.makeText(this,"你拒绝了权限,不能拍照",Toast.LENGTH_SHORT).show()}}}}funtakePicture(){Toast.makeText(this,"开始拍照",Toast.LENGTH_SHORT).show()}}可以看到,首先调用了requestPermissions()方法请求相机权限和位置权限,然后是authorization结果在onRequestPermissionsResult()方法中被监控。如果用户同意这两个权限,那么我们就可以拍照了。如果用户拒绝了某个权限,就会弹出一个Toast提示,告诉用户某个权限被拒绝了,这样就不能拍照了。这种写法麻烦吗?这只是仁者见仁智者见智。有的朋友可能会觉得代码行数不多,就不麻烦了。但是个人觉得还是蛮麻烦的。每次需要申请运行时权限,都觉得很累,不想写这么啰嗦的代码。不过我们暂时先不从简单的角度来考虑。从正确性的角度看,这样写对吗?我觉得是有问题的,因为我们只是在权限被拒绝的时候打一个Toast来提醒用户,并没有提供后续的操作方案。如果用户真的拒绝了某项权限,则该应用程序无法继续使用。因此,我们还需要提供一种机制,当权限被用户拒绝时,再次重新请求权限。现在我修改代码如下:String>,grantResults:IntArray){super.onRequestPermissionsResult(requestCode,permissions,grantResults)when(requestCode){1->{varallGranted=truefor(resultinggrantResults){if(result!=PackageManager.PERMISSION_GRANTED){allGranted=false}}if(allGranted){takePicture()}else{AlertDialog.Builder(this).apply{setMessage("相机功能需要您同意相机和位置权限")setCancelable(false)setPositiveButton("OK"){_,_->requestPermissions()}}.show()}}}}funrequestPermissions(){ActivityCompat.requestPermissions(this,arrayOf(Manifest.permission.CAMERA,Manifest.permission.ACCESS_FINE_LOCATION),1)}funtakePicture(){Toast.makeText(this,"开始拍照",Toast.LENGTH_SHORT).show()}}这里我将请求权限的代码提取到requestPermissions()方法,然后在onRequestPermissionsResult()中进行判断,如果用户拒绝某个权限,那么会弹出一个对话框,告诉用户需要摄像头和定位权限,然后在点击事件中调用requestPermissions()的setPositiveButton方法重新请求权限可以看出,现在我们已经更充分地考虑了权限被拒绝的场景。那么现在这种写法,是不是考虑了所有申请运行时权限的场景呢?其实还没有,因为Android的权限系统还提供了一个很“恶心”的机制叫rejectionandnolongerasking。当某个权限被用户拒绝了一次,如果我们下次再去申请这个权限,界面上会有拒绝和不再询问的选项。只要用户选择了这个项目就结束了,我们不能再去请求这个权限了,因为系统会直接返回给我们权限被拒绝了。这种机制对用户非常友好,因为它可以防止一些恶意软件以流氓的方式重复申请权限,从而严重骚扰用户。但是对于开发者来说,这让我们苦不堪言。如果我的某个功能必须依赖这个权限才能运行,现在用户拒绝了,不再询问,怎么办?当然,绝大多数用户都不是傻子。他们当然知道拍照功能需要使用摄像头权限。相信99%的用户都会点击同意授权。但是我们能不能忽略剩下的1%的用户呢?不会,因为你们公司的测试对象是那1%的用户,他们会执行这种愚蠢的操作。也就是说,即使只是针对那1%的用户,对于这种不太可能的操作方式,我们还是要在程序中充分考虑到这个场景。那么,权限被拒绝了,不再请求了,我们应该怎么处理呢?比较常见的处理方式是提醒用户在设置中手动开启权限。如果想做的更好,可以提供自动跳转到当前应用设置界面的功能。让我们改进这个场景,如下:字符串>,grantResults:IntArray){super.onRequestPermissionsResult(requestCode,permissions,grantResults)when(requestCode){1->{valdenied=ArrayList()valdeniedAndNeverAskAgain=ArrayList()grantResults.forEachIndexed{index,result->if(result!=PackageManager.PERMISSION_GRANTED){if(ActivityCompat.shouldShowRequestPermissionRationale(this,permissions[index])){denied.add(permissions[index])}else{deniedAndNeverAskAgain.add(permissions[index])}}}if(denied.isEmpty()&&deniedAndNeverAskAgain.isEmpty()){takePicture()}else{if(denied.isNotEmpty()){AlertDialog.Builder(this).apply{setMessage("拍照功能需要您同意到相册和位置权限")setCancelable(false)setPositiveButton("OK"){_,_->requestPermissions()}}.show()}else{AlertDialog.Builder(this).apply{setMessage("您需要在设置中同意相册和位置权限")setCancelable(false)setPositiveButton("OK"){_,_->valintent=Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)valuri=Uri.fromParts("package",packageName,null)intent.data=uristartActivityForResult(intent,1)}}.show()}}}}}overridefunonActivityResult(requestCode:Int,resultCode:Int,data:Intent?){super.onActivityResult(requestCode,resultCode,data)when(requestCode){1->{requestPermissions()}}}funrequestPermissions(){ActivityCompat.requestPermissions(this,arrayOf(Manifest.permission.CAMERA,Manifest.permission.ACCESS_FINE_LOCATION),1)}funtakePicture(){Toast.makeText(this,"开始拍照",Toast.LENGTH_SHORT).show()}}既然代码变得比较长了,还是带大家整理一下。这里我在onRequestPermissionsResult()方法中添加了denied和deniedAndNeverAskAgain两个集合,用于记录拒绝和拒绝再次询问的权限。如果这两个集合都是空的,说明所有权限都已经授权,可以直接拍照了。而如果denied集合不为空,则表示该权限已被用户拒绝。这个时候我们还是弹出对话框提醒用户,重新申请权限。而如果deniedAndNeverAskAgain不为空,则表示该权限已被用户拒绝,不会再提问。此时只能提示用户在设置中手动开启权限。我们写了一个Intent来执行跳转逻辑,在onActivityResult()方法中,即用户从设置返回时重新申请权限。可以看到,当我们第一次拒绝权限时,会提示用户需要摄像头和位置权限。而如果用户继续无视,选择拒绝,不再询问,那么我们会提醒用户,他必须手动为这些权限开户才能继续运行程序。到此为止,我们已经比较完整的处理了一个“简单”的权限请求流程。然而,这里写的代码真的很简单吗?每次申请一个runtime权限,都要写这么长的一段代码,你真的受得了吗?这也是我写开源库PermissionX的原因,在Android中申请权限从来都不是一件容易的事,但也不应该这么复杂。PermissionX封装了请求运行时权限时应该考虑的复杂逻辑,只把最简单的接口暴露给开发者,让你不需要考虑我上面讨论的那么多场景。而我们使用PermissionX来实现和上面一样的功能,只需要这样写:init(this).permissions(Manifest.permission.CAMERA,Manifest.permission.ACCESS_FINE_LOCATION).onExplainRequestReason{scope,deniedList->valmessage="相机功能需要同意相册和位置权限"valok="OK"scope.showRequestReasonDialog(deniedList,message,ok)}.onForwardToSettings{scope,deniedList->valmessage="您需要进入设置同意相册和位置权限"valok="OK"scope.showForwardToSettingsDialog(deniedList,message,ok)}.request{_,_,_->takePicture()}}funtakePicture(){Toast.makeText(this,"开始拍照",Toast.LENGTH_SHORT).show()}}可以看到代码突然之间,请求权限变得非常精简。我们只需要在permissions()方法中传入需要请求的权限名,在onExplainRequestReason()和onForwardToSettings()回调中填写对话框的提示信息,然后保证所有的请求都已经获取到request()回调权限授权,调用takePicture()方法即可开始拍照。通过这样直观的对比,大家应该能感受到PermissionX带来的便利吧?真的写了上面请求权限的长代码是为了给大家演示一下,不想再往上写了。另外,本文只是演示了PermissionX的易用性,并没有涉及很多具体的使用,比如Android11的兼容性、自定义对话框样式等等。如果您有兴趣,请参阅下面的链接以了解更多用法。Android运行时权限的终极解决方案,使用PermissionXPermissionX现在支持Java!还有一个Android11权限更改解释了PermissionX的重大更新,它支持自定义权限提醒对话框。项目中引入PermissionX也很简单,添加如下依赖即可:dependencies{...implementation'com.permissionx.guolindev:permissionx:1.3.1'}最后附上PermissionX开源库地址:github.com/guolindev/P…