一、综述

       Android使用Dalvik虚拟机加载可执行程序,所以不能直接加载基于class的jar,而是需要将class转化为dex字节码,从而执行代码。优化后的字节码文件可以存在一个*.jar中,只要其内部存放的是*.dex即可使用。

       将class的jar包转化为dex需要用到命令dx(在*\android-sdk\build-tools\version[23.0.1] 或 *\android-sdk\platform-tools下能找到);命令使用方式为:dx --dex --output=output.jar origin.jar,该命令将包含class的origin.jar转化为包含dex的output.jar文件。

        Android支持动态加载的两种方式是:DexClassLoader和PathClassLoader,DexClassLoader可加载jar/apk/dex,且支持从SD卡加载;PathClassLoader据说只能加载已经安装在Android系统内APK文件

 PathClassLoader 的限制要更多一些,它只能加载已经安装到 Android 系统中的 apk 文件,也就是 /data/app 目录下的 apk 文件。其它位置的文件加载的时候都会出现 ClassNotFoundException. 例如:

PathClassLoader cl = new PathClassLoader(jarFile.toString(), "/data/app/", ClassLoader.getSystemClassLoader());  



二、实验步骤

假如你已经有一个工程,需要把一些功能代码什么用于动态加载于其他应用上,则可以新建一个module,或者你可以选择  新建一个Android工程,并进行如下操作:

            2.1 定义一个接口IShowToast.java

/**
 * 动态加载测试接口
 */
public interface IShowToast {
    int showToast(Context context);
}

   2.2 再定义一个简单实现ShowToastImpl.java


/**
 * 动态加载测试实现
 */
public class ShowToastImpl implements IShowToast {
    @Override
    public int showToast(Context context) {
        Toast.makeText(context, "我来自另一个dex文件", Toast.LENGTH_LONG).show();
        return 100;
    }
}



很简单输出一句话,"我来自另一个dex文件"


整体工程目录如下:

android动态加载dex使用反射的方式 安卓动态加载dex_android动态加载


假如是新建的module,则点击Build--make module,不然点击make Project。

android动态加载dex使用反射的方式 安卓动态加载dex_加载_02

这时会在对应module或者project的build--intermediates--生成classes文件夹,内部结构如下:

android动态加载dex使用反射的方式 安卓动态加载dex_android动态加载_03



好了我们要把IShowToastImpl这个class转换成Dalvik可识别的dex文件,分两步:


IShowToastImpl这个类为jar包的形式;


2.通过android sdk自带的dx.jar工具转换jar包为dex文件。


完成第一步,当时遇到点麻烦,由于eclipse是基于ant并且有可视化工具,可以直接导出指定文件的jar包,但是android studio不行,那怎么办呢?


程序运行的效果图如下:们可以通过gradle task来打包,打开app目录下的build.gradle文件,切记不是根目录的build.gradle文件,加上以下代码:


(注:假如是新建module,则在该module下的build.gradle下加入代码)

//删除isshowtoast.jar包任务
task clearJar(type: Delete) {
    delete 'libs/ishowtoast.jar'
}
task makeJar(type:org.gradle.api.tasks.bundling.Jar){
    //指定生成的jar名
    baseName 'ishowtoast'
    //从哪里打包class文件
    from('build/intermediates/classes/debug/com/example/dexlibs/')
    //打包到jar后的目录结构
    into('com/example/dexlibs/')
    //去掉不需要打包的目录和文件
    exclude('test/','IShowToast.class','BuildConfig.class','R.class')
    //去掉R$开头的文件
    exclude{it.name.startsWith('R$')}
}
makeJar.dependsOn(clearJar,build)




android动态加载dex使用反射的方式 安卓动态加载dex_Android_04




打开AS的 terminal窗口: cd app进入app目录,执行gradlew makeJar,然后等待直到出现Build Successfully,这时会在build目录下出现libs/ishowtoast.jar文件,这个文件就是我们要用的jar包。


android动态加载dex使用反射的方式 安卓动态加载dex_Android_05


ishowtoast.jar转换成Dalvik可识别的dex格式,新版的sdk已经将dx.jar放到build-tools\23.0.2\lib目录下,我们在dos下或者在Android studio terminal下面进入到此目录,然后运行下面的命令:


(备注;请把ishowtoast.jar复制到C:\Android\android-sdk-windows\build-tools\23.0.2目录下,不然会提示找不到该文件)


android动态加载dex使用反射的方式 安卓动态加载dex_android动态加载_06


output是你的输出目录,默认就是在当前的根目录下,执行完成后我们就在当前目录下生成了Davilk虚拟机可执行的dex文件,因为这条命令同时会打包dex文件,因此后缀是jar,如图所示,生成了ishowtoast_dex的jar文件,右键使用WinRaR即可看到有class.dex文件

android动态加载dex使用反射的方式 安卓动态加载dex_android动态加载_07

android动态加载dex使用反射的方式 安卓动态加载dex_Android_08

接着我们再新建一个项目,准备动态加载刚才的ishowtoast_dex的jar文件。

android动态加载dex使用反射的方式 安卓动态加载dex_android动态加载_09

务必要注意,需要复制过来的接口IShowToast所在的包名需要跟前一个app一样,不然会报错

fileUtils是一个工具类,用于读取sssets中我们复制过来的那个ishowtoast_dex的jar文件,上图所示,下图则是具体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 len=0;
            while ((len=in.read(bytes))!=-1)
                out.write(bytes,0,len);
            out.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                if (in!=null)
                    in.close();
                if (out!=null)
                    out.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

打开MainActivity:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //  //添加一个点击事件
        findViewById(R.id.tv_click).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                loadDexClass();
            }
        });
    }
    /**
     * 加载dex文件中的class,并调用其中的showToast方法
     */
    private void loadDexClass() {
        File cacheFile = getDir("dex",0);
        String internalPath = cacheFile.getAbsolutePath() + File.separator + "ishowtoast_dex.jar";
        File desFile=new File(internalPath);
        try {
            if (!desFile.exists()) {
                desFile.createNewFile();
                FileUtils.copyFiles(this,"ishowtoast_dex.jar",desFile);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        //下面开始加载dex class
        //1.待加载的dex文件路径,如果是外存路径,一定要加上读外存文件的权限,
        //2.解压后的dex存放位置,此位置一定要是可读写且仅该应用可读写
        //3.指向包含本地库(so)的文件夹路径,可以设为null
        //4.父级类加载器,一般可以通过Context.getClassLoader获取到,也可以通过ClassLoader.getSystemClassLoader()取到。
        DexClassLoader dexClassLoader=new DexClassLoader(internalPath,cacheFile.getAbsolutePath(),null,getClassLoader());
        try {
            //该name就是internalPath路径下的dex文件里面的ShowToastImpl这个类的包名+类名
            Class<?> clz = dexClassLoader.loadClass("com.example.dexlibs.ShowToastImpl");
            IShowToast impl= (IShowToast) clz.newInstance();//通过该方法得到IShowToast类
            if (impl!=null)
                impl.showToast(this);//调用打开弹窗
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}



程序运行的效果图如下:

android动态加载dex使用反射的方式 安卓动态加载dex_Android_10

android动态加载dex使用反射的方式 安卓动态加载dex_加载_11

动图不好搞,mp4转gif还要下格式工厂,所以就用图片代替了

至此,我们关于Android Dex动态加载机制的过程就结束了。