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行業人員進入!

相關文章
相關標籤/搜索