這是一篇遲到的文章。在6.0之前,在應用安裝的時候,提示用戶所需要用到的權限列表,同意之后安裝,該app就被賦予所有的權限,我們暫且稱它為
安裝時權限
,安裝后,被賦予的權限也無法取消,當然,國內的一些rom會在系統級別進行額外的一層權限管理,這個不在本文敘述范圍之內;在6.0之后,google對權限進行了運行時的管理,而不是在安裝時候,危險權限
需要在運行時申請,我們暫且稱它為運行時權限
,非危險權限,在安裝時由用戶授予,這樣簡化了應用安裝過程,因為用戶在安裝或更新應用時不需要授予權限,也給予了用戶對app功能更多的控制
權限分組
系統權限主要分為兩類,正常權限
和危險權限
正常權限
不會直接危及用戶的隱私,如果你的應用在它的Manifest中列出了正常權限,系統會自動授予權限
危險權限
可以讓app訪問用戶的機密數據,如果你的應用在它的Manifest列出了危險權限,用戶必須明確批準你的app使用該權限
當然,不管哪個版本的android,你應用中所用到的所有權限,不管是正常權限
還是危險權限
,都需要在應用的Manifest中申明
如果你的設備運行Android 5.1以及5.1以下版本,或者你的應用的目標SDK是22以及22以下版本:如果你在應用的Manifest中申明了危險權限,用戶在安裝時必須授予權限,如果拒絕授予權限,那么系統就不會安裝應用,也就是所謂的“一刀切”方式,不同意所有權限,就不能安裝應用
如果你的設備運行Android 6.0以及6.0以上版本,或者你的目標SDK是23以及23以上版本:應用必須在Manifest中羅列出所有的權限,并且在程序運行時,它必須請求用戶授予每一個危險權限,此時用戶可以授予或者拒絕每一個權限,并且應用程序可以繼續運行有限的功能,即使用戶拒絕了權限請求
注意:從Android 6.0開始(API 23),用戶可以在任何時候,對任何應用撤銷權限,即使app申明的目標SDK低于23
正常權限
正常權限有以下幾個特點
就是在安裝時,由用戶授予,后續運行時無需再次申請,無需再顯示提醒用戶,用戶也不能取消這些權限(在Android 6.0及以上版本例外,用戶可以通過管理界面撤銷權限)
對用戶隱私或安全沒有較大影響
正常權限列表
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
我們大概看一遍過去,可以總結歸納出正常權限有幾類:藍牙,網絡狀態,NFC,指紋,鬧鈴,快捷方式,震動等常用權限
危險權限
危險權限才是運行時權限
的主要處理對象,因為這些權限可能引起隱私wenti或者影響其他程序運行,android中危險權限主要以組的形式出現,可以歸納為一下幾個分組:
CALENDAR
CAMERA
CONTACTS
LOCATION
MICROPHONE
PHONE
SENSORS
SMS
STORAGE
具體請看下圖:
那么問題來了
Q1.我們的應用是否必須支持運行時權限?
Android為了應用兼容,實際上是可以不需要支持運行時權限的,只要設置targetSdkVersion低于23就可以了,意思是我的應用還乜有在API 23上面完全兼容,不要給我開啟運行時權限新特性,當然了,早晚你還是要支持的,時間問題而已
Q2.如果不支持運行時權限,應用會崩潰么?
可能會奔潰,具體要根據你使用運行時權限在代碼的什么地方,假設你應用啟動就開始創建SDCard文件夾,但是在Android 6.0的設備上,系統或者用戶在系統權限管理界面禁止了你的SDCard權限,如果應用代碼處理不當,此時重新啟動應用就會發生奔潰
這里我們還要注意的一點是,危險權限是以組的方式授予的,怎么理解呢?按照我們上面列出的9大分組,舉個栗子,如果你申請讀取SDCard權限,在用戶授予權限后,你自動就獲得了寫入SDCard的權限,也就是說你獲得了STORAGE分組的所有權限
運行時權限申請
API使用
申請運行時權限主要使用到的API有下面三個:
# 檢測系統當前是否被授予某個運行時權限,傳入參數是權限名稱,比如Manifest.permission.READ_EXTERNAL_STORAGE
ContextCompat.checkSelfPermission(@NonNull Context context, @NonNull String permission)
# 是否要顯示權限說明
# 1.用戶第一次被拒絕(非永久拒絕)授予某個權限后,下次再次請求該權限,這個方法會返回true,用戶有機會以某種方式對用戶進行說明該權限用處
# 2.用戶在第一次拒絕某個權限后,下次再次申請時,授權的dialog中將會出現“不再提醒”選項,一旦選中勾選了,那么下次申請將不會提示用戶。
# 3.第二次請求權限時,用戶拒絕了,并選擇了“不再提醒”的選項,調用shouldShowRequestPermissionRationale()后返回false。
# 4.設備的策略禁止當前應用獲取這個權限的授權:shouldShowRequestPermissionRationale()返回false 。
# 5.加這個提醒的好處在于,用戶拒絕過一次權限后我們再次申請時可以提醒該權限的重要性,免得再次申請時用戶勾選“不再提醒”并決絕,導致下次申請權限直接失敗。
ActivityCompat.shouldShowRequestPermissionRationale(@NonNull Activity activity,@NonNull String permission)
# 請求權限授予,當然可以傳入多個權限名稱同時申請,用戶會依次彈出多個提示框申請權限,App不能配置和修改這個對話框,如果需要提示用戶這個權限相關的信息或說明,需要在調用 requestPermissions() 之前處理,該方法有兩個參數:
# int requestCode,會在回調onRequestPermissionsResult()時返回,用來判斷是哪個授權申請的回調。
# String[] permissions,權限數組,你需要申請的的權限的數組。
# 由于該方法是異步的,所以無返回值,當用戶處理完授權操作時,會回調Activity或者Fragment的onRequestPermissionsResult()方法。
ActivityCompat.requestPermissions(final @NonNull Activity activity,final @NonNull String[] permissions, final @IntRange(from = 0) int requestCode)
# 用戶拒絕或者授予用戶權限后的回調方法,該方法在Activity/Fragment中應該被重寫,當用戶處理完授權操作時,系統會自動回調該方法,該方法有三個參數:
# int requestCode,在調用requestPermissions()時的第一個參數。
# String[] permissions,權限數組,在調用requestPermissions()時的第二個參數。
# int[] grantResults,授權結果數組,對應permissions,具體值和上方提到的PackageManager中的兩個常量做比較。
onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults)
我們以請求讀寫SDCard權限,來看一下一個標準的請求權限流程
# 判斷當前系統版本是都是6.0以上
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
# 檢測是否已經被用戶授予該權限
if (ContextCompat.checkSelfPermission(LaunchActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
# 沒有被授予權限
# 判斷如果已經被用戶拒絕過一次(非永久拒絕),則顯示用戶此權限的一些說明,這里可以用toast,當然也可以用自定義界面來顯示給用戶,如果是采用自定義界面來顯示給用戶,在有“確定”,“取消”等按鈕的情況下,下面的“請求權限”步驟要酌情調用
if (ActivityCompat.shouldShowRequestPermissionRationale(LaunchActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE)) {
Toast.makeText(LaunchActivity.this, "request read external storage", Toast.LENGTH_LONG).show();
}
# 請求權限,數組傳入多個值,可以一次請求多個權限
ActivityCompat.requestPermissions(LaunchActivity.this,
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
PERMISSIONS_REQUEST_EXTERNAL_STORAGE);
}
} else {
# 6.0以下版本,直接使用權限
// 做一些權限對應的操作
}
如果是在6.0及6.0以上版本時,執行 ActivityCompat.requestPermissions()方法請求權限后,如果用戶同意或者拒絕后,會回調onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults)方法,該方法的幾個參數,我們做一下解釋:
requestCode 對應的是ActivityCompat.requestPermissions請求時的requestCode,這個與Activity中常用的onActivityResult方法中的requestCode類似,用來標識某一次或者某一個請求的回調對應關系
permissions 對應ActivityCompat.requestPermissions()方法中請求的權限列表
grantResults 對應2中permissions的每個權限的用戶應答結果
好了,接下來就是根據requestCode,遍歷permissions和grantResults中的結果,做對應的操作
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch (requestCode) {
case PERMISSIONS_REQUEST_EXTERNAL_STORAGE: {
// 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;
}
}
}
注意事項
API問題
由于checkSelfPermission和requestPermissions從API 23才加入,低于23版本,需要在運行時判斷 或者使用Support Library v4中提供的方法
ContextCompat.checkSelfPermission
ActivityCompat.requestPermissions
ActivityCompat.shouldShowRequestPermissionRationale
兩個權限
運行時權限對于應用影響比較大的權限有兩個,他們分別是
READ_PHONE_STATE
WRITE_EXTERNAL_STORAGE/READ_EXTERNAL_STORAGE
其中READ_PHONE_STATE用來獲取deviceID,即IMEI號碼。這是很多統計依賴計算設備唯一ID的參考。如果新的權限導致讀取不到,避免導致統計的異常。建議在完全支持運行時權限之前,將對應的值寫入到App本地數據中,對于新安裝的,可以采取其他策略減少對統計的影響。
WRITE_EXTERNAL_STORAGE/READ_EXTERNAL_STORAGE這兩個權限和外置存儲(即sdcard)有關,對于下載相關的應用這一點還是比較重要的,我們應該盡可能的說明和引導用戶授予該權限。
建議
不要使用多余的權限,新增權限時要慎重
使用Intent來替代某些權限,如撥打電話,選擇圖片(和你的產品經理PK去吧)
對于使用權限獲取的某些值,比如deviceId,盡量本地存儲,下次訪問直接使用本地的數據值
注意,由于用戶可以撤銷某些權限,所以不要使用應用本地的標志位來記錄是否獲取到某權限,在使用的時候要遵循流程實時判斷后使用,避免不正確的使用導致應用崩潰
注意
即使支持了運行時權限,也要在Manifest聲明,因為市場應用會根據這個信息和硬件設備進行匹配,決定你的應用是否在該設備上顯示。
防止一次請求太多的權限或請求次數太多,用戶可能對你的應用感到厭煩,在應用啟動的時候,最好先請求應用必須的一些權限,非必須權限在使用的時候才請求,建議整理并按照上述分類管理自己的權限
權限申請彈出的對話框不能自定義
解釋你的應用為什么需要這些權限:在你調用requestPermissions()之前,你為什么需要這個權限
個人覺得Marshmallow的運行時權限對于用戶來說絕對是一個好東西,但是目前想要支持需要做的事情還是比較多的。
對于一個有很多依賴的宿主應用,想要做到支持還是有一些工作量的,因為你的權限申請受制于依賴。
最后,通過以上使用例子的代碼我們看到,雖然原理和內容很簡單,但是流程上我們還是要寫很多代碼,判斷幾個條件,稍后我將在另外一片文章中根據請求運行時權限的特點,封裝出一個庫EPermission ,建議大家使用這個庫,簡化權限申請流程
附錄
權限結果常量
PackageManager.PERMISSION_DENIED:該權限是被拒絕的。
PackageManager.PERMISSION_GRANTED:該權限是被授權的。
權限名稱
Manifest.permission.READ_EXTERNAL_STORAGE
Manifest.permission.CAMERA
等等Manifest.permission類中對應的常量