欢迎访问移动开发之家(rcyd.net),关注移动开发教程。移动开发之家  移动开发问答|  每日更新
页面位置 : > > > 内容正文

为什么说在 Android 中请求权限从来都不是一件简单的事情?,

来源: 开发者 投稿于  被查看 12723 次 评论:171

为什么说在 Android 中请求权限从来都不是一件简单的事情?,


周末时间参加了东莞和深圳的两场 GDG,因为都是线上参与,所以时间上并不赶,我只需要坐在家里等活动开始就行了。

等待的时间一时兴起,突然想写一篇原创,聊一聊我自己在写 Android 权限请求代码时的一些技术心得。

正如这篇文章标题所描述的一样,在 Android 中请求权限从来都不是一件简单的事情。为什么?我认为 Google 在设计运行时权限这块功能时,充分考虑了用户的使用体验,但是却没能充分考虑开发者的编码体验。

之前在公众号的留言区和大家讨论时,有朋友说:我觉得 Android 提供的运行时权限 API 很好用呀,并没有觉得哪里使用起来麻烦。

真的是这样吗?我们来看一个具体的例子。

假设我正在开发一个拍照功能,拍照功能通常都需要用到相机权限和定位权限,也就是说,这两个权限是我实现拍照功能的先决条件,一定要用户同意了这两个权限我才能继续进行拍照。

那么怎样去申请这两个权限呢?Android 提供的运行时权限 API 相信每个人都很熟悉了,我们自然而然可以写出如下代码:

  1. class MainActivity : AppCompatActivity() { 
  2.  
  3.     override fun onCreate(savedInstanceState: Bundle?) { 
  4.         super.onCreate(savedInstanceState) 
  5.         setContentView(R.layout.activity_main) 
  6.         ActivityCompat.requestPermissions(this, 
  7.             arrayOf(Manifest.permission.CAMERA, Manifest.permission.ACCESS_FINE_LOCATION), 1) 
  8.     } 
  9.  
  10.     override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) { 
  11.         super.onRequestPermissionsResult(requestCode, permissions, grantResults) 
  12.         when (requestCode) { 
  13.             1 -> { 
  14.                 var allGranted = true 
  15.                 for (result in grantResults) { 
  16.                     if (result != PackageManager.PERMISSION_GRANTED) { 
  17.                         allGranted = false 
  18.                     } 
  19.                 } 
  20.                 if (allGranted) { 
  21.                     takePicture() 
  22.                 } else { 
  23.                     Toast.makeText(this, "您拒绝了某项权限,无法进行拍照", Toast.LENGTH_SHORT).show() 
  24.                 } 
  25.             } 
  26.         } 
  27.     } 
  28.  
  29.     fun takePicture() { 
  30.         Toast.makeText(this, "开始拍照", Toast.LENGTH_SHORT).show() 
  31.     } 
  32.  

可以看到,这里先是通过调用 requestPermissions() 方法请求相机权限和定位权限,然后在 onRequestPermissionsResult() 方法里监听授权的结果。如果用户同意了这两个权限,那么我们就可以去进行拍照了,如果用户拒绝了任意一个权限,那么弹出一个 Toast 提示,告诉用户某项权限被拒绝了,从而无法进行拍照。

这种写法麻烦吗?这个就仁者见仁智者见智了,有些朋友可能觉得这也没多少行代码呀,有什么麻烦的。但我个人认为还是比较麻烦的,每次需要请求运行时权限时,我都会觉得很心累,不想写这么啰嗦的代码。

不过我们暂时不从简易性的角度考虑,从正确性的角度上来讲,这种写法对吗?我认为是有问题的,因为我们在权限被拒绝时只是弹了一个 Toast 来提醒用户,并没有提供后续的操作方案,用户如果真的拒绝了某个权限,应用程序就无法继续使用了。

因此,我们还需要提供一种机制,当权限被用户拒绝时,可以再次重新请求权限。

现在我对代码进行如下修改:

  1. class MainActivity : AppCompatActivity() { 
  2.  
  3.     override fun onCreate(savedInstanceState: Bundle?) { 
  4.         super.onCreate(savedInstanceState) 
  5.         setContentView(R.layout.activity_main) 
  6.         requestPermissions() 
  7.     } 
  8.  
  9.     override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) { 
  10.         super.onRequestPermissionsResult(requestCode, permissions, grantResults) 
  11.         when (requestCode) { 
  12.             1 -> { 
  13.                 var allGranted = true 
  14.                 for (result in grantResults) { 
  15.                     if (result != PackageManager.PERMISSION_GRANTED) { 
  16.                         allGranted = false 
  17.                     } 
  18.                 } 
  19.                 if (allGranted) { 
  20.                     takePicture() 
  21.                 } else { 
  22.                     AlertDialog.Builder(this).apply { 
  23.                         setMessage("拍照功能需要您同意相机和定位权限") 
  24.                         setCancelable(false) 
  25.                         setPositiveButton("确定") { _, _ -> 
  26.                             requestPermissions() 
  27.                         } 
  28.                     }.show() 
  29.                 } 
  30.             } 
  31.         } 
  32.     } 
  33.  
  34.     fun requestPermissions() { 
  35.         ActivityCompat.requestPermissions(this, 
  36.             arrayOf(Manifest.permission.CAMERA, Manifest.permission.ACCESS_FINE_LOCATION), 1) 
  37.     } 
  38.  
  39.     fun takePicture() { 
  40.         Toast.makeText(this, "开始拍照", Toast.LENGTH_SHORT).show() 
  41.     } 
  42.  

