一、概要
Android M已经发布一段时间了,市面上很多应用都已经适配Android M。权限机制,作为Android M的一大特性,受到了很多开发者的关注。本文主要分享了以下几个知识点的内容,1、Android权限机制关键知识点;2、QQ音乐对于权限的适配经验;3、近段时间以来遇到的一些Android权限方面的问题。OK,下面进入主题。
二、Android权限机制
已经了解过基本知识的,建议直接跳到第三点(QQ音乐的权限适配经验)。
Android6.0以前,Android的权限机制比较简单,开发者在 AndroidManifest 文件中声明需要的权限,APP安装时,系统提示用户APP将获取的权限,需要用户同意授权才能继续安装,从此APP便永久的获得了授权。然而,同期的iOS对于权限的处理会更加灵活,权限的授予并不是在安装时,而是在APP运行时,用户可以根据自身的需要,决定是否授予APP某一权限,同时,用户也可以很方便回收授予的权限。显然,动态权限管理的机制,对于用户的隐私保护是更加适用的,Android过于简单的权限机制也受到了不少人的吐槽。终于,Android6.0也发布了动态权限的机制。
开始适配和如何兼容
APP要适配Android6.0非常简单,只需要将 targetSdkVersion 和 compileSdkVersion 都升级到23及以上,同时加入权限检查申请等代码逻辑即可。这里很多人会有一些疑惑,如果针对旧版本的APP在Android6.0机型上运行或者针对Android6.0适配了的APP在Android6.0以下机型上运行,会有什么表现呢?是如何兼容的呢?
1、首先,旧版本APP( targetSdkVersion 低于23),因为没有适配权限的申请相关逻辑,在Android6.0以上机型运行的时候,仍然采用安装时授权的方案。
2、适配了Android6.0的APP,在低版本Android系统上运行的时候,仍然采用安装时授权的方案,但是开发者需要注意的是,权限申请的代码逻辑只应该在Android6.0及以上的机型被执行。
危险权限与普通权限
一开始,听到要加入权限判断和申请代码逻辑的程序员内心可能是崩溃的:正常的一个有一定规模的APP,很容易就七七八八的声明了很多权限,如果每个权限都申请岂不是非常麻烦?
好歹,Google还算比较明智, 并不是所有的权限都需要运行时申请才能使用 。Google对每个权限的隐私危害性进行了评估。将权限分为了两大类:普通权限和危险权限。举个例子,控制手机震动的权限对于用户并没有什么危害,只要开发者声明了这个权限,安装后就可以一直被授权,也不能被回收,但是,像读取sd卡数据这类权限,很显然就是危险权限了,APP必须向用户申请这个权限。
Google还是很体贴我们开发者的,为了进一步减少开发的工作量和申请权限对用户的骚扰,对危险权限根据各自的属性进行了分组。举个例子,读sd卡和写sd卡,这两个权限通常都是成对声明和使用的,因此,它们被分为一组,而且,只要我们获取了这个权限组里面的任意一个权限,就可以获取整个权限组的权限。Google对于危险权限的定义和分组见下图。
权限相关API说明
首先,在动态权限申请的流程中,开发者主要关注流程和API如下:
1、检查权限是否授予。
Activity.java
public int checkSelfPermission(permission)
2、申请权限。
Activity.java
public final void requestPermissions( new String[permission1,permission2,...], requestCode)
这个时候,会弹出系统授权弹窗( 授权弹窗是不支持自定义的,原因理所当然 )。
3、权限回调。
用户在系统弹窗里面选择后,结果会通过 Activity 的 onRequestPermissionsResult 方法回调APP。
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
//继续执行逻辑或者提示权限获取失败
}
4、权限说明。
用户如果选择了拒绝,下一次在需要声明该权限的时候,Google建议APP开发者给予用户更多的说明,因此提供了下面这个API, 这个方法返回值在使用过程中会发现有点纠结(具体解析见下面代码块说明) 。
public boolean shouldShowRequestPermissionRationale(permission)
{
1、APP没有申请这个权限的话,返回false
2、用户拒绝时,勾选了不再提示的话,返回false
3、用户拒绝,但是没有勾选不再提示的话,返回true
因此如果想在第一次就给用户提示,需要记录权限是否申请过,没有申请过的话,强制弹窗提示,而不能根据这个方法的返回值来。
}
三、QQ音乐的权限适配经验
1、不同权限,申请的时机不同
QQ音乐作为一个比较复杂的流媒体应用,也需要不少权限,但是究竟在什么时候来申请这些权限就成了适配6.0时首当其冲问题。针对这个问题,我们也对需要的权限进行了思考,大致认为申请权限需要分为两个时机。
用户触发:这个很好理解,有些和特性相关的权限,比如说听歌识曲的录音权限、自建歌单封面拍照权限等,这类权限平时APP运行时并不需要,那么我们选择在用户触发或者进入该功能的时候,进行授权受阻逻辑。
应用启动时:我们在梳理的时候发现,有些权限(读取设备信息,读写sd卡等)并不是由用户或者特性触发的,而是网络免流,登录安全,日志系统这些底层逻辑无时不刻触发的。对于这些权限,就比较纠结了。不过回过头来看,这些权限通常是开发者或者APP不能妥协的权限,因为如果用户不授权的话,将会影响整个APP的功能和数据。所以,我们选择比较暴力的方式,在应用启动的时候,就受阻。这也是Google建议的一种方式。
但是需要注意的是,一开始就申请授权也不要冷冰冰地直接拉起系统弹窗授权,建议先用APP自己的弹窗向用户礼貌地说明为什么需要这几个权限,比如,读取不到设备信息无法联通免流,无法保证登录安全,读取不到SD卡无法播放歌曲等,避免太生硬引起用户的反感。特别是,因为本地化翻译的原因,Google对于权限的弹窗说明很不local,例如我们申请读取设备信息的权限时,系统的弹窗是“电话权限”,这里很容易引起用户的误解,所以, 合理的引导和解释是必不可少的 。
2、应用启动授权,需要一个壳
刚刚已经说到了,很多隐形的权限和特性无关。那么,如果我们直接启动APP,用户又还没有授权的情况下,很多初始化逻辑很容易就因为没有权限crash了,即使没有crash,后面也可能会有或多或少其他的问题。因此,我们需要在这些权限完全授予前,禁止这些逻辑的执行。
做过启动相关的同学都知道,拦截一个APP正常的启动后面再恢复,是很复杂的一件事情,往往我们需要一个外壳来把业务逻辑的内壳隔绝开。就QQ音乐而言,我们很容易的就想到了dex加载的壳,需求也很类似,dex加载也需要优先于业务来做。顺着这个思路,很自然地,我们就选择了在dex的壳里面做权限的受阻逻辑,而且也很快很好的达到了预期的效果。相信现在大部分APP都是分dex的了,因此建议按照这个方式来做,可以节省很多的工作量。
四、Android权限机制“乱象”
这里要说的乱象,其实是和Android严重的碎片化有一定的关系。随着国产ROM越来越个性,很多ROM在尝试建立自己的权限机制,有些甚至基于Android5.x就开放了原生的或者开发了自己的权限机制。而面对这些情况,我们往往能做的非常有限,举几个例子。
1、读取运动数据权限
开发QQ音乐跑步电台的过程中发现,在某国产ROM的一些机型上会提示“应用读取运动数据权限”的系统弹窗。可是,反复查阅相关API发现,我们使用的计步相关的 Sensor 并不需要申请什么权限。可如果用户选择了拒绝,即使APP注册了 Sensor ,也收不到系统的回调。后来联系该厂商的相关人员后,给出的答复是,第三方APP无法检查和申请这个权限,这个权限本身也属于该厂商ROM自己的权限机制。
类似的案例还有一个,就是在某厂商的手机管家,会一直提示QQ音乐尝试读取应用程序列表。其实,我们并没有读取应用程序列表,只是调用了 PackageManager 相关的一些API,就是触发这个告警。
对于这类问题,我们怀疑,第三方ROM是在运行时检测到了APP调用了相关的API后,进行权限阻断。这里开发同学需要注意的是,被阻断的API不一定会导致crash,但是可能导致我们获取不到正确的返回值或者收不到系统的一些消息回调。
2、无法添加快捷方式
本来 声明后,我们就可以在桌面上创建快捷方式了,而且这个权限也不是危险权限。可是某些国产ROM,对于APP添加快捷方式限制的比较严,必须要用户在设置里面手动允许添加快捷方式后,APP才能最终成功的添加。这种情况,APP也不能知道是否能添加快捷方式,只能默默的添加失败了。不过好在这里受影响并不是主快捷方式,而且某些功能的快捷方式入口。
3、消失的桌面歌词,悬浮窗权限
QQ音乐桌面歌词采用了向 WindowManager 里面添加 View 的方式实现。可是很多国产ROM很早就具备了悬浮窗权限。一开始,我们将 type 改为 LayoutParams.TYPE_TOAST 同时声明 这个普通权限,躲避了大多数系统的问题。可是,2016年底,随着某ROM系统的升级,这一招也没用了,大批用户反馈爆发。
我们继续尝试检测悬浮窗权限,发现 checkPermission(”android.permission.SYSTEM_ALERT_WINDOW”) 返回的结果永远是 true ,因此这条路也走不通。
最终,经过各种查阅,发现这个悬浮窗权限并不在Android6.0标准的权限机制内,而是 AppOpsManager 里面已经被隐藏了的一个开关位,对应于第24个开关。需要注意的是, AppOpsManager 这个类很早就有了,但是很多ROM隐藏了 checkOp 的方法,好在最后发现通过反射仍旧可以调用这个方法检测权限是否打开。
AppOpsManager manager = (AppOpsManager) context.getSystemService("appops");
try {
Object object = invokeMethod(manager, "checkOp", op, Binder.getCallingUid(), getPackageName(context));
return AppOpsManager.MODE_ALLOWED == (Integer) object;
} catch (Exception e) {
MLog.e(TAG, "CheckPermission " + e.toString());
}
不过,要打开悬浮窗权限,不同ROM的路径还不一样,有的是在设置里面,有的是在系统自带的管家里面,最后我们只能根据不同的ROM,给予用户不同的引导,终于将反馈量降了下去。