Android系统动态申请权限的机制流程总结

Posted LQS_Android

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android系统动态申请权限的机制流程总结相关的知识,希望对你有一定的参考价值。

android6.0开始,Android系统提供动态申请权限的机制, APP在使用危险权限时,需要用户的授权才可进一步操作。

权限申请方式

Android系统中权限申请的方式有两种,如下图所示:

静态申请

Android6.0以前的系统(API < 23)采用的这种方式,只要用户在AndroidManifest.xml中注册了权限,安装APP后默认就获取了这些权限。这种授权方式安全性极低,如果用户安装后没有关闭相应的权限,用户的私密数据很容易被哪些垃圾APP窃取。为了解决这种问题,国内的各大手机厂商为Android5.0以下的系统,针对某些权限做了一定的限制,即便在Android5.0以下,也需要用户进行手动授权才可使用,这在某种程度上提高了安全性,但也因没有统一的标准,从而出现了各种兼容问题。

动态申请

随着系统的升级,Google也意识到静态申请权限的弊端,所以在Android6.0中,对权限进行了重新梳理,将权限分为普通权限和危险权限:

  • 正常权限:不会给用户隐私带来危险的权限,只要开发者在AndroidManifest.xml中注册了,系统将自动授权。如下:
 <!-- PHONE_STATE权限-->
 <uses-permission android:name="android.permission.READ_PHONE_STATE" />
 <!-- 网络权限-->
 <uses-permission android:name="android.permission.INTERNET" />
  • 危险权限:可以访问用户隐私数据的权限,需要弹出交互界面,必须获取用户的同意才可获得授权;

危险权限汇总

在Android开发中我们应该尽可能使用隔离式申请权限,用户没有授权时屏蔽相应功能即可。现在很多内容性APP,用户进去什么都没看到,就需要各种权限,没有权限还不能使用,这可能会引起用户的反感,当然,也是对用户安全意识的一种冲击,时间久了,用户可能会觉得使用APP就应该授权,从而给一些恶意APP作恶的余地。

动态申请权限的过程

核心API介绍

检查权限是否已获取:

// ContextCompat.java
public static int checkSelfPermission(@NonNull Context context, @NonNull String permission)
 
// PermissionChecker.java
public static int checkSelfPermission(@NonNull Context context,@NonNull String permission)
复制代码

这两个方法都是检查权限是否获取的方法,但ContextCompat.checkSelfPermission在某些系统上(如基于Android8.0的MIUI10检查短信权限时)有bug, 不能准确判断权限是否已获取,此时可结合PermissionChecker.checkSelfPermission进行判断, 所以判断权限是否已获取可采用以下实现:

public static boolean hasPermission(@NonNull Context context, @NonNull String permission) {
    if (ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED
            || PermissionChecker.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED) {
        return false;
    }
 
    return true;
}

申请权限

当未获取权限时,需要向系统请求,请求时使用requestPermissions方法:

// ActivityCompat.java
// 在Activity中申请权限
public static void requestPermissions(final @NonNull Activity activity,
            final @NonNull String[] permissions, final @IntRange(from = 0) int requestCode)
        
 
// Fragment.java
// 在Fragment中申请权限
public final void requestPermissions(@NonNull String[] permissions, int requestCode)

在Fragment使用ActivityCompat.requestPermissions申请权限时,如果用户拒绝了(且勾选了不再提示)请求,Fragment中的onRequestPermissionsResult不会被回调,也就不能引导用户开启权限。所以在Fragment中应该使用Fragment的成员方法requestPermissions来请求权限。

检查APP是否应该向用于展示申请权限的解释

// ActivtyCompat.java
// 检查APP是否应该向用户展示申请权限的解释
public static boolean shouldShowRequestPermissionRationale(@NonNull Activity activity, @NonNull String permission);

此方法的返回值解释如下:
  • 当APP从未申请过指定的权限或申请了指定权限,但被用户拒绝,且勾选了【不再提示】,返回false;
  • APP申请指定权限时被用户拒绝,但未勾选【不再提示】,返回true

所以在使用此方法时,我们要先判断APP是否已申请过权限,否则难以判断返回false的两种情况。

申请权限的结果回调

public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults)

