MultiDex与热修复实现原理

一、Android的ClassLoader体系

由上图可以看出,在叶子节点上,我们能使用到的是DexClassLoader和PathClassLoader,他们有如下使用场景:

  1. PathClassLoader是Android应用中的默认加载器,PathClassLoader只能加载/data/app中的apk,也就是已经安装到手机中的apk。这个也是PathClassLoader作为默认的类加载器的原因,因为一般程序都是安装了,在打开,这时候PathClassLoader就去加载指定的apk(解压成dex,然后在优化成odex)就可以了。
  2. DexClassLoader可以加载任何路径的apk/dex/jar,PathClassLoader只能加载已安装到系统中(即/data/app目录下)的apk文件。

从上面我们知道,DexClassLoader和PathClassLoader加载原理其实是一样的,就是使用场景不一样。

二、DexClassLoader动态加载的实现

第一步:创建DexClassLoader对象,加载对应的apk/dex/jar文件。

is = getAssets().open("app.apk");
file = new File(getFilesDir(), "plugin.apk");

fos = new FileOutputStream(file);
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) != -1) {
    fos.write(buffer, 0, len);
}
fos.flush();
String apkPath = file.getAbsolutePath();
dexClassLoader = new DexClassLoader(apkPath, getFilesDir().getAbsolutePath(), null, getClassLoader());

下面来看看DexClassLoader的构造函数

public class DexClassLoader extends BaseDexClassLoader {
    // dexPath:是加载apk/dex/jar的路径
    // optimizedDirectory:是dex的输出路径(因为加载apk/jar的时候会解压除dex文件,这个路径就是保存dex文件的)
    // libraryPath:是加载的时候需要用到的lib库,这个一般不用
    // parent:给DexClassLoader指定父加载器
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }
}

可以看到它调用的是父类的构造函数,所以直接来看BaseDexClassLoader的构造函数。

public BaseDexClassLoader(String dexPath, File optimizedDirectory,
        String libraryPath, ClassLoader parent) {
    super(parent);
    this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}

可以看到,它创建了一个DexPathList实例,下面来看看构造函数。

private final Element[] dexElements;

// definingContext对应的就是当前classLoader
// dexPath对应的就是上面传进来的apk/dex/jar的路径
// libraryPath就是上面传进来的加载的时候需要用到的lib库的目录,这个一般不用
// optimizedDirectory就是上面传进来的dex的输出路径
public DexPathList(ClassLoader definingContext, String dexPath,
        String libraryPath, File optimizedDirectory) {
    ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
    this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                       suppressedExceptions);
}

可以看到它调用的是makeDexElements方法,这个方法就是得到一个装有dex文件的数组Element[],每个Element对象里面包含一个DexFile对象成员,它对应的就是dex文件。

static class Element {
    private final File file;
    private final boolean isDirectory;
    private final File zip;
    private final DexFile dexFile;
    ......
}

具体的我们后面再说,下面先看看makeDexElements方法。

// files是一个ArrayList<File>列表,它对应的就是apk/dex/jar文件,因为我们可以指定多个文件。
// optimizedDirectory是前面传入dex的输出路径
// suppressedExceptions为一个异常列表
private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory,
                                         ArrayList<IOException> suppressedExceptions) {
    ArrayList<Element> elements = new ArrayList<Element>();
    /*
     * Open all files and load the (direct or contained) dex files
     * up front.
     */
    for (File file : files) {
        File zip = null;
        DexFile dex = null;
        String name = file.getName();

        // 如果是一个dex文件
        if (name.endsWith(DEX_SUFFIX)) {
            // Raw dex file (not inside a zip/jar).
            try {
                dex = loadDexFile(file, optimizedDirectory);
            } catch (IOException ex) {
                System.logE("Unable to load dex file: " + file, ex);
            }
        // 如果是一个apk或者jar或者zip文件
        } else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
                || name.endsWith(ZIP_SUFFIX)) {
            zip = file;

            try {
                dex = loadDexFile(file, optimizedDirectory);
            } catch (IOException suppressed) {
                /*
                 * IOException might get thrown "legitimately" by the DexFile constructor if the
                 * zip file turns out to be resource-only (that is, no classes.dex file in it).
                 * Let dex == null and hang on to the exception to add to the tea-leaves for
                 * when findClass returns null.
                 */
                suppressedExceptions.add(suppressed);
            }
        } else if (file.isDirectory()) {
            // We support directories for looking up resources.
            // This is only useful for running libcore tests.
            elements.add(new Element(file, true, null, null));
        } else {
            System.logW("Unknown file type for: " + file);
        }

        if ((zip != null) || (dex != null)) {
            elements.add(new Element(file, false, zip, dex));
        }
    }

    return elements.toArray(new Element[elements.size()]);
}

