Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android 6.0及以上版本的運行時權限介紹

Android 6.0及以上版本的運行時權限介紹

編輯:關於Android編程

運行時權限(Runtime Permission)是Android 6.0( 代號為 Marshmallow,API版本為 23)及以上版本新增的功能,相比於以往版本,這是一個較大變化。本文將介紹如何在代碼中加入並配置運行時權限功能。


如需閱讀英文原文,請您點擊這個鏈接:《Everything every Android Developer must know about new Android’s Runtime Permission》。

如需閱讀官方運行時權限的相關介紹,請您點擊這個鏈接:《Working with System Permissions》


運行時權限介紹

一直以來,為了保證最大的安全性,安裝Android應用時,系統總是讓用戶選擇是否同意該應用所需的所有權限。一旦安裝應用,就意味著該應用所需的所有權限均已獲得。若在使用某個功能時用到了某個權限,系統將不會提醒用戶該權限正在被獲取(比如微信需要使用攝像頭拍照,在Android 6.0以前的設備上,用戶將不會被系統告知正在使用“使用系統攝像頭”的權限)。


這在安全性上是個隱患:在不經用戶同意的情況下,一些應用在後台可以自由地收集用戶隱私信息而不被用戶察覺。


從Android 6.0版本開始,這個隱患終於被消除了:在安裝應用時,該應用無法取得任何權限!相反,在使用應用的過程中,若某個功能需要獲取某個權限,系統會彈出一個對話框,顯式地由用戶決定是否將該權限賦予應用,只有得到了用戶的許可,該功能才可以被使用。


這裡寫圖片描述


需要注意的是,在上述的右圖中,對話框並不會自動彈出,而需要由開發者手動調用。若程序調用的某個方法需要用戶賦予相應權限,而此時該權限並未被賦予時,那麼程序就會拋出異常並崩潰(Crash),如下圖所示。<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4KCjxocj4KCjxwPjxpbWcgc3JjPQ=="/uploadfile/Collfiles/20160413/20160413100413482.jpg" alt="這裡寫圖片描述" title="\">


除此之外,用戶還可以在任何時候撤銷賦予過的權限。


這裡寫圖片描述


運行時權限無疑提升了安全性,有效地保護了用戶的隱私,這對於用戶來說確實是個好消息,但對於開發者來說簡直就是噩夢:因為這需要開發者在調用方法時,檢查該方法使用了什麼系統權限——這仿佛顛覆了傳統的編程的邏輯——開發者編寫每一句代碼時都得小心翼翼,否則應用可能隨時崩潰。


在程序中,設置目標SDK版本(targetSDKVersion)為23及以上時(這意味著程序可以在Android 6.0及以上的版本中運行),將應用安裝在Android 6.0及以上機型中,運行時權限功能才能生效;若將其安裝在Android 6.0以前的機型中,權限檢查仍將僅僅發生在安裝應用時。


運行時權限與各版本間的兼容性問題

假如將一個早期版本的應用安裝在Android 6.0版本的機型上,應用是不會崩潰的,因為這只有兩種情況:1)該應用的targetSDKVersion < 23,在這種情況下,權限檢查仍是早期的形式(僅在安裝時賦予權限,使用時將不被提醒);2)該應用的targetSDKVersion ≥ 23時,則將使用新的運行時權限規則。


這裡寫圖片描述


所以,這個早期版本的應用將運行如常。不過,將該應用安裝在Android 6.0上,且targetSDKVersion ≥ 23時,用戶仍然可以隨時手動撤銷權限,當然這種做法不被官方推薦。


這裡寫圖片描述


不被推薦的原因是,這種做法容易導致應用崩潰。若targetSDKVersion < 23,當然不會出問題;若早期應用的targetSDKVersion ≥ 23,在使用應用時手動撤消了某個權限,那麼程序在調用了需要這個權限才能執行的方法時,應用什麼也不做,若該方法還有返回值,那麼會根據實際情況返回 0 或者 null。如下圖所示。


這裡寫圖片描述


