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();
        }

    }
}

執行效果

到此換膚成功! 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章