Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android插件化(二):使用DexClassLoader動態加載assets中的apk

Android插件化(二):使用DexClassLoader動態加載assets中的apk

編輯:關於Android編程

上一篇博客講到,我們可以使用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 bundleDexClassLoaderList = new ArrayList();

    /**
     * 加載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");
    }
}

注意點:

1.install方法
install方法主要是將assets中的apk全部拷貝到私有目錄,然後再遍歷私有目錄,使用BundleDexClassLoader加載apk文件,然後將這些BundleDexClassLoader保存到數組中。

2.loadClass方法
該方法先從當前的ClassLoader中查找需要的類,如果找不到,在從List中遍歷查找。

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,需要引用資源文件,這又改如何處理呢?

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved