AndroidHook機制——應用換膚

Android系統使用了ClassLoader機制來進行Activity等組件的加載;apk被安裝之后,APK文件的代碼以及資源會被系統存放在固定的目錄(比如/data/app/package_name/1.apk)系統在進行類加載的時候,會自動去這一個或者幾個特定的路徑來尋找這個類;但是系統并不知道存在于插件中的Activity組件的信息,插件可以是任意位置,甚至是網絡,系統無法提前預知,因此正常情況下系統無法加載我們插件中的類;因此也沒有辦法創建Activity的對象,更不用談啟動組件了。這個時候就需要使用動態加載技術了,關于Activity如何插件化,后面系列在說,本文講了一個應用程序換膚的故事,雖然老套,但是對于理解動態加載技術很實用,讀完之后你可以知道如何解決插件之中的資源加載問題。

關于類加載器,請看深入探討 Java 類加載器

一、動態加載dex的技術

Android使用Dalvik虛擬機加載可執行程序,所以不能直接加載基于class的jar,而是需要將class轉化為dex字節碼,從而執行代碼。優化后的字節碼文件可以存在一個.jar中,只要其內部存放的是.dex即可使用。

我們現在要實現的一個需求是:如何調用一個非本應用的java程序,如下:

app 與loutillib兩個模塊沒有任何的依耐關系,在Module App中,我們想調用Loutillib中的LogUitl輸出一條log。LogUitl如下,so easy。

public class LogUitl {

? ? public static final String TAG="LogUitl";

? ? private void? printLog(){

? ? ? ? Log.e(TAG,"這是來自另外一個dex中的log");

? ? }

}

所以我們要在運行時把LogUitl動態加載到app這個進程中, Android支持動態加載的兩種方式是:DexClassLoader和PathClassLoader,DexClassLoader可加載jar/apk/dex,且支持從SD卡加載;PathClassLoader只能加載已經安裝在Android系統內APK文件( /data/app 目錄下),其它位置的文件加載的時候都會出現 ClassNotFoundException。 因為 PathClassLoader 會去讀取 /data/dalvik-cache 目錄下的經過 Dalvik 優化過的 dex 文件,這個目錄的 dex 文件是在安裝 apk 包的時候由 Dalvik 生成的,沒有安裝的時候,自然沒有生成那個文件。

這里我們用DexClassLoader來加載,LogUitl所生成的dex文件。首先用gradle打出LogUtil的jar包。

task makeJar(type:Copy){

? ? delete 'build/libs/log.jar'

? ? from('build/intermediates/bundles/release/')

? ? into('build/libs/')

? ? include('classes.jar')

? ? rename ('classes.jar', 'log.jar')

? ? exclude('test/','BuildConfig.class','R.class')

? ? exclude{it.name.startsWith('R$');}

}

makeJar.dependsOn(build)

注意,這個jar還不能被加載,這個是基于class的jar,Dalvik虛擬機加載的是dex字節碼,所以需要將class轉化為dex字節碼。這個需要用到dx命令,這個可以在Android\sdk\build-tools\23.0.0中找到,把log.jar拷貝到這個目錄下,執行

dx --dex --output=new_log.jar log.jar

在執行

adb? push? new_log.jar? sdcard/

把這個new_log放進SDCARD中,這樣dex的準備工作就OK了。以下是用DexClassLoader動態加載的代碼。

public class MainActivity extends Activity {

? ? @Override

? ? protected void attachBaseContext(Context newBase) {

? ? ? ? super.attachBaseContext(newBase);

? ? }

? ? @Override

? ? protected void onCreate(Bundle savedInstanceState) {

? ? ? ? super.onCreate(savedInstanceState);

? ? ? ? setContentView(R.layout.activity_main);

? ? }

? ? public void start(View view) {

? ? ? ? //dex解壓釋放后的目錄?

? ? ? ? final File dexOutPutDir = getDir("dex", 0);

? ? ? ? //dex所在目錄?

? ? ? ? final String dexPath = Environment.getExternalStorageDirectory().toString() + File.separator + "new_log.jar";

? ? ? ? //第一個參數:是dex壓縮文件的路徑

? ? ? ? //第二個參數:是dex解壓縮后存放的目錄

? ? ? ? //第三個參數:是C/C++依賴的本地庫文件目錄,可以為null

? ? ? ? //第四個參數:是上一級的類加載器?

? ? ? ? DexClassLoader classLoader=new DexClassLoader(dexPath,dexOutPutDir.getAbsolutePath(),null,getClassLoader());

? ? ? ? try {

? ? ? ? ? ? final Class<?> loadClazz = classLoader.loadClass("zhangwan.wj.com.logutillib.LogUitl");

? ? ? ? ? ? final Object o = loadClazz.newInstance();

? ? ? ? ? ? final Method printLogMethod = loadClazz.getDeclaredMethod("printLog");

? ? ? ? ? ? printLogMethod.setAccessible(true);

? ? ? ? ? ? printLogMethod.invoke(o);

? ? ? ? } catch (Exception e) {

? ? ? ? ? ? e.printStackTrace();

? ? ? ? }

? ? }

}

執行結果:

發現成功的調用了printLog方法。有上面的基礎,現在實現一個難一點的,如何給應用程序換膚,這個難體現在資源加載上。通常各種各樣的皮膚都是一個個的apk文件,當用戶需要哪個皮膚,就下載到本地,然后動態加載,但是當宿主程序調起未安裝的皮膚插件apk的時候,插件中以R開頭的資源都不能被訪問,程序會拋出異常,無法找到某某id所對應的資源。這是因為加載資源都是通過Resourse來實現的,Resource對象是由Context得到的,我們知道一個app的工程的資源文件都會隱射到R文件中,而這個R文件的包名則是這個應用的包名,所以一個包名一般對應一個Context。宿主與皮膚插件的包名是不一樣的,所以宿主Context找不到皮膚插件的資源。

二、應用換膚

1、皮膚程序準備

sky.apk

children.apk

準備兩個apk,sky.apk中有一張名字為skin_one的背景圖,顯示的是藍色的天空;children.apk中也有一張名字為skin_one的背景圖,顯示的是一個小孩。將這兩個apk都push到SD里面,兩套皮膚準備完成。

2、資源加載問題怎么解決

通過分析系統資源加載了解到,系統是通過ContextImpl中的getAssets與getResources加載資源的

/**

? ? * Returns an AssetManager instance for the application's package.

? ? * <p>

? ? * <strong>Note:</strong> Implementations of this method should return

? ? * an AssetManager instance that is consistent with the Resources instance

? ? * returned by {@link #getResources()}. For example, they should share the

? ? * same {@link Configuration} object.

? ? *

? ? * @return an AssetManager instance for the application's package

? ? * @see #getResources()

? ? */

? ? public abstract AssetManager getAssets();

? ? /**

? ? * Returns a Resources instance for the application's package.

? ? * <p>

? ? * <strong>Note:</strong> Implementations of this method should return

? ? * a Resources instance that is consistent with the AssetManager instance

? ? * returned by {@link #getAssets()}. For example, they should share the

? ? * same {@link Configuration} object.

? ? *

? ? * @return a Resources instance for the application's package

? ? * @see #getAssets()

? ? */

? ? public abstract Resources getResources();

ContextImpl中,也就是說,只要實現這兩個方法,就可以解決資源問題了。不饒彎子了,直接上代碼,解釋請移步Android動態加載技術三個關鍵問題詳解。

/**

? ? * 獲取AssetManager? 用來加載插件資源

? ? * @param pFilePath? 插件的路徑

? ? * @return

? ? */

? ? private AssetManager createAssetManager(String pFilePath) {

? ? ? ? try {

? ? ? ? ? ? final AssetManager assetManager = AssetManager.class.newInstance();

? ? ? ? ? ? final Class<?> assetManagerClazz = Class.forName("android.content.res.AssetManager");

? ? ? ? ? ? final Method addAssetPathMethod = assetManagerClazz.getDeclaredMethod("addAssetPath", String.class);

? ? ? ? ? ? addAssetPathMethod.setAccessible(true);

? ? ? ? ? ? addAssetPathMethod.invoke(assetManager, pFilePath);

? ? ? ? ? ? return assetManager;

? ? ? ? } catch (Exception e) {

? ? ? ? ? ? e.printStackTrace();

? ? ? ? }

? ? ? ? return null;

? ? }

