n1cef1sh's Blog

前言

前面 学习了Java和Android里的类加载机制,了解了一些系统的ClassLoader,那么回到最初的起点,关于动态加载的使用。这方面的内容也有很多,暂且分为三部分,先做一个简单的demo体会动态加载,然后是动态加载资源,最后是动态加载activity。这篇是第一部分。

过程

1、编写接口类

把要实现的方法抽象成公共接口,处理之后放在项目里方便调用。 这里只做一个简单的输入字符串功能。

package com.fish.test2;

public interface User {
String SayHello();
}

2、另写一个类实现接口。

package com.fish.test3;
import com.fish.test2.User;
public class UserImpl implements User{
	@Override
	public String SayHello() {
		// TODO Auto-generated method stub		
		return "Hello, Test for demo.";
}
}

3、将接口类和实现方法的类分别打成jar包,把后者用AndroidSDK自带的dx工具优化一下。一般dx.bat在sdk目录下的/platform-tools里,但是我这里没找到,就去/build-tools下任意一个版本的目录里都可以找到。

把UserImpl.jar拷贝到这个目录下,执行命令

dx --dex --output=UserImpl_tmp.jar UserImpl.jar

其实是把java代码优化成dex文件,而至于jar这个格式.

JAR 文件格式以流行的 ZIP 文件格式为基础。与 ZIP 文件不同的是,JAR 文件不仅用于压缩和发布,而且还用于部署和封装库、组件和插件程序,并可被像编译器和 JVM 这样的工具直接使用。

无论加载jar还是apk,和加载dex是差不多的,因为前面类加载机制里我们看到这几种格式都能加载,会从jar或apk中解压提取出dex出来。所以这里我们姑且就处理成jar包,作为所谓的插件在项目里动态加载。

4、创建Android工程,包名要和上面的包名相同不然会报错。把接口类的jar包放到libs下,把优化后的test.jar放到手机的sdcard下。

adb push test.jar /sdcard

然后在AndroidManifest.xml里添加读取外存权限。

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

5、编写MainActivity。添加个点击按钮和点击事件。

package com.fish.test2;

import android.os.Environment;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

import java.io.File;
import dalvik.system.DexClassLoader;

public class MainActivity extends AppCompatActivity {

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

    Button button = (Button)findViewById(R.id.button1);

    button.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {

			//getDir("dex1", 0)会在/data/data/**package/
			//创建一个名叫”app_dex1“的文件夹
            File dexOutputDir = getDir("dex1",0);

			//在sdcard目录下的test.jar的路径
            String path = Environment.getExternalStorageDirectory().toString()+File.separator+"test.jar";

			//第一个参数是待加载的dex文件目录
			//也就是test.jar的目录
			//第二个参数是解压优化后的dex文件目录
			//该位置要可读写且仅该应用可读写(安全性考虑)
			//因此放在/data/dat/下
			//第三个参数没用到so库,设置null
			//第四个参数是父类加载器
            DexClassLoader cl = new DexClassLoader(path, dexOutputDir.getAbsolutePath(), null, getClassLoader());

            Class libProviderClass = null;

            try{
				//要写完整的类名
                libProviderClass  = cl.loadClass("com.fish.test2.UserImpl");

				//实例化接口类的对象
                User lib = (User)libProviderClass.newInstance();
				
				//调用里面SayHello()方法输出字符串
                Toast.makeText(MainActivity.this, lib.SayHello(), Toast.LENGTH_LONG).show();
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    });
}
}

到这里步骤就结束了,编译运行即可。但是过程中出现了不少问题,单独记录。

问题

Error:Error converting bytecode to dex: Cause: Dex cannot parse version 52 byte code. This is caused by library dependencies that have been compiled using Java 8 or above. If you are using the ‘java’ gradle plugin in a library submodule add targetCompatibility = ‘1.7’ sourceCompatibility = ‘1.7’ to that submodule’s build.gradle file.

无法解析JDK1.8,在app/build.gradle的dependencies里的最开始添加

apply plugin: 'java'
sourceCompatibility = 1.7
targetCompatibility = 1.7

但是出现新的错误

Error:The ‘java’ plugin has been applied, but it is not compatible with the Android plugins. 删除apply plugin: ‘java’后报上面第一个错

后来得知as2.3.3默认是JDK1.7,所以配置一下让它支持1.8

配置流程,红框为添加内容

编译通过,带有一个warning,且button无效。

无效的原因是换了个模拟器,忘了把test.jar放进sdcard,所以找不到插件类。

然后再测试

成功调用了插件类里的SayHello()方法,输出了指定的字符串。

参考来源

Android黑科技动态加载(二)之Android中的ClassLoader

https://blog.csdn.net/u013478336/article/details/50734108