這是一篇遲來的文章,Android M已經發布一年多了(6.0的變化),在Android M中權限系統被重新設計,發生了顛覆性的變化,很多人把握不好這個變化,一是對這個權限策略和套路還沒有摸透,二是沒有一個很好的實踐來支撐,在我的技術開發羣裏很多人問我關於權限的問題,往往我都沒有直接回答,因爲這個問題不是一兩句說的清楚的,這幾點是今天我寫這篇文章的原因。這裏有一切關於Android運行時權限你需要知道的,包括如何在代碼中實現,如果你以前不知道這些東西,現在來看也爲時不晚,我將在詳解之後給你一個最佳的實踐方案。
運行時權限開源庫AndPermission:https://github.com/yanzhenjie/AndPermission。
如果你的英文夠好,推薦你閱讀官網的文章:
正開始開始之前來幾張我的實例圖:
-
Activity/Fragment中申請單個權限
-
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_CONTACTS
和GET_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] ...
關於運行時權限的一些建議
-
只請求你需要的權限,減少請求的次數,或用Intent來代替,讓其他的應用來處理。
- 如果你使用Intent,你不需要設計界面,由第三方的應用來完成所有操作。比如打電話、選擇圖片等。
- 如果你請求權限,你可以完全控制用戶體驗,自己定義UI。但是用戶也可以拒絕權限,就意味着你的應用不能執行這個特殊操作。
-
防止一次請求太多的權限或請求次數太多,用戶可能對你的應用感到厭煩,在應用啓動的時候,最好先請求應用必須的一些權限,非必須權限在使用的時候才請求,建議整理並按照上述分類管理自己的權限:
- 普通權限(Normal PNermissions):只需要在Androidmanifest.xml中聲明相應的權限,安裝即許可。
- 需要運行時申請的權限(Dangerous Permissions):
- 必要權限:最好在應用啓動的時候,進行請求許可的一些權限(主要是應用中主要功能需要的權限)。
- 附帶權限:不是應用主要功能需要的權限(如:選擇圖片時,需要讀取SD卡權限)。
-
解釋你的應用爲什麼需要這些權限:在你調用
requestPermissions()
之前,你爲什麼需要這個權限。- 例如,一個攝影的App可能需要使用定位服務,因爲它需要用位置標記照片。一般的用戶可能會不理解,他們會困惑爲什麼他們的App想要知道他的位置。所以在這種情況下,所以你需要在
requestpermissions()
之前告訴用戶你爲什麼需要這個權限。
- 例如,一個攝影的App可能需要使用定位服務,因爲它需要用位置標記照片。一般的用戶可能會不理解,他們會困惑爲什麼他們的App想要知道他的位置。所以在這種情況下,所以你需要在
-
使用兼容庫
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-v4
的ActivityCompat
中都有,建議使用兼容庫中的方法。最後一個方法是用戶授權或者拒絕某個權限組時系統會回調Activity或者Fragment中的方法。
checkSelfPermission() 檢查權限
- 檢查某一個權限的當前狀態,你應該在請求某個權限時檢查這個權限是否已經被用戶授權,已經授權的權限重複申請可能會讓用戶產生厭煩。
- 該方法有一個參數是權限名稱,有一個int的返回值,用這個值與上面提到的兩個常量做比較可判斷檢查的權限當前的狀態。
if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) { // 沒有權限,申請權限。 }else{ // 有權限了,去放肆吧。 }
requestPermissions() 申請權限
- 請求用戶授權幾個權限,調用後系統會顯示一個請求用戶授權的提示對話框,App不能配置和修改這個對話框,如果需要提示用戶這個權限相關的信息或說明,需要在調用 requestPermissions() 之前處理,該方法有兩個參數:
- int requestCode,會在回調
onRequestPermissionsResult()
時返回,用來判斷是哪個授權申請的回調。 - String[] permissions,權限數組,你需要申請的的權限的數組。
- 由於該方法是異步的,所以無返回值,當用戶處理完授權操作時,會回調Activity或者Fragment的
onRequestPermissionsResult()
方法。ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.READ_CONTACTS}, MMM);
onRequestPermissionsResult() 處理權限結果回調
- 該方法在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()
- 望文生義,是否應該顯示請求權限的說明。
- 第一次請求權限時,用戶拒絕了,調用
shouldShowRequestPermissionRationale()
後返回true,應該顯示一些爲什麼需要這個權限的說明。 - 用戶在第一次拒絕某個權限後,下次再次申請時,授權的dialog中將會出現“不再提醒”選項,一旦選中勾選了,那麼下次申請將不會提示用戶。
- 第二次請求權限時,用戶拒絕了,並選擇了“不在提醒”的選項,調用
shouldShowRequestPermissionRationale()
後返回false。 - 設備的策略禁止當前應用獲取這個權限的授權:
shouldShowRequestPermissionRationale()
返回false 。 - 加這個提醒的好處在於,用戶拒絕過一次權限後我們再次申請時可以提醒該權限的重要性,面得再次申請時用戶勾選“不再提醒”並決絕,導致下次申請權限直接失敗。
綜上所述,整合代碼後:
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;
}
}
}
運行時權限最佳實踐的套路
總體下來我們應該對運行時權限有一個系統的認識,我總結出了一些套路:
- 需要區分各種Normal Permissioin和Dangerous Permissions。
- 判斷多個權限授權回調時需要判斷每一個權限是否全都是被授權了,否則操作不能繼續。
- 需要請求多個權限時需要挨個檢查是否已經被授權過,沒授權的纔去請求,還要檢查這些權限是否需要提示用戶,如果多個權限都需要提示,該如何處理。
- 上述1 2 3如果在需要在多個頁面 實現,代碼重複。
- ...
其實問題遠遠不止這些,認真看過文章的人應該會發現,實現代碼比較簡單,但是代碼重複加上需要我們考慮和注意的細節太多了,那麼下面我就爲大家介紹一個開源內褲來解決這一系列問題。
AndPermission
這個開源庫名叫AndPermission:https://github.com/yanzhenjie/AndPermission,經過我的實踐是完全解決了上述問題,推薦大家使用,有興趣的朋友可以去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>
使用介紹
1、申請權限就是這麼簡單
AndPermission.with(this)
.requestCode(101)
.permission(Manifest.permission.WRITE_CONTACTS,
Manifest.permission.READ_SMS,
Manifest.permission.WRITE_EXTERNAL_STORAGE)
.send();
只需要在Activity中或者Fragment中直接調用即可,AndPermission自動爲你打理好後宮。
2、接受權限回調更簡單 只需要重寫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自動完成。
3、如果你需要在用戶多次拒絕權限後提示用戶
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()
方法的介紹。