前面我们提到过Element,它里面具体包含哪些元素,现在从上面代码我们就可以知道了。

static class Element {
    private final File file;  // 它对应的就是需要加载的apk/dex/jar文件
    private final boolean isDirectory; // 第一个参数file是否为一个目录,一般为false,因为我们传入的是要加载的文件
    private final File zip;  // 如果加载的是一个apk或者jar或者zip文件,该对象对应的就是该apk或者jar或者zip文件
    private final DexFile dexFile; // 它是得到的dex文件
    ......
}

上面我们可以看到,它调用的是loadDexFile方法。

// file为需要加载的apk/dex/jar文件
// optimizedDirectorydex的输出路径
private static DexFile loadDexFile(File file, File optimizedDirectory)
        throws IOException {
    if (optimizedDirectory == null) {
        return new DexFile(file);
    } else {
        String optimizedPath = optimizedPathFor(file, optimizedDirectory);
        return DexFile.loadDex(file.getPath(), optimizedPath, 0);
    }
}

如果我们没有指定dex输出目录的话,就直接创建一个DexFile对象,如果我们指定了dex输出目录,我们就需要构造dex输出路径。

optimizedPathFor方法用来得到输出文件dex路径,就是optimizedDirectory/filename.dex,optimizedDirectory是前面指定的输出目录,filename就是加载的文件名,后缀为.dex,最终构造得到一个输出dex文件路径.

下面我们重点看看DexFile.loadDex方法。

static public DexFile loadDex(String sourcePathName, String outputPathName,
    int flags) throws IOException {
    return new DexFile(sourcePathName, outputPathName, flags);
}

下面我们就不往下看了,我们这里可以进行总结。

1、在DexClassLoader我们指定了加载的apk/dex/jar文件和dex输出路径optimizedDirectory,它最终会被解析得到DexFile文件。

2、将DexFile文件对象放在Element对象里面,它对应的就是Element对象的dexFile成员变量。

3、将这个Element对象放在一个Element[]数组中,然后将这个数组返回给DexPathList的dexElements成员变量。

4、DexPathList是BaseDexClassLoader的一个成员变量。

最终得到一个装有dex文件的数组Element[],每个Element对象里面包含一个DexFile对象成员,它对应的就是dex文件。

第二步:调用dexClassLoader的loadClass,得到加载的dex里面的指定的Class.

clazz = dexClassLoader.loadClass("com.example.apkplugin.PluginTest");

下面我们来分析一下loadClass方法。因为DexClassLoader和BaseDexClassLoader都没有实现loadClass方法,所以最终调用的是ClassLoader的loadClass方法。

public Class<?> loadClass(String className) throws ClassNotFoundException {
    return loadClass(className, false);
}

protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
    Class<?> clazz = findLoadedClass(className);

    if (clazz == null) {
        ClassNotFoundException suppressed = null;
        try {
            clazz = parent.loadClass(className, false);
        } catch (ClassNotFoundException e) {
            suppressed = e;
        }

        if (clazz == null) {
            try {
                clazz = findClass(className);
            } catch (ClassNotFoundException e) {
                e.addSuppressed(suppressed);
                throw e;
            }
        }
    }

    return clazz;
}

