做Android的同学对dex文件一定不会陌生,它其中包含了我们一个工程所有的类,方法,字段等等的信息。通过对dex文件格式的学习,我们可以收获到的东西是非常多的。

可能很多同学一想到文件格式这类的内容都会觉得头大,认为这是一个很高深的内容。我一开始接触这方面东西的时候也有这样的抵触情绪,不过随着学习的深入,发现其实整个架构是很清晰的,所以写了这篇文章,一方面帮助自己总结,另一方面也是给有需要的同学,因为这方面的内容是dalvik和ART共有的特性,了解这些也有助于大家更好的理解Android虚拟机的运行。



前言

最开始接触dex文件这方面的内容是因为hotpatch,当时出现了现有的hotpatch框架在ART虚拟机上crash的问题,于是我就去翻了翻虚拟机的源码,其中发现它加载类和方法的方式比较特殊,有很多的类似method_id,class_id的东西。当时我天真的以为这是ART的特性,直到前几天在看dalvik的源码的时候我才恍然大悟,原来这是整个Android环境共有的特性,或者说是dalvik上的特性,ART只是沿用了而已。这也是可以理解的,毕竟ART上的oat文件只是对dex文件进行了一层包装,多了native code,具体的类,方法查找流程肯定是不会变的。所以我就下定决心要好好研究研究dex文件,以加深对整个Android的理解。

本来是想写一篇纯dex文件结构分析的,不过在写之前看到区长已经写了这样一篇文章了,所以决定对于这方面的分析从简,更多的讲讲该如何利用dex文件。

dex文件结构解析

在开始分析之前,先上一张dex文件结构图:

因为没有找到这张图的具体出处,就不写来源了,有知道的同学可以告诉我,我再加上。

通过上面这张图,我们可以很清晰的理解整个dex文件中都含有上面。总体来说可以分为三部分:

1.Header Section

2.Table Section

3.Data Section

下面我一一来讲解,当然有兴趣的同学也可以直接去这里看Google官方的分析。

首先是Header Section。这其中定义了Table Section中各个Table的大小和偏移量,用以找到对应的某一个Section。此外,Header中还有一些用于校验的数据,类似checksum和魔术:

上面这张表格里就是Header Section中的所有内容,通过字段名加描述应该很好理解。其中第二列格式表示的是每一个字段的数据类型和大小,其中uint占4个字节,ubyte占一个字节。

从这张表格中我们可以知道,Header Section中存储了大致三部分的东西:

1.文件校验相关(magic,checksum和signature)。这部分的数据让解析调用该文件的使用者明确这个文件是否已经损坏。

2.文件内容相关(file_size,header_size和endian_tag)。这没什么好说的。对于大小端,如果有不清楚的同学可以参考这篇文章。

3.map_off字段。用来表示MapList在Data Section中的偏移量,通过这个偏移量,我们可以得到MapList数据(也就是第一张图Data Section中的Map Section)。这里我不详细展开,大家只需要这知道这个MapList中包含了dex文件中可能出现的所有类型,作用是用于校验。

4.size&offset(剩余部分)。通过这些部分,我们可以拿到Table Section中的各项数据。

第二部分是Table Section。这部分的内容和Header Section是紧密相连的。可以看到这一部分一共有6个table(string,type,proto,field和method),而Header Section中也正好对应了这6个table的size和offset。所以我们可以通过Header Section里的size,offset以及它们各自所占的大小来精确的定位到每一个table的在dex文件中的位置,大致过程如下:

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

到这里我们知道,我们可以通过Header Section中每一个table的size和offset去Table Section中寻址到具体的table。而这个table中就包含了一个dex中的所有数据(字符串,类,字段,方法等等)。那当我们定位到了一个具体的table,我们如何找到这个table中一个具体的item呢?这其中的秘密就要从每一个table内部的结构说起了。

那我们该怎么去看一个table的内部结构呢?这里就要用到一句至理名言了:Read the fucking source code!我们要查找的是关于dex的结构,所以当然要看的是类似Dex,DexFile这样的类。在AndroidXRef上搜索一下,就可以找到DexFile这样类了,具体的路径是dalvik/libdex/DexFile.h。通过路径我们可以知道,这个类已经是和dalvik虚拟机紧密相连的了,当然我前面也提到,ART上沿用了这样的机制,毕竟都是处理dex文件嘛。需要注意的是,在ART上还存在一个dex_file,具体路径是art/runtime/dex_file.h,我比较了一下两者的内容,大致都是一样的,只是后者有了一些ART平台的特性。

仔细观察这个头文件,你可以发现它内部定义的struct其实就是我们前面讲的那些,举个例子:

struct DexHeader里所定义的所有字段,就是我们上面提到的Header Section里的内容,并且每一个字段的大小也是一一对应的。

接着让我们通过dex_file.h来观察一下Table Section里每一个table的内容:

让我们一个个来讲解。

struct DexStringId对应Table Section的string_ids_size/off。其中内部有一个4字节的stringDataOff,指向文中第一张图的Data Section。通过这个stringDataOff,我们就可以定位到该dex文件中所有的字符串数据,包括类名,方法名,输入输出的字符串等等。
struct DexTypeId对应Table Section的type_ids_size/off。其中内部有一个4字节的descriptor_idx,是string_ids数组中的一个索引,通过该索引我们就可以得到一个用来描述类型的字符串,包括方法的返回类型,方法的参数类型等等。
struct DexFieldId对应Table Section中的field_ids_size/off。其中内部是
一个2字节(ushort类型)的classIdx,用来表示该field所属的类,它是type_ids数组中的一个索引;
一个2字节(ushort类型)的typeIdx,用来表示该field的类型,它是type_ids数组中的一个索引;
一个4字节的nameIdx,用来表示该field的名字,它是string_ids数组中的一个索引。


struct DexMethodId对应Table Section的method_ids_size/off。其中内部是 
一个2字节(ushort类型)的classIdx,和DexTypeId一样用来表示该field所属的类,它是type_ids数组中的一个索引;
一个2字节(ushort类型)的protoIdx,用来表示该method的原型(返回类型,入参类型),它是proto_ids数组中的一个索引;
一个4字节的nameIdx,用来表示该method的名字,它是string_ids数组中的一个索引。
struct DexProtoId对应Table Section的proto_ids_size/off。其中内部是
一个4字节的shortyIdx,用来表示一个method的原型,它是string_ids数组中的一个索引;
一个4字节的returnTypeIdx,用来表示一个method的返回类型,它是type_ids数组中的一个索引;
一个4字节的parametersOff,用来表示TypeList在Data Section中的偏移量,TypeList是一个method的参数列表。
struct DexClassDef对应Table 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两个字段所指向的数据并不是简单的字符串资源,而是一个结构体,其中annotations_directory_item中包含一个类中所有的类注解数据,方法注解数据和字段注解数据;而class_data_item中则包含一个类中的类数据,方法数据和字段数据。有兴趣的同学可以自行研究下去。

至此,Table Section中的每一个table我们都了解了它们的结构:

String Table: 
一个stringDataOff表示StringData在Data Section中的偏移量,StringData中包含了一个dex文件中的所有字符串资源,比如类名,方法名等等。这个table也将被后面几个table反复的用到。
Type Table: 
一个descriptor_idx,用来索引String Table,得到一个字符串资源用来表示一个类型,比如方法的返回类型,类类型等等。
Field Table: 
一个classIdx用来索引Type Table,进而得到一个Type Item来索引String Table,得到一个字符串资源用来表示该field所属的类;
一个typeIdx用来索引Type Table,进而得到一个Type Item来索引String Table,得到一个字符串资源用来表示该field的类型;
一个nameIdx用来索引String Table,得到一个字符串资源用来表示该field的名称。
Method Table: 
一个classIdx用来索引Type Table,进而得到一个Type Item来索引String Table,得到一个字符串资源用来表示该method所属的类;
一个protoIdx用来索引Proto Table,进而得到一个Proto Item,其中包含了一个method的原型,返回类型和入参类型;
一个nameIdx用来索引String Table,得到一个字符串资源用来表示该method的名称。
Proto Table:
一个shortyIdx用来索引String Table,得到一个字符串资源用来表示该method的原型;
一个returnTypeIdx用来索引Type Table,进而得到一个Type Item来索引String Table,得到一个字符串资源用来表示该method的返回类型;
一个parametersOff用来表示TypeList(Type Item的集合)在Data Section中的偏移量,TypeList中包含了一个方法的所有入参类型。
Proto Table:
一个shortyIdx用来索引String Table,得到一个字符串资源用来表示该method的原型;
一个returnTypeIdx用来索引Type Table,进而得到一个Type Item来索引String Table,得到一个字符串资源用来表示该method的返回类型;
一个parametersOff用来表示TypeList在Data Section中的偏移量,TypeList中包含了一个方法的所有入参类型。
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用来表示该类的所有静态数据。

一句话总结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就有机的结合在了一起。

虚拟机是如何利用dex文件结构的

了解完了dex的文件结构,让我们看看Android虚拟机是如何利用这样的结构的。

在虚拟机中,查找一个类是通过ClassLinker的resolveType来做的:

mirror::Class* ClassLinker::ResolveType(const DexFile& dex_file, uint16_t type_idx,
                                        Handle<mirror::DexCache> dex_cache,
                                        Handle<mirror::ClassLoader> class_loader) {
  DCHECK(dex_cache.Get() != nullptr);
  mirror::Class* resolved = dex_cache->GetResolvedType(type_idx);
  if (resolved == nullptr) {
    Thread* self = Thread::Current();
    const char* descriptor = dex_file.StringByTypeIdx(type_idx);
    resolved = FindClass(self, descriptor, class_loader);
    if (resolved != nullptr) {
      // TODO: we used to throw here if resolved's class loader was not the
      //       boot class loader. This was to permit different classes with the
      //       same name to be loaded simultaneously by different loaders
      dex_cache->SetResolvedType(type_idx, resolved);
    } else {
      CHECK(self->IsExceptionPending())
          << "Expected pending exception for failed resolution of: " << descriptor;
      // Convert a ClassNotFoundException to a NoClassDefFoundError.
      StackHandleScope<1> hs(self);
      Handle<mirror::Throwable> cause(hs.NewHandle(self->GetException()));
      if (cause->InstanceOf(GetClassRoot(kJavaLangClassNotFoundException))) {
        DCHECK(resolved == nullptr);  // No Handle needed to preserve resolved.
        self->ClearException();
        ThrowNoClassDefFoundError("Failed resolution of: %s", descriptor);
        self->GetException()->SetCause(cause.Get());
      }
    }
  }
  DCHECK((resolved == nullptr) || resolved->IsResolved() || resolved->IsErroneous())
      << PrettyDescriptor(resolved) << " " << resolved->GetStatus();
  return resolved;
}

其中下面这句话是最关键的:

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

通过type_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然后去索引string_ids这个数组。和我上面提到的过程是一样的。

下面在让我们看看查找方法的过程,具体的方法在ClassLinker的resolveMethod。

ArtMethod* ClassLinker::ResolveMethod(const DexFile& dex_file, uint32_t method_idx,
                                      Handle<mirror::DexCache> dex_cache,
                                      Handle<mirror::ClassLoader> class_loader,
                                      ArtMethod* referrer, InvokeType type) {
  DCHECK(dex_cache.Get() != nullptr);

  ........

  const DexFile::MethodId& method_id = dex_file.GetMethodId(method_idx);
  mirror::Class* klass = ResolveType(dex_file, method_id.class_idx_, dex_cache, class_loader);

  ........

  switch (type) {
    case kDirect:  // Fall-through.
    case kStatic:
      resolved = klass->FindDirectMethod(dex_cache.Get(), method_idx, image_pointer_size_);
      DCHECK(resolved == nullptr || resolved->GetDeclaringClassUnchecked() != nullptr);
      break;
    case kInterface:
      resolved = klass->FindInterfaceMethod(dex_cache.Get(), method_idx, image_pointer_size_);
      DCHECK(resolved == nullptr || resolved->GetDeclaringClass()->IsInterface());
      break;
    case kSuper:  // Fall-through.
    case kVirtual:
      resolved = klass->FindVirtualMethod(dex_cache.Get(), method_idx, image_pointer_size_);
      break;
    default:
      LOG(FATAL) << "Unreachable - invocation type: " << type;
      UNREACHABLE();
  }
  if (resolved == nullptr) {
    // Search by name, which works across dex files.
    const char* name = dex_file.StringDataByIdx(method_id.name_idx_);
    const Signature signature = dex_file.GetMethodSignature(method_id);
    switch (type) {
      case kDirect:  // Fall-through.
      case kStatic:
        resolved = klass->FindDirectMethod(name, signature, image_pointer_size_);
        DCHECK(resolved == nullptr || resolved->GetDeclaringClassUnchecked() != nullptr);
        break;
      case kInterface:
        resolved = klass->FindInterfaceMethod(name, signature, image_pointer_size_);
        DCHECK(resolved == nullptr || resolved->GetDeclaringClass()->IsInterface());
        break;
      case kSuper:  // Fall-through.
      case kVirtual:
        resolved = klass->FindVirtualMethod(name, signature, image_pointer_size_);
        break;
    }
  }

  .........
}

首先通过dex_file的GetMethodId去获取对应的method id:

const MethodId& GetMethodId(uint32_t idx) const {
  DCHECK_LT(idx, NumMethodIds()) << GetLocation();
  return method_ids_[idx];
}

和上面说的一样,索引method_ids数组。

下面的一个调用很有意思:

mirror::Class* klass = ResolveType(dex_file, method_id.class_idx_, dex_cache, class_loader);

可以看到,通过method_id的class_idx去找对应method所属的类。这一点我在上面也有提到,field_id和method_id的结构中都有一个class_idx用来索引它所属的那个类。

接着,就是通过class的方法找寻method了,需要注意的是,如果通过method_idx的方式找寻不到对应的method,会先获取该method的name和signature,再通过这两个属性去查找method:

const char* name = dex_file.StringDataByIdx(method_id.name_idx_);
const Signature signature = dex_file.GetMethodSignature(method_id);
switch (type) {
  case kDirect:  // Fall-through.
  case kStatic:
    resolved = klass->FindDirectMethod(name, signature, image_pointer_size_);
    DCHECK(resolved == nullptr || resolved->GetDeclaringClassUnchecked() != nullptr);
    break;
  case kInterface:
    resolved = klass->FindInterfaceMethod(name, signature, image_pointer_size_);
    DCHECK(resolved == nullptr || resolved->GetDeclaringClass()->IsInterface());
    break;
  case kSuper:  // Fall-through.
  case kVirtual:
    resolved = klass->FindVirtualMethod(name, signature, image_pointer_size_);
    break;
}

而这个name和signature也是通过上面提到的方式去获取的。name通过name_idx去索引String Table,而signature则是通过parameters_off去获取TypeList。

从resolveType和resolveMethod中我们就可以知道,Android虚拟机在查找class,method和field的过程中,充分的利用了dex文件的结构。通过结合对应的代码,大家对这一方面应该也会有一个具体的认知。

利用dex文件结构可以做的事

在全面的了解了dex的文件结构之后,我们可以做什么呢?

首先想得到的就是dexdiff,微信的hotpatch框架tinker就是用了这个算法去算出两个dex文件之间的差量。下面让我们看看具体的实现吧。

首先,我们先看一个tinker工程里的algorithm包:

可以看到它下面有非常多的算法,但是我们仔细看一下,这些算法的前缀好像都是我们熟悉的,有FieldIdSection,MethodIdSection等等,从这我们可以知道,dexdiff最大的一个特点就是充分利用了dex文件结构,对每一个Section(注意,这里所说的Section并不是前文中讲到的那三个大Section,而是每一块块细分后的Section,例如ProtoId,FieldId和MethodId)进行差量,然后再整合,得到一个diff文件。而普通的bsdiff是对整个dex文件进行的差量,在差量粒度上来说,比改进后的dexdiff要大的多。这里不得不佩服tinker的开发,收下我的膝盖吧!

具体的算法在DexPatchGenerator类的executeAndSaveTo方法中:

public void executeAndSaveTo(OutputStream out) throws IOException {
    // Firstly, collect information of items we want to remove additionally
    // in new dex and set them to corresponding diff algorithm implementations.
    Pattern[] classNamePatterns = new Pattern[this.additionalRemovingClassPatternSet.size()];
    int classNamePatternCount = 0;
    for (String regExStr : this.additionalRemovingClassPatternSet) {
        classNamePatterns[classNamePatternCount++] = Pattern.compile(regExStr);
    }

    List<Integer> typeIdOfClassDefsToRemove = new ArrayList<>(classNamePatternCount);
    List<Integer> offsetOfClassDatasToRemove = new ArrayList<>(classNamePatternCount);
    for (ClassDef classDef : this.newDex.classDefs()) {
        String typeName = this.newDex.typeNames().get(classDef.typeIndex);
        for (Pattern pattern : classNamePatterns) {
            if (pattern.matcher(typeName).matches()) {
                typeIdOfClassDefsToRemove.add(classDef.typeIndex);
                offsetOfClassDatasToRemove.add(classDef.classDataOffset);
                break;
            }
        }
    }

    ((ClassDefSectionDiffAlgorithm) this.classDefSectionDiffAlg)
            .setTypeIdOfClassDefsToRemove(typeIdOfClassDefsToRemove);
    ((ClassDataSectionDiffAlgorithm) this.classDataSectionDiffAlg)
            .setOffsetOfClassDatasToRemove(offsetOfClassDatasToRemove);

    // Then, run diff algorithms according to sections' dependencies.

    // Use size calculated by algorithms above or from dex file definition to
    // calculate sections' offset and patched dex size.

    // Calculate header and id sections size, so that we can work out
    // the base offset of typeLists Section.
    int patchedheaderSize = SizeOf.HEADER_ITEM;
    int patchedStringIdsSize = newDex.getTableOfContents().stringIds.size * SizeOf.STRING_ID_ITEM;
    int patchedTypeIdsSize = newDex.getTableOfContents().typeIds.size * SizeOf.TYPE_ID_ITEM;

    // Although simulatePatchOperation can calculate this value, since protoIds section
    // depends on typeLists section, we can't run protoIds Section's simulatePatchOperation
    // method so far. Instead we calculate protoIds section's size using information in newDex
    // directly.
    int patchedProtoIdsSize = newDex.getTableOfContents().protoIds.size * SizeOf.PROTO_ID_ITEM;

    int patchedFieldIdsSize = newDex.getTableOfContents().fieldIds.size * SizeOf.MEMBER_ID_ITEM;
    int patchedMethodIdsSize = newDex.getTableOfContents().methodIds.size * SizeOf.MEMBER_ID_ITEM;
    int patchedClassDefsSize = newDex.getTableOfContents().classDefs.size * SizeOf.CLASS_DEF_ITEM;

    int patchedIdSectionSize =
            patchedStringIdsSize
                    + patchedTypeIdsSize
                    + patchedProtoIdsSize
                    + patchedFieldIdsSize
                    + patchedMethodIdsSize
                    + patchedClassDefsSize;

    this.patchedHeaderOffset = 0;

    // The diff works on each sections obey such procedure:
    //  1. Execute diff algorithms to calculate indices of items we need to add, del and replace.
    //  2. Execute patch algorithm simulation to calculate indices and offsets mappings that is
    //  necessary to next section's diff works.

    // Immediately do the patch simulation so that we can know:
    //  1. Indices and offsets mapping between old dex and patched dex.
    //  2. Indices and offsets mapping between new dex and patched dex.
    // These information will be used to do next diff works.
    this.patchedStringIdsOffset = patchedHeaderOffset + patchedheaderSize;
    if (this.oldDex.getTableOfContents().stringIds.isElementFourByteAligned) {
        this.patchedStringIdsOffset
                = SizeOf.roundToTimesOfFour(this.patchedStringIdsOffset);
    }
    this.stringDataSectionDiffAlg.execute();
    this.patchedStringDataItemsOffset = patchedheaderSize + patchedIdSectionSize;
    if (this.oldDex.getTableOfContents().stringDatas.isElementFourByteAligned) {
        this.patchedStringDataItemsOffset
                = SizeOf.roundToTimesOfFour(this.patchedStringDataItemsOffset);
    }
    this.stringDataSectionDiffAlg.simulatePatchOperation(this.patchedStringDataItemsOffset);

    this.typeIdSectionDiffAlg.execute();
    this.patchedTypeIdsOffset = this.patchedStringIdsOffset + patchedStringIdsSize;
    if (this.oldDex.getTableOfContents().typeIds.isElementFourByteAligned) {
        this.patchedTypeIdsOffset
                = SizeOf.roundToTimesOfFour(this.patchedTypeIdsOffset);
    }
    this.typeIdSectionDiffAlg.simulatePatchOperation(this.patchedTypeIdsOffset);

    this.typeListSectionDiffAlg.execute();
    this.patchedTypeListsOffset
            = patchedheaderSize
            + patchedIdSectionSize
            + this.stringDataSectionDiffAlg.getPatchedSectionSize();
    if (this.oldDex.getTableOfContents().typeLists.isElementFourByteAligned) {
        this.patchedTypeListsOffset
                = SizeOf.roundToTimesOfFour(this.patchedTypeListsOffset);
    }
    this.typeListSectionDiffAlg.simulatePatchOperation(this.patchedTypeListsOffset);

    this.protoIdSectionDiffAlg.execute();
    this.patchedProtoIdsOffset = this.patchedTypeIdsOffset + patchedTypeIdsSize;
    if (this.oldDex.getTableOfContents().protoIds.isElementFourByteAligned) {
        this.patchedProtoIdsOffset = SizeOf.roundToTimesOfFour(this.patchedProtoIdsOffset);
    }
    this.protoIdSectionDiffAlg.simulatePatchOperation(this.patchedProtoIdsOffset);

    this.fieldIdSectionDiffAlg.execute();
    this.patchedFieldIdsOffset = this.patchedProtoIdsOffset + patchedProtoIdsSize;
    if (this.oldDex.getTableOfContents().fieldIds.isElementFourByteAligned) {
        this.patchedFieldIdsOffset = SizeOf.roundToTimesOfFour(this.patchedFieldIdsOffset);
    }
    this.fieldIdSectionDiffAlg.simulatePatchOperation(this.patchedFieldIdsOffset);

    this.methodIdSectionDiffAlg.execute();
    this.patchedMethodIdsOffset = this.patchedFieldIdsOffset + patchedFieldIdsSize;
    if (this.oldDex.getTableOfContents().methodIds.isElementFourByteAligned) {
        this.patchedMethodIdsOffset = SizeOf.roundToTimesOfFour(this.patchedMethodIdsOffset);
    }
    this.methodIdSectionDiffAlg.simulatePatchOperation(this.patchedMethodIdsOffset);

    this.annotationSectionDiffAlg.execute();
    this.patchedAnnotationItemsOffset
            = this.patchedTypeListsOffset
            + this.typeListSectionDiffAlg.getPatchedSectionSize();
    if (this.oldDex.getTableOfContents().annotations.isElementFourByteAligned) {
        this.patchedAnnotationItemsOffset
                = SizeOf.roundToTimesOfFour(this.patchedAnnotationItemsOffset);
    }
    this.annotationSectionDiffAlg.simulatePatchOperation(this.patchedAnnotationItemsOffset);

    this.annotationSetSectionDiffAlg.execute();
    this.patchedAnnotationSetItemsOffset
            = this.patchedAnnotationItemsOffset
            + this.annotationSectionDiffAlg.getPatchedSectionSize();
    if (this.oldDex.getTableOfContents().annotationSets.isElementFourByteAligned) {
        this.patchedAnnotationSetItemsOffset
                = SizeOf.roundToTimesOfFour(this.patchedAnnotationSetItemsOffset);
    }
    this.annotationSetSectionDiffAlg.simulatePatchOperation(
            this.patchedAnnotationSetItemsOffset
    );

    this.annotationSetRefListSectionDiffAlg.execute();
    this.patchedAnnotationSetRefListItemsOffset
            = this.patchedAnnotationSetItemsOffset
            + this.annotationSetSectionDiffAlg.getPatchedSectionSize();
    if (this.oldDex.getTableOfContents().annotationSetRefLists.isElementFourByteAligned) {
        this.patchedAnnotationSetRefListItemsOffset
                = SizeOf.roundToTimesOfFour(this.patchedAnnotationSetRefListItemsOffset);
    }
    this.annotationSetRefListSectionDiffAlg.simulatePatchOperation(
            this.patchedAnnotationSetRefListItemsOffset
    );

    this.annotationsDirectorySectionDiffAlg.execute();
    this.patchedAnnotationsDirectoryItemsOffset
            = this.patchedAnnotationSetRefListItemsOffset
            + this.annotationSetRefListSectionDiffAlg.getPatchedSectionSize();
    if (this.oldDex.getTableOfContents().annotationsDirectories.isElementFourByteAligned) {
        this.patchedAnnotationsDirectoryItemsOffset
                = SizeOf.roundToTimesOfFour(this.patchedAnnotationsDirectoryItemsOffset);
    }
    this.annotationsDirectorySectionDiffAlg.simulatePatchOperation(
            this.patchedAnnotationsDirectoryItemsOffset
    );

    this.debugInfoSectionDiffAlg.execute();
    this.patchedDebugInfoItemsOffset
            = this.patchedAnnotationsDirectoryItemsOffset
            + this.annotationsDirectorySectionDiffAlg.getPatchedSectionSize();
    if (this.oldDex.getTableOfContents().debugInfos.isElementFourByteAligned) {
        this.patchedDebugInfoItemsOffset
                = SizeOf.roundToTimesOfFour(this.patchedDebugInfoItemsOffset);
    }
    this.debugInfoSectionDiffAlg.simulatePatchOperation(this.patchedDebugInfoItemsOffset);

    this.codeSectionDiffAlg.execute();
    this.patchedCodeItemsOffset
            = this.patchedDebugInfoItemsOffset
            + this.debugInfoSectionDiffAlg.getPatchedSectionSize();
    if (this.oldDex.getTableOfContents().codes.isElementFourByteAligned) {
        this.patchedCodeItemsOffset = SizeOf.roundToTimesOfFour(this.patchedCodeItemsOffset);
    }
    this.codeSectionDiffAlg.simulatePatchOperation(this.patchedCodeItemsOffset);

    this.classDataSectionDiffAlg.execute();
    this.patchedClassDataItemsOffset
            = this.patchedCodeItemsOffset
            + this.codeSectionDiffAlg.getPatchedSectionSize();
    if (this.oldDex.getTableOfContents().classDatas.isElementFourByteAligned) {
        this.patchedClassDataItemsOffset
                = SizeOf.roundToTimesOfFour(this.patchedClassDataItemsOffset);
    }
    this.classDataSectionDiffAlg.simulatePatchOperation(this.patchedClassDataItemsOffset);

    this.encodedArraySectionDiffAlg.execute();
    this.patchedEncodedArrayItemsOffset
            = this.patchedClassDataItemsOffset
            + this.classDataSectionDiffAlg.getPatchedSectionSize();
    if (this.oldDex.getTableOfContents().encodedArrays.isElementFourByteAligned) {
        this.patchedEncodedArrayItemsOffset
                = SizeOf.roundToTimesOfFour(this.patchedEncodedArrayItemsOffset);
    }
    this.encodedArraySectionDiffAlg.simulatePatchOperation(this.patchedEncodedArrayItemsOffset);

    this.classDefSectionDiffAlg.execute();
    this.patchedClassDefsOffset = this.patchedMethodIdsOffset + patchedMethodIdsSize;
    if (this.oldDex.getTableOfContents().classDefs.isElementFourByteAligned) {
        this.patchedClassDefsOffset = SizeOf.roundToTimesOfFour(this.patchedClassDefsOffset);
    }

    // Calculate any values we still know nothing about them.
    this.patchedMapListOffset
            = this.patchedEncodedArrayItemsOffset
            + this.encodedArraySectionDiffAlg.getPatchedSectionSize();
    if (this.oldDex.getTableOfContents().mapList.isElementFourByteAligned) {
        this.patchedMapListOffset = SizeOf.roundToTimesOfFour(this.patchedMapListOffset);
    }
    int patchedMapListSize = newDex.getTableOfContents().mapList.byteCount;

    this.patchedDexSize
            = this.patchedMapListOffset
            + patchedMapListSize;

    // Finally, write results to patch file.
    writeResultToStream(out);
}

代码非常的多,但是只要你一步一步分析下去,就会发现其实逻辑是很清晰的。

第一步,把一些额外添加进来需要删除的数据删除:

Pattern[] classNamePatterns = new Pattern[this.additionalRemovingClassPatternSet.size()];
int classNamePatternCount = 0;
for (String regExStr : this.additionalRemovingClassPatternSet) {
    classNamePatterns[classNamePatternCount++] = Pattern.compile(regExStr);
}

List<Integer> typeIdOfClassDefsToRemove = new ArrayList<>(classNamePatternCount);
List<Integer> offsetOfClassDatasToRemove = new ArrayList<>(classNamePatternCount);
for (ClassDef classDef : this.newDex.classDefs()) {
    String typeName = this.newDex.typeNames().get(classDef.typeIndex);
    for (Pattern pattern : classNamePatterns) {
        if (pattern.matcher(typeName).matches()) {
            typeIdOfClassDefsToRemove.add(classDef.typeIndex);
            offsetOfClassDatasToRemove.add(classDef.classDataOffset);
            break;
        }
    }
}

((ClassDefSectionDiffAlgorithm) this.classDefSectionDiffAlg)
        .setTypeIdOfClassDefsToRemove(typeIdOfClassDefsToRemove);
((ClassDataSectionDiffAlgorithm) this.classDataSectionDiffAlg)
        .setOffsetOfClassDatasToRemove(offsetOfClassDatasToRemove);

第二步,计算出Header Section的大小和Table Section中每一个table的大小以及Table Section的总大小:

int patchedheaderSize = SizeOf.HEADER_ITEM;

int patchedStringIdsSize = newDex.getTableOfContents().stringIds.size * SizeOf.STRING_ID_ITEM;
int patchedTypeIdsSize = newDex.getTableOfContents().typeIds.size * SizeOf.TYPE_ID_ITEM;
int patchedProtoIdsSize = newDex.getTableOfContents().protoIds.size * SizeOf.PROTO_ID_ITEM;
int patchedFieldIdsSize = newDex.getTableOfContents().fieldIds.size * SizeOf.MEMBER_ID_ITEM;
int patchedMethodIdsSize = newDex.getTableOfContents().methodIds.size * SizeOf.MEMBER_ID_ITEM;
int patchedClassDefsSize = newDex.getTableOfContents().classDefs.size * SizeOf.CLASS_DEF_ITEM;

int patchedIdSectionSize =
        patchedStringIdsSize
                + patchedTypeIdsSize
                + patchedProtoIdsSize
                + patchedFieldIdsSize
                + patchedMethodIdsSize
                + patchedClassDefsSize;

它是怎么计算的呢?非常简单,先看patchedheaderSize,它的大小直接就是SizeOf.HEADER_ITEM:

/**
 * magic ubyte[8]
 * checksum uint
 * signature ubyte[20]
 * file_size uint
 * header_size uint
 * endian_tag uint
 * link_size uint
 * link_off uint
 * map_off uint
 * string_ids_size uint
 * string_ids_off uint
 * type_ids_size uint
 * type_ids_off uint
 * proto_ids_size uint
 * proto_ids_off uint
 * field_ids_size uint
 * field_ids_off uint
 * method_ids_size uint
 * method_ids_off uint
 * class_defs_size uint
 * class_defs_off uint
 * data_size uint
 * data_off uint
 */
public static final int HEADER_ITEM = (8 * UBYTE) + UINT + SIGNATURE + (20 * UINT); // 0x70

就是把Header Section中的每一个item的大小相加而已。这点我在前面已经提到了,也就是Header Section的内部结构。

至于Table Section中的每一个table的大小就是table中的item大小和table_size的乘积,我们以methodIds这个table为例:

int patchedMethodIdsSize = newDex.getTableOfContents().methodIds.size * SizeOf.MEMBER_ID_ITEM;

/**
  * class_idx ushort
  * type_idx/proto_idx ushort
  * name_idx uint
*/
public static final int MEMBER_ID_ITEM = USHORT + USHORT + UINT;

其实也就是文章上面提到的Method Table的内部结构了~

第三步,进行真正的diff计算:

this.patchedStringIdsOffset = patchedHeaderOffset + patchedheaderSize;
if (this.oldDex.getTableOfContents().stringIds.isElementFourByteAligned) {
    this.patchedStringIdsOffset
            = SizeOf.roundToTimesOfFour(this.patchedStringIdsOffset);
}
this.stringDataSectionDiffAlg.execute();
this.patchedStringDataItemsOffset = patchedheaderSize + patchedIdSectionSize;
if (this.oldDex.getTableOfContents().stringDatas.isElementFourByteAligned) {
    this.patchedStringDataItemsOffset
            = SizeOf.roundToTimesOfFour(this.patchedStringDataItemsOffset);
}
this.stringDataSectionDiffAlg.simulatePatchOperation(this.patchedStringDataItemsOffset);

this.typeIdSectionDiffAlg.execute();
this.patchedTypeIdsOffset = this.patchedStringIdsOffset + patchedStringIdsSize;
if (this.oldDex.getTableOfContents().typeIds.isElementFourByteAligned) {
    this.patchedTypeIdsOffset
            = SizeOf.roundToTimesOfFour(this.patchedTypeIdsOffset);
}
this.typeIdSectionDiffAlg.simulatePatchOperation(this.patchedTypeIdsOffset);

this.typeListSectionDiffAlg.execute();
this.patchedTypeListsOffset
        = patchedheaderSize
        + patchedIdSectionSize
        + this.stringDataSectionDiffAlg.getPatchedSectionSize();
if (this.oldDex.getTableOfContents().typeLists.isElementFourByteAligned) {
    this.patchedTypeListsOffset
            = SizeOf.roundToTimesOfFour(this.patchedTypeListsOffset);
}
this.typeListSectionDiffAlg.simulatePatchOperation(this.patchedTypeListsOffset);

this.protoIdSectionDiffAlg.execute();
this.patchedProtoIdsOffset = this.patchedTypeIdsOffset + patchedTypeIdsSize;
if (this.oldDex.getTableOfContents().protoIds.isElementFourByteAligned) {
    this.patchedProtoIdsOffset = SizeOf.roundToTimesOfFour(this.patchedProtoIdsOffset);
}
this.protoIdSectionDiffAlg.simulatePatchOperation(this.patchedProtoIdsOffset);

this.fieldIdSectionDiffAlg.execute();
this.patchedFieldIdsOffset = this.patchedProtoIdsOffset + patchedProtoIdsSize;
if (this.oldDex.getTableOfContents().fieldIds.isElementFourByteAligned) {
    this.patchedFieldIdsOffset = SizeOf.roundToTimesOfFour(this.patchedFieldIdsOffset);
}
this.fieldIdSectionDiffAlg.simulatePatchOperation(this.patchedFieldIdsOffset);

this.methodIdSectionDiffAlg.execute();
this.patchedMethodIdsOffset = this.patchedFieldIdsOffset + patchedFieldIdsSize;
if (this.oldDex.getTableOfContents().methodIds.isElementFourByteAligned) {
    this.patchedMethodIdsOffset = SizeOf.roundToTimesOfFour(this.patchedMethodIdsOffset);
}
this.methodIdSectionDiffAlg.simulatePatchOperation(this.patchedMethodIdsOffset);

this.annotationSectionDiffAlg.execute();
this.patchedAnnotationItemsOffset
        = this.patchedTypeListsOffset
        + this.typeListSectionDiffAlg.getPatchedSectionSize();
if (this.oldDex.getTableOfContents().annotations.isElementFourByteAligned) {
    this.patchedAnnotationItemsOffset
            = SizeOf.roundToTimesOfFour(this.patchedAnnotationItemsOffset);
}
this.annotationSectionDiffAlg.simulatePatchOperation(this.patchedAnnotationItemsOffset);

this.annotationSetSectionDiffAlg.execute();
this.patchedAnnotationSetItemsOffset
        = this.patchedAnnotationItemsOffset
        + this.annotationSectionDiffAlg.getPatchedSectionSize();
if (this.oldDex.getTableOfContents().annotationSets.isElementFourByteAligned) {
    this.patchedAnnotationSetItemsOffset
            = SizeOf.roundToTimesOfFour(this.patchedAnnotationSetItemsOffset);
}
this.annotationSetSectionDiffAlg.simulatePatchOperation(
        this.patchedAnnotationSetItemsOffset
);

this.annotationSetRefListSectionDiffAlg.execute();
this.patchedAnnotationSetRefListItemsOffset
        = this.patchedAnnotationSetItemsOffset
        + this.annotationSetSectionDiffAlg.getPatchedSectionSize();
