使用到的技术点
1、Java类加载机制;
2、Android加载dex文件;
3、反射;原理:
用修复好的类替换有问题的类。在App重新启动后让Classloader去加载新的类。因为在App运行到一半的时候,所有需要发生变更的类已经被加载过了,在Android上是无法对一个类进行卸载的。如果不重启,
原来的类还在虚拟机中,就无法加载新类
。因此,只有在下次重启的时候,在还没有走到业务逻辑之前抢先加载补丁中的新类,这样后续访问这个类时,就会Resolve为新类。从而达到热修复的目的。关键问题是:
如何替换一个类?
这里没有直接替换一个类,而是通过类加载机制,在加载问题类之前,先加载补丁类的方案达到替换的目的。具体操作的依据,首先因为同一个类加载器在尝试加载一个类的时候,会先判断这个类是否已经加载,如果已经加载则不会再次去加载;其次,Android加载dex时,会按照先后顺序依次加载的
。
一、Java加载机制
ClassLoader#loadClass()方法
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;
}
(1)、从当前类加载器的已加载类缓存中根据类的全路径名查询是否存在该类,如果存在则直接返回。
(2)、如果当前类存在父类加载器,则调用父类加载器的loadClass(name,false)方法对其进行加载。
(3)、如果当前类加载器不存在父类加载器,则直接调用根类加载器对该类进行加载。
(4)、如果当前类的所有父类加载器都没有成功加载class,则尝试调用当前类加载器的findClass方法对其进行加载,该方法就是我们自定义加载器需要重写的方法。
(5)、最后如果类被成功加载,则做一些性能数据的统计。
(6)、由于loadClass指定了resolve为false,所以不会进行连接阶段的继续执行,这也就解释了为什么通过类加载器加载类并不会导致类的初始化。
二、Android类加载的机制
Android中相关的类加载器PathClassLoader和DexClassLoader。
(1)、DexClassLoader:能够加载
自定义的jar/apk/dex
(2)、PathClassLoader:只能加载系统中已经安装过的apk
所以Android系统默认的类加载器为PathClassLoader
,而DexClassLoader可以像JVM的ClassLoader一样提供动态加载。
PathClassLoader和DexClassLoader加载Class的相关源码:
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;
@Override
protected Class> findClass(String name) throws ClassNotFoundException {
List suppressedExceptions = new ArrayList();
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 #findClass()
final class DexPathList {
/**
* List of dex/resource (class path) elements.
* Should be called pathElements, but the Facebook app uses reflection
* to modify 'dexElements' (http://b/7726934).
*/
private Element[] dexElements;
public Class findClass(String name, List suppressed) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
}
DexFile#loadClassBinaryName()
public Class loadClassBinaryName(String name, ClassLoader loader, List suppressed) {
return defineClass(name, loader, mCookie, this, suppressed);
}
private static Class defineClass(String name, ClassLoader loader, Object cookie,
DexFile dexFile, List suppressed) {
Class result = null;
try {
result = defineClassNative(name, loader, cookie, dexFile);
} catch (NoClassDefFoundError e) {
if (suppressed != null) {
suppressed.add(e);
}
} catch (ClassNotFoundException e) {
if (suppressed != null) {
suppressed.add(e);
}
}
return result;
}
从上面的源码可以得知,当Android中类加载器尝试加载类的时候,会调用DexPathList 的findClass方法,通过遍历dexElements中的Element,得到DexFile,再通过DexFile调用loadClassBinaryName方法加载类。类加载成功后,就直接返回。因此,可以通过将修复好的dex插入到dexElements的集合(出现bug的xxx.class所在的dex的前面)的位置,就可以达到间接替换bug类的目的。
最本质的实现原理:类加载器去加载某个类的时候,是去dexElements里面从头往下查找的
。fixed.dex,classes1.dex,classes2.dex,classes3.dex
三、具体实现
Class BaseDexClassLoader{
private DexPathList pathList;
}
Class DexPathList{
private Element[] dexElements;
}
先获取到已安装App中原有的dexElements(appDexElements)和通过DexClassLoader加载已经修复好的dex得到的dexElements(fixedDexElements)。然后再将appDexElements和fixedDexElements合并成一个新的dexElements(newDexElements)。
核心类代码
FixDexUtils
public class FixDexUtils {
private static HashSet sLoadedDexFiles = new HashSet();
static {
sLoadedDexFiles.clear();
}
public static void loadFixedDex(Context context){
File fileDir = context.getDir(Constants.DEX_DIR, Context.MODE_PRIVATE);
File[] dexFiles = fileDir.listFiles();
for(File file:dexFiles){
if(file.getName().startsWith("classes") && file.getName().endsWith(".dex")){
sLoadedDexFiles.add(file);
}
}
doDexInject(context, fileDir, sLoadedDexFiles);
}
private static void doDexInject(final Context context, File filesDir, HashSet loadedDexs){
String optimizedDir = filesDir.getAbsolutePath() + File.separator + Constants.OPT_DEX;
File fopt = new File(optimizedDir);
if(!fopt.exists()){
fopt.mkdirs();
}
try {
PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
for(File dex:loadedDexs){
DexClassLoader dexClassLoader = new DexClassLoader(dex.getAbsolutePath(), fopt.getAbsolutePath(), null, pathClassLoader);
Object dexObj = getPathList(dexClassLoader);
Object pathObj = getPathList(pathClassLoader);
Object dexElementsList = getDexElements(dexObj);
Object pathDexElementsList = getDexElements(pathObj);
//合并dexElements
Object dexElements = combineArray(dexElementsList, pathDexElementsList);
//重新给PathList里面的Element[] dexElements赋值
Object pathList = getPathList(pathClassLoader);
setFiled(pathList, pathList.getClass(), "dexElements", dexElements);
Object finalElementsList = getDexElements(pathList);
System.out.println("--------"+finalElementsList);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static void setFiled(Object obj, Class> clazz, String fieldName, Object value) throws Exception {
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
private static Object getField(Object obj, Class> clazz, String fieldName) throws NoSuchFieldException, IllegalAccessException {
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(obj);
}
private static Object getPathList(Object baseDexClassLoader) throws Exception {
return getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
}
private static Object getDexElements(Object obj) throws Exception {
return getField(obj, obj.getClass(), "dexElements");
}
private static Object combineArray(Object arrLhs, Object arrRhs){
Class> clazz = arrLhs.getClass().getComponentType();
int i = Array.getLength(arrLhs);
int len = i + Array.getLength(arrRhs);
Object arrResult = Array.newInstance(clazz, len);
for(int k = 0; k < len; k++){
if(k < i){
Array.set(arrResult, k, Array.get(arrLhs, k));
}else{
Array.set(arrResult, k, Array.get(arrRhs, k - i));
}
}
return arrResult;
}
}
FMApplication
public class FMApplication extends Application {
@Override
public void onCreate() {
ClassLoader classLoader = getClassLoader();
System.out.println("--------onCreate---ClassLoader:"+getClassLoader());
super.onCreate();
}
@Override
protected void attachBaseContext(Context base) {
System.out.println("--------attachBaseContext");
MultiDex.install(base);
FixDexUtils.loadFixedDex(base);
super.attachBaseContext(base);
}
}
app#build.gradle的核心配置
android {
compileSdkVersion 27
defaultConfig {
...
multiDexEnabled true
...
}
buildTypes {
release {
//指定单独打到一个dex的类
multiDexKeepFile file('dex.keep')
def myFile = file('dex.keep')
println("isFileExists:"+myFile.exists())
println "dex keep"
minifyEnabled false
}
}
}
dependencies {
...
implementation 'com.android.support:multidex:1.0.1'
...
}
四、关于FixedClass.class打包成classes.dex
dx.bat所在目录:
sdk\build-tools\27.0.3
dx --dex --output=D:\Users\x\Desktop\dex\classes2.dex D:\Users\x\Desktop\dex
命令解释:
--output=D:\Users\x\Desktop\dex\classes2.dex 指定输出路径
D:\Users\x\Desktop\dex 最后指定去打包哪个目录下面的class字节文件(注意要包括全路径的文件夹,也可以有多个class)
五、Java反射机制可以动态修改实例中final修饰的成员变量吗?
回答是分两种情况的。
- 当final修饰的成员变量在定义的时候就初始化了值,那么java反射机制就已经不能动态修改它的值了。
- 当final修饰的成员变量在定义的时候并没有初始化值的话,那么就还能通过java反射机制来动态修改它的值。
【相关源码】HotFixJava