1. 程式人生 > >Android 許可權機制與適配經驗

Android 許可權機制與適配經驗

一、概要

Android M已經發布一段時間了,市面上很多應用都已經適配Android M。許可權機制,作為Android M的一大特性,受到了很多開發者的關注。

本文主要分享了以下幾個知識點的內容:

  1. Android許可權機制關鍵知識點;

  2. QQ音樂對於許可權的適配經驗;

  3. 近段時間以來遇到的一些Android許可權方面的問題。

OK,下面進入主題。

二、Android許可權機制

已經瞭解過基本知識的,建議直接跳到第三點(QQ音樂的許可權適配經驗)。

Android6.0以前,Android的許可權機制比較簡單,開發者在AndroidManifest檔案中宣告需要的許可權,APP安裝時,系統提示使用者APP將獲取的許可權,需要使用者同意授權才能繼續安裝,從此APP便永久的獲得了授權。然而,同期的iOS對於許可權的處理會更加靈活,許可權的授予並不是在安裝時,而是在APP執行時,使用者可以根據自身的需要,決定是否授予APP某一許可權,同時,使用者也可以很方便回收授予的許可權。顯然,動態許可權管理的機制,對於使用者的隱私保護是更加適用的,Android過於簡單的許可權機制也受到了不少人的吐槽。終於,Android6.0也釋出了動態許可權的機制。

開始適配和如何相容

APP要適配Android6.0非常簡單,只需要將targetSdkVersioncompileSdkVersion都升級到23及以上,同時加入許可權檢查申請等程式碼邏輯即可。這裡很多人會有一些疑惑,如果針對舊版本的APP在Android6.0機型上執行或者針對Android6.0適配了的APP在Android6.0以下機型上執行,會有什麼表現呢?是如何相容的呢?

  1. 首先,舊版本APP(targetSdkVersion低於23),因為沒有適配許可權的申請相關邏輯,在Android6.0以上機型執行的時候,仍然採用安裝時授權的方案。

  2. 適配了Android6.0的APP,在低版本Android系統上執行的時候,仍然採用安裝時授權的方案,但是開發者需要注意的是,許可權申請的程式碼邏輯只應該在Android6.0及以上的機型被執行。

危險許可權與普通許可權

一開始,聽到要加入許可權判斷和申請程式碼邏輯的程式設計師內心可能是崩潰的:正常的一個有一定規模的APP,很容易就七七八八的聲明瞭很多許可權,如果每個許可權都申請豈不是非常麻煩?

好歹,Google還算比較明智,並不是所有的許可權都需要執行時申請才能使用。Google對每個許可權的隱私危害性進行了評估。將許可權分為了兩大類:普通許可權和危險許可權。舉個例子,控制手機震動的許可權對於使用者並沒有什麼危害,只要開發者聲明瞭這個許可權,安裝後就可以一直被授權,也不能被回收,但是,像讀取sd卡資料這類許可權,很顯然就是危險許可權了,APP必須向用戶申請這個許可權。

Google還是很體貼我們開發者的,為了進一步減少開發的工作量和申請許可權對使用者的騷擾,對危險許可權根據各自的屬性進行了分組。舉個例子,讀sd卡和寫sd卡,這兩個許可權通常都是成對宣告和使用的,因此,它們被分為一組,而且,只要我們獲取了這個許可權組裡面的任意一個許可權,就可以獲取整個許可權組的許可權。Google對於危險許可權的定義和分組見下圖。

許可權相關API說明

首先,在動態許可權申請的流程中,開發者主要關注流程和API如下:

1、檢查許可權是否授予。

//Activity.java

public int checkSelfPermission(permission)

2、申請許可權。

//Activity.java

public final void requestPermissions( new String[permission1,permission2,...], requestCode)

這個時候,會彈出系統授權彈窗(授權彈窗是不支援自定義的,原因理所當然)。

3、許可權回撥。

使用者在系統彈窗裡面選擇後,結果會通過ActivityonRequestPermissionsResult方法回撥APP。

public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults)
{
    //繼續執行邏輯或者提示許可權獲取失敗
}

4、許可權說明。

使用者如果選擇了拒絕,下一次在需要宣告該許可權的時候,Google建議APP開發者給予使用者更多的說明,因此提供了下面這個API,這個方法返回值在使用過程中會發現有點糾結(具體解析見下面程式碼塊說明)

public boolean shouldShowRequestPermissionRationale(permission)
{
    1、APP沒有申請這個許可權的話,返回false
    2、使用者拒絕時,勾選了不再提示的話,返回false
    3、使用者拒絕,但是沒有勾選不再提示的話,返回true
    因此如果想在第一次就給使用者提示,需要記錄許可權是否申請過,沒有申請過的話,強制彈窗提示,而不能根據這個方法的返回值來。
}

三、QQ音樂的許可權適配經驗

1、不同許可權,申請的時機不同

QQ音樂作為一個比較複雜的流媒體應用,也需要不少許可權,但是究竟在什麼時候來申請這些許可權就成了適配6.0時首當其衝問題。針對這個問題,我們也對需要的許可權進行了思考,大致認為申請許可權需要分為兩個時機。

使用者觸發:這個很好理解,有些和特性相關的許可權,比如說聽歌識曲的錄音許可權、自建歌單封面拍照許可權等,這類許可權平時APP執行時並不需要,那麼我們選擇在使用者觸發或者進入該功能的時候,進行授權受阻邏輯。

