• 热修复之原理3


      

    3. 典型的热修复原理
    目前市场上热修复有两大主流方案,分别是阿里系的底层替换方案,腾讯系的类加载方案,优劣如下:

    底层替换方案:从底层C的二进制来解决问题,这样做限制颇多,但时效性最好,加载轻快,立即见效;
    类加载方案:从Java加载机制来解决问题,这样做时效性差,需要重新冷启动才能见效,但修复范围广,限制少;

    (1)底层替换方案

    底层替换方案的原理是直接在已加载类中替换掉原有方法,即在原来类基础上进行修改,因此无法实现增减原有类方法或字段,这样会破坏原有类的结构。

    不仅如此,一旦补丁类中的方法数量有增减,会直接导致此类以及整个Dex的方法熟变化,从而访问方法时无法正常索引到正确方法。若字段发生了增减,和方法数变化情况相同,而且所有字段索引都会变化。更严重的后果是,若程序运行中间某个类突然增加字段,那么对于原先已经产生的类实例,它还是原来的结构,而新方法使用到这些“过期”实例对象时,访问新增字段就会产生不可预期的结构!

    以上是底层替换方案的固有限制,既然决定从底层出发,那么必定就要承担它本身带来的问题。

    不仅如此,其中最令人诟病的地方就是它的稳定性,传统的底层替换方式如Dexposed、Andfix及其他安全界的Hook方案都是直接依赖修改虚拟机方法实体的具体字段,例如修改Dalvik方法的jni函数指针、修改类或方法的访问权限等。这里埋藏着一个隐患,由于Android是开源的,各个手机厂商都可以对代码进行改造,而Andfix里ArtMethod结构题进行了修改,就和原先开源代码结构不同,导致在修改过了的设备上,通用性的替换机制会出问题,这就是不稳定的根源。

    而hotfix技术框架针对以上的问题做了完善,它实现的是一种无视底层具体结构的替换方式,不仅解决了兼容性问题,并且忽略了底层ArtMethod 结构的差异,从而对所有Android版本都兼容,大量减少代码量。即使以后都Android版本不断修改ArtMethod的成员,只要保证ArtMethod数组仍是线性结构排列,就可适应以后Android新版本,也无须针对新的系统版本进行适配。

    (2)类加载方案

    类加载方案的原理是在app重新启动后让Classloader加载新的类。因为当app运行到一半时,所需发生变更的类已经被加载过,

    而在Android上无法对一个类进行卸载操作,若不重启,原来的类还存储于虚拟机中,新类无法被加载。因此只有在下次重启时,在业务逻辑运行之前抢先加载补丁中的新类,这样后续访问此类时,才会Resolve为新类,从而达到热修复的目的。

    说到腾讯系三大类加载方案的实现原理,QQ空间方案会侵入打包流程,可能为了hack而添加一些无用信息;

    而QFix的方案需要获取底层虚拟机的函数,不够稳定,最大的问题时无法增加public函数。微信的Tinker方案是完整的全量dex加载,将补丁做到了极致,其合成方案是从dex方法和指令维度进行全量合成,整个过程是自己研发的。

    在上一部分技术比较中也体现出了微信的Tinker方案的综合优势,但是结合上一段所说它采用的dex全量合成,可以很大地节省空间,但由于对dex内容的比较粒度过细,实现较复杂,对于性能会有所损耗。实际上dex占据APK的比例是很小的,资源文件才是占据APK的主要部分,因此Tinker用空间换取性能的转换并非理想。

    此种方案虽然尤其限制,但也有提升空间:dex比较多最佳粒度,在于类的维度,它既不像方法和指令维度那样细微,也不像bsbiff那样粗糙,因此在类的维度上是可以达到时间和空间平衡的最佳效果。

    (3)两者结合方案

    上述分析可见底层替换方案和类加载方案都有各自的优缺点,阿里的Sophix技术结合了两张方案,可灵活地根据实际情况切换。

    在补丁生成阶段,补丁工具会根据实际代码变动情况进行自动选择,针对一些在底层替换方案限制范围内的小修改,就直接采用底层替换方案,便于修复即时生效;而对于代码修复超出底层替换限制的,采用类加载方案,虽然及时性不太好,但可达到热修复的目的。

    不仅如此,Sophix在运行时阶段,还会判断所运行机型是否支持热修复,防止部分机型底层虚拟机构造不支持情况,可以执行类加载方案,从而达到最好的兼容性。

    4. 资源修复和so库修复

    Google官方Instant Run方案资源修复原理

    说起Android热修复浪潮的主因,不得不提Instant Run的实现,市面上大多数资源热修复方案基本参考了Instant Run的实现。简要而言,Instant Run中的资源热修复分为两步:

    首先构造一个新的AssetManager,并通过反射调用addAssetPath 方法,把这个完整的新资源包加入到AssetManager中,这样就获得了一个含有所有新资源的AssetManager。找到所有之前引用到原有AssetManager的地方,通过反射将引用处替换成AssetManager。

    阿里实现资源修复原理:

    阿里对于“资源修复”这一块没有直接采用Instant Run技术,而是构造一个package id为0x66的资源包,该包只包含修改了的资源项,然后直接在原有AssetManager中调用addAssetPath 方法添加此包即可。由于补丁包的package id 为0x66,不与目前已经加载的0x7f冲突,因此直接加入到已有的AssetManager中就可以使用了。补丁包中的资源只包含原有包里没有的新增资源,以及原有内容发生改变的资源,并且采用的替换方式是直接在原有的AssetManager对象上进行析构和重构,这样所有原先对AssetManager对象的引用是没有改变的,因此无需像Instant Run那样繁琐修改引用了。

    两者比较

    总之阿里的资源修复方案相较于Google官方研制的Instant Run方案,优势如下:

    不需要修改AssetManager的引用处,替换更快更完全。(对比Instant Run以及所有copycat的实现)
    不必下发完整包,补丁包中只包含有变动的资源。(对比Instant Run以及所有Amigo等方式的实现)
    不需要在运行时合成完整包,不占用运行时计算和内存资源。(对比Tinker的实现)


    so库修复

    so库的修复本质上是对native方法对修复和替换。

    阿里采用的是类似类修复反射注入方式,即把补丁so库的路径插入到nativeLibraryDirectories数组的最前面,就能够达到加载so库时的是补丁库,并非原来so库的目录,从而达到修复的目的。Sophix是在启动期间反射注入patch中的so库,对开发者依然透明,而其他方案则需要手动替换系统的System.load来实现替换目的。

    (以上阿里叙述部分及相关图片来自于《深入探索Android热修复技术原理》一书,书中可能在评价其他产品稍稍带有主观意识,但在技术原理比较部分很客观了,特别是在对比官方、其他第三方库实现功能原理,阿里在书中给出了自己的实现思路,从不同的角度剖析问题、讲解透彻,给了笔者醍醐灌顶的感觉。)

    ps. 尽量在上述内容去掉了“优雅”二字,书中特别喜欢形容自我“优雅” :)

    二. Android类加载机制源码探究
    注意:此大点只是重点研究Android类加载机制源码,涉及到的热修复的原理后篇文章讲解!

    1. JVM类加载之双亲委派模式
    (此小节只做简单介绍,详细分析请阅读笔者的另一篇文章:JVM高级特性与实践(九):类加载器 与 双亲委派模式(自定义类加载器源码探究ClassLoader))

    (1)介绍

    Java开发者对于“双亲委派模式”必然不陌生,这是JVM中的一个重要知识点,它是类加载器的重要特征,类加载器分类如下:

    启动类加载器:负责将指定类库加载到虚拟机内存中。无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,直接使用null代替即可。
    拓展类加载器:负责将指定类库加载到内存中。开发者可以直接使用标准扩展类加载器
    自定义类加载器:负责用户类路径(ClassPath)上所指定的类库。开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
    上图展示的类加载器之间的层次关系,称为类加载器的双亲委派模型(Parents Delegation Model)。

    该模型要求除了顶层的启动类加载器外,其余的类加载器都应该有自己的父类加载器,而这种父子关系一般通过组合(Composition)关系来实现,而不是通过继承(Inheritance)。

    (2)工作过程

    如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载,而是把这个请求委派给父类加载器,每一个层次的加载器都是如此,依次递归,

    因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成此加载请求(它搜索范围中没有找到所需类)时,子加载器才会尝试自己加载。

    (3)模式优点

    使用双亲委派模型来组织类加载器之间的关系,好处在于Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存在在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。

    相反,如果没有双亲委派模型而是由各个类加载器自行加载的话,用户编写了一个java.lang.Object的类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,程序将混乱。因此,如果开发者尝试编写一个与rt.jar类库中已有类重名的Java类,将会发现可以正常编译,但是永远无法被加载运行。

    2. Android类加载介绍
    Android中的ClassLoader类加载机制主要用来加载dex文件,系统提供了PathClassLoader、DexClassLoader两个API可供选择。ClassLoader种类如下:

    BootClassLoader
    BaseDexClassLoader:父类
    PathClassLoader:只能加载已安装到Android系统的APK文件;
    DexClassLoader:支持加载外部的APK、Jar或dex文件;(所有的插件化方案都是使用它来加载插件APK中的.class文件,也是动态加载的核心依据!)
    如上, 发现Android的ClassLoade和Java的大体上是一一对应的,只不过内部实现有些变化。

    思考一个问题,一个App正常运行最少需要哪些ClassLoade?

    答案揭晓:最少需要BootClassLoader和PathClassLoader。首先BootClassLoader是无可或缺的,因为它需要加载framework层的一些class文件,而PathClassLoader用来加载已安装到系统上的文件。因此一个应用运行至少需要以上两个ClassLoade,下面通过一个简单demo来证实以上猜想。

    public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    ClassLoader classLoader = getClassLoader();
    if(classLoader != null){

    Log.e("lemonnnnnn", "classLoader: " + classLoader.toString());

    while (classLoader.getParent() != null){
    classLoader = classLoader.getParent();
    Log.e("lemonnnnnn", "classLoader: " + classLoader.toString());
    }
    }
    }
    }

    上述测试代码逻辑也很简单,获取并输出加载当前应用的类加载器,然后 再判断其父加载器并输出(双亲委派模式)。查看控制台显示可知输出了PathClassLoader、BootClassLoader,因此证实了以上猜想。

    双亲委派模式 特点及作用:

    类加载的共享功能,一些framework层的类被顶层classLoader加载过后会缓存在内存中,避免重复加载。
    类加载的隔离功能,不同继承实现的classLoader加载的类肯定不会是同一个类,一些系统层级类java.lang.String 会在初始化时被加载,可避免用户写代码访问核心类库可见的成员变量。 例如java.lang.String就是在系统启动之前就已经加载好,用户可自定义一个String类提前加载与之替换,这会带来严重的安全问题。
    上述就引发出一个问题:如何的两个类才算是相同的类呢?两个类的包名、类名相同即可?并非如此!还需加上一个条件:同一个ClassLoader加载,以上三个条件成立,这两个类才能被称为相同类。

    3. Android类加载源码过程解析
    此处的ClassLoader是java.lang包下的,因此与那篇讲解Java类加载机制中讲解的逻辑大同小异,最多只是版本上的差别,最大的区别则在于继承此类并实现的一些类,也就是Android的dalvik.system包下的BaseDexClassLoader、PathClassLoader、DexClassLoader,见下图: 

    如上图,在AS编辑器中点进详情无法阅读dalvik.system包下类源码,接下来在网页中提供源码作以分析。

    4 重点总结
    以上就是对Android的ClassLoader加载机制源码部分的剖析,其实整个过程并不复杂,只是有些逻辑上的嵌套,

    涉及到ClassLoader、DexClassLoader 、PathClassLoader 、BaseDexClassLoader 、DexPathList, DexFile多个类之间方法互相调用,真正有难度的是最后native方法中的C层处理(此处不深究,有兴趣可自行研究C层)。

    (笔者强烈建议认真阅读下面时序图,也许上述一系列的源码分析让你有些云里雾里,但笔者在画完时序图后,骤然理解,颇有“拨开云雾见天日 守得云开见月明”之感!画图实在有助于理解)

    结合以上Android类加载时序图,再次回顾一下ClassLoader源码的解读研究过程

    首先类的加载是在ClassLoader类的loadClass 方法中进行,此方法会判断此类是否被自己或双亲加载过(这也是著名的“双亲委派模式”);
    若加载过则无需重复load,直接返回类实例;
    否则调用findClass方法寻找获取这个类,可是findClass方法在ClassLoader类中是一个空实现,真正实现是在BaseDexClassLoader类中;
    而BaseDexClassLoader类也未具体实现,调用的实则是DexPathList类中的findClass方法;
    DexPathList类中 findClass方法最终又调用DexFile中的defineClassNative ,DexFile的一个native方法来完成主要类加载逻辑。
    以上是类加载过程涉及到的几个类中方法互相调用最终实现“类加载”的过程,

    以下是重点方法中实现的逻辑总结:

    首先在DexPathList类的构造方法中:将所有的dex文件(File类型)转换成DexFile类型,并且将其转化为Element数组,便于findClass方法逻辑处理,

    然后在findClass 方法中遍历Element数组(Element类型中存储着DexFile类型),获取Element中的DexFile,

    调用DexFile的内部方法loadClassBinaryName,在dex文件中查找获取拼接成class字节码文件返回(loadClassBinaryName是一个 native方法)。

    而这整个过程,一系列方法、类之间调用的核心逻辑是:通过指定加载dex路径中,遍历文件找到dex文件,然后在存储了整个工程class文件数据中的dex文件中,查找搜索并拼接 class字节码文件返回。

    (1)DexClassLoader源码分析

    package dalvik.system;
    import java.io.File;

    public class DexClassLoader extends BaseDexClassLoader {

    public DexClassLoader(String dexPath, String optimizedDirectory,
    String librarySearchPath, ClassLoader parent) {
    super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
    }
    }

    以上源码可以看到DexClassLoader类中只有一个构造方法,4个参数含义分别是:

    dexPath:指定要加载dex文件的路径;
    optimizedDirectory:指定dex文件需要被写入的目录,一般是应用程序内部路径(不可以为null);
    librarySearchPath:包含native库的目录列表(可能为null);
    parent:父类加载器;
    DexClassLoader类注释: 用来加载包含dex的jar包或apk中的类,也可以执行于尚未安装到应用中的代码,因此它才是动态加载的核心!

    (2)PathClassLoader源码分析

    package dalvik.system;

    public class PathClassLoader extends BaseDexClassLoader {
    public PathClassLoader(String dexPath, ClassLoader parent) {
    super(dexPath, null, null, parent);
    }

    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
    super(dexPath, null, librarySearchPath, parent);
    }
    }
    有别于DexClassLoader,PathClassLoader只是一个简单的类加载器实现,运行于本地文件系统中的文件和目录,但不尝试从网络加载类。

    Android用此类加载器PathClassLoader来加载一些系统级别类和已存在于应用中的类。

    查看源码可知PathClassLoader有两个构造方法,其参数相较于DexClassLoader,少了一个指定dex文件需要被写入的内部目录optimizedDirectory,因此PathClassLoader只能加载已安装到应用的dex文件。

    (3)BaseDexClassLoader源码分析

    以上DexClassLoader、PathClassLoader两个类源码没有具体实现,最大的区别在于后者只能加载已安装于应用的dex文件,而详情部分还是要参数它们的父类——BaseDexClassLoader

    上图是 BaseDexClassLoader类重点源码部分,类中只有一个成员变量DexPathList,继续查看其构造方法,其中创建了DexPathList对象,传入了四个参数,分别是:

    DexClassLoader:父类加载器本身;
    dexPath: 需要加载的dex文件路径;
    librarySearchPath: 包含native库的目录列表(可能为null);
    optimizedDirectory:  dex文件需要被写入的内部目录(可能为null);
    BaseDexClassLoader 构造方法中的这些参数是其子类传过来的,只是对于在其构造方法中只做了一件事——创建DexPathList对象,有些不解。继续查看重点方法findClass(String name),重点部分笔者用红框圈出来了,通过成员变量dexList的findClass 加载获取的类返回,若类为null则报错,此处意味着真正执行加载类的重点部分并非是BaseDexClassLoader,它也只是一个中介,真相在于DexPathList类,继续延伸查看此类。

    (4)DexPathList源码分析——背后的Boss

    首先查看它的一些重要成员变量:

    DEX_SUFFIX:字符串类型,值是”.dex”;
    definingContext: ClassLoader类型,加载器,也就是BaseDexClassLoader 构造方法中创建DexPathList时传入的加载器;
    dexElements: Element[]类型,Element是一个内部类。此类作用就是指定dex/resource/native 库路径,其内部重要成员DexFile的dexFile,这是dex文件在Dalvik安卓虚拟机中的具体实现,稍后讲解;后续成员变量类型类似,只是代表不同数据,不再赘述……

    接下来查看其构造方法:

    查看其构造方法,就是用来接收参数并对成员变量赋值。由此可知参数definingContext(即ClassLoader)、dexPath一定不可为null,否则直接报异常,optimizedDirectory被写入内部的目录可能为null(即使用默认系统目录),

    然而重点在于笔者圈起来的第二个红框, 调用内部makeElements方法,  获取Element数组 赋值给 成员变量dexElements。

    深入查看,如何通过上述几个参数获得Element数组,此方法有几个重载,最终调用的方法如下:

    private static Element[] makeElements(List<File> files, File optimizedDirectory, List<IOException> suppressedExceptions, 
    boolean ignoreDexFiles, ClassLoader loader) {
    Element[] elements = new Element[files.size()];
    int elementsPos = 0;
    //循环遍历所有File并加载dex
    for (File file : files) {
    File zip = null;
    File dir = new File("");
    DexFile dex = null;
    String path = file.getPath();
    String name = file.getName();
    if (path.contains(zipSeparator)) {
    String split[] = path.split(zipSeparator, 2);
    zip = new File(split[0]);
    dir = new File(split[1]);
    } else if (file.isDirectory()) {
    //若果该file是文件夹格式,则继续递归
    elements[elementsPos++] = new Element(file, true, null, null);
    } else if (file.isFile()) {
    if (!ignoreDexFiles && name.endsWith(DEX_SUFFIX)) {
    //若该file是文件且是以dex后缀结尾,说明正是需要加载的文件,调用loadDexFile去创建一个dex(DexFile类型)
    try {
    dex = loadDexFile(file, optimizedDirectory, loader, elements);
    } catch (IOException suppressed) {
    System.logE("Unable to load dex file: " + file, suppressed);
    suppressedExceptions.add(suppressed);
    }
    } else {
    zip = file;
    // 若该file是压缩文件,调用loadDexFile去创建一个dex(DexFile类型)
    if (!ignoreDexFiles) {
    try {
    dex = loadDexFile(file, optimizedDirectory, loader, elements);
    } catch (IOException suppressed) {
    suppressedExceptions.add(suppressed);
    }
    }
    }
    } else {
    System.logW("ClassLoader referenced unknown path: " + file);
    }

    if ((zip != null) || (dex != null)) {
    elements[elementsPos++] = new Element(dir, false, zip, dex);
    }
    }
    if (elementsPos != elements.length) {
    elements = Arrays.copyOf(elements, elementsPos);
    }
    return elements;
    }

    如上DexPathList类的重点方法makeElements源码,方法中的参数经过上述源码讲解后也能够知名见意了,

    只有一个需要特别说明:files其实是对dexPath的一个转化,获得了该路径内的所有文件。笔者已在源码中加以注释,

    主要逻辑就是 循环遍历files(由dexPath转化的),文件中可能包含文件夹或压缩文件,分别判断,找到后缀为dex文件,调用loadDexFile加载生成DexFile文件(⭐️注释处),最后将生成的dex文件和路径等信息传入Element构造方法来创建对象,返回Element数组。

    此方法makeElements逻辑并不复杂,需要格外注意一下内部临时变量dex,它是DexFile类型,代表着dex文件。

    在makeElements方法中判断file是文件格式或者zip压缩格式时,都会调用此方法来创建DexFile对象,具体有何不同呢?进一步查看loadDexFile方法源码,查看其内部细节:

    /*
    *实例化DexFile类:查看loadDexFile源码,其主要作用就是创建DexFile对象返回,
    */
    private static DexFile loadDexFile(File file, File optimizedDirectory, ClassLoader loader,
    Element[] elements)
    throws IOException {
    if (optimizedDirectory == null) {
    return new DexFile(file, loader, elements);
    } else {
    String optimizedPath = optimizedPathFor(file, optimizedDirectory);
    return DexFile.loadDex(file.getPath(), optimizedPath, 0, loader, elements);
    }
    }
    首先判断写入目录optimizedDirectory是否为null,如果为null表明file确实是一个dex文件,直接创建DexFile,否则会先将其解压获取真正DexFile文件。

    DexPathList类的makeElements方法核心作用就是:

           将指定加载路径dexPath的所有文件遍历获取dex文件,并转换成DexFile类型存储到Element数组中。

    (Element数组的作用是为了后续DexPathList类的findClass方法铺垫)

    下面就来解析最万众瞩目的重点,DexPathList类的findClass方法

          作用 就是遍历之前makeElements 方法中存储好的Element数组,将Element类型转换为DexFile类型,调用DexFile的内部方法loadClassBinaryName在dex文件中查找获取拼接成class字节码文件返回。

    public Class findClass(String name, List<Throwable> suppressed) {
    for (Element element : dexElements) {
    DexFile dex = element.dexFile;
    if (dex != null) {
    //通过dex来加载类返回字节码⭐️
    Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
    if (clazz != null) {
    return clazz;
    }
    }
    }
    if (dexElementsSuppressedExceptions != null) {
    suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
    }
    return null;
    }

    小结

    Android中类加载器的PathClassLoader和DexClassloader所调用的findClass方法其实并非是自己实现的,它们内部只实现了本身的构造方法,因此调用的是其父类BaseDexClassLoader中实现的方法,可是最后追述到的真正实现者是DexPathList类!由它来具体实现了findClass方法,

    而此方法内部具体是调用了 DexFile的核心内部方法loadClassBinaryName实现重要功能:在dex文件中查找获取拼接成class字节码文件返回。

    DexPathList源码

    (5)DexFile源码分析——Boss的心腹

    下面具体查看DexFile的核心内部方法 loadClassBinaryName实现:

    public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
    return defineClass(name, loader, mCookie, this, suppressed);
    }

    private static Class defineClass(String name, ClassLoader loader, Object cookie,
    DexFile dexFile, List<Throwable> suppressed) {
    Class result = null;
    try {
    //⭐️
    result = defineClassNative(name, loader, cookie, dexFile);
    } catch (NoClassDefFoundError e) {
    if (suppressed != null) {
    suppressed.add(e);
    }
    } catch (ClassNotFoundException e) {
    if (suppressed != null) {
    suppressed.add(e);
    }
    }
    return result;
    }

    以上源码可以发现代码的一个设计准则:loadClassBinaryName方法类型是public的,可供外部调用,但其内部只有调用defineClass方法这一行代码,

    而defineClass方法类型是private的,仅供内部调用,因此它只是借助loadClassBinaryName 方法做了一层封装,保持了私有性!

    继续查看defineClass方法源码,逻辑也十分简单,除了异常捕获之外,核心代码只有一行:defineClassNative(name, loader, cookie, dexFile),通过它完成类的查找,查看详情:

    最后的结果很明显,这是一个native方法,我们无法再向下剖析。

    若是对dex文件格式颇有了解或者阅读过笔者写过的分析dex格式文章,可知一个dex文件中存储了整个工程中所有的class文件,其文件数据存储在dex文件中的“数据区”。因此, 也可以推理出defineClassNativenative方法是通过C/C++ 在dex文件中查找获取拼接成class字节码文件返回。

    DexFile源码地址

    5. 动态加载难点:
    以上就是ClassLoader中的一个loadClass和findClass的过程,了解之后,接下来介绍Android动态加载的难点:

    在了解以上源码解析后,发现Android的动态加载,不过是使用DexClassLoader指定需要加载的APK路径,思路很简单呀?

    其实在实际使用中并不尽然,由于Android系统的特点和第三方原因带来了以下限制:

    许多组件类需要注册才能使用。例如Android系统中的Activity、Service等需要在Manifest中注册才可以工作,这意味着即使在工程中加载了一个新的组件,若没有注册也将无法工作。
    资源的动态加载复杂度高。 Android开发的一大特点就是使用资源,将资源通过ID注册好再来使用,因此资源的注册这一步不可或缺,之后才可以通过ID向Resource实例中获取对应资源。这意味着动态加载时运行的新类中, 若涉及到资源的加载,由于新类资源没有注册的原因,程序会抛出异常。
    Android各版本的差异可能存在加载资源、注册的方式不同的隐患问题,对适配造成影响。

    以上总结的问题存在一个共性: 即Android程序运行需要一个上下文环境,它可以为Android中的组件提供使用的功能,例如主题、资源、组件查询等等。

    因此现在面临的问题,就是如何为动态加载到程序中的类或者 资源提供这样一个Android上下文环境呢?这也是许多第三发动态加载库Tinker、AndFix核心解决问题关键,学习它们的实现原理着实必要,后续涉及。

    建议阅读完此篇文章后,阅读笔者上一篇特地为了热修复系列去学习的Android dex格式解析:Android Dex VS Class:实例图解剖析两种格式文件结构、优劣,这样会对Android类加载机制的了解更加深入。

    此系列对笔者而言又是一个“大头”,刚开始实在理解无能,研究原理、探索源码是一个痛苦而又艰难的过程,通过相关书籍、视频、博客、官方源码等渠道慢慢悟道。想要把一个知识点分析透彻实属不易,个中牵涉的部分太多,只能尽自己目前的知识存储量去理解并研究,共勉。

    (例如此篇的Android类加载机制,首先毫无疑问你需要 了解JVM中的类加载机制 及双亲委托模式,之后你会发现Android的ClassLoader与Java中的不同之处,因着前者加载的是dex文件,并非是class字节码文件,再去学习dex相关概念知识,再从源码角度慢慢深入探索总结其原理 )

    下一篇博文的内容是解析如何使用类加载实现热修复,以及用类加载方案自行实现demo完成热修复技术。
     

  • 相关阅读:
    Redis学习第二天
    Redis学习
    jQuery基础
    Hashtable 和 HashMap 的区别
    JSP页面乱码问题
    Day28 java8:Stream API
    转 链表中节点每k个一组反转
    day 27 lambda表达式(针对接口) & 函数式接口
    day20异常2
    day20 异常1
  • 原文地址:https://www.cnblogs.com/awkflf11/p/12555848.html
Copyright © 2020-2023  润新知