Android 6.0权限管理的解析与实战

一、引言

随着Android6.0发布,系统增加了一些新的特性和功能。这次的发布介绍了一种新的权限机制。用户可以在运行时直接管理应用程序的权限。这个功能提升了权限控制的可见性和可控性。同时简化了安装和自动升级过程,用户可以单独撤销或者授予应用程序某项权限,对应用拥有更多的控制权。

二、Android 6.0权限机制

当你应用程序target是Android 6.0及以上(API level 23),确保在运行时检查和请求权限。为了确定你的app是否授予某个权限,通过checkSelfPermission()方法判断,请求权限使用requestPermissions()方法。即使你的app不是target Android 6.0,你也应该在新的权限机制下测试你的应用。

用户很容易被一个app繁多的权限请求淹没,当用户发现一个app使用起来很麻烦,或者用户担心app窃取自己的信息。用户会拒绝使用这个app,更有甚至会卸载它。因此,在申请权限的时候,我们应该遵守以下几点规则进行申请。

(a)只向用户请求你需要的权限(尽可能少请求权限)

每次app请求权限,都是强制用户去做决定。你应该尽量减少这些请求的次数,以保证用户流畅操作。

(b)不要让繁多的权限申请请求淹没用户

不要一次性请求所有权限,只有当你需要使用的时候才去申请。在某些情况下,一个或多个权限是你app必须的,这个时候可能在应用一启动就去权限更加有意义。例如,你做的是一个摄影类的应用,应用需要使用设备照相机。当用户第一次启动app的时候,对于app请求使用照相机的权限,用户并不会感到迷惑。但是,如果这个应用拥有向联系人分享照片的功能,你最好不要在应用启动的时候就去申请READ_CONTACTS权限。你应该等到用户使用分享功能的时候,再去询问用户是否申请这个权限。

如果你的app提供了一个引导教程,在引导结束的时候申请必要权限是比较好的。

(c)测试所有的权限模式

从Android 6.0(API 23)开始,用户在运行时授予或者拒绝权限,而不是在安装app的时候。因此,你必须在更广泛的条件下测试你的应用。在Android 6.0之前,你可以合理假设如果你的程序运行,你程序拥有所有申明的权限。但在新的权限模式下,你不能进行这种假设。

下面的一些小贴士,帮助你在Android 6.0或者更高的版本上识别权限相关代码问题。

(1)识别app中当前权限和相关代码路径

(2)用permission-protected服务和数据测试用户流。

(3)通过各种组合测试权限授予和拒绝。

如一个拍照的app,可能会在manifest中声明CAMERA,READ_CONTACTS和ACCESS_FINE_LOCATION权限。你需要测试所有权限的授予和拒绝组合状态,确保app可以在所有的权限组合下优雅滴工作。(注意,从Android 6.0开始,用户可以对任意app的权限进行授予或者拒绝,即使app的目标API是小于23的)

(4)使用adb工具通过命令行管理权限

--组展示权限和状态

$ adb shell pm list permissions -d -g

--授权或者拒绝一个或者多个权限

adb shell pm [grant|revoke] ...

(5)分析应用中使用到权限的相关服务

三、Android系统权限介绍

Google将权限分为两类,一类是普通权限(Normal Permissions),这类权限一般不涉及用户隐私,也不需要用户进行授权,如网络访问,手机震动等,这些权限如下所示:

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_INSTALL_PACKAGES

SET_ALARM

SET_TIME_ZONE

SET_WALLPAPER

SET_WALLPAPER_HINTS

TRANSMIT_IR

UNINSTALL_SHORTCUT

USE_FINGERPRINT

VIBRATE

WAKE_LOCK

WRITE_SYNC_SETTINGS

另外一类是危险权限(Dangerous Permission),涉及用户隐私,需要用户授权,如对sd卡读取、访问用户手机通讯录等。如图所示:

