Android DexClassLoader/PathClassLoader 动态加载jar/APK

时间:2022-02-07 19:39:18

可以使用Android ClassLoader完成DEX的动态加载,DEX文件可以附属在assets或raw目录也可以运行时从网络下载。

1.当前动态加载常用的class loader

  (1)DexClassLoader:这个可以加载jar/apk/dex,也可以从SD卡中加载,也是本文的重点。
  (2)PathClassLoader:只能加载dex文件和已经安装到Android系统中的apk文件。

这两者的区别在于DexClassLoader需要提供一个可写的outpath路径,用来释放.apk包或者.jar包中的dex文件。换个说法来说,就是PathClassLoader不能主动从zip包中释放出dex,因此只支持直接操作dex格式文件,或者已经安装的apk(因为已经安装的apk在cache中存在缓存的dex文件)。而DexClassLoader可以支持.apk、.jar和.dex文件,并且会在指定的outpath路径释放出dex文件。另外,PathClassLoader在加载类时调用的是DexFile的loadClassBinaryName,而DexClassLoader调用的是loadClass。因此,在使用PathClassLoader时类全名需要用”/”替换”.”。

2.首先将打包好的 jar 转为dex 格式。在Android中无法像Java中那样方便动态加载jar, Android的虚拟机(Dalvik VM)是不认识Java打出jar的byte code,需要通过dx工具来优化转换成Dalvik byte code才行,执行命令:

 $dx --dex --output=testDex.jar test.jar

(注意:打包test.jar时请不要把接口文件打进来. 否则加载时会报错:java.lang.IllegalAccessError: Class ref in pre-verified class resolved to unexpected implementation)

3. 动态加载jar 包示例代码:

首先编写接口和实现,生成jar包(打包jar的时候不要把接口ITest.class打包进来,否则加载时会有冲突报错。之所以定义接口ITest是为了说明下面动态加载方法二:类型强转,前提是知道Test.jar中Class所实现的接口类ITest)

package com.test.mytest;

public interface ITest {
public String test();
}
package com.test.mytest;

public class Test implements ITest {
@Override
public String test() {
return "Hello";
}
}
动态加载jar包

    public void loadDexFile(Context context) {
copyAssetsToFiles(context);
String filePath = context.getFilesDir().getPath() + "/testDex.jar";

//动态加载的也可以是APK文件,没有任何区别,APK 文件中的class.dex文件会被DexClassLoader加载。
//但是,APK中的Activity类,由于是使用反射,无法取得Context,与普通的类毫无区别,没有生命周期。
//String filePath = context.getFilesDir().getPath() + "/Test.apk";

DexClassLoader classLoader = new DexClassLoader(filePath, context.getFilesDir().getPath(), null, context.getClassLoader());
try {
//方法一:反射调用
Class myClass = classLoader.loadClass("com.test.mytest.Test");
Constructor myConstructor = myClass.getConstructor(Context.class);
Method method = myClass.getMethod("test", null);
String data = (String) method.invoke(myConstructor.newInstance(this), null);
//method.setAccessible(true);访问private函数
System.out.println(data);

//方法二:类型强转,前提是知道Test.jar中Class所实现的接口类<span style="font-family: Arial, Helvetica, sans-serif;">ITest</span>
Class myClass2 = classLoader.loadClass("com.test.mytest.Test");
Constructor myConstructor2 = myClass2.getConstructor(Context.class);
ITest obj = (ITest) myConstructor2.newInstance(this);
String data2 = obj.test();
System.out.println(data2);
} catch (Exception e) {
e.printStackTrace();
}
}

/**
* Copy the APK "assets/" to "/data/data/package-name/file/"
*/
public static void copyAssetsToFiles(Context context) {
String fileDir = context.getFilesDir().getPath() + "/";
File workingDir = new File(fileDir);
if (!workingDir.exists()) {
workingDir.mkdirs();
}

File outFile_bin = new File(workingDir, "test.jar");
if (!outFile_bin.exists()) {
copyFile(context, "test.jar", outFile_bin);
outFile_bin.setExecutable(true, false);
}
}

/**
* Copy assets file to the data folder
*/
private static void copyFile(Context context, String sourceFileName, File targetFile) {
InputStream in = null;
FileOutputStream out = null;
try {
in = context.getAssets().open(sourceFileName);
out = new FileOutputStream(targetFile);
byte[] temp = new byte[1024];
int count = 0;
while ((count = in.read(temp)) > 0) {
out.write(temp, 0, count);
}

if (in != null) {
in.close();
}
if (out != null) {
out.close();
}
} catch (Exception e) {
if (in != null) {
try {
in.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}

if (out != null) {
try {
out.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
}
4. 动态加载APK示例代码:

/**
* 在APK1中调用手机上已经安装的APK2中的类
*/
public void loadAPKFile(){
//在APK1中已知APK2的包名,寻找APK2文件所在路径
Intent intent = new Intent();
intent.setPackage("com.example.test");
PackageManager pm = mContext.getPackageManager();
final List<ResolveInfo> plugins = pm.queryIntentActivities(intent,0);
if(plugins.size() <= 0){
Log.i(TAG, "resolve info size is:" + plugins.size());
return;
}

ResolveInfo resolveInfo = plugins.get(0);
ActivityInfo activityInfo = resolveInfo.activityInfo;
String packageName = activityInfo.packageName;
//目标类所在的apk路径,class loader会通过这个路径来加载目标类文件
String dexPath = activityInfo.applicationInfo.sourceDir;
//Classloader用来释放dex文件的输出路径
String dexOutputDir = mContext.getApplicationInfo().dataDir;
//目标类可能使用的c或者c++的库文件的存放路径
String libPath = activityInfo.applicationInfo.nativeLibraryDir;

Log.i(TAG, "div:" + div + " " +
"packageName:" + packageName + " " +
"dexPath:" + dexPath + " " +
"dexOutputDir:" + dexOutputDir + " " +
"libPath:" + libPath);

DexClassLoader dcLoader = new DexClassLoader(dexPath, dexOutputDir,libPath,this.getClass().getClassLoader());
try {
Class<?> clazz = dcLoader.loadClass(packageName + ".TestClass");//包名分隔符应该由“/”变为“.”
Object obj = clazz.newInstance();
Class[] param = new Class[1];
param[0] = String.class;
Method action = clazz.getMethod("invoke", param);
action.invoke(obj, "test this function");
} catch (ClassNotFoundException e) {
Log.i(TAG, "ClassNotFoundException");
} catch (InstantiationException e) {
Log.i(TAG, "InstantiationException");
} catch (IllegalAccessException e) {
Log.i(TAG, "IllegalAccessException");
} catch (NoSuchMethodException e) {
Log.i(TAG, "NoSuchMethodException");
} catch (IllegalArgumentException e) {
Log.i(TAG, "IllegalArgumentException");
} catch (InvocationTargetException e) {
Log.i(TAG, "InvocationTargetException");
}
}