可以看到它调用的是findClass方法,由于DexClassLoader没有实现这个方法,所以我们看BaseDexClassLoader的findClass

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
    List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
    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;
}

pathList就是前面创建的DexPathList对象,从上面我们知道,我们加载的dex文件都存放在它的exElements成员变量上面,dexElements就是Element[]数组,所以可以看到BaseDexClassLoader的findClass方法调用的是pathList的findClass方法,我们具体来看看。

可以看到BaseDexClassLoader的findClass方法调用的是DexPathList的findClass方法。

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;
}

可以看到它就是遍历dexElements数组,从每个Element对象中拿到DexFile类型的dex文件,然后就是从dex去加载所需要的class文件,直到找到为止。

总结:一个ClassLoader可以包含多个dex文件,每个dex文件是一个Element,多个dex文件排列成一个有序的数组dexElements,当找类的时候,会按顺序遍历dex文件,然后从当前遍历的dex文件中找类,如果找类则返回,如果找不到从下一个dex文件继续查找。

三、MultiDex基本原理

当一个app的功能越来越复杂,代码量越来越多,可以遇到下面两种情况:

1. 生成的apk在2.3以前的机器无法安装,提示INSTALL_FAILED_DEXOPT

2. 方法数量过多,编译时出错,提示:Conversion to Dalvik format failed:Unable to execute dex: method ID not in [0, 0xffff]: 65536

原因:

1. Android2.3及以前版本用来执行dexopt(用于优化dex文件)的内存只分配了5M

2. 一个dex文件最多只支持65536个方法。

解决方案:

1、使用Multidex,将编译好的class文件拆分打包成两个dex,绕过dex方法数量的限制以及安装时的检查,在运行时再动态加载第二个dex文件中。

2、使用插件化,将功能模块分离,减少宿主apk的大小和代码。

插件化我们这里先不讨论,这里主要来说说Multidex的原理。

基本原理:

1、除了第一个dex文件(即正常apk包唯一包含的Dex文件),其它dex文件都以资源的方式放在安装包中。所以我们需要将其他dex文件并在Application的onCreate回调中注入到系统的ClassLoader。并且对于那些在注入之前已经引用到的类(以及它们所在的jar),必须放入第一个Dex文件中。

2、PathClassLoader作为默认的类加载器,在打开应用程序的时候PathClassLoader就去加载指定的apk(解压成dex,然后在优化成odex),也就是第一个dex文件是PathClassLoader自动加载的。所以,我们需要做的就是将其他的dex文件注入到这个PathClassLoader中去。

3、因为PathClassLoader和DexClassLoader的原理基本一致,从前面的分析来看,我们知道PathClassLoader里面的dex文件是放在一个Element数组里面,可以包含多个dex文件,每个dex文件是一个Element,所以我们只需要将其他的dex文件放到这个数组中去就可以了。

实现:

1、通过反射获取PathClassLoader中的DexPathList中的Element数组(已加载了第一个dex包,由系统加载)

2、通过反射获取DexClassLoader中的DexPathList中的Element数组(将第二个dex包加载进去)

3、将两个Element数组合并之后,再将其赋值给PathClassLoader的Element数组

谷歌提供的MultiDex支持库就是按照这个思路来实现的,我们可以直接来看看源码。

首先来看看使用:

1、修改Gradle的配置,支持multidex:

android {
    compileSdkVersion 21
    buildToolsVersion "21.1.0"
    defaultConfig {
        ...
        minSdkVersion 14
        targetSdkVersion 21
        ...
        // Enabling multidex support.
        multiDexEnabled true
    }
    ...
}
dependencies {
  compile ‘com.android.support:multidex:1.0.0‘
}

在manifest文件中,在application标签下添加MultidexApplication Class的引用,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.android.multidex.myapplication">
    <application
        ...
        android:name="android.support.multidex.MultiDexApplication">
        ...
    </application>
</manifest>

使用起来很简单,下面我们来看看源码,看是不是按照前面介绍的思路实现的。

首先我们来看看MultiDexApplication类。

