http://www.cnblogs.com/punkisnotdead/p/4968851.html
以前寫的這篇文章 可以高仿出 知乎 新浪微博等 絕大多數app的換膚技術,但是遺漏了騰訊的效果,
實際上騰訊的這方面比 上述app要稍微複雜一些,有一點像 現在流行的外掛技術。
其實也可以理解,騰訊畢竟是可以靠 皮膚賺錢的公司,所謂 “沒錢玩你麻痺” 說的就是騰訊。
靠這個賺錢當然做的會更好一點。今天就來看看騰訊是咋做的。我們也來仿一仿!
就拿qq空間來說吧。
你看我使用了一個qq空間的 黑色主題。不使用別的是因為別的要開通什麼黃鑽綠鑽,但是顯然我沒有錢。 然後去命令列下看點東西:
記住這個路徑,這個時候我要提一下,你只有使用了 特殊的主題以後 這個theme.xml才有值的。
你如果不使用這個你下載的主題 用預設的主題的話就會這樣:
我一開始分析騰訊app的時候 這裡也卡過一會 後來發現是你得使用主題以後 這個theme 檔案才有變化~~
回到前面那個有內容的xml檔案看一看。他指向了一個地址,我們就去這個地址下面看看 到底是什麼
一看到這個,相信大家 就都明白了,這不就是個apk麼?我們打包出去的apk 解壓縮以後不就是這些內容麼?
所以這裡你看 騰訊的做法事 把新的皮膚apk 放在自己data data 包名 這個路徑下的某個資料夾內。
但是並沒有安裝他 對吧。
新浪微博 我們那會分析的時候 他們的皮膚包就是得下載下來以後 再安裝一次的。從使用者體驗上來說,騰訊的
這個明顯更加優秀。
到這裡 應該很多人就明白了,騰訊的所謂換膚技術,無非就是 把新的皮膚包 下載到自己的安裝目錄下面,
然後自己的app 去載入這個皮膚包apk裡的 資源 即可(注意這裡要再強調以下,新浪的皮膚apk是安裝好了的,
而騰訊的這個根本沒讓你安裝)!這個就是騰訊旗下app 換膚的原理。你可以開啟你的設定---應用裡面看一下:
你看明顯微博的皮膚都已經在應用列表裡面了,但是騰訊的可沒有~~~
我們下面就來仿照騰訊的 來實現以下這個效果。
這個效果的關鍵點 其實就在於 如何在我們的主apk裡面 載入到 主題apk裡的資源。並且這個主題apk 是不可以被安裝的。
就好像高德地圖sdk 裡提供的那些資源包一樣,也是不需要安裝 自動就可以使用的。
這個資源包裡面 一般都包含 字型顏色啊 背景色啊 背景圖啊 複雜的甚至會包含佈局檔案!
那我現在就做一個最簡單的效果,主apk裡 有一個tv 他有一個背景色,然後我們點選更換主題以後 這個tv就會 把這個背景色
更換成一個 背景圖(我這個背景圖是用的林熙蕾的照片)。當然了 我們這個背景圖顯然是放在我們的主題apk裡的。我們的
主apk裡當然是不會有這張圖的,不然還做個毛啊!(如果能做出這個demo 那麼很顯然其他的就全部都能通了)
我們首先來做一下這個主題apk,
第一步,把我們的背景圖片放到相映的路徑下:
第二步:定義主題apk裡的 一個類和一個方法:
1 package com.example.administrator.themeapk; 2 3 import android.content.res.Resources; 4 import android.graphics.drawable.Drawable; 5 6 /** 7 * Created by Administrator on 2015/12/24. 8 */ 9 //這裡我們因為是demo演示 所以實際上就只有一個返回Drawable的方法 10 //實際上你可以自己往下面寫,返回任何資源,比如theme,比如string,比如color,甚至資原始檔等等 11 public class ResourceUtils { 12 13 public static Drawable getTextViewBackGroundDrawable(Resources resources) { 14 return resources.getDrawable(R.mipmap.lxl); 15 } 16 17 //可以思考一下 為什麼這個地方我們不用這個context作為引數的方法,把這個方法給註釋掉了。 18 //其實原因也很簡單 一個Context對應著唯一的一個Recource,如果我們想要在主apk裡呼叫 19 //我們主題apk裡的資源,那這個context引數就無法構造了,因為主apk裡只能拿到自己的context, 20 //肯定是拿不到主題apk裡的context的。所以我們要用上面的Resources這個引數,因為雖然我們拿不到 21 //主題的context,但是我們可以把主題apk裡的resource 加入到主apk裡的resource。 22 // public static Drawable getTextViewBackGroundDrawable(Context context) 23 // { 24 // return context.getResources().getDrawable(R.mipmap.lxl); 25 // } 26 27 28 }
然後我們的主題apk實際上就編寫完成了,然後我們對這個工程進行打包,並且命名為theme.apk
然後我們把這個theme.apk 放到我們主apk的 cache目錄下面:
最後我們可以先執行一下程式 看看效果:
最後我們看下最關鍵的主apk裡的程式碼 應該怎麼寫:
1 //這個changeTv: 一按就自動載入主題apk裡的資源 並且更換themetv 這個tv裡的背景色了 2 changeTv = (TextView) findViewById(R.id.changeTv); 3 //themeTv: 就是用於展現效果的textview 替換背景色 就是替換這個textview的 4 themeTv = (TextView) findViewById(R.id.themeTv); 5 changeTv.setOnClickListener(new View.OnClickListener() { 6 @Override 7 public void onClick(View v) { 8 //這個fileDir 一般都是返回/data/data/你程式的包名/cache/ 9 String fileDir = getCacheDir() + File.separator; 10 //我們是把theme.apk這個檔案push到/data/data/你程式的包名/cache/這個路徑下的 11 //注意如果你自己做的話,這些主題包 當然是從網上下載下來 注意下載下來以後放在/data/data/你程式的包名/ 12 //這個路徑下 任何一個目錄都可以 不一定非要是/cache/這個目錄 13 String filePath = fileDir + "theme.apk"; 14 //這個目錄是用來構建DexClassLoader物件的 ,用作建構函式裡的第二個引數 15 //是dex的輸出路徑(因為載入apk/jar的時候會解壓出dex檔案,這個路徑就是儲存dex檔案的) 16 String optimizedDirectory = getCacheDir() + File.separator; 17 //DexClassLoader可以載入任何路徑的apk/dex/jar 這裡要注意了PathClassLoader只能載入/data/app中的apk,也就是已經安裝到手機中的apk。 18 //這個也是PathClassLoader作為預設的類載入器的原因,因為一般程式都是安裝了,在開啟,這時候PathClassLoader就去載入指定的apk(解壓成dex,然後在優化成odex)就可以了。 19 ClassLoader classLoader = new DexClassLoader(filePath, optimizedDirectory, null, getClassLoader()); 20 //把我們主題apk包裡的資源 載入到本apk自己的resouce裡 21 addOtherResourcesToMain(filePath); 22 try { 23 //DexClassLoader物件來 載入theme.apk包裡的ResourceUtils這個類的getTextViewBackGroundDrawable這個方法 24 Class clazz = classLoader.loadClass("com.example.administrator.themeapk.ResourceUtils"); 25 Method method = clazz.getMethod("getTextViewBackGroundDrawable", Resources.class); 26 //invoke 也就是執行方法的時候 可以看到我們傳的引數是mResource 而這個mResource是我們自己新構造出來的 27 //裡面包含了theme.apk裡的資源。 28 Drawable drawable = (Drawable) method.invoke(null, mResource); 29 //成功獲取了 主題apk裡的圖片資源以後 剩下的事情就水稻渠成了. 30 themeTv.setBackgroundDrawable(drawable); 31 } catch (ClassNotFoundException e) { 32 e.printStackTrace(); 33 } catch (NoSuchMethodException e) { 34 e.printStackTrace(); 35 } catch (InvocationTargetException e) { 36 e.printStackTrace(); 37 } catch (IllegalAccessException e) { 38 e.printStackTrace(); 39 } 40 } 41 });
1 //這個方法把我們主題apk裡的resource 加入到我們自己的主apk裡的resource裡 2 //這個dexPath就是 我們theme.apk在 我們主apk 的存放路徑 3 private void addOtherResourcesToMain(String dexPath) { 4 try { 5 AssetManager assetManager = AssetManager.class.newInstance(); 6 //反射呼叫addAssetPath這個方法 就可以 7 Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class); 8 addAssetPath.invoke(assetManager, dexPath); 9 mAssetManager = assetManager; 10 } catch (InstantiationException e) { 11 e.printStackTrace(); 12 } catch (IllegalAccessException e) { 13 e.printStackTrace(); 14 } catch (NoSuchMethodException e) { 15 e.printStackTrace(); 16 } catch (InvocationTargetException e) { 17 e.printStackTrace(); 18 } 19 //把themeapk裡的資源 通過addAssetPath 這個方法增加到本apk自己的path裡面以後 就可以重新構建出resource物件了 20 mResource = new Resources(mAssetManager, getResources().getDisplayMetrics(), getResources().getConfiguration()); 21 }
註釋應該寫的比較清楚了。相信大家應該能理解的比較好。其原理可以參考老羅的部落格:http://blog.csdn.net/luoshengyang/article/details/8791064
總結起來qq的皮膚載入技術 其實就下面幾步:
1.例項化 AssetManager 物件,並通過反射呼叫 addAssetPath(String) 方法載入目標 apk(或與 apk 檔案架構一致的目錄)
2.通過第一步得到的 AssetManager 例項化 Resource 物件
3.利用第二步得到的 Resource 物件來動態載入資源(這個方案是比較簡單的方案 但是有一定侷限性 讀者可以自己這樣寫一個,我這篇blog裡的方案是直接第四步)
4.通過dexclassloader 來反射呼叫 主題包裡的方法 來得到資源。引數就用我們第二步得到的Resource物件。這樣做的好處是,我們可以定義一個規範的介面出來,
我們的主apk 直接呼叫介面方法 即可,theme.apk裡 實現這個介面就行了。這樣你就算有100個主題包,我們的主apk裡的程式碼也只用寫一份即可!非常方便。