Android 6.0 运行时权限处理彻底解析

1、概述html

随着Android 6.0发布以及普及,咱们开发者所要应对的主要就是新版本SDK带来的一些变化,首先关注的就是权限机制的变化。对于6.0的几个主要的变化,查看查看官网的这篇文章http://developer.android.com/intl/zh-cn/about/versions/marshmallow/android-6.0-changes.html,其中固然包含Runtime Permissionsandroid

ok,本篇文章目的之一就是对运行时权限处理的一个介绍,以及对目前权限相关的库的一些了解。git

固然很是推荐阅读官网权限相关文章:github

本文也是在上述文章基础上理解、实验以及封装。数组

2、运行时权限的变化及特色

对于6.0如下的权限及在安装的时候,根据权限声明产生一个权限列表,用户只有在赞成以后才能完成app的安装,形成了咱们想要使用某个app,就要默默忍受其一些没必要要的权限(好比是个app都要访问通信录、短信等)。而在6.0之后,咱们能够直接安装,当app须要咱们授予不恰当的权限的时候,咱们能够予以拒绝(好比:单机的象棋对战,请求访问任何权限,我都是不一样意的)。固然你也能够在设置界面对每一个app的权限进行查看,以及对单个权限进行受权或者解除受权。网络

新的权限机制更好的保护了用户的隐私,Google将权限分为两类,一类是Normal Permissions,这类权限通常不涉及用户隐私,是不须要用户进行受权的,好比手机震动、访问网络等;另外一类是Dangerous Permission,通常是涉及到用户隐私的,须要用户进行受权,好比读取sdcard、访问通信录等。架构

  • Normal Permissions以下app

Java

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

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 Permissions:

Java

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

group:android.permission-group.CONTACTS

  permission:android.permission.WRITE_CONTACTS

  permission:android.permission.GET_ACCOUNTS

  permission:android.permission.READ_CONTACTS

 

group:android.permission-group.PHONE

  permission:android.permission.READ_CALL_LOG

  permission:android.permission.READ_PHONE_STATE

  permission:android.permission.CALL_PHONE

  permission:android.permission.WRITE_CALL_LOG

  permission:android.permission.USE_SIP

  permission:android.permission.PROCESS_OUTGOING_CALLS

  permission:com.android.voicemail.permission.ADD_VOICEMAIL

 

group:android.permission-group.CALENDAR

  permission:android.permission.READ_CALENDAR

  permission:android.permission.WRITE_CALENDAR

 

group:android.permission-group.CAMERA

  permission:android.permission.CAMERA

 

group:android.permission-group.SENSORS

  permission:android.permission.BODY_SENSORS

 

group:android.permission-group.LOCATION

  permission:android.permission.ACCESS_FINE_LOCATION

  permission:android.permission.ACCESS_COARSE_LOCATION

 

group:android.permission-group.STORAGE

  permission:android.permission.READ_EXTERNAL_STORAGE

  permission:android.permission.WRITE_EXTERNAL_STORAGE

 

group:android.permission-group.MICROPHONE

  permission:android.permission.RECORD_AUDIO

 

group:android.permission-group.SMS

  permission:android.permission.READ_SMS

  permission:android.permission.RECEIVE_WAP_PUSH

  permission:android.permission.RECEIVE_MMS

  permission:android.permission.RECEIVE_SMS

  permission:android.permission.SEND_SMS

  permission:android.permission.READ_CELL_BROADCASTS

能够经过adb shell pm list permissions -d -g进行查看。

看到上面的dangerous permissions,会发现一个问题,好像危险权限都是一组一组的,恩,没错,的确是这样的,

那么有个问题:分组对咱们的权限机制有什么影响吗?

的确是有影响的,若是app运行在Android 6.x的机器上,对于受权机制是这样的。若是你申请某个危险的权限,假设你的app早已被用户受权了同一组的某个危险权限,那么系统会当即受权,而不须要用户去点击受权。好比你的app对READ_CONTACTS已经受权了,当你的app申请WRITE_CONTACTS时,系统会直接受权经过。此外,对于申请时弹出的dialog上面的文本说明也是对整个权限组的说明,而不是单个权限(ps:这个dialog是不能进行定制的)。

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

