好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

Android动态权限详解

1. 什么是动态权限

去年底,上级主管部门为加强国内Android应用隐私管理,出台了一系列规定,我们的App也做了相应的修改。主要一条修改为,隐私提示与权限获取顺序。修改测试过程中,发觉部分同学对Android权限相关知识和历史并不了解,就此疫情期间忙里偷闲,整理些东西供参阅。

首先,从一张图开始此文。

时间回到2013年,苹果公司发布IOS7系统。其中一项令开发者头疼的修改点:隐私中增加相册、录音等权限,App如需使用相应权限,需要申请并由用户同意(IOS7以前,可以直接访问相册)。

针对此点,很多App在首次启动时一通弹窗,申请各式各样的权限。后来苹果为改善用户体验,在App Store审核时要求App必须在使用前一刻才能申请权限,有效改善了此类问题。比如一款直播App,当你启动App时并不需要相机、录音权限,等到你开播时才需要申请这两个权限。这一场景,其实就类似今天要提到的Android动态授权。

??谷歌于2015年推出Android 6.0 Marshmallow,其中一个主要特点便是加入了危险权限管理。这里的“危险权限管理”就带来了“运行时权限”这个新特性。

??“危险权限管理”即在进行一些涉及到用户隐私的操作时,需要获取用户的授权才能使用。如通讯录、短信、相机、定位等隐私权限。获取用户权限,谷歌提倡在应用运行时向其授权,简称,运行时权限(也被叫做“动态权限/动态授权”,后文称“动态权限”)。

??那,在这之前,Android权限管理是怎样的呢?自己杜撰了下国内Android权限管理经历的大概四个阶段。

Android权限管理简史

第一阶段:没遮拦

??早期Android系统(Android 6.0以前),在安装App前,会罗列出App申请的所有权限。如果继续安装,视为用户同意赋予App所需权限。

??例如:sony L36h Android 4.2.2系统。在尝试安装App时,弹窗罗列了App申请的全部权限。只能对所需权限进行查看,无法拒绝授权,可选择取消安装或继续安装。

Sony L36h安装提示

??这种方式,对于开发者极为友好,仅需在Manifest中配置App所需权限即可,代码就可以直接调用了。但是对于用户来说,这种方法存在极大的安全隐患。

??例:获取手机IMEI,需要PHONE_STATE权限;访问网络,需要INIERNET权限。只许在Manifest文件中添加权限即可。

 ?<!--?PHONE_STATE权限-->

?<uses-permission?android:name="android.permission.READ_PHONE_STATE"?/>

?<!--?网络权限-->

?<uses-permission?android:name="android.permission.INTERNET"?/>
 

第二阶段:第三方安全App

??基于以上背景,为解决部分敏感权限被不合理使用,国内部分公司的安全类App,开始监控应用获取手机敏感权限并做出提示。如360手机卫士、腾讯手机管家等产品,当监测到有App尝试使用短信权限、定位等敏感权限,会告知用户,并可以拒绝赋予权限。刚开始,还比较顺利。但随着手机厂商逐渐开始修改ROM,第三方安全App的兼容、性能问题逐步爆发。

??例:HTC T328 Android 4.0.2系统。浏览器扫码功能触发相机调用时,360手机卫士会弹出权限提示窗,用户可以允许或拒绝授权。注意,此窗由第三方安全软件弹出,非系统级弹窗,跟后面要说的两种弹窗有所区别。

360手机卫士 弹窗

第三阶段:手机厂商介入

??随着时间的推移,手机厂商开始发力,纷纷将第三方软件的权限提示功能直接做入ROM。

??例:小米4,基于Android 4.4.4的MIUI7;oppo R9,基于Android 5.1的ColorOS 3.0,浏览器扫码功能触发相机调用时,会弹出权限提示窗。此窗,由ROM也就是系统自己弹出,为系统级权限弹窗。

小米4授权

OPPO R9授权

第四阶段:谷歌升级权限管理

??以上3个时期,App在申请权限时都不需做改变,只需配置Manifest。2015年推出的Android 6.0,加入了危险权限管理。因手机厂商对ROM的修改,部分6.0以上机器并不支持此项特性。