若上述調用的方法沒有崩潰,那麼這個方法被其他方法調用時也會因為返回值是 0 或者 null 而崩潰。


不過好消息是,用戶幾乎不會手動撤銷已經賦予給應用的權限。


說了這麼多,在避免應用崩潰的前提下,適配新的運行時權限功能才是王道:對於那些在代碼中並未支持運行時權限的應用,請將targetSDKVersion設置為 < 23,否則應用有崩潰隱患;若代碼中支持了運行時權限,再將targetSDKVersion設置為 ≥ 23。


請注意:在Android Studio中新建Project時,會自動賦予targetSDKVersion為最新版本,若您的應用還暫時無法完全支持運行時權限功能,建議首先將targetSDKVersion手動設置為22。


自動賦予應用的權限

以下羅列了在安裝應用時,自動賦予應用的權限,這些權限無法在安裝後手動撤銷。我們稱其為基本權限(Normal Permission):

android.permission.ACCESS_LOCATION_EXTRA_COMMANDS
android.permission.ACCESS_NETWORK_STATE
android.permission.ACCESS_NOTIFICATION_POLICY
android.permission.ACCESS_WIFI_STATE
android.permission.ACCESS_WIMAX_STATE
android.permission.BLUETOOTH
android.permission.BLUETOOTH_ADMIN
android.permission.BROADCAST_STICKY
android.permission.CHANGE_NETWORK_STATE
android.permission.CHANGE_WIFI_MULTICAST_STATE
android.permission.CHANGE_WIFI_STATE
android.permission.CHANGE_WIMAX_STATE
android.permission.DISABLE_KEYGUARD
android.permission.EXPAND_STATUS_BAR
android.permission.FLASHLIGHT
android.permission.GET_ACCOUNTS
android.permission.GET_PACKAGE_SIZE
android.permission.INTERNET
android.permission.KILL_BACKGROUND_PROCESSES
android.permission.MODIFY_AUDIO_SETTINGS
android.permission.NFC
android.permission.READ_SYNC_SETTINGS
android.permission.READ_SYNC_STATS
android.permission.RECEIVE_BOOT_COMPLETED
android.permission.REORDER_TASKS
android.permission.REQUEST_INSTALL_PACKAGES
android.permission.SET_TIME_ZONE
android.permission.SET_WALLPAPER
android.permission.SET_WALLPAPER_HINTS
android.permission.SUBSCRIBED_FEEDS_READ
android.permission.TRANSMIT_IR
android.permission.USE_FINGERPRINT
android.permission.VIBRATE
android.permission.WAKE_LOCK
android.permission.WRITE_SYNC_SETTINGS
com.android.alarm.permission.SET_ALARM
com.android.launcher.permission.INSTALL_SHORTCUT
com.android.launcher.permission.UNINSTALL_SHORTCUT

開發者僅需要在AndroidManifest.xml中聲明這些權限,應用就能自動獲取無需用戶授權。


為應用適配新的運行時權限


為了設配新的運行時權限,首先需要將compileSdkVersiontargetSdkVersion設置為23:

android {
    compileSdkVersion 23
    ...

    defaultConfig {
        ...
        targetSdkVersion 23
        ...
    }

下面演示了一個增加聯系人的方法,該方法是需使用WRITE_CONTACTS的權限:

private static final String TAG = "Contacts";
private void insertDummyContact() {
    // Two operations are needed to insert a new contact.
    ArrayList operations = new ArrayList(2);

    // 1、設置一個新的聯系人
    ContentProviderOperation.Builder op =
            ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
                    .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, null)
                    .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null);
    operations.add(op.build());

    // 1、為聯系人設置姓名
    op = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
            .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
            .withValue(ContactsContract.Data.MIMETYPE,
                    ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
            .withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME,
                    "__DUMMY CONTACT from runtime permissions sample");
    operations.add(op.build());

    // 3、使用ContentResolver添加該聯系人
    ContentResolver resolver = getContentResolver();
    try {
        resolver.applyBatch(ContactsContract.AUTHORITY, operations);
    } catch (RemoteException e) {
        Log.d(TAG, "Could not add a new contact: " + e.getMessage());
    } catch (OperationApplicationException e) {
        Log.d(TAG, "Could not add a new contact: " + e.getMessage());
    }
}