3、相关API

好在运行时相关的API也比较简单,因此适配起来并不会很是痛苦。

API的讲解就跟着申请权限步骤一块儿了:

  1. 在AndroidManifest文件中添加须要的权限。这个步骤和咱们以前的开发并无什么变化,试图去申请一个没有声明的权限可能会致使程序崩溃。

  2. 检查权限

    Java

    1

    2

    3

    4

    5

    6

    if (ContextCompat.checkSelfPermission(thisActivity,

                    Manifest.permission.READ_CONTACTS)

            != PackageManager.PERMISSION_GRANTED) {

    }else{

        //

    }


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

  3. 申请受权

    Java

    1

    2

    3

    ActivityCompat.requestPermissions(thisActivity,

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

                    MY_PERMISSIONS_REQUEST_READ_CONTACTS);


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

  4. 处理权限申请回调

    Java

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    18

    19

    20

    21

    @Override

    public void onRequestPermissionsResult(int requestCode,

            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 you need to do.

     

                } else {

     

                    // permission denied, boo! Disable the

                    // functionality that depends on this permission.

                }

                return;

            }

        }

    }


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

固然,到此咱们的权限申请的不走,基本介绍就如上述。不过还有个API值得提一下:

Java

1

2

3

4

5

6

7

8

// 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主要用于给用户一个申请权限的解释,该方法只有在用户在上一次已经拒绝过你的这个权限申请。也就是说,用户已经拒绝一次了,你又弹个受权框,你须要给用户一个解释,为何要受权,则使用该方法。

那么将上述几个步骤结合到一块儿就是:

Java

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

// Here, thisActivity is the current activity

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,

                new String[]{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.

    }

}

4、简单的例子

这里写一个简单的例子,针对于运行时权限。相信你们在最开始接触Android的时候,都利用Intent实验了打电话、发短信等功能。

咱们看看直接拨打电话在Android 6.x的设备上权限须要如何处理。

固然代码很是简单:

Java

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

package com.zhy.android160217;

 

import android.Manifest;

import android.content.Intent;

import android.content.pm.PackageManager;

import android.net.Uri;

import android.os.Bundle;

import android.support.v4.app.ActivityCompat;

import android.support.v4.content.ContextCompat;

import android.support.v7.app.AppCompatActivity;

import android.view.View;

import android.widget.Toast;

 

public class MainActivity extends AppCompatActivity

{

 

    private static final int MY_PERMISSIONS_REQUEST_CALL_PHONE = 1;

 

    @Override

    protected void onCreate(Bundle savedInstanceState)

    {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

    }

 

    public void testCall(View view)

    {

        if (ContextCompat.checkSelfPermission(this,

                Manifest.permission.CALL_PHONE)

                != PackageManager.PERMISSION_GRANTED)

        {

 

            ActivityCompat.requestPermissions(this,

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

                    MY_PERMISSIONS_REQUEST_CALL_PHONE);

        } else

        {

            callPhone();

        }

    }

 

    public void callPhone()

    {

        Intent intent = new Intent(Intent.ACTION_CALL);

        Uri data = Uri.parse("tel:" + "10086");

        intent.setData(data);

        startActivity(intent);

    }

 

    @Override

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

    {

 

        if (requestCode == MY_PERMISSIONS_REQUEST_CALL_PHONE)

        {

            if (grantResults[0] == PackageManager.PERMISSION_GRANTED)

            {

                callPhone();

            } else

            {

                // Permission Denied

                Toast.makeText(MainActivity.this, "Permission Denied", Toast.LENGTH_SHORT).show();

            }

            return;

        }

        super.onRequestPermissionsResult(requestCode, permissions, grantResults);

    }

}

在Android 6.x上运行是,点击testCall,即会弹出受权窗口,如何你Allow则直接拨打电话,若是Denied则Toast弹出”Permission Denied”。

例子很简单,不过须要注意的是,对于Intent这种方式,不少状况下是不须要受权的甚至权限都不须要的,好比:你是到拨号界面而不是直接拨打电话,就不须要去申请权限;打开系统图库去选择照片;调用系统相机app去牌照等。更多请参考Consider Using an Intent
.

