Android 換膚技術(shù)

1、前言

目前有非常多的產(chǎn)品支持換膚技術(shù),比如QQ空間,各大手機(jī)廠商都支持切換主題包。

換膚技術(shù)能為公司帶來(lái)經(jīng)濟(jì)效益,也能為程序員帶來(lái)更多的便利,可以賦能主題包,讓合作公司自己折騰。

本文總結(jié)以下兩種換膚場(chǎng)景:

  • 手機(jī)主題切換
  • QQ空間主題切換

2、手機(jī)主題切換

手機(jī)不同主題效果

通過(guò)下載不同主題包,設(shè)置使用不同主題包即可實(shí)現(xiàn)皮膚切換,那么主題包中到底有些啥內(nèi)容呢?

oppo論壇中下載主題包,主題包后綴為.theme,但它的實(shí)質(zhì)是一個(gè)壓縮包,將后綴改為.zip,解壓。

oppo主題包內(nèi)容

內(nèi)部很多以包名命名的無(wú)后綴文件,其實(shí)它們也都是壓縮包,加后綴,解壓縮發(fā)現(xiàn),里邊全是圖片和colors.xml之類。

從目前來(lái)看,主題包里無(wú)代碼,只有資源,應(yīng)該是進(jìn)行了資源重定向,于是換膚得以完成。

Android資源查找分析 文章中已經(jīng)詳細(xì)分析資源的查找過(guò)程,本文以圖片查找為主,添加 java 層的邏輯,分析換膚的可能性。

圖片查找過(guò)程
  • 調(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)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,247評(píng)論 6 543
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,520評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 178,362評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 63,805評(píng)論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,541評(píng)論 6 412
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 55,896評(píng)論 1 328
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,887評(píng)論 3 447
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 43,062評(píng)論 0 290
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,608評(píng)論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,356評(píng)論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,555評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,077評(píng)論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,769評(píng)論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 35,175評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 36,489評(píng)論 1 295
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,289評(píng)論 3 400
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,516評(píng)論 2 379

推薦閱讀更多精彩內(nèi)容