月下載 40 萬次的框架是怎麼練成的

image

  • 有圖有真相,Jitpack 月下載將近 40 萬次,並且呈現持續上升態勢,戰績真實可查

image

  • 市面上的 Android 權限請求框架多如牛毛,那麼是什麼原因讓大家不約而同選擇了 XXPermissions 呢?我想最打動人心的應該是細節,今天就帶你深入了解想要做好一套框架,過程究竟會遇到什麼樣的牛鬼蛇神,以及我對問題的思考和解決方案,這可能是 Android 全網第一篇也是唯一一篇分享框架歷程系列的技術文章,因為超過 99% 的開源作者熬不到這一步就已經停更了,更別談分享心路歷程了,全程硬乾貨,請系好安全帶,我要準備發車了!

目錄

Intent 極端跳轉兜底機制

  • 在介紹這個功能之前,我先問大家一個問題,請你分析一下這段代碼是否有什麼問題?
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 }

image

android.content.ActivityNotFoundException: 
No Activity found to handle Intent { act=android.settings.MANAGE_UNKNOWN_APP_SOURCES (has data) }

image

  • 看完你是不是想吐槽一下?但問題已經存在,非理性的抱怨永遠解決不了問題,只有理性的分析和認真的思考才是唯一的出路。這個問題無非是 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 }

image

  • 說這些並不是想讓大家解決,而是讓大家意識到有這個問題,當然框架內部已經處理好這個問題,你能想到的所有問題,框架已經提前想到了,並且已經幫你處理好了,只需要一句代碼,調用 XXPermissions.startPermissionActivity 方法即可。假設你很好奇框架是怎麼實現的,又懶得看源碼實現,這點我也幫你想到了,在這裡我介紹框架是怎麼實現的,原理其實很簡單,框架獲取這個權限設置頁的時候,把能想到的 Intent 寫到 List 集合中,再篩選掉不存在的 Intent,然後挨個 Intent 進行跳轉,如果失敗就跳轉到下個,直到跳轉成功或者沒有下一個 Intent 了為止。

兼容請求權限 API 崩潰問題

  • 在介紹這個功能之前,我先問大家一個問題,請你分析一下這段代碼是否有什麼問題?
activity.requestPermissions(new String[]{Manifest.permission.CAMERA}, 1024);
android.content.ActivityNotFoundException: 
No Activity found to handle Intent { act=android.content.pm.action.REQUEST_PERMISSIONS pkg=com.android.packageinstaller (has extras) }

image

  • 出現這種情況有以下幾種可能:

    1. 廠商開發工程師修改了 com.android.packageinstaller 系統應用的包名,但是沒有自測好就上線了(概率較小)
    2. 廠商開發工程師刪除了 com.android.packageinstaller 這個系統應用,但是沒有自測好就上線了(概率較小)
    3. 廠商開發工程師在修改 Android 系統源碼的時候,改動的代碼影響到權限模塊,但是沒有自測好就上線了(概率較小)
    4. 廠商主動阉割掉了權限申請功能,例如在電視 TV 設備上面,間接導致請求危險權限的 App 一請求權限就閃退(概率較小)
    5. 使用者有 Root 權限,在精簡系統 App 的時候不小心刪掉了 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 方法,偷偷修改權限請求 IntentAction 成錯誤的,結果權限回調能正常回調。

  • 如果真的出現這種極端情況,所有危險權限的申請必然會走失敗的回調,但是框架能做的是:盡量讓應用不要崩潰,並且能走完整個權限申請的流程。

規避系統權限回調空指針問題

  • 在介紹這個功能之前,我先問大家一個問題,請你分析一下這段代碼是否有什麼問題?
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("獲取相機權限失敗");
        }
    }
}
  • 你可能會說:這不是正常在權限回調中處理權限請求結果,我平時就是這麼寫的,看起來沒什麼毛病啊,你是不是沒事找碴?
  • 如果我告訴你一件事,系統返回的 permissionsgrantResults 參數有可能會為空,你會不會相信呢?我知道你肯定不信,因為你看到了 permissionsgrantResults 參數上面都有 @NonNull 注解(點進去 Activity 源碼裡面看到的也是 @NonNull 注解),就代表系統返回的一定不為空,到這裡你肯定認為我在欺騙你。