固然,上例也说明了并不是全部的经过Intent的方式都不须要申请权限。通常状况下,你是经过Intent打开另外一个app,让用户经过该app去作一些事情,你只关注结果(onActivityResult),那么权限是不须要你处理的,而是由打开的app去处理。

5、封装

(1)申请权限

虽然权限处理并不复杂,可是须要编写不少重复的代码,因此目前也有不少库对用法进行了封装,你们能够在github首页搜索:android permission,对比了几个库的使用方式,发现https://github.com/lovedise/PermissionGen这个库据我所见相比较而言使用算是比较简单的,那么就以这个库的源码为基础来说解,你们有兴趣能够多看几个库的源码。

封装的代码很简单,正如你们的对上面的权限代码所见,没有特别复杂的地方。

对于申请权限的代码,本来的编写为:

Java

1

2

3

4

5

6

7

8

9

10

11

12

if (ContextCompat.checkSelfPermission(this,

                Manifest.permission.CALL_PHONE)

                != PackageManager.PERMISSION_GRANTED)

{

 

    ActivityCompat.requestPermissions(this,

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

            MY_PERMISSIONS_REQUEST_CALL_PHONE);

} else

{

    callPhone();

}

你们能够看到,对于其余的权限,其实申请的逻辑是相似的;惟一不一样的确定就是参数,那么看上面的代码,咱们须要3个参数:Activity|Fragment权限字符串数组int型申请码

也就是说,咱们只须要写个方法,接受这几个参数便可,而后逻辑是统一的。

Java

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

public static void needPermission(Fragment fragment, int requestCode, String[] permissions)

{

    requestPermissions(fragment, requestCode, permissions);

}

 

@TargetApi(value = Build.VERSION_CODES.M)

private static void requestPermissions(Object object, int requestCode, String[] permissions)

{

    if (!Utils.isOverMarshmallow())

    {

        doExecuteSuccess(object, requestCode);

        return;

    }

    List deniedPermissions = Utils.findDeniedPermissions(getActivity(object), permissions);

 

    if (deniedPermissions.size() > 0)

    {

        if (object instanceof Activity)

        {

            ((Activity) object).requestPermissions(deniedPermissions.toArray(new String[deniedPermissions.size()]), requestCode);

        } else if (object instanceof Fragment)

        {

            ((Fragment) object).requestPermissions(deniedPermissions.toArray(new String[deniedPermissions.size()]), requestCode);

        } else

        {

            throw new IllegalArgumentException(object.getClass().getName() + " is not supported");

        }

 

    } else

    {

        doExecuteSuccess(object, requestCode);

    }

}

Utils.findDeniedPermissions其实就是check没有受权的权限。

Java

1

2

3

4

5

6

7

8

9

10

11

12

13

14

#Utils

@TargetApi(value = Build.VERSION_CODES.M)

public static ListfindDeniedPermissions(Activity activity, String... permission)

{

    List denyPermissions = new ArrayList();

    for (String value : permission)

    {

        if (activity.checkSelfPermission(value) != PackageManager.PERMISSION_GRANTED)

        {

            denyPermissions.add(value);

        }

    }

    return denyPermissions;

}

那么上述的逻辑就很清晰了,须要的3种参数传入,先去除已经申请的权限,而后开始申请权限。

ok,我相信上面代码,你们扫一眼就能够了解了。

(2)处理权限回调

对于回调,由于要根据是否受权去执行不一样的事情,因此不少框架也须要些一连串的代码,或者和前面的申请代码耦合。不过这个框架仍是比较方便的,也是我选择它来说解的缘由。

回调主要作的事情:

  1. 了解是否受权成功。

  2. 根据受权状况进行回调。

对于第一条逻辑都同样;对于第二条,由于涉及到两个分支,每一个分支执行不一样的方法。

对于第二条,不少框架选择将两个分支的方法在申请权限的时候进行注册,而后在回调中根据requestCode进行匹配执行,不过这样须要在针对每次申请进行对象管理。

不过这个框架采起了一种颇有意思的作法,它利用注解去肯定须要执行的方法,存在两个注解:

Java

1

2

@PermissionSuccess(requestCode = 100)

@PermissionFail(requestCode = 100)