public class MultiDexApplication extends Application {
    public MultiDexApplication() {
    }

    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        MultiDex.install(this);
    }
}

原来要求使用MultiDexApplication的原因就是它重写了Application,主要是为了将其他dex文件注入到系统的ClassLoader。

进入MultiDex.install(this)方法。

public static void install(Context context) {
    if(IS_VM_MULTIDEX_CAPABLE) {
        Log.i("MultiDex", "VM has multidex support, MultiDex support library is disabled.");
    // 可以看到,MultiDex不支持SDK版本小于4的系统
    } else if(VERSION.SDK_INT < 4) {
        throw new RuntimeException("Multi dex installation failed. SDK " + VERSION.SDK_INT + " is unsupported. Min SDK version is " + 4 + ".");
    } else {
        try {
            // 获取到应用信息
            ApplicationInfo e = getApplicationInfo(context);
            if(e == null) {
                return;
            }

            Set var2 = installedApk;
            synchronized(installedApk) {
                // 得到我们这个应用的apk文件路径
                // 拿到这个apk文件路径之后,后面就可以从中提取出其他的dex文件
                // 并且加载dex放到一个Element数组中
                String apkPath = e.sourceDir;
                if(installedApk.contains(apkPath)) {
                    return;
                }
                // 将这个apk文件路径放到一个set中
                installedApk.add(apkPath);

                // 得到classLoader,它就是PathClassLoader
                // 后面就可以从这个PathClassLoader中拿到DexPathList中的Element数组
                // 这个数组里面就包括由系统加载第一个dex包
                ClassLoader loader;
                try {
                    loader = context.getClassLoader();
                } catch (RuntimeException var9) {
                    Log.w("MultiDex", "Failure while trying to obtain Context class loader. Must be running in test mode. Skip patching.", var9);
                    return;
                }

                // 得到apk解压后得到的dex文件的存放目录,放到应用的data目录下
                File dexDir = new File(e.dataDir, SECONDARY_FOLDER_NAME);

                // 这个方法就是从apk中提取dex文件,放到data目录下,就不展开了
                List files = MultiDexExtractor.load(context, e, dexDir, false);
                if(checkValidZipFiles(files)) {
                    // 这个方法就是将其他的dex文件注入到系统classloader中的具体操作
                    installSecondaryDexes(loader, dexDir, files);
                } else {
                    files = MultiDexExtractor.load(context, e, dexDir, true);
                    installSecondaryDexes(loader, dexDir, files);
                }
            }
        } catch (Exception var11) {
            Log.e("MultiDex", "Multidex installation failure", var11);
            throw new RuntimeException("Multi dex installation failed (" + var11.getMessage() + ").");
        }
    }
}

下面我们重点看看installSecondaryDexes方法。

// loader对应的就是PathClassLoader
// dexDir是dex的存放目录
// files对应的就是dex文件
private static void installSecondaryDexes(ClassLoader loader, File dexDir, List<File> files) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
    if(!files.isEmpty()) {
        if(VERSION.SDK_INT >= 19) {
            MultiDex.V19.install(loader, files, dexDir);
        } else if(VERSION.SDK_INT >= 14) {
            MultiDex.V14.install(loader, files, dexDir);
        } else {
            MultiDex.V4.install(loader, files);
        }
    }

}

可以看到不同的sdk版本实现是有差别的,因为它里面是使用反射实现的,所以会有不同,我们看看MultiDex.V14.install方法。

private static void install(ClassLoader loader, List<File> additionalClassPathEntries, File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
    // 这个方法就是使用反射来得到loader的pathList字段
    Field pathListField = MultiDex.findField(loader, "pathList");
    // 得到loader的pathList字段后,我们就可以得到这个字段的值,也就是DexPathList对象
    Object dexPathList = pathListField.get(loader);
    // 这个方法就是将其他的dex文件Element数组和第一个dex的Element数组合并
    // makeDexElements方法就是用来得到其他dex的Elements数组
    MultiDex.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new ArrayList(additionalClassPathEntries), optimizedDirectory));
}

