热更新 补丁包方式(通过学习Qzone的热修复框架总结)

目录

实际操作

1、修改有BUG的代码

2、修改好代码之后,把这个java文件编译成.class文件

1)、可以使用编译工具

2)、通过命令行工具,执行java命令进行编译

3、打包,把修改好的.class文件使用dx.bat工具打包成

 1)打包jar

2)jar转换成dex

4、加载dex包到用户端(通过网络去自己的服务器下载,测试的时候我们直接放入到手机里面,通过程序去读取)

5、(程序读取)把dex包插入到dexPathList集合中,注意要插入到前面,一般插入到角标0位置

 1)形参patch是补丁的路径,先去获取ClassLoader。

2)获取到DexPathList

3)通过同样的方法获取到DexPathList中的集合dexElements,这是dex真正的藏身之地

4)操作dexElements,添加补丁包

6、bug修复

 


 

Qzone热修复技术是一个冷启动修复方式,为什么是冷启动呢,这需要根据它的实现原理来解析这个问题:

这种修复方式在于我们的代码编译成class文件之后,并打包成的dex文件的运行机制。我们都知道我们的虚拟机在执行我们的代码的时候,是从dex包中获取class文件进行读取的,而一个apk不止有一个dex文件,寻找class文件的时候,通过循环遍历所有dex文件,找到了某个class,无论后面有没有文件,循环都会中止,通过这个原理 我们可以使用插入补丁包的方式,把补丁dex包插入到dex集合最前面,这样虚拟机在寻找之前出bug的文件之前,首先会找到这个补丁包下面的 我们已经修改过的class,原先的有bug的dex不会被执行到,问题也就解决了


实际操作

理论是理论,实际的还需要实战去操作,我们先说一下具体的操作流程:

1、修改有BUG的代码

 这一步,就是更改代码,把有问题的java类里面有问题的的代码更改过来,无论几个类

2、修改好代码之后,把这个java文件编译成.class文件

这一步也很简单,

1)、可以使用编译工具

build一下,然后在 项目➡️APP➡️build➡️intermediates➡️classes文件夹debug或者release中找到对应的已经编译好的.class文件(在编译工具的工具栏有一个 build 按钮,点击弹出列表找到build apk(s),点击)

2)、通过命令行工具,执行java命令进行编译

javac classpath/class1.java  classpath/class2.java   

  (文件之间需要空一格,以便区分)

3、打包,把修改好的.class文件使用dx.bat工具打包成

dx.bat工具位于sdk目录下的build-tools下(Android\sdk\build-tools\27.0.3)

打包命令:

dx --dex [--output=<file>] [<file>.class | <file>.{zip,jar,apk} | <directory>]

例如: --dex --output=/Users/**/Desktop/testDex.dex  TextUtil.class TextUtil2.class

第一个参数是打包后的dex放置位置,后面的是需要打包的class文件 多个文件之间用空格隔开

注意 如果提示 dx命令找不到。说明没配置环境变量 要么去配置环境变量或者去de .bat的目录中去运行这个命令

我是先把.class文件打包成jar格式 然后在转换成dex

提醒大家一句:我的打包jar跟转换dex,都是在项目的debug或者release文件下操作的,(否则会引起java文件的路径问题

 1)打包jar

jar cvf patch.jar com/jjf/hotfixdemo2/TextUtil.class   com/jjf/hotfixdemo2/TextUtil.class 
com/jjf/hotfixdemo2/TextUtil.class 

 

图片。00001

2)jar转换成dex

dx  --dex --output=patch.dex patch.jar

因为所有的操作都在debug或者release下操作的,而我们是直接指定的文件名 并没有写路径,所以产生的文件都在当前目录下,还有一个原因,每个java类都有一个

package com.**.**;

来说明自己在项目中的路径,如果不在这里操作,编译成.dex文件时会提示java文件路径错误(如果只是测试两个java类,不涉及Android,跟本文章瘦的热更新无关,可以删掉这行去手动编译成class,并打包如果其中有集成JFrame,并在main方法调用了窗口,则可以直接双击这个jar运行,会出现一个你编辑的窗口)

4、加载dex包到用户端(通过网络去自己的服务器下载,测试的时候我们直接放入到手机里面,通过程序去读取)

因为是做demo,这一步我直接把打包好的dex文件放入了提前准备好的手机内存中的某一路径,在代码中直接去读取这个路径,

public class MyApplication extends Application {
 @SuppressLint("SdCardPath")
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);//hotfixTextAddress    测试热修复补丁地址
        String address = Environment.getExternalStorageDirectory() + "/测试热修复补丁地址/patch.jar";

        File file = new File(address);
        Log.i("TextUtil", "" + file.exists());
        if (file.exists()) {
            //把布丁插入到pathList中(pathList进程变量,一个存放dex编译文件的集合)
            HiAppFix.installPatch(this, address);
        }
    }
}

