Android 6.0 運行時權限管理最佳實踐

這是一篇遲來的文章,Android M已經發布一年多了(6.0的變化),在Android M中權限系統被從新設計,發生了顛覆性的變化,不少人把握很差這個變化,一是對這個權限策略和套路尚未摸透,二是沒有一個很好的實踐來支撐,在個人技術開發羣裏不少人問我關於權限的問題,每每我都沒有直接回答,由於這個問題不是一兩句說的清楚的,這幾點是今天我寫這篇文章的緣由。這裏有一切關於Android運行時權限你須要知道的,包括如何在代碼中實現,若是你之前不知道這些東西,如今來看也爲時不晚,我將在詳解以後給你一個最佳的實踐方案。html

運行時權限開源庫AndPermission:github.com/yanzhenjie/…java

若是你的英文夠好,推薦你閱讀官網的文章: android

正開始開始以前來幾張個人實例圖:git

  • Activity/Fragment中申請單個權限
    Activity/Fragment中申請單個權限github

  • Activity/Fragment中同時申請多個權限
    Activity/Fragment中同時申請多個權限shell

  • Activity/Fragment中被用戶拒絕後,下次申請時提醒用戶
    Activity/Fragment中被用戶拒絕後,下次申請時提醒用戶數組


關於運行時權限

在舊的權限管理系統中,權限僅僅在App安裝時詢問用戶一次,用戶贊成了這些權限App才能被安裝(某些深度定製系統另說),App一旦安裝後就能夠偷偷的作一些鮮爲人知的事情了。微信

在Android6.0開始,App能夠直接安裝,App在運行時一個一個詢問用戶授予權限,系統會彈出一個對話框讓用戶選擇是否受權某個權限給App(這個Dialog不能由開發者定製),當App須要用戶授予不恰當的權限的時候,用戶能夠拒絕,用戶也能夠在設置頁面對每一個App的權限進行管理。網絡

特別注意:這個對話框不是開發者調用某個權限的功能時由系統自動彈出,而是須要開發者手動調用,若是你直接調用而沒有去申請權限的話,將會致使App奔潰。異步

也許你已經開始慌了,這對於用戶來講是好事,可是對於開發者來講咱們不能直接調用方法了,咱們不得不在每個須要權限的地方檢查並請求用戶受權,因此就引出瞭如下兩個問題。

哪些權限須要動態申請

新的權限策略講權限分爲兩類,第一類是不涉及用戶隱私的,只須要在Manifest中聲明便可,好比網絡、藍牙、NFC等;第二類是涉及到用戶隱私信息的,須要用戶受權後纔可以使用,好比SD卡讀寫、聯繫人、短信讀寫等。

Normal Permissions

此類權限都是正常保護的權限,只須要在AndroidManifest.xml中簡單聲明這些權限便可,安裝即受權,不須要每次使用時都檢查權限,並且用戶不能取消以上受權,除非用戶卸載App。

  • 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_IGNORE_BATTERY_OPTIMIZATIONS
  • 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

全部危險的Android系統權限屬於權限組,若是APP運行在Android 6.0 (API level 23)或者更高級別的設備中,並且targetSdkVersion>=23時,系統將會自動採用動態權限管理策略,若是你在涉及到特殊權限操做時沒有作動態權限的申請將會致使App崩潰,所以你須要注意:

  • 此類權限也必須在Manifest中申明,不然申請時不提使用用戶,直接回調開發者權限被拒絕。
  • 同一個權限組的任何一個權限被受權了,這個權限組的其餘權限也自動被受權。例如,一旦WRITE_CONTACTS被受權了,App也有READ_CONTACTSGET_ACCOUNTS了。
  • 申請某一個權限的時候系統彈出的Dialog是對整個權限組的說明,而不是單個權限。例如我申請READ_EXTERNAL_STORAGE,系統會提示"容許xxx訪問設備上的照片、媒體內容和文件嗎?"

