Android 6.0權限管理的解析與實戰

一、引言

隨著Android6.0發布,系統增加了一些新的特性和功能。這次的發布介紹了一種新的權限機制。用戶可以在運行時直接管理應用程序的權限。這個功能提升了權限控制的可見性和可控性。同時簡化了安裝和自動升級過程,用戶可以單獨撤銷或者授予應用程序某項權限,對應用擁有更多的控制權。

二、Android 6.0權限機制

當你應用程序target是Android 6.0及以上(API level?23),確保在運行時檢查和請求權限。為了確定你的app是否授予某個權限,通過checkSelfPermission()方法判斷,請求權限使用requestPermissions()方法。即使你的app不是target Android 6.0,你也應該在新的權限機制下測試你的應用。

用戶很容易被一個app繁多的權限請求淹沒,當用戶發現一個app使用起來很麻煩,或者用戶擔心app竊取自己的信息。用戶會拒絕使用這個app,更有甚至會卸載它。因此,在申請權限的時候,我們應該遵守以下幾點規則進行申請。

(a)只向用戶請求你需要的權限(盡可能少請求權限)

每次app請求權限,都是強制用戶去做決定。你應該盡量減少這些請求的次數,以保證用戶流暢操作。

(b)不要讓繁多的權限申請請求淹沒用戶

不要一次性請求所有權限,只有當你需要使用的時候才去申請。在某些情況下,一個或多個權限是你app必須的,這個時候可能在應用一啟動就去權限更加有意義。例如,你做的是一個攝影類的應用,應用需要使用設備照相機。當用戶第一次啟動app的時候,對于app請求使用照相機的權限,用戶并不會感到迷惑。但是,如果這個應用擁有向聯系人分享照片的功能,你最好不要在應用啟動的時候就去申請READ_CONTACTS權限。你應該等到用戶使用分享功能的時候,再去詢問用戶是否申請這個權限。

如果你的app提供了一個引導教程,在引導結束的時候申請必要權限是比較好的。

(c)測試所有的權限模式

從Android 6.0(API 23)開始,用戶在運行時授予或者拒絕權限,而不是在安裝app的時候。因此,你必須在更廣泛的條件下測試你的應用。在Android 6.0之前,你可以合理假設如果你的程序運行,你程序擁有所有申明的權限。但在新的權限模式下,你不能進行這種假設。

下面的一些小貼士,幫助你在Android?6.0或者更高的版本上識別權限相關代碼問題。

(1)識別app中當前權限和相關代碼路徑

(2)用permission-protected服務和數據測試用戶流。

(3)通過各種組合測試權限授予和拒絕。

如一個拍照的app,可能會在manifest中聲明CAMERA,READ_CONTACTS和ACCESS_FINE_LOCATION權限。你需要測試所有權限的授予和拒絕組合狀態,確保app可以在所有的權限組合下優雅滴工作。(注意,從Android 6.0開始,用戶可以對任意app的權限進行授予或者拒絕,即使app的目標API是小于23的)

(4)使用adb工具通過命令行管理權限

--組展示權限和狀態

$ adb shell pm list permissions -d -g

--授權或者拒絕一個或者多個權限

adb shell pm [grant|revoke] ...

(5)分析應用中使用到權限的相關服務

三、Android系統權限介紹

Google將權限分為兩類,一類是普通權限(Normal Permissions),這類權限一般不涉及用戶隱私,也不需要用戶進行授權,如網絡訪問,手機震動等,這些權限如下所示:

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_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?Permission),涉及用戶隱私,需要用戶授權,如對sd卡讀取、訪問用戶手機通訊錄等。如圖所示:

查看dangerou?Permissions可以發現權限是分組的,這個和Android 6.0的授權機制有關。如果你申請某個危險的權限,假設你的app早已被用戶授權了同一組的某個危險權限,那么系統會立即授權,而不需要用戶去點擊授權。比如你的app對READ_CONTACTS已經授權了,當你的app申請WRITE_CONTACTS時,系統會直接授權通過。此外,申請時彈出的dialog上面的文本說明也是對整個權限組的說明,而不是單個權限(ps:這個dialog是不能進行定制的)。

