1、前言
目前有非常多的產(chǎn)品支持換膚技術(shù),比如QQ空間,各大手機(jī)廠商都支持切換主題包。
換膚技術(shù)能為公司帶來(lái)經(jīng)濟(jì)效益,也能為程序員帶來(lái)更多的便利,可以賦能主題包,讓合作公司自己折騰。
本文總結(jié)以下兩種換膚場(chǎng)景:
- 手機(jī)主題切換
- QQ空間主題切換
2、手機(jī)主題切換
通過(guò)下載不同主題包,設(shè)置使用不同主題包即可實(shí)現(xiàn)皮膚切換,那么主題包中到底有些啥內(nèi)容呢?
從oppo論壇中下載主題包,主題包后綴為.theme,但它的實(shí)質(zhì)是一個(gè)壓縮包,將后綴改為.zip,解壓。
內(nèi)部很多以包名命名的無(wú)后綴文件,其實(shí)它們也都是壓縮包,加后綴,解壓縮發(fā)現(xiàn),里邊全是圖片和colors.xml之類。
從目前來(lái)看,主題包里無(wú)代碼,只有資源,應(yīng)該是進(jìn)行了資源重定向,于是換膚得以完成。
Android資源查找分析 文章中已經(jīng)詳細(xì)分析資源的查找過(guò)程,本文以圖片查找為主,添加 java 層的邏輯,分析換膚的可能性。
- 調(diào)用 getDrawable 接口函數(shù)
- getValue,調(diào)用c層接口方法,獲取圖片資源的相關(guān)信息,包含路徑等等
- loadDrawable,查看要獲取圖片是否已有緩存,如果沒(méi)有則讀取文件,緩存保存在 Resources.mDrawableCache 中。
- loadDrawableForCookie,根據(jù)圖片路徑,獲取文件流,根據(jù)文件流生成圖片
如何實(shí)現(xiàn)主題切換,如何實(shí)現(xiàn)資源重定向,現(xiàn)在應(yīng)該有明確思路。補(bǔ)充一句,圖片資源好處理,但xml資源不好處理,Android資源查找分析 文章中指出,xml資源都將被編成二進(jìn)制文件,即使重定向讀取也會(huì)出錯(cuò),這才是整個(gè)主題包機(jī)制中最難的。
這部分內(nèi)容將不給出具體思路和代碼了,保密要求,防止相關(guān)公司追究本人責(zé)任,各位讀者自己嘗試。
3、QQ空間主題切換
QQ空間主題切換 是眾多換膚機(jī)制的一個(gè)場(chǎng)景,本人也不保證它的原理和我下文說(shuō)的一樣。但本文中提到的兩個(gè)場(chǎng)景有何不同呢?
一個(gè)是系統(tǒng)層面地?fù)Q膚,一個(gè)只是應(yīng)用層面的換膚,而且QQ空間肯定無(wú)法更換系統(tǒng)源碼,所以QQ空間用不了 第2節(jié) 所說(shuō)的內(nèi)容。
QQ空間換膚可通過(guò)動(dòng)態(tài)加載完成。
為什么Resources能夠讀取自身apk資源呢?Android資源查找分析 文中指出,在Resources初始化時(shí),Resources的成員變量AssetManager添加了自身apk的路徑,所以Resources能夠讀取自身apk資源。
如果要通過(guò)動(dòng)態(tài)加載讀取插件apk的資源,那么構(gòu)造一個(gè)Resources,同時(shí)添加插件apk的路徑,是否就可以讀取插件的資源呢?
public void loadRes() {
AssetManager assetManager = null;
try {
assetManager = AssetManager.class.newInstance();
Method addAssetPath = AssetManager.class.getMethod("addAssetPath", new Class[] { String.class });
addAssetPath.invoke(assetManager, mPluginDir);
Log.i("okunu", "dir = " + getActivity().getApplicationInfo().sourceDir);
addAssetPath.invoke(assetManager, getActivity().getApplicationInfo().sourceDir);
} catch (Exception e) {
Log.i("okunu", "e", e);
e.printStackTrace();
}
mResources = new Resources(assetManager, super.getResources().getDisplayMetrics(), super.getResources().getConfiguration());
}
因?yàn)閍ddAssetPath是隱藏方法,所以只能通過(guò)反射調(diào)用,現(xiàn)在已經(jīng)獲取了Resources,但資源id還沒(méi)有得到,因?yàn)椴寮馁Y源id,無(wú)法在主apk內(nèi)通過(guò)R獲取,主apk內(nèi)只能獲取到主apk內(nèi)的資源id。
為了獲取插件資源id,利用動(dòng)態(tài)加載的方式,定義接口,使用反射獲取資源id。
public void getTail() {
getPluginDir();
DexClassLoader loader = new DexClassLoader(mPluginDir, getActivity().getApplicationInfo().dataDir, null, getClass().getClassLoader());
try {
Class<?> clazz = loader.loadClass("com.okunu.demoplugin.TailImpl");
Constructor<?> constructor = clazz.getConstructor(new Class[] {});
// 兩種構(gòu)造方式都記錄下,可通過(guò)構(gòu)造函數(shù)也可以直接newInstance,前提是這個(gè)類有無(wú)參構(gòu)造函數(shù)
// Object instance = constructor.newInstance(new Object[]{});
// mTail = (ITail) instance;
mTail = (ITail) clazz.newInstance();
// int id = mTail.getImageId();
// Log.i("okunu", "id = " + id);
} catch (Exception e) {
Log.i("okunu", "e", e);
e.printStackTrace();
}
}
int id = mTail.getImageId();
mImage.setImageDrawable(mResources.getDrawable(id));
通過(guò)上述代碼,就能完美獲取插件apk的資源。不過(guò)仍然有個(gè)問(wèn)題,構(gòu)造的mResources只能獲取插件apk的資源,無(wú)法獲取本身apk的資源,因?yàn)闆](méi)有添加自身apk的路徑。
/*
* 需要將本地的apk路徑也添加,否則無(wú)法獲取本地的資源 如果不添加下面這句話將要報(bào)錯(cuò)
* mResources是可以直接使用系統(tǒng)資源的,因?yàn)閍ssetManager在構(gòu)造的時(shí)候就添加了系統(tǒng)資源路徑了
*/
addAssetPath.invoke(assetManager, getActivity().getApplicationInfo().sourceDir);
4、動(dòng)態(tài)加載說(shuō)明
java通過(guò)虛擬機(jī)加載類。常規(guī)下,apk運(yùn)行之前所有類已經(jīng)加載完畢,動(dòng)態(tài)加載正好相反,apk運(yùn)行的時(shí)候才通過(guò) ClassLoader 加載插件中的類,所以叫動(dòng)態(tài)加載。
DexClassLoader loader = new DexClassLoader(mPluginDir,
getActivity().getApplicationInfo().dataDir,
null,
getClass().getClassLoader());
Android中動(dòng)態(tài)加載最核心的調(diào)用就是上面的代碼,各參數(shù)分別是啥意思呢?
mPluginDir, 代表著插件apk的路徑,真實(shí)地絕對(duì)路徑
-
第二參數(shù),代表著自身apk的緩存目錄,??吹接袃煞N寫法,getApplicationInfo().dataDir,獲取應(yīng)用的data/data目錄,另一種寫法是 getDir("dex", 0).getAbsolutePath(),這種寫法會(huì)在data/data目錄下創(chuàng)建一個(gè)新目錄。具體值如下:
dex = /data/data/com.okunu.app/app_dex data = /data/data/com.okunu.app
第三參數(shù),可填null
第四參數(shù),是生成的DexClassLoader的父ClassLoader
理解動(dòng)態(tài)加載的內(nèi)涵最重要,如果報(bào)錯(cuò)說(shuō)找不到類,有可能是插件apk報(bào)錯(cuò),修改異常即可。
5、雜談
還有其它方法能夠?qū)崿F(xiàn)換皮膚功能,但本人目前能確認(rèn),感覺(jué)比較好用的只有以上兩種,歡迎讀者們提供新思路。
第2節(jié)中曾談到,Resources中的緩存mDrawableCache,讀取圖片時(shí)先去緩存中查找,如果緩存中已有,則直接返回。
曾讀過(guò)大神文章,通過(guò)反射機(jī)制更改 mDrawableCache 中的對(duì)象,也能實(shí)現(xiàn)換膚。本人嘗試失敗,但覺(jué)得還是有一定可行性,不過(guò)此方法有兩個(gè)大缺點(diǎn):
- DrawableCache不是公開(kāi)類,要在應(yīng)用中使用必須去android源碼中把它和相關(guān)的類一起扒出來(lái),放到應(yīng)用中才行,得偽裝sdk
- 整體方案太折騰,效果一樣,但比動(dòng)態(tài)加載方案折騰多了
因此,本人不再詳細(xì)研究了,也不貼代碼,各位讀者有興趣自己嘗試。
另外一種常見(jiàn)的換膚方案,就是夜間模式了。不過(guò)夜間模式比較簡(jiǎn)單,就是定義兩種不同的 theme ,通過(guò)設(shè)置主題,然后重新啟動(dòng)activity實(shí)現(xiàn),比較簡(jiǎn)單,耦合相當(dāng)大,本文就不再詳述了
所有代碼均上傳至本人的github空間,歡迎訪問(wèn)。