? ? //這個Resources就可以加載非宿主apk中的資源

? ? private Resources? createResources(String pFilePath){

? ? ? ? final AssetManager assetManager = createAssetManager(pFilePath);

? ? ? ? Resources superRes = this.getResources();

? ? ? ? return new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());

? ? }

3、動態加載皮膚apk

public class MainActivity extends Activity {

? ? private TextView? mSkinTv;

? ? private? boolean mChange=false;

? ? @Override

? ? protected void onCreate(Bundle savedInstanceState) {

? ? ? ? super.onCreate(savedInstanceState);

? ? ? ? setContentView(R.layout.activity_proxy);

? ? ? ? mSkinTv= (TextView) findViewById(R.id.skin_bg);

? ? }

? ? /**

? ? * 獲取未安裝apk的信息

? ? * @param context

? ? * @param pApkFilePath apk文件的path

? ? * @return

? ? */

? ? private String getUninstallApkPkgName(Context context, String pApkFilePath) {

? ? ? ? PackageManager pm = context.getPackageManager();

? ? ? ? PackageInfo pkgInfo = pm.getPackageArchiveInfo(pApkFilePath, PackageManager.GET_ACTIVITIES);

? ? ? ? if (pkgInfo != null) {

? ? ? ? ? ? ApplicationInfo appInfo = pkgInfo.applicationInfo;

? ? ? ? ? ? return appInfo.packageName;

? ? ? ? }

? ? ? ? return "";

? ? }

? ? public void switchSkin(View view) {

? ? ? ? String skinType="";

? ? ? ? if(!mChange){

? ? ? ? ? ? skinType= "sky.apk";

? ? ? ? ? ? mChange=true;

? ? ? ? }else {

? ? ? ? ? ? skinType= "children.apk";

? ? ? ? ? ? mChange=false;

? ? ? ? }

? ? ? ? final String path = Environment.getExternalStorageDirectory() + File.separator + skinType;

? ? ? ? final String pkgName = getUninstallApkPkgName(this, path);

? ? ? ? dynamicLoadApk(path,pkgName);

? ? }

? ? private? void dynamicLoadApk(String pApkFilePath,String pApkPacketName){

? ? ? ? File file=getDir("dex", Context.MODE_PRIVATE);

? ? ? ? //第一個參數:是dex壓縮文件的路徑

? ? ? ? //第二個參數:是dex解壓縮后存放的目錄

? ? ? ? //第三個參數:是C/C++依賴的本地庫文件目錄,可以為null

? ? ? ? //第四個參數:是上一級的類加載器

? ? ? ? DexClassLoader? classLoader=new DexClassLoader(pApkFilePath,file.getAbsolutePath(),null,getClassLoader());

? ? ? ? try {

? ? ? ? ? ? final Class<?> loadClazz = classLoader.loadClass(pApkPacketName + ".R$drawable");

? ? ? ? ? //插件中皮膚的名稱是skin_one

? ? ? ? ? ? final Field skinOneField = loadClazz.getDeclaredField("skin_one");

? ? ? ? ? ? skinOneField.setAccessible(true);

? ? ? ? ? ? //反射獲取skin_one的resousreId

? ? ? ? ? ? final int resousreId = (int) skinOneField.get(R.id.class);

? ? ? ? ? ? //可以加載插件資源的Resources

? ? ? ? ? ? final Resources resources = createResources(pApkFilePath);

? ? ? ? ? ? if (resources != null) {

? ? ? ? ? ? ? ? final Drawable drawable = resources.getDrawable(resousreId);

? ? ? ? ? ? ? ? mSkinTv.setBackground(drawable);

? ? ? ? ? ? }

? ? ? ? } catch (Exception e) {

? ? ? ? ? ? e.printStackTrace();

? ? ? ? }

? ? }

}

執行效果

到此換膚成功!


?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,428評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,024評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,285評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,548評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,328評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,878評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,971評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,098評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,616評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,554評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,725評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,243評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,971評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,361評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,613評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,339評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,695評論 2 370

推薦閱讀更多精彩內容