Android支持动态加载的两种方式是:DexClassLoader和PathClassLoader,DexClassLoader可加载jar/apk/dex,且支持从SD卡加载;PathClassLoader只能加载已经安装在Android系统内APK文件( /data/app 目录下),其它位置的文件加载的时候都会出现 ClassNotFoundException。 因为 PathClassLoader 会去读取 /data/dalvik-cache 目录下的经过 Dalvik 优化过的 dex 文件,这个目录的 dex 文件是在安装 apk 包的时候由 Dalvik 生成的,没有安装的时候,自然没有生成那个文件。
我们现在要实现的一个需求是:如何调用一个非本应用的java程序,如下:
app 与loutillib两个模块没有任何的依耐关系,在Module App中,我们想调用Loutillib中的LogUitl输出一条log。
所以我们采取dexClassLoader来加载这个jar
第一步:
首先我们将logutil这个module 依赖主app 然后运行下,这是时候会生成class.jar
然后在logutil中添加如下代码: 将class.jar重新命名下log.jar
task makeJar(type:Copy){
delete 'build/libs/log.jar'
from('build/intermediates/bundles/release/')
into('build/libs/')
include('classes.jar')
rename ('classes.jar', 'log.jar')
exclude('test/','BuildConfig.class','R.class')
exclude{it.name.startsWith('R$');}
}
makeJar.dependsOn(build)
上面配置好gradle 环境变量后,就可以再终端执行: gradlew makeJar
注意,这个jar还不能被加载,这个是基于class的jar,Dalvik虚拟机加载的是dex字节码,所以需要将class转化为dex字节码。这个需要用到dx命令,这个可以在Android\sdk\build-tools\23.0.0中找到,把log.jar拷贝到这个目录下,执行
dx --dex --output=new_log.jar log.jar
生成新的jar 然后push 到sdcard中
adb push new_log.jar sdcard/
以上准备工作 做好了之后, 第二步就开始撸DexClassLoader的代码了。
第二步:直接贴上代码,利用java 类加载
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
//动态加载dex文件 这里是个jar 包里面的方法
public void start(View view) {
//dex解压释放后的目录
final File dexOutPutDir = getDir("dex", 0);
//dex所在目录
final String dexPath = Environment.getExternalStorageDirectory().toString() + File.separator + "new_log.jar";
//第一个参数:是dex压缩文件的路径
//第二个参数:是dex解压缩后存放的目录
//第三个参数:是C/C++依赖的本地库文件目录,可以为null
//第四个参数:是上一级的类加载器
DexClassLoader classLoader = new DexClassLoader(dexPath, dexOutPutDir.getAbsolutePath(), null, getClassLoader());
try {
final Class<?> loadClazz = classLoader.loadClass("plugin.charles.com.logutil.LogUitl");
final Object o = loadClazz.newInstance();
final Method printLogMethod = loadClazz.getDeclaredMethod("printLog");
printLogMethod.setAccessible(true);
printLogMethod.invoke(o);
} catch (Exception e) {
e.printStackTrace();
}
}
}
下面介绍的是动态加载未安装的apk.然后读取apk中图片,显示在宿主app中
1.首先准备换肤的apk
skindemo-debug.apk
skindemo2-debug.apk
这2个apk中都有一张图片,然后我们push这两个apk到sdcard中就可以
- 资源加载问题,怎么通过Resources 加载另外一个apk中的图片
/**
* 获取AssetManager 用来加载插件资源
*
* @param pFilePath 插件的路径
* @return
*/
private AssetManager createAssetManager(String pFilePath) {
try {
final AssetManager assetManager = AssetManager.class.newInstance();
final Class<?> assetManagerClazz = Class.forName("android.content.res.AssetManager");
final Method addAssetPathMethod = assetManagerClazz.getDeclaredMethod("addAssetPath", String.class);
addAssetPathMethod.setAccessible(true);
addAssetPathMethod.invoke(assetManager, pFilePath);
return assetManager;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
//这个Resources就可以加载非宿主apk中的资源
private Resources createResources(String pFilePath) {
final AssetManager assetManager = createAssetManager(pFilePath);
Resources superRes = this.getResources();
return new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());
}
3.宿主apk,动态换肤的点击事件代码
private boolean mChange = false;
private String skinType = "";
private int APK_TYPE;
/**
* 替换bg
*/
public void replace(View view) {
if (!mChange) {
skinType = "skindemo-debug.apk";
APK_TYPE = 1;
mChange = true;
} else {
skinType = "skindemo2-debug.apk";
APK_TYPE = 2;
mChange = false;
}
final String path = Environment.getExternalStorageDirectory() + File.separator + skinType;
final String pkgName = getUninstallApkPkgName(this, path);
dynamicLoadApk(path, pkgName);
}
private void dynamicLoadApk(String pApkFilePath, String pApkPacketName) {
File file = getDir("dex", Context.MODE_PRIVATE);
//第一个参数:是dex压缩文件的路径
//第二个参数:是dex解压缩后存放的目录
//第三个参数:是C/C++依赖的本地库文件目录,可以为null
//第四个参数:是上一级的类加载器
DexClassLoader classLoader = new DexClassLoader(pApkFilePath, file.getAbsolutePath(), null, getClassLoader());
try {
final Class<?> loadClazz = classLoader.loadClass(pApkPacketName + ".R$drawable");
//插件中皮肤的名称是skin_one
final Field skinOneField = APK_TYPE == 1 ? loadClazz.getDeclaredField("skin_one") : loadClazz.getDeclaredField("skin_two");
skinOneField.setAccessible(true);
//反射获取skin_one的resousreId
final int resousreId = (int) skinOneField.get(R.id.class);
//可以加载插件资源的Resources
final Resources resources = createResources(pApkFilePath);
if (resources != null) {
final Drawable drawable = resources.getDrawable(resousreId);
linearLayout.setBackgroundDrawable(drawable);
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 根据sdcard路径,获取未安装apk的信息
*
* @param context
* @param pApkFilePath apk文件的path
* @return
*/
private String getUninstallApkPkgName(Context context, String pApkFilePath) {
PackageManager pm = context.getPackageManager();
PackageInfo pkgInfo = pm.getPackageArchiveInfo(pApkFilePath, PackageManager.GET_ACTIVITIES);
if (pkgInfo != null) {
ApplicationInfo appInfo = pkgInfo.applicationInfo;
return appInfo.packageName;
}
return "";
}
此时就完成了,只需要看效果了
源码链接:https://github.com/15189611/pluginDemo