这里我将请求权限的代码提取到了一个 requestPermissions() 方法当中,然后在 onRequestPermissionsResult() 里判断,如果用户拒绝了某项权限,那么就弹出一个对话框,告诉用户相机和定位权限是必须的,然后在 setPositiveButton 的点击事件中调用 requestPermissions() 方法重新请求权限。

可以看到,现在我们对权限被拒绝的场景进行了更加充分的考虑。

那么现在这种写法,是不是就将请求运行时权限的各种场景都考虑周全了呢?其实还没有,因为 Android 权限系统还提供了一种非常 “恶心” 的机制,叫拒绝并不再询问。

当某个权限被用户拒绝了一次,下次我们如果再申请这个权限的话,界面上会多出一个拒绝并不再询问的选项。只要用户选择了这一项,那么完了,我们之后都不能再去请求这个权限了,因为系统会直接返回我们权限被拒绝。

这种机制对于用户来说非常友好,因为它可以防止一些恶意软件流氓式地无限重复申请权限,从而严重骚扰用户。但是对于开发者来说,却让我们苦不堪言,如果我的某项功能就是必须依赖于这个权限才能运行,现在用户把它拒绝并不再询问了,我该怎么办?

当然,绝大多数的用户都不是傻 X,当然知道拍照功能需要用到相机权限了,相信 99% 的用户都会点击同意授权。但是我们可以不考虑那剩下 1% 的用户吗?不可以,因为你们公司的测试就是那 1% 的用户,他们会进行这种傻 X 式的操作。

也就是说,即使只为了那 1% 的用户,为了这种不太可能会出现的操作方式,我们在程序中还是得要将这种场景充分考虑进去。

那么,权限被拒绝且不再询问了,我们该如何处理呢?比较通用的处理方式就是提醒用户手动去设置当中打开权限,如果想做得再好一点,可以提供一个自动跳转到当前应用程序设置界面的功能。

下面我们就来针对这种场景进行完善,如下所示:

  1. class MainActivity : AppCompatActivity() { 
  2.  
  3.     override fun onCreate(savedInstanceState: Bundle?) { 
  4.         super.onCreate(savedInstanceState) 
  5.         setContentView(R.layout.activity_main) 
  6.         requestPermissions() 
  7.     } 
  8.  
  9.     override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) { 
  10.         super.onRequestPermissionsResult(requestCode, permissions, grantResults) 
  11.         when (requestCode) { 
  12.             1 -> { 
  13.                 val denied = ArrayList<String>() 
  14.                 val deniedAndNeverAskAgain = ArrayList<String>() 
  15.                 grantResults.forEachIndexed { index, result -> 
  16.                     if (result != PackageManager.PERMISSION_GRANTED) { 
  17.                         if (ActivityCompat.shouldShowRequestPermissionRationale(this, permissions[index])) { 
  18.                             denied.add(permissions[index]) 
  19.                         } else { 
  20.                             deniedAndNeverAskAgain.add(permissions[index]) 
  21.                         } 
  22.                     } 
  23.                 } 
  24.                 if (denied.isEmpty() && deniedAndNeverAskAgain.isEmpty()) { 
  25.                     takePicture() 
  26.                 } else { 
  27.                     if (denied.isNotEmpty()) { 
  28.                         AlertDialog.Builder(this).apply { 
  29.                             setMessage("拍照功能需要您同意相册和定位权限") 
  30.                             setCancelable(false) 
  31.                             setPositiveButton("确定") { _, _ -> 
  32.                                 requestPermissions() 
  33.                             } 
  34.                         }.show() 
  35.                     } else { 
  36.                         AlertDialog.Builder(this).apply { 
  37.                             setMessage("您需要去设置当中同意相册和定位权限") 
  38.                             setCancelable(false) 
  39.                             setPositiveButton("确定") { _, _ -> 
  40.                                 val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) 
  41.                                 val uri = Uri.fromParts("package", packageName, null) 
  42.                                 intent.data = uri 
  43.                                 startActivityForResult(intent, 1) 
  44.                             } 
  45.                         }.show() 
  46.                     } 
  47.                 } 
  48.             } 
  49.         } 
  50.     } 
  51.  
  52.     override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { 
  53.         super.onActivityResult(requestCode, resultCode, data) 
  54.         when (requestCode) { 
  55.             1 -> { 
  56.                 requestPermissions() 
  57.             } 
  58.         } 
  59.     } 
  60.  
  61.     fun requestPermissions() { 
  62.         ActivityCompat.requestPermissions(this, 
  63.             arrayOf(Manifest.permission.CAMERA, Manifest.permission.ACCESS_FINE_LOCATION), 1) 
  64.     } 
  65.  
  66.     fun takePicture() { 
  67.         Toast.makeText(this, "开始拍照", Toast.LENGTH_SHORT).show() 
  68.     } 
  69.  