下面来看看合并的过程

// instance对应的就是pathList对象
// fieldName 对应的就是字段名,我们要得到的就是pathList对象里面的dexElements数组
// extraElements对应的就是其他dex对应的Element数组
private static void expandFieldArray(Object instance, String fieldName, Object[] extraElements) throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
    // 得到Element数组字段
    Field jlrField = findField(instance, fieldName);
    // 得到pathList对象里面的dexElements数组
    Object[] original = (Object[])((Object[])jlrField.get(instance));
    // 创建一个新的数组用来存放合并之后的结果
    Object[] combined = (Object[])((Object[])Array.newInstance(original.getClass().getComponentType(), original.length + extraElements.length));
    // 将第一个dex的Elements数组复制到创建的数组中去
    System.arraycopy(original, 0, combined, 0, original.length);
    // 将其他dex的Elements数组复制到创建的数组中去
    System.arraycopy(extraElements, 0, combined, original.length, extraElements.length);
    // 将得到的这个合并的新数组的值设置到pathList对象的Element数组字段上
    jlrField.set(instance, combined);
}

整体思路跟上面说的基本一致,理解思路,结合上面的注释基本还是比较清楚的。

四、热修复的一种实现原理

一个ClassLoader可以包含多个dex文件,每个dex文件是一个Element,多个dex文件排列成一个有序的数组dexElements,当找类的时候,会按顺序遍历dex文件,然后从当前遍历的dex文件中找类,如果找类则返回,如果找不到从下一个dex文件继续查找。

理论上,如果在不同的dex中有相同的类存在,那么会优先选择排在前面的dex文件的类,如下图:

所以,如果某些类需要修复,我们可以把有问题的类打包到一个dex(patch.dex)中去,然后把这个dex插入到Elements的最前面,如下图:

具体的方案可以参看文章:安卓App热补丁动态修复技术介绍

使用该原理的开源方案有:

Nuwa

https://github.com/jasonross/Nuwa

HotFix

https://github.com/dodola/HotFix

DroidFix

https://github.com/bunnyblue/DroidFix

参考文章:

Android中插件开发篇之—-类加载器

Android dex分包方案

Android分包原理

时间: 2024-09-30 06:00:56

MultiDex与热修复实现原理的相关文章

安卓 热修复的原理

韩梦飞沙  韩亚飞  [email protected]  yue31313  han_meng_fei_sha #热修复技术 APP提早发出去的包,如果出现客户端的问题,实在是干着急,覆水难收.因此线上修复方案迫在眉睫. ###概述 基于Xposed中的思想,通过修改c层的Method实例描述,来实现更改与之对应的java方法的行为,从而达到修复的目的. ###Xposed 诞生于XDA论坛,类似一个应用平台,不同的是其提供诸多系统级的应用.可实现许多神奇的功能.Xposed需要以越狱为前提,

热修复的原理

我们知道Java虚拟机 -- JVM 是加载类的class文件的,而Android虚拟机--Dalvik/ART VM 是加载类的dex文件, 而他们加载类的时候都需要ClassLoader,ClassLoader有一个子类BaseDexClassLoader,而BaseDexClassLoader下有一个 数组--DexPathList,是用来存放dex文件,当BaseDexClassLoader通过调用findClass方法时,实际上就是遍历数组, 找到相应的dex文件,找到,则直接将它re

Android热修复原理普及

Android热修复原理普及 这段时间比较难闲,就抽空研究一下Android热修复的原理.自从Android热修复这项技术出现之后,随之而现的是多种热修复方案的出现.前两天又看到一篇文章分析了几种热修复方案的比较. 原文地址是:[Android热修复] 技术方案的选型与验证 看完这篇文章,有点汗颜.有这么多的热修复方案,并且他们之间的实现原理也不一样,各有优缺点. 然后在尼古拉斯_赵四的博客中看到几篇关于热修复的文章,对着这几篇文章撸了一番.大概的了解了热修复一种原理,其思路和QQ空间提出的安卓

