阅读 681

Gradle

Gradle的架构

  • 在最下层的是底层Gradle框架,它主要提供一些基础服务,如task的依赖,有向无环图的构建等

  • 上面的则是Google编译工具团队的Android Gradle plugin框架,它主要是在Gradle框架的基础上,创建了很多与Android项目打包有关的task及artifacts

  • 最上面的则是开发者自定义的Plugin,一般是在Android Gradle plugin提供的task的基础上,插入一些自定义的task,或者是增加Transform进行编译时代码注入

Gradle Plugin以及Extension的多种用法,以及buildSrc及gradle插件调试的方法。

Plugin

语言选择:Google编译工具组从3.2.0开始,新增的插件全部都是用Kotlin编写的。

插件名与Plugin的关系:其声明在源码的META-INF中,如下图所示:

可以看到,不仅仅有com.android.appliation, 还有我们经常用到的com.android.library,以及com.android.feature, com.android.dynamic-feature。

以com.android.application.properties为例,其内容如下:

implementation-class=com.android.build.gradle.AppPlugin
复制代码

定义插件的方法:要定义一个Gradle Plugin,则要实现Plugin接口,该接口如下:

public interface Plugin<T>{
    void apply(T var)
}
复制代码

以我们经常用的AppPlugin和LibraryPlugin, 其继承关系如下:

可以看到,LibraryPlugin和AppPlugin都继承自BasePlugin, 而BasePlugin实现了Plugin接口,如下:

public abstract class BasePlugin<E extends BaseExtension2>
        implements Plugin<Project>, ToolingRegistryProvider {

    @VisibleForTesting
    public static final GradleVersion GRADLE_MIN_VERSION =
            GradleVersion.parse(SdkConstants.GRADLE_MINIMUM_VERSION);

    private BaseExtension extension;

    private VariantManager variantManager;
    
    ...
    }
复制代码

这里继承的层级多一层的原因是,有很多共同的逻辑可以抽出来放到BasePlugin中,然而大多数时候,我们可能没有这么复杂的关系,所以直接实现Plugin这个接口即可。

Extension

Extension其实可以理解成Java中的Java bean,它的作用也是类似的,即获取输入数据,然后在插件中使用。

最简单的Extension为例,比如我定义一个名为Student的Extension,其定义如下:

class Student{
    String name
    int age
    boolean isMale
}
复制代码

然后在Plugin的apply()方法中,添加这个Extension,不然编译时会出现找不到的情形:

project.extensions.create("student",Student.class)
复制代码

这样,我们就可以在build.gradle中使用名为student的Extension了,如下:

student{
    name 'Mike'
    age 18
    isMale true
}
复制代码

注意,这个名称要与创建Extension时的名称一致。

而获取它的方式也很简单:

Student studen = project.extensions.getByType(Student.class)
复制代码

如果Extension中要包含固定数量的配置项,那很简单,类似下面这样就可以:

class Fruit{
    int count
    Fruit(Project project){
        project.extensions.create("apple",Apple,"apple")
        project.extension.create("banana",Banana,"banana")
    }
}
复制代码

其配置如下:

fruit{
    count 3
    apple{
        name 'Big Apple'
        weight 580f
    }
    
    banana{
        name 'Yellow Banana'
        size 19f
    }
}
复制代码

下面要说的是包含不定数量的配置项的Extension,就需要用到NamedDomainObjectContainer,比如我们常用的编译配置中的productFlavors,就是一个典型的包含不定数量的配置项的Extension。

但是,如果我们不进行特殊处理,而是直接使用NamedDomainObjectContainer的话,就会发现这个配置项都要用=赋值,类似下面这样。

接着使用Student, 如果我需要在某个配置项中添加不定项个Student输入,其添加方式如下:

NamedDomainObjectContainer<Student>studentContainer = project.container(Student)
project.extensions.add('team',studentContainer)
复制代码

然而,此时其配置只能如下:

team{
    John{
       age=18
       isMale=true
    }
    Daisy{
        age=17
        isMale=false
    }
}
复制代码

注意,这里不需要name了,因为John和Daisy就是name了。

Groovy的语法不是可以省略么?就比如productFlavors这样:

要达到这样的效果其实并不难,只要做好以下两点:

  • item Extension的定义中必须有name这个属性,因为在Factory中会在创建时为这个名称的属性赋值。定义如下:
class Cat{
    String name
    
    String from
    float weight
}
复制代码
  • 需要定义一个实现了NamedDomainObjectFactory接口的类,这个类的构造方法中必须有instantiator这个参数,如下:
class CatExtFactory implements NamedDomainObjectFactory<Cat>{
    private Instantiator instantiator
    
    CatExtFactory(Instantiator instantiator){
        this.instantiator=instantiator
    }
    
    @Override
    Cat create(String name){
        return instantiator.newInstance(Cat.class, name)
    }
}
复制代码

此时,gradle配置文件中就可以类似这样写了:

animal{
    count 58
    
    dog{
        from 'America'
        isMale false
    }
    
    catConfig{
        chinaCat{
            from 'China'
            weight 2900.8f
        }
        
        birman{
            from 'Burma'
            weight 5600.51f
        }
        
        shangHaiCat{
            from 'Shanghai'
            weight 3900.56f
        }
        
        beijingCat{
            from 'Beijing'
            weight 4500.09f
        }
    }
}
复制代码

Plugin Transform

Transform是Android Gradle plugin团队提供给开发者使用的一个抽象类,它的作用是提供接口让开发者可以在源文件编译成为class文件之后,dex之前进行字节码层面的修改。

借助javaassist,ASM这样的字节码处理工具,可在自定义的Transform中进行代码的插入,修改,替换,甚至是新建类与方法。

如下是一个自定义Transform实现:

public class AllenCompTransform extends Transform {

    private Project project;
    private IComponentProvider provider

    public AllenCompTransform(Project project,IComponentProvider componentProvider) {
        this.project = project;
        this.provider=componentProvider
    }

    @Override
    public String getName() {
        return "AllenCompTransform";
    }

    @Override
    public Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS;
    }

    @Override
    public Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT;
    }

    @Override
    public boolean isIncremental() {
        return false;
    }

    @Override
    public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {

        long startTime = System.currentTimeMillis();

        transformInvocation.getOutputProvider().deleteAll();
        File jarFile = transformInvocation.getOutputProvider().getContentLocation("main", getOutputTypes(), getScopes(), Format.JAR);
        if (!jarFile.getParentFile().exists()) {
            jarFile.getParentFile().mkdirs()
        }
        if (jarFile.exists()) {
            jarFile.delete();
        }

        ClassPool classPool = new ClassPool()
        project.android.bootClasspath.each{
            classPool.appendClassPath((String)it.absolutePath)
        }

        def box=ConvertUtils.toCtClasses(transformInvocation.getInputs(),classPool)

        CodeWeaver codeWeaver=new AsmWeaver(provider.getAllActivities(),provider.getAllServices(),provider.getAllReceivers())
        codeWeaver.insertCode(box,jarFile)

        System.out.println("AllenCompTransform cost "+(System.currentTimeMillis()-startTime)+" ms")
    }
}
复制代码

Gradle插件的发布

绝大多数Gradle插件,我们可能都是只要在公司内部使用,那么只要使用公司内部的maven仓库即可,即配置并运用maven插件,然后执行其upload task即可。

特殊的buildSrc

在buildSrc中定义的插件,可以直接在其他module中运用,而且是类似这种运用方式:

apply plugin: wang.imallen.blog.comp.MainPlugin
复制代码

即直接apply具体的类,而不是其发布名称,这样的话,不管做什么修改,都能马上体现,而不需要等到重新发布版本。

Gradle插件的调试

以调试:app:assembleRelease这个task为例,其实很简单,分如下两步即可:

  • 新建remote target
  • 在命令行输入./gradlew --no-daemon -Dorg.gradle.debug=true :app:assembleRelease
  • 之后选择刚刚创建的remote target,然后点击调试按钮即可

依赖实现分析

依赖声明:

implementation “org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version”

implementation project(":applemodule")

implementation fileTree(dir:‘libs’, include:[’*.jar’])
复制代码
implementation project(path: ‘:applemdoule’)

implementation project(path: ‘:applemodule’, configuration: ‘configA’)
复制代码

从implemenation说起

按照groovy的语法,这里要执行的是DependencyHandler的implementation()方法,参数则为’com.android.support:appcompat-v7:25.1.0’. 可是我们可以看到,DependencyHandler中并没有implementation()这个方法。

MethodMissing

这其实涉及到groovy语言的一个重要特性: methodMissing, 这个特性允许在运行时catch对于未定义方法的调用。

gradle对这个特性进行了封装,一个类要想使用这个特性,只要实现MixIn接口即可,这个接口如下:

其中MethodAccess接口如下:

也就是说,对于DependeancyHandler中未定义的方法(如implementation()方法),只要hasMeethod()返回true, 就 最终会调用到MethodAccess的实现者的tryInvokeMethod()方法中,其中name为configuration名称,argusments就是’com.android.support:appcompat-v7:25.1.0’这个参数。

那DependencyHandler接口的实现者DefaultDependencyHandler是如何实现MethodMixIn这个接口的呢?

非常简单,就是直接返回dynamicMethods这个成员,而dynamicMethods的赋值在DefaultDependencyHandler的构造方法中,如下:

而DynamicAddDependencyMethods类定义如下:

注意到它是实现了MethodAccess这个接口的,首先看它的hasMethod()方法,很简单,返回true的条件是:

  • 参数长度不为0

  • configuration必须是已经定义过的

然后再看tryInvokeMethod(), 它会先通过configurationsContainer找到对应的configuration, 然后分如下几种情况:

  • 参数个数为2,并且第2个参数是Closure

  • 参数个数为1

  • 其他情形

不过不管哪种情形,都会先调用dependencyAdder的add()方法,而dependencyAdder是DefaultDependencyHandler.DirectDependencyAdder对象,其add()方法如下:

可见,其实是调用外部类DefaultDependencyHandler的doAdd()方法.

DefaultDependencyHandler.doAdd()方法分析

可见,这里会先判断dependencyNotation是否为Configuration, 如果是的话,就让当前的configuration继承自other这个configuration,而继承的意思就是,后续所有添加到other的依赖,也会添加到当前这个configuration中。

为什么还要考虑参数中的dependencyNotation是否为Configuration的情形呢?

其实就是考虑到有诸如implementation project(path: ‘:applemodule’, configuration: ‘configA’)这样的依赖声明。

依赖创建过程分析

DefaultDependencyHandler的create()方法如下:

其中的dependencyFactory为DefaultDependencyFactory对象,其createDependency()方法如下:

可见,它是直接调用dependencyNotationParser这个解析器对于dependencyNotation进行解析。

其中的dependencyNotationParser是实现了接口NotationParser<Object, Dependency>接口的对象。

为了找出这里的dependencyNotationParser到底是哪个类的实例,查看DefaultDependencyFactory的创建,如下:

可见,它是通过DependencyNotationParser.parser()方法创建的,该方法如下:

这个方法其实很好理解,它其实是创建了多个实现了接口NotationConverter的对象,然后将这些转换器都添加在一起,构成一个综合的转换器。

其中,

DependencyStringNotationConverter负责将字符串类型的notation转换为DefaultExternalModuleDependency,也就是对应implementation 'com.android.support:appcompat-v7:25.1.0’这样的声明;

DependencyFilesNotationConverter将FileCollection转换为SelfResolvingDependency,也就是对应implementation fileTree(dir:‘libs’, include:[’*.jar’])这样的声明;

DependencyProjectNotationConverter将Project转换为ProjectDependency, 对应implementation project(":applemodule")这样的情形;

DependencyClasspathNotationConverter将ClasspathNotation转换为SelfResolvingDependency;

到这里,就知道类似compile ‘com.android.support:appcompat-v7:25.1.0’,implementation project(’:applemodule’)这样的声明,其实是被不同的转换器,转换成了SelfResolvingDependency或者ProjectDependency.

这里可以看出,除了project依赖之外,其他都转换成SelfResolvingDependency, 所谓的SelfResolvingDependency其实是可以自解析的依赖,独立于repository.

ProjectDependency则不然,它与依赖于repository的,下面就分析ProjectDependency的独特之处。

DependencyHandler的project()方法分析

ProjectDependency的创建过程

DependencyHandler.project()方法是为了添加project依赖,而DefaultDependencyHandler.project()方法如下:

其中dependencyFactory为DefaultDependencyFactory对象,其createProjectDependencyFromMap()方法如下:

其中的projectDependencyFactory为ProjectDependencyFactory对象,其createFromMap()方法如下:

可见,它其实是依靠ProjectDependencyMapNotationConverter这个转换器实现将project转换为ProjectDependency的,而ProjectDependencyMapNotationConverter的定义非常简单:

显然,就是先通过projectFinder找到相应的Project, 然后通过factory创建ProjectDependency,其中的factory为DefaultProjectDependencyFactory, 其定义如下:

显然,就是根据传入的project和configuration名称,创建DefaultProjectDependency对象。

project依赖到底是如何体现的

其实与configuration息息相关。

注意DefaultProjectDependency中的getBuildDependencies()方法:

TaskDependencyImpl是一个内部类,其定义如下:

其中findProjectConfiguration()方法如下:

这个方法的含义是,如果依赖project时指定了configuration(比如implementation project(":applemodule")时的implementation), 那就获取implementation这个configuration, 如果没用,那就使用default这个configuration.

再回到TaskDependencyImpl类中,注意如下两个调用:

这两个语句的真实含义如下:

  1. configuration实现了FileCollection接口,而FileCollection继承自Buildable, 所以context.add(configuration);是将其作为一个Buildable对象添加进去。其中configuration是DefaultConfiguration对象,它实现了getBuildDependencies()方法,如下:

2. context.add(configuration.getAllArtifacts());这个,则是因为configuration.getAllArtifacts()获得的是DefaultPublishArtifactSet对象,而DefaultPublishArtifactSet也实现了Buildable接口,其getBuildDependencies()方法如下:

其中的builtBy是内部类ArtifactsTaskDependency对象,而ArtifactsTaskDependency定义如下:

可见,这里是直接将PublishArtifact对象添加到context中,而PublishArtifact中含有编译依赖。

经过本文的分析,可得出如下结论:

DependencyHandler是没有implementation(), api(), compile()这些方法的,是通过MethodMissing机制,间接地调用DependencyHandler的实现DefaultDependencyHandler的add()方法将依赖添加进去的;

如果dependencyNotation中含有configuration(如configA),则让当前的configuration(如configB)继承这个configuration, 意思就是后续所有添加到configA的依赖,都会添加到configB中;

不同的依赖声明,其实是由不同的转换器进行转换的,比如DependencyStringNotationConverter负责将类似"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"这样的依赖声明转换为依赖,DependencyProjectNotationConverter负责将project(":applemodule")这样的依赖声明转换为依赖;

除了project依赖之外,其他的依赖最终都转换为SelfResolvingDependency, 即可自解析的依赖;

project依赖的本质是artifacts依赖。
复制代码

从dependencies出发,阐述DependencyHandler的原理

关于gradle configuration的预备知识介绍,以及artifacts的发布流程

artifacts的获取流程

从TaskManager出发,分析如ApplicationTaskManager,LibraryTaskManager中各主要的Task,最后给出当前版本的编译流程图

比较3.2.1相比3.1.2中架构的变化

关于Gradle Transform

从打包的角度讲解app bundles的原理

分析资源编译的流程,特别是aapt2的编译流程

Android Plugin DSL Reference

更改默认源集配置

可以使用模块级 build.gradle 文件中的 sourceSets 代码块更改 Gradle 希望为源集的每个组件收集文件的位置。

AndroidSourceSet表示Java,aidl和RenderScript源以及Android和非Android(Java样式)资源的逻辑组。

Android Studio 按逻辑关系将每个模块的源代码和资源分组为源集

如果不同源集包含同一文件的不同版本,Gradle 将按以下优先顺序决定使用哪一个文件(左侧源集替换右侧源集的文件和设置):

构建变体 > 构建类型 > 产品风格 > 主源集 > 库依赖项

查看当前sourceSet对应版本的APP所依赖的文件路径

android {
  ...
  sourceSets {
    // 封装main源集的配置。
    main {
      // 更改Java源的目录。默认目录是'src/main/java'。
      java.srcDirs = ['other/java']

      // 当列出多个目录时,Gradle会使用它们来收集所有目录来源。
      res.srcDirs = ['other/res1', 'other/res2']

      // 对于每个源集,只能指定一个Android清单。
      manifest.srcFile 'other/AndroidManifest.xml'
      ...
    }

    // 创建其他块以配置其他源集。
    androidTest {

      // 如果源集的所有文件都位于单个根目录下目录,可以使用setRoot属性指定该目录。
      //收集源集的源时,Gradle仅在相对于您指定的根目录位置中查找。
      setRoot 'src/tests'
      ...
    }
  }
}
...
复制代码

配置项目范围的属性

对于包含多个模块的项目,在项目级别定义属性,然后在所有模块间共享这些属性可能会非常有用。可以将额外属性添加到顶级 build.gradle 文件的 ext 代码块中。

buildscript {...}
allprojects {...}

// 此块封装自定义属性并使其可供所有人使用。
ext {
    // 以下是您可以定义的属性类型的几个示例。
    compileSdkVersion = 28
    buildToolsVersion = "28.0.3"

    // 您还可以使用它来指定依赖项的版本。一致模块之间的版本可以避免行为冲突。
    supportLibVersion = "28.0.0"
    ...
}
复制代码

要从相同项目中的模块访问这些属性,请在模块级 build.gradle 文件中使用以下语法。

android {
  // 使用以下语法访问在项目级别定义的属性:
  // rootProject.ext.property_name
  compileSdkVersion rootProject.ext.compileSdkVersion
  buildToolsVersion rootProject.ext.buildToolsVersion
  ...
}
...
dependencies {
    compile "com.android.support:appcompat-v7:${rootProject.ext.supportLibVersion}"
    ...
}
复制代码

管理库和依赖项

Gradle 提供了一种稳健的机制来管理依赖项,不管它们是远程库还是本地库模块。

将依赖项配置针对特定构建

如果您希望某个依赖项仅用于特定的构建变体源集或者测试源集,则必须大写依赖项配置名称并在其前面加上构建变体或测试源集的名称作为前缀。

android {...}

// 创建要在依赖项块中使用的Gradle依赖关系配置。
configurations {
  // 对于结合了产品风味和构建类型的变体,需要为其依赖项配置初始化占位符。
  freeDebugApk {}
  ...
}

dependencies {
    // 仅向“free”产品风格添加编译依赖项。
    freeCompile 'com.google.firebase:firebase-ads:9.8.0'
    // 仅向“freeDebug”构建变体添加apk依赖项。
    freeDebugApk fileTree(dir: 'libs', include: ['*.jar'])
    // 仅为本地测试添加远程二进制依赖项。
    testCompile 'junit:junit:4.12'
    // 仅为已检测的测试APK添加远程二进制依赖项。
    androidTestCompile 'com.android.support.test.espresso:espresso-core:3.0.2'
}
复制代码

developer.android.com/studio/buil…

ProductFlavor

封装此项目的所有产品风格属性。

产品风格代表希望在单个设备,Google Play商店或存储库上共存的项目的不同版本。 例如,可以为应用配置“演示”和“完整”产品风格,并且每种风格都可以指定不同的功能,设备要求,资源和应用程序ID,同时共享公共源代码和资源。 因此,产品风格允许您通过仅更改它们之间不同的组件和设置来输出项目的不同版本。

配置产品风格类似于配置构建类型:将它们添加到模块的build.gradle文件的productFlavors块并配置所需的设置。 产品风格支持与BaseExtension.getDefaultConfig()块相同的属性 - 这是因为defaultConfig定义了一个ProductFlavor对象,该插件使用该对象作为所有其他风格的基本配置。 您配置的每个flavor都可以覆盖defaultConfig中的任何默认值,例如applicationId。

使用Android插件3.0.0及更高版本时,每种风格必须属于一个维度。

配置产品风格时,Android插件会自动将它们与您的BuildType配置相结合,以创建构建变体。 如果插件创建了您不想要的某些构建变体,则可以使用android.variantFilter过滤变体。

BuildType

用于配置构建类型的DSL对象。

DefaultConfig

defaultConfig对象的DSL对象。

SigningConfig

用于配置签名配置的DSL对象。

CompileOptions

Java编译选项。

build.gradle 文件中的所有模块内容均可在官网查看。

Gradle Build Language Reference

build.gradle 文件中的所有模块内容均可在官网查看。

Gradle 的工作过程

随着持续集成思想的普及,一次成功的构建可能分为 checkStyle,Lint,编译,单元测试,集成测试,代码裁剪,代码混淆,打包部署等多个步骤。如果项目中引用了第三方 lib,那么第三方 lib 会有版本迭代,甚至多个第三方 lib 可能又依赖了不同版本的同一个第三方 lib,造成依赖版本冲突,事情会越来越复杂。我们需要使每一个 Commit 总是能构建出完全相同的结果,Git 对于二进制文件的版本管理又不是那么得心应手,手动构建常常会引入人为变数导致构建出错。所以构建过程自动化迫在眉睫。

常见的 Java 构建工具

  • Ant (Anothre Neat Tool) 2000年

    • 使用 XML 描述构建的步骤
    • 只负责构建步骤管理,如果要添加依赖管理的功能,还需要引入 Ivy
  • Maven 2004年

    • convention over configuration 的思想,无需配置或者仅需少量配置即可开始构建
    • 和 Ant 对比增加了依赖库管理
  • Gradle 2007年

    • 使用 Groovy DSL 替代繁琐的 XML
    • 支持增量构建
    • 项目结构更加灵活

Google 基于 Gradle 通过 Android Gradle Plugin 提供了自动化构建的工具,对开发者隐藏了大量的繁琐的构建过程,暴露一些可被开发者配置的属性,大大的简化了 Android 项目管理的复杂度的同时又不失灵活性。 在这里列举的构建工具不止可以用来构建 Java 相关的项目。只要能表达出构建步骤,就可以使用这些工具来进行项目构建。比如,你可以使用 Gradle 来构建一个 iOS 的项目。

Gradle Wrapper

Wrapper是一个脚本,它调用Gradle的声明版本,必要时事先下载它。 因此,开发人员可以快速启动并运行Gradle项目,而无需遵循手动安装过程,从而节省公司的时间和金钱。

构建工具也是需要版本迭代的,一个大的版本迭代可能不会提供向前的兼容性,也就是说,在 A 机器上和 B 机器上装了两个不同版本的 Gradle,结果可能导致同一个项目,在 A 的机器上可以成功构建,而在 B 的机器上会构建失败。 为了避免这个问题,保证每个 Commit 总能构建出完全相同的结果。Gradle 提供了 Gradle Wrapper,通过 Wrapper 运行 Gradle Task 的时候,会先检查 gradle-wrapper.properties 中指定的位置下,指定版本的 Gradle 是否安装,如果已经安装,则将该 Gradle Task 交给 Gradle 处理。如果没有安装,则先下载安装指定版本的 Gradle,然后再将 Gradle Task 交给 Gradle 处理。 gradlew 是一个 script,是 Gradle Wrapper 的入口,Windows 下是 gradlew.bat。 gradle-wrapper.jar 提供了 Gradlew Wrapper 的核心功能。

目录结构如下图:

如下图所示是一个典型的使用 Gradle 进行构建的 Android 工程。 工程中包含两个 Project:

  • TutorialAndroid -- RootProject
  • app -- SubProject

可以使用如下命令查看工程中的 Project

gradlew projects
复制代码

gradlew 是入口 Script, projects 实际上是 Gradle 一个内置的 Task。 关于 Task 的概念,下面再解释。 运行上面的命令,结果如下图所示,可以看到,一般我们开发时修改 **app **只是一个子项目,RootProject 实际上是 app 的上级目录中的 TutorialAndroid。

构建过程

Gradle 的构建过程分为以下几个阶段: initialization -> configuration -> execution

  1. initialization phase
  • Gradle 使用 Project 对象来表示项目,在 initialization 阶段,Gradle 会为每个参与本次构建的项目创建一个 Project 对象。
  • 因为 Gradle 支持多项目构建,所以在初始化阶段的时候,需要判断哪些项目需要参与本次构建。
  • Gradle 可以从 Project 的根目录开始构建,也可以从任意包含 build file 的子文件架开始构建。无论从哪里开始构建,Gradle 都需要知道有哪些 Project 需要参与构建,Root Project 的 settings.gradle 中声明了需要参与构建的 Project 的信息。所以 Gradle 在这个阶段做的事情,就是从当前目录开始,逐级向上搜索 settings.gradle ,如果找到了,就按照 settings.gradle 中声明的信息设置本次构建,如果最终没有找到,那么就默认只有当前所在的 Project 需要参与本次构建。
  1. configuration phase

Task表示构建的单个原子工作,例如编译类或生成javadoc。 Task由一系列Action对象组成。 执行任务时,通过调用Action.execute(T)依次执行每个操作。 可以通过调用Task.doFirst(org.gradle.api.Action)或Task.doLast(org.gradle.api.Action)向任务添加操作。

  • Task 属于 Project 对象。可以在 build.gradle 文件中简单定义 Task
 // 定义好 Task 之后,就可以通过 `gradlew simpleTask` 来运行指定的 Task
 task simpleTask {
     doLast {
        println "This is a simple task."
     }
 }
复制代码
  • 项目构建过程分为很多步骤,在 Gradle 中用 Task 来表示这些步骤,Task 之间可能有依赖关系,例如:必须先执行完 compile Task,才能执行 unitTest Task。在 configuration 阶段,Gradle 会分析 Task 之间的依赖关系,配置初始化阶段创建的 Project 对象。

Gradle确定要在执行的配置阶段创建和配置的任务子集。 子集由传递给gradle命令和当前目录的任务名称参数确定。

  • 当一个 Project 的 Task 越来越复杂,或者多个项目都需要共用同一个 Task 的时候,为了提高代码复用性,可以编写 Plugin 将创建 Task 等逻辑封装起来。

build.gradle 中,如图所示就是在使用封装好的 Plugin。

  • 提高了代码复用性的同时,还需要提供足够的灵活性。Plugin 可以通过 Extension 暴露一些可配置的属性。
  1. execution phase

根据上一步计算出的任务执行顺序去执行需要执行的 Tasks。 以上就是 Gradle 的工作过程。

Android Transform + ASM

随着项目中对 APM (Application Performance Management) 越来越关注,诸如像 Debug 日志,运行耗时监控等都会陆陆续续加入到源码中,随着功能的增多,这些监控日志代码在某种程度上会影响甚至是干扰业务代码的阅读,有没有一些可以自动化在代码中插入日志的方法,“插桩”就映入眼帘了,本质的思想都是 AOP,在编译或运行时动态注入代码。本文选了一种在编译期间修改字节码的方法,实现在方法执行前后插入日志代码的方式进行一些初步的试探,目的旨在学习这个流程。

因为是编译期间搞事情,所以首先要在编译期间找一个时间点,这也就是标题前半部分 Transform 的内容;找到“作案”地点后,接下来就是“作案对象”了,这里选择的是对编译后的 .class 字节码下手,要到的工具就是后半部分要介绍的 ASM 了。

Transform

官方出品的编译打包签名流程,我们要搞事情的位置就是 Java Compiler 编译成 .class Files 之到打包为 .dex Files 这之间。Google 官方在 Android Gradle 的 1.5.0 版本以后提供了 Transfrom API, 允许第三方自定义插件在打包 dex 文件之前的编译过程中操作 .class 文件,所以这里先要做的就是实现一个自定义的 Transform 进行.class文件遍历拿到所有方法,修改完成对原文件进行替换。

下面说一下如何引入 Transform 依赖,在 Android gradle 插件 1.5 版本以前,是有一个单独的 transform api 的;从 2.0 版本开始,就直接并入到 gradle api 中了。

Gradle 1.5:

Compile ‘com.android.tools.build:transfrom-api:1.5.0’
复制代码

Gradle 2.0 开始:

implementation 'com.android.tools.build:gradle-api:3.0.1'
复制代码

每个 Transform 其实都是一个 Gradle task,他们链式组合,前一个的输出作为下一个的输入,而我们自定义的 Transform 是作为第一个 task 最先执行的。

每个Transform其实都是一个gradle task,Android编译器中的TaskManager将每个Transform串连起来,第一个Transform接收来自javac编译的结果,以及已经拉取到在本地的第三方依赖(jar. aar),还有resource资源,注意,这里的resource并非android项目中的res资源,而是asset目录下的资源。这些编译的中间产物,在Transform组成的链条上流动,每个Transform节点可以对class进行处理再传递给下一个Transform。我们常见的混淆,Desugar等逻辑,它们的实现如今都是封装在一个个Transform中,而我们自定义的Transform,会插入到这个Transform链条的最前面。

但其实,上面这幅图,只是展示Transform的其中一种情况。而Transform其实可以有两种输入,一种是消费型的,当前Transform需要将消费型型输出给下一个Transform,另一种是引用型的,当前Transform可以读取这些输入,而不需要输出给下一个Transform,比如Instant Run就是通过这种方式,检查两次编译之间的diff的。至于怎么在一个Transform中声明两种输入,以及怎么处理两种输入,后面将有示例代码。

为了印证Transform的工作原理和应用方式,我们也可以从Android gradle plugin源码入手找出证据,在TaskManager中,有一个方法createPostCompilationTasks.

Jacoco,Desugar,MergeJavaRes,AdvancedProfiling,Shrinker,Proguard, JarMergeTransform, MultiDex, Dex都是通过Transform的形式一个个串联起来。其中也有将我们自定义的Transform插进去。

讲完了Transform的数据流动的原理,我们再来介绍一下Transform的输入数据的过滤机制,Transform的数据输入,可以通过Scope和ContentType两个维度进行过滤。

ContentType,顾名思义,就是数据类型,在插件开发中,我们一般只能使用CLASSES和RESOURCES两种类型,注意,其中的CLASSES已经包含了class文件和jar文件

从图中可以看到,除了CLASSES和RESOURCES,还有一些我们开发过程无法使用的类型,比如DEX文件,这些隐藏类型在一个独立的枚举类ExtendedContentType中,这些类型只能给Android编译器使用。另外,我们一般使用 TransformManager中提供的几个常用的ContentType集合和Scope集合,如果是要处理所有class和jar的字节码,ContentType我们一般使用TransformManager.CONTENT_CLASS。

Scope相比ContentType则是另一个维度的过滤规则,

我们可以发现,左边几个类型可供我们使用,而我们一般都是组合使用这几个类型,TransformManager有几个常用的Scope集合方便开发者使用。 如果是要处理所有class字节码,Scope我们一般使用TransformManager.SCOPE_FULL_PROJECT。

本文是基于 buildSrc 的方式定义 Gradle 插件的,因为只在 Demo 项目中应用,所以 buildSrc 的方式就够了。需要注意一点的是,buildSrc 方式要求 library module 的名称必须为 buildSrc,在实现中注意一下。

buildSrc module:

在 buildSrc 中自定义一个基于 Groovy 的插件

在主项目 App 的 build.gradle 中引入自定义的 AsmPlugin

apply plugin: AsmPlugin
复制代码

最后,在 settings.gradle 中加入 buildSrc module

include ':app', ':buildSrc'
复制代码

至此,我们就完成了一个自定义的插件,功能十分简陋,只是在控制台输出 “hello gradle plugin",让我们编译一下看看这个插件到底有没有生效。

好了,看到控制台的输出表明我们自定义的插件生效了,“作案地方”就此埋伏完毕。

ASM

ASM 是一个功能比较齐全的 Java 字节码操作与分析框架。它能被用来动态生成类或者增强既有类的功能。ASM 可以直接 产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类的行为。

ASM 官网

ASM 提供一种基于 Visitor 的 API,通过接口的方式,分离读 class 和写 class 的逻辑,提供一个 ClassReader 负责读取class字节码,然后传递给 Class Visitor 接口,Class Visitor 接口提供了很多 visitor 方法,比如 visit class,visit method 等,这个过程就像 ClassReader 带着 ClassVisitor 游览了 class 字节码的每一个指令。

光有读还不够,如果我们要修改字节码,ClassWriter 就出场了。ClassWriter 其实也是继承自 ClassVisitor 的,所做的就是保存字节码信息并最终可以导出,那么如果我们可以代理 ClassWriter 的接口,就可以干预最终生成的字节码了。

先看一下插件目录的结构

这里新建了 AsmTransform 插件,以及 class visitor 的 adapter(TestMethodClassAdapter),使得在 visit method 的时候可以调用自定义的 TestMethodVisitor。

同时,buildSrc 的 build.gradle 中也要引入 ASM 依赖

// ASM 相关
implementation 'org.ow2.asm:asm:7.1'
implementation 'org.ow2.asm:asm-util:7.1'
implementation 'org.ow2.asm:asm-commons:7.1'
复制代码

通过Visitor API读取一个class的内容,保存到另一个文件

private void copy(String inputPath, String outputPath) {
    try {
        FileInputStream is = new FileInputStream(inputPath);
        ClassReader cr = new ClassReader(is);
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
        cr.accept(cw, 0);
        FileOutputStream fos = new FileOutputStream(outputPath);
        fos.write(cw.toByteArray());
        fos.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}
复制代码

首先,我们通过ClassReader读取某个class文件,然后定义一个ClassWriter,这个ClassWriter我们可以看它源码,其实就是一个ClassVisitor的实现,负责将ClassReader传递过来的数据写到一个字节流中,而真正触发这个逻辑就是通过ClassWriter的accept方式。

public void accept(ClassVisitor classVisitor, Attribute[] attributePrototypes, int parsingOptions) {
    
    // 读取当前class的字节码信息
    int accessFlags = this.readUnsignedShort(currentOffset);
    String thisClass = this.readClass(currentOffset + 2, charBuffer);
    String superClass = this.readClass(currentOffset + 4, charBuffer);
    String[] interfaces = new String[this.readUnsignedShort(currentOffset + 6)];
    
    
    //classVisitor就是刚才accept方法传进来的ClassWriter,每次visitXXX都负责将字节码的信息存储起来
    classVisitor.visit(this.readInt(this.cpInfoOffsets[1] - 7), accessFlags, thisClass, signature, superClass, interfaces);
    
    /**
        略去很多visit逻辑
    */
    //visit Attribute
    while(attributes != null) {
        Attribute nextAttribute = attributes.nextAttribute;
        attributes.nextAttribute = null;
        classVisitor.visitAttribute(attributes);
        attributes = nextAttribute;
    }
    /**
        略去很多visit逻辑
    */
    classVisitor.visitEnd();
}
复制代码

最后,我们通过ClassWriter的toByteArray(),将从ClassReader传递到ClassWriter的字节码导出,写入新的文件即可。这就完成了class文件的复制,这个demo虽然很简单,但是涵盖了ASM使用Visitor API修改字节码最底层的原理,大致流程如图

我们来分析一下,不难发现,如果我们要修改字节码,就是要从ClassWriter入手,上面我们提到ClassWriter中每个visitXXX(这些接口实现自ClassVisitor)都会保存字节码信息并最终可以导出,那么如果我们可以代理ClassWriter的接口,就可以干预最终字节码的生成了。

那么上面的图就应该是这样

我们只要稍微看一下ClassVisitor的代码,发现它的构造函数,是可以接收另一个ClassVisitor的,从而通过这个ClassVisitor代理所有的方法。让我们来看一个例子,为class中的每个方法调用语句的开头和结尾插入一行代码

修改前的方法是这样

private static void printTwo() {
    printOne();
    printOne();
}
复制代码

被修改后的方法是这样

private static void printTwo() {
    System.out.println("CALL printOne");
    printOne();
    System.out.println("RETURN printOne");
    System.out.println("CALL printOne");
    printOne();
    System.out.println("RETURN printOne");
}
复制代码

让我们来看一下如何用ASM实现

private static void weave(String inputPath, String outputPath) {
    try {
        FileInputStream is = new FileInputStream(inputPath);
        ClassReader cr = new ClassReader(is);
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
        CallClassAdapter adapter = new CallClassAdapter(cw);
        cr.accept(adapter, 0);
        FileOutputStream fos = new FileOutputStream(outputPath);
        fos.write(cw.toByteArray());
        fos.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}
复制代码

这段代码和上面的实现复制class的代码唯一区别就是,使用了CallClassAdapter,它是一个自定义的ClassVisitor,我们将ClassWriter传递给CallClassAdapter的构造函数。来看看它的实现

//CallClassAdapter.java
public class CallClassAdapter extends ClassVisitor implements Opcodes {
    public CallClassAdapter(final ClassVisitor cv) {
        super(ASM5, cv);
    }
    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        super.visit(version, access, name, signature, superName, interfaces);
    }
    @Override
    public MethodVisitor visitMethod(final int access, final String name,
                                     final String desc, final String signature, final String[] exceptions) {
        MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
        return mv == null ? null : new CallMethodAdapter(name, mv);
    }
}
//CallMethodAdapter.java
class CallMethodAdapter extends MethodVisitor implements Opcodes {
    public CallMethodAdapter(final MethodVisitor mv) {
        super(ASM5, mv);
    }
    @Override
    public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
        mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        mv.visitLdcInsn("CALL " + name);
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
        mv.visitMethodInsn(opcode, owner, name, desc, itf);
        mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        mv.visitLdcInsn("RETURN " + name);
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
    }
}
复制代码

CallClassAdapter中的visitMethod使用了一个自定义的MethodVisitor—–CallMethodAdapter,它也是代理了原来的MethodVisitor,原理和ClassVisitor的代理一样。

下面先来看一下 AsmTransform

class AsmTransform extends Transform {

    Project project

    AsmTransform(Project project) {
        this.project = project
    }

    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        super.transform(transformInvocation)
        println("===== ASM Transform =====")
        println("${transformInvocation.inputs}")
        println("${transformInvocation.referencedInputs}")
        println("${transformInvocation.outputProvider}")
        println("${transformInvocation.incremental}")

        //当前是否是增量编译
        boolean isIncremental = transformInvocation.isIncremental()
        //消费型输入,可以从中获取jar包和class文件夹路径。需要输出给下一个任务
        Collection<TransformInput> inputs = transformInvocation.getInputs()
        //引用型输入,无需输出。
        Collection<TransformInput> referencedInputs = transformInvocation.getReferencedInputs()
        //OutputProvider管理输出路径,如果消费型输入为空,你会发现OutputProvider == null
        TransformOutputProvider outputProvider = transformInvocation.getOutputProvider()
        for (TransformInput input : inputs) {
            for (JarInput jarInput : input.getJarInputs()) {
                File dest = outputProvider.getContentLocation(
                        jarInput.getFile().getAbsolutePath(),
                        jarInput.getContentTypes(),
                        jarInput.getScopes(),
                        Format.JAR)
                //将修改过的字节码copy到dest,就可以实现编译期间干预字节码的目的了        
                transformJar(jarInput.getFile(), dest)
            }
            for (DirectoryInput directoryInput : input.getDirectoryInputs()) {
                println("== DI = " + directoryInput.file.listFiles().toArrayString())
                File dest = outputProvider.getContentLocation(directoryInput.getName(),
                        directoryInput.getContentTypes(), directoryInput.getScopes(),
                        Format.DIRECTORY)
                //将修改过的字节码copy到dest,就可以实现编译期间干预字节码的目的了
                //FileUtils.copyDirectory(directoryInput.getFile(), dest)
                transformDir(directoryInput.getFile(), dest)
            }
        }
    }

    @Override
    String getName() {
        return AsmTransform.simpleName
    }

    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS
    }

    @Override
    Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT
    }

    @Override
    boolean isIncremental() {
        return true
    }

    private static void transformJar(File input, File dest) {
        println("=== transformJar ===")
        FileUtils.copyFile(input, dest)
    }

    private static void transformDir(File input, File dest) {
        if (dest.exists()) {
            FileUtils.forceDelete(dest)
        }
        FileUtils.forceMkdir(dest)
        String srcDirPath = input.getAbsolutePath()
        String destDirPath = dest.getAbsolutePath()
        println("=== transform dir = " + srcDirPath + ", " + destDirPath)
        for (File file : input.listFiles()) {
            String destFilePath = file.absolutePath.replace(srcDirPath, destDirPath)
            File destFile = new File(destFilePath)
            if (file.isDirectory()) {
                transformDir(file, destFile)
            } else if (file.isFile()) {
                FileUtils.touch(destFile)
                transformSingleFile(file, destFile)
            }
        }
    }

    private static void transformSingleFile(File input, File dest) {
        println("=== transformSingleFile ===")
        weave(input.getAbsolutePath(), dest.getAbsolutePath())
    }

    private static void weave(String inputPath, String outputPath) {
        try {
            FileInputStream is = new FileInputStream(inputPath)
            ClassReader cr = new ClassReader(is)
            ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES)
            TestMethodClassAdapter adapter = new TestMethodClassAdapter(cw)
            cr.accept(adapter, 0)
            FileOutputStream fos = new FileOutputStream(outputPath)
            fos.write(cw.toByteArray())
            fos.close()
        } catch (IOException e) {
            e.printStackTrace()
        }
    }
}
复制代码

我们的 InputTypes 是 CONTENT_CLASS, 表明是 class 文件,Scope 先无脑选择 SCOPE_FULL_PROJECT 在 transform 方法中主要做的事情就是把 Inputs 保存到 outProvider 提供的位置去。生成的位置见下图:

你会发现所有jar包命名都是123456递增,这是正常的,这里的命名规则可以在OutputProvider.getContentLocation的具体实现中找到

public synchronized File getContentLocation(
        @NonNull String name,
        @NonNull Set<ContentType> types,
        @NonNull Set<? super Scope> scopes,
        @NonNull Format format) {
    // runtime check these since it's (indirectly) called by 3rd party transforms.
    checkNotNull(name);
    checkNotNull(types);
    checkNotNull(scopes);
    checkNotNull(format);
    checkState(!name.isEmpty());
    checkState(!types.isEmpty());
    checkState(!scopes.isEmpty());
    // search for an existing matching substream.
    for (SubStream subStream : subStreams) {
        // look for an existing match. This means same name, types, scopes, and format.
        if (name.equals(subStream.getName())
                && types.equals(subStream.getTypes())
                && scopes.equals(subStream.getScopes())
                && format == subStream.getFormat()) {
            return new File(rootFolder, subStream.getFilename());
        }
    }
    //按位置递增!!	
    // didn't find a matching output. create the new output
    SubStream newSubStream = new SubStream(name, nextIndex++, scopes, types, format, true);
    subStreams.add(newSubStream);
    return new File(rootFolder, newSubStream.getFilename());
}
复制代码

我们将每个jar包和class文件复制到dest路径,这个dest路径就是下一个Transform的输入数据,而在复制时,我们就可以做一些狸猫换太子,偷天换日的事情了,先将jar包和class文件的字节码做一些修改,再进行复制即可.

对照代码,主要有两个 transform 方法,一个 transformJar 就是简单的拷贝,另一个 transformSingleFile,我们就是在这里用 ASM 对字节码进行修改的。

关注一下 weave 方法,可以看到我们借助 ClassReader 从 inputPath 中读取输入流,在 ClassWriter 之前用一个 adapter 进行了封装,接下来就让我们看看 adapter 做了什么。

public class TestMethodClassAdapter extends ClassVisitor implements Opcodes {

    public TestMethodClassAdapter(ClassVisitor classVisitor) {
        super(ASM7, classVisitor);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
        return (mv == null) ? null : new TestMethodVisitor(mv);
    }
}
复制代码

这个 adapter 接收一个 classVisitor 作为输入(即 ClassWriter),在 visitMethod 方法时使用自定义的 TestMethodVisitor 进行访问,再看看 TestMethodVisitor:

public class TestMethodVisitor extends MethodVisitor {

    public TestMethodVisitor(MethodVisitor methodVisitor) {
        super(ASM7, methodVisitor);
    }

    @Override
    public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {
        System.out.println("== TestMethodVisitor, owner = " + owner + ", name = " + name);
        //方法执行之前打印
        mv.visitLdcInsn(" before method exec");
        mv.visitLdcInsn(" [ASM 测试] method in " + owner + " ,name=" + name);
        mv.visitMethodInsn(INVOKESTATIC,
                "android/util/Log", "i", "(Ljava/lang/String;Ljava/lang/String;)I", false);
        mv.visitInsn(POP);

        super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);

        //方法执行之后打印
        mv.visitLdcInsn(" after method exec");
        mv.visitLdcInsn(" method in " + owner + " ,name=" + name);
        mv.visitMethodInsn(INVOKESTATIC,
                "android/util/Log", "i", "(Ljava/lang/String;Ljava/lang/String;)I", false);
        mv.visitInsn(POP);
    }
}
复制代码

TestMethodVisitor 重写了 visitMethodInsn 方法,在默认方法前后插入了一些 “字节码”,这些字节码近似 bytecode,可以认为是 ASM 格式的 bytecode。具体做的事情其实就是分别输出了两条日志:

Log.i("before method exec", "[ASM 测试] method in" + owner + ", name=" + name);
Log.i("after method exec", "method in" + owner + ", name=" + name);
复制代码

那么如何写出上面visitMethodInsn方法中插入打印方法名的逻辑,这就需要一些字节码的基础知识了.别担心,ASM 提供了一款的插件,可以转化源码为 ASM bytecode。

找一个简单的方法试一下,见下图:

左边是源码,test 方法也是只打了一条日志,右图是插件翻译出来的“ASMified” 代码,如果想看 bytecode,也是有的哈。

最后让我们看看编译后的 AsmTest.class 变成了什么样

可以看到,不单在 test() 方法中原本的日志前后新加入日志,连构造函数方法前后都加了,这是因为对 visitorMethod 方法没有进行任何区分和限制,所以任何方法调用前后都被“插桩”了。

上面我们给每一句方法调用的前后都插入了一行日志打印,那么有没有想过,这样岂不是打乱了代码的行数,这样,万一crash了,定位堆栈岂不是乱套了。其实并不然,在上面visitMethodInsn中做的东西,其实都是在同一行中插入的代码.

ClassWriter在Android上的坑

如果我们直接按上面的套路,将ASM应用到Android编译插件中,会踩到一个坑,这个坑来自于ClassWriter,具体是因为ClassWriter其中的一个逻辑,寻找两个类的共同父类。可以看看ClassWriter中的这个方法getCommonSuperClass,

protected String getCommonSuperClass(final String type1, final String type2) {
    Class<?> c, d;
    ClassLoader classLoader = getClass().getClassLoader();
    try {
        c = Class.forName(type1.replace('/', '.'), false, classLoader);
        d = Class.forName(type2.replace('/', '.'), false, classLoader);
    } catch (Exception e) {
        throw new RuntimeException(e.toString());
    }
    if (c.isAssignableFrom(d)) {
        return type1;
    }
    if (d.isAssignableFrom(c)) {
        return type2;
    }
    if (c.isInterface() || d.isInterface()) {
        return "java/lang/Object";
    } else {
        do {
            c = c.getSuperclass();
        } while (!c.isAssignableFrom(d));
        return c.getName().replace('.', '/');
    }
}
复制代码

这个方法用于寻找两个类的共同父类,我们可以看到它是获取当前class的classLoader加载两个输入的类型,而编译期间使用的classloader并没有加载Android项目中的代码,所以我们需要一个自定义的ClassLoader,将前面提到的Transform中接收到的所有jar以及class,还有android.jar都添加到自定义ClassLoader中。

如果只是替换了getCommonSuperClass中的Classloader,依然还有一个更深的坑,我们可以看看前面getCommonSuperClass的实现,它是如何寻找父类的呢?它是通过Class.forName加载某个类,然后再去寻找父类,但是,但是,android.jar中的类可不能随随便便加载的呀,android.jar对于Android工程来说只是编译时依赖,运行时是用Android机器上自己的android.jar。而且android.jar所有方法包括构造函数都是空实现,其中都只有一行代码

throw new RuntimeException("Stub!");
复制代码

这样加载某个类时,它的静态域就会被触发,而如果有一个static的变量刚好在声明时被初始化,而初始化中只有一个RuntimeException,此时就会抛异常。

所以,我们不能通过这种方式来获取父类,能否通过不需要加载class就能获取它的父类的方式呢?谜底就在眼前,父类其实也是一个class的字节码中的一项数据,那么我们就从字节码中查询父类即可。最终实现是这样。

public class ExtendClassWriter extends ClassWriter {
    public static final String TAG = "ExtendClassWriter";
    private static final String OBJECT = "java/lang/Object";
    private ClassLoader urlClassLoader;
    public ExtendClassWriter(ClassLoader urlClassLoader, int flags) {
        super(flags);
        this.urlClassLoader = urlClassLoader;
    }
    @Override
    protected String getCommonSuperClass(final String type1, final String type2) {
        if (type1 == null || type1.equals(OBJECT) || type2 == null || type2.equals(OBJECT)) {
            return OBJECT;
        }
        if (type1.equals(type2)) {
            return type1;
        }
        ClassReader type1ClassReader = getClassReader(type1);
        ClassReader type2ClassReader = getClassReader(type2);
        if (type1ClassReader == null || type2ClassReader == null) {
            return OBJECT;
        }
        if (isInterface(type1ClassReader)) {
            String interfaceName = type1;
            if (isImplements(interfaceName, type2ClassReader)) {
                return interfaceName;
            }
            if (isInterface(type2ClassReader)) {
                interfaceName = type2;
                if (isImplements(interfaceName, type1ClassReader)) {
                    return interfaceName;
                }
            }
            return OBJECT;
        }
        if (isInterface(type2ClassReader)) {
            String interfaceName = type2;
            if (isImplements(interfaceName, type1ClassReader)) {
                return interfaceName;
            }
            return OBJECT;
        }
        final Set<String> superClassNames = new HashSet<String>();
        superClassNames.add(type1);
        superClassNames.add(type2);
        String type1SuperClassName = type1ClassReader.getSuperName();
        if (!superClassNames.add(type1SuperClassName)) {
            return type1SuperClassName;
        }
        String type2SuperClassName = type2ClassReader.getSuperName();
        if (!superClassNames.add(type2SuperClassName)) {
            return type2SuperClassName;
        }
        while (type1SuperClassName != null || type2SuperClassName != null) {
            if (type1SuperClassName != null) {
                type1SuperClassName = getSuperClassName(type1SuperClassName);
                if (type1SuperClassName != null) {
                    if (!superClassNames.add(type1SuperClassName)) {
                        return type1SuperClassName;
                    }
                }
            }
            if (type2SuperClassName != null) {
                type2SuperClassName = getSuperClassName(type2SuperClassName);
                if (type2SuperClassName != null) {
                    if (!superClassNames.add(type2SuperClassName)) {
                        return type2SuperClassName;
                    }
                }
            }
        }
        return OBJECT;
    }
    private boolean isImplements(final String interfaceName, final ClassReader classReader) {
        ClassReader classInfo = classReader;
        while (classInfo != null) {
            final String[] interfaceNames = classInfo.getInterfaces();
            for (String name : interfaceNames) {
                if (name != null && name.equals(interfaceName)) {
                    return true;
                }
            }
            for (String name : interfaceNames) {
                if(name != null) {
                    final ClassReader interfaceInfo = getClassReader(name);
                    if (interfaceInfo != null) {
                        if (isImplements(interfaceName, interfaceInfo)) {
                            return true;
                        }
                    }
                }
            }
            final String superClassName = classInfo.getSuperName();
            if (superClassName == null || superClassName.equals(OBJECT)) {
                break;
            }
            classInfo = getClassReader(superClassName);
        }
        return false;
    }
    private boolean isInterface(final ClassReader classReader) {
        return (classReader.getAccess() & Opcodes.ACC_INTERFACE) != 0;
    }
    private String getSuperClassName(final String className) {
        final ClassReader classReader = getClassReader(className);
        if (classReader == null) {
            return null;
        }
        return classReader.getSuperName();
    }
    private ClassReader getClassReader(final String className) {
        InputStream inputStream = urlClassLoader.getResourceAsStream(className + ".class");
        try {
            if (inputStream != null) {
                return new ClassReader(inputStream);
            }
        } catch (IOException ignored) {
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException ignored) {
                }
            }
        }
        return null;
    }
}
复制代码

