android中的类加载
先看个简单的打印日志,直接将代码贴到Activity#onCreate()中即可:
protected void onCreate(Bundle savedInstanceState) {
//...其他省略
Log.e(TAG, "classLoader -> " + getClassLoader());
String classPath = System.getProperty("java.class.path", ".");
String librarySearchPath = System.getProperty("java.library.path", "");
Log.e(TAG, "classPath -> " + classPath);
Log.e(TAG, "librarySearchPath -> " + librarySearchPath);
}
//打印日志
07-11 17:42:39.299 3874-3874/com.tv189.dl E/MainActivity: classLoader -> dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.tv189.dl-1/base.apk"],nativeLibraryDirectories=[/data/app/com.tv189.dl-1/lib/arm64, /vendor/lib64, /system/lib64]]]
07-11 17:42:39.299 3874-3874/com.tv189.dl E/MainActivity: classPath -> .
07-11 17:42:39.299 3874-3874/com.tv189.dl E/MainActivity: librarySearchPath -> /vendor/lib64:/system/lib64
说明:
dalvik.system.PathClassLoader
为android虚拟机类加载器
System.getProperty("java.class.path", ".")
查看当前虚拟机中加载类的路径
System.getProperty("java.library.path", "")
查看当前虚拟机中加载的动态链接库的路径
显然这里android-VM中是通过PathClassLoader来加载类的。
android中类加载器的类图如下:
涉及的类
PathClassLoader.java
PathClassLoader作用于本地文件系统中的文件列表和目录,而不能从网络加载classes。DexClassLoader.java
从包含classes.dex的jar/apk文件中加载class,该类可以加载未打包到apk中的代码。所以可以用来动态加载dex等文件中的classes。
该加载器需要传入一个应用私有的可写入的目录来缓存优化过的dex,如File dexOutputDir = context.getDir("dex", 0);
。BaseDexClassLoader.java
在android中使用该类加载系统classes和app中的classes。可以加载的文件类型为包含classes的jar/apk文件,也支持多个文件同时加载,使用File.pathSeparator
把文件路径连接即可并且android中默认为冒号:
,如path/to/a.jar:path/to/b.apk
。dex文件实际被加在到private DexPathList pathList
,而findClass(name)
也是从pathList
中来寻找已被加载的class的。
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}
- DexPathList.java
主要是类加载器把dex/resource保存在private final Element[] dexElements;
数组中的,每个Element
对应一个dex文件; 而BaseDexClassLoader#findClass
加载类时会从dexElements
中遍历获得,如果找到了Class就终止遍历。
注意这一段说明
/**
* A pair of lists of entries, associated with a {@code ClassLoader}.
* One of the lists is a dex/resource path — typically referred
* to as a "class path" — list, and the other names directories
* containing native code libraries. Class path entries may be any of:
* a {@code .jar} or {@code .zip} file containing an optional
* top-level {@code classes.dex} file as well as arbitrary resources,
* or a plain {@code .dex} file (with no possibility of associated
* resources).
*
* <p>This class also contains methods to use these lists to look up
* classes and resources.</p>
*/
/*package*/ final class DexPathList {
private static final String DEX_SUFFIX = ".dex";
/** class definition context */
private final ClassLoader definingContext;
/**
* List of dex/resource (class path) elements.
* Should be called pathElements, but the Facebook app uses reflection
* to modify 'dexElements' (http://b/7726934).
*/
private final Element[] dexElements;
/** List of native library directories. */
private final File[] nativeLibraryDirectories;
解决android中方法数超出问题
由于android系统的bug,一个dex文件中方法数最大值为65536;所以随着应用代码的膨胀必然会导致程序dex中的方法数超过最大值的,要解决这个问题的方案就是把dex进行切分为多个dex,但是android5.0之前dalvik虚拟机系统只加载一个dex,从android5.0开始系统使用art虚拟机已支持加载多个dex文件;google官方也出了补丁(使用multidex加载多个dex)来修复这个bug;所以如果app要适配android5.0以下的系统程序中就要使用MultiDex方案,这个补丁支持api4 ~ api20(android5.0以下)。
涉及的类
主要类在 android.support.multidex包中
MultiDex.java
加载多个dex,主要过程:- 删除旧的secondarydex缓存
clearOldDexDir
; - 尝试加载已存在的secondarydexes,失败则从新解压apk然后加载secondarydex;
- 删除旧的secondarydex缓存
MultiDexExtractor .java
解压apk,并从中提取dexes
动态加载dex
参考QQ空间热修复方案,在app启动时把补丁dex加载到Element[]
数组的前端(参见classloader类图);
测试时可以先修改某个类,然后把这个修改过的类转为jar,再把jar转为dex;
再把修改的内容改回去;
把下面代码放入项目中去加载dex;
import android.content.Context;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import dalvik.system.BaseDexClassLoader;
import dalvik.system.DexClassLoader;
/**
* 加载补丁dex文件,并插入系统dex数组中的第一个位置
* <p>
* Created by June on 2017/7/14.
*/
public class PatchDexLoader {
private Context mContext;
public PatchDexLoader(Context context) {
this.mContext = context;
}
public void load(String path) {
try {
// 已加载的dex
Object dexPathList = getField(BaseDexClassLoader.class, "pathList", mContext.getClassLoader());
Object dexElements = getField(dexPathList.getClass(), "dexElements", dexPathList);
// patchdex
String dexOptDir = mContext.getDir("patchDex_optDir", 0).getAbsolutePath();
DexClassLoader dcl = new DexClassLoader(path, dexOptDir, null, mContext.getClassLoader());
Object patchDexPathList = getField(BaseDexClassLoader.class, "pathList", dcl);
Object patchDexElements = getField(patchDexPathList.getClass(), "dexElements", patchDexPathList);
// 将patchdex和已加载的dexes数组拼接连接
Object concatDexElements = concatArray(patchDexElements, dexElements);
// 重新给dexPathList#dexElements赋值
setField(dexPathList.getClass(), "dexElements", dexPathList, concatDexElements);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
/**
* @param cls 被访问对象的class
* @param fieldName 对象的成员变量名
* @param object 被访问对象
* @return
* @throws NoSuchFieldException
* @throws IllegalAccessException
*/
public Object getField(Class<?> cls, String fieldName, Object object) throws NoSuchFieldException, IllegalAccessException {
Field field = cls.getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(object);
}
/**
* @param cls 被访问对象的class
* @param fieldName 对象的成员变量名
* @param object 被访问对象
* @param value 赋值给成员变量
* @throws NoSuchFieldException
* @throws IllegalAccessException
*/
public void setField(Class<?> cls, String fieldName, Object object, Object value) throws NoSuchFieldException, IllegalAccessException {
Field field = cls.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}
/**
* 连接两个数组(指定位置)
*
* @param left 连接后在新数组的左侧
* @param right 连接后在新数组的右侧
* @return
*/
public Object concatArray(Object left, Object right) {
int len1 = Array.getLength(left);
int len2 = Array.getLength(right);
int totalLen = len1 + len2;
Object concatArray = Array.newInstance(left.getClass().getComponentType(), totalLen);
for(int i = 0; i < len1; i++) {
Array.set(concatArray, i, Array.get(left, i));
}
for(int j = 0; j < len2; j++) {
Array.set(concatArray, len1 + j, Array.get(right, j));
}
return concatArray;
}
}