ClassLoader解析以及应用
[TOC]
Android ClassLoader浅析
Android中的dex、apk、ClassLoader详解
- 程序在运行时把对应的类加载到内存中,在Android上来说就是把Dex文件中的类加载到内存。
- 双亲委派机制
public abstract class ClassLoader { private ClassLoader(Void unused, ClassLoader parent) { this.parent = parent; } protected ClassLoader(ClassLoader parent) { this(checkCreateClassLoader(), parent); } protected ClassLoader() { this(checkCreateClassLoader(), getSystemClassLoader()); } public static ClassLoader getSystemClassLoader() { return SystemClassLoader.loader; } static private class SystemClassLoader { public static ClassLoader loader = ClassLoader.createSystemClassLoader(); } private static ClassLoader createSystemClassLoader() { String classPath = System.getProperty("java.class.path", "."); String librarySearchPath = System.getProperty("java.library.path", ""); return new PathClassLoader(classPath, librarySearchPath, BootClassLoader.getInstance()); } public Class<?> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); } protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { // First, check if the class has already been loaded Class<?> c = findLoadedClass(name); if (c == null) { try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the class. c = findClass(name); } } return c; } protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); }}
1. ClassLoader 继承关系
继承关系
[图片上传失败...(image-d446ce-1549854936566)]
-
PathClassLoader
只能加载已经安装到Android系统中的apk文件(/data/app目录),是Android默认使用的类加载器。 -
DexClassLoader
可以加载任意目录下的dex/jar/apk/zip文件,比PathClassLoader更灵活,是实现热修复的重点。
源码查看(基于API28 9.0)
在8.0以上,optimizedDirectory参数传的都是null,也就是说PathClassLoader
和DexClassLoader
没差别,DexClassLoader
可以加载未安装的apk,PathClassLoader也可以。
/** * Provides a simple {@link ClassLoader} implementation that operates on a list * of files and directories in the local file system, but does not attempt to * load classes from the network. Android uses this class for its system class * loader and for its application class loader(s). *///public class PathClassLoader extends BaseDexClassLoader { public PathClassLoader(String dexPath, ClassLoader parent) { super(dexPath, null, null, parent); } public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent){ super(dexPath, null, librarySearchPath, parent); }}
/** * A class loader that loads classes from {@code .jar} and {@code .apk} files * containing a {@code classes.dex} entry. This can be used to execute code not * installed as part of an application.*///public class DexClassLoader extends BaseDexClassLoader { public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) { super(dexPath, null, librarySearchPath, parent); }}
具体实现还得看BaseDexClassLoader
的构造方法。
public class BaseDexClassLoader extends ClassLoader { private final DexPathList pathList; public BaseDexClassLoader(String dexPath, File optimizedDirectory, String librarySearchPath, ClassLoader parent) { this(dexPath, optimizedDirectory, librarySearchPath, parent, false); } // 参数dexPath:待加载的apk/dex/jar文件路径; // 参数optimizedDirectory:dex的输出路径,将apk/dex/jar解压出dex文件,复制到指定路径,用于dalvik运行 // 参数librarySearchPath:加载时候需要用到的lib库,这个一般不用,可以传入Null // 参数parent:指定父加载器 public BaseDexClassLoader(String dexPath, File optimizedDirectory, String librarySearchPath, ClassLoader parent, boolean isTrusted) { super(parent); this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted); ... } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { ... Class c = pathList.findClass(name, suppressedExceptions); ... return c; } @Override protected URL findResource(String name) { return pathList.findResource(name); } @Override public String findLibrary(String name) { return pathList.findLibrary(name); }}
构造方法中创建了一个DexPathList对象
,而BaseDexClassLoader
中的各个findxxx()
调用的是DexPathList对象.findxxx()
DexPathList的构造方法和findxxx()方法。
final class DexPathList { private Element[] dexElements; DexPathList(ClassLoader definingContext, String dexPath, String librarySearchPath, File optimizedDirectory, boolean isTrusted) { ... // 加载dexPath路径下的dex和resource this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions, definingContext, isTrusted); ... } private static Element[] makeDexElements(List files, File optimizedDirectory, List suppressedExceptions, ClassLoader loader, boolean isTrusted) { Element[] elements = new Element[files.size()]; int elementsPos = 0; for (File file : files) { if (file.isDirectory()) { elements[elementsPos++] = new Element(file); } else if (file.isFile()) { String name = file.getName(); DexFile dex = null; if (name.endsWith(DEX_SUFFIX)) { // Raw dex file (not inside a zip/jar). try { dex = loadDexFile(file, optimizedDirectory, loader, elements); if (dex != null) { elements[elementsPos++] = new Element(dex, null); } } catch (IOException suppressed) { System.logE("Unable to load dex file: " + file, suppressed); suppressedExceptions.add(suppressed); } } else { try { dex = loadDexFile(file, optimizedDirectory, loader, elements); } catch (IOException suppressed) { suppressedExceptions.add(suppressed); } if (dex == null) { elements[elementsPos++] = new Element(file); } else { elements[elementsPos++] = new Element(dex, file); } } } else { System.logW("ClassLoader referenced unknown path: " + file); } } return elements; }}
挨个文件中寻找Class对象
public Class<?> findClass(String name, List suppressed) { for (Element element : dexElements) { Class<?> clazz = element.findClass(name, definingContext, suppressed); if (clazz != null) { return clazz; } } if (dexElementsSuppressedExceptions != null) { suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions)); } return null; }
2. 热更新实现原理
源码
如果是.dex
后缀的文件,会直接放到dexElements
数组中,否则就从这个文件中寻找dex
文件,找到后放到dexElements
数组。
ClassLoader#loadClass
就是遍历dexElements
数组,从dex
文件中找到要找的class
文件。
那我们是不是在这个数组前安插一个自己的数组,是不是就可以了?
做法:
拿到我当前应用的dexElements
数组,然后拿到已修复的dex或apk
文件的dexElements
数组,把已修复的数组放到当前应用的数组前面。
public class HotfixApplication extends Application { @Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); File apk = new File(getCacheDir() + "/hotfix.dex"); if (apk.exists()) { try { //1. 获取当前应用的dexElements数组 ClassLoader classLoader = getClassLoader(); Class loaderClass = BaseDexClassLoader.class; Field pathListField = loaderClass.getDeclaredField("pathList"); pathListField.setAccessible(true); Object pathListObject = pathListField.get(classLoader); Class pathListClass = pathListObject.getClass(); Field dexElementsField = pathListClass.getDeclaredField("dexElements"); dexElementsField.setAccessible(true); Object dexElementsObject = dexElementsField.get(pathListObject); //2. 获取已修复dex或apk的dexElements数组 PathClassLoader newClassLoader = new PathClassLoader(apk.getPath(), null); Object newPathListObject = pathListField.get(newClassLoader); Object newDexElementsObject = dexElementsField.get(newPathListObject); //3. 数组合并,把修复的数组放在自己的数组前面 int oldLength = Array.getLength(dexElementsObject); int newLength = Array.getLength(newDexElementsObject); Object concatDexElementsObject = Array.newInstance(dexElementsObject.getClass().getComponentType(), oldLength + newLength); for (int i = 0; i < newLength; i++) { Array.set(concatDexElementsObject, i, Array.get(newDexElementsObject, i)); } for (int i = 0; i < oldLength; i++) { Array.set(concatDexElementsObject, newLength + i, Array.get(dexElementsObject, i)); } dexElementsField.set(pathListObject, concatDexElementsObject); } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } }}
3. 插件化原理
利用ClassLoader
在新的apk中loadClass
,利用反射获取新的apk中的class
3.1 问题
Q: 怎么启动插件apk中的Activity?
A: 由于启动Activity系统会校验清单文件,如果没有该Activity会异常。我们可以创建一个代理的Activity,启动这个代理Activity,然后在代理Activity中执行真实对象的方法(这个真实对象需要有和Activity一样的方法)。
Q: 怎样获取插件apk中的Resource文件
A: 我们要知道,Resource也是利用AssetManager
获取的,所以我们要自定义一个AssetManager
,安放到Resource对象中。
public class ProxyActivity extends Activity { Object realActivity;// realActivity = ???; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState);// realActivity.onCreate(savedInstanceState); } @Override public AssetManager getAssets() { try { Class assetManagerClass = AssetManager.class; AssetManager assetManager = (AssetManager) assetManagerClass.newInstance(); Method addAssetPath = assetManagerClass.getDeclaredMethod("adAssetPath", String.class); addAssetPath.invoke(assetManager, "apkPath"); return assetManager; } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } return super.getAssets(); } @Override public Resources getResources() { return new Resources(getAssets(), getResources().getDisplayMetrics(), getResources().getConfiguration()); }}
更多相关文章
- 一款常用的 Squid 日志分析工具
- GitHub 标星 8K+!一款开源替代 ls 的工具你值得拥有!
- RHEL 6 下 DHCP+TFTP+FTP+PXE+Kickstart 实现无人值守安装
- Linux 环境下实战 Rsync 备份工具及配置 rsync+inotify 实时同步
- android 向服务器上传
- GLES2.0 on Android(安卓)emulator
- android studio 导入 融云问题之一 兼容4.0一下版本
- android Java代码设置textview的字体资源颜色
- Android安全风险检测项