DexClassLoader介绍:
DexClassLoader可以载入一个含有classes.dex文件的压缩包,可以是jar,可以是apk,也可以是含有dex文件的zip。
构造器DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent)
-
dexPath: 包含dex文件的压缩文件(jar,apk,zip)的绝对路径,不能为空。
-
optimizedDirectory: 解压后存储路径,建议放在程序私有路径
/data/data/com.xxx.xxx
下。 -
libraryPath:os库的存放路径,可以为空,若有os库,必须填写。
-
parent:父类加载器,一般为
context.getClassLoader()
。
接下来,开始动手,编码实战。
一个假设性的需求:
假设有一个这样的需求,宿主需要加载Plugin.apk,获取到插件Plugin相关的信息或资源。
1. 新建个名为Plugins项目
新建一个Plugin项目,该项目放入一些需要被调用的java代码和资源文件,如下图所示:
先放入一种图片资源,供宿主调用,路下图所示:
例如:宿主获取到Plugin中的某个实体的信息。
这里,先创建一个Bean实体:
public class Bean implements BeanProvider{
private String name="根根";
@Override
public void setName(String name) {
this.name=name;
}
@Override
public String getName() {
return name;
}
}
这里思考一下,宿主又该入如何获取到该实体信息呢?
因Plugin和宿主是属于不同的Module,无法直接通过new方式创建。
在Java中,跨Jar调用,一般考虑反射。
反射调用Plugin中某个类的方法或者静态方法,比较容易实现,这里省略不说。
可能存在异步业务处理逻辑,考虑面向接口方式,回调方式获取。
2. 新建一个通用CommmonLibrary项目
新建一个宿主和Plugin都依赖的通用类库,提供对应接口,如下图所示:
在实际开发中,采用面向接口编程方式来调用Plugin中的功能逻辑,会有很多个回调接口,为了方便统一管理。
先创建一个动态调用的接口,用作统一的入口:
public interface IDynamic {
/**
* 涉及插件中回调
* @param callBack
*/
void invokeCallback(CallBack callBack);
}
接下来,创建一个回调接口,用于传递信息实体:
public interface CallBack {
void callback(BeanProvider beanProvider);
}
在提出一个Bean实体的操作接口:
public interface BeanProvider {
void setName(String name);
String getName();
}
又返回到Plugin项目中去,去添加具体的的Bean业务逻辑。
在实际开发中,该bean对象可能是从数据库中读取,或者从网络上获取,也可能需要经过一系列的逻辑操作才产生。
创建一个IDynamic接口的实现类。
public class Dynamic implements IDynamic {
@Override
public void invokeCallback(CallBack callBack) {
//具体的业务逻辑操作
BeanProvider provider=new Bean();
provider.setName("根根");
callBack.callback(provider);
}
}
注意点:Plugins项目和宿主App项都需要依赖该通用库。
3. 宿主加载Plugins项目生成的apk
3.1 准备步骤:
通过AndroidStudio中Build-->Build APK(s)
生成对应的apk。
将Plugin项目产生apk拷贝到宿主中assets文件夹下,如下图所示:
接下来,编写Assets文件的Utils工具类:
public class Utils {
public static String copyFiles(Context context, String fileName) {
File dir = getCacheDir(context);
String filePath = dir.getAbsolutePath() + File.separator + fileName;
try {
File desFile = new File(filePath);
if (!desFile.exists()) {
desFile.createNewFile();
copyFiles(context, fileName, desFile);
}
} catch (Exception e) {
e.printStackTrace();
}
return filePath;
}
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;
}
}
将assets中plugin.apk拷贝到手机磁盘中,方便DexClassLoader加载:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private String dexPath;
private String fileName = "plugin-debug.apk";
private String cacheDir;
@Override
protected void attachBaseContext(Context newBase) {
super.attachBaseContext(newBase);
this.dexPath = Utils.copyFiles(newBase, fileName);
this.cacheDir = Utils.getCacheDir(newBase).getAbsolutePath();
}
}
3.2 插件中代码接口回调:
在onCreate()中创建DexClassLoader对象:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private DexClassLoader dexClassLoader;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
dexClassLoader = new DexClassLoader(dexPath, cacheDir, null, getClassLoader());
findViewById(R.id.main_test).setOnClickListener(this);
}
}
然后按钮点击调用,插件中回调返回Bean实体信息:
@Override
public void onClick(View v) {
try {
//通过dexClassLoader加载指定包名的类
Class<?> mClass = dexClassLoader.loadClass("com.xingen.plugin.Dynamic");
IDynamic iDynamic = (IDynamic) mClass.newInstance();
iDynamic.invokeCallback(new CallBack() {
@Override
public void callback(BeanProvider beanProvider) {
//插件回调宿主
Toast.makeText(getApplicationContext()," 插件回调宿主 ,获取的Bean实体的字段是 "+beanProvider.getName(),Toast.LENGTH_LONG).show();
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
3.3加载插件中图片资源:
思路:先反射方式获取apk的AssetManager,在创建apk的Resource对象,通过Resource获取到资源文件。
先创建一个工具类:
public class AssertsDexLoader {
/**
* 获取指定apk的AssetManager
*
* 例如:获取插件的AssetManager
*
* @param apkPath
* @return
*/
public static AssetManager createAssetManager(String apkPath) {
try {
AssetManager assetManager = AssetManager.class.newInstance();
AssetManager.class.getDeclaredMethod("addAssetPath", String.class).invoke(
assetManager, apkPath);
return assetManager;
} catch (Throwable th) {
th.printStackTrace();
}
return null;
}
/**
* 获取到插件中的Resource
* @param context
* @param apkPath
* @return
*/
public static Resources getResource(Context context, String apkPath){
AssetManager assetManager = createAssetManager(apkPath);
return new Resources(assetManager, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());
}
}
接下来,加载显示到ImageView中:
/**
* 加载插件中的resource资源
*/
private void loadImage(){
ImageView imageView=findViewById(R.id.main_iv);
File file = new File(dexPath);
Log.d("xingen", "file exist " + file.exists()+" "+dexPath);
Resources resources=AssertsDexLoader.getResource(getApplicationContext(),dexPath);
try {
//加载Drawable下的bd_logo1图片
Drawable drawable=resources.getDrawable(resources.getIdentifier("bd_logo1","drawable","com.xingen.plugin"));
imageView.setImageDrawable(drawable);
}catch (Exception e){
e.printStackTrace();
}
}