標籤:
Android外掛程式化(二):使用DexClassLoader動態載入assets中的apk簡介
上一篇部落格講到,我們可以使用MultiDex.java載入離線的apk檔案。需要注意的是,apk中的類是載入到當前的PathClassLoader當中的,如果apk檔案過多,可能會出現ANR的情況。那麼,我們能不能使用DexClassLoader載入apk呢?當然是可以的!首先看一下Doc文檔.
A class loader that loads classes from .jar and .apk files containing a classes.dex entry. This can be used to execute code not installed as part of an application.
也就是說,DexClassLoader可以載入一個含有classes.dex檔案的壓縮包,既可以是jar也可以是apk。那麼載入一個離線的apk檔案需要注意哪些呢?
1.DexClassLoader的構造方法:
DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent)
2.私人目錄
This class loader requires an application-private, writable directory to cache optimized classes.
瞭解到上述兩點,我們就可以根據DexClassLoader所需要的參數,動態載入assets中的apk了。
源碼
BundleClassLoaderManager
該類主要是負責管理這些DexClassLoader的,首先,我們定義了一個叫做BundleDexClassLoader的類,它繼承自DexClassLoader,用於載入離線的apk檔案。每一個apk檔案對應一個BundleDexClassLoader,而BundleClassLoaderManager則儲存了一個List,在載入的時候,用於尋找類。具體代碼如下:
package net.mobctrl.hostapk;import java.io.File;import java.io.FilenameFilter;import java.util.ArrayList;import java.util.List;import android.annotation.TargetApi;import android.content.Context;import android.os.Build;/** * @Author Zheng Haibo * @PersonalWebsite http://www.mobctrl.net * @version $Id: BundleClassLoaderManager.java, v 0.1 2015年12月11日 下午7:30:59 * mochuan.zhb Exp $ * @Description */@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)public class BundleClassLoaderManager { public static List<BundleDexClassLoader> bundleDexClassLoaderList = new ArrayList<BundleDexClassLoader>(); /** * 載入Assets裡的apk檔案 * @param context */ public static void install(Context context) { AssetsManager.copyAllAssetsApk(context); // 擷取dex檔案清單 File dexDir = context.getDir(AssetsManager.APK_DIR, Context.MODE_PRIVATE); File[] szFiles = dexDir.listFiles(new FilenameFilter() { @Override public boolean accept(File dir, String filename) { return filename.endsWith(AssetsManager.FILE_FILTER); } }); for (File f : szFiles) { System.out.println("debug:load file:" + f.getName()); BundleDexClassLoader bundleDexClassLoader = new BundleDexClassLoader( f.getAbsolutePath(), dexDir.getAbsolutePath(), null, context.getClassLoader()); bundleDexClassLoaderList.add(bundleDexClassLoader); } } /** * 尋找類 * * @param className * @return * @throws ClassNotFoundException */ public static Class<?> loadClass(Context context,String className) throws ClassNotFoundException { try { Class<?> clazz = context.getClassLoader().loadClass(className); if (clazz != null) { System.out.println("debug: class find in main classLoader"); return clazz; } } catch (Exception e) { e.printStackTrace(); } for (BundleDexClassLoader bundleDexClassLoader : bundleDexClassLoaderList) { try { Class<?> clazz = bundleDexClassLoader.loadClass(className); if (clazz != null) { System.out.println("debug: class find in bundle classLoader"); return clazz; } } catch (Exception e) { e.printStackTrace(); } } throw new ClassCastException(className + " not found exception"); }}
注意點:
DEMO運行
在MainActivity中,我們可以通過如下方式,調用apk類中的方法:
private void loadApk() { try { Class<?> clazz = BundleClassLoaderManager.loadClass(getApplicationContext(), "net.mobctrl.normal.apk.Utils"); Constructor<?> constructor = clazz.getConstructor(); Object bundleUtils = constructor.newInstance(); Method printSumMethod = clazz.getMethod("printSum", Context.class, int.class, int.class, String.class); printSumMethod.setAccessible(true); Integer sum = (Integer) printSumMethod.invoke(bundleUtils, getApplicationContext(), 10, 20, "計算結果"); System.out.println("debug:sum = " + sum); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (SecurityException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } }
與MultiDex不同時,我們是通過BundleClassLoaderManager來載入類的,而不是當前的ClassLoader。
改進方案
正如BundleClassLoaderManager中的loadClass方法,其實我們建立一個ClassLoader對象,通過重寫當前ClassLoader的findClass方法即可,然後在Override的findClass方法中,首先從當前ClassLoader中尋找類,然後再從BundleDexClassLoader中遍曆尋找,這樣既可以在Host項目中調用Bundle中的類,也能夠在Bundle中調用Host中的類。
mClassLoader = new ClassLoader(super.getClassLoader()) { @Override protected Class<?> findClass(String className) throws ClassNotFoundException { Class clazz = BundleClassLoaderManager.loadClass(context,className); if (clazz == null) { throw new ClassNotFoundException(className); } return clazz; } };
總結
上一篇部落格和這一篇部落格將的都是類的載入。如果所需要載入的類都是工具類,不需要載入資源等,那麼上面的方案都沒啥問題。但是如果載入的類是Fragment或者Activity等UI,需要引用資源檔,這又改如何處理呢?
下一篇博文:Android資源的離線載入。
參考
1.BaseDexClassLoader源碼
Android外掛程式化(二):使用DexClassLoader動態載入assets中的apk