比较 | Tinker | QZone | AndFix | Robust |
---|---|---|---|---|
类替换 | ✅ | ✅ | ❌ | ❌ |
So替换 | ✅ | ❌ | ❌ | ❌ |
资源替换 | ✅ | ✅ | ❌ | ❌ |
全平台替换 | ✅ | ✅ | ✅ | ✅ |
即时生效 | ❌ | ❌ | ✅ | ✅ |
性能损耗 | ⬇️ | ⬆️ | ⬇️ | ⬇️ |
Class 加载过程
PathClassLoader只是个包装类,不会加载文件。
在Android中, Dalvik能执行的文件之一是dex文件。我们编写的.java文件会被编译为class文件然后打包成dex文件。Dalvik就是将会将所有的dex文件以Element的方式存放在DexPathList中。
当加载一个class文件时,Dalvik会从DexPathList中去遍历每一个dex寻找其中的class文件,找到对应的立即返回,如果所有的dex文件中均不含这个class文件,就会返回ClassNotFoundException。
源码
/*
* Copyright (C) 2007 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dalvik.system;
/**
* 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 {
/**
* Creates a {@code PathClassLoader} that operates on a given list of files
* and directories. This method is equivalent to calling
* {@link #PathClassLoader(String, String, ClassLoader)} with a
* {@code null} value for the second argument (see description there).
*
* @param dexPath the list of jar/apk files containing classes and
* resources, delimited by {@code File.pathSeparator}, which
* defaults to {@code ":"} on Android
* @param parent the parent class loader
*/
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
/**
* Creates a {@code PathClassLoader} that operates on two given
* lists of files and directories. The entries of the first list
* should be one of the following:
*
* <ul>
* <li>JAR/ZIP/APK files, possibly containing a "classes.dex" file as
* well as arbitrary resources.
* <li>Raw ".dex" files (not inside a zip file).
* </ul>
*
* The entries of the second list should be directories containing
* native library files.
*
* @param dexPath the list of jar/apk files containing classes and
* resources, delimited by {@code File.pathSeparator}, which
* defaults to {@code ":"} on Android
* @param librarySearchPath the list of directories containing native
* libraries, delimited by {@code File.pathSeparator}; may be
* {@code null}
* @param parent the parent class loader
*/
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}
BaseDexClassLoader.java
public class BaseDexClassLoader extends ClassLoader {
//内部包装所有的Dex文件
private final DexPathList pathList;
/**
* Constructs an instance.
*
* @param dexPath the list of jar/apk files containing classes and
* resources, delimited by {@code File.pathSeparator}, which
* defaults to {@code ":"} on Android
* @param optimizedDirectory directory where optimized dex files
* should be written; may be {@code null}
* @param librarySearchPath the list of directories containing native
* libraries, delimited by {@code File.pathSeparator}; may be
* {@code null}
* @param parent the parent class loader
*/
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);
}
//加载class文件
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
//调用pathList的findClass
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;
}
.......省略其他代码...........
}
final class DexPathList {
private static final String DEX_SUFFIX = ".dex";
private static final String zipSeparator = "!/";
/** class definition context */
private final ClassLoader definingContext;
/** 存放所有的dex文件
* 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;
/**
Element用来包装dex文件
* Finds the named class in one of the dex files pointed at by
* this instance. This will find the one in the earliest listed
* path element. If the class is found but has not yet been
* defined, then this method will define it in the defining
* context that this instance was constructed with.
*
* @param name of class to find
* @param suppressed exceptions encountered whilst finding the class
* @return the named class or {@code null} if the class is not
* found in any of the dex files
*/
public Class findClass(String name, List<Throwable> 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;
}
.....省略其他代码.....
}
修复方案
到这里我们已经知道了,加载class的流程是通过BaseDexClassLoader
调用findClass(String name)
,进而调用DexPathList
的findClass(String name)
,而这个方法又是通过遍历包装了dex文件对应的包装类Element的dexElements
来完成的。
那么我们只需要将修复好的class文件编译打包成dex文件,然后通过反射变更系统的dexElements
,将这个fixBug.dex
解析提取出dexElements
然后跟系统的dexElements
进行合并,并且放到第一位置。那么在加载class文件时,findClass在第一位置找到了class文件后,不再理会后面的class文件。这样即完成了修复。
出问题了
我们假定出问题的文件如下:
package com.example.myapplication;
import android.widget.Toast;
public class TestBug {
public void test(MainActivity mainActivity) {
Toast.makeText(mainActivity, "This is bug file", Toast.LENGTH_LONG).show();
}
}
而正确的文件如下
package com.example.myapplication;
import android.widget.Toast;
public class TestBug {
public void test(MainActivity mainActivity) {
Toast.makeText(mainActivity, "This is file", Toast.LENGTH_LONG).show();
}
}
生成dex文件
进入到修复的class文件目录下,不要进入到包名目录下(com里面是包名)
dx --dex --output [你要输出的文件] [要编译的文件或者目录]
dex文件copy到 data/user/package name/app_odex
修复后的文件打包的dex,反编译后确认无误。
将fixbug.dex文件复制到手机的SD卡目录下。
热修复核心代码以及注解
package com.example.myapplication;
import android.content.Context;
import android.os.Environment;
import android.widget.Toast;
import dalvik.system.DexClassLoader;
import dalvik.system.PathClassLoader;
import java.io.*;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
public class DexUtils {
/**
* 将dex包复制到/data/user/包名之下
* @param context
*/
public static void copyFixDex2Data(Context context){
File odex = context.getDir("odex", Context.MODE_PRIVATE);
String filePath = new File(odex, "fixbug.dex").getAbsolutePath();
File fixBugDexFile = new File(filePath);
if (fixBugDexFile.exists()) {
fixBugDexFile.delete();
}
InputStream is = null;
FileOutputStream os = null;
try {
is = new FileInputStream(new File(Environment.getExternalStorageDirectory(), "fixbug.dex"));
os = new FileOutputStream(filePath);
int len = 0;
byte[] buffer = new byte[1024];
while ((len = is.read(buffer)) != -1) {
os.write(buffer, 0, len);
}
File f = new File(filePath);
if (f.exists()) {
Toast.makeText(context, "dex overwrite", Toast.LENGTH_SHORT).show();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (os != null) {
os.close();
}
if (is != null) {
is.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void loadDex(Context context) {
File fileDir = context.getDir("odex", Context.MODE_PRIVATE);
//优化后的缓存路径
String optimizedDirectory = fileDir.getAbsolutePath() + File.separator + "opt_dex";
File[] listFiles = fileDir.listFiles();
for (File file : listFiles) {
if (file.getName().startsWith("classes") || file.getName().endsWith(".dex")) {
String dexPath = file.getAbsolutePath();
//dex中的lib路径
String librarySearchPath = null;
//APP中真正运行的加载dex的loader,我们的目的是吧dexClassLoader中的Element"合并"到pathClassLoader的Element[]中
//BaseDexClassLoader->pathList(DexPathList)->dexElements(Element[])
try {
//-------step1-----------反射到系统的Element[]---------------------
//拿到系统的ClassLoader
Class baseDexClassLoader = Class.forName("dalvik.system.BaseDexClassLoader");
Field pathListField = baseDexClassLoader.getDeclaredField("pathList");
pathListField.setAccessible(true);
//APP中真正运行的加载dex的loader
PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
Object pathListObj = pathListField.get(pathClassLoader);
Class<?> pathListObjClass = pathListObj.getClass();
Field dexElementsField = pathListObjClass.getDeclaredField("dexElements");
dexElementsField.setAccessible(true);
//private Element[] dexElements;
Object systemElements = dexElementsField.get(pathListObj);
//--------step2----------反射到自己的Element[]---------------------
Class myBaseDexClassLoader = Class.forName("dalvik.system.BaseDexClassLoader");
Field myPathListField = myBaseDexClassLoader.getDeclaredField("pathList");
myPathListField.setAccessible(true);
//我们的dex包中的
DexClassLoader
dexClassLoader = new DexClassLoader(dexPath, optimizedDirectory, librarySearchPath, context.getClassLoader());
Object myPathListObj = myPathListField.get(dexClassLoader);
Class<?> myPathListObjClass = myPathListObj.getClass();
Field myDexElementsField = myPathListObjClass.getDeclaredField("dexElements");
myDexElementsField.setAccessible(true);
//private Element[] dexElements;
Object myElements = myDexElementsField.get(myPathListObj);
//----------step3-------------自己的Element合并到System的Element中-------------------------------
//Element的类型反射拿到
Class elementType = systemElements.getClass().getComponentType();
int sysDexLength = Array.getLength(systemElements);
int myDexLength = Array.getLength(myElements);
int newElementsAryLength = myDexLength + sysDexLength;
Object newElements = Array.newInstance(elementType, newElementsAryLength);
//-----------核心----将fixDex放到最前面----------
for (int i = 0; i < newElementsAryLength; i++) {
if (i < myDexLength) {
Array.set(newElements, i, Array.get(myElements, i));
} else {
Array.set(newElements, i, Array.get(systemElements, i - myDexLength));
}
}
Field elements = pathListObj.getClass().getDeclaredField("dexElements");
elements.setAccessible(true);
elements.set(pathListObj, newElements);//合并完毕!!!!!!!
System.out.println("fix finish");
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (Exception e){
e.printStackTrace();
}
}
}
}
}
BaseApplication
package com.example.myapplication;
import android.app.Application;
import android.content.Context;
import android.support.multidex.MultiDex;
public class BaseApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
}
@Override
protected void attachBaseContext(Context base) {
MultiDex.install(this);
DexUtils.copyFixDex2Data(this)
DexUtils.loadDex(base);
super.attachBaseContext(base);
}
}