本文由 Luzhuo 编写,转发请保留该信息. 原文: http://blog.csdn.net/rozol/article/details/77758194
ClassLoader 用来动态加载class文件到内存中使用的 ClassLoader 使用 双亲委托模型 来搜索类的 ClassLoader 在Android中有两个子类来加载.dex文件, 分别是 DexClassLoader / PathClassLoader DexClassLoader 可以加载 jar / apk(未安装/已安装) / dex PathClassLoader 只可以加载 系统中已安装的apk
从 ClassLoader 源码可知, 这是个抽象类, 首先调用 Class<?> c = findLoadedClass(name) 去检查该类是否已经被加载, 如果已被加载则返回被加载的类, 如果未被加载则调用 c = parent.loadClass(name, false); 去父加载器检索, 如果父加载器为null则说明父加载器是 BootStrap ClassLoader 则调用 c = findBootstrapClassOrNull(name); 去查找该类, 如果 BootStrap ClassLoader 没有检索到, 则调用 Class<?> findClass(String name) 方法, 按自己的规则去检索.
ClassLoader 的子类
其中MyClassLoader是自定义ClassLoader, 继承ClassLoader, 复写 findClass(String name) 方法示意图如下, 双亲委托模型 容易让人联想到责任链设计模式 (但是他们不一样)
自定义ClassLoader: 继承 java.lang.ClassLoader ;复写 Class
/** * 自定义类加载器 * 需求: 加载网络上的Class文件, 并生成对象 * * 自定义类加载器步骤: 继承 java.lang.ClassLoader ;复写 Class<?> findClass(String name) 方法 * @author Luzhuo * */ public class MyClassLoader extends ClassLoader{ private String classUrl; public MyClassLoader(String url) { this.classUrl = url; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { Class<?> clazz = null; // 根据名字, 获取Class文件的字节码数据 byte[] classData = getClassData(name); if (classData == null) throw new ClassNotFoundException(name); // 将Class文件的字节码数据生成Class类实例 clazz = defineClass(name, classData, 0, classData.length); return clazz; } /** * 根据名字, 获取Class文件的字节码数据 * @param name * @return */ private byte[] getClassData(String name) { InputStream is = null; try { // http://me.luhzuo/me.luzhuo.classloader.MyClassLoader.class => http://me.luhzuo/me/luzhuo/classloader/MyClassLoader.class String classNetPath = classUrl + "/" + name.replace(".", "/") + ".class"; is = new URL(classNetPath).openStream(); // TODO 如果这个类 ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte[] buff = new byte[1024]; int len = -1; while((len = is.read(buff)) != -1) { baos.write(buff, 0 ,len); } return baos.toByteArray(); } catch (Exception e) { e.printStackTrace(); } finally { if (is != null) { try { is.close(); } catch(IOException e) { e.printStackTrace(); } } } return null; } }MyClass.java: 存在于网络上的class文件
public class MyClass { /** * 获取简短类名 * @return */ public String getName(){ return MyClass.class.getSimpleName(); } }Test.java: 测试文件
public class Test{ public static void main(String[] args) throws ClassNotFoundException, Exception { // 测试自定义类加载器 Class<?> clazz = testMyClassLoader("http://luzhuo.me/code/ClassLoader/bin", "me.luzhuo.MyClass"); // 调用方法 Method getName = clazz.getMethod("getName"); String name = (String) getName.invoke(clazz.newInstance()); System.out.println("TestMyClassLoader - getName: " + name); System.out.println("TestMyClassLoader - classLoader: " + clazz.getClassLoader()); } /** * 测试自定义类加载器 * @param url * @param className * @return * @throws ClassNotFoundException */ private static Class<?> testMyClassLoader(String url, String className) throws ClassNotFoundException{ MyClassLoader classLoader = new MyClassLoader(url); Class<?> clazz = classLoader.loadClass(className); return clazz; } }Java的 ClassLoader 不能直接在Android中使用, 因为Android用的是.dex文件, 是将java代码重新整合后的文件; 因此在Android中分别有两个主要的类 DexClassLoader 和 PathClassLoader, 他们都继承ClassLoader是对其的拓展. DexClassLoader 可以加载 jar / apk(未安装/已安装) / dex PathClassLoader 只可以加载 系统中已安装的apk
Plugin.java: 插件类
public class Plugin { /** * 普通方法 */ public String getName(){ return "Name is " + Plugin.class.getSimpleName(); } }创建一个插件工程, 并创建一个插件类(me.luzhuo.testapk.Plugin), 然后在资源目录mipmap下放上一张图片 testapk_bg.png. 接下来就需要把该工程生成一个插件apk, 直接点run即可生成apk文件.
DexClassLoaderTest.java: DexClassLoader 的使用代码
/** * DexClassLoader 可以加载 jar / apk(未安装/已安装) / dex */ public class DexClassLoaderTest { /** * 从apk文件中加载Class字节码对象 * @param context * @param apkPath apk文件路径 * @param libPath .so文件路径 * @param className 要加载的全类名 * @return 找到的类定义 * @exception FileNotFoundException apk文件不存在 * @exception ClassNotFoundException 没有找到指定的类 */ @NonNull public Class<?> dexClassLoder(@NonNull Context context, @NonNull String apkPath, @Nullable String libPath, @NonNull String className) throws ClassNotFoundException, FileNotFoundException { if (!new File(apkPath).exists()) throw new FileNotFoundException("要查找的文件 " + apkPath + " 没有找到."); String dexOutputDir = context.getApplicationInfo().dataDir; // apk文件路径, 解压后.dex存储路径, .os文件路径, 父类加载器 DexClassLoader classLoader = new DexClassLoader(apkPath, dexOutputDir, libPath, this.getClass().getClassLoader()); // 从加载进来的apk中获取指定的类, 并用反射调用其方法 Class<?> clazz = classLoader.loadClass(className); // me.luzhuo.testapk.Plugin return clazz; } }PathClassLoaderTest.java: PathClassLoader 的使用代码
/** * PathClassLoader 只可以加载 系统中已安装的apk */ public class PathClassLoaderTest { /** * 从已安装的apk中获取Class字节码对象 * @param apkPath 系统中安装的apk路径 * @param libPath .os文件路径 * @param className 全类名 * @return 找到的类定义 * @exception FileNotFoundException apk文件不存在 * @exception ClassNotFoundException 没有找到指定的类 */ @NonNull public Class<?> pathClassLoader(@NonNull String apkPath, @Nullable String libPath, @NonNull String className) throws FileNotFoundException, ClassNotFoundException { if (!new File(apkPath).exists()) throw new FileNotFoundException("要查找的文件 " + apkPath + " 没有找到."); // apk文件路径, .os文件路径, 父类加载器 PathClassLoader classLoader = new PathClassLoader(apkPath, libPath, this.getClass().getClassLoader()); Class<?> clazz = classLoader.loadClass(className); return clazz; } }从上述代码就可看出, PathClassLoader 的使用只是比 DexClassLoader 的使用上了一个dex文件的输出路径(dexOutputDir)而已, 这个会在后面分析源码时会讲到.
APkUtils.java: getAPKInfoByLocal() 方法用于查找并获取已安装的应用信息
public class APkUtils { /** * 从本地获取指定包名的的apk信息 * @param context * @param packName 包名 * @return 返回String[4], 0: 包名; 1: apk路径; 2: .so文件路径 */ @NonNull public static String[] getAPKInfoByLocal(@NonNull Context context, @NonNull final String packName) throws FileNotFoundException { Intent intent = new Intent(Intent.ACTION_MAIN, null); intent.addCategory(Intent.CATEGORY_LAUNCHER); PackageManager pm = context.getPackageManager(); // 从已安装的应用中找出满足 'ACTION_MAIN' 和 'CATEGORY_LAUNCHER' 条件的应用 final List<ResolveInfo> plugins = pm.queryIntentActivities(intent, 0); ResolveInfo tagResolveInfo = null; if(plugins != null){ for(int i = 0;i < plugins.size();i++){ ResolveInfo resolveInfo = plugins.get(i); ActivityInfo activityInfo = resolveInfo.activityInfo; String packageName = activityInfo.packageName; // 获取应用的包名 if(packageName.equals(packName)){ tagResolveInfo = plugins.get(i); break; } } } if(tagResolveInfo == null) throw new FileNotFoundException("要查找应用 " + packName + " 没有找到."); ActivityInfo activityInfo = tagResolveInfo.activityInfo; String packageName = activityInfo.packageName; String apkPath = activityInfo.applicationInfo.sourceDir; // /data/app/me.luzhuo.testapk-1.apk String dexOutputDir = context.getApplicationInfo().dataDir; // /data/data/me.luzhuo.classloader_android String libPath = activityInfo.applicationInfo.nativeLibraryDir; // /data/app-lib/me.luzhuo.testapk-1 return new String[]{packageName, apkPath, libPath}; } }MainActivity: 测试代码
/** * ClassLoader在Android中的运用 * * DexClassLoader 可以加载 jar / apk(未安装/已安装) / dex * PathClassLoader 只可以加载 系统中已安装的apk * * Android程序与Java程序最大的区别在于 上下文环境(Context) 不同, * 而这个不同也将导致Java在Android中运行会面临几个问题: * 1. 组件需要注册才能使用 * 2. 程序获取敏感信息需要权限 * 3. 加载进来的代码需要的资源id, 未必在现有的Resource实例中 */ public class MainActivity extends AppCompatActivity { private static final String TAG = MainActivity.class.getSimpleName(); private ImageView iv; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); iv = (ImageView) findViewById(R.id.iv); callClassLoader(); } /** * 使用的案例代码 */ private final void callClassLoader(){ try { // 方式1: 加载外部的apk文件 String apkPath = Environment.getExternalStorageDirectory() + File.separator + "testapk.apk"; String libPath = null; String className = "me.luzhuo.testapk" + ".Plugin"; // 方式2: 加载已安装的apk文件 String packageName = "me.luzhuo.testapk"; String[] apkinfos = getAPKInfoByLocal(this, packageName); // 0: 包名; 1: apk路径; 2: .so文件路径 apkPath = apkinfos[1]; libPath = apkinfos[2]; className = packageName + ".Plugin"; // 加载apk文件, 获取Class // 方式1: DexClassLoader 可以加载 jar / apk(未安装/已安装) / dex DexClassLoaderTest dexClassLoader = new DexClassLoaderTest(); Class<?> clazz = dexClassLoader.dexClassLoder(this, apkPath, libPath, className); // 方式2: PathClassLoader 只可以加载 系统中已安装的apk PathClassLoaderTest pathClassLoader = new PathClassLoaderTest(); clazz = pathClassLoader.pathClassLoader(apkPath, libPath, className); // 调用插件里的方法 Object obj = clazz.newInstance(); Method getName = clazz.getMethod("getName"); String name = (String) getName.invoke(obj); Log.e(TAG, "Plugin: " + name); // 获取插件里的资源文件 // 插件的上下文 Context plugContext = this.createPackageContext(packageName, Context.CONTEXT_IGNORE_SECURITY | Context.CONTEXT_INCLUDE_CODE); String packageName_mipmap = packageName + ".R$mipmap"; Class<?> resDrawable = pathClassLoader.pathClassLoader(apkPath, null, packageName_mipmap); // pathClassLoader resDrawable = dexClassLoader.dexClassLoder(this, apkPath, libPath, packageName_mipmap); // dexClassLoader for (Field resField : resDrawable.getDeclaredFields()){ // 查找指定图片名的字段 String resName = resField.getName(); if (!resName.equals("testapk_bg")) continue; int resId = resField.getInt(R.mipmap.class); // 获取该资源的id值 iv.setImageDrawable(plugContext.getResources().getDrawable(resId)); // 设置图片 Log.e(TAG, "resId: " + id); } } catch (Exception e) { e.printStackTrace(); } } }以上就是完整的代码了.
在获取apk文件的方式分为两种: 第一种是从sd卡等外部存储设备上获取; 第二种则是从已安装的内部存储获取
第一种, 从外部文件获取, DexClassLoader 可以使用, PathClassLoader 不可以使用
String apkPath = Environment.getExternalStorageDirectory() + File.separator + "testapk.apk"; String libPath = null; String className = "me.luzhuo.testapk" + ".Plugin";第二种, 从已安装在手机的内部存储区获取, 主要是把手机里已安装的包都遍历一遍 DexClassLoader 可以使用, PathClassLoader 可以使用
@NonNull public static String[] getAPKInfoByLocal(@NonNull Context context, @NonNull final String packName) throws FileNotFoundException { Intent intent = new Intent(Intent.ACTION_MAIN, null); intent.addCategory(Intent.CATEGORY_LAUNCHER); PackageManager pm = context.getPackageManager(); // 从已安装的应用中找出满足 'ACTION_MAIN' 和 'CATEGORY_LAUNCHER' 条件的应用 final List<ResolveInfo> plugins = pm.queryIntentActivities(intent, 0); ResolveInfo tagResolveInfo = null; if(plugins != null){ for(int i = 0;i < plugins.size();i++){ ResolveInfo resolveInfo = plugins.get(i); ActivityInfo activityInfo = resolveInfo.activityInfo; String packageName = activityInfo.packageName; // 获取应用的包名 if(packageName.equals(packName)){ tagResolveInfo = plugins.get(i); break; } } } if(tagResolveInfo == null) throw new FileNotFoundException("要查找应用 " + packName + " 没有找到."); ActivityInfo activityInfo = tagResolveInfo.activityInfo; String packageName = activityInfo.packageName; String apkPath = activityInfo.applicationInfo.sourceDir; // /data/app/me.luzhuo.testapk-1.apk String dexOutputDir = context.getApplicationInfo().dataDir; // /data/data/me.luzhuo.classloader_android String libPath = activityInfo.applicationInfo.nativeLibraryDir; // /data/app-lib/me.luzhuo.testapk-1 return new String[]{packageName, apkPath, libPath}; }在 DexClassLoader 和 PathClassLoader 的使用上, 除了需要主要apk文件的来源外, 主要是少个一个dex解压路径, 其他没有区别
DexClassLoader
// apk文件路径, 解压后.dex存储路径, .so文件路径, 父类加载器 DexClassLoader classLoader = new DexClassLoader(apkPath, dexOutputDir, libPath, this.getClass().getClassLoader()); Class<?> clazz = classLoader.loadClass(className);PathClassLoader
// apk文件路径, .so文件路径, 父类加载器 PathClassLoader classLoader = new PathClassLoader(apkPath, libPath, this.getClass().getClassLoader()); Class<?> clazz = classLoader.loadClass(className);调用插件里的方法
// 调用插件里的方法 Object obj = clazz.newInstance(); Method getName = clazz.getMethod("getName"); String name = (String) getName.invoke(obj); Log.e(TAG, "Plugin: " + name);获取插件里的资源文件
// 获取插件里的资源文件 // 插件的上下文 Context plugContext = this.createPackageContext(packageName, Context.CONTEXT_IGNORE_SECURITY | Context.CONTEXT_INCLUDE_CODE); String packageName_mipmap = packageName + ".R$mipmap"; Class<?> resDrawable = pathClassLoader.pathClassLoader(apkPath, null, packageName_mipmap); // pathClassLoader resDrawable = dexClassLoader.dexClassLoder(this, apkPath, libPath, packageName_mipmap); // dexClassLoader for (Field resField : resDrawable.getDeclaredFields()){ // 查找指定图片名的字段 String resName = resField.getName(); if (!resName.equals("testapk_bg")) continue; int resId = resField.getInt(R.mipmap.class); // 获取该资源的id值 iv.setImageDrawable(plugContext.getResources().getDrawable(resId)); // 设置图片 Log.e(TAG, "resId: " + id); }