淺談「Runtime Resource Overlay」
概要
簡介
本篇要跟各位介紹一套與資源覆蓋有關的「Android」框架:「RROs」,其全名為「Runtime Resource Overlays」,它的好處是它允許在不修改原有程式碼的情況下,達到置換「應用程式」資源的目的。
由來
「RROs」起初是由「Sony」公司所研發的開源框架,其詳細相關資訊請參考「Sony contributes Runtime Resource Overlay framework to Android」一文。
對於多數「APPs」開發者來說,如果要更換「APPs」的資源就直接修改原始碼就好,何必研發一套專門置換「UI」框架呢?
的確,對於大多數「Android APPs」開發者而言,「RROs」非常雞肋,事實上,它開發的目的本就不是針對一般應用的開發者,而是針對系統製造廠或運營商,如「Sony」、「Asus」、「三星」或「小米」等。
原因要從「Android OS」的產品週期說起,其第一步是由「Google」釋出,該階段稱為「Original SDK」,也是最初的「Android SDK」,如下圖的階段「1」:
但「Android OS」是可以廣泛應用於各種設備的作業系統,例如「Android Phone」、「Android TV」、「Android Wear OS」、「Android Things」…等,而不同種設備間,其硬體可能完全不同;即使是同種設備也會有「廠牌」、「型號」的差異。
以「手機」為例,不同的廠牌、型號,其選用的「芯片」規格可能都存在差異,而不同的硬體配置,其驅動、組態設定可能都不同;在如此情況下,「Original SDK」顯然不可能支援所有類型的「芯片」,所以該部份的配置將由「芯片製造商」負責,如上圖「2」。
因此,由「Google」釋出的「Original SDK」在經過「芯片廠商」定製後,就成為某系列產品的「Pure BSP SDK」,並產生出「產品原形」,通常我們會稱之為「公版」,然後系統廠就會根據「品牌商」的需求,對原形產品加以客製,如產品外觀、軟體界面、特定功能…等,如上圖階段「3」。
而經過「品牌客製化」的商品就會再出貨給「品牌運營商」,並由其負責銷售,如上圖階段「4」,最後產品才會到終端客。
備註:上述的內容僅是流程概念,其詳細的分工會根據廠商的性質與合約的內容而有所差異,例如該廠為「OEM」、「ODM」或「OBM」等。
關鍵點是,許多不同品牌、型號的手機,其「硬體規格」是類似甚至是相同的,又或是同品牌、同型號,不同產品線;在這些情況下,造成差異的關鍵在於「品牌客製化」,通常在該階段都必須進行大量「客製化改動」。
在「Android」產品週期的圖片中,在階段「3」到「4」之間,也就是所謂的「系統製造廠」或「品牌運營商」的工程師,就會因為「品牌需求」針對「芯片廠」所提供的源碼進行客製化的改動。
問題是,在「客製化」的過程幾乎無法避免對源代碼進行改動,因此專案程式碼的狀況就會如下:
使得「原始代碼」與「客製化代碼」難以區分,即使是在以版本控制軟體管理的情況下,其仍舊免不了凌亂。
此時,「Sony」就注意到了,在實際上,「品牌客製化」有很大的一部分是屬於「畫面上」的更動,例如「風格」、「樣式」、「圖片」、「顏色」與「文字」⋯等,因此,「Sony」就針對這項需求開發了「Android Resource Overlays」這套框架。
其機制與「Android OS」對於資源採「外部化」的管理方式有關,簡單的說就是「Android」會將圖片、字串和佈局⋯等資源放置在外部,並建立相應的目錄,當系統在運行時才根據當前配置需求來獲取需要的資源。
備註:關於「Android OS」資源存取機制的完整說明請參考「App resources overview」。
而「Resource Overlays」就是利用該機制的特性,以達到在不改動原有程式碼的情況下,讓新的「res」置換原有「res」,如下所述:
原理知道了,而其具體的方式就是藉由撰寫一個不含有程式碼的「Overlays APK」去覆蓋原本的「Original APK」,如下:
簡單的說就是在「RROs」的技術中,「UI Customization」的部分將會被編譯成「APK」。
由上述可知,若套用「Runtime Resource Overlays」的機制,那麼在最理想的情況下,「客製化」的代碼將與「源碼」分離,使得「系統製造廠」僅需維護「品牌客製化」的部分。
換句話說,在最理想的情況下,「UI」的更換僅需重新安裝「APK」即可,如下圖:
備註:「Runtime Resource Overlays」僅針對「res」的覆蓋,若是跟硬體操作相關的,則應使用「APK」+「HIDL」的方式。
正文
在上述,我們僅介紹了「RROs」的由來與其所解決的問題。
事實上,在「Sony」所貢獻的原始模型中,「Android Resource Overlays」可以被分為「RROs」:Runtime Resource Overlays 與「SROs」:Static Resource Overlays。
Static Resource Overlays
簡稱為「SROs」,與「RROs」相同,它同樣是用來修改應用程序的資源屬性,但與「RRO」不同的是,「SRO」是發生在「編譯時期」,而非「執行時期」,如下所述:
其作法很簡單,首先在「AOSP」項目中放入「Overlays」所需的相關資源,其目錄通常是「vendor」底下的「overlays」資料夾,接著藉由編寫「.mk」檔將其關聯「原始 APK」,如此一來,在編譯過程中藉由「AAPT」打包的時後,就會將「overlays」底下的資源包入「APK」中,如下圖:
該機制與「RROs」不同,其過程比較像是「APK」的「重新打包」。
雖然「SROs」仍然是在「源目錄」中加入「UI customized code」,不過至少是區分目錄的,「UI customized code」並不會混在源碼中。
Static RROs
然而,經過幾次的演進,現在我們能在官方文件找到的是「static RROs」,如下圖:
之所以冠上「static」,是因為它與「SROs」一樣,必須將「Overlays」所需的「Resource」放在「AOSP」專案中,並一起編譯成「映像檔」。
但重點是,「static RROs」的「資源置換」時間點並不是在編譯時期,而是與「RROs」相同,在執行時期置換,因此,它才被稱為「static RROs」,事實上,筆者認為「static RROs」只是「RROs」的其中一種機制。
如何配置「static RROs」呢?
其關鍵在於須將配置清單中的「isStatic」屬性的值設為「true」,並同時給定「priority」的值,如下所述:
當「isStatic」設定為「true」,資源覆蓋默認就會「enable」,此外,該資源也不可以再被其它資源覆蓋;若遇到有多個「Static」資源要覆蓋同個目標資源時,則就會依據「priority」屬性就會決定其覆蓋優先權。
但有一點必須注意,「static RROs」機制在「Android 11」後被移除了,如下:
事實上,「static RROs」被移除是可以預期的,因為它存在著諸多的限制,其中最明顯的是「static RROs」依然是必須去改動源碼。
要知道的是,「源碼異動」可能會涉及安全性、「兼容性」,使得某些需要經過「Google XTS」認證的產品無法通過。
而在達到相同的效果下,「RROs」的機制僅需去改動並重新安裝「APK」即可,其無論是在「便捷性」還是「安全性」上,都是「static RROs」無可相比。
備註:對於「系統製造廠」或「品牌商」而言,「源碼異動」幾乎意味著新版本的發佈,在多數的情況下,其必須經過繁複的程序,例如產品的功能測試、認證測試等,其代價之大,遠大於「APK」的更新。
Runtime Resource Overlays
本文的主角,機制與原理在先前已經提過,而其主要目的就是「Overlaying Resources」,如下圖:
具體的操作方式就是藉由撰寫一個不含有程式碼的「Overlays APK」去覆蓋原本的「Original APK」,此外,由於該機制預設為「關閉」,因此還必須經過「啟用」,如下圖:
那「Overlays APK」要怎麼製作呢?「Overlays APK」的組成大致上可以分為兩個部分,分別是「配置文件」與「資源」,但根據「Android」的版本不同,其需要的配置略有差異。
Android 9
在「9」中,「Overlays APK」的配置設定都在「AndroidManifest」中。
其關鍵是必須在「Overlays APK」的「AndroidManifest.xml」列表中設置一個「<overlay>」標籤,系統會根據該標籤將該「APK」判定為「Overlays APK」,如下:
該標籤有兩個重要的屬性,分別為「必要屬性:android:targetPackage」以及「可選屬性:android:targetName」;其中,「targetPackage」是用來指定要被覆蓋的「Package」。
備註:若目標資源屬於「框架層」,則「targetPackage」設「android」即可。
另外,「android:targetName」則是用來指定要覆蓋的資源名稱,而該屬性與「Android 10」以後才有的「<overlayable>」標籤有關,因此,在「9」的版本中,忽略即可。
此外,有一點要特別注意,由於疊加層無法疊加代碼,因此要避免代碼中還有「DEX」文件,並且清單中「<application >」的「android:hasCode」屬性必須設置為「false 」,如下:
至此,清單的配置已經完成。
備註:若在「<overlay>」加上「isStatic」屬性,並設置為「true」,它就會是「static RROs」,如本文先前的介紹。
而資源的部分,在「Android 10」之前,資源都是根據其名稱覆蓋,如下圖所述:
此外,在「Android 9」以下的版本,由於沒有「權限分級」,因此一律視為系統層級,因此必須是「預安裝」或使用「系統簽章」的「APK」才能具有覆蓋功能,如下:
Android 10
事實上,「Android 10」與「Android 9」最大的差異是,在「Android 10」以後,「Android」提供了一組「<overlayable>」標籤可以使用;並且要求須標記為「<overlayable>」的資源才可以被覆蓋,如下:
但在「預安裝」或使用「系統簽章」的情況下,則不受此限制,如下:
另外,在「<overlayable>」標籤中有一個重要的屬性:「name」,其對應的就是「AndroidManifest」中「<overlay>」的屬性:「targetName」。
此外,若「RROs」要生效,還必須是唯一「對應名稱」,完整的敘述如下:
簡單整理一下,「Android 9」與「Android 10」在「AndroidManifest」中的配置方式多數是相同的,其差異僅在於「<overlay>」的「targetName」。
而在「資源清單」中,要被覆蓋的資源需用「<overlayable>」標籤所標記,如下:
最後是「資源」本身的部分,在「Android 10」之前,資源都是根據其名稱覆蓋。
備註:關於覆蓋的詳細規範,請參考「Restricting overlayable resources」。
另外,若版本在「9」以下,或是版本為「10」但預期被覆蓋的資源沒有「<overlayable>」的標記,那麼就必須是「預安裝」或使用「系統簽章」的「APK」才能具有覆蓋功能,如下:
順帶一提,「<overlayable>」標籤中有個屬性「policy」,其會影響覆蓋的權限,如下:
但由於在系統廠上,一般都是使用「系統簽章」,因此該限制機制我們就不多談了。
Android 11
在「11」之後,「RROs」有了較大幅度的改動,首先是在配置文件的部分多了「Resource Map」和「OverlayConfig」;另外還有就是「static RROs」將被移除,請參考「Changes in Android 11」,如下:
首先,要介紹的是「Resource Map」,如下:
不同於「10」之前的版本,其資源映射不再是依賴「資源名稱」,而是會根據「Resource Map」,然而,該文件的路徑是定義在「AndroidManifest」的「resourceMap」屬性中,如下:
除了「Resource Map」外,還有另一個文件可以用於配置「RROs」,其就是「OverlayConfig」,如下:
簡單的說,就是我們可以藉由該文件,依不同的覆蓋層級的進行針對性覆蓋,使得「RROs」更有彈性,但目前筆者比較少使用之,就不多談了。
另外,還有是「資源定義了多配置」的情況,例如「i18n」,在「10」之前,這問題並不容易處理,因為「Overlay APK」與「Original APK」共享資源「Id」,但在「11」以後,「Overlay APK」與「Original APK」將具有屬於自己的獨立「Id」,詳細資訊請參考「Resolving resources」,如下:
在介紹完「APK」的配置以後,剩下就的是「打包」和「啟用」。
打包成「APK」
在「10」之前,請用「AAPT」的指令,請在放有「AndroidManifest.xml」的目錄中,輸入指令如下:
aapt package -f -M AndroidManifest.xml -S res/ -I ~/Android/Sdk/platforms/android-28/android.jar -F <overlays_apk_name.apk>
若是「11」或以上,請參考「Building the package」。
在預設的情況下,「RROs」是「Disable」,因此,我們需要藉由加入「權限:CHANGE_OVERLAY_PACKAGES」,並透過「OverlayManager API」來控制啟用和禁用。
此外,我們亦可以藉由指令操作的啟用「RROs」,指令如下:
adb exec-out cmd overlay enable <package_name>
而「Android」也為開發者提供「Debugging」的指令,筆者介紹兩個常用的指令,第一個指令是用來確認「RROs」是否生效,如下:
adb shell cmd overlay list
輸入指令後,其會顯示「Overlays」的清單,以上下兩行為一組,上行顯示的是「Original APK Package」,下行則是「Overlays APK Package」,在下行前面會有個中括號,其中若是「空白」,代表尚未啟用,若是「X」,則代表已經啟用,如下:
此外,我們也可以藉由指令來確認「RROs」所覆蓋的資源項目,指令如下:
adb shell idmap2 dump --idmap-path [file]
其原理簡單說就是「OverlayManagerService」會藉由「idamp2」將資源的映射「Id」儲存至「/data/resource-cache」,因此,若當「Overlays」一旦生效,則我們可以藉由讀取「resource-cache file」來查詢映射的項目,其位置如下:
結果如下:
範例實作:「RROs Signed」
筆者簡單的弄個範例,演示一下。
在實作部份,由於「RROs」的機制在「Q」、「R」的改動較大,而筆者以現有環境來示範;筆者所使用的系統平台是「10」,設備是「ATV」;並且是具有系統金鑰的情況下。
Original APK
首先,筆者簡單撰寫一個「RROs APP」,其「UI」為僅由一個「TextView」和一個「ImageView」所組成,而本範例要置換的資源就是「TextView」的字串,與「ImageView」的圖片,「XML」如下:
要特別注意的是,要置換的資源若為「字串」,例如「文字」、「色碼」等,則必須抽出。
資源清單如下:
圖片資源的路徑如下:
備註:筆者將「系統簽章」來簽名,因此也就不設置「<Overlayable>」標籤了。
應用程式運行時的畫面如下:
Overlays APK
因為筆者的版本是「10」,沒有「Resources Map」的機制,因此存放資源的路徑必須與符合「res」資料夾的結構,如下:
而「AndroidManifest.xml」的配置如下:
備註:因為筆者是「系統簽章」,因此無須設置屬性「targetName」。
而字串資源的配置如下:
圖片的檔名與路徑都與「RROs APK」相同,但圖檔不同:
當配置完成後,回到存放「AndroidManifest.xml」的目錄,使用「AAPT」的指令將其打包,指令如下:
aapt package -f -M AndroidManifest.xml -S res/ -I ~/Android/Sdk/platforms/android-28/android.jar -F rros_overlay.apk
完成後會產生一個檔名為「rros_overlay」的「APK」,如下:
然後,我們需要對「APK」進行「系統簽章」,至於如何簽章,詳細請參考筆者的另外一篇文章「如何使用『signapk.jar』對『APK』進行簽章」,指令如下:
java -Djava.library.path=./lib64/ -jar signapk.jar platform.x509.pem platform.pk8 rros_overlay.apk rros_overlay-signed.apk
最後,將「Signed」的「APK」安裝,指令如下:
adb install rros_overlay-signed.apk
顯示如下:
此時,就已經完成安裝了,我們可以下「overlay list」的指令來確認,指令如下:
adb shell cmd overlay list
顯示如下:
此時代表「RROs」尚未被啟用,啟用指令如下:
adb exec-out cmd overlay enable com.rick.rrosoverlay
顯示如下:
下完啟用指令後,記得只用「overlay list」來確認,如下:
當有「X」就代表覆蓋成功,此時,若在運行就會「RROs APK」,畫面就會如下:
另外,我們也可以藉由查訊「resource-cache」的指令來確認哪些資源被覆蓋,如下:
備註:由於該範例受限於「系統簽」,代碼部分相對簡單,因此筆者就不額外提供完整專案的連結了。
總結
由於「RROs」的技術在最近幾版的更新上都有著一定幅度上的異動,而網絡上關於「RROs」的資料也大多都過於老舊,或不齊全。
因此,筆者針就對自己工作所需而整體這篇文章,但筆者目前的主要工作環境是「Android 10」,對於「11」的著墨相對較少,但「RROs」改動最大的是在「Android 11」,因此,若有地方描述不清楚或不完整,還請見諒。
備註:其資料來源皆為「Google」的官方文件。
完整程式碼連結:My GitHub