gradle之差分包以及dexDiff算法

911 阅读3分钟

定义差分插件

  • 1.在根目录下建议buildSrc目录,并新建build.gradle文件,sync,会自动生成.gradle目录和build目录,build.gradle文件内容如下,主意 apply plugin会识别buildSrc中的脚本,并生成对应的插件,比如apply plugin : "groovy",我们新建的文件BsDiffPlugin.groovy才能生效
buildscript {

    repositories {
        google()
        jcenter()
        mavenCentral()

    }
    dependencies {
        classpath 'com.android.tools.build:gradle:4.1.2'
    }
}

//识别groovy 插件
apply plugin : "groovy"
apply plugin: "java"

repositories {
    mavenCentral()
}

dependencies {

}
  • 2.新建groovy目录,定义BsDiffPlugin.groovy

plugin与task成对出现,plugin负责构建出task和对task进行参数的初始化,task负责真正的任务执行。

class BsDiffPlugin implements Plugin<Project>{

    @Override
    void apply(Project project) {
        project.afterEvaluate {
            //只是定义任务,不会执行,执行还需要手动 ./gradlew bsDiff
            project.task(type:BsDiffTask , "bsDiffWrapper"){
                inputs.files "${project.rootDir.absolutePath}/temp/testold.txt","${project.rootDir.absolutePath}/temp/testnew.txt"
                outputs.files "${project.rootDir.absolutePath}/temp/testdiff.patch"
            }
        }
    }
}

当我们在app下的build.gralde中增加

apply plugin: BsDiffPlugin

在gradle的配置阶段,BsDiffPlugin的apply方法会被执行,当其中定义的任务BsDiffTask并不会执行,它仅仅是被初始化出来了

project.task(type:BsDiffTask , "bsDiffWrapper") 第二个参数会修改任务的名称

  • 3.执行任务

执行任务有两种方式,可以通过

./gradlew tasks 

列出任务列表,然后执行

./gradlew bsDiffWrapper

或者勾住某个默认构建任务执行

project.getTasksByName("assembleDebug",false).getAt(0).finalizedBy 'bsDiffWrapper'
project.getTasksByName("bsDiffWrapper",false).getAt(0).finalizedBy 'bsPatchWrapper'

给插件定义id

自定义插件id的路径

  • 1.在buildSrc中的src/main下新建目录resources/META_INF/gradle-plugins

  • 2.新建两个properties文件,名字就是我们要依赖的id com.hch.plugin.bsDiff.properties com.hch.plugin.bsPatch.properties

  • 3.在文件中增加一行表示插件的实现类路径

implementation-class=com.hch.plugin.BsPatchPlugin
  • 4.在需要依赖的gradle中采用id依赖,如app下的build.gradle,主意plugins必须在文件的最开始,前面不能加其它的脚本
plugins{
    id 'com.hch.plugin.bsDiff'
    id 'com.hch.plugin.bsPatch'
    id 'com.android.application'
}

dex 文件格式分析

格式查看工具 010Editor

方法数不能超过65535的原因

在dex中对方法指向的类描述id长度为ushort,长度为65535,因此超过65535个类会提示 too much classes.

bsDiff/dexDiff比对算法的缺陷与改进

查看tinker比对算法的方法,依赖tinker的patch插件,拷贝出来实现类

compileOnly "com.tencent.tinker:tinker-patch-gradle-plugin:1.9.1"

dex的结构

dex 是结构化的数据,包含header , map , data section 。

header中包含:

  • magic string[dex.035.],
  • checksum 校验数据
  • 其它一些区段的类型和长度描述,如string,id,prototype,method,class等的初始位置,长度。其中string,id,prototype,method,class每个结构又是一个结构化数据

maplist 相比于 header更加全面的描述了dex的整个分块信息

datasection 实际的各个secion的内容信息

dex 的算法实现

因此对比算法的基本流程可以确定为:

  • dex file 到dex内存对象的读取转化
  • 对dex中15种数据结构的结构化对比,大致实现流程为,读取每个结构化列表的item并排序,产生oldList和newList,循环list的cursor 分别根据当前的cursor,获取oldItem和newItem,对其value对对比:

dex结构

    • 如果<0 ,则认为该old Item被删除了,记录为PatchOperation.OP_DEL,并记录该oldItem index到PatchOperation对象,加入到patchOperationList中。
    • 如果>0,则认为该newItem是新增的,记录为PatchOperation.OP_ADD,并记录该newItem index和value到PatchOperation对象,加入到patchOperationList中。
    • 如果=0,不会生成PatchOperation。
  • 得到对比结果 map ,oldDex中要删除的item,newDex中要新增的item,以及要替换的item
 // 2.遍历,对比,收集patch操作
while (oldCursor < this.oldItemCount || newCursor < this.newItemCount) {
    if (oldCursor >= this.oldItemCount) {
        // rest item are all newItem.
        while (newCursor < this.newItemCount) {
            // 对剩下的newItem做ADD操作
        }
    } else if (newCursor >= newItemCount) {
        // rest item are all oldItem.
        while (oldCursor < oldItemCount) {
            // 对剩下的oldItem做DEL操作
        }
    } else {
    
    }
}

dex diff的缺陷

双重循环遍历这里,可以使用数组+链表来存放 根据item的hashcode生成key值(int),通过比对key值是否相等,以o(1)的复杂度得到对比结果

bsDiff 原理