这个加载的步骤,最好放在启动页 初始化的时候去完成,在实际项目中的流程:

热更新流程图

 

 

5、(程序读取)把dex包插入到dexPathList集合中,注意要插入到前面,一般插入到角标0位置

这一步是核心的代码。涉及到了代码具体加载的步骤,直接上代码,然后再一步一步解读,大家也可以去看看我画的热更新思维导图,里面有对源码的解释总结

  private static final String TAG = "TextUtil";

    /**
     * 执行修复
     */
    public static void installPatch(Context context, String patch) {
        //优化目录必须是私有目录
        File cacheDir = context.getCacheDir();

        //PathClassLoader
        ClassLoader classLoader = context.getClassLoader();

        try {
            //先获取pathList属性
            Field pathList = SharedReflectUtils.getField(classLoader, "pathList");
            //通过属性反射获取属性的对象 DexPathList
            Object pathListObject = pathList.get(classLoader);
            //通过 pathListObject 对象获取 pathList类中的dexElements 属性
            //原本的dex element数组
            Field dexElementsField = SharedReflectUtils.getField(pathListObject, "dexElements");

            //通过dexElementsField 属性获取它存在的对象
            Object[] dexElementsObject = (Object[]) dexElementsField.get(pathListObject);

            List<File> files = new ArrayList<>();

            File file = new File(patch);//补丁包
            if(file.exists()){
                files.add(file);
            }
            //插桩所用到的类
//            files.add(antiazyFile);
            Method method = SharedReflectUtils.getMethod(pathListObject, "makeDexElements", List.class, File.class, List.class,ClassLoader.class);
            final List<IOException> suppressedExceptionList = new ArrayList<IOException>();
            //补丁的element数组
            Object[] patchElement = (Object[]) method.invoke(null, files, cacheDir, suppressedExceptionList, classLoader);
            //用于替换系统原本的element数组
            Object[] newElement = (Object[]) Array.newInstance(dexElementsObject.getClass().getComponentType(),
                    dexElementsObject.length + patchElement.length);

            //合并复制element
            System.arraycopy(patchElement, 0, newElement, 0, patchElement.length);
            System.arraycopy(dexElementsObject, 0, newElement, patchElement.length, dexElementsObject.length);

            //  替换
            dexElementsField.set(pathListObject,newElement);
        } catch (NoSuchFieldException | IllegalAccessException | InvocationTargetException e) {
            e.printStackTrace();
            Log.i(TAG,"installPatch="+e.toString());
        }
    }

/**
 *修复工具类(反射)
 */
public class SharedReflectUtils {


    /**
     * 反射获取某个属性
     *
     * @param instance
     * @param name
     * @return
     */
    public static Field getField(Object instance, String name) throws NoSuchFieldException {
        for (Class<?> cls = instance.getClass(); cls != null; cls = cls.getSuperclass()) {
            try {
                Field declaredField = cls.getDeclaredField(name);
                //如果反射获取的类 方法 属性不是public 需要设置权限
                declaredField.setAccessible(true);
                return declaredField;
            } catch (NoSuchFieldException e) {
                e.printStackTrace();
            }
        }
        throw new NoSuchFieldException("Field: " + name + " not found in " + instance.getClass());
    }


    /**
     * 反射获取某个属性
     *
     * @param instance
     * @param name
     * @return
     */
    public static Method getMethod(Object instance, String name,Class<?>... parameterTypes) throws NoSuchFieldException {
        for (Class<?> cls = instance.getClass(); cls != null; cls = cls.getSuperclass()) {
            try {
                Method methodMethod = cls.getDeclaredMethod(name,parameterTypes);
                //如果反射获取的类 方法 属性不是public 需要设置权限
                methodMethod.setAccessible(true);
                return methodMethod;
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            }
        }
        throw new NoSuchFieldException("Field: " + name + " not found in " + instance.getClass());
    }

}

 1)形参patch是补丁的路径,先去获取ClassLoader。

因为类加载器是完成热更新的核心类,这里我们要强调一下,类加载器ClassLoader 有两个子类BootClassLoader属于加载系统源码,我们不必理会,BaseDexClassLoader才是我们要关注的,这个类里面什么都没有,我们去使用过的是他的字类 DexClassLoader、PathClassLoader,DexClassLoader在API26之后,代码已经与PathClassLoader完全相同,但是默认使用PathClassLoader这个加载器去加载类

2)获取到DexPathList

 private final DexPathList pathList;
     /**
      * @hide
      */
     public void addDexPath(String dexPath) {
         pathList.addDexPath(dexPath, null /*optimizedDirectory*/);
     }
 