利用反射根据受权状况+requestCode便可找到注解标注的方法,而后直接执行便可。

大体的代码为:

Java

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

@Override public void onRequestPermissionsResult(int requestCode, String[] permissions,

      int[] grantResults) {

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

}

 

private static void requestResult(Object obj, int requestCode, String[] permissions,

                                  int[] grantResults)

{

    List deniedPermissions = new ArrayList();

    for (int i = 0; i if (grantResults[i] != PackageManager.PERMISSION_GRANTED)

        {

            deniedPermissions.add(permissions[i]);

        }

    }

 

    if (deniedPermissions.size() > 0)

    {

        doExecuteFail(obj, requestCode);

    } else

    {

        doExecuteSuccess(obj, requestCode);

    }

}

首先根据grantResults进行判断成功仍是失败,对于成功则:

Java

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

private static void doExecuteSuccess(Object activity, int requestCode)

{

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

            PermissionSuccess.class, requestCode);

 

    executeMethod(activity, executeMethod);

}

 

#Utils

public static  Method findMethodWithRequestCode(Class clazz,  Class annotation, int requestCode)

{

    for (Method method : clazz.getDeclaredMethods())

    {

        if (method.isAnnotationPresent(annotation))

        {

            if (isEqualRequestCodeFromAnntation(method, annotation, requestCode))

            {

                return method;

            }

        }

    }

    return null;

}

根据注解和requestCode找到方法,而后反射执行便可。失败的逻辑相似,不贴代码了。

ok,到此咱们的运行时权限相对于早起版本的变化、特色、以及如何处理和封装都介绍完了。

不过对于上面讲解的库,确定有人会说:运行时反射会影响效率,没错,不过我已经在上述代码的基础上将运行时注解改为Annotation Processor的方式了,即编译时注解,这样的话,就不存在反射损失效率的问题了。原本准备fork修改,而后PR,结果写完,改动太大,估计PR是不可能经过了,因此另起项目了,也方便后面的作一些扩展和维护。

详见库:https://github.com/hongyangAndroid/MPermissions.

6、MPermissions用法

对外的接口和PermissionGen基本一致,由于申请只须要三个参数,抛弃了使用本来类库的单例的方式,直接一个几个静态方法,简单整洁暴力。

贴一个用法:

Java

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

public class MainActivity extends AppCompatActivity

{

 

    private Button mBtnSdcard;

    private static final int REQUECT_CODE_SDCARD = 2;

 

    @Override

    protected void onCreate(Bundle savedInstanceState)

    {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

 

        mBtnSdcard = (Button) findViewById(R.id.id_btn_sdcard);

        mBtnSdcard.setOnClickListener(new View.OnClickListener()

        {

            @Override

            public void onClick(View v)

            {

                MPermissions.requestPermissions(MainActivity.this, REQUECT_CODE_SDCARD, Manifest.permission.WRITE_EXTERNAL_STORAGE);

            }

        });

    }

 

    @Override

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

    {

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

        super.onRequestPermissionsResult(requestCode, permissions, grantResults);

    }

 

    @PermissionGrant(REQUECT_CODE_SDCARD)

    public void requestSdcardSuccess()

    {

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

    }

 

    @PermissionDenied(REQUECT_CODE_SDCARD)

    public void requestSdcardFailed()

    {

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

    }

}

是否是简单明了~~对于onRequestPermissionsResult全部的Activity都是一致的,因此能够放到BaseActivity中去。此外,在Fragment中使用的方式一致,详见demo。

详见库:https://github.com/hongyangAndroid/MPermissions.

至于为何不直接介绍MPermission的源码,由于主要涉及到Annotation Processor,因此以这种方式引出,后面考虑单篇博文介绍下我目前所会的编译时注解的相关作法以及API的使用。

原文出处: 张鸿洋(@鸿洋_)

问啊-定制化IT教育平台,牛人一对一服务,有问必答,开发编程社交头条 官方网站:www.wenaaa.com


QQ群290551701 汇集不少互联网精英,技术总监,架构师,项目经理!开源技术研究,欢迎业内人士,大牛及新手有志于从事IT行业人员进入!

相关文章
相关标签/搜索