去年底,上級主管部門為加強國內Android應用隱私管理,出臺了一系列規定,我們的App也做了相應的修改。主要一條修改為,隱私提示與權限獲取順序。修改測試過程中,發覺部分同學對Android權限相關知識和歷史並不了解,就此疫情期間忙裡偷閒,整理些東西供參閱。
首先,從一張圖開始此文。
IOS 12定位權限時間回到2013年,蘋果公司發布IOS7系統。其中一項令開發者頭疼的修改點:隱私中增加相冊、錄音等權限,App如需使用相應權限,需要申請並由用戶同意(IOS7以前,可以直接訪問相冊)。
針對此點,很多App在首次啟動時一通彈窗,申請各式各樣的權限。後來蘋果為改善用戶體驗,在App Store審核時要求App必須在使用前一刻才能申請權限,有效改善了此類問題。比如一款直播App,當你啟動App時並不需要相機、錄音權限,等到你開播時才需要申請這兩個權限。這一場景,其實就類似今天要提到的Android動態授權。谷歌於2015年推出Android 6.0 Marshmallow,其中一個主要特點便是加入了危險權限管理。這裡的「危險權限管理」就帶來了「運行時權限」這個新特性。危險權限管理」即在進行一些涉及到用戶隱私的操作時,需要獲取用戶的授權才能使用。如通訊錄、簡訊、相機、定位等隱私權限。獲取用戶權限,谷歌提倡在應用運行時向其授權,簡稱,運行時權限(也被叫做「動態權限/動態授權」,後文稱「動態權限」)。那,在這之前,Android權限管理是怎樣的呢?自己杜撰了下國內Android權限管理經歷的大概四個階段。早期Android系統(Android 6.0以前),在安裝App前,會羅列出App申請的所有權限。如果繼續安裝,視為用戶同意賦予App所需權限。例如:sony L36h Android 4.2.2系統。在嘗試安裝App時,彈窗羅列了App申請的全部權限。只能對所需權限進行查看,無法拒絕授權,可選擇取消安裝或繼續安裝。Sony L36h安裝提示這種方式,對於開發者極為友好,僅需在Manifest中配置App所需權限即可,代碼就可以直接調用了。但是對於用戶來說,這種方法存在極大的安全隱患。例:獲取手機IMEI,需要PHONE_STATE權限;訪問網絡,需要INIERNET權限。只許在Manifest文件中添加權限即可。<!-- PHONE_STATE權限-->
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<!-- 網絡權限-->
<uses-permission android:name="android.permission.INTERNET" />基於以上背景,為解決部分敏感權限被不合理使用,國內部分公司的安全類App,開始監控應用獲取手機敏感權限並做出提示。如360手機衛士、騰訊手機管家等產品,當監測到有App嘗試使用簡訊權限、定位等敏感權限,會告知用戶,並可以拒絕賦予權限。剛開始,還比較順利。但隨著手機廠商逐漸開始修改ROM,第三方安全App的兼容、性能問題逐步爆發。
例:HTC T328 Android 4.0.2系統。瀏覽器掃碼功能觸發相機調用時,360手機衛士會彈出權限提示窗,用戶可以允許或拒絕授權。注意,此窗由第三方安全軟體彈出,非系統級彈窗,跟後面要說的兩種彈窗有所區別。
360手機衛士 彈窗
隨著時間的推移,手機廠商開始發力,紛紛將第三方軟體的權限提示功能直接做入ROM。例:小米4,基於Android 4.4.4的MIUI7;oppo R9,基於Android 5.1的ColorOS 3.0,瀏覽器掃碼功能觸發相機調用時,會彈出權限提示窗。此窗,由ROM也就是系統自己彈出,為系統級權限彈窗。小米4授權OPPO R9授權
以上3個時期,App在申請權限時都不需做改變,只需配置Manifest。2015年推出的Android 6.0,加入了危險權限管理。因手機廠商對ROM的修改,部分6.0以上機器並不支持此項特性。
到了第四階段,App需要在對權限代碼進行修改後,才能正常使用對應權限。簡單理解為3步:1、判斷是否授權;2、如果未授權需申請權限,根據授權結果繼續執行;3、已授權可以繼續操作。
例:Pixel2,原生Android 10;華為mate8,基於Android 8.0的EMUI8。瀏覽器掃碼功能觸發相機調用時,會彈出權限提示窗。此窗,由App通知系統彈出,為系統級權限彈窗。
pixel2授權彈窗華為mate8授權彈窗第三階段與第四階段,同為系統彈出授權彈窗。二者有什麼區別嗎?首先,從UI上很難判斷所彈授權窗為第三階段或第四階段。第三階段彈的系統授權窗大都帶有一個倒計時自動拒絕邏輯;第四階段彈的系統授權窗基本不帶自動拒絕邏輯。此點可以粗略判斷系統使用的哪種機制。其次,從原理上。第三階段的彈窗,為系統監測到App在使用危險權限行為自動彈出彈窗。第四階段的彈窗,為App發覺自己沒有權限,讓系統彈出的彈窗。粗俗的理解,第三階段,你去朋友家串門,到門口看到大門敞開就直接往裡走,觸發了紅外線報警器,報警器通知了你朋友;第四階段,你去朋友家串門,到門口發覺門關著,就按下門鈴呼叫朋友給你開門。目前,國內主要處於第三階段(涵蓋Android4.0~7.1)和第四階段(涵蓋Android6.0~10),此點將在後文用到。因為動態權限特性,僅從Android 6.0開始擁有,所以,可以簡單粗暴的通過不提升targetSDK(targetSDK<23)的方式,便可不觸發此特性。
targetSDK18正常獲取IMEI僅提升targetSDK到26直接運行崩潰如果不改變任何代碼,直接將targetSDK提升到26,然後運行App,做同樣操作時會發生異常甚至崩潰,崩潰舉例如下:無PHONE_STATE獲取IMEI崩潰產生這個崩潰的原因,是在Android 6.0及以上,未獲取權限的情況下直接執行了需要權限的操作。那麼如何解決呢,就涉及到了真正的修改方案。
1. 在使用權限前,檢測權限。首先,我們需要判斷自己是否擁有權限。判斷時間點為執行需要權限的對應操作前。如我們在獲取IMEI前,需要判斷是否擁有PHONE_STATE權限。
我們可以調用ContextCompat.checkSelfPermission()方法檢測授權狀態,返回的結果為PackageManager中的兩個常量:PERMISSION_GRANTED(已授權)和PERMISSION_DENIED(未授權)。
2. 已授權的情況下,執行你的原有操作。當已授權時,就可以執行你原有的操作了。代碼如下:
// 檢測PHONE_STATE 如果已授權
if (ContextCompat.checkSelfPermission(this,Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED) {
//做你想做的
}3. 未授權的情況下,申請權限。如果App未獲得授權,我們就需要向用戶申請授權。可以調用requestPermissions()方法來請求授權。代碼如下:
// 檢測PHONE_STATE 如果未授權
if (ContextCompat.checkSelfPermission(this,Manifest.permission.READ_PHONE_STATE) != PackageManager.PERMISSION_GRANTED) {
//申請權限
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_PHONE_STATE), PERMISSIONS_REQUEST_PHONE_STATE)
}
requestPermissions()中的第三個參數是一個int型請求碼,方便回調處理。調用申請授權方法後,ROM會調起一個系統級彈窗(如下圖),這個dialog你無法定製。當用戶點擊同意後,系統會記錄,下次再判斷權限時就會返回已授權狀態;當App卸載時,記錄會被清除。Android 10授權彈窗以上,就完成了最樸素版的授權邏輯。整體代碼如下:
// 檢測PHONE_STATE 如果未授權
if (ContextCompat.checkSelfPermission(this,Manifest.permission.READ_PHONE_STATE) != PackageManager.PERMISSION_GRANTED) {
//申請權限
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_PHONE_STATE), PERMISSIONS_REQUEST_PHONE_STATE)
}else {
//如果已授權做你想做的
}那麼彈出申請彈窗之後呢?上面說道,彈出的dialog為系統的,我們無法在dialog中加代碼,但當彈窗被用戶點擊後,會觸發回調,我們在指定函數中處理回調即可。4. 重寫函數,處理授權彈窗的點擊結果。直接在Activity或Fragment中重寫onRequestPermissionsResult()函數,來處理權限申請結果。requestPermissions()的第三個參數,將在這裡被用到。代碼如下:
// 處理授權彈窗回調
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
when(requestCode){
// 識別剛剛用到的請求碼,根據請求碼識別不同彈窗回調並處理
PERMISSIONS_REQUEST_PHONE_STATE ->{
// 如果用戶點擊「允許」
if (grantResults.size > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED){
Toast.makeText(this, "用戶允許權限!",Toast.LENGTH_SHORT).show()
// 可以繼續執行你原來想做的事情了
}else{
Toast.makeText(this, "用戶拒絕權限!",Toast.LENGTH_SHORT).show()
// 用戶拒絕了,你想咋辦?
}
return;
}
// 可以識別其他請求碼並處理
}
}這樣,就完成了授權流程。然後,為提升授權概率,對流程進行優化。
5. 優化授權流程,提高授權機率。首先,系統授權窗我們無法定製,但是我們可以在這之前做個引導。在觸發系統彈窗之前,彈出一個引導UI,來告知用戶將要申請權限,並說明所需權限可帶來哪些更好體驗。尤其當你申請的權限看似與主要功能並無關係時,比如一個相機App如果需要申請定位權限的時候。其次,谷歌官方還提供了個函數shouldShowRequestPermissionRationale(),這個函數可以用來判斷,用戶上次是否拒絕了且未選則不再詢問。可以在授權前,通過此判斷,來決定給用戶展示首次授權引導或非首次授權引導。
最後,當用戶還是選擇了拒絕授權時,如果是必要權限(比如導航軟體申請定位權限),我們可以通過處理授權回調,在用戶點擊拒絕時彈出引導,告知用戶功能不可用,並引導用戶重新授權或到設置中手動開啟權限。引導授權流程綜上,動態權限主要實現步驟
在AndroidManifest明確我們需要哪些權限。(非動態權限也需要此步)在執行操作前檢是否獲得對應授權 -> checkSelfPermission()。如果已授權可以繼續操作;如果未授權,判斷之前是否授權被拒 -> shouldShowRequestPermissionRationale() (非必須操作)
a) 判斷如果沒有被拒過,彈出首次授權引導。
b) 判斷如果被據過,彈出非首次授權引導。引導後,申請權限-> requestPermissions()。處理申請的結果信息-> 回調函數onRequestPermissionsResult()。系統一共提供如下4個函數完成動態權限相關操作。
/**
* 檢查指定的權限是否授權(Context對象調用)
*/
public static int checkSelfPermission (Context context,
String permission)
/**
* 在沒有授權的情況下,有些時候可能需要提示給用戶為什麼需要改權限,就通過該函數來實現。
* 關於shouldShowRequestPermissionRationale的返回值問題,我們分三種情況
* 1. 第一次打開App時 -> false
* 2. 上次彈出權限點擊了禁止(但沒有勾選「下次不在詢問」) -> true
* 3. 上次選擇禁止並勾選:下次不在詢問 -> false
*/
public static boolean shouldShowRequestPermissionRationale (Activity activity,
String permission)
/**
* 申請指定的權限(Activity或者Fragment對象調用)
* @param permissions 權限列表,可以同時申請多個權限
* @param requestCode 該次權限申請對應的requestCode。和 onRequestPermissionsResult()回調函數裡面的requestCode對應
*/
public static void requestPermissions (Activity activity,
String[] permissions,
int requestCode)
/**
* 處理請求權限的響應,當用戶對請求權限的dialog做出響應之後,系統會回調該函數(Activity或者Fragment中重寫)
* @param requestCode 申請權限對應的requestCode
* @param permissions 權限列表
* @param grantResults 權限列表對應的返回值,判斷permissions裡面的每個權限是否申請成功
*/
public abstract void onRequestPermissionsResult (int requestCode,
String[] permissions,
int[] grantResults)寫到這裡,動態授權實現demo部分均已完成,實際業務場景肯定比以上流程複雜的多
動態權限為Android 6.0新特性,那低於6.0的系統,該如何寫適配代碼呢?首先想到的,是判斷系統版本,針對6.0以上使用動態權限代碼,針對低版本,使用老代碼。fun test(){
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
// 走動態授權
return
else
// 走非動態授權
return
}其實,可以不必如此麻煩。對於低版本,可以不必單獨寫代碼適配。在不支持動態授權的系統上,Manifest中申請過的權限,checkSelfPermission()方法,會直接返回PERMISSION_GRANTED。
另外,根據系統版本區分是否支持動態權限,實際是不靠譜的。前文有提到,部分手機廠商在ROM提升到Android 6.0以後,閹割了動態權限特性。目前沒有找到準確的API判斷當前系統是否支持動態權限。這會帶來什麼問題呢?
舉一個前不久遇到的實例。App的某一功能,是對別人顯示我所在城市(地理位置屬於敏感數據),用戶反饋關閉系統定位權限後,仍會顯示他所在城市。我們需要考慮如何解決用戶的問題,所以增加個需求,如果用戶關閉了定位權限,則不獲取城市。那麼問題來了,怎麼判斷用戶是否關閉了定位權限呢?為了避開不支持動態權限的ROM,需求只能退一步,6.0及以上系統做以上邏輯,6.0以下直接不獲取地理位置。但是根據測試經驗6.0以上系統仍不一定支持動態權限, 7.0及以上系統,絕大部分ROM支持動態權限。所以妥協決定7.0以下全部不獲取,7.0以上調checkSelfPermission()判斷是否授權,少數不支持動態權限的設備會誤認為已授權,需要增加設置項關閉功能。(提升到Android8.0應該是絕對安全的,不過覆蓋量太少)
以下為目前主流國內廠商對動態權限支持情況。(測試方法:在全新安裝未進行過授權操作的情況下,使用checkSelfPermission()檢查PHONE_STATE、定位、相機權限,返回如果是PERMISSION_GRANTED,則認為不支持動態權限)
基於Android6.0的ROM基於Android7.0的ROM小米
支持華為支持支持OPPO不支持支持VIVO不支持7.1.1不支持
7.1.2支持部分權限魅族
支持錘子
不支持360不支持不支持中興
支持1. 授權彈窗元素Android 8.0授權彈窗2. 是否存在不再詢問選項關於權限彈窗,針對同一個App的同一個權限,有時彈窗不帶「拒絕&不再詢問」選項,有時帶此選項。如下圖是谷歌原生系統、小米MIUI系統的兩種彈窗對比。這是什麼原因呢?Android原生實現:App全新安裝後首次申請權限,彈窗不帶此選項,即圖左效果。當用戶拒絕授權後,App下次再申請該權限時,則帶此選項,即圖右效果。但是,國內部分手機廠商並未遵循此標準,比如華為的Android 10之前的系統、OPPO/VIVO的部分權限,授權彈窗不管是否首次,都帶此選項。此為系統行為,App無法決定。
pixel2不再詢問MIUI不再詢問3. 彈窗選項與App設置中權限選項對應關係系統的授權彈窗,實際具有3項(允許、拒絕、 拒絕不再詢問)。但設置中的App權限選項,有的系統有2項(允許、拒絕),有的有3項(允許、詢問、拒絕)。授權彈窗選項與設置中的選項對應關係如下。
以原生Android 10系統為例:
彈窗 拒絕&不再詢問 -> 設置 拒絕 (跟上一項UI一致,本質有區別)。pixel2彈窗對應設置以基於Android 9.0的MIUI10.4.8為例:
MIUI彈窗對應設置4. 彈窗選項對四個函數的影響。彈窗彈出,用戶操作指定選項後,下次再調用四個函數會有如下現象:
UI選項與函數調用結果Android 6.0系統開始,權限被分為Normal permissions、Signature permissions、Dangerous permissions,其中Signature permissions比較超綱,僅介紹普通權限和危險權限。其中普通權限使用方法跟低版本一樣,只用在Manifest裡申請就可使用。大部分低風險權限,不需要通過確認框這種形式讓用戶顯示的同意。比如訪問網絡、檢查WiFi狀態等權限。另一種危險權限,也就是本文介紹的對象,它的產生主要為了保護用戶隱私,換言之,涉及到用戶隱私的一些權限,屬於危險權限。例如:相機權限、定位權限、PHONE_STATE(可讀取手機IMEI等識別碼)權限等。危險權限關於權限,還有一個權限組的概念。例如,讀取外置存儲權限(READ_EXTERNAL_STORAGE)和寫入外置存儲權限(WRITE_EXTERNAL_STORAGE),同屬存儲權限組(STORAGE)。
權限組有什麼作用呢?在Android O之前,同一權限組的權限,只要用戶授權一個,則整個權限組都被授權。例如:
步驟一:Manifest中加入了READ_EXTERNAL_STORAGE、WRITE_EXTERNAL_STORAGE
步驟二:在程序中只申請了READ_EXTERNAL_STORAGE權限,用戶同意後
步驟三:在程序中未申請WRITE_EXTERNAL_STORAGE權限,並嘗試直接使用
結果:可以直接使用,同組權限不需再申請。而Android O對此進行了修改。同一權限組不同權限,必須都要動態申請權限。但是如果第一個被用戶同意了,後面的同組權限再申請時,就不會再彈窗而是被直接同意了。例如:
步驟一:Manifest中加入了READ_EXTERNAL_STORAGE、WRITE_EXTERNAL_STORAGE
步驟二:在程序中只申請了READ_EXTERNAL_STORAGE權限,用戶同意後
步驟三:在程序中未申請WRITE_EXTERNAL_STORAGE權限,並嘗試直接使用
結果:崩潰。
修改步驟三:在程序中申請WRITE_EXTERNAL_STORAGE權限
結果:不會彈出授權彈窗,同一權限組直接被自動授權But,部分ROM修改了此邏輯。比如,華為9.0以下系統,遵循的是原生系統Android 8.0之前的邏輯。但是,華為9.0以後系統和小米6.0以後系統,都用的比原生系統Android 8.0更嚴格的邏輯。每個權限都需要單獨申請權限,而且會單獨彈窗要求用戶確認。
例如:
不同ROM權限組內影響
步驟一:Manifest中加入了READ_EXTERNAL_STORAGE、WRITE_EXTERNAL_STORAGE
步驟二:在程序中只申請了READ_EXTERNAL_STORAGE權限,用戶同意後
步驟三:在程序中申請WRITE_EXTERNAL_STORAGE權限
結果:會彈出授權彈窗,需要用戶再次授權
帶來問題:相同權限組不同權限的授權彈窗是一毛一樣的。這就導致用戶很懵逼,明明剛剛授權過了,為什麼又要問我一次。所以,部分手機上,你會發覺有些App,先後彈出兩個訪問文件存儲的權限彈窗。那是因為寫App的時候,先後申請了READ_EXTERNAL_STORAGE、WRITE_EXTERNAL_STORAGE權限導致。如何解決?
查看requestPermissions()方法的第二個參數,為一個數組。也就是說,可以傳入一個權限列表。
/**
* 申請指定的權限(Activity或者Fragment對象調用)
* @param permissions 權限列表,可以同時申請多個權限
* @param requestCode 該次權限申請對應的requestCode。和 onRequestPermissionsResult()回調函數裡面的requestCode對應
*/
public static void requestPermissions (Activity activity,
String[] permissions,
int requestCode)經測試,如果直接調該方法同時傳入READ_EXTERNAL_STORAGE、WRITE_EXTERNAL_STORAGE只會彈出一個授權窗,而且用戶同意後可以同時獲得兩個權限。如果傳入不同組權限,則按先後每組彈出一個彈窗。而且,這種單次傳入多組權限的情況,彈窗中大都會出現一個m/n的編號,以標識彈到第幾個,還剩幾個。如下圖分別是MIUI10(基於android9)和EMUI10(基於android10)的彈窗樣式:
紅米Note8Pro連續授權窗華為Mate20連續授權窗
後期的一些權限策略變化,僅列部分戶感知較大的。
IOS 8(2014年),定位權限選項分為「使用期間」(新增項)、「始終允許」、「不允許」。(減少App後臺定位)IOS 10(2016年),App訪問網絡需要授權。安裝未知來源應用需要申請權限。(App自升級、三方應用市場、廣告App安裝其他App需申請權限)定位權限選項分為「使用期間」(新增項)、「始終允許」、「拒絕」。(減少App後臺定位)部分電話、藍牙、WLAN的API,需要申請精確位置權限。IOS 13(2019年),定位權限選項分為「使用App時允許」、「允許一次」(新增選項)、「不允許」,去除了「始終允許」。(「允許一次」相當於試用權限或臨時權限,重啟App後需要重新申請權限)分區存儲強制執行。Download目錄、SD卡目錄訪問受限。對位置、麥克風、相機增加一次性權限許可,見IOS 13定位權限(即,如果用戶選了一次性許可,重啟App後需要重新申請權限)。自動阻止App重複的權限請求。也就是說如果用戶點擊2次拒絕授權,那麼系統會自動停止詢問授權,當然了,用戶也可以前往設置中手動調整。兩大平臺,都在多個版本中對用戶隱私進行了優化,僅定位權限的優化就多次提及。
可見,在手機逐漸轉化為人體器官之一的今天,IOS和Android兩大移動平臺對於權限、隱私的管理越發嚴苛,而且趨同的速度約來越快。估計以後Android App想訪問網絡也需申請授權。但手機廠商自行定製修改ROM,仍是開發者最頭疼的問題。
參考文獻:谷歌官方文檔:https://developer.android.com/training/permissions/requesting.html
留言區