Android插件化(一) 動(dòng)態(tài)加載技術(shù)

Android支持動(dòng)態(tài)加載的兩種方式是:DexClassLoader和PathClassLoader,DexClassLoader可加載jar/apk/dex,且支持從SD卡加載;PathClassLoader只能加載已經(jīng)安裝在Android系統(tǒng)內(nèi)APK文件( /data/app 目錄下),其它位置的文件加載的時(shí)候都會(huì)出現(xiàn) ClassNotFoundException。 因?yàn)?PathClassLoader 會(huì)去讀取 /data/dalvik-cache 目錄下的經(jīng)過(guò) Dalvik 優(yōu)化過(guò)的 dex 文件,這個(gè)目錄的 dex 文件是在安裝 apk 包的時(shí)候由 Dalvik 生成的,沒(méi)有安裝的時(shí)候,自然沒(méi)有生成那個(gè)文件。

我們現(xiàn)在要實(shí)現(xiàn)的一個(gè)需求是:如何調(diào)用一個(gè)非本應(yīng)用的java程序,如下:


豆芽圖片20171130151459511.png

app 與loutillib兩個(gè)模塊沒(méi)有任何的依耐關(guān)系,在Module App中,我們想調(diào)用Loutillib中的LogUitl輸出一條log。

所以我們采取dexClassLoader來(lái)加載這個(gè)jar

第一步:

首先我們將logutil這個(gè)module 依賴(lài)主app 然后運(yùn)行下,這是時(shí)候會(huì)生成class.jar
然后在logutil中添加如下代碼: 將class.jar重新命名下log.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)

上面配置好gradle 環(huán)境變量后,就可以再終端執(zhí)行: gradlew makeJar

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

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

生成新的jar 然后push 到sdcard中

adb  push  new_log.jar  sdcard/

以上準(zhǔn)備工作 做好了之后, 第二步就開(kāi)始擼DexClassLoader的代碼了。

第二步:直接貼上代碼,利用java 類(lèi)加載
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    //動(dòng)態(tài)加載dex文件  這里是個(gè)jar 包里面的方法
    public void start(View view) {
        //dex解壓釋放后的目錄
        final File dexOutPutDir = getDir("dex", 0);
        //dex所在目錄
        final String dexPath = Environment.getExternalStorageDirectory().toString() + File.separator + "new_log.jar";

        //第一個(gè)參數(shù):是dex壓縮文件的路徑
        //第二個(gè)參數(shù):是dex解壓縮后存放的目錄
        //第三個(gè)參數(shù):是C/C++依賴(lài)的本地庫(kù)文件目錄,可以為null
        //第四個(gè)參數(shù):是上一級(jí)的類(lèi)加載器
        DexClassLoader classLoader = new DexClassLoader(dexPath, dexOutPutDir.getAbsolutePath(), null, getClassLoader());

        try {
            final Class<?> loadClazz = classLoader.loadClass("plugin.charles.com.logutil.LogUitl");
            final Object o = loadClazz.newInstance();
            final Method printLogMethod = loadClazz.getDeclaredMethod("printLog");
            printLogMethod.setAccessible(true);
            printLogMethod.invoke(o);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

下面介紹的是動(dòng)態(tài)加載未安裝的apk.然后讀取apk中圖片,顯示在宿主app中
1.首先準(zhǔn)備換膚的apk

 skindemo-debug.apk
 skindemo2-debug.apk
 這2個(gè)apk中都有一張圖片,然后我們push這兩個(gè)apk到sdcard中就可以
  1. 資源加載問(wèn)題,怎么通過(guò)Resources 加載另外一個(gè)apk中的圖片
    /**
     * 獲取AssetManager   用來(lái)加載插件資源
     *
     * @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;
    }

    //這個(gè)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,動(dòng)態(tài)換膚的點(diǎn)擊事件代碼

private boolean mChange = false;
   private String skinType = "";
   private int APK_TYPE;

   /**
    * 替換bg
    */
   public void replace(View view) {
       if (!mChange) {
           skinType = "skindemo-debug.apk";
           APK_TYPE = 1;
           mChange = true;
       } else {
           skinType = "skindemo2-debug.apk";
           APK_TYPE = 2;
           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);
       //第一個(gè)參數(shù):是dex壓縮文件的路徑
       //第二個(gè)參數(shù):是dex解壓縮后存放的目錄
       //第三個(gè)參數(shù):是C/C++依賴(lài)的本地庫(kù)文件目錄,可以為null
       //第四個(gè)參數(shù):是上一級(jí)的類(lèi)加載器
       DexClassLoader classLoader = new DexClassLoader(pApkFilePath, file.getAbsolutePath(), null, getClassLoader());
       try {
           final Class<?> loadClazz = classLoader.loadClass(pApkPacketName + ".R$drawable");
           //插件中皮膚的名稱(chēng)是skin_one
           final Field skinOneField = APK_TYPE == 1 ? loadClazz.getDeclaredField("skin_one") : loadClazz.getDeclaredField("skin_two");
           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);
               linearLayout.setBackgroundDrawable(drawable);
           }
       } catch (Exception e) {
           e.printStackTrace();
       }

   }
 
   /**
    * 根據(jù)sdcard路徑,獲取未安裝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 "";
   }

此時(shí)就完成了,只需要看效果了
源碼鏈接:https://github.com/15189611/pluginDemo

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容