Transform的优化:增量与并发

Transform如果直接这样使用,会大大拖慢编译时间,为了解决这个问题,摸索了一段时间后,也借鉴了Android编译器中Desugar等几个Transform的实现,发现我们可以使用增量编译,并且上面transform方法遍历处理每个jar/class的流程,其实可以并发处理.

想要开启增量编译,我们需要重写Transform的这个接口,返回true。

@Override 
public boolean isIncremental() {
    return true;
}
复制代码

虽然开启了增量编译,但也并非每次编译过程都是支持增量的,毕竟一次clean build完全没有增量的基础,所以,我们需要检查当前编译是否是增量编译。

如果不是增量编译,则清空output目录,然后按照前面的方式,逐个class/jar处理 如果是增量编译,则要检查每个文件的Status,Status分四种,并且对这四种文件的操作也不尽相同

  • NOTCHANGED: 当前文件不需处理,甚至复制操作都不用;
  • ADDED、CHANGED: 正常处理,输出给下一个任务;
  • REMOVED: 移除outputProvider获取路径对应的文件。
@Override
public void transform(TransformInvocation transformInvocation){
    Collection<TransformInput> inputs = transformInvocation.getInputs();
    TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
    boolean isIncremental = transformInvocation.isIncremental();
    //如果非增量,则清空旧的输出内容
    if(!isIncremental) {
        outputProvider.deleteAll();
    }	
    for(TransformInput input : inputs) {
        for(JarInput jarInput : input.getJarInputs()) {
            Status status = jarInput.getStatus();
            File dest = outputProvider.getContentLocation(
                    jarInput.getName(),
                    jarInput.getContentTypes(),
                    jarInput.getScopes(),
                    Format.JAR);
            if(isIncremental && !emptyRun) {
                switch(status) {
                    case NOTCHANGED:
                        continue;
                    case ADDED:
                    case CHANGED:
                        transformJar(jarInput.getFile(), dest, status);
                        break;
                    case REMOVED:
                        if (dest.exists()) {
                            FileUtils.forceDelete(dest);
                        }
                        break;
                }
            } else {
                transformJar(jarInput.getFile(), dest, status);
            }
        }
        for(DirectoryInput directoryInput : input.getDirectoryInputs()) {
            File dest = outputProvider.getContentLocation(directoryInput.getName(),
                    directoryInput.getContentTypes(), directoryInput.getScopes(),
                    Format.DIRECTORY);
            FileUtils.forceMkdir(dest);
            if(isIncremental && !emptyRun) {
                String srcDirPath = directoryInput.getFile().getAbsolutePath();
                String destDirPath = dest.getAbsolutePath();
                Map<File, Status> fileStatusMap = directoryInput.getChangedFiles();
                for (Map.Entry<File, Status> changedFile : fileStatusMap.entrySet()) {
                    Status status = changedFile.getValue();
                    File inputFile = changedFile.getKey();
                    String destFilePath = inputFile.getAbsolutePath().replace(srcDirPath, destDirPath);
                    File destFile = new File(destFilePath);
                    switch (status) {
                        case NOTCHANGED:
                            break;
                        case REMOVED:
                            if(destFile.exists()) {
                                FileUtils.forceDelete(destFile);
                            }
                            break;
                        case ADDED:
                        case CHANGED:
                            FileUtils.touch(destFile);
                            transformSingleFile(inputFile, destFile, srcDirPath);
                            break;
                    }
                }
            } else {
                transformDir(directoryInput.getFile(), dest);
            }
        }
    }
}
复制代码

这就能为我们的编译插件提供增量的特性。

实现了增量编译后,我们最好也支持并发编译,并发编译的实现并不复杂,只需要将上面处理单个jar/class的逻辑,并发处理,最后阻塞等待所有任务结束即可。

private WaitableExecutor waitableExecutor = WaitableExecutor.useGlobalSharedThreadPool();
//异步并发处理jar/class
waitableExecutor.execute(() -> {
    bytecodeWeaver.weaveJar(srcJar, destJar);
    return null;
});
waitableExecutor.execute(() -> {
    bytecodeWeaver.weaveSingleClassToFile(file, outputFile, inputDirPath);
    return null;
});  
//等待所有任务结束
waitableExecutor.waitForTasksWithQuickFail(true);
复制代码

修改字节码应用案例

一种是hack代码调用,一种是hack代码实现.

比如修改Android Framework(android.jar)的实现,你是没办法在编译期间达到这个目的的,因为最终Android Framework的class在Android设备上。所以这种情况下你需要从hack代码调用入手,比如Log.i(TAG, “hello”),你不可能hack其中的实现,但是你可以把它hack成HackLog.i(TAG, “seeyou”)。

例如

而如果是要修改第三方依赖或者工程中写的代码,则可以直接hack代码实现,但是,当如果你要插入的字节码比较多时,也可以通过一定技巧减少写ASM code的量,你可以将大部分可以抽象的逻辑抽象到某个写好的class中,然后ASM code只需写调用这个写好的class的语句。

例如

使用OkHttp的人知道,OkHttp里每一个OkHttp都可以设置自己独立的Intercepter/Dns/EventListener(EventListener是okhttp3.11新增),但是需要对全局所有OkHttp设置统一的Intercepter/Dns/EventListener就很麻烦,需要一处处设置,而且一些第三方依赖中的OkHttp很大可能无法设置。

来看看我们要怎么来对OkHttp动刀

public Builder(){
    this.dispatcher = new Dispatcher();
    this.protocols = OkHttpClient.DEFAULT_PROTOCOLS;
    this.connectionSpecs = OkHttpClient.DEFAULT_CONNECTION_SPECS;
    this.eventListenerFactory = EventListener.factory(EventListener.NONE);
    this.proxySelector = ProxySelector.getDefault();
    this.cookieJar = CookieJar.NO_COOKIES;
    this.socketFactory = SocketFactory.getDefault();
    this.hostnameVerifier = OkHostnameVerifier.INSTANCE;
    this.certificatePinner = CertificatePinner.DEFAULT;
    this.proxyAuthenticator = Authenticator.NONE;
    this.authenticator = Authenticator.NONE;
    this.connectionPool = new ConnectionPool();
    this.dns = Dns.SYSTEM;
    this.followSslRedirects = true;
    this.followRedirects = true;
    this.retryOnConnectionFailure = true;
    this.connectTimeout = 10000;
    this.readTimeout = 10000;
    this.writeTimeout = 10000;
    this.pingInterval = 0;
    this.eventListenerFactory = OkHttpHooker.globalEventFactory;
    this.dns = OkHttpHooker.globalDns;
    this.interceptors.addAll(OkHttpHooker.globalInterceptors);
    this.networkInterceptors.addAll(OkHttpHooker.globalNetworkInterceptors);
}
复制代码

这是OkhttpClient中内部类Builder的构造函数,我们的目标是在方法末尾加上四行代码,这样一来,所有的OkHttpClient都会拥有共同的Intercepter/Dns/EventListener。我们再来看看OkHttpHooker的实现

public class OkHttpHooker {
    public static EventListener.Factory globalEventFactory = new EventListener.Factory() {
        public EventListener create(Call call) {
            return EventListener.NONE;
        }
    };;
    public static Dns globalDns = Dns.SYSTEM;
    public static List<Interceptor> globalInterceptors = new ArrayList<>();
    public static List<Interceptor> globalNetworkInterceptors = new ArrayList<>();
    public static void installEventListenerFactory(EventListener.Factory factory) {
        globalEventFactory = factory;
    }
    public static void installDns(Dns dns) {
        globalDns = dns;
    }
    public static void installInterceptor(Interceptor interceptor) {
        if(interceptor != null)
            globalInterceptors.add(interceptor);
    }
    public static void installNetworkInterceptors(Interceptor networkInterceptor) {
        if(networkInterceptor != null)
            globalNetworkInterceptors.add(networkInterceptor);
    }
}
复制代码

首先,我们通过Hunter的框架,可以隐藏掉Transform和ASM绝大部分细节,我们只需把注意力放在写ClassVisitor以及MethodVisitor即可。我们一共需要做以下几步

1、新建一个自定义transform,添加到一个自定义gradle plugin中 2、继承HunterTransform实现自定义transform 3、实现自定义的ClassVisitor,并依情况实现自定义MethodVisitor

继承HunterTransform,就可以让你的transform具备并发、增量的功能。

final class OkHttpHunterTransform extends HunterTransform {
    private Project project;
    private OkHttpHunterExtension okHttpHunterExtension;
    public OkHttpHunterTransform(Project project) {
        super(project);
        this.project = project;
        //依情况而定,看看你需不需要有插件扩展
        project.getExtensions().create("okHttpHunterExt", OkHttpHunterExtension.class);
        //必须的一步,继承BaseWeaver,帮你隐藏ASM细节
        this.bytecodeWeaver = new OkHttpWeaver();
    }
    @Override
    public void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {
        okHttpHunterExtension = (OkHttpHunterExtension) project.getExtensions().getByName("okHttpHunterExt");
        super.transform(context, inputs, referencedInputs, outputProvider, isIncremental);
    }
    // 用于控制修改字节码在哪些debug包还是release包下发挥作用,或者完全打开/关闭
    @Override
    protected RunVariant getRunVariant() {
        return okHttpHunterExtension.runVariant;
    }
}
//BaseWeaver帮你隐藏了ASM的很多复杂逻辑
public final class OkHttpWeaver extends BaseWeaver {
    @Override
    protected ClassVisitor wrapClassWriter(ClassWriter classWriter) {
        return new OkHttpClassAdapter(classWriter);
    }
}
//插件扩展
public class OkHttpHunterExtension {
    public RunVariant runVariant = RunVariant.ALWAYS;
    @Override
    public String toString() {
        return "OkHttpHunterExtension{" +
                "runVariant=" + runVariant +
                '}';
    }
}
复制代码

接下来看自定义ClassVisitor,它在OkHttpWeaver返回。

我们新建一个ClassVisitor(自定义ClassVisitor是为了代理ClassWriter,前面讲过)