??到了第四阶段,App需要在对权限代码进行修改后,才能正常使用对应权限。简单理解为3步:1、判断是否授权;2、如果未授权需申请权限,根据授权结果继续执行;3、已授权可以继续操作。

??例:Pixel2,原生Android 10;华为mate8,基于Android 8.0的EMUI8。浏览器扫码功能触发相机调用时,会弹出权限提示窗。此窗,由App通知系统弹出,为系统级权限弹窗。

pixel2授权弹窗

华为mate8授权弹窗

??第三阶段与第四阶段,同为系统弹出授权弹窗。二者有什么区别吗?

??首先,从UI上很难判断所弹授权窗为第三阶段或第四阶段。第三阶段弹的系统授权窗大都带有一个倒计时自动拒绝逻辑;第四阶段弹的系统授权窗基本不带自动拒绝逻辑。此点可以粗略判断系统使用的哪种机制。

??其次,从原理上。第三阶段的弹窗,为系统监测到App在使用危险权限行为自动弹出弹窗。第四阶段的弹窗,为App发觉自己没有权限,让系统弹出的弹窗。粗俗的理解,第三阶段,你去朋友家串门,到门口看到大门敞开就直接往里走,触发了红外线报警器,报警器通知了你朋友;第四阶段,你去朋友家串门,到门口发觉门关着,就按下门铃呼叫朋友给你开门。

??目前,国内主要处于第三阶段(涵盖Android4.0~7.1)和第四阶段(涵盖Android6.0~10),此点将在后文用到。

如何应对动态权限特性

方案一:逃避

因为动态权限特性,仅从Android 6.0开始拥有,所以,可以简单粗暴的通过不提升targetSDK(targetSDK<23)的方式,便可不触发此特性。

targetSDK18正常获取IMEI

仅提升targetSDK到26直接运行崩溃

??如果不改变任何代码,直接将targetSDK提升到26,然后运行App,做同样操作时会发生异常甚至崩溃,崩溃举例如下:

无PHONE_STATE获取IMEI崩溃

产生这个崩溃的原因,是在Android 6.0及以上,未获取权限的情况下直接执行了需要权限的操作。那么如何解决呢,就涉及到了真正的修改方案。

方案二:实现动态权限

1. 在使用权限前,检测权限。

??首先,我们需要判断自己是否拥有权限。判断时间点为执行需要权限的对应操作前。如我们在获取IMEI前,需要判断是否拥有PHONE_STATE权限。

??我们可以调用ContextCompat.checkSelfPermission()方法检测授权状态,返回的结果为PackageManager中的两个常量:PERMISSION_GRANTED(已授权)和PERMISSION_DENIED(未授权)。

2. 已授权的情况下,执行你的原有操作。

??当已授权时,就可以执行你原有的操作了。代码如下:

 ?//?检测PHONE_STATE?如果已授权

?if?(ContextCompat.checkSelfPermission(this,Manifest.permission.READ_PHONE_STATE)?==?PackageManager.PERMISSION_GRANTED)?{

?????//做你想做的

?}
 

??那么如果未授权怎么办?

3. 未授权的情况下,申请权限。

??如果App未获得授权,我们就需要向用户申请授权。可以调用requestPermissions()方法来请求授权。代码如下:

 ?//?检测PHONE_STATE?如果未授权

?if?(ContextCompat.checkSelfPermission(this,Manifest.permission.READ_PHONE_STATE)?!=?PackageManager.PERMISSION_GRANTED)?{

?????//申请权限

?????ActivityCompat.requestPermissions(this,?arrayOf(Manifest.permission.READ_PHONE_STATE),?PERMISSIONS_REQUEST_PHONE_STATE)

?}
 

requestPermissions()中的第三个参数是一个int型请求码,方便回调处理。

??调用申请授权方法后,ROM会调起一个系统级弹窗(如下图),这个dialog你无法定制。当用户点击同意后,系统会记录,下次再判断权限时就会返回已授权状态;当App卸载时,记录会被清除。