調用這個方法需要配置WRITE_CONTACTS權限,否則應用將崩潰:在AndroidManifest.xml中配置如下權限:


接著,我們需要創建一個方法用於判斷WRITE_CONTACTS權限是否確實被賦予;若方法為創建,那麼可以彈出一個對話框向用戶申請該權限。待權限被賦予後,方可新建聯系人。


權限被歸類成權限組(Permission Group),如下表所示:


這裡寫圖片描述


若應用被賦予了某個權限組中的一個權限(比如READ_CONTACTS權限被賦予),那麼該組中的其他權限將被自動獲取(WRITE_CONTACTSGET_ACCOUNTS權限被自動獲取)。


檢查和申請權限的方法分別是Activity.checkSelfPermission()Activity.requestPermissions,這兩個方法是在 API 23 中新增的。


final private int REQUEST_CODE_ASK_PERMISSIONS = 123;

private void insertDummyContactWrapper() {
    //檢查AndroidManiFest中是否配置了WRITE_CONTACTS權限
    int hasWriteContactsPermission = checkSelfPermission(Manifest.permission.WRITE_CONTACTS);
    //若未配置該權限
    if (hasWriteContactsPermission != PackageManager.PERMISSION_GRANTED) {
        //申請配置該權限
        requestPermissions(new String[] {Manifest.permission.WRITE_CONTACTS},
                REQUEST_CODE_ASK_PERMISSIONS);
        //直接返回,不執行insertDummyContact()方法
        return;
    }
    //若配置了該權限,才能調用方法
    insertDummyContact();
}

若程序賦予了權限,insertDummyContact()方法將被調用;否則,requestPermissions()方法將彈出一個對話框申請權限,如下所示:


這裡寫圖片描述


無論您選擇的是“DENY”還是“ALLOW”,程序都將回調Activity.onRequestPermissionsResult()方法,並將選擇的結果傳到方法的第三個參數中:

@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
    switch (requestCode) {
        case REQUEST_CODE_ASK_PERMISSIONS:
            if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                // 用戶選擇了“ALLOW”,獲取權限,調用方法
                insertDummyContact();
            } else {
                // 用戶選擇了“DENY”,未獲取權限
                Toast.makeText(MainActivity.this, "WRITE_CONTACTS Denied", Toast.LENGTH_SHORT)
                        .show();
            }
            break;
        default:
            super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }
}

這就是Android 6.0的全新運行時權限機制,為了提高安全性,增加代碼量在所難免:為了匹配運行時權限機制,必須把處理方法的所有情況考慮在內。


處理 “不再詢問”(“Never Ask Again”)


每當系統申請權限時,彈出的對話框會有一個“不再詢問”(“Never Ask Again”)的勾選項。
若用戶打了勾,並選擇拒絕(“DENY”),那麼下次程序調用Activity。requestPermissions()方法時,將不會彈出對話框,權限也不會被賦予。
這種沒有反饋的交互並不是一個好的用戶體驗(User Experience)。所以,下次啟動時,程序應彈出一個對話框,提示用戶“您已經拒絕了使用該功能所需要的權限,若需要使用該功能,請手動開啟權限”,應調用Activity.shouldShowRequestPermissionRationale()方法:


final private int REQUEST_CODE_ASK_PERMISSIONS = 123;

private void insertDummyContactWrapper() {
    int hasWriteContactsPermission = checkSelfPermission(Manifest.permission.WRITE_CONTACTS);
    if (hasWriteContactsPermission != PackageManager.PERMISSION_GRANTED) {
            if (!shouldShowRequestPermissionRationale(Manifest.permission.WRITE_CONTACTS)) {
                showMessageOKCancel("You need to allow access to Contacts",
                        new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                requestPermissions(new String[] {Manifest.permission.WRITE_CONTACTS},
                                        REQUEST_CODE_ASK_PERMISSIONS);
                            }
                        });
                return;
            }
        requestPermissions(new String[] {Manifest.permission.WRITE_CONTACTS},
                REQUEST_CODE_ASK_PERMISSIONS);
        return;
    }
    insertDummyContact();
}

private void showMessageOKCancel(String message, DialogInterface.OnClickListener okListener) {
    new AlertDialog.Builder(MainActivity.this)
            .setMessage(message)
            .setPositiveButton("OK", okListener)
            .setNegativeButton("Cancel", null)
            .create()
            .show();
}

效果如下:
這裡寫圖片描述


上述對話框應在兩種情形下彈出:
1)應用第一次申請權限時;

2)用戶勾選了“不再詢問”復選框。


對於第二種情況,Activity.onRequestPermissionsResult()方法將被回調,並回傳參數PERMISSION_DENIED,該對話框將不再彈出。


一次性申請多個權限


有些功能需要申請多個權限,仍然可以像上述方式一樣編寫代碼:

final private int REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS = 124;

private void insertDummyContactWrapper() {
    //提示用戶需要手動開啟的權限集合
    List permissionsNeeded = new ArrayList();

    //功能所需權限的集合
    final List permissionsList = new ArrayList();
    //若用戶拒絕了該權限申請,則將該申請的提示添加到“用戶需要手動開啟的權限集合”中
    if (!addPermission(permissionsList, Manifest.permission.ACCESS_FINE_LOCATION))
        permissionsNeeded.add("GPS");
    if (!addPermission(permissionsList, Manifest.permission.READ_CONTACTS))
        permissionsNeeded.add("Read Contacts");
    if (!addPermission(permissionsList, Manifest.permission.WRITE_CONTACTS))
        permissionsNeeded.add("Write Contacts");

    //若在AndroidManiFest中配置了所有所需權限,則讓用戶逐一賦予應用權限,若權限都被賦予,則執行方法並返回
    if (permissionsList.size() > 0) {
        //若用戶賦予了一部分權限,則需要提示用戶開啟其余權限並返回,該功能將無法執行
        if (permissionsNeeded.size() > 0) {
            // Need Rationale
            String message = "You need to grant access to " + permissionsNeeded.get(0);
            for (int i = 1; i < permissionsNeeded.size(); i++)
                message = message + ", " + permissionsNeeded.get(i);
            //彈出對話框,提示用戶需要手動開啟的權限
            showMessageOKCancel(message,
                    new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            requestPermissions(permissionsList.toArray(new String[permissionsList.size()]),
                                    REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS);
                        }
                    });
            return;
        }
        requestPermissions(permissionsList.toArray(new String[permissionsList.size()]),
                REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS);
        return;
    }

    insertDummyContact();
}
//判斷用戶是否授予了所需權限 
private boolean addPermission(List permissionsList, String permission) {
    //若配置了該權限,返回true
    if (checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
        //若未配置該權限,將其添加到所需權限的集合,返回true
        permissionsList.add(permission);
        // 若用戶勾選了“永不詢問”復選框,並拒絕了權限,則返回false
        if (!shouldShowRequestPermissionRationale(permission))
            return false;
    }
    return true;
}

當用戶設置了每個權限是否可被賦予後,Activity.onRequestPermissionsResult()方法被回調,並傳入第三個參數:


@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
    switch (requestCode) {
        case REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS:
            {
            //初始化Map集合,其中Key存放所需權限,Value存放該權限是否被賦予
            Map perms = new HashMap();
            // 向Map集合中加入元素,初始時所有權限均設置為被賦予(PackageManager.PERMISSION_GRANTED)
            perms.put(Manifest.permission.ACCESS_FINE_LOCATION, PackageManager.PERMISSION_GRANTED);
            perms.put(Manifest.permission.READ_CONTACTS, PackageManager.PERMISSION_GRANTED);
            perms.put(Manifest.permission.WRITE_CONTACTS, PackageManager.PERMISSION_GRANTED);
            // 將第二個參數回傳的所需權限及第三個參數回傳的權限結果放入Map集合中,由於Map集合要求Key值不能重復,所以實際的權限結果將覆蓋初始值
            for (int i = 0; i < permissions.length; i++)
                perms.put(permissions[i], grantResults[i]);
            // 若所有權限均被賦予,則執行方法
            if (perms.get(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
                    && perms.get(Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED
                    && perms.get(Manifest.permission.WRITE_CONTACTS) == PackageManager.PERMISSION_GRANTED) {
                // All Permissions Granted
                insertDummyContact();
            } 
            //否則彈出toast,告知用戶需手動賦予權限
            else {
                // Permission Denied
                Toast.makeText(MainActivity.this, "Some Permission is Denied", Toast.LENGTH_SHORT)
                        .show();
            }
            }
            break;
        default:
            super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }
}

使用支持庫(Support Library)提高程序的兼容性


盡管上述代碼在Android 6.0版本的設備上能夠正常運行,但運行在早前版本的設備上,程序將崩潰。
簡單直接的方式是事先進行版本判斷:


if (Build.VERSION.SDK_INT >= 23) {
    // Marshmallow+
} else {
    // Pre-Marshmallow
}

但這樣會使程序變得臃腫。

比較好的解決方式是使用Support Library v4支持庫中的方法替換原來的方法,這將省去為不同版本的設備分別提供代碼的麻煩:


// 將Activity.checkSelfPermission()方法替換為如下方法
ContextCompat.checkSelfPermission()

// 將Activity.requestPermissions()方法替換為如下方法
ActivityCompat.requestPermissions()

//將Activity.shouldShowRequestPermissionRationale()方法替換為如下方法,在早期版本中,該方法直接返回false
ActivityCompat.shouldShowRequestPermissionRationale() 

無論哪個版本,調用上面的三個方法都需要Content或Activity參數。


以下是使用Support Library v4支持庫中的方法替換原代碼中相應方法後的程序:


private void insertDummyContactWrapper() {
    int hasWriteContactsPermission = ContextCompat.checkSelfPermission(MainActivity.this,
            Manifest.permission.WRITE_CONTACTS);
    if (hasWriteContactsPermission != PackageManager.PERMISSION_GRANTED) {
        if (!ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this,
                Manifest.permission.WRITE_CONTACTS)) {
            showMessageOKCancel("You need to allow access to Contacts",
                    new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            ActivityCompat.requestPermissions(MainActivity.this,
                                    new String[] {Manifest.permission.WRITE_CONTACTS},
                                    REQUEST_CODE_ASK_PERMISSIONS);
                        }
                    });
            return;
        }
        ActivityCompat.requestPermissions(MainActivity.this,
                new String[] {Manifest.permission.WRITE_CONTACTS},
                REQUEST_CODE_ASK_PERMISSIONS);
        return;
    }
    insertDummyContact();
}

需要注意的是,若程序中用到了Fragment,也最好使用android.support.v4.app.Fragment,這樣可以兼容更低的版本,使應用適配更多設備。


使用第三方開源庫(3rd Party Library)簡化代碼


為了是代碼更加簡潔,推薦一個第三方框架。該框架可以方便地集成運行時權限機制並有效兼容新舊版本。


在應用打開時撤銷權限所帶來的問題


如上所述,用戶可以隨時撤銷賦予應用的權限,若某個應用正在運行時,用戶撤消了其某些權限,應用所在進程會立刻終止(application’s process is suddenly terminated),所以盡量不要在應用運行時,改變其權限規則。


總結與建議

總結:

運行時權限機制大大提高了應用的安全性,不過開發者需要為此修改代碼以匹配新的版本,不過好消息是,大部分常用的權限都被自動賦予了,所以,只有很小一部分代碼需要修改。


建議:

使用運行時機制時應該以版本的兼容作為前提。

不要將未適配運行時機制的程序的targetSdkVersion設置為 23 及以上。


感謝

特別感謝原創作者的付出,下面是作者的介紹信息:

這裡寫圖片描述

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved