这是一篇迟到的文章。在6.0之前,在应用安装的时候,提示用户所需要用到的权限列表,同意之后安装,该app就被赋予所有的权限,我们暂且称它为
安装时权限
,安装后,被赋予的权限也无法取消,当然,国内的一些rom会在系统级别进行额外的一层权限管理,这个不在本文叙述范围之内;在6.0之后,google对权限进行了运行时的管理,而不是在安装时候,危险权限
需要在运行时申请,我们暂且称它为运行时权限
,非危险权限,在安装时由用户授予,这样简化了应用安装过程,因为用户在安装或更新应用时不需要授予权限,也给予了用户对app功能更多的控制
权限分组
系统权限主要分为两类,正常权限
和危险权限
正常权限
不会直接危及用户的隐私,如果你的应用在它的Manifest中列出了正常权限,系统会自动授予权限
危险权限
可以让app访问用户的机密数据,如果你的应用在它的Manifest列出了危险权限,用户必须明确批准你的app使用该权限
当然,不管哪个版本的android,你应用中所用到的所有权限,不管是正常权限
还是危险权限
,都需要在应用的Manifest中申明
如果你的设备运行Android 5.1以及5.1以下版本,或者你的应用的目标SDK是22以及22以下版本:如果你在应用的Manifest中申明了危险权限,用户在安装时必须授予权限,如果拒绝授予权限,那么系统就不会安装应用,也就是所谓的“一刀切”方式,不同意所有权限,就不能安装应用
如果你的设备运行Android 6.0以及6.0以上版本,或者你的目标SDK是23以及23以上版本:应用必须在Manifest中罗列出所有的权限,并且在程序运行时,它必须请求用户授予每一个危险权限,此时用户可以授予或者拒绝每一个权限,并且应用程序可以继续运行有限的功能,即使用户拒绝了权限请求
注意:从Android 6.0开始(API 23),用户可以在任何时候,对任何应用撤销权限,即使app申明的目标SDK低于23
正常权限
正常权限有以下几个特点
就是在安装时,由用户授予,后续运行时无需再次申请,无需再显示提醒用户,用户也不能取消这些权限(在Android 6.0及以上版本例外,用户可以通过管理界面撤销权限)
对用户隐私或安全没有较大影响
正常权限列表
ACCESS_LOCATION_EXTRA_COMMANDS
ACCESS_NETWORK_STATE
ACCESS_NOTIFICATION_POLICY
ACCESS_WIFI_STATE
BLUETOOTH
BLUETOOTH_ADMIN
BROADCAST_STICKY
CHANGE_NETWORK_STATE
CHANGE_WIFI_MULTICAST_STATE
CHANGE_WIFI_STATE
DISABLE_KEYGUARD
EXPAND_STATUS_BAR
GET_PACKAGE_SIZE
INSTALL_SHORTCUT
INTERNET
KILL_BACKGROUND_PROCESSES
MODIFY_AUDIO_SETTINGS
NFC
READ_SYNC_SETTINGS
READ_SYNC_STATS
RECEIVE_BOOT_COMPLETED
REORDER_TASKS
REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
REQUEST_INSTALL_PACKAGES
SET_ALARM
SET_TIME_ZONE
SET_WALLPAPER
SET_WALLPAPER_HINTS
TRANSMIT_IR
UNINSTALL_SHORTCUT
USE_FINGERPRINT
VIBRATE
WAKE_LOCK
WRITE_SYNC_SETTINGS
我们大概看一遍过去,可以总结归纳出正常权限有几类:蓝牙,网络状态,NFC,指纹,闹铃,快捷方式,震动等常用权限
危险权限
危险权限才是运行时权限
的主要处理对象,因为这些权限可能引起隐私wenti或者影响其他程序运行,android中危险权限主要以组的形式出现,可以归纳为一下几个分组:
CALENDAR
CAMERA
CONTACTS
LOCATION
MICROPHONE
PHONE
SENSORS
SMS
STORAGE
具体请看下图:
那么问题来了
Q1.我们的应用是否必须支持运行时权限?
Android为了应用兼容,实际上是可以不需要支持运行时权限的,只要设置targetSdkVersion低于23就可以了,意思是我的应用还乜有在API 23上面完全兼容,不要给我开启运行时权限新特性,当然了,早晚你还是要支持的,时间问题而已
Q2.如果不支持运行时权限,应用会崩溃么?
可能会奔溃,具体要根据你使用运行时权限在代码的什么地方,假设你应用启动就开始创建SDCard文件夹,但是在Android 6.0的设备上,系统或者用户在系统权限管理界面禁止了你的SDCard权限,如果应用代码处理不当,此时重新启动应用就会发生奔溃
这里我们还要注意的一点是,危险权限是以组的方式授予的,怎么理解呢?按照我们上面列出的9大分组,举个栗子,如果你申请读取SDCard权限,在用户授予权限后,你自动就获得了写入SDCard的权限,也就是说你获得了STORAGE分组的所有权限
运行时权限申请
API使用
申请运行时权限主要使用到的API有下面三个:
# 检测系统当前是否被授予某个运行时权限,传入参数是权限名称,比如Manifest.permission.READ_EXTERNAL_STORAGE
ContextCompat.checkSelfPermission(@NonNull Context context, @NonNull String permission)
# 是否要显示权限说明
# 1.用户第一次被拒绝(非永久拒绝)授予某个权限后,下次再次请求该权限,这个方法会返回true,用户有机会以某种方式对用户进行说明该权限用处
# 2.用户在第一次拒绝某个权限后,下次再次申请时,授权的dialog中将会出现“不再提醒”选项,一旦选中勾选了,那么下次申请将不会提示用户。
# 3.第二次请求权限时,用户拒绝了,并选择了“不再提醒”的选项,调用shouldShowRequestPermissionRationale()后返回false。
# 4.设备的策略禁止当前应用获取这个权限的授权:shouldShowRequestPermissionRationale()返回false 。
# 5.加这个提醒的好处在于,用户拒绝过一次权限后我们再次申请时可以提醒该权限的重要性,免得再次申请时用户勾选“不再提醒”并决绝,导致下次申请权限直接失败。
ActivityCompat.shouldShowRequestPermissionRationale(@NonNull Activity activity,@NonNull String permission)
# 请求权限授予,当然可以传入多个权限名称同时申请,用户会依次弹出多个提示框申请权限,App不能配置和修改这个对话框,如果需要提示用户这个权限相关的信息或说明,需要在调用 requestPermissions() 之前处理,该方法有两个参数:
# int requestCode,会在回调onRequestPermissionsResult()时返回,用来判断是哪个授权申请的回调。
# String[] permissions,权限数组,你需要申请的的权限的数组。
# 由于该方法是异步的,所以无返回值,当用户处理完授权操作时,会回调Activity或者Fragment的onRequestPermissionsResult()方法。
ActivityCompat.requestPermissions(final @NonNull Activity activity,final @NonNull String[] permissions, final @IntRange(from = 0) int requestCode)
# 用户拒绝或者授予用户权限后的回调方法,该方法在Activity/Fragment中应该被重写,当用户处理完授权操作时,系统会自动回调该方法,该方法有三个参数:
# int requestCode,在调用requestPermissions()时的第一个参数。
# String[] permissions,权限数组,在调用requestPermissions()时的第二个参数。
# int[] grantResults,授权结果数组,对应permissions,具体值和上方提到的PackageManager中的两个常量做比较。
onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults)
我们以请求读写SDCard权限,来看一下一个标准的请求权限流程
# 判断当前系统版本是都是6.0以上
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
# 检测是否已经被用户授予该权限
if (ContextCompat.checkSelfPermission(LaunchActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
# 没有被授予权限
# 判断如果已经被用户拒绝过一次(非永久拒绝),则显示用户此权限的一些说明,这里可以用toast,当然也可以用自定义界面来显示给用户,如果是采用自定义界面来显示给用户,在有“确定”,“取消”等按钮的情况下,下面的“请求权限”步骤要酌情调用
if (ActivityCompat.shouldShowRequestPermissionRationale(LaunchActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE)) {
Toast.makeText(LaunchActivity.this, "request read external storage", Toast.LENGTH_LONG).show();
}
# 请求权限,数组传入多个值,可以一次请求多个权限
ActivityCompat.requestPermissions(LaunchActivity.this,
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
PERMISSIONS_REQUEST_EXTERNAL_STORAGE);
}
} else {
# 6.0以下版本,直接使用权限
// 做一些权限对应的操作
}
如果是在6.0及6.0以上版本时,执行 ActivityCompat.requestPermissions()方法请求权限后,如果用户同意或者拒绝后,会回调onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults)方法,该方法的几个参数,我们做一下解释:
requestCode 对应的是ActivityCompat.requestPermissions请求时的requestCode,这个与Activity中常用的onActivityResult方法中的requestCode类似,用来标识某一次或者某一个请求的回调对应关系
permissions 对应ActivityCompat.requestPermissions()方法中请求的权限列表
grantResults 对应2中permissions的每个权限的用户应答结果
好了,接下来就是根据requestCode,遍历permissions和grantResults中的结果,做对应的操作
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch (requestCode) {
case PERMISSIONS_REQUEST_EXTERNAL_STORAGE: {
// If request is cancelled, the result arrays are empty.
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// permission was granted, yay! Do the
// contacts-related task you need to do.
} else {
// permission denied, boo! Disable the
// functionality that depends on this permission.
}
return;
}
}
}
注意事项
API问题
由于checkSelfPermission和requestPermissions从API 23才加入,低于23版本,需要在运行时判断 或者使用Support Library v4中提供的方法
ContextCompat.checkSelfPermission
ActivityCompat.requestPermissions
ActivityCompat.shouldShowRequestPermissionRationale
两个权限
运行时权限对于应用影响比较大的权限有两个,他们分别是
READ_PHONE_STATE
WRITE_EXTERNAL_STORAGE/READ_EXTERNAL_STORAGE
其中READ_PHONE_STATE用来获取deviceID,即IMEI号码。这是很多统计依赖计算设备唯一ID的参考。如果新的权限导致读取不到,避免导致统计的异常。建议在完全支持运行时权限之前,将对应的值写入到App本地数据中,对于新安装的,可以采取其他策略减少对统计的影响。
WRITE_EXTERNAL_STORAGE/READ_EXTERNAL_STORAGE这两个权限和外置存储(即sdcard)有关,对于下载相关的应用这一点还是比较重要的,我们应该尽可能的说明和引导用户授予该权限。
建议
不要使用多余的权限,新增权限时要慎重
使用Intent来替代某些权限,如拨打电话,选择图片(和你的产品经理PK去吧)
对于使用权限获取的某些值,比如deviceId,尽量本地存储,下次访问直接使用本地的数据值
注意,由于用户可以撤销某些权限,所以不要使用应用本地的标志位来记录是否获取到某权限,在使用的时候要遵循流程实时判断后使用,避免不正确的使用导致应用崩溃
注意
即使支持了运行时权限,也要在Manifest声明,因为市场应用会根据这个信息和硬件设备进行匹配,决定你的应用是否在该设备上显示。
防止一次请求太多的权限或请求次数太多,用户可能对你的应用感到厌烦,在应用启动的时候,最好先请求应用必须的一些权限,非必须权限在使用的时候才请求,建议整理并按照上述分类管理自己的权限
权限申请弹出的对话框不能自定义
解释你的应用为什么需要这些权限:在你调用requestPermissions()之前,你为什么需要这个权限
个人觉得Marshmallow的运行时权限对于用户来说绝对是一个好东西,但是目前想要支持需要做的事情还是比较多的。
对于一个有很多依赖的宿主应用,想要做到支持还是有一些工作量的,因为你的权限申请受制于依赖。
最后,通过以上使用例子的代码我们看到,虽然原理和内容很简单,但是流程上我们还是要写很多代码,判断几个条件,稍后我将在另外一片文章中根据请求运行时权限的特点,封装出一个库EPermission ,建议大家使用这个库,简化权限申请流程
附录
权限结果常量
PackageManager.PERMISSION_DENIED:该权限是被拒绝的。
PackageManager.PERMISSION_GRANTED:该权限是被授权的。
权限名称
Manifest.permission.READ_EXTERNAL_STORAGE
Manifest.permission.CAMERA
等等Manifest.permission类中对应的常量