Android 10授权弹窗

以上,就完成了最朴素版的授权逻辑。整体代码如下:

 ?//?检测PHONE_STATE?如果未授权

?if?(ContextCompat.checkSelfPermission(this,Manifest.permission.READ_PHONE_STATE)?!=?PackageManager.PERMISSION_GRANTED)?{

?????//申请权限

?????ActivityCompat.requestPermissions(this,?arrayOf(Manifest.permission.READ_PHONE_STATE),?PERMISSIONS_REQUEST_PHONE_STATE)

?}else?{

?????//如果已授权做你想做的

?}
 

??那么弹出申请弹窗之后呢?上面说道,弹出的dialog为系统的,我们无法在dialog中加代码,但当弹窗被用户点击后,会触发回调,我们在指定函数中处理回调即可。

4. 重写函数,处理授权弹窗的点击结果。

??直接在Activity或Fragment中重写onRequestPermissionsResult()函数,来处理权限申请结果。requestPermissions()的第三个参数,将在这里被用到。代码如下:

 ?//?处理授权弹窗回调

?override?fun?onRequestPermissionsResult(

?????requestCode:?Int,

?????permissions:?Array<out?String>,

?????grantResults:?IntArray

?)?{

?????when(requestCode){

?????????//?识别刚刚用到的请求码,根据请求码识别不同弹窗回调并处理

?????????PERMISSIONS_REQUEST_PHONE_STATE?->{

?????????????//?如果用户点击“允许”

?????????????if?(grantResults.size?>?0?&&?grantResults[0]?==?PackageManager.PERMISSION_GRANTED){

?????????????????Toast.makeText(this,?"用户允许权限!",Toast.LENGTH_SHORT).show()

?????????????????//?可以继续执行你原来想做的事情了

?

?????????????}else{

?????????????????Toast.makeText(this,?"用户拒绝权限!",Toast.LENGTH_SHORT).show()

?????????????????//?用户拒绝了,你想咋办?

?????????????}

?????????????return;

?????????}

?????????//?可以识别其他请求码并处理

?????}

?}
 

??这样,就完成了授权流程。然后,为提升授权概率,对流程进行优化。

5. 优化授权流程,提高授权几率。

??首先,系统授权窗我们无法定制,但是我们可以在这之前做个引导。在触发系统弹窗之前,弹出一个引导UI,来告知用户将要申请权限,并说明所需权限可带来哪些更好体验。尤其当你申请的权限看似与主要功能并无关系时,比如一个相机App如果需要申请定位权限的时候。

??其次,谷歌官方还提供了个函数shouldShowRequestPermissionRationale(),这个函数可以用来判断,用户上次是否拒绝了且未选则不再询问。可以在授权前,通过此判断,来决定给用户展示首次授权引导或非首次授权引导。

??最后,当用户还是选择了拒绝授权时,如果是必要权限(比如导航软件申请定位权限),我们可以通过处理授权回调,在用户点击拒绝时弹出引导,告知用户功能不可用,并引导用户重新授权或到设置中手动开启权限。

??以上3部分大体流程如下:

引导授权流程

综上,动态权限主要实现步骤

在AndroidManifest明确我们需要哪些权限。(非动态权限也需要此步) 在执行操作前检是否获得对应授权 -> checkSelfPermission()。 如果已授权可以继续操作;如果未授权,判断之前是否授权被拒 -> shouldShowRequestPermissionRationale() (非必须操作)

a) 判断如果没有被拒过,弹出首次授权引导。

b) 判断如果被据过,弹出非首次授权引导。

引导后,申请权限-> requestPermissions()。 处理申请的结果信息-> 回调函数onRequestPermissionsResult()。

??系统一共提供如下4个函数完成动态权限相关操作。

 ????/**

?????*?检查指定的权限是否授权(Context对象调用)

?????*/

????public?static?int?checkSelfPermission?(Context?context,?

????????????????String?permission)

????/**

?????*?在没有授权的情况下,有些时候可能需要提示给用户为什么需要改权限,就通过该函数来实现。

?????*?关于shouldShowRequestPermissionRationale的返回值问题,我们分三种情况

?????*?1.?第一次打开App时?->?false

?????*?2.?上次弹出权限点击了禁止(但没有勾选“下次不在询问”)?->?true

?????* 3. 上次选择禁止并勾选:下次不在询问?-> false

?????*/

????public?static?boolean?shouldShowRequestPermissionRationale?(Activity?activity,?

????????????????String?permission)

????/**

?????*?申请指定的权限(Activity或者Fragment对象调用)

?????*?@param?permissions?权限列表,可以同时申请多个权限

?????*?@param requestCode 该次权限申请对应的requestCode。和 onRequestPermissionsResult()回调函数里面的requestCode对应

?????*/

????public?static?void?requestPermissions?(Activity?activity,?

????????????????String[]?permissions,?

????????????????int?requestCode)

????/**

?????*?处理请求权限的响应,当用户对请求权限的dialog做出响应之后,系统会回调该函数(Activity或者Fragment中重写)

?????*?@param?requestCode?申请权限对应的requestCode

?????*?@param?permissions?权限列表

?????*?@param?grantResults?权限列表对应的返回值,判断permissions里面的每个权限是否申请成功

?????*/

????public?abstract?void?onRequestPermissionsResult?(int?requestCode,?

????????????????String[]?permissions,?

????????????????int[]?grantResults)
 

??写到这里,动态授权实现demo部分均已完成,实际业务场景肯定比以上流程复杂的多。

系统版本兼容

??动态权限为Android 6.0新特性,那低于6.0的系统,该如何写适配代码呢?

??首先想到的,是判断系统版本,针对6.0以上使用动态权限代码,针对低版本,使用老代码。

 ?fun?test(){

?????if?(Build.VERSION.SDK_INT?>=?Build.VERSION_CODES.M)

?????????//?走动态授权

?????????return

?????else

?????????//?走非动态授权

?????????return

?}
 

??其实,可以不必如此麻烦。对于低版本,可以不必单独写代码适配。在不支持动态授权的系统上,Manifest中申请过的权限,checkSelfPermission()方法,会直接返回PERMISSION_GRANTED。

??另外,根据系统版本区分是否支持动态权限,实际是不靠谱的。前文有提到,部分手机厂商在ROM提升到Android 6.0以后,阉割了动态权限特性。目前没有找到准确的API判断当前系统是否支持动态权限。这会带来什么问题呢?

??举一个前不久遇到的实例。App的某一功能,是对别人显示我所在城市(地理位置属于敏感数据),用户反馈关闭系统定位权限后,仍会显示他所在城市。我们需要考虑如何解决用户的问题,所以增加个需求,如果用户关闭了定位权限,则不获取城市。那么问题来了,怎么判断用户是否关闭了定位权限呢?为了避开不支持动态权限的ROM,需求只能退一步,6.0及以上系统做以上逻辑,6.0以下直接不获取地理位置。但是根据测试经验6.0以上系统仍不一定支持动态权限, 7.0及以上系统,绝大部分ROM支持动态权限。所以妥协决定7.0以下全部不获取,7.0以上调checkSelfPermission()判断是否授权,少数不支持动态权限的设备会误认为已授权,需要增加设置项关闭功能。(提升到Android8.0应该是绝对安全的,不过覆盖量太少)

??以下为目前主流国内厂商对动态权限支持情况。(测试方法:在全新安装未进行过授权操作的情况下,使用checkSelfPermission()检查PHONE_STATE、定位、相机权限,返回如果是PERMISSION_GRANTED,则认为不支持动态权限)

? 基于Android6.0的ROM 基于Android7.0的ROM 小米 支持 华为 支持 支持 OPPO 不支持 支持 VIVO 不支持 7.1.1不支持 7.1.2支持部分权限 魅族 支持 锤子 不支持 360 不支持 不支持 中兴 支持

关于权限弹窗

1. 授权弹窗元素

Android 8.0授权弹窗:

