Android MultiDex实现原理以及Dex文件结构

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/weixiao1999/article/details/54579634
  1. 什么是64K限制?

    Dalvik 规范将可在单个 DEX 文件内可引用的方法总数限制在 65536。
    其中包括 Android 框架方法、内容库方法以及您自己代码中的方法。

    如果Android应用中的方法数达到64K的上限,打包过程中就会出现错误。

    Conversion to Dalvik format failed:
    Unable to execute dex: method ID not in [0, 0xffff]: 65536

    或者

    trouble writing output:
    Too many field references: 131000; max is 65536.
    You may try using –multi-dex option.

    为了解决64K限制的问题,所以有了以下方法

    1. 对于Android 5.0及更高的版本

      使用ART运行时,原生支持从Apk文件加载多个 dex 文件

    2. 对于Android 5.0之前的版本

      因为使用的是Dalvik运行时,限制Apk只能使用单个classes.dex文件
      所以可以使用Dalvik可执行文件(Dex)分包支持库

      com.android.support:multidex:1.0.0

      这个支持库MultiDex可能存在一些局限性

  2. MultiDex实现原理

    首先Dex分包必须使用到MultilDex支持库,否则只会生成一个主Dex(classes.dex)
    如果Android sdk 版本支持分包(> 5.0),则MultilDex支持库将被禁用

    1. MultiDex拆分步骤

      1. 自动扫描整个工程代码得到 main-dex-list(即需要写入主dex的文件清单)
      2. 根据main-dex-list对整个工程编译后的所有class文件按照需要写入主或辅dex进行拆分
      3. 使用dx工具将主、辅dex的class文件分别打包成 .dex 文件(这是Apk打包流程的第四步)

      如何自动生成main-dex-list?

      Android SDK 从 build tools 21 开始提供了 mainDexClasses 脚本
      该脚本就是在打包的时候用作与生成主、辅dex文件。

      mainDexClasses脚本执行步骤

      1. 调用 Proguard 的 shrink功能,生成一个临时的jar包 temp.jar

        shrink功能是Proguard根据keep规则保留需要的类和类成员,并丢弃不需要Keep的类和类成员。
        这些被保留的类生成了临时jar包,并且都是需要放在主dex文件中的入口类。

        入口类(即rules中的Instrumentation、application、Activity、Annotation等等)。

      2. 将生成的临时jar包和输入的文件集合(编译后的目录)作为参数,调用com.android.multidex.MainDexListBuilder 来生成 main-dex-list(主dex文件清单)

    2. MultiDex加载

      启动应用时,系统只加载了主dex(classes.dex),MultiDex需要我们在应用启动之后进行动态加载安装。

      这个动态加载的jar包是 android-support-multidex.jar

      在Android sdk 5.0之前,需要手动调用MultiDex支持库的方法

      MultiDex.install(this);

      如:

      public class HelloMultiDexApplication extends Application {
          @Override
          protected void attachBaseContext(Context base) {
              super.attachBaseContext(base);
              MultiDex.install(this);
          }
      }
      

      当然MultiDex支持库提供了MutiDexApplication这个API,可以直接继承。

      在Android sdk 5.0及之后的版本,原生支持MultiDex,并且build tools版本> 21.1

      动态加载的流程图:

      其主要是

      1. 从APK中提取所有的辅dex(classes2.dex,classes3.dex,…),
        依次写入到/data/data/pkgName/code_cache/secondary-dexes/apkName.apk.classes2.zip…等zip文件的classes.dex中

      2. 然后通过反射调用makeDexElements()方法这些zip文件封装为Elements数组

      3. 最后将Elements合并到BaseDexClassloader的DexPathList实例pathList的dexElements数组。

      MultiDex.install(this);源码解析,见这里

  3. Dex文件结构解析

    Dex文件,即Dalvik Executable format file(Dalvik可执行格式文件)

    Dex文件中包含了应用程序中运行时数据(一个工程需要的类、方法、字段等信息)

    Dex文件结构图

    通过这张图,可以知道Dex总体分为三部分:

    Header Section
    Table Section
    Data Section

    1. Header Section

      上面这张图表是Header Section所有的内容,大致分为三个部分:

      1. 文件校验相关 (magic,checksum和signature)

        这部分数据可以用来检查这个dex文件是否损坏。

      2. 文件内容相关(file_size,header_size和endian_tag)

        其中endian_tag(字节序)就是大小端

      3. map_offset字段

        这个字段表示MapList数据在Data Section中的偏移量。
        MapList即Data Section中的Map Section,包含了Dex文件中可能出现的所有类型,用于校验。

      4. size 和 offset

        这部分内容可以拿到Table Section中的各项数据。

    2. Table Section

      Table Section包含了一个dex文件中所有的数据(字符串、类、字段、方法等等)
      这部分内容又共分为6个Table
      String 、 Type 、Proto 、 Field 、 Method

      在Header Section中的size&offset部分正好对应这个6个Table。

      所以Header Section中的 size&offset 以及其所占的长度可以精确定位每一个Table在Dex文件中的位置,大致过程如下:

      BufferByte buffer = file2buffer(dexFile);
      buffer.position(table_offset);
      ByteBuffer b = buffer.slice();
      b.limit(table_size * item_length);
      

      当定位到了一个Table,那么每一个Table的具体结构又是什么样呢?

      Reading the fucking source code..
      路径:dalvik/libdex/DexFile.h

      这个头文件中定义了一系列的struct,其一一对应Dex文件结构图中的内容。

      其中对应Header Section的struct DexHeader

      对应Table Section部分struct,分别一一对应6个Table

      每个struct / Table的内容释义:

      struct DexStringId / String Table

      struct DexStringId  
      对应Header Section的string_ids_size/off。  
      其中内部有一个4字节的stringDataOff,指向Dex文件结构图的Data Section。  
      
      通过这个stringDataOff,我们就可以定位到该dex文件中所有的字符串数据,  
      包括类名,方法名,输入输出的字符串等等。  
      
      String Table:  
      一个stringDataOff表示StringData在Data Section中的偏移量,  
      StringData中包含了一个dex文件中的所有字符串资源,比如类名,方法名等等。  
      这个table也将被后面几个table反复的用到。
      

      struct DexTypeId / Type Table

      struct DexTypeId:  
      对应Header Section的type_ids_size/off。  
      其中内部有一个4字节的descriptor_idx,是string_ids数组中的一个索引,  
      通过该索引我们就可以得到一个用来描述类型的字符串,包括方法的返回类型,方法的参数类型等等。
      
      Type Table: 
      一个descriptor_idx,用来索引String Table,得到一个字符串资源用来表示一个类型,  
      比如方法的返回类型,类类型等等。
      

      struct DexFieldId / Field Table

      struct DexFieldId:  
      对应Header Section中的field_ids_size/off。  
      其中内部是
      一个2字节(ushort类型)的classIdx,用来表示该field所属的类,它是type_ids数组中的一个索引;
      一个2字节(ushort类型)的typeIdx,用来表示该field的类型,它是type_ids数组中的一个索引;
      一个4字节的nameIdx,用来表示该field的名字,它是string_ids数组中的一个索引。
      
      Field Table:
      一个classIdx用来索引Type Table,进而得到一个Type Item来索引String Table,得到一个字符串资源用来表示该field所属的类;  
      一个typeIdx用来索引Type Table,进而得到一个Type Item来索引String Table,得到一个字符串资源用来表示该field的类型;  
      一个nameIdx用来索引String Table,得到一个字符串资源用来表示该field的名称。
      

      struct DexMethodId / Method Table

      struct DexMethodId:  
      对应Header Section的method_ids_size/off。  
      其中内部是 
      一个2字节(ushort类型)的classIdx,和DexTypeId一样用来表示该field所属的类,它是type_ids数组中的一个索引;
      一个2字节(ushort类型)的protoIdx,用来表示该method的原型(返回类型,入参类型),它是proto_ids数组中的一个索引;
      一个4字节的nameIdx,用来表示该method的名字,它是string_ids数组中的一个索引。
      
      Method Table: 
      一个classIdx用来索引Type Table,进而得到一个Type Item来索引String Table,得到一个字符串资源用来表示该method所属的类;
      一个protoIdx用来索引Proto Table,进而得到一个Proto Item,其中包含了一个method的原型,返回类型和入参类型;
      一个nameIdx用来索引String Table,得到一个字符串资源用来表示该method的名称。
      

      struct DexProtoId / Proto Table

      struct DexProtoId  
      对应Header Section的proto_ids_size/off。  
      其中内部是
      一个4字节的shortyIdx,用来表示一个method的原型,它是string_ids数组中的一个索引;
      一个4字节的returnTypeIdx,用来表示一个method的返回类型,它是type_ids数组中的一个索引;
      一个4字节的parametersOff,用来表示TypeList在Data Section中的偏移量,TypeList是一个method的参数列表。
      
      Proto Table:
      一个shortyIdx用来索引String Table,得到一个字符串资源用来表示该method的原型;
      一个returnTypeIdx用来索引Type Table,进而得到一个Type Item来索引String Table,得到一个字符串资源用来表示该method的返回类型;
      一个parametersOff用来表示TypeList(Type Item的集合)在Data Section中的偏移量,TypeList中包含了一个方法的所有入参类型。
      

      struct DexClassDef / ClassDefs Table

      struct DexClassDef:  
      对应Header Section的class_defs_size/off。  
      其中是
      内部一个4字节的classIdx,用来表示一个类,它是type_ids数组的一个索引;
      一个4字节的accessFlags,用来表示该类的权限,如public,private等等,用一个int值表示(就是该4字节);
      一个4字节的superclassIdx,用来表示该类的父类(类名),它是type_ids数组中的一个索引;
      一个4字节的interfacesOff,用来表示该类所实现的接口,它用来表示TypeList在Data Section中的偏移量(和DexProtoId中的parametersOff类似);
      一个4字节的sourceFileIdx,用来表示该类的文件名(xxxx.java),它用来表示string_ids数组中的一个索引;
      一个4字节的annotationsOff,用来表示该类的所有注解,它是annotations_directory_item在Data Section中的偏移量;
      一个4字节的classDataOff,它表示该类的所有有关数据(方法,字段等等),它用来表示class_data_item在Data Section中的偏移量;  
      一个4字节的staticValuesOff,用来表示该类中的静态数据名,它用来表示DexEncodedArray在Data Section中的偏移量。
      
      需要注意的是:annotationsOff,classDataOff两个字段所指向的数据并不是简单的字符串资源,而是一个结构体 struct,  
      其中annotations_directory_item中包含一个类中所有的类注解数据,方法注解数据和字段注解数据;  
      而class_data_item中则包含一个类中的类数据,方法数据和字段数据。有兴趣的同学可以自行研究下去。
      
      ClassDefs Table:
      一个classIdx用来索引Type Table,进而得到一个Type Item来索引String Table,得到一个字符串资源用来表示该类;
      一个accessFlags用来表示该类的访问权限,如public,private等;
      一个superclassIdx用来索引Type Table,进而得到一个Type Item来索引String Table,得到一个字符串资源用来表示该类的父类;
      一个interfacesOff用来表示TypeList(Type Item的集合)在Data Section中的偏移量,TypeList中包含了一个类所实现的所有接口;
      一个sourceFileIdx用来索引String Table,得到一个字符串资源用来表示该类的文件名;
      一个annotationsOff用来表示annotations_directory_item在Data Section中的偏移量,annotations_directory_item中包含了该类所有的注解;
      一个classDataOff用来表示class_data_item在Data Section中的偏移量,class_data_item用来表示该类的所有类数据;
      一个staticValuesOff用来表示DexEncodedArray在Data Section中的偏移量,DexEncodedArray用来表示该类的所有静态数据。
      
    3. 总结

      Table Section中各个Table之间的关系

      String Table中的item可以获取到StringData在Data Section中的偏移量,从而获取String Data。  
      而其他table中的item的结构中的值分成两类:  
      第一类直接或者间接的索引String Table从而获取String Data;  
      第二类直接获取到Data Section中的偏移量,从而获取Data。
      

      总结

      首先Header Section中的size和off可以用来得到Table Section中每一个Table的位置。  
      然后Table Section中每一个table都有它自己的内部结构,通过id索引的方法,获取到Data Section中的数据。  
      这样一个dex文件中的三个Section就有机的结合在了一起。
      

      那么,Android虚拟机在查找class,method和field的过程中,充分的利用了dex文件的结构。

      例如:查找一个类,查找一个类是通过ClassLinker的resolveType方法来做的

      const char* descriptor = dex_file.StringByTypeIdx(type_idx);
      

      通过 type_ idx 去获取这个类的描述(descriptor_idx):

      const char* StringByTypeIdx(uint32_t idx) const {
        const TypeId& type_id = GetTypeId(idx);
        return StringDataByIdx(type_id.descriptor_idx_);
      }
      
      const TypeId& GetTypeId(uint32_t idx) const {
          DCHECK_LT(idx, NumTypeIds()) << GetLocation();
          return type_ids_[idx];
      }
      
      const char* StringDataByIdx(uint32_t idx) const {
          uint32_t unicode_length;
          return StringDataAndUtf16LengthByIdx(idx, &unicode_length);
      }
      
      const char* StringDataAndUtf16LengthByIdx(uint32_t idx, uint32_t* utf16_length) const {
          if (idx == kDexNoIndex) {
            *utf16_length = 0;
            return nullptr;
          }
          const StringId& string_id = GetStringId(idx);
          return GetStringDataAndUtf16Length(string_id, utf16_length);
      }
      
      const StringId& GetStringId(uint32_t idx) const {
          DCHECK_LT(idx, NumStringIds()) << GetLocation();
          return string_ids_[idx];
      }
      

      可以看到就是通过type_ idx去索引type_ ids数组得到Type_ Item,
      然后用descriptor_ idx索引string_ ids得到类型的字符串描述。

      • 利用Dex文件,我们可以做的事

    DexDiff,即2个Dex文件的差异计算。
    比如微信的热修复框架Tinker,就用到了dexdiff算法。

    微信的DexDiff算法,就是利用Dex文件的结构,对每一个细分的Section(不是三大Section)进行差量计算,得到一个diff文件。

    Dex文件加密和解密,APK安全问题。

  4. 技术文章

    1. 官方文档: 配置支持方法数超过 64K 的应用
    2. Android编译及Dex过程源码分析
    3. main-dex-list 产生过程源码分析
    4. MultiDex加载(install)过程源码分析
    5. Android 分包原理
    6. Dex文件结构及其应用
    7. Dalvik Executable format(Google官方文档 - Dex)
    8. 字节序是个什么鬼 - (学习Dex文件结构相关)

猜你喜欢

转载自blog.csdn.net/weixiao1999/article/details/54579634