【Android高级】DexClassloader和PathClassloader动态加载插件的实现

时间:2021-08-31 19:41:10

(一)DexClassloader

一、基本概念:


         在Android中可以跟java一样实现动态加载jar,但是Android使用德海Dalvik VM,不能直接加载java打包jar的byte code,需要通过dx工具来优化Dalvik byte code。
         Android在API中给出可动态加载的有:DexClassLoader 和 PathClassLoader。
         DexClassLoader:可加载jar、apk和dex,可以从SD卡中加载(本文使用这种方式)
         PathClassLoader:只能加载已经安装搭配Android系统中的apk文件

二、实施
        编写接口:Dynamic

package com.smilegames.dynamic.interfaces;

public interface Dynamic {
public String helloWorld();

public String smileGames();

public String fyt();
}
        编写实现类:DynamicTest
package com.smilegames.dynamic.impl;

import com.smilegames.dynamic.interfaces.Dynamic;

public class DynamicTest implements Dynamic {

@Override
public String helloWorld() {
return "Hello Word!";
}

@Override
public String smileGames() {
return "Smile Games";
}

@Override
public String fyt() {
return "fengyoutian";
}

}
三、打包并编译成dex
       将接口打包成jar:dynamic.jar( 只打包这Dynamic.java这一个接口
       将实现类打包成jar:dynamic_test.jar( 只打包DynamicTest.java这一个实现类
       将打包后的实现类(dynamic_test.jar)编译成dex:dynamic_impl.jar
              1、将dynamic_test.jar拷贝到SDK安装目录android-sdk-windows\platform-tools下( ps:如果platform-tools没有dx.bat,可拷贝到build-tools目录下有dx.bat的子目录
              2、执行以下命令:
dx --dex --output=dynamic_impl.jar dynamic_test.jar
             3、将dynamic.jar引入测试实例
             4、将dynamic_impl.jar放到模拟器或真机的sdcard

四、修改onCreate例子
       
    @Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

// 或许activity按钮
helloWorld = (Button) findViewById(R.id.helloWorld);
smileGames = (Button) findViewById(R.id.smileGames);
fyt = (Button) findViewById(R.id.fyt);

/*使用DexCkassLoader方式加载类*/
// dex压缩文件的路径(可以是apk,jar,zip格式)
String dexPath = Environment.getExternalStorageDirectory().toString() + File.separator + "dynamic_impl.jar";

// dex解压释放后的目录
String dexOutputDirs = Environment.getExternalStorageDirectory().toString();

// 定义DexClassLoader
// 第一个参数:是dex压缩文件的路径
// 第二个参数:是dex解压缩后存放的目录
// 第三个参数:是C/C++依赖的本地库文件目录,可以为null
// 第四个参数:是上一级的类加载器
DexClassLoader dexClassLoader = new DexClassLoader(dexPath,dexOutputDirs,null,getClassLoader());

Class libProvierClazz = null;
// 使用DexClassLoader加载类
try {
libProvierClazz = dexClassLoader.loadClass("com.smilegames.dynamic.impl.DynamicTest");
// 创建dynamic实例
dynamic = (Dynamic) libProvierClazz.newInstance();
} catch (Exception e) {
e.printStackTrace();
}

helloWorld.setOnClickListener(new HelloWorldOnClickListener());
smileGames.setOnClickListener(new SmileGamesOnClickListener());
fyt.setOnClickListener(new FytOnClickListener());
}

private final class HelloWorldOnClickListener implements View.OnClickListener {
@Override
public void onClick(View v) {
if (null != dynamic) {
Toast.makeText(getApplicationContext(), dynamic.helloWorld(), 1500).show();
} else {
Toast.makeText(getApplicationContext(), "类加载失败", 1500).show();
}
}
}

private final class SmileGamesOnClickListener implements View.OnClickListener {
@Override
public void onClick(View v) {
if (null != dynamic) {
Toast.makeText(getApplicationContext(), dynamic.smileGames(), 1500).show();
} else {
Toast.makeText(getApplicationContext(), "类加载失败", 1500).show();
}
}
}
       1、运行这段代码时4.0.1以上版本会报: java.lang.IllegalArgumentException: optimizedDirectory not readable/writable: /storage/sdcard0
      可以通过这个授权解决:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> 
      2、授权之后又会报: java.lang.IllegalArgumentException: Optimized data directory /storage/sdcard0 is not owned by the current user. Shared storage cannot protect your application from code injection attacks.
      这个问题的原因是:在4.1系统由于This class loader requires an application-private, writable directory to cache optimized classes为了防止一下问题:
External storage does not provide access controls necessary to protect your application from code injection attacks.
所以加了一个判断Libcore.os.getuid() != Libcore.os.stat(parent).st_uid判断两个程序是不是同一个uid
 private DexFile(String sourceName, String outputName, int flags) throws IOException {
if (outputName != null) {
try {
String parent = new File(outputName).getParent();
if (Libcore.os.getuid() != Libcore.os.stat(parent).st_uid) {
throw new IllegalArgumentException("Optimized data directory " + parent
+ " is not owned by the current user. Shared storage cannot protect"
+ " your application from code injection attacks.");
}
} catch (ErrnoException ignored) {
// assume we'll fail with a more contextual error later
}
}

mCookie = openDexFile(sourceName, outputName, flags);
mFileName = sourceName;
guard.open("close");
//System.out.println("DEX FILE cookie is " + mCookie);

}
        解决方法是:指定dexoutputpath为APP自己的缓存目录
File dexOutputDir = context.getDir("dex", 0);
DexClassLoader dexClassLoader = new DexClassLoader(dexPath,dexOutputDir.getAbsolutePath(),null,getClassLoader());
  ps:第四步的问题最终是2导致的,所以只需用2的解决方案即可,不需要1的授权。

 执行结果自行尝试。。。


(二)PathClassloader


public void dexLoad() {
/** 使用DexClassLoader方式加载类 */
// dex压缩文件的路径(可以是apk,jar,zip格式)
String dexPath = Environment.getExternalStorageDirectory().toString()
+ File.separator + "dynamic_temp.jar";
// dex解压释放后的目录
// String dexOutputDir = getApplicationInfo().dataDir;
// String dexOutputDirs = Environment.getExternalStorageDirectory()
// .toString();
// 定义DexClassLoader
// 第一个参数:是dex压缩文件的路径
// 第二个参数:是dex解压缩后存放的目录
// 第三个参数:是C/C++依赖的本地库文件目录,可以为null
// 第四个参数:是上一级的类加载器
File dexOutputDir = getApplicationContext().getDir("dex", 0);
DexClassLoader cl = new DexClassLoader(dexPath,
dexOutputDir.getAbsolutePath(), null, getClassLoader());

Log.i("tag", dexOutputDir.getAbsolutePath());

Class libProviderClazz;
try {
libProviderClazz = cl.loadClass("com.dynamic.impl.Dynamic");
// dynamic = (IDynamic) libProviderClazz.newInstance();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

}

// MyInterface dynamic;
Class dynamic;

public void pathLoad() {
/** 使用PathClassLoader方法加载类 */
// 创建一个意图,用来找到指定的apk:这里的"com.dynamic.impl是指定apk中在AndroidMainfest.xml文件中定义的<action name="com.dynamic.impl"/>
Intent intent = new Intent(
"com.example.androidendyeartest.MainActivity", null);
// 获得包管理器
PackageManager pm = getPackageManager();
List<ResolveInfo> resolveinfoes = pm.queryIntentActivities(intent, 0);
// 获得指定的activity的信息
ActivityInfo actInfo = resolveinfoes.get(0).activityInfo;
// 获得apk的目录或者jar的目录
String apkPath = actInfo.applicationInfo.sourceDir;
// native代码的目录
String libPath = actInfo.applicationInfo.nativeLibraryDir;
// 创建类加载器,把dex加载到虚拟机中
// 第一个参数:是指定apk安装的路径,这个路径要注意只能是通过actInfo.applicationInfo.sourceDir来获取
// 第二个参数:是C/C++依赖的本地库文件目录,可以为null
// 第三个参数:是上一级的类加载器
PathClassLoader pcl = new PathClassLoader(apkPath, libPath,
this.getClassLoader());
// 加载类
try {
dynamic = pcl.loadClass("com.test.bean.MyBean");


} catch (Exception exception) {
exception.printStackTrace();
}
}
应用:

@OnClick(R.id.b1)
public void t1(View v) {
if (dynamic != null) {
// dynamic.show1(getApplicationContext());

Method method;
try {

method = dynamic.getMethod("show1",Context.class);
//Context context=getApplicationContext();
method.invoke(dynamic.newInstance(), getApplicationContext());
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

} else {
Toast.makeText(getApplicationContext(), "类加载失败", 1500).show();
}
}