权限组icon App名称 申请的权限 允许、拒绝 操作 不再询问选项 多弹窗索引

2. 是否存在不再询问选项

??关于权限弹窗,针对同一个App的同一个权限,有时弹窗不带“拒绝&不再询问”选项,有时带此选项。如下图是谷歌原生系统、小米MIUI系统的两种弹窗对比。这是什么原因呢?Android原生实现:App全新安装后首次申请权限,弹窗不带此选项,即图左效果。当用户拒绝授权后,App下次再申请该权限时,则带此选项,即图右效果。但是,国内部分手机厂商并未遵循此标准,比如华为的Android 10之前的系统、OPPO/VIVO的部分权限,授权弹窗不管是否首次,都带此选项。此为系统行为,App无法决定。

pixel2不再询问

MIUI不再询问

3. 弹窗选项与App设置中权限选项对应关系

??系统的授权弹窗,实际具有3项(允许、拒绝、 拒绝不再询问)。但设置中的App权限选项,有的系统有2项(允许、拒绝),有的有3项(允许、询问、拒绝)。授权弹窗选项与设置中的选项对应关系如下。

以原生Android 10系统为例:

弹窗 允许 -> 设置 允许; 弹窗 拒绝 -> 设置 拒绝; 弹窗 拒绝&不再询问 -> 设置 拒绝 (跟上一项UI一致,本质有区别)。

pixel2弹窗对应设置

以基于Android 9.0的MIUI10.4.8为例:

弹窗 允许 -> 设置 允许; 弹窗 拒绝 -> 设置 询问; 弹窗 拒绝&不再询问 -> 设置 拒绝。

MIUI弹窗对应设置

4. 弹窗选项对四个函数的影响。

??弹窗弹出,用户操作指定选项后,下次再调用四个函数会有如下现象:

UI选项与函数调用结果

权限分类

??Android 6.0系统开始,权限被分为Normal permissions、Signature permissions、Dangerous permissions,其中Signature permissions比较超纲,仅介绍普通权限和危险权限。

??其中普通权限使用方法跟低版本一样,只用在Manifest里申请就可使用。大部分低风险权限,不需要通过确认框这种形式让用户显示的同意。比如访问网络、检查WiFi状态等权限。

??另一种危险权限,也就是本文介绍的对象,它的产生主要为了保护用户隐私,换言之,涉及到用户隐私的一些权限,属于危险权限。例如:相机权限、定位权限、PHONE_STATE(可读取手机IMEI等识别码)权限等。

危险权限和权限组。(不同系统危险权限可能不同)

??关于权限,还有一个权限组的概念。例如,读取外置存储权限(READ_EXTERNAL_STORAGE)和写入外置存储权限(WRITE_EXTERNAL_STORAGE),同属存储权限组(STORAGE)。

??权限组有什么作用呢?在Android O之前,同一权限组的权限,只要用户授权一个,则整个权限组都被授权。

例如:

步骤一:Manifest中加入了READ_EXTERNAL_STORAGE、WRITE_EXTERNAL_STORAGE 步骤二:在程序中只申请了READ_EXTERNAL_STORAGE权限,用户同意后 步骤三:在程序中未申请WRITE_EXTERNAL_STORAGE权限,并尝试直接使用

结果:可以直接使用,同组权限不需再申请。

??而Android O对此进行了修改。同一权限组不同权限,必须都要动态申请权限。但是如果第一个被用户同意了,后面的同组权限再申请时,就不会再弹窗而是被直接同意了。

例如:

步骤一:Manifest中加入了READ_EXTERNAL_STORAGE、WRITE_EXTERNAL_STORAGE

步骤二:在程序中只申请了READ_EXTERNAL_STORAGE权限,用户同意后

步骤三:在程序中未申请WRITE_EXTERNAL_STORAGE权限,并尝试直接使用

结果:崩溃。

修改步骤三:在程序中申请WRITE_EXTERNAL_STORAGE权限

结果:不会弹出授权弹窗,同一权限组直接被自动授权