现在代码已经变得比较长了,我还是带着大家来梳理一下。

这里我在 onRequestPermissionsResult() 方法中增加了 denied 和 deniedAndNeverAskAgain 两个集合,分别用于记录拒绝和拒绝并不再询问的权限。如果这两个集合都为空,那么说明所有权限都被授权了,这时就可以直接进行拍照了。

而如果 denied 集合不为空,则说明有权限被用户拒绝了,这时候我们还是弹出一个对话框来提醒用户,并重新申请权限。而如果 deniedAndNeverAskAgain 不为空,说明有权限被用户拒绝且不再询问,这时就只能提示用户去设置当中手动打开权限,我们编写了一个 Intent 来执行跳转逻辑,并在 onActivityResult() 方法,也就是用户从设置回来的时候重新申请权限。

可以看到,当我们第一次拒绝权限的时候,会提醒用户,相机和定位权限是必须的。而如果用户继续置之不理,选择拒绝并不再询问,那么我们将提醒用户,他必须手动开户这些权限才能继续运行程序。

到现在为止,我们才算是把一个 “简单” 的权限请求流程用比较完善的方式处理完毕。然而代码写到这里真的还算是简单吗?每次申请运行时权限,都要写这么长长的一段代码,你真的受得了吗?

这也就是我编写 PermissionX 这个开源库的原因,在 Android 中请求权限从来都不是一件简单的事情,但它不应该如此复杂。

PermissionX 将请求运行时权限时那些应该考虑的复杂逻辑都封装到了内部,只暴露最简单的接口给开发者,从而让大家不需要考虑上面我所讨论的那么多场景。

而我们使用 PermissionX 来实现和上述一模一样的功能,只需要这样写就可以了:

  1. class MainActivity : AppCompatActivity() { 
  2.  
  3.     override fun onCreate(savedInstanceState: Bundle?) { 
  4.         super.onCreate(savedInstanceState) 
  5.         setContentView(R.layout.activity_main) 
  6.         PermissionX.init(this) 
  7.             .permissions(Manifest.permission.CAMERA, Manifest.permission.ACCESS_FINE_LOCATION) 
  8.             .onExplainRequestReason { scope, deniedList -> 
  9.                 val message = "拍照功能需要您同意相册和定位权限" 
  10.                 val ok = "确定" 
  11.                 scope.showRequestReasonDialog(deniedList, message, ok) 
  12.             } 
  13.             .onForwardToSettings { scope, deniedList -> 
  14.                 val message = "您需要去设置当中同意相册和定位权限" 
  15.                 val ok = "确定" 
  16.                 scope.showForwardToSettingsDialog(deniedList, message, ok) 
  17.             } 
  18.             .request { _, _, _ -> 
  19.                 takePicture() 
  20.             } 
  21.     } 
  22.  
  23.     fun takePicture() { 
  24.         Toast.makeText(this, "开始拍照", Toast.LENGTH_SHORT).show() 
  25.     } 
  26.  

可以看到,请求权限的代码一下子变得极其精简。

我们只需要在 permissions() 方法中传入要请求的权限名,在 onExplainRequestReason() 和 onForwardToSettings() 回调中填写对话框上的提示信息,然后在 request() 回调中即可保证已经得到了所有请求权限的授权,调用 takePicture() 方法开始拍照即可。

通过这样的直观对比大家应该能感受到 PermissionX 所带来的便利了吧?上面那段长长的请求权限的代码我真的是为了给大家演示才写的,而我再也不想写第二遍了。

另外,本篇文章主要只是演示了一下 PermissionX 的易用性,并不涉及其中具体的诸多用法,如 Android 11 兼容性,自定义对话框样式等等。如果大家感兴趣的话,更多用法请参考下面的链接。

  • Android 运行时权限终极方案,用 PermissionX 吧
  • PermissionX 现在支持 Java 了!还有 Android 11 权限变更讲解
  • PermissionX 重磅更新,支持自定义权限提醒对话框

在项目中引入 PermissionX 也非常简单,只需要添加如下的依赖即可:

  1. dependencies { 
  2.     ... 
  3.     implementation 'com.permissionx.guolindev:permissionx:1.3.1' 

最后附上 PermissionX 开源库地址:github.com/guolindev/P…

用户评论