权限请求可以保护设备中的敏感信息,并且仅当应用正常工作需要访问相关信息时才能使用这些信息。利用本文档提供的技巧,您无需请求访问此类信息即可实现相同(或更好的)功能;但本文不会详细讨论权限在 Android 操作系统中的工作方式。
要更笼统地了解 Android 权限,请参阅权限概述。要详细了解如何在代码中使用权限,请参阅请求应用权限。
使用 Android 权限的原则
使用 Android 权限时,我们建议遵循以下原则:
#1:仅使用应用正常工作所需的权限。根据您使用权限的方式,您可以通过其他方式执行所需的操作(系统 intent、标识符、电话的后台处理),而无需依赖于访问敏感信息。
#2:注意库所需的权限。 添加某个库时,您也会继承它的权限要求。您应了解正在添加的库、它们需要的权限以及这些权限的用途。
#3:公开透明。 请求权限时,请清晰说明您要访问的内容以及访问原因,以便用户可以做出明智的决策。在请求权限时(包括安装、运行时或更新权限对话框)列出这些信息。
#4:让系统以显式方式访问。 在访问敏感功能(例如,摄像头或麦克风)时提供连续指示,让用户知道您在收集数据,避免让他们认为您在偷偷地收集数据。
本指南剩下的部分将以开发 Android 应用为背景详细介绍这些规则。
Android 6.0+ 中的权限
Android 6.0 Marshmallow 引入了一个新的权限模式,让应用可以在运行时而不是在安装之前向用户请求权限。支持这个新模式的应用会在应用确实需要相关服务或这些服务保护的数据时请求权限。尽管这不会(不一定会)改变整体应用行为,但会给敏感用户数据的处理方式带来一些变化:
增加了情境上下文:系统会在运行时在应用的上下文中提示用户提供访问相关权限组涵盖的功能所需的权限。用户对请求权限的上下文更加敏感,如果您请求的权限与应用的用途不匹配,则一定要向用户详细解释您为什么请求此权限;您应尽可能在请求时以及后续对话框中(如果用户拒绝请求)解释您的请求。
在授予权限时更加灵活:用户可以在收到请求时以及在设置中拒绝访问各个权限,但是当功能因此而中断时,他们可能仍会感到惊讶。最好监控有多少用户拒绝权限请求(例如,使用 Google Analytics(分析)),以便重构应用以避免依赖该权限,或更好地解释应用需要此权限才能正常工作的原因。还应确保应用可以处理当用户拒绝权限请求或在设置中关闭权限时产生的异常。
增加了事务负担:系统将要求用户单独授予权限组的访问权限,而不是以集合的形式授予。这样一来,最大程度降低请求的权限数量就变得非常重要,因为数量多会增加用户授予权限的负担,并且会增大至少有一个请求被拒绝的概率。
避免请求不必要的权限
每次您请求某个权限时,都是在强迫用户做出决定。应尽量减少提出这些请求的次数。如果用户运行的是 Android 6.0(API 级别 23)或更高版本,则每次用户尝试一些请求权限的新应用功能时,应用都必须中断用户的操作而发起权限请求。如果用户运行的是较低版本的 Android,则在安装应用时必须授予应用每一种权限;如果列表过长或看起来不合适,用户可能会决定根本不安装应用。因此,应尽量减少应用需要的权限数量。
本部分提供了常见用例的替代方法,有助于限制您提出权限请求的次数。由于向用户请求的权限数量和类型会影响下载量(与其他请求较少权限的类似应用相比),因此最好避免为不必要的功能请求权限。
改用 intent
在许多情况下,要让应用执行某项任务,有两种方法供您选择。应用可以要求提供权限来自行执行该任务,也可以使用 intent 让其他应用执行该任务。
例如,假设应用需要使用设备摄像头才能够拍摄照片。应用可以请求 CAMERA 权限,以便允许应用直接访问摄像头。然后,应用将使用摄像头 API 控制摄像头并拍摄照片。此方法使应用能够完全控制拍摄过程,并且您可以将摄像头界面整合到应用中。
不过,如果您很少需要访问用户数据,换句话说,每次您需要访问数据时都向用户显示运行时对话框,这种中断操作并非不可接受,那么您可以使用基于 intent 的请求。Android 提供了一些系统 intent,借助这些 intent,应用无需请求权限,因为在发出基于 intent 的请求时用户会选择与应用共享的内容(如果有)。
例如,您可以使用 intent 操作类型 MediaStore.ACTION_IMAGE_CAPTURE 或 MediaStore.ACTION_VIDEO_CAPTURE 来拍摄图像或视频,而无需直接使用 Camera 对象(或请求权限)。在这种情况下,每次拍摄图像时,系统 intent 都会代表您请求用户提供权限。
同样,如果您需要拨打电话、访问用户的联系人或执行其他操作,您可以通过创建适当的 intent 来完成,也可以直接请求权限并访问相应的对象。每种方法各有优缺点。
如果使用权限:
- 当您执行操作时,您的应用可以完全控制用户体验。不过,如此广泛的控制会增加代码的复杂性,因为您需要设计适当的界面。
- 系统会在运行时或安装时(具体取决于用户的 Android 版本)提示用户授予权限一次。之后,应用即可执行操作,不再需要用户进行其他互动。不过,如果用户未授予权限(或之后撤消权限),则应用将根本无法执行操作。
如果使用 intent:
- 您不必为操作设计界面。处理 intent 的应用将提供界面。
- 用户可以使用他们首选的应用执行任务。例如,用户可以选择用他们喜爱的照片应用拍照。
- 如果用户没有适用于操作的默认应用,则系统会提示用户选择一款应用。如果用户未指定默认处理程序,则他们每次执行此操作时都可能必须处理一个额外的对话框。
不要让用户感到无所适从
如果用户运行的是 Android 6.0(API 级别 23)或更高版本,则用户必须在运行应用时为其授予权限。如果您让用户一次面对大量的权限请求,可能会让用户感到无所适从,导致他们退出您的应用。您应根据需要请求权限。
在某些情况下,一项或多项权限可能对您的应用来说必不可少。在这种情况下,合理的做法是,在应用启动之后立即请求提供所有这些权限。例如,如果您创建的是摄影应用,则该应用将需要访问设备的摄像头。当用户首次启动该应用时,系统会要求他们提供摄像头使用权限,他们不会对此感到惊讶。但是,如果同一应用还具备与用户的联系人分享照片的功能,那么您或许不应在应用首次启动时请求用户提供 READ_CONTACTS 权限,而应等到用户尝试使用“分享”功能之后再请求该权限。
如果应用提供教程,则合理的做法是,在教程结束时请求提供应用的必要权限。
失去音频焦点后暂停媒体
在这种情况下,当用户接电话时,您的应用需要转入后台,只有在通话停止后才会重新获得焦点。
出现此类情况(例如,媒体播放器在通话期间静音或暂停)时,通常采用的方法是使用 PhoneStateListener
或监听 android.intent.action.PHONE_STATE
的广播,以监听通话状态有无变化。这种解决方法的问题是它需要 READ_PHONE_STATE
权限,这将强制用户授予对广泛的敏感数据(如用户的设备和 SIM 硬件 ID 以及来电的电话号码)的访问权限。
您可以通过为应用请求 AudioFocus
,在没有 READ_PHONE_STATE
或 MODIFY_PHONE_STATE
权限的情况下检测用户是否在通话中,这么做不需要显式权限,因为它不访问敏感信息。只需将对音频放入后台所需的代码放入onAudioFocusChange() 事件处理程序,当操作系统转换其音频焦点时,它将自动运行。要详细了解如何执行此操作,请参阅此文档。
确定正在运行实例的设备
在这种情况下,您需要一个唯一标识符来确定您的应用实例正在哪个设备上运行。
应用可能具有设备特定的偏好设置或消息(例如,在云端为用户保存设备特定的播放列表,以便他们在车上和家里可以有不同的播放列表)。常见的解决方案是利用设备标识符(如 Device IMEI
),但这需要 Device ID and call information
权限组(M+ 中为 PHONE
)。它还使用一个无法重置且在所有应用之间共享的标识符。
下面两种方法可以替代这些类型的标识符:
- 使用
com.google.android.gms.iid
InstanceID API。getInstance(Context context).getID()
将为您的应用实例返回一个唯一设备标识符。结果是一个应用实例作用域标识符,在存储有关应用的信息时,该标识符可用作键,如果用户重新安装应用,该标识符会重置。 - 使用
randomUUID()
之类的基本系统函数创建您自己的标识符,其作用域限定为应用的存储空间。
为广告或用户分析创建唯一标识符
在这种情况下,您需要一个唯一标识符来为没有登录您应用的用户构建配置文件(例如,用于广告定位或衡量转化率)。
为广告和用户分析构建配置文件有时需要一个在其他应用之间共享的标识符。此问题的常见解决方案需要利用设备标识符(如 Device IMEI
),这需要 Device ID
and call information
权限组(API 级别 23+ 中为 PHONE
),并且无法由用户重置。无论是上述哪种情况,除了使用不可重置的标识符并请求用户可能认为不寻常的权限外,还会违反 Play 开发者计划政策。
遗憾的是,在这些情况下,使用 com.google.android.gms.iid
InstanceID API 或系统函数创建应用作用域 ID 并不是适当的解决方案,因为可能需要在应用之间共享该 ID。一种替代解决方案是使用通过 getId()
方法从AdvertisingIdClient.Info 类中获取的 Advertising Identifier
。您可以使用 getAdvertisingIdInfo(Context)
方法创建一个 AdvertisingIdClient.Info
对象,并调用 getId()
方法来使用该标识符。请注意,此方法会产生阻塞,因此,您不应从主线程调用它;有关此方法的详细说明,请点击此处。
了解您正在使用的库
有时,您在应用中使用的库需要一些权限。例如,广告和分析库可能需要访问 Location
或 Identity
权限组以实现必需的功能。但从用户的角度来看,权限请求来自于您的应用,而不是库。
就像用户会选择使用较少权限即可实现相同功能的应用一样,开发者也应检查他们的库,并选择不会使用非必要权限的第三方 SDK。例如,设法避免使用需要 Identity
权限组的库,除非可以清楚地向用户解释应用为什么需要这些权限。尤其是对于提供位置功能的库,请确保您不需要请求 FINE_LOCATION
权限,除非您正在使用基于位置的定位功能。
解释为何需要权限
系统在您调用 requestPermissions()
时显示的权限对话框将说明应用需要哪些权限,但不会解释为何需要这些权限。在某些情况下,用户可能会感到困惑。最好在调用 requestPermissions()
之前向用户解释应用需要相应权限的原因。
研究表明,如果用户知道应用需要相应权限的原因,他们会更容易接受权限请求。用户研究表明:
…用户是否愿意为某个移动应用授予给定权限,在很大程度上受此类权限关联用途的影响。例如,用户是否愿意授予访问其位置的权限取决于该权限请求是否为支持应用的核心功能所必需,或者应用是否将与广告网络或分析公司分享此信息。1
卡内基梅隆大学 (CMU) 的 Jason Hong 教授根据他所带领的小组的研究成果得出一个一般结论:
…与只是告诉用户应用正在使用其位置相比,如果用户知道应用为什么使用像他们的位置这样敏感的信息(例如,用于定向广告),那么用户会更容易接受。1
因此,如果您仅使用归入权限组的一小部分 API 调用,明确列出您使用哪些权限以及使用原因会非常有用。例如:
- 如果您仅使用粗略位置,请在应用说明或应用的帮助文档中告知用户。
- 如果您需要访问短信以接收身份验证码,从而防止用户被欺诈,请在应用说明和/或首次访问数据时告知用户。
注意:如果应用面向 Android 8.0(API 级别 26)或更高版本,请不要在验证用户凭据过程中请求 READ_SMS 权限,而应使用
createAppSpecificSmsToken()
生成应用特定的令牌,然后将此令牌传递给可以发送验证短信的其他应用或服务。
在特定条件下,让用户实时了解应用在访问敏感数据也是非常有益的。例如,如果应用在访问摄像头或麦克风,通常最好在应用中的某个位置或在通知托盘中(如果应用正在后台运行)使用通知图标告知用户,这样不会让您看起来像是在偷偷地收集数据。
最后,如果您需要请求权限以便在应用中运行某项功能,但用户不清楚原因,则需要找到一种方法让用户知道您为什么需要最敏感的权限。
测试两种权限模式
自 Android 6.0(API 级别 23)起,用户是在运行时(而不是在安装应用时)授予和撤消应用权限。因此,您必须在多种不同条件下测试应用。在低于 Android 6.0 的版本中,您可以合理地认为,如果应用能运行,它就已经获得在应用清单中声明的全部权限。自 Android 6.0 起,用户可以开启或关闭任何应用的权限,即使面向 API 级别 22 或更低级别的应用也是如此。您应测试以确保您的应用能正常运行,无论它是否具有任何权限。
以下提示可帮助您在运行 API 级别 23 或更高级别的设备上找出与权限有关的代码问题:
- 确定应用的当前权限和相关的代码路径。
- 在各种受权限保护的服务和数据中测试用户流。
- 使用授予或撤消权限的各种组合进行测试。例如,相机应用可能会在其清单中列出 CAMERA、READ_CONTACTS和 ACCESS_FINE_LOCATION。您应在测试应用时逐一开启和关闭这些权限,确保应用可以妥善处理所有权限配置。
- 使用 adb 工具从命令行管理权限:
- 按组列出权限和状态:
$ adb shell pm list permissions -d -g
- 授予或撤消一项或多项权限:
$ adb shell pm [grant|revoke] <permission-name> ...
- 针对使用权限的服务对应用进行分析。
其他资源
- Android 权限的 Material Design 准则
- Android Marshmallow 6.0:请求权限 - 此视频介绍了 Android 运行时权限模式以及请求用户提供权限的正确方法。
- 解释应用为什么需要权限
- 唯一标识符最佳做法
网友评论