??But,部分ROM修改了此逻辑。比如,华为9.0以下系统,遵循的是原生系统Android 8.0之前的逻辑。但是,华为9.0以后系统和小米6.0以后系统,都用的比原生系统Android 8.0更严格的逻辑。每个权限都需要单独申请权限,而且会单独弹窗要求用户确认。

例如:

步骤一:Manifest中加入了READ_EXTERNAL_STORAGE、WRITE_EXTERNAL_STORAGE

步骤二:在程序中只申请了READ_EXTERNAL_STORAGE权限,用户同意后

步骤三:在程序中申请WRITE_EXTERNAL_STORAGE权限

结果:会弹出授权弹窗,需要用户再次授权

带来问题:相同权限组不同权限的授权弹窗是一毛一样的。这就导致用户很懵逼,明明刚刚授权过了,为什么又要问我一次。

不同ROM权限组内影响

??所以,部分手机上,你会发觉有些App,先后弹出两个访问文件存储的权限弹窗。那是因为写App的时候,先后申请了READ_EXTERNAL_STORAGE、WRITE_EXTERNAL_STORAGE权限导致。如何解决?

??查看requestPermissions()方法的第二个参数,为一个数组。也就是说,可以传入一个权限列表。

 ????/**

?????*?申请指定的权限(Activity或者Fragment对象调用)

?????*?@param?permissions?权限列表,可以同时申请多个权限

?????*?@param?requestCode 该次权限申请对应的requestCode。和 onRequestPermissionsResult()回调函数里面的requestCode对应

?????*/

????public?static?void?requestPermissions?(Activity?activity,?

????????????????String[]?permissions,?

????????????????int?requestCode)
 

??经测试,如果直接调该方法同时传入READ_EXTERNAL_STORAGE、WRITE_EXTERNAL_STORAGE只会弹出一个授权窗,而且用户同意后可以同时获得两个权限。如果传入不同组权限,则按先后每组弹出一个弹窗。而且,这种单次传入多组权限的情况,弹窗中大都会出现一个m/n的编号,以标识弹到第几个,还剩几个。如下图分别是MIUI10(基于android9)和EMUI10(基于android10)的弹窗样式:

红米Note8Pro连续授权窗

华为Mate20连续授权窗

写在最后

后期的一些权限策略变化,仅列部分户感知较大的。

IOS 8(2014年),定位权限选项分为“使用期间”(新增项)、“始终允许”、“不允许”。(减少App后台定位) IOS 10(2016年),App访问网络需要授权。 Android 8.0(2017年), 安装未知来源应用需要申请权限。(App自升级、三方应用市场、广告App安装其他App需申请权限) 权限组授权问题修复,上文有提及。 Android 10(2019年), 定位权限选项分为“使用期间”(新增项)、“始终允许”、“拒绝”。(减少App后台定位) 部分电话、蓝牙、WLAN的API,需要申请精确位置权限。 无法再获取手机IMEI IOS 13(2019年),定位权限选项分为“使用App时允许”、“允许一次”(新增选项)、“不允许”,去除了“始终允许”。(“允许一次”相当于试用权限或临时权限,重启App后需要重新申请权限) Android 11 预览版(2020年), 分区存储强制执行。Download目录、SD卡目录访问受限。 对位置、麦克风、相机增加一次性权限许可,见IOS 13定位权限(即,如果用户选了一次性许可,重启App后需要重新申请权限)。 自动阻止App重复的权限请求。也就是说如果用户点击2次拒绝授权,那么系统会自动停止询问授权,当然了,用户也可以前往设置中手动调整。

??两大平台,都在多个版本中对用户隐私进行了优化,仅定位权限的优化就多次提及。

??可见,在手机逐渐转化为人体器官之一的今天,IOS和Android两大移动平台对于权限、隐私的管理越发严苛,而且趋同的速度约来越快。估计以后Android App想访问网络也需申请授权。但手机厂商自行定制修改ROM,仍是开发者最头疼的问题。

参考文献:谷歌官方文档: https://developer.android测试数据/training/permissions/requesting.html

查看更多关于Android动态权限详解的详细内容...

  阅读:132次