查看dangerou Permissions可以发现权限是分组的,这个和Android 6.0的授权机制有关。如果你申请某个危险的权限,假设你的app早已被用户授权了同一组的某个危险权限,那么系统会立即授权,而不需要用户去点击授权。比如你的app对READ_CONTACTS已经授权了,当你的app申请WRITE_CONTACTS时,系统会直接授权通过。此外,申请时弹出的dialog上面的文本说明也是对整个权限组的说明,而不是单个权限(ps:这个dialog是不能进行定制的)。

注:不要对权限组过多的依赖,尽可能对每个危险权限都进行正常流程的申请,因为在后期的版本中这个权限组可能会产生变化。

新权限机制实践

(1)在AndroidManifest文件中添加需要的权限

这个步骤和我们之前的开发并没有什么变化,试图去申请一个没有声明的权限可能会导致程序崩溃。

(2)检查权限

if(ContextCompat.checkSelfPermission(thisActivity, Manifest.permission.READ_CONTACTS)!= PackageManager.PERMISSION_GRANTED) {

//

}else{

//

}

这里涉及到一个API,ContextCompat.checkSelfPermission,主要用于检测某个权限是否已经被授予,方法返回值为PackageManager.PERMISSION_DENIED或者PackageManager.PERMISSION_GRANTED。当返回DENIED就需要进行申请授权了。

(3)申请授权

ActivityCompat.requestPermissions(thisActivity,

new String[]{Manifest.permission.READ_CONTACTS},

MY_PERMISSIONS_REQUEST_READ_CONTACTS);

该方法是异步的,第一个参数是Context;第二个参数是需要申请的权限的字符串数组;第三个参数为requestCode,主要用于回调的时候检测。可以从方法名requestPermissions以及第二个参数看出,是支持一次性申请多个权限的,系统会通过对话框逐一询问用户是否授权。

(4)处理权限申请回调

@Override

public void onRequestPermissionsResult(intrequestCode,

String permissions[], int[] grantResults) {

switch (requestCode) {

case MY_PERMISSIONS_REQUEST_READ_CONTACTS: {

// 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 youneed to do.

} else {

// permission denied,boo! Disable the

// functionality that dependson this permission.

}

return;

}

}

}

ok,对于权限的申请结果,首先验证requestCode定位到你的申请,然后验证grantResults对应于申请的结果,这里的数组对应于申请时的第二个权限字符串数组。如果你同时申请两个权限,那么grantResults的length就为2,分别记录你两个权限的申请结果。如果申请成功,就可以做你的事情了~

// Should we show an explanation?

if(ActivityCompat.shouldShowRequestPermissionRationale(thisActivity,

Manifest.permission.READ_CONTACTS))

//Show an expanation to the user *asynchronously* -- don't block

// this thread waiting for the user's response! After the user

// sees the explanation, try again to request the permission.

}

这个API主要用于给用户一个申请权限的解释,该方法只有在用户在上一次已经拒绝过你的这个权限申请。也就是说,用户已经拒绝一次了,你又弹个授权框,你需要给用户一个解释,为什么要授权,则使用该方法。

// Here, thisActivity is the currentactivity

if(ContextCompat.checkSelfPermission(thisActivity,

Manifest.permission.READ_CONTACTS)

!= PackageManager.PERMISSION_GRANTED) {

// Should we show an explanation?

if (ActivityCompat.shouldShowRequestPermissionRationale(thisActivity,

Manifest.permission.READ_CONTACTS)) {

// Show an expanation to the user *asynchronously* -- don't block

// this thread waiting for the user's response! After the user

// sees the explanation, try again to request the permission.

}else {

// No explanation needed, we can request the permission.

ActivityCompat.requestPermissions(thisActivity,

newString[]{Manifest.permission.READ_CONTACTS},

MY_PERMISSIONS_REQUEST_READ_CONTACTS);

// MY_PERMISSIONS_REQUEST_READ_CONTACTS is an

// app-defined int constant. The callback method gets the

// result of the request.

}

}


四、权限库封装

在系统是Android 6.0及以上的手机上,申请每个Dangerous Permission,都需要进行权限处理。如果不进行封装,会有很多的重复代码。这样的代码不够美观,且不利于阅读和维护。下面我介绍两种封装好的权限控制库PermissionGen和MPermissions。其中MPermissions是在PermissionGen的基础上做了优化,即将运行时注解改为编译时注解,提升了性能。

4.1 PermissionGen

4.1.1 库的使用方式

首先我们来看看PermissionGen是怎么使用的。在Activity或者fragment中,申请权限可以通过以下方式:

PermissionGen.needPermission(this,200,Manifest.permission.CAMERA);

或者

PermissionGen.with(MainActivity.this)

.addRequestCode(100)

.permissions(

Manifest.permission.READ_CONTACTS,

Manifest.permission.RECEIVE_SMS,

Manifest.permission.WRITE_CONTACTS)

.request();

通过设置参数,可以实现申请单个权限或者多个权限。我们知道,系统原生权限请求处理都是在回调onRequestPermissionsResult中处理的,这里我们使用PermissionGen,只需要在onRequestPermissionsResult中调用一下以下方法,如此库即可对回调结果进行分发处理。

@Override

public void onRequestPermissionsResult(intrequestCode,String[] permissions,

int[] grantResults) {

PermissionGen.onRequestPermissionsResult(this,requestCode,permissions,grantResults);

}

最后,对于权限授权的回调,是在activity或者fragment中处理的,如下所示。授权成功则执行PermissionSuccess修饰的方法,失败则执行PermissionFail修饰的方法,不同的权限通过requestCode来区分。

@PermissionSuccess(requestCode=200)

public void successOpenCamera(){

Dlog.debug("open camera success");

}

@PermissionFail(requestCode=200)

public void failOpenCamera(){

Toast.makeText(this,"Camera permission is not granted",Toast.LENGTH_SHORT).show();

}

4.1.2 库实现原理解析

通过上面库的使用,我们发现这个用起来真的很方便。下面我们从权限申请开始,一步步从源码分析库是怎么实现的。

首先,当我们去申请权限的时候,都会调用到PermissionGen.java类中的requestPermissions方法,我们来看看这个方法的实现:

首先遍历自己申请的所有权限,没有授权的相关权限存入列表deniedPermissions。若权限全都申请,deniedPermissions长度为0,则通过doExecuteSuccess方法,反射找到相应requestCode对应的回调方法并执行;若权限有未申请的,则调用activity或者fragment的requestPermissions方法去申请相应权限,此时会弹出相应弹窗(弹窗是系统级,不可更改),执行结果回调到onRequestPermissionResult方法。在方法中判断所有申请权限是否已经授权成功。成功执行doExecuteSuccess,失败执行doExecuteFailed。

接下来我们看看onRequestPermissionsResult方法,我们知道在activity或者fragment中,我们重写了onRequestPermissionsResult方法,在其中去执行PermissionGen中的onRequestPermissionsResult方法进行授权结果处理。而在PermissionGen中,对于授权结果的处理,主要是在requestResult中进行处理。

代码中遍历授权请求结果数组,只要有一个权限没有授权通过,则本次的授权就是失败的,调用doExecuteFail方法,成功则调用doExecuteSuccess方法。这里是这个库的精髓所在,库是如何通过doExecuteSuccess和doExecuteFail方法,执行requestCode对应的请求回调方法?这里以doExecuteSuccess为例进行介绍。我们发现doExecuteSuccess有两个参数,第一个参数表示的是请求执行的actvity或者fragment;第二个参数是requestCode。下面我们来看doExecuteSuccess的代码:

private static void doExecuteSuccess(Object activity, intrequestCode) {

Method executeMethod = Utils.findMethodWithRequestCode(activity.getClass(),

PermissionSuccess.class,requestCode);

executeMethod(activity,executeMethod);

}

看到这里就有种豁然开朗的感觉了,findMethodWithRequestCode这个方法必定是通过反射的方式找到注解修饰的对应的方法,然后通过executeMethod去执行对应的方法。我们来看findMethodWithRequestCode的源码:

正如我们所猜测的,代码中通过反射找到通过注解修饰的方法;然后通过对比注解的requestCode定位到正确的回调方法,最后通过executeMethod执行回调方法。

4.2 MPermissions

4.2.1 MPermissons库介绍

MPermissons库是作者在PermissionGen的基础上修改而来的,相信大家也都发现一个问题。PermissionGen是基于反射,在运行时获取回调方法并执行的,这在一定程度上影响了性能。MPermissons基于Annotation Processor,运行时注解,很好解决PermissionGen的问题。

4.2.2 MPermissions的使用

我们来看看MPermissions的使用,基本是和PermissionGen一样的。

首先:请求权限:

MPermissions.requestPermissions(MainActivity.this,REQUECT_CODE_CALL_PHONE,Manifest.permission.CALL_PHONE);

然后,重写onRequestPermissionsResult方法:

@Override

public void onRequestPermissionsResult(intrequestCode,String[] permissions, int[] grantResults)

{

MPermissions.onRequestPermissionsResult(this,requestCode,permissions,grantResults);

super.onRequestPermissionsResult(requestCode,permissions,grantResults);

}

最后注解修饰回调方法:

@PermissionGrant(REQUECT_CODE_CALL_PHONE)

public voidrequestCallPhoneSuccess()

{

Toast.makeText(this,"GRANT ACCESS SDCARD!",Toast.LENGTH_SHORT).show();

}

@PermissionDenied(REQUECT_CODE_CALL_PHONE)

public voidrequestCallPhoneFailed()

{

Toast.makeText(this,"DENY ACCESS SDCARD!",Toast.LENGTH_SHORT).show();

}

4.2.3 MPermissions的实现原理

从上面可以发现MPermissions和PermissionGen的使用方式是一样的,只是接口的命名方式不同而已。同样我们从权限申请开始分析,一路看下去,与PermissionGen的处理逻辑都是一样的。最后不同的doExecuteSuccess和doExecuteFail方法的实现逻辑。

看源码发现,MPermissions是通过Class.forName静态加载的方式,加载了对应activity或者fragment类的代理类,并执行requestCode对应的回调方法。这与PermissionGen的运行时反射处理相比,性能肯定是有提升的。但是到这里我们应该会有一个疑问,那就是这个代理类是从哪里冒出来的?我们继续往下看,发现库项目目录下有一个compile的工程,这是关键所在。我们的代理类,通过annotation Processor,在编译时,生成对应的XXXActivity$PermissionProxy,通过PermissionProcessor中的process方法生成。具体annotation processor的实现,大家可以自己查阅相关资料。


生成代理类如下所示:

public classMainActivity$$PermissionProxyimplementsPermissionProxy {

    publicMainActivity$$PermissionProxy(){

    }

    public void grant(MainActivity source, intrequestCode) {

        switch(requestCode){

        case20:

            source.requestSdcardSuccess();

            break;

        case30:

            source.requestCallPhoneSuccess();

        }

    }

    public void denied(MainActivity source, intrequestCode) {

        switch(requestCode){

        case20:

            source.requestSdcardFailed();

            break;

        case30:

            source.requestCallPhoneFailed();

        }

    }

}

最后,在doExecuteSuccess()或者doExecuteFail()方法中,通过Class.forName()加载的是生成的代理类,通过newInstance()新建对象,然后通过requestCode区分,调用对应的grant或者deny方法。

五、总结

Android6.0的权限控制,虽然目前看来处理起来还是有点繁琐,但使用户对应用有了更多的控制权。借助一些封装的权限控制第三方库,对于应用权限可以做到比较简洁的控制。PermisionGen操作比较简单,但是是运行时反射实现,性能较差;MPermissions解决了这个问题,在编译时生成代理类,解决了库运行时效率低的问题,但与之相比库实现起来略烦琐。


参考文章:Android 6.0 运行时权限处理完全解析

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容