Android 高仿騰訊旗下app的 皮膚載入技術

希爾瓦娜斯女神發表於2015-12-28

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裡的程式碼也只用寫一份即可!非常方便。

 

相關文章