image

image

  • 看完是不是不知道你是何種想法?系統這是要鬧哪樣?把參數標記成不為空結果卻給我返回空的,這難道不是在欺騙我的感情?問題已經存在,非理性的抱怨永遠解決不了問題,只有理性的分析和認真的思考才是唯一的出路。

  • 目前反饋這個問題的機型品牌有 vivo、小米、聯想;就說明這個問題大概率又是 Google 工程師挖的坑,解決這個問題的思路有兩種:

    1. 仍然要用 permissionsgrantResults 參數來判斷權限的狀態:使用之前需要先對數組對象進行防空判斷,然後繼續使用。
    2. 不再使用 permissionsgrantResults 參數來判斷權限的狀態:改用 checkSelfPermission 的方式來判斷權限狀態。
  • 雖然兩種都可以解決問題,但兩種略有區別,框架最終採用的是第二種,中國有一句老話叫:一次不忠終身不用,既然它能幹這種毫無底線的事情,就不得不防它還有其他小動作,例如以下場景:

    1. 返回的數組對象不為空,但是數組裡面沒有元素,如果事先不進行判斷,一調用就可能會觸發角標越界異常 ArrayIndexOutOfBoundsException
    2. 返回的數組對象不為空,數組裡面也有元素,但是 permissionsgrantResults 兩個數組的長度不一樣,如果事先不進行判斷,一調用就可能會觸發角標越界異常 ArrayIndexOutOfBoundsException
    3. 返回的數組對象不為空,數組裡面也有元素,兩個數組的長度也是正常的,但是返回的 grantResults 與實際不匹配,使用者明明授予了權限,但是這個數組存的卻是 -1PackageManager.PERMISSION_DENIED
  • 到這裡你是不是瞬間覺得解決這個空指針的問題好像不是只是加一下防空判斷那麼簡單?原來裡面的學問那麼多。在此我想跟大家說,無論是什麼問題,我都會認真對待,因為我追求的從來不是能解決問題就好,而是在所有能想到的解決方案中找出最優解。

應用商店權限合規處理

image

  • 現在國內的應用商店,在申請權限的時候,需要同步告知權限申請的目的,否則會被拒絕上架或更新,框架已經幫你考慮到這點了,目前已經開放相應的接口,你可以實現接口來這一需求,具體效果如下圖所示,可以下載 Demo Apk 體驗。

image

  • 雖然這個功能自己不需要框架提供接口也能實現,只需要在權限申請前顯示彈窗,權限申請完成取消彈窗就行,但是這樣會使你寫的代碼不優雅,因為這部分的代碼是直接寫死在 Activity/Fragment 類中的,不僅會增加 Activity/Fragment 類的複雜度,並且每個用到權限申請的 Activity/Fragment 類都要寫一份這樣的代碼,後續會變得難以維護,正是考慮到這個問題,框架才開放了這個接口,還在 Demo 工程實現了一份完整的案例供你參考,你不僅可以輕而易舉實現這個功能,過程無需操心實現的細節和是否有 Bug,因為你能想到的,我都幫你想到了,你沒有想到了,我也幫你想到了。

自動拆分權限進行請求

  • 在一些需求場景下,需要同時申請多種權限,例如麥克風權限和日曆權限,這個時候產品經理想要拆分成兩次權限進行申請,以便能夠分開顯示兩個權限的說明彈窗,這樣的設計會導致功能開發比較複雜,如果不拆分申請的情況下只需要在請求權限前後加一下顯示和關閉彈窗的邏輯就行了,現在要分成兩次權限申請後就不能這樣寫了,只能分開寫,分開寫就意味著要寫各種嵌套和回調,一想到要這樣做就一個頭兩個大,差點就把昨晚吃的宵夜給嘔出來。

  • 大家的苦,大家的痛,不用多說,我都懂,所以我在框架加了一套處理機制,會自動將你傳入的權限進行歸類分組,例如文件權限歸為一組,日曆權限歸為一組,然後會拆分成兩次權限申請,這個時候在搭配上框架開放的權限說明接口,這個接口會告訴你申請什麼權限,你再根據權限來展示具體的權限說明彈窗就行了,至此這個功能輕鬆又優雅地完成了,iOS 端還吭哧吭哧實現,你已經完成並提前下班了,沒有延遲,沒有痛苦,有的只是實現功能的爽感。