若是App運行在Android 5.1 (API level 22)或者更迭級別的設備中,或者targetSdkVersion<=22時(此時設備能夠是Android 6.0 (API level 23)或者更高),在全部系統中仍將採用舊的權限管理策略,系統會要求用戶在安裝的時候授予權限。其次,系統就告訴用戶App須要什麼權限組,而不是個別的某個權限。

  • CALENDAR(日曆)
    • READ_CALENDAR
    • WRITE_CALENDAR
  • CAMERA(相機)
    • CAMERA
  • CONTACTS(聯繫人)
    • READ_CONTACTS
    • WRITE_CONTACTS
    • GET_ACCOUNTS
  • LOCATION(位置)
    • ACCESS_FINE_LOCATION
    • ACCESS_COARSE_LOCATION
  • MICROPHONE(麥克風)
    • RECORD_AUDIO
  • PHONE(手機)
    • READ_PHONE_STATE
    • CALL_PHONE
    • READ_CALL_LOG
    • WRITE_CALL_LOG
    • ADD_VOICEMAIL
    • USE_SIP
    • PROCESS_OUTGOING_CALLS
  • SENSORS(傳感器)
    • BODY_SENSORS
  • SMS(短信)
    • SEND_SMS
    • RECEIVE_SMS
    • READ_SMS
    • RECEIVE_WAP_PUSH
    • RECEIVE_MMS
  • STORAGE(存儲卡)
    • READ_EXTERNAL_STORAGE
    • WRITE_EXTERNAL_STORAGE

使用adb命令能夠查看這些須要受權的權限組:

adb shell pm list permissions -d -g複製代碼

使用adb命令一樣能夠受權/撤銷某個權限:

adb shell pm [grant|revoke] 
  
  
  

 
  
  ... 

 複製代碼

關於運行時權限的一些建議

  1. 只請求你須要的權限,減小請求的次數,或用Intent來代替,讓其餘的應用來處理。

    1. 若是你使用Intent,你不須要設計界面,由第三方的應用來完成全部操做。好比打電話、選擇圖片等。
    2. 若是你請求權限,你能夠徹底控制用戶體驗,本身定義UI。可是用戶也能夠拒絕權限,就意味着你的應用不能執行這個特殊操做。
  2. 防止一次請求太多的權限或請求次數太多,用戶可能對你的應用感到厭煩,在應用啓動的時候,最好先請求應用必須的一些權限,非必須權限在使用的時候才請求,建議整理並按照上述分類管理本身的權限:

    1. 普通權限(Normal PNermissions):只須要在Androidmanifest.xml中聲明相應的權限,安裝即許可。
    2. 須要運行時申請的權限(Dangerous Permissions):
    • 必要權限:最好在應用啓動的時候,進行請求許可的一些權限(主要是應用中主要功能須要的權限)。
    • 附帶權限:不是應用主要功能須要的權限(如:選擇圖片時,須要讀取SD卡權限)。
  3. 解釋你的應用爲何須要這些權限:在你調用requestPermissions()以前,你爲何須要這個權限。

    1. 例如,一個攝影的App可能須要使用定位服務,由於它須要用位置標記照片。通常的用戶可能會不理解,他們會困惑爲何他們的App想要知道他的位置。因此在這種狀況下,因此你須要在requestpermissions()以前告訴用戶你爲何須要這個權限。
  4. 使用兼容庫support-v4中的方法

    ContextCompat.checkSelfPermission()
    ActivityCompat.requestPermissions()
    ActivityCompat.shouldShowRequestPermissionRationale()複製代碼

幾個重要的方法與常量解釋

  • PackageManager中的兩個常量:

    • PackageManager.PERMISSION_DENIED:該權限是被拒絕的。
    • PackageManager.PERMISSION_GRANTED:該權限是被受權的。
  • Activity中或者Fragment都會有如下幾個方法:

    int checkSelfPermission(String) void requestPermissions(int, String...) boolean shouldShowRequestPermissionRationale(String) void onRequestPermissionsResult()複製代碼