if (this.oldDex.getTableOfContents().annotationSetRefLists.isElementFourByteAligned) {
    this.patchedAnnotationSetRefListItemsOffset
            = SizeOf.roundToTimesOfFour(this.patchedAnnotationSetRefListItemsOffset);
}
this.annotationSetRefListSectionDiffAlg.simulatePatchOperation(
        this.patchedAnnotationSetRefListItemsOffset
);

this.annotationsDirectorySectionDiffAlg.execute();
this.patchedAnnotationsDirectoryItemsOffset
        = this.patchedAnnotationSetRefListItemsOffset
        + this.annotationSetRefListSectionDiffAlg.getPatchedSectionSize();
if (this.oldDex.getTableOfContents().annotationsDirectories.isElementFourByteAligned) {
    this.patchedAnnotationsDirectoryItemsOffset
            = SizeOf.roundToTimesOfFour(this.patchedAnnotationsDirectoryItemsOffset);
}
this.annotationsDirectorySectionDiffAlg.simulatePatchOperation(
        this.patchedAnnotationsDirectoryItemsOffset
);

this.debugInfoSectionDiffAlg.execute();
this.patchedDebugInfoItemsOffset
        = this.patchedAnnotationsDirectoryItemsOffset
        + this.annotationsDirectorySectionDiffAlg.getPatchedSectionSize();
if (this.oldDex.getTableOfContents().debugInfos.isElementFourByteAligned) {
    this.patchedDebugInfoItemsOffset
            = SizeOf.roundToTimesOfFour(this.patchedDebugInfoItemsOffset);
}
this.debugInfoSectionDiffAlg.simulatePatchOperation(this.patchedDebugInfoItemsOffset);

this.codeSectionDiffAlg.execute();
this.patchedCodeItemsOffset
        = this.patchedDebugInfoItemsOffset
        + this.debugInfoSectionDiffAlg.getPatchedSectionSize();
if (this.oldDex.getTableOfContents().codes.isElementFourByteAligned) {
    this.patchedCodeItemsOffset = SizeOf.roundToTimesOfFour(this.patchedCodeItemsOffset);
}
this.codeSectionDiffAlg.simulatePatchOperation(this.patchedCodeItemsOffset);

this.classDataSectionDiffAlg.execute();
this.patchedClassDataItemsOffset
        = this.patchedCodeItemsOffset
        + this.codeSectionDiffAlg.getPatchedSectionSize();
if (this.oldDex.getTableOfContents().classDatas.isElementFourByteAligned) {
    this.patchedClassDataItemsOffset
            = SizeOf.roundToTimesOfFour(this.patchedClassDataItemsOffset);
}
this.classDataSectionDiffAlg.simulatePatchOperation(this.patchedClassDataItemsOffset);

this.encodedArraySectionDiffAlg.execute();
this.patchedEncodedArrayItemsOffset
        = this.patchedClassDataItemsOffset
        + this.classDataSectionDiffAlg.getPatchedSectionSize();
if (this.oldDex.getTableOfContents().encodedArrays.isElementFourByteAligned) {
    this.patchedEncodedArrayItemsOffset
            = SizeOf.roundToTimesOfFour(this.patchedEncodedArrayItemsOffset);
}
this.encodedArraySectionDiffAlg.simulatePatchOperation(this.patchedEncodedArrayItemsOffset);

this.classDefSectionDiffAlg.execute();
this.patchedClassDefsOffset = this.patchedMethodIdsOffset + patchedMethodIdsSize;
if (this.oldDex.getTableOfContents().classDefs.isElementFourByteAligned) {
    this.patchedClassDefsOffset = SizeOf.roundToTimesOfFour(this.patchedClassDefsOffset);
}

// Calculate any values we still know nothing about them.
this.patchedMapListOffset
        = this.patchedEncodedArrayItemsOffset
        + this.encodedArraySectionDiffAlg.getPatchedSectionSize();
if (this.oldDex.getTableOfContents().mapList.isElementFourByteAligned) {
    this.patchedMapListOffset = SizeOf.roundToTimesOfFour(this.patchedMapListOffset);
}
int patchedMapListSize = newDex.getTableOfContents().mapList.byteCount;

this.patchedDexSize
        = this.patchedMapListOffset
        + patchedMapListSize;

这一步是整个dexdiff算法的核心,别看代码这么多,其实内容都是重复的,就是通过前面提到的每一个Section(MethodId,FieldId等等)的algorithm去进行两个dex中相应部分的差量,具体的差量算法这里不说了,不是这篇文章的重点。看到这里你应该可以明白前面说的,为什么dexdiff的差量粒度比bsdiff小了。

第四步,把数据写入patch文件:

// Finally, write results to patch file.
writeResultToStream(out);

具体的写入规则大家有兴趣的可以去看看,就是按照dex文件格式来操作的。

除了dexdiff,我们能做的事情还有很多,比如你可以手动去分析每一个Section,从而去做一个dex的文件分析器,这里我已经写了一个小的demo,大家可以对比着上面的文章去看代码,这样会有一个更好的认知。另外,大名鼎鼎的dex2jar,其实也是运用了这方面的知识。由此可见,掌握这样的知识还是很有必要的。

后记

这篇文章的长度有点长了,但是已经是我一再精简之后的版本了,像LEB128格式,MapList和最重要的DexClassDef我都跳过了,希望大家看完之后会有收获,最主要的还是要结合代码来看,才会有收获啊~