zoukankan      html  css  js  c++  java
  • 热修复之原理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完成热修复技术。
     

  • 相关阅读:
    Dot Net WinForm 控件开发 (七) 为属性提下拉式属性编辑器
    WinForm 程序的界面多语言切换
    c#遍历HashTable
    Dot Net WinForm 控件开发 (三) 自定义类型的属性需要自定义类型转换器
    Dot Net WinForm 控件开发 (六) 为属性提供弹出式编辑对话框
    Dot Net WinForm 控件开发 (一) 写一个最简单的控件
    Dot Net WinForm 控件开发 (四) 设置属性的默认值
    Dot Net WinForm 控件开发 (二) 给控件来点描述信息
    Dot Net WinForm 控件开发 (八) 调试控件的设计时行为
    Dot Net WinForm 控件开发 (五) 复杂属性的子属性
  • 原文地址:https://www.cnblogs.com/awkflf11/p/12555848.html
Copyright © 2011-2022 走看看