Android动态加载Dex文件及DexClassLoader详解


ClassLoader


“类加载器”(ClassLoader),顾名思义,就是用来动态加载class文件的。

ClassLoader作用主要有三个:

  1. 负责将 Class 加载到 JVM 中
  2. 审查每个类由谁加载(父优先的等级加载机制)
  3. 将 Class 字节码重新解析成 JVM 统一要求的对象格式

有兴趣的小伙伴可以看看JVM是如何加载一个类的 类的加载机制

ClassLoader(Java)
Class clz = Classloader.loadClass(类全名),其实就是通过一个类的全名,生成这个类的Class对象。loadClass()内部是先进行parent.loadClass()让父类先进行加载,如果加载不成功,再使用该classLoader加载(双亲委派)。然后,通过findClass(类全名)来加载得到Class对象。我们如果想自定义一个classLoader,那么就是重写findClass()方法。findClass()中,我们拿到要加载的路径,然后拿到路径对应文件的数据流。然后使用classLoader定义好的defineClass(inputStream)来生成Class对象就可以了,主要就是给它提供一个路径,然后类全名能找到这个路径下对应的.class文件,然后生成inputStream流。

ClassLoader(Android)
Android中的ClassLoader于Java中的稍微有些不同,虽然两者都是满足双亲委派,但是直接findClass()会抛异常,所以我们不能直接继承classloader来自定义classLoader。要使用BaseDexClassLoader,BaseDexClassLoader重写了findClass(),要注意的是这里的classloader加载的是dex,不是class字节码。

ClassLoader比较常用的分为两种,PathClassLoader和DexClassLoader,虽然两者继承于BaseDexClassLoader,BaseDexClassLoader继承于ClassLoader,但是前者只能加载已安装的Apk里面的dex文件,后者则支持加载apk、dex以及jar,也可以从SD卡里面加载。
作为 ClassLoader 的子类,复写了父类的 findClass 方法:

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
     
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        //在自己的成员变量DexPathList中寻找,找不到抛异常
        Class c = pathList.findClass(name, suppressedExceptions);
        if (c == null) {
     
            ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
            for (Throwable t : suppressedExceptions) {
     
                cnfe.addSuppressed(t);
            }
            throw cnfe;
        }
        return c;
    }

DexPathList 的 findClass 方法:

public Class findClass(String name, List<Throwable> suppressed) {
     
        //循环遍历成员变量dexElements,调用DexFile.loadClassBinaryName加载class
        for (Element element : dexElements) {
     
            DexFile dex = element.dexFile;

            if (dex != null) {
     
                Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
                if (clazz != null) {
     
                    return clazz;
                }
            }
        }
        if (dexElementsSuppressedExceptions != null) {
     
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }

通过以上两段代码我们可以看出,虽然 Android 中的 ClassLoader 的findClass 方法的实现被取消了,但是 ClassLoader 的基类 BaseDexClassLoader 实现了 findClass 方法来加载指定的 Class文件。

DexClassLoader

DexClassLoader用来加载外部的类,外部类的dexpath路径在构造方法中传入,比如从网络下载的dex等,或插件化的apk,从网络下载到dex后,new 一个DexClassLoader,new的时候就把网络下载到的dex路径告诉DexClassLoader,DexClassLoader会将dex一步步封装,放到DexClassLoader中的pathList里面。dex放到DexClassLoader之后,使用DexClassLoader.loadClass(需要加载的patch类全名)得到补丁类的Class对象,然后class.newInstance()对应的实例。通过这个实例里的信息来找到要修补的是哪个类,然后找到这个类对应的Class对象,如果没有就使用PathClassLoader加载。

DexClassLoader的构造方法如下:

DexClassLoader (String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent)
参数 含义
dexPath 包含dex文件的jar包或apk文件路径
optimizedDirectory 释放目录,可以理解为缓存目录,必须为应用私有目录,不能为空
librarySearchPath native库的路径(so文件),可为空
parent 父类加载器

需要注意的是:DexClassLoader中还要传入一个ClassLoader作为该DexClassLoader的父类。这样,我们使用DexClassLoader加载一个类时,根据双亲委派,会先让父类classloader进行加载。

双亲委派模型
双亲委派模型是一种组织类加载器之间关系的一种规范,他的工作原理是:如果一个类加载器收到了类加载的请求,它不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,这样层层递进,最终所有的加载请求都被传到最顶层的启动类加载器中,只有当父类加载器无法完成这个加载请求(它的搜索范围内没有找到所需的类)时,才会交给子类加载器去尝试加载。

优点:java类随着它的类加载器一起具备了带有优先级的层次关系,这是十分必要的。比如java.langObject,它存放在\jre\lib\rt.jar中,它是所有java类的父类,因此无论哪个类加载都要加载这个类,最终所有的加载请求都汇总到顶层的启动类加载器中,因此Object类会由启动类加载器来加载,所以加载的都是同一个类,如果不使用双亲委派模型,由各个类加载器自行去加载的话,系统中就会出现不止一个Object类,应用程序就会全乱了。

最后就来说说Android动态加载Dex那些事…

Android动态加载Dex

新建一个Android工程,在包下面创建一个Impl文件夹,里面创建DexImpl类,然后在上一层创建一个接口类IDex,代码如下:
DexImpl:继承Idex接口类,重写其方法,并简单输出一句话,“包名+ is loaded by DexClassLoader”

public class DexImpl implements IDex {
     
    @Override
    public String getMessage() {
     
        return new StringBuilder(getClass().getName()).append(" is loaded by DexClassLoader").toString();
    }
}

接口类IDex:

public interface IDex {
     
    public String getMessage();
}

具体的工程目录如下图
Android动态加载Dex文件及DexClassLoader详解_第1张图片
点击Build -> make project,这时候会在build/intermediates/javac/debug/classes目录下生成对应的classes文件(这个因AS的版本而异,我现在用的不是androidx的版本,所以我的文件在build/intermediates/javac/debug/classes下面)
Android动态加载Dex文件及DexClassLoader详解_第2张图片

然后我们需要把DexImpl这个class转换成Dalvik可识别的dex文件,分两步:
1.先导出DexImpl这个类为jar包的形式;
2.通过android sdk自带的dx.jar工具转换jar包为包含dex文件的Jar文件。

打开app目录下的build.gradle文件,切记不是根目录的build.gradle文件,加上以下代码:

    //删除dynamic.jar包任务
    task clearJar(type: Delete) {
     
        delete 'libs/dynamic.jar'
    }

    //打包任务
    task makeJar(type:org.gradle.api.tasks.bundling.Jar) {
     
        //指定生成的jar名
        baseName 'dynamic'
        //从哪里打包class文件
        from('build/intermediates/javac/debug/classes/com/xy/dex/plugin/impl/')
        //打包到jar后的目录结构
        into('com/xy/dex/plugin/impl')
        //去掉不需要打包的目录和文件
        exclude('test/', 'IDex.class', 'BuildConfig.class', 'R.class', 'FileUtils.class')
        //去掉R$开头的文件
        exclude{
      it.name.startsWith('R$');}
    }
    makeJar.dependsOn(clearJar, build)

写完这段代码之后,我们在AS的界面最右边可以看到Gradle,点击去,然后在app->other里面找到makeJar这个东西,双击它,然后AS就会帮你生成你所需要的Jar包了,生成后的Jar包路径在app\build\lib文件夹下面

然后使用sdk提供的dx.jar将导出的 dynamic.jar转换成Dalvik可识别的dex格式,我是将这个jar包copy出来,然后置于某个文件夹下(需要跟dx.jar同目录),然后执行dx --dex --output=out.jar dynamic.jar,就会得到含dex的out.jar(这里你可以右键这个jar包,看看里面是不是有一个dex文件,是的话就没错了,那我们继续…)

因为等下我们要使用的是dex下面的IDex实现类,所以我们需要删除当前工程下的DexImpl文件和impl包,避免运行时出错。同时,我们要把刚刚生成的out.jar文件放到assets目录下,等下需要把它copy到app/data下使用,删除后的整个工程目录如下:
Android动态加载Dex文件及DexClassLoader详解_第3张图片

FileUtils类是从assets目录下copy文件到app/data/cache目录:

public class FileUtils {
     
    public static void copyFiles(Context context, String fileName, File desFile) {
     
        InputStream in = null;
        OutputStream out = null;
        try {
     
            in = context.getApplicationContext().getAssets().open(fileName);
            out = new FileOutputStream(desFile.getAbsolutePath());
            byte[] bytes = new byte[1024];
            int i;
            while ((i = in.read(bytes)) != -1)
                out.write(bytes, 0 , i);
        } catch (IOException e) {
     
            e.printStackTrace();
        }finally {
     
            try {
     
                if (in != null)
                    in.close();
                if (out != null)
                    out.close();
            } catch (IOException e) {
     
                e.printStackTrace();
            }

        }
    }

    public static boolean hasExternalStorage() {
     
        return Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
    }

    /**
     * 获取缓存路径
     *
     * @param context
     * @return 返回缓存文件路径
     */
    public static File getCacheDir(Context context) {
     
        File cache;
        if (hasExternalStorage()) {
     
            cache = context.getExternalCacheDir();
        } else {
     
            cache = context.getCacheDir();
        }
        if (!cache.exists())
            cache.mkdirs();
        return cache;
    }
}

核心思想就是使用DexClassLoader去加载dex,然后通过反射调用我们之前定义的方法获取相关资源.

public class MainActivity extends AppCompatActivity {
     
    @Override
    protected void onCreate(Bundle savedInstanceState) {
     
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    public void Track(View view) {
     
        loadDexClass();
    }

    private void loadDexClass() {
     
        // getDir("dex1", 0)会在/data/data/**package/下创建一个名叫”app_dex1“的文件夹,其内存放的文件是自动生成output.dex
        File OutputDir = FileUtils.getCacheDir(getApplicationContext());
        String dexPath = OutputDir.getAbsolutePath() + File.separator + "out.jar";

        File desFile=new File(dexPath);
        try {
     
            if (!desFile.exists()) {
     
                desFile.createNewFile();
                FileUtils.copyFiles(this,"out.jar",desFile);
            }
        } catch (IOException e) {
     
            e.printStackTrace();
        }
        /**
         * 参数1 dexPath:待加载的dex文件路径,如果是外存路径,一定要加上读外存文件的权限
         * 参数2 optimizedDirectory:解压后的dex存放位置,此位置一定要是可读写且仅该应用可读写(安全性考虑),所以只能放在data/data下。
         * 参数3 libraryPath:指向包含本地库(so)的文件夹路径,可以设为null
         * 参数4 parent:父级类加载器,一般可以通过Context.getClassLoader获取到,也可以通过ClassLoader.getSystemClassLoader()取到。
         */
        DexClassLoader classLoader = new DexClassLoader(dexPath, OutputDir.getAbsolutePath(),null,getClassLoader());
        try {
     
            // 该方法将Class文件加载到内存时,并不会执行类的初始化,直到这个类第一次使用时才进行初始化.该方法因为需要得到一个ClassLoader对象
            Class clz = classLoader.loadClass("com.xy.dex.plugin.impl.DexImpl");
            IDex dex = (IDex) clz.newInstance();
            Toast.makeText(this, dex.getMessage(), Toast.LENGTH_LONG).show();

        } catch (Exception e) {
     
            e.printStackTrace();
        }

    }
}

Android动态加载Dex文件及DexClassLoader详解_第4张图片

附上下载链接Android动态加载Dex

你可能感兴趣的:(Android)