我们在上一篇文章《Android插件化原理和实践 (一)之 插件化简介和基本原理简述》中介绍了插件化一些基本知识和历史,最后还列出了三个根本问题。接下来我们打算围绕着这三个根本问题展开对插件化的学习。首先本章将介绍第一个根本问题:宿主和插件中如何相互调用代码。要实现它们相互调用,就得要宿主先将插件加载起来。Android中要想从加载外部插件就在于ClassLoader。

1 初识PathClassLoader和DexClassLoader

ClassLoader下有子类BaseDexClassLoader,BaseDexClassLoader又有两个重要的子类:PathClassLoader和DexClassLoader。先来看看PathClassLoader和DexClassLoader的源码:

PathClassLoader.java

/**
     * Provides a simple {@link ClassLoader} implementation that operates on a list
     * of files and directories in the local file system, but does not attempt to
     * load classes from the network. Android uses this class for its system class
     * loader and for its application class loader(s).
     */
    public class PathClassLoader extends BaseDexClassLoader {
        /**
         * Creates a {@code PathClassLoader} that operates on a given list of files
         * and directories. This method is equivalent to calling
         * {@link #PathClassLoader(String, String, ClassLoader)} with a
         * {@code null} value for the second argument (see description there).
         *
         * @param dexPath the list of jar/apk files containing classes and
         * resources, delimited by {@code File.pathSeparator}, which
         * defaults to {@code ":"} on Android
         * @param parent the parent class loader
         */
        public PathClassLoader(String dexPath, ClassLoader parent) {
            super(dexPath, null, null, parent);
        }

        /**
         * Creates a {@code PathClassLoader} that operates on two given
         * lists of files and directories. The entries of the first list
         * should be one of the following:
         *
         * <ul>
         * <li>JAR/ZIP/APK files, possibly containing a "classes.dex" file as
         * well as arbitrary resources.
         * <li>Raw ".dex" files (not inside a zip file).
         * </ul>
         *
         * The entries of the second list should be directories containing
         * native library files.
         *
         * @param dexPath the list of jar/apk files containing classes and
         * resources, delimited by {@code File.pathSeparator}, which
         * defaults to {@code ":"} on Android
         * @param libraryPath the list of directories containing native
         * libraries, delimited by {@code File.pathSeparator}; may be
         * {@code null}
         * @param parent the parent class loader
         */
        public PathClassLoader(String dexPath, String libraryPath,
                ClassLoader parent) {
            super(dexPath, null, libraryPath, parent);
        }
    }

DexClassLoader.java

/**
     * A class loader that loads classes from {@code .jar} and {@code .apk} files
     * containing a {@code classes.dex} entry. This can be used to execute code not
     * installed as part of an application.
     *
     * <p>This class loader requires an application-private, writable directory to
     * cache optimized classes. Use {@code Context.getDir(String, int)} to create
     * such a directory: <pre>   {@code
     *   File dexOutputDir = context.getDir("dex", 0);
     * }</pre>
     *
     * <p><strong>Do not cache optimized classes on external storage.</strong>
     * External storage does not provide access controls necessary to protect your
     * application from code injection attacks.
     */
    public class DexClassLoader extends BaseDexClassLoader {
        /**
         * Creates a {@code DexClassLoader} that finds interpreted and native
         * code.  Interpreted classes are found in a set of DEX files contained
         * in Jar or APK files.
         *
         * <p>The path lists are separated using the character specified by the
         * {@code path.separator} system property, which defaults to {@code :}.
         *
         * @param dexPath the list of jar/apk files containing classes and
         *     resources, delimited by {@code File.pathSeparator}, which
         *     defaults to {@code ":"} on Android
         * @param optimizedDirectory directory where optimized dex files
         *     should be written; must not be {@code null}
         * @param libraryPath the list of directories containing native
         *     libraries, delimited by {@code File.pathSeparator}; may be
         *     {@code null}
         * @param parent the parent class loader
         */
        public DexClassLoader(String dexPath, String optimizedDirectory,
                String libraryPath, ClassLoader parent) {
            super(dexPath, new File(optimizedDirectory), libraryPath, parent);
        }
    }

PathClassLoader和DexClassLoader这两个类很简单,区别仅在于构造函数的第2个参数optimizedDirectory,其中PathClassLoader把这个参数设置为null。我们来翻译一下PathClassLoader的头部注释,大概意思是:PathClassLoader用于加载本地文件系统上的文件和目录,它不能从网络上加载,Android使用这个类来加载系统类及应用程序。再来翻译一下DexClassLoader的头部注释,大概意思是:DexClassLoader用于加载来自外部jar、apk包含的dex目录。

说到加载dex,是不是想起了为了解决65535问题,而使用了multidex来将一个apk文件内的dex进行拆分。没错,其实在使用这个过程中,主的classes.dex是由App使用PathClassLoader进行加载的,而其余的classes2.dex这些子dex都会以资源的形式在App启动后使用DexClassLoader进行加载到ClassLoader中的。

接着再来翻译一下DexClassLoader的构造函数中参数的意思:

dexPath

接收一个jar或apk文件列表,多个的话,由File.pathSeparator分隔,在Android中默认使用”:”分隔

optimizedDirectory

dex文件被加载后会被编译器优化,优化之后的dex存放路径,不能为空。要注意的是,它需要一个应用私有的可写的一个路径,以防止应用被注入攻击,例子如: File dexOutputDir = context.getDir(“dex”, 0);

libraryPath

包含libraries的目录列表,同样用File.pathSeparator分割,可为空,传入null

parent

父类的class loader

2 加载插件中的类

好了,我们已经清楚了DexClassLoader是用来加载外部apk的dex,而且也了解了它的构造函数的调用了,那么现在就来通过一个Demo来看看它是否真的可以加载外部apk的dex吧。Demo工程目录如下所示,存在两个applicaton模块,一个是宿主Host,一个是插件Plugin。

Android插装方式ASM_optimizedDirectory

Plugin插件中仅有一个TestBean.java类:

package com.zyx.plugin;

public class TestBean {
    private String mName = "子云心";

    public void setName(String name) {
        mName = name;
    }

    public String getName() {
        return mName;
    }
}

Host的MainActivity中编写所有的逻辑代码:

package com.zyx.plugindemo;

import android.app.Activity;
import android.content.Context;
import android.content.res.AssetManager;
import android.os.Bundle;
import android.widget.Toast;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;

import dalvik.system.DexClassLoader;

public class MainActivity extends Activity {
    private final static String sApkName = "Plugin-debug.apk";
    private DexClassLoader mClassLoader;

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

        simulationDownload(this, sApkName);
        mClassLoader = loadPlugin(this, sApkName);
        onDo();
    }

    /**
     * 加载插件
     * @param context
     * @param apkName
     * @return
     */
    private DexClassLoader loadPlugin(Context context, String apkName) {
        File extractFile = context.getFileStreamPath(apkName);
        String dexpath = extractFile.getPath();

        File fileRelease = context.getDir("dex", 0);
        String absolutePath = fileRelease.getAbsolutePath();

        DexClassLoader classLoader = new DexClassLoader(dexpath, absolutePath, null, context.getClassLoader());
        return classLoader;
    }

    /**
     * 执行插件代码
     */
    private void onDo() {
        try {
            Class mLoadClassBean = mClassLoader.loadClass("com.zyx.plugin.TestBean");
            Object testBeanObject = mLoadClassBean.newInstance();
            Method getNameMethod = mLoadClassBean.getMethod("getName");
            getNameMethod.setAccessible(true);
            String name = (String) getNameMethod.invoke(testBeanObject);

            Toast.makeText(getApplicationContext(), name, Toast.LENGTH_LONG).show();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 模拟下载,实际上是将Assets中的插件apk文件复制到/data/data/files 目录下
     * @param context
     * @param sourceName
     */
    private void simulationDownload(Context context, String sourceName) {
        AssetManager am = context.getAssets();
        InputStream is = null;
        FileOutputStream fos = null;
        try {
            is = am.open(sourceName);
            File extractFile = context.getFileStreamPath(sourceName);
            fos = new FileOutputStream(extractFile);
            byte[] buffer = new byte[1024];
            int count = 0;
            while ((count = is.read(buffer)) > 0) {
                fos.write(buffer, 0, count);
            }
            fos.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (is != null) {
                    is.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                if (fos != null) {
                    fos.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

上述代码中,simulationDownload是一个测试方法代码,为了方便开发验证,我们将Plugin通过编译后生成的Plugin-debug.apk文件放在了Host的assets目录下。然后再通过simulationDownload方法将Plugin-debug.apk文件转存到/data/data/files 目录下来模拟真实开发时的下载逻辑。

我们将重点放回在loadPlugin和onDo两个方法上,loadPlugin方法中通过获取Plugin-debug.apk文件的目录信息来构造了一个DexClassLoader对象,然后在onDo方法中,通过这个对象使用了反射来读到Plugin插件中的TestBean类。App运行后,会弹出一个Toast,内容就是TestBean中的mName值。

你以为插件化中加载插件代码就是这么简单吗?你错了,上面的Demo只能使宿主调用插件中的普通类,而实际上并不能使宿主调起插件中的四大组件。即使你在宿主中的AndroidMainifest.xml中声明了组件信息也是不行。可以尝试在插件中创建一个TestService服务,并在宿主中的AndroidMainifest.xml中声明它,然后在宿主中通过startService来启动服务,最后再次运行App后就会报出以下的异常:

Android插装方式ASM_PathClassLoader_02

至于为什么?和解决方法是怎样?我们留在下一篇文章来介绍。