上述四個方法中,前三個方法在support-v4ActivityCompat中都有,建議使用兼容庫中的方法。最後一個方法是用戶受權或者拒絕某個權限組時系統會回調Activity或者Fragment中的方法。

checkSelfPermission() 檢查權限

  1. 檢查某一個權限的當前狀態,你應該在請求某個權限時檢查這個權限是否已經被用戶受權,已經受權的權限重複申請可能會讓用戶產生厭煩。
  2. 該方法有一個參數是權限名稱,有一個int的返回值,用這個值與上面提到的兩個常量作比較可判斷檢查的權限當前的狀態。
    if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS)
         != PackageManager.PERMISSION_GRANTED) {
     // 沒有權限,申請權限。
    }else{
     // 有權限了,去放肆吧。
    }複製代碼

requestPermissions() 申請權限

  1. 請求用戶受權幾個權限,調用後系統會顯示一個請求用戶受權的提示對話框,App不能配置和修改這個對話框,若是須要提示用戶這個權限相關的信息或說明,須要在調用 requestPermissions() 以前處理,該方法有兩個參數:
  • int requestCode,會在回調onRequestPermissionsResult()時返回,用來判斷是哪一個受權申請的回調。
  • String[] permissions,權限數組,你須要申請的的權限的數組。
  1. 因爲該方法是異步的,因此無返回值,當用戶處理完受權操做時,會回調Activity或者Fragment的onRequestPermissionsResult()方法。
    ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.READ_CONTACTS}, MMM);複製代碼

onRequestPermissionsResult() 處理權限結果回調

  1. 該方法在Activity/Fragment中應該被重寫,當用戶處理完受權操做時,系統會自動回調該方法,該方法有三個參數:
  • int requestCode,在調用requestPermissions()時的第一個參數。
  • String[] permissions,權限數組,在調用requestPermissions()時的第二個參數。
  • int[] grantResults,受權結果數組,對應permissions,具體值和上方提到的PackageManager中的兩個常量作比較。
    @Override
    public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
      switch (requestCode) {
          case MMM: {
              if (grantResults.length > 0
                  && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                  // 權限被用戶贊成,能夠去放肆了。
              } else {
                  // 權限被用戶拒絕了,洗洗睡吧。
              }
              return;
          }
      }
    }複製代碼

shouldShowRequestPermissionRationale()

  1. 望文生義,是否應該顯示請求權限的說明。
  2. 第一次請求權限時,用戶拒絕了,調用shouldShowRequestPermissionRationale()後返回true,應該顯示一些爲何須要這個權限的說明。
  3. 用戶在第一次拒絕某個權限後,下次再次申請時,受權的dialog中將會出現「再也不提醒」選項,一旦選中勾選了,那麼下次申請將不會提示用戶。
  4. 第二次請求權限時,用戶拒絕了,並選擇了「不在提醒」的選項,調用shouldShowRequestPermissionRationale()後返回false。
  5. 設備的策略禁止當前應用獲取這個權限的受權:shouldShowRequestPermissionRationale()返回false 。
  6. 加這個提醒的好處在於,用戶拒絕過一次權限後咱們再次申請時能夠提醒該權限的重要性,面得再次申請時用戶勾選「再也不提醒」並決絕,致使下次申請權限直接失敗。

綜上所述,整合代碼後:

if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {// 沒有權限。
    if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.READ_CONTACTS)) {
            // 用戶拒絕過這個權限了,應該提示用戶,爲何須要這個權限。
    } else {
        // 申請受權。
        ActivityCompat.requestPermissions(thisActivity, new String[]{Manifest.permission.READ_CONTACTS}, MMM);
    }
}

...

@Override
public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
    switch (requestCode) {
        case MMM: {
            if (grantResults.length > 0
                && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                // 權限被用戶贊成,能夠去放肆了。
            } else {
                // 權限被用戶拒絕了,洗洗睡吧。
            }
            return;
        }
    }
}複製代碼

運行時權限最佳實踐的套路

整體下來咱們應該對運行時權限有一個系統的認識,我總結出了一些套路:

  1. 須要區分各類Normal Permissioin和Dangerous Permissions。
  2. 判斷多個權限受權回調時須要判斷每個權限是否全都是被受權了,不然操做不能繼續。
  3. 須要請求多個權限時須要挨個檢查是否已經被受權過,沒受權的纔去請求,還要檢查這些權限是否須要提示用戶,若是多個權限都須要提示,該如何處理。
  4. 上述1 2 3若是在須要在多個頁面 實現,代碼重複。
  5. ...

其實問題遠遠不止這些,認真看過文章的人應該會發現,實現代碼比較簡單,可是代碼重複加上須要咱們考慮和注意的細節太多了,那麼下面我就爲你們介紹一個開源內褲來解決這一系列問題。

AndPermission

這個開源庫名叫AndPermission:github.com/yanzhenjie/…,通過個人實踐是徹底解決了上述問題,推薦你們使用,有興趣的朋友能夠去star下。

  • AndroidStudio使用方法,gradle一句話遠程依賴

    compile 'com.yanzhenjie:permission:1.0.0'複製代碼

    Or Maven:

    <dependency>
    <groupId>com.yanzhenjie</groupId>
    <artifactId>permission</artifactId>
    <version>1.0.0</version>
    <type>pom</type>
    </dependency>複製代碼
  • Eclipse 下載jar包,或者下載源碼

使用介紹

一、申請權限就是這麼簡單

AndPermission.with(this)
    .requestCode(101)
    .permission(Manifest.permission.WRITE_CONTACTS,
        Manifest.permission.READ_SMS,
        Manifest.permission.WRITE_EXTERNAL_STORAGE)
    .send();複製代碼

只須要在Activity中或者Fragment中直接調用便可,AndPermission自動爲你打理好後宮。

二、接授權限回調更簡單 只須要重寫Activity/Fragment的一個方法,而後提供一個受權時回調的方法便可:

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    // 只須要調用這一句,剩下的AndPermission自動完成。
    AndPermission.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
}

// 成功回調的方法,用註解便可,裏面的數字是請求時的requestCode。
@PermissionYes(100)
private void getLocationYes() {
    // 申請權限成功,能夠去作點什麼了。
    Toast.makeText(this, "獲取定位權限成功", Toast.LENGTH_SHORT).show();
}

// 失敗回調的方法,用註解便可,裏面的數字是請求時的requestCode。
@PermissionNo(100)
private void getLocationNo() {
    // 申請權限失敗,能夠提醒一下用戶。
    Toast.makeText(this, "獲取定位權限失敗", Toast.LENGTH_SHORT).show();
}複製代碼

只須要上面這麼幾句話便可,你就能夠大刀闊斧的幹了,在總結中提到的各類判斷、複雜的狀況AndPermission自動完成。

三、若是你須要在用戶屢次拒絕權限後提示用戶

AndPermission.with(this)
    .requestCode(101)
    .permission(Manifest.permission.WRITE_CONTACTS,
        Manifest.permission.READ_SMS,
        Manifest.permission.WRITE_EXTERNAL_STORAGE)
    .rationale(mRationaleListener)
    .send();

private RationaleListener mRationaleListener = new RationaleListener() {
    @Override
    public void showRequestPermissionRationale(int requestCode, final Rationale rationale) {
        new AlertDialog.Builder(RationalePermissionActivity.this)
            .setTitle("友好提醒")
            .setMessage("沒有定位權限將不能爲您推薦附近妹子,請把定位權限賜給我吧!")
            .setPositiveButton("好,給你", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    dialog.cancel();
                    rationale.resume();// 用戶贊成繼續申請。
                }
            })
            .setNegativeButton("我拒絕", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    dialog.cancel();
                    rationale.cancel(); // 用戶拒絕申請。
                }
        }).show();
    }
};複製代碼

這麼作的好處請看上面shouldShowRequestPermissionRationale()方法的介紹。

基本上就到這裏啦,你們有疑問能夠關注個人微信,微信公衆號搜索:嚴振杰,便可關注我。

相關文章
相關標籤/搜索