基于QQ空间热修复原理实践

基于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文件的类,如下图:
QQ截图20161214104419.png-9.6kB
因此,热补丁方案就是把有问题的类打包到一个dex(patch.dex)中去,然后把这个dex插入到Elements的最前面,如下图:
QQ截图20161214105628.png-14.5kB
这就是补丁修复的基本原理了,当然实现过程中还存在其他问题:方法中直接引用到的类(第一层级关系,不会进行递归搜索)和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;
    }
    1. 引用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自动拆包及动态加载简介

猜你喜欢

转载自blog.csdn.net/baidu_17508977/article/details/53637057