public final class OkHttpClassAdapter extends ClassVisitor{
    private String className;
    OkHttpClassAdapter(final ClassVisitor cv) {
        super(Opcodes.ASM5, cv);
    }
    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        super.visit(version, access, name, signature, superName, interfaces);
        this.className = name;
    }
    @Override
    public MethodVisitor visitMethod(final int access, final String name,
                                     final String desc, final String signature, final String[] exceptions) {
        MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
        if(className.equals("okhttp3/OkHttpClient$Builder")) {
            return mv == null ? null : new OkHttpMethodAdapter(className + File.separator + name, access, desc, mv);
        } else {
            return mv;
        }
    }
}
复制代码

我们寻找出okhttp3/OkHttpClientBuilder这个类,其他类不管它,那么其他类只会被普通的复制,而okhttp3/OkHttpClientBuilder将会有自定义的MethodVisitor来处理

我们来看看这个MethodVisitor的实现

public final class OkHttpMethodAdapter extends LocalVariablesSorter implements Opcodes {
    private boolean defaultOkhttpClientBuilderInitMethod = false;
    OkHttpMethodAdapter(String name, int access, String desc, MethodVisitor mv) {
        super(Opcodes.ASM5, access, desc, mv);
        if ("okhttp3/OkHttpClient$Builder/<init>".equals(name) && "()V".equals(desc)) {
            defaultOkhttpClientBuilderInitMethod = true;
        }
    }
    @Override
    public void visitInsn(int opcode) {
        if(defaultOkhttpClientBuilderInitMethod) {
            if ((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) {
                //EventListenFactory
                mv.visitVarInsn(ALOAD, 0);
                mv.visitFieldInsn(GETSTATIC, "com/hunter/library/okhttp/OkHttpHooker", "globalEventFactory", "Lokhttp3/EventListener$Factory;");
                mv.visitFieldInsn(PUTFIELD, "okhttp3/OkHttpClient$Builder", "eventListenerFactory", "Lokhttp3/EventListener$Factory;");
                //Dns
                mv.visitVarInsn(ALOAD, 0);
                mv.visitFieldInsn(GETSTATIC, "com/hunter/library/okhttp/OkHttpHooker", "globalDns", "Lokhttp3/Dns;");
                mv.visitFieldInsn(PUTFIELD, "okhttp3/OkHttpClient$Builder", "dns", "Lokhttp3/Dns;");
                //Interceptor
                mv.visitVarInsn(ALOAD, 0);
                mv.visitFieldInsn(GETFIELD, "okhttp3/OkHttpClient$Builder", "interceptors", "Ljava/util/List;");
                mv.visitFieldInsn(GETSTATIC, "com/hunter/library/okhttp/OkHttpHooker", "globalInterceptors", "Ljava/util/List;");
                mv.visitMethodInsn(INVOKEINTERFACE, "java/util/List", "addAll", "(Ljava/util/Collection;)Z", true);
                mv.visitInsn(POP);
                //NetworkInterceptor
                mv.visitVarInsn(ALOAD, 0);
                mv.visitFieldInsn(GETFIELD, "okhttp3/OkHttpClient$Builder", "networkInterceptors", "Ljava/util/List;");
                mv.visitFieldInsn(GETSTATIC, "com/hunter/library/okhttp/OkHttpHooker", "globalNetworkInterceptors", "Ljava/util/List;");
                mv.visitMethodInsn(INVOKEINTERFACE, "java/util/List", "addAll", "(Ljava/util/Collection;)Z", true);
                mv.visitInsn(POP);
            }
        }
        super.visitInsn(opcode);
    }
}
复制代码

首先,我们先找出okhttp3/OkHttpClient$Builder的构造函数,然后在这个构造函数的末尾,执行插入字节码的逻辑,我们可以发现,字节码的指令是符合逆波兰式的,都是操作数在前,操作符在后。

如何将URLConnection的请求导向自己指定的OkhttpClient.

由于一些ISP的LocalDNS的问题,用户经常会获得一个次优的DNS解析结果,导致网络访问缓慢,其中原因无非三点,第一:ISP的LocalDNS缓存;第二:ISP为了节约成本,转发DNS请求到其他ISP;第三:ISP递归解析DNS时,可能由于NAT解析错误,导致出口IP不对。这些问题也促进了各大互联网公司推出自己的DNS服务,也就是HttpDNS,传统的DNS协议是通过UDP实现,而HttpDNS是通过Http协议访问自己搭建的DNS服务器。

【鹅厂网事】全局精确流量调度新思路-HttpDNS服务详解

而对于Android应用,我们要如何接入HttpDNS服务呢?首先,你需要找一个可以用的HttpDNS服务器,比如腾讯云的HttpDNS服务器或者阿里云的HttpDNS服务器,这些服务都是让客户端提交一个域名,然后返回若干个IP解析结果给客户端,得到IP之后,如果客户端简单粗暴地将本地的网络请求的域名替代成IP,会面临很多问题:

1、Https如何进行域名验证 2、如何处理SNI的问题,一个服务器使用多个域名和证书,服务器不知道应该提供哪个证书。 3、WebView中的资源请求要如何托管 4、第三方组件中的网络请求,我们要如何为它们提供HttpDNS … 以上四点,腾讯云和阿里云的接入文档对前三点都给出了相应的解决方案,然而,不仅仅第四点的问题无法解决,腾讯云和阿里云对其他几点的解决方案也都不算完美,因为它们都有一个共同问题,不能在一个地方统一处理所有网络DNS,需要逐个使用网络请求的地方去相应地解决这些问题,而且这种接入HttpDNS的方式对代码的侵入性太强,缺乏可插拔的便捷性。

有没有其他侵入性更低的方式呢?接下来让我们来探索几种通过Hook的方式来为Android应用提供全局的HttpDNS服务。

Native hook

可以借助dlopen的方式hook系统NDK中网络连接connect方法,在hook实现中处理域名解析(可参考Android hacking: hooking system functions used by Dalvik),我们也确实在很长一段时间里都是使用这种方式处理HttpDNS,但是,从Android 7.0发布后,系统将阻止应用动态链接非公开NDK库,这种库可能会导致您的应用崩溃,可参考Android 7.0 行为变更

根据应用使用的私有原生库及其目标 API 级别 (android:targetSdkVersion),应用预期显示的行为

native层行不通,那么只能在Java层寻找新的出路。

Java hook

让我们分析一下,目前Java层的Http请求是怎么发出的,可以分为两种方式,

  • 直接使用HttpURLConnection,或者基于HttpURLConnection封装的Android-async-http,Volley等第三方库。注意,这里只提HttpURLConnection,为了行文方便,默认包含HttpsURLConnection
  • 使用OkHttp。OkHttp按照Http1.x, Http2.0, SPDY的语义,用刀耕火种的方式,从Socket一步步实现Http(可能你会想,Android 4.4开始,HttpURLConnection的实现不是使用了OkHttp吗?确实是的,不过这个问题按下不表,后面解释)

OkHttp

OkHttp开放了如下代码所示的DNS接口,我们可以为每个OkHttpClient设置自定义的DNS服务,如果没有设置,则OkHttpClient将使用一个默认的DNS服务。

我们可以为每个OkHttpClient设置我们的HttpDNS服务,但是这种方式不能一劳永逸,每增加一个OkHttpClient我们都需要手动做相应修改,而且,第三方依赖库中的OkHttpClient我们更是无能为力。换一种思路,我们可以通过反射,替换掉Dns.SYSTEM这个默认的DNS实现,这样就可以一劳永逸了。

以下是Dns接口的代码

public interface Dns {
  /**
   * A DNS that uses {@link InetAddress#getAllByName} to ask the underlying operating system to
   * lookup IP addresses. Most custom {@link Dns} implementations should delegate to this instance.
   */
  Dns SYSTEM = new Dns() {
    @Override public List<InetAddress> lookup(String hostname) throws UnknownHostException {
      if (hostname == null) throw new UnknownHostException("hostname == null");
      return Arrays.asList(InetAddress.getAllByName(hostname));
    }
  };
  /**
   * Returns the IP addresses of {@code hostname}, in the order they will be attempted by OkHttp. If
   * a connection to an address fails, OkHttp will retry the connection with the next address until
   * either a connection is made, the set of IP addresses is exhausted, or a limit is exceeded.
   */
  List<InetAddress> lookup(String hostname) throws UnknownHostException;
}
复制代码

HttpURLConnection

这里说的HttpURLConnection,除了它本身,也包含了所有基于HttpURLConnection封装的第三方网络库,如Android-async-http,Volley等等。那么,我们要如何统一的处理所有HttpURLConnection的DNS呢?

我们从前面提到的问题开始切入,Android 4.4开始,HttpURLConnection的实现使用了OkHttp的实现.

OkHttp的实现不是基于HttpURLConnection,而是自己从Socket开始,重新实现的。

回到刚才的问题,HttpURLConnection是通过什么方式,将内核实现切换到OkHttp实现,让我们从代码中寻找答案,我们一般都这样构建一个HttpURLConnection

HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
复制代码

接下来,在URL这个类中寻找,HttpURLConnection是如何被构建出来的,

/**
 * The URLStreamHandler for this URL.
 */
transient URLStreamHandler handler;
public URLConnection openConnection() throws java.io.IOException {
    return handler.openConnection(this);
}
复制代码

继续寻找这个URLStreamHandler的实现

static URLStreamHandlerFactory factory;
public static void setURLStreamHandlerFactory(URLStreamHandlerFactory fac) {
    synchronized (streamHandlerLock) {
        if (factory != null) {
            throw new Error("factory already defined");
        }
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkSetFactory();
        }
        handlers.clear();
        factory = fac;
    }
}
 /**
 * Returns the Stream Handler.
 * @param protocol the protocol to use
 */
static URLStreamHandler getURLStreamHandler(String protocol) {
    URLStreamHandler handler = (URLStreamHandler)handlers.get(protocol);
    if (handler == null) {
        boolean checkedWithFactory = false;
        // Use the factory (if any)
        if (factory != null) {
            handler = factory.createURLStreamHandler(protocol);
            checkedWithFactory = true;
        }
        //...
        // Fallback to built-in stream handler.
        // Makes okhttp the default http/https handler
        if (handler == null) {
            try {
                if (protocol.equals("file")) {
                    handler = (URLStreamHandler)Class.
                        forName("sun.net.www.protocol.file.Handler").newInstance();
                } else if (protocol.equals("ftp")) {
                    handler = (URLStreamHandler)Class.
                        forName("sun.net.www.protocol.ftp.Handler").newInstance();
                } else if (protocol.equals("jar")) {
                    handler = (URLStreamHandler)Class.
                        forName("sun.net.www.protocol.jar.Handler").newInstance();
                } else if (protocol.equals("http")) {
                    handler = (URLStreamHandler)Class.
                        forName("com.android.okhttp.HttpHandler").newInstance();
                } else if (protocol.equals("https")) {
                    handler = (URLStreamHandler)Class.
                        forName("com.android.okhttp.HttpsHandler").newInstance();
                }
            } catch (Exception e) {
                throw new AssertionError(e);
            }
        }
        //...
    }
    return handler;
}
复制代码

到这里,我们找到了OkHttp的影子,Android这里反射获取的com.android.okhttp.HttpHandler和com.android.okhttp.HttpsHandler,可以到AOSP external模块中找到它们,它们都是URLStreamHandler的实现,

URLStreamHandler的职责主要是构建URLConnection。上面getURLStreamHandler的代码,我们可以另外注意到一点,这里有一个URLStreamHandler的工厂实现,也就是URLStreamHandlerFactory factory,这个工厂默认为空,如果我们为它赋予一个实现,则可以让系统通过这个工厂,获取我们自定义的URLStreamHandler,这就是我们统一处理所有HttpURLConnection的关键所在,我们只需为系统提供一个自定义的URLStreamHandlerFactory,在其中返回一个自定义的URLStreamHandler,而这个URLStreamHandler可以返回我们提供了HttpDNS服务的URLConnection。

到此为止,我们大致知道如何统一处理所有HttpURLConnection,接下来需要揣摩的问题有两个:

1、如何实现一个自定义的URLStreamHandlerFactory

2、Android系统会使用了哪个版本的OkHttp呢?

关于如何实现自定义的URLStreamHandlerFactory,可以参考OkHttp其中一个叫okhttp-urlconnection的module,这个module其实就是为了构建了一个基于OkHttp的URLStreamHandlerFactory。

在自定义工厂中,我们都可以为其设置一个自定义的OkhttpClient,所以,我们也可以和前面一样,为OkhttpClient设置自定义的DNS服务,到此为止,我们就实现全局地为HttpURLConenction提供HttpDNS服务了。

另外提一点,okhttp-urlconnection这个模块的核心代码被标记为deprecated。

/**
 * @deprecated OkHttp will be dropping its ability to be used with {@link HttpURLConnection} in an
 * upcoming release. Applications that need this should either downgrade to the system's built-in
 * {@link HttpURLConnection} or upgrade to OkHttp's Request/Response API.
 */
public final class OkUrlFactory implements URLStreamHandlerFactory, Cloneable {
    //...
}
复制代码

放心,我们在AOSP的external/okhttp发现,前面提到的com.android.okhttp.HttpHandler也是一样的实现原理,所以这样看来,这种方式还是可以继续用的。上面提到的deprecated,原因不是因为接口不稳定,而是因为OkHttp官方想安利使用标准的OkHttp API。

另一个问题,Android系统会使用哪个版本的OkHttp呢?以下是截止目前AOSP master分支上最新的OkHttp版本

Android Framework竟然只使用了OkHttp2.6的代码,不知道是出于什么考虑,Android使用的OkHttp版本迟迟没有更新,可以看一下OkHttp的CHANGELOG.md,从2.6版本到如今最新的稳定版3.8.1,已经添加了诸多提高稳定性的bugfix、feature。所以,如果我们为应用提供一个自定义的URLStreamHandlerFactory,还有一个好处,就是可以使HttpURLConnection获得最新的Okhttp优化。

除此之外,还可以做很多事情,比如利用基于责任链机制的Interceptors来做Http流量的抓包工具,或者Http流量监控工具,可以参考chuck.

到目前为止,我们已经可以处理所有的Http流量,为其添加HttpDNS服务,虽然已经满足我们的业务,但是还不够,作为一个通用的解决方案,还是需要为TCP流量也提供HttpDNS服务,也就是,如何处理所有的Socket的DNS,而如果一旦为Socket提供了统一的HttpDNS服务,也就不用再去处理Http流量的DNS,接下来开始介绍我们是如何处理的。

如何全局处理所有Socket的DNS

关于这个问题,我们考虑过两种思路,第一种,使用SocketImplFactory,构建自定义的SocketImpl,这种方式会相对第二种方式复杂一点,这一种方式还没真正执行,不过,这种方式有另外一个强大的地方,就是可以实现全局的流量监控,接下来可能会围绕它来做流量监控。接下来介绍另一种方式。

我们从Android应用默认的DNS解析过程入手,发现默认的DNS解析,都是调用以下getAllByName接口

public class InetAddress implements java.io.Serializable {
	//,,,
    static final InetAddressImpl impl = new Inet6AddressImpl();
    public static InetAddress[] getAllByName(String host) throws UnknownHostException {
        return impl.lookupAllHostAddr(host, NETID_UNSET).clone();
    }	
	//,,,
}
复制代码

而进入代码,我们可以发现,Inet6AddressImpl就是一个标准的接口类,我们完全可以动态代理它,以添加我们的HttpDNS实现,再将新的Inet6AddressImpl反射设置给上面的InetAddressImpl impl,至此,完美解决问题。

目前,QQ邮箱最新版本使用了自定义URLStreamHandlerFactory的方式,接下来准备迁移到动态代理InetAddressImpl的方式。不过还是会保留自定义URLStreamHandlerFactory,用于引入最新OkHttp特性,以及流量监控。

简单介绍一下踩到的几个坑

1、X509TrustManager获取失败

这个问题,应该很多人都遇到过,如果只设置了SSLSocketFactory,OkHttp会自定尝试反射获取一个X509TrustManager,而反射的来源,sun.security.ssl.SSLContextImpl在Android上是不存在的,所以最终抛出Unable to extract the trust manager的Crash。

public Builder sslSocketFactory(SSLSocketFactory sslSocketFactory) {
      if (sslSocketFactory == null) throw new NullPointerException("sslSocketFactory == null");
      X509TrustManager trustManager = Platform.get().trustManager(sslSocketFactory);
      if (trustManager == null) {
        throw new IllegalStateException("Unable to extract the trust manager on " + Platform.get()
            + ", sslSocketFactory is " + sslSocketFactory.getClass());
      }
      this.sslSocketFactory = sslSocketFactory;
      this.certificateChainCleaner = CertificateChainCleaner.get(trustManager);
      return this;
}
//上面提到的Platform.get().trustManager方法
public X509TrustManager trustManager(SSLSocketFactory sslSocketFactory) {
    // Attempt to get the trust manager from an OpenJDK socket factory. We attempt this on all
    // platforms in order to support Robolectric, which mixes classes from both Android and the
    // Oracle JDK. Note that we don't support HTTP/2 or other nice features on Robolectric.
    try {
      Class<?> sslContextClass = Class.forName("sun.security.ssl.SSLContextImpl");
      Object context = readFieldOrNull(sslSocketFactory, sslContextClass, "context");
      if (context == null) return null;
      return readFieldOrNull(context, X509TrustManager.class, "trustManager");
    } catch (ClassNotFoundException e) {
      return null;
    }
}
复制代码

为了解决这个问题,应该重写okhttp-urlconnection中的OkHttpsURLConnection类,对以下方法做修改

@Override public void setSSLSocketFactory(SSLSocketFactory sslSocketFactory) {
    // This fails in JDK 9 because OkHttp is unable to extract the trust manager.
    delegate.client = delegate.client.newBuilder()
        .sslSocketFactory(sslSocketFactory) //改为sslSocketFactory(sslSocketFactory, yourTrustManager)
        .build();
}
复制代码
// 18(samsung) 19 (oppo)  sslSocketFactory -- sslParameters -- trustManager
    // 22(oppo) 24(hw)   27(nexus) sslSocketFactory -- sslParameters -- x509TrustManager
    public TrustManager getTrustManagerFromSSLSocketFactory(SSLSocketFactory sslSocketFactory) {
        try {
            TrustManager result = null;
            Field fieldSslPm = sslSocketFactory.getClass().getDeclaredField("sslParameters");
            fieldSslPm.setAccessible(true);
            Object objSSLParameters = fieldSslPm.get(sslSocketFactory);
            if(Build.VERSION.SDK_INT > 19) {
                Field fieldTmg = objSSLParameters.getClass().getDeclaredField("x509TrustManager");
                fieldTmg.setAccessible(true);
                result = (TrustManager)fieldTmg.get(objSSLParameters);
            } else {
                Field fieldTmg = objSSLParameters.getClass().getDeclaredField("trustManager");
                fieldTmg.setAccessible(true);
                result = (TrustManager)fieldTmg.get(objSSLParameters);
            }
            return result;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
复制代码
private X509TrustManager findTrustManagerFromSocketFactory(SSLContext mCtx) {
        try {
            //SSLContext --> contextSpi(OpenSSLContextImpl) --> sslParameters(SSLParametersImpl) --> x509TrustManager(X509TrustManager)

            // find OpenSSLContextImpl
            Field contextSpiField = mCtx.getClass().getDeclaredField("contextSpi");
            contextSpiField.setAccessible(true);
            Object openSSLContextImplObj = contextSpiField.get(mCtx);

            // find SSLParametersImpl
            Field sslParametersField = openSSLContextImplObj.getClass().getSuperclass().getDeclaredField("sslParameters");
            sslParametersField.setAccessible(true);
            Object sslParametersImplObj = sslParametersField.get(openSSLContextImplObj);

            // find X509TrustManager
            Field x509TrustManagerField = sslParametersImplObj.getClass().getDeclaredField("x509TrustManager");
            x509TrustManagerField.setAccessible(true);
            Object x509TrustManagerObj = x509TrustManagerField.get(sslParametersImplObj);
            Log.i(TAG, "findTrustManagerFromSocketFactory object " + x509TrustManagerObj.getClass() + " " + (x509TrustManagerObj instanceof X509TrustManager));
            if(x509TrustManagerObj instanceof X509TrustManager) {
                return (X509TrustManager)x509TrustManagerObj;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
复制代码

2、Proxy的认证

OkHttp对Proxy的认证信息,是通过一个自定义的Authenticator接口获取的,而非从头部获取,所以在设置Proxy的认证信息时,需要为OkHttpClient添加一个Authenticator用于代理的认证。

3、死循环

如果你的HttpDNS的查询接口,是IP直连的,那么没有这个问题,可以跳过,如果是通过域名访问的,那需要注意,不要对这个域名进行HttpDNS解析,否则会陷入死循环。

文章分类
阅读