image

框架內部完全剝離 UI 層

  • 某些權限框架內部會實現一套權限說明彈窗的 UI 和邏輯,需要實現特定的接口才能修改,但是我認為這樣的設計是不合理的,因為展示權限說明彈窗並不是一個必須的操作,沒有它調用 requestPermissions 系統照樣會彈出授權框,另外涉及到 UI,框架內部設計的 UI 肯定無法滿足所有人的需求(吃力不討好),因為每個人拿到的設計圖都是不一樣的,所以最好的方案是,框架自己不要在內部寫 UI 和邏輯,而是設計好這方面相關的接口,然後全權交由外層實現,當然框架 Demo 模塊也會實現一份案例供外層借鑒(供外層直接抄代碼),這樣不僅能解決 UI 需求不一致的問題,還能減少框架的體積,一箭雙雕。

核心邏輯和具體權限完全解耦

  • 你在市面上能看到的能同時支持申請危險權限和特殊權限的框架,它們的代碼耦合度非常高,這樣會導致一個問題,例如你只拿它申請了危險權限,但是最終打包的時候,會連同特殊權限或者其他危險權限一起給打包到 apk 中的,這就好比你現在想吃炸雞,但店員告訴你只有點十個人的套餐才有炸雞,你心想自己一個人就算撐死也沒有辦法吃完這十個人的套餐,這種設計不是明擺著坑人嗎?雖然 app 多一些代碼不會跟人一樣被撐死,但是也不要肆意揮霍,這裡浪費一點,那裡浪費一點,開發完後一看 apk 體積 250 mb,還得考慮體積優化,關鍵是你還沒有辦法優化,因為這部分代碼是寫死在框架中,框架又是通過遠程依賴,你就得換成本地依賴去改,改了就意味著可能有 Bug 要增加很多自測的工作量,重要的是改了收益不高,但是風險極高,很容易改著改著將自己送上裁員名單。

  • 針對這個問題,框架有一個堪稱鬼才的設計方案,就是將不同權限的實現封裝成對象,你申請什麼權限就傳什麼對象,這樣沒有引用的對象就會在開啟代碼混淆的時候一並移除,這樣打 release 包的時候就不會有冗餘的代碼,更不會佔用多餘的 apk 的體積,真正做到了用多少算多少,再也不用為了想吃一塊炸雞而考慮要不要買個十個套餐,無需糾結,無需徘徊,在 XXPermissions 這裡可以做到分開買,想吃什麼買什麼,想吃多少買多少,老少皆宜,童叟無欺。

  • 當然對於某些框架,它即不支持任何特殊權限,也沒有針對某個危險權限做單獨的處理,只是簡單套用系統的 API,請求權限就直接用 context.requestPermissions,判斷權限就直接用 context.checkSelfPermission,這種算不算完全解耦呢?其實是算的,因為人家確實沒有在核心邏輯中直接依賴具體某個權限,但是這種框架不符合現實開發的需求,因為在一個商業化的 app 中不可能只請求危險權限,通知權限要吧?安裝包權限要吧?懸浮窗權限要吧?只要這些框架支持任何一個特殊權限,就會存在這個問題,當然不支持當然就沒有這個問題,關鍵是能不能做到既能支持,又能對代碼進行解耦呢?這才是難題,非常考驗你對框架的理解和代碼的設計,截止目前只有 XXPermissions 真正做到了既要又要。

image

自動適配後台權限


原文出處:https://juejin.cn/post/7547408384585629711

關於作者
精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝9   💬8   ❤️13
429
🥈
我愛JS
📝1   💬6   ❤️4
88
🥉
酷豪
📝1   ❤️1
51
#4
AppleLily
📝1   💬4   ❤️1
39
#5
💬3  
10
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次