99%
的開源作者熬不到這一步就已經停更了,更別談分享心路歷程了,全程硬乾貨,請系好安全帶,我要準備發車了!Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData("package:" + getPackageName());
startActivityForResult(intent, 1024);
ACTION_APPLICATION_DETAILS_SETTINGS
這個意圖,是的你沒有聽錯,就是直接阉割,這段代碼在這些設備上面運行,應用就會閃崩,沒有跟你開玩笑,android.content.ActivityNotFoundException:
No Activity found to handle Intent { act=android.settings.APPLICATION_DETAILS_SETTINGS dat=Package Name:com.xxx.xxx }
No Activity found to handle Intent act=android.settings.APPLICATION_DETAILS_SETTINGS
:ACTION_APPLICATION_DETAILS_SETTINGS
這個意圖,其他的意圖也會,一個都跑不了,你如果不信可以看這裡 Github Search No Activity found to handle Intent act=android
。android.content.ActivityNotFoundException:
No Activity found to handle Intent { act=android.settings.MANAGE_UNKNOWN_APP_SOURCES (has data) }
Intent
找不到了,最簡單有效的方法,就是在跳轉前對 Intent
進行判斷,如果存在這個 Intent
再跳轉,如果不存在就不跳轉,你如果要是真的那麼想問題就太片面了,事情往往沒有你想得那麼簡單,不存在的 Intent
跳轉會失敗,那你有沒有想過,存在的 Intent
也不代表一定能跳轉成功,你如果不信可以看這裡 Github Search Permission Denial: starting Intent
,現在知道為什麼叫天坑了吧?java.lang.SecurityException:
Permission Denial: starting Intent { act=android.settings.MANAGE_UNKNOWN_APP_SOURCES (has data) cmp=xxxx/xxx }
XXPermissions.startPermissionActivity
方法即可。假設你很好奇框架是怎麼實現的,又懶得看源碼實現,這點我也幫你想到了,在這裡我介紹框架是怎麼實現的,原理其實很簡單,框架獲取這個權限設置頁的時候,把能想到的 Intent
寫到 List 集合中,再篩選掉不存在的 Intent
,然後挨個 Intent
進行跳轉,如果失敗就跳轉到下個,直到跳轉成功或者沒有下一個 Intent
了為止。activity.requestPermissions(new String[]{Manifest.permission.CAMERA}, 1024);
act=android.content.pm.action.REQUEST_PERMISSIONS
,看完是不是瞬間顛覆了你的認知?android.content.ActivityNotFoundException:
No Activity found to handle Intent { act=android.content.pm.action.REQUEST_PERMISSIONS pkg=com.android.packageinstaller (has extras) }
出現這種情況有以下幾種可能:
com.android.packageinstaller
系統應用的包名,但是沒有自測好就上線了(概率較小)com.android.packageinstaller
這個系統應用,但是沒有自測好就上線了(概率較小)com.android.packageinstaller
這個系統應用(概率較大)經過分析 Activity.requestPermissions
的源碼,它本質上還是調用 startActivityForResult
,只不過 Activity
找不到了而已,目前能想到最好的解決方式,就是用 try catch
避免它出現崩潰,看到這裡你可能會有一個疑問,就簡單粗暴 try catch
?你確定不會引發其他問題?會不會導致 onRequestPermissionsResult
沒有回調?從而導致權限請求流程卡住的情況?雖然這個問題沒有辦法測試,但理論上是不會的,因為我用了錯誤的 Intent
進行 startActivityForResult
並進行 try catch
做實驗,結果 onActivityResult
還是有被系統正常回調,證明對 startActivityForResult
進行 try catch
並沒有影響 onActivityResult
的回調,我還分析了 Activity
回調方面的源碼實現,發現無論是 onRequestPermissionsResult
還是 onActivityResult
,回調它們的都是 dispatchActivityResult
方法,在那種極端情況下,既然 onActivityResult
能被回調,那麼就證明 dispatchActivityResult
肯定有被系統正常調用的,同理 onRequestPermissionsResult
也肯定會被 dispatchActivityResult
正常調用,從而形成一個完整的邏輯閉環。
補充測試結論:我在 debug 了 Activity.requestPermissions
方法,偷偷修改權限請求 Intent
的 Action
成錯誤的,結果權限回調能正常回調。
如果真的出現這種極端情況,所有危險權限的申請必然會走失敗的回調,但是框架能做的是:盡量讓應用不要崩潰,並且能走完整個權限申請的流程。
public final class XxxActivity extends AppCompatActivity {
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (permissions[0].equals(Manifest.permission.CAMERA) && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
System.out.println("獲取相機權限成功");
} else {
System.out.println("獲取相機權限失敗");
}
}
}
permissions
或 grantResults
參數有可能會為空,你會不會相信呢?我知道你肯定不信,因為你看到了 permissions
和 grantResults
參數上面都有 @NonNull
注解(點進去 Activity 源碼裡面看到的也是 @NonNull
注解),就代表系統返回的一定不為空,到這裡你肯定認為我在欺騙你。NullPointerException onRequestPermissionsResult
;看完是不是不知道你是何種想法?系統這是要鬧哪樣?把參數標記成不為空結果卻給我返回空的,這難道不是在欺騙我的感情?問題已經存在,非理性的抱怨永遠解決不了問題,只有理性的分析和認真的思考才是唯一的出路。
目前反饋這個問題的機型品牌有 vivo、小米、聯想;就說明這個問題大概率又是 Google
工程師挖的坑,解決這個問題的思路有兩種:
permissions
和 grantResults
參數來判斷權限的狀態:使用之前需要先對數組對象進行防空判斷,然後繼續使用。permissions
和 grantResults
參數來判斷權限的狀態:改用 checkSelfPermission
的方式來判斷權限狀態。雖然兩種都可以解決問題,但兩種略有區別,框架最終採用的是第二種,中國有一句老話叫:一次不忠終身不用,既然它能幹這種毫無底線的事情,就不得不防它還有其他小動作,例如以下場景:
ArrayIndexOutOfBoundsException
permissions
和 grantResults
兩個數組的長度不一樣,如果事先不進行判斷,一調用就可能會觸發角標越界異常 ArrayIndexOutOfBoundsException
grantResults
與實際不匹配,使用者明明授予了權限,但是這個數組存的卻是 -1
(PackageManager.PERMISSION_DENIED
)到這裡你是不是瞬間覺得解決這個空指針的問題好像不是只是加一下防空判斷那麼簡單?原來裡面的學問那麼多。在此我想跟大家說,無論是什麼問題,我都會認真對待,因為我追求的從來不是能解決問題就好,而是在所有能想到的解決方案中找出最優解。
Activity/Fragment
類中的,不僅會增加 Activity/Fragment
類的複雜度,並且每個用到權限申請的 Activity/Fragment
類都要寫一份這樣的代碼,後續會變得難以維護,正是考慮到這個問題,框架才開放了這個接口,還在 Demo 工程實現了一份完整的案例供你參考,你不僅可以輕而易舉實現這個功能,過程無需操心實現的細節和是否有 Bug,因為你能想到的,我都幫你想到了,你沒有想到了,我也幫你想到了。在一些需求場景下,需要同時申請多種權限,例如麥克風權限和日曆權限,這個時候產品經理想要拆分成兩次權限進行申請,以便能夠分開顯示兩個權限的說明彈窗,這樣的設計會導致功能開發比較複雜,如果不拆分申請的情況下只需要在請求權限前後加一下顯示和關閉彈窗的邏輯就行了,現在要分成兩次權限申請後就不能這樣寫了,只能分開寫,分開寫就意味著要寫各種嵌套和回調,一想到要這樣做就一個頭兩個大,差點就把昨晚吃的宵夜給嘔出來。
大家的苦,大家的痛,不用多說,我都懂,所以我在框架加了一套處理機制,會自動將你傳入的權限進行歸類分組,例如文件權限歸為一組,日曆權限歸為一組,然後會拆分成兩次權限申請,這個時候在搭配上框架開放的權限說明接口,這個接口會告訴你申請什麼權限,你再根據權限來展示具體的權限說明彈窗就行了,至此這個功能輕鬆又優雅地完成了,iOS 端還吭哧吭哧實現,你已經完成並提前下班了,沒有延遲,沒有痛苦,有的只是實現功能的爽感。
requestPermissions
系統照樣會彈出授權框,另外涉及到 UI,框架內部設計的 UI 肯定無法滿足所有人的需求(吃力不討好),因為每個人拿到的設計圖都是不一樣的,所以最好的方案是,框架自己不要在內部寫 UI 和邏輯,而是設計好這方面相關的接口,然後全權交由外層實現,當然框架 Demo 模塊也會實現一份案例供外層借鑒(供外層直接抄代碼),這樣不僅能解決 UI 需求不一致的問題,還能減少框架的體積,一箭雙雕。你在市面上能看到的能同時支持申請危險權限和特殊權限的框架,它們的代碼耦合度非常高,這樣會導致一個問題,例如你只拿它申請了危險權限,但是最終打包的時候,會連同特殊權限或者其他危險權限一起給打包到 apk 中的,這就好比你現在想吃炸雞,但店員告訴你只有點十個人的套餐才有炸雞,你心想自己一個人就算撐死也沒有辦法吃完這十個人的套餐,這種設計不是明擺著坑人嗎?雖然 app 多一些代碼不會跟人一樣被撐死,但是也不要肆意揮霍,這裡浪費一點,那裡浪費一點,開發完後一看 apk 體積 250 mb,還得考慮體積優化,關鍵是你還沒有辦法優化,因為這部分代碼是寫死在框架中,框架又是通過遠程依賴,你就得換成本地依賴去改,改了就意味著可能有 Bug 要增加很多自測的工作量,重要的是改了收益不高,但是風險極高,很容易改著改著將自己送上裁員名單。
針對這個問題,框架有一個堪稱鬼才的設計方案,就是將不同權限的實現封裝成對象,你申請什麼權限就傳什麼對象,這樣沒有引用的對象就會在開啟代碼混淆的時候一並移除,這樣打 release 包的時候就不會有冗餘的代碼,更不會佔用多餘的 apk 的體積,真正做到了用多少算多少,再也不用為了想吃一塊炸雞而考慮要不要買個十個套餐,無需糾結,無需徘徊,在 XXPermissions 這裡可以做到分開買,想吃什麼買什麼,想吃多少買多少,老少皆宜,童叟無欺。
當然對於某些框架,它即不支持任何特殊權限,也沒有針對某個危險權限做單獨的處理,只是簡單套用系統的 API,請求權限就直接用 context.requestPermissions
,判斷權限就直接用 context.checkSelfPermission
,這種算不算完全解耦呢?其實是算的,因為人家確實沒有在核心邏輯中直接依賴具體某個權限,但是這種框架不符合現實開發的需求,因為在一個商業化的 app 中不可能只請求危險權限,通知權限要吧?安裝包權限要吧?懸浮窗權限要吧?只要這些框架支持任何一個特殊權限,就會存在這個問題,當然不支持當然就沒有這個問題,關鍵是能不能做到既能支持,又能對代碼進行解耦呢?這才是難題,非常考驗你對框架的理解和代碼的設計,截止目前只有 XXPermissions 真正做到了既要又要。