基于QQ空间热修复原理实践
关于热修复技术,去年真是火的一塌糊涂,俺们没有及时赶上,好在现在赶上也不算晚,好了废话不多说,直接进入正题。
- 原理:
简单阐述一下,具体的还是看原文吧。
说白了这个方案还是在java层的改动,没有涉及到底层C/C++代码,还是比较好理解的。说到这里就不得不提到Android类加载机制。
//DexPathList.java
/**
* List of dex/resource (class path) elements.
* Should be called pathElements, but the Facebook app uses
* reflection to modify 'dexElements' (http://b/7726934).不得不说Facebook好牛叉
*/
private final Element[] dexElements;
/**
* 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
* 这个方法就是这种热修复的核心所在:
* 一个ClassLoader可以包含多个dex文件,每个dex文件是一个Element,多个
* dex文件排列成一个有序的数组dexElements,当加载类的时候,会按顺序遍历
* dex文件,然后从当前遍历的dex文件中找类,如果找类则返回,如果找不到
* 从下一个dex文件继续查找。
*/
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;
}
理论上,如果在不同的dex中有相同的类存在,那么会优先选择排在前面的dex文件的类,如下图:
因此,热补丁方案就是把有问题的类打包到一个dex(patch.dex)中去,然后把这个dex插入到Elements的最前面,如下图:
这就是补丁修复的基本原理了,当然实现过程中还存在其他问题:方法中直接引用到的类(第一层级关系,不会进行递归搜索)和clazz都在同一个dex中的话,那么这个类就会被打上CLASS_ISPREVERIFIED:
不过也给出了解决方案:
所以为了实现补丁方案,所以必须从这些方法中入手,防止类被打上CLASS_ISPREVERIFIED标志。 最终空间的方案是往所有类的构造函数里面插入了一段代码,代码如下:
if (ClassVerifier.PREVENT_VERIFY) {
System.out.println(AntilazyLoad.class);
}
其中AntilazyLoad类会被打包成单独的hack.dex,这样当安装apk的时候,classes.dex内的类都会引用一个在不相同dex中的AntilazyLoad类,这样就防止了类被打上CLASS_ISPREVERIFIED的标志了,只要没被打上这个标志的类都可以进行打补丁操作。
热修复实践
首先:要来了解2个ClassLoader的子类,
PathClassLoader 用来记载程序的dex;
DexClassLoader 用来加载指定的dex文件(限制:必须要在应用程序的目录下面)public class BaseDexClassLoader extends ClassLoader { //待会利用反射要获取这个属性 private final DexPathList pathList; }
- 引用MultiDex分包
1.
dependencies {
compile 'com.android.support:multidex:1.0.1'
}
2.
defaultConfig {
multiDexEnabled true
}
3.
buildTypes {
release {
multiDexKeepFile file('dex.keep')
def myFile = file('dex.keep')
println("isFileExists:"+myFile.exists())
println "dex keep"
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
}
}
4.
public class MyApplication extends Application{
@Override
protected void attachBaseContext(Context base) {
// TODO Auto-generated method stub
MultiDex.install(base);
}
}
然后就是核心的处理工具类:
public class FixDexUtils {
//用来存放dex文件
private static HashSet<File> loadedDex = new HashSet<File>();
//public static final String DEX_DIR = "odex";
// /data/data/packageName/odex dex存放路径
static{
loadedDex.clear();
}
//在Application中初始化
public static void loadFixedDex(Context context){
if(context == null){
return ;
}
//遍历所有的修复的dex,
File fileDir = context.getDir(MyConstants.DEX_DIR,Context.MODE_PRIVATE);
File[] listFiles = fileDir.listFiles();
for(File file:listFiles){
if(file.getName().startsWith("classes")&&file.getName().endsWith(".dex")){
loadedDex.add(file);//存入集合
}
}
//dex合并之前的dex
doDexInject(context,fileDir,loadedDex);
}
private static void setField(Object obj,Class<?> cl, String field, Object value) throws Exception {
Field localField = cl.getDeclaredField(field);
localField.setAccessible(true);
localField.set(obj,value);
}
//合并dex
private static void doDexInject(final Context appContext, File filesDir,HashSet<File> loadedDex) {
// /data/data/packageName/odex/opt_dex
String optimizeDir = filesDir.getAbsolutePath()+File.separator+"opt_dex";
File fopt = new File(optimizeDir);
if(!fopt.exists()){
fopt.mkdirs();
}
//1.加载应用程序的dex
try {
PathClassLoader pathLoader = (PathClassLoader) appContext.getClassLoader();
for (File dex : loadedDex) {
//2.加载指定的修复的dex文件。
DexClassLoader classLoader = new DexClassLoader(
dex.getAbsolutePath(),//String dexPath,
fopt.getAbsolutePath(),//String optimizedDirectory,
null,//String libraryPath,
pathLoader//ClassLoader parent
);
//3.合并
//获取补丁的pathList属性
Object dexObj = getPathList(classLoader);
//获取程序的pathList属性
Object pathObj = getPathList(pathLoader);
//获取补丁的dexElements数组
Object mDexElementsList = getDexElements(dexObj);
//获取程序的dexElements数组
Object pathDexElementsList = getDexElements(pathObj);
//合并完成,将补丁dex插入到第一个
Object dexElements = combineArray(mDexElementsList,pathDexElementsList);
//重写给PathList里面的Element[] dexElements;赋值
Object pathList = getPathList(pathLoader);
setField(pathList,pathList.getClass(),"dexElements",dexElements);
}
} catch (Exception e) {
e.printStackTrace();
}
}
//利用反射机制,获取cl属性
private static Object getField(Object obj, Class<?> cl, String field)
throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
Field localField = cl.getDeclaredField(field);
localField.setAccessible(true);
return localField.get(obj);
}
//利用反射获取BaseDexClassLoader中的pathList
private static Object getPathList(Object baseDexClassLoader) throws Exception {
return getField(baseDexClassLoader,Class.forName("dalvik.system.BaseDexClassLoader"),"pathList");
}
//利用反射获取DexPathList中的dexElements数组
private static Object getDexElements(Object obj) throws Exception {
return getField(obj,obj.getClass(),"dexElements");
}
/**
* 两个数组合并
* @param arrayLhs 补丁的
* @param arrayRhs 程序原来的
* @return
*/
private static Object combineArray(Object arrayLhs, Object arrayRhs) {
Class<?> localClass = arrayLhs.getClass().getComponentType();
int i = Array.getLength(arrayLhs);
int j = i + Array.getLength(arrayRhs);
//新建一个length=j的localClass[]数组
Object result = Array.newInstance(localClass, j);
for (int k = 0; k < j; ++k) {
//先插入补丁,在插入程序原来的
if (k < i) {
Array.set(result, k, Array.get(arrayLhs, k));
} else {
Array.set(result, k, Array.get(arrayRhs, k - i));
}
}
return result;
}
}
页面中实现:
private void fixBug() {
//目录:/data/data/packageName/odex
File fileDir = getDir(MyConstants.DEX_DIR,Context.MODE_PRIVATE);
//往该目录下面放置我们修复好的dex文件。
String name = "classes2.dex";
String filePath = fileDir.getAbsolutePath()+File.separator+name;
File file= new File(filePath);
if(file.exists()){
file.delete();
}
//搬家:把下载好的在SD卡里面的修复了的classes2.dex搬到应用目录filePath
InputStream is = null;
FileOutputStream os = null;
try {
is = new FileInputStream(Environment.getExternalStorageDirectory().getAbsolutePath()+File.separator+name);
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(this ,"dex 重写成功", Toast.LENGTH_SHORT).show();
}
//热修复
FixDexUtils.loadFixedDex(this);
} catch (Exception e) {
e.printStackTrace();
}
}
- 手动生成classes2.dex
看下怎么手动生成dex文件
1,先找到class文件,javac编译,或者找IDE编译好的,MyTestClass.class
dn_fix_ricky_as\app\build\intermediates\bin\MyTestClass.class
2,dx.bat命令生成dex文件
dx –dex –output=D:\Users\ricky\Desktop\dex\classes2.dex D:\Users\ricky\Desktop\dex
命令解释:
–output=D:\Users\ricky\Desktop\dex\classes2.dex 指定输出路径
D:\Users\ricky\Desktop\dex 最后指定去打包哪个目录下面的class字节文件(注意要包括全路径的文件夹,也可以有多个class)
参考:
1. 安卓App热补丁动态修复技术介绍
2. DexPathList源码
3. BaseDexClassLoader源码
4. Android4.4.2 DexClassLoader源码分析
5. 美团Android DEX自动拆包及动态加载简介