一、引言
隨著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解決了這個問題,在編譯時生成代理類,解決了庫運行時效率低的問題,但與之相比庫實現起來略煩瑣。