上面是BaseDexClassLoader中关于pathList 的源码,可以看出它的作用

Field.get(Object obj) 返回指定对象obj上此 Field 表示的字段的值

通过属性方法Field的get获取这个属性在ClassLoader中的值pathListObject,这才是真正的获取到了  DexPathList 的对象pathList,然后我们要通过这个对象来操作

3)通过同样的方法获取到DexPathList中的集合dexElements,这是dex真正的藏身之地

 /**
      * Constructs an instance.
116     *
117     * @param definingContext the context in which any as-yet unresolved
118     * classes should be defined
119     * @param dexPath list of dex/resource path elements, separated by
120     * {@code File.pathSeparator}
121     * @param librarySearchPath list of native library directory path elements,
122     * separated by {@code File.pathSeparator}
123     * @param optimizedDirectory directory where optimized {@code .dex} files
124     * should be found and written to, or {@code null} to use the default
125     * system directory for same
126     */
127    public DexPathList(ClassLoader definingContext, String dexPath,
128            String librarySearchPath, File optimizedDirectory) {
129
130        if (definingContext == null) {
131            throw new NullPointerException("definingContext == null");
132        }
133
134        if (dexPath == null) {
135            throw new NullPointerException("dexPath == null");
136        }
137
138        if (optimizedDirectory != null) {
139            if (!optimizedDirectory.exists())  {
140                throw new IllegalArgumentException(
141                        "optimizedDirectory doesn't exist: "
142                        + optimizedDirectory);
143            }
144
145            if (!(optimizedDirectory.canRead()
146                            && optimizedDirectory.canWrite())) {
147                throw new IllegalArgumentException(
148                        "optimizedDirectory not readable/writable: "
149                        + optimizedDirectory);
150            }
151        }
152
153        this.definingContext = definingContext;
154
155        ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
156        // save dexPath for BaseDexClassLoader
157        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
158                                           suppressedExceptions, definingContext);
159
160        // Native libraries may exist in both the system and
161        // application library paths, and we use this search order:
162        //
163        //   1. This class loader's library path for application libraries (librarySearchPath):
164        //   1.1. Native library directories
165        //   1.2. Path to libraries in apk-files
166        //   2. The VM's library path from the system property for system libraries
167        //      also known as java.library.path
168        //
169        // This order was reversed prior to Gingerbread; see http://b/2933456.
170        this.nativeLibraryDirectories = splitPaths(librarySearchPath, false);
171        this.systemNativeLibraryDirectories =
172                splitPaths(System.getProperty("java.library.path"), true);
173        List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);
174        allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);
175
176        this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories);
177
178        if (suppressedExceptions.size() > 0) {
179            this.dexElementsSuppressedExceptions =
180                suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
181        } else {
182            dexElementsSuppressedExceptions = null;
183        }
184    }

我们要把补丁加载到这个集合当中去,可以借鉴addDexPath方法里面的流程

 从上面的源码码可以看出,可以通过makeDexElements得到Element[]数组,那我们就把补丁传入反射出这个方法,并运行他,得到这个数组

4)操作dexElements,添加补丁包

 关键代码在于这几行

   Object[] patchElement = (Object[]) method.invoke(null, files, cacheDir, suppressedExceptionList, classLoader);
            //用于替换系统原本的element数组
            Object[] newElement = (Object[]) Array.newInstance(dexElementsObject.getClass().getComponentType(),
                    dexElementsObject.length + patchElement.length);

            //合并复制element
            System.arraycopy(patchElement, 0, newElement, 0, patchElement.length);
            System.arraycopy(dexElementsObject, 0, newElement, patchElement.length, dexElementsObject.length);

            //  替换
            dexElementsField.set(pathListObject,newElement);

最后的 dexElementsField.set(pathListObject,newElement); 不能忘记 这行代码的意思是 设置某对象(第一个参数)下的某个属性(第二个参数)的值,这里我们相当于直接把原来的数组覆盖了

6、bug修复

好了到了这里,我们就可以重启app

总结:这只是简单的实现热修复的其中一个方式,而且和没有具体涉及到业务部分,所以看起来很简单,实际上在热修复中还有一个不可忽视的问题,就是如果想要去适配4.4及之前的版本,还需要使用到插桩技术,这个涉及到了4.4 之前的编译优化问题,在后面的版本取消掉了,并且插桩技术在美团的热修复中也使用到了,今天说到的Qzone热修复属于冷启动修复,美团热修复技术属于热启动修复。下节我们再说有关插桩的部分-------------如果具体修复部分的代码没看懂,可以直接复制第五节5、(程序读取)把dex包插入到dexPathList集合中,注意要插入到前面,一般插入到角标0位置 这个里面的方法在Application中调用,前提是修复包你已经准备并放入了内存中。

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章