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