这是Activity和Fragment中申请权限的结果回调方法,其中permissions表示申请的权限数组,grantResults表示每个权限的请求结果。取值为:

// 获得授权
public static final int PERMISSION_GRANTED = 0;
// 未获授权
public static final int PERMISSION_DENIED = -1;

通常申请权限后的处理逻辑都是在该方法中实现。

动态权限申请的实现

动态申请权限的条件:

  • targetSdkVersion >= 23;
  • Android系统版本在6.0及以上;

对于不能同时满足以上条件的情况,默认使用的静态申请权限的方式,但不同的ROM为了安全性,可能对其机制进行了修改,所以可能因ROM不同而有所差异。

了解了申请权限的核心API,接下来就介绍一下在Activity中申请权限的实现过程,下面以点击申请拍照权限为例:

private void startPhoto() {
    if (hasPermission(this, new String[]{Manifest.permission.CAMERA})) {
        // 执行拍照的逻辑
    } else {
         ActivityCompat.requestPermissions(context, rnew String[]{Manifest.permission.ACCESS_FINE_LOCATION}),
                        PERMISSION_REQUEST_CODE);
    }
}

然后在onRequestPermissionsResult中监听权限申请的结果:

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
	super.onRequestPermissionsResult(requestCode, permissions, grantResults);
	
    if (hasPermission(permissions)) {
        // 执行拍照的逻辑
    } else {
        if (ActivityCompat.shouldShowRequestPermissionRationale(context, permissions[refusedPermissionIndex])) {
            // 向用户展示申请权限的理由
        } else {
            // 引用用户去开启权限
        }
    }
}

上面只是一个申请权限的基本流程,真正实现时还要考虑多权限问题,版本的兼容问题,ROM的兼容问题等。

当然,在开发中也不会这样在每个需要申请权限的Activity/Fragment中写这一段代码,即便封装成工具类也需要在每个Activity/Fragment引用,耦合性太高。通常可将权限申请在一个透明的Activity中实现,这样在申请权限时,直接跳转到该Activity即可。

优化授权流程,提高授权几率

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

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

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

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

综上,引导授权流程,动态权限主要实现步骤:

  1. 在AndroidManifest明确我们需要哪些权限。(非动态权限也需要此步)

  2. 在执行操作前检是否获得对应授权 -> checkSelfPermission()。

  3. 如果已授权可以继续操作;如果未授权,判断之前是否授权被拒 -> shouldShowRequestPermissionRationale() (非必须操作)
    a) 判断如果没有被拒过,弹出首次授权引导。
    b) 判断如果被据过,弹出非首次授权引导。

  4. 引导后,申请权限-> requestPermissions()。

  5. 处理申请的结果信息-> 回调函数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)
 
 

下面推荐一个动态申请权限的库供参考。

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

弹窗弹出,用户操作指定选项后,下次再调用四个函数会有如下现象:(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之前,同一权限组的权限,只要用户授权一个,则整个权限组都被授权。

危险权限

关于权限,还有一个权限组的概念。例如,读取外置存储权限(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权限
结果:不会弹出授权弹窗,同一权限组直接被自动授权

但是,部分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权限导致。如何解决?

    /**
     * 申请指定的权限(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,仍是开发者最头疼的问题。

PermissionManager

PermissionManager是一个基于AOP实现的动态申请权限的开源库,目的是让申请权限的过程更简单。当然,也可当做学习Aspectj的的参考项目。具有以下优点:

  • 支持多权限申请;
  • 简单易用,一行注解实现权限申请
  • 提供了可扩展的权限说明和引导设置方式;
  • 提供了阻塞/非阻塞的申请方式;

具体的介绍可参考其源码地址:PermissionManager

部分内容转载自:https://blog.csdn.net/qihoo_tech/article/details/105828426?utm_medium=distribute.pc_relevant_download.none-task-blog-baidujs-2.nonecase&depth_1-utm_source=distribute.pc_relevant_download.none-task-blog-baidujs-2.nonecase

以上是关于Android系统动态申请权限的机制流程总结的主要内容,如果未能解决你的问题,请参考以下文章

Android动态权限申请

Android 批量申请权限

Android权限之动态权限

Android权限篇 Android 6.0动态权限申请

android 自定义的launcher在6.0后需要动态申请权限?

android动态申请权限步骤