热修复(一)原理与实现详解

一.简述 热修复无疑是这2年较火的新技术,是作为安卓工程师必学的技能之一.在热修复出现之前,一个已经上线的app中如果出现了bug,即使是一个非常小的bug,不及时更新的话有可能存在风险,若要及时更新就得将app重新打包发布到应用市场后,让用户再一次下载,这样就大大降低了用户体验,当热修复出现之后,这样的问题就不再是问题了. 目前较火的热修复方案大致分为两派,分别是: 阿里系:DeXposed.andfix:从底层二进制入手(c语言). 腾讯系:tinker:从java加载机制入手. 本篇的主题

Android 热修复原理(主要谈代码修复)

Android开发中,热修复技术被越来越多的开发者使用,市面上也出现很多成熟的开源框架.但对大部分开发者来说,热修复依然是一个既熟悉又陌生的词.仅仅知道热修复的作用,会使用框架,那样意义并不大.我们还要知道热修复的原理,这样不管框架如何变化,只要基本原理不变,我们都可以快速掌握它,或者自己动手写一个适合项目的热修复框架. 热修复介绍 1.开发流程 当项目出现紧急bug时,传统的开发流程是发布新版本,引导用户覆盖安装.抛开平台审核上线的时间不说,一天重复下载安装至少两次的用户体验是很差的.而热修复

android产品研发(七)--&gt;Apk热修复

转载请标明出处:一片枫叶的专栏 去年一整年android社区中刮过了一阵热修复的风,各大厂商,逼格大牛纷纷开源了热修复框架,恩,产品过程中怎么可能没有bug呢?重新打包上线?成本太高用户体验也不好,咋办?上热修复呗. 好吧,既然要开始上热修复的功能,那么就得调研一下热修复的原理.下面我将分别讲述一下热修复的原理,各大热修复框架的比较,以及自身产品中热修复功能的实践. 热修复的原理 通过更改dex加载顺序实现热修复 最新github上开源了很多热补丁动态修复框架,大致有: HotFix      

Android 热修复 Tinker接入及源码浅析

本文已在我的公众号hongyangAndroid首发.转载请标明出处: http://blog.csdn.net/lmj623565791/article/details/54882693本文出自张鸿洋的博客 一.概述 放了一个大长假,happy,先祝大家2017年笑口常开. 假期中一行代码没写,但是想着马上要上班了,赶紧写篇博客回顾下技能,于是便有了本文. 热修复这项技术,基本上已经成为项目比较重要的模块了.主要因为项目在上线之后,都难免会有各种问题,而依靠发版去修复问题,成本太高了. 现在热

全面了解Android热修复技术

WeTest 导读 本文探讨了Android热修复技术的发展脉络,现状及其未来. 热修复技术概述 热修复技术在近年来飞速发展,尤其是在InstantRun方案推出之后,各种热修复技术竞相涌现.国内大部分成熟的主流APP都拥有自己的热修复技术,像手淘.支付宝.QQ.饿了么.美团等等. 目前能搜集到的资料,大多简单罗列每个方案的特点并进行横向比较,而其中技术发展的脉络往往被掩盖了.热修复技术从何而来,又将往何处去?在这些资料中都找不到答案. 我认为,走马观花地看一遍各家的热修复方案并不能找到答案,所

热修复 RocooFix篇(一)

吐槽之前先放一张大帅图. (md 这张图貌似有点小 不纠结这个了==) 有时候项目刚刚上线或者迭代 测试或者在线上使用测出一个bug来 真让人蛋疼 不得不重新改bug测试 打包混淆上线感觉就向findviewById一样让你无法忍受 热修复从15年开始火起来 关于热修复的理论知识基于QQ空间热修复的一片文章(后面我会附上这几天学习的了解 不想看吐槽的可以滑到最后面 没办法为了凑字 数不够150个字数不允许发表 难道这就可以阻挡我吐槽的 呸 是学习的热情了吗) 其实在学习热修复之前 我们还是有必要