注:不要對權限組過多的依賴,盡可能對每個危險權限都進行正常流程的申請,因為在后期的版本中這個權限組可能會產生變化。

新權限機制實踐

(1)在AndroidManifest文件中添加需要的權限

這個步驟和我們之前的開發并沒有什么變化,試圖去申請一個沒有聲明的權限可能會導致程序崩潰。

(2)檢查權限

if(ContextCompat.checkSelfPermission(thisActivity,?Manifest.permission.READ_CONTACTS)!= PackageManager.PERMISSION_GRANTED) {

//

}else{

//

}

這里涉及到一個API,ContextCompat.checkSelfPermission,主要用于檢測某個權限是否已經被授予,方法返回值為PackageManager.PERMISSION_DENIED或者PackageManager.PERMISSION_GRANTED。當返回DENIED就需要進行申請授權了。

(3)申請授權

ActivityCompat.requestPermissions(thisActivity,

new String[]{Manifest.permission.READ_CONTACTS},

MY_PERMISSIONS_REQUEST_READ_CONTACTS);

該方法是異步的,第一個參數是Context;第二個參數是需要申請的權限的字符串數組;第三個參數為requestCode,主要用于回調的時候檢測。可以從方法名requestPermissions以及第二個參數看出,是支持一次性申請多個權限的,系統會通過對話框逐一詢問用戶是否授權。

(4)處理權限申請回調

@Override

public void onRequestPermissionsResult(intrequestCode,

String permissions[], int[] grantResults) {

switch (requestCode) {

case MY_PERMISSIONS_REQUEST_READ_CONTACTS: {

// 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 youneed to do.

} else {

// permission denied,boo! Disable the

// functionality that dependson this permission.

}

return;

}

}

}

ok,對于權限的申請結果,首先驗證requestCode定位到你的申請,然后驗證grantResults對應于申請的結果,這里的數組對應于申請時的第二個權限字符串數組。如果你同時申請兩個權限,那么grantResults的length就為2,分別記錄你兩個權限的申請結果。如果申請成功,就可以做你的事情了~

// Should we show an explanation?

if(ActivityCompat.shouldShowRequestPermissionRationale(thisActivity,

Manifest.permission.READ_CONTACTS))

//Show an expanation to the user *asynchronously* -- don't block

// this thread waiting for the user's response! After the user

// sees the explanation, try again to request the permission.

}

這個API主要用于給用戶一個申請權限的解釋,該方法只有在用戶在上一次已經拒絕過你的這個權限申請。也就是說,用戶已經拒絕一次了,你又彈個授權框,你需要給用戶一個解釋,為什么要授權,則使用該方法。

// Here, thisActivity is the currentactivity

if(ContextCompat.checkSelfPermission(thisActivity,

Manifest.permission.READ_CONTACTS)

!= PackageManager.PERMISSION_GRANTED) {

// Should we show an explanation?

if (ActivityCompat.shouldShowRequestPermissionRationale(thisActivity,

Manifest.permission.READ_CONTACTS)) {

// Show an expanation to the user *asynchronously* -- don't block

// this thread waiting for the user's response! After the user

// sees the explanation, try again to request the permission.

}else {

// No explanation needed, we can request the permission.

ActivityCompat.requestPermissions(thisActivity,

newString[]{Manifest.permission.READ_CONTACTS},

MY_PERMISSIONS_REQUEST_READ_CONTACTS);

// MY_PERMISSIONS_REQUEST_READ_CONTACTS is an

// app-defined int constant. The callback method gets the

// result of the request.

}

}


四、權限庫封裝

在系統是Android 6.0及以上的手機上,申請每個Dangerous Permission,都需要進行權限處理。如果不進行封裝,會有很多的重復代碼。這樣的代碼不夠美觀,且不利于閱讀和維護。下面我介紹兩種封裝好的權限控制庫PermissionGen和MPermissions。其中MPermissions是在PermissionGen的基礎上做了優化,即將運行時注解改為編譯時注解,提升了性能。

4.1 PermissionGen

4.1.1 庫的使用方式

首先我們來看看PermissionGen是怎么使用的。在Activity或者fragment中,申請權限可以通過以下方式:

PermissionGen.needPermission(this,200,Manifest.permission.CAMERA);

或者

PermissionGen.with(MainActivity.this)

.addRequestCode(100)

.permissions(

Manifest.permission.READ_CONTACTS,

Manifest.permission.RECEIVE_SMS,

Manifest.permission.WRITE_CONTACTS)

.request();

通過設置參數,可以實現申請單個權限或者多個權限。我們知道,系統原生權限請求處理都是在回調onRequestPermissionsResult中處理的,這里我們使用PermissionGen,只需要在onRequestPermissionsResult中調用一下以下方法,如此庫即可對回調結果進行分發處理。

@Override

public void onRequestPermissionsResult(intrequestCode,String[] permissions,

int[] grantResults) {

PermissionGen.onRequestPermissionsResult(this,requestCode,permissions,grantResults);

}

最后,對于權限授權的回調,是在activity或者fragment中處理的,如下所示。授權成功則執行PermissionSuccess修飾的方法,失敗則執行PermissionFail修飾的方法,不同的權限通過requestCode來區分。

@PermissionSuccess(requestCode=200)

public void successOpenCamera(){

Dlog.debug("open camera success");

}

@PermissionFail(requestCode=200)

public void failOpenCamera(){

Toast.makeText(this,"Camera permission is not granted",Toast.LENGTH_SHORT).show();

}

4.1.2 庫實現原理解析

通過上面庫的使用,我們發現這個用起來真的很方便。下面我們從權限申請開始,一步步從源碼分析庫是怎么實現的。

首先,當我們去申請權限的時候,都會調用到PermissionGen.java類中的requestPermissions方法,我們來看看這個方法的實現:

首先遍歷自己申請的所有權限,沒有授權的相關權限存入列表deniedPermissions。若權限全都申請,deniedPermissions長度為0,則通過doExecuteSuccess方法,反射找到相應requestCode對應的回調方法并執行;若權限有未申請的,則調用activity或者fragment的requestPermissions方法去申請相應權限,此時會彈出相應彈窗(彈窗是系統級,不可更改),執行結果回調到onRequestPermissionResult方法。在方法中判斷所有申請權限是否已經授權成功。成功執行doExecuteSuccess,失敗執行doExecuteFailed。

接下來我們看看onRequestPermissionsResult方法,我們知道在activity或者fragment中,我們重寫了onRequestPermissionsResult方法,在其中去執行PermissionGen中的onRequestPermissionsResult方法進行授權結果處理。而在PermissionGen中,對于授權結果的處理,主要是在requestResult中進行處理。

代碼中遍歷授權請求結果數組,只要有一個權限沒有授權通過,則本次的授權就是失敗的,調用doExecuteFail方法,成功則調用doExecuteSuccess方法。這里是這個庫的精髓所在,庫是如何通過doExecuteSuccess和doExecuteFail方法,執行requestCode對應的請求回調方法?這里以doExecuteSuccess為例進行介紹。我們發現doExecuteSuccess有兩個參數,第一個參數表示的是請求執行的actvity或者fragment;第二個參數是requestCode。下面我們來看doExecuteSuccess的代碼:

private static void doExecuteSuccess(Object activity, intrequestCode) {

Method executeMethod = Utils.findMethodWithRequestCode(activity.getClass(),

PermissionSuccess.class,requestCode);

executeMethod(activity,executeMethod);

}

看到這里就有種豁然開朗的感覺了,findMethodWithRequestCode這個方法必定是通過反射的方式找到注解修飾的對應的方法,然后通過executeMethod去執行對應的方法。我們來看findMethodWithRequestCode的源碼:

正如我們所猜測的,代碼中通過反射找到通過注解修飾的方法;然后通過對比注解的requestCode定位到正確的回調方法,最后通過executeMethod執行回調方法。

4.2 MPermissions

4.2.1 MPermissons庫介紹

MPermissons庫是作者在PermissionGen的基礎上修改而來的,相信大家也都發現一個問題。PermissionGen是基于反射,在運行時獲取回調方法并執行的,這在一定程度上影響了性能。MPermissons基于Annotation Processor,運行時注解,很好解決PermissionGen的問題。

4.2.2 MPermissions的使用

我們來看看MPermissions的使用,基本是和PermissionGen一樣的。

首先:請求權限:

MPermissions.requestPermissions(MainActivity.this,REQUECT_CODE_CALL_PHONE,Manifest.permission.CALL_PHONE);

然后,重寫onRequestPermissionsResult方法:

@Override

public void onRequestPermissionsResult(intrequestCode,String[] permissions, int[] grantResults)

{

MPermissions.onRequestPermissionsResult(this,requestCode,permissions,grantResults);

super.onRequestPermissionsResult(requestCode,permissions,grantResults);

}

最后注解修飾回調方法:

@PermissionGrant(REQUECT_CODE_CALL_PHONE)

public voidrequestCallPhoneSuccess()

{

Toast.makeText(this,"GRANT ACCESS SDCARD!",Toast.LENGTH_SHORT).show();

}

@PermissionDenied(REQUECT_CODE_CALL_PHONE)

public voidrequestCallPhoneFailed()

{

Toast.makeText(this,"DENY ACCESS SDCARD!",Toast.LENGTH_SHORT).show();

}

4.2.3 MPermissions的實現原理

從上面可以發現MPermissions和PermissionGen的使用方式是一樣的,只是接口的命名方式不同而已。同樣我們從權限申請開始分析,一路看下去,與PermissionGen的處理邏輯都是一樣的。最后不同的doExecuteSuccess和doExecuteFail方法的實現邏輯。

看源碼發現,MPermissions是通過Class.forName靜態加載的方式,加載了對應activity或者fragment類的代理類,并執行requestCode對應的回調方法。這與PermissionGen的運行時反射處理相比,性能肯定是有提升的。但是到這里我們應該會有一個疑問,那就是這個代理類是從哪里冒出來的?我們繼續往下看,發現庫項目目錄下有一個compile的工程,這是關鍵所在。我們的代理類,通過annotation Processor,在編譯時,生成對應的XXXActivity$PermissionProxy,通過PermissionProcessor中的process方法生成。具體annotation processor的實現,大家可以自己查閱相關資料。


生成代理類如下所示:

public classMainActivity$$PermissionProxyimplementsPermissionProxy {

? ? publicMainActivity$$PermissionProxy(){

? ? }

? ? public void grant(MainActivity source, intrequestCode) {

? ? ? ? switch(requestCode){

? ? ? ? case20:

? ? ? ? ? ? source.requestSdcardSuccess();

? ? ? ? ? ? break;

? ? ? ? case30:

? ? ? ? ? ? source.requestCallPhoneSuccess();

? ? ? ? }

? ? }

? ? public void denied(MainActivity source, intrequestCode) {

? ? ? ? switch(requestCode){

? ? ? ? case20:

? ? ? ? ? ? source.requestSdcardFailed();

? ? ? ? ? ? break;

? ? ? ? case30:

? ? ? ? ? ? source.requestCallPhoneFailed();

? ? ? ? }

? ? }

}

最后,在doExecuteSuccess()或者doExecuteFail()方法中,通過Class.forName()加載的是生成的代理類,通過newInstance()新建對象,然后通過requestCode區分,調用對應的grant或者deny方法。

五、總結

Android6.0的權限控制,雖然目前看來處理起來還是有點繁瑣,但使用戶對應用有了更多的控制權。借助一些封裝的權限控制第三方庫,對于應用權限可以做到比較簡潔的控制。PermisionGen操作比較簡單,但是是運行時反射實現,性能較差;MPermissions解決了這個問題,在編譯時生成代理類,解決了庫運行時效率低的問題,但與之相比庫實現起來略煩瑣。


參考文章:Android 6.0 運行時權限處理完全解析

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,702評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,615評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,606評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,044評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,826評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,227評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,307評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,447評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,992評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,807評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,001評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,550評論 5 361
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,243評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,667評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,930評論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,709評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,996評論 2 374

推薦閱讀更多精彩內容