應用啟動時:我們在梳理的時候發現,有些許可權(讀取裝置資訊,讀寫sd卡等)並不是由使用者或者特性觸發的,而是網路免流,登入安全,日誌系統這些底層邏輯無時不刻觸發的。對於這些許可權,就比較糾結了。不過回過頭來看,這些許可權通常是開發者或者APP不能妥協的許可權,因為如果使用者不授權的話,將會影響整個APP的功能和資料。所以,我們選擇比較暴力的方式,在應用啟動的時候,就受阻。這也是Google建議的一種方式。

但是需要注意的是,一開始就申請授權也不要冷冰冰地直接拉起系統彈窗授權,建議先用APP自己的彈窗向用戶禮貌地說明為什麼需要這幾個許可權,比如,讀取不到裝置資訊無法聯通免流,無法保證登入安全,讀取不到SD卡無法播放歌曲等,避免太生硬引起使用者的反感。特別是,因為本地化翻譯的原因,Google對於許可權的彈窗說明很不local,例如我們申請讀取裝置資訊的許可權時,系統的彈窗是“電話許可權”,這裡很容易引起使用者的誤解,所以, 合理的引導和解釋是必不可少的

2、應用啟動授權,需要一個殼

剛剛已經說到了,很多隱形的許可權和特性無關。那麼,如果我們直接啟動APP,使用者又還沒有授權的情況下,很多初始化邏輯很容易就因為沒有許可權crash了,即使沒有crash,後面也可能會有或多或少其他的問題。因此,我們需要在這些許可權完全授予前,禁止這些邏輯的執行。

做過啟動相關的同學都知道,攔截一個APP正常的啟動後面再恢復,是很複雜的一件事情,往往我們需要一個外殼來把業務邏輯的內殼隔絕開。就QQ音樂而言,我們很容易的就想到了dex載入的殼,需求也很類似,dex載入也需要優先於業務來做。順著這個思路,很自然地,我們就選擇了在dex的殼裡面做許可權的受阻邏輯,而且也很快很好的達到了預期的效果。相信現在大部分APP都是分dex的了,因此建議按照這個方式來做,可以節省很多的工作量。

四、Android許可權機制“亂象”

這裡要說的亂象,其實是和Android嚴重的碎片化有一定的關係。隨著國產ROM越來越個性,很多ROM在嘗試建立自己的許可權機制,有些甚至基於Android5.x就開放了原生的或者開發了自己的許可權機制。而面對這些情況,我們往往能做的非常有限,舉幾個例子。

1、讀取運動資料許可權

開發QQ音樂跑步電臺的過程中發現,在某國產ROM的一些機型上會提示“應用讀取運動資料許可權”的系統彈窗。可是,反覆查閱相關API發現,我們使用的計步相關的Sensor並不需要申請什麼許可權。可如果使用者選擇了拒絕,即使APP註冊了 Sensor,也收不到系統的回撥。後來聯絡該廠商的相關人員後,給出的答覆是,第三方APP無法檢查和申請這個許可權,這個許可權本身也屬於該廠商ROM自己的許可權機制。

類似的案例還有一個,就是在某廠商的手機管家,會一直提示QQ音樂嘗試讀取應用程式列表。其實,我們並沒有讀取應用程式列表,只是呼叫了PackageManager相關的一些API,就是觸發這個告警。

對於這類問題,我們懷疑,第三方ROM是在執行時檢測到了APP呼叫了相關的API後,進行許可權阻斷。這裡開發同學需要注意的是,被阻斷的API不一定會導致crash,但是可能導致我們獲取不到正確的返回值或者收不到系統的一些訊息回撥。

2、無法新增快捷方式

本來<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT"/>聲明後,我們就可以在桌面上建立快捷方式了,而且這個許可權也不是危險許可權。可是某些國產ROM,對於APP新增快捷方式限制的比較嚴,必須要使用者在設定裡面手動允許新增快捷方式後,APP才能最終成功的新增。這種情況,APP也不能知道是否能新增快捷方式,只能默默的新增失敗了。不過好在這裡受影響並不是主快捷方式,而且某些功能的快捷方式入口。

3、消失的桌面歌詞,懸浮窗許可權

QQ音樂桌面歌詞采用了向WindowManager裡面新增 View的方式實現。可是很多國產ROM很早就具備了懸浮窗許可權。一開始,我們將type改為 LayoutParams.TYPE_TOAST同時宣告<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>這個普通許可權,躲避了大多數系統的問題。可是,2016年底,隨著某ROM系統的升級,這一招也沒用了,大批使用者反饋爆發。

我們繼續嘗試檢測懸浮窗許可權,發現checkPermission("android.permission.SYSTEM_ALERT_WINDOW")返回的結果永遠是 true,因此這條路也走不通。

最終,經過各種查閱,發現這個懸浮窗許可權並不在Android6.0標準的許可權機制內,而是AppOpsManager裡面已經被隱藏了的一個開關位,對應於第24個開關。需要注意的是, AppOpsManager這個類很早就有了,但是很多ROM隱藏了checkOp的方法,好在最後發現通過反射仍舊可以呼叫這個方法檢測許可權是否開啟。

AppOpsManager manager = (AppOpsManager) context.getSystemService("appops");
try {
    Object object = invokeMethod(manager, "checkOp", op, Binder.getCallingUid(), getPackageName(context));
    return AppOpsManager.MODE_ALLOWED == (Integer) object;
} catch (Exception e) {
    MLog.e(TAG, "CheckPermission " + e.toString());
}

不過,要開啟懸浮窗許可權,不同ROM的路徑還不一樣,有的是在設定裡面,有的是在系統自帶的管家裡面,最後我們只能根據不同的ROM,給予使用者不同的引導,終於將反饋量降了下去。