在最下层的是底层Gradle框架,它主要提供一些基础服务,如task的依赖,有向无环图的构建等html
上面的则是Google编译工具团队的Android Gradle plugin框架,它主要是在Gradle框架的基础上,建立了不少与Android项目打包有关的task及artifactsjava
最上面的则是开发者自定义的Plugin,通常是在Android Gradle plugin提供的task的基础上,插入一些自定义的task,或者是增长Transform进行编译时代码注入android
语言选择:Google编译工具组从3.2.0开始,新增的插件所有都是用Kotlin编写的。git
插件名与Plugin的关系:其声明在源码的META-INF中,以下图所示: github
能够看到,不只仅有com.android.appliation, 还有咱们常常用到的com.android.library,以及com.android.feature, com.android.dynamic-feature。api
以com.android.application.properties为例,其内容以下:缓存
implementation-class=com.android.build.gradle.AppPlugin
复制代码
定义插件的方法:要定义一个Gradle Plugin,则要实现Plugin接口,该接口以下:bash
public interface Plugin<T>{
void apply(T var)
}
复制代码
以咱们常常用的AppPlugin和LibraryPlugin, 其继承关系以下: 服务器
能够看到,LibraryPlugin和AppPlugin都继承自BasePlugin, 而BasePlugin实现了Plugin接口,以下:cookie
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其实能够理解成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这样:
要达到这样的效果其实并不难,只要作好如下两点:
class Cat{
String name
String from
float weight
}
复制代码
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
}
}
}
复制代码
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插件,咱们可能都是只要在公司内部使用,那么只要使用公司内部的maven仓库便可,即配置并运用maven插件,而后执行其upload task便可。
在buildSrc中定义的插件,能够直接在其余module中运用,并且是相似这种运用方式:
apply plugin: wang.imallen.blog.comp.MainPlugin
复制代码
即直接apply具体的类,而不是其发布名称,这样的话,无论作什么修改,都能立刻体现,而不须要等到从新发布版本。
以调试:app:assembleRelease这个task为例,其实很简单,分以下两步便可:
依赖声明:
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’)
复制代码
按照groovy的语法,这里要执行的是DependencyHandler的implementation()方法,参数则为’com.android.support:appcompat-v7:25.1.0’. 但是咱们能够看到,DependencyHandler中并无implementation()这个方法。
这其实涉及到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()方法.
可见,这里会先判断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()方法是为了添加project依赖,而DefaultDependencyHandler.project()方法以下:
其中dependencyFactory为DefaultDependencyFactory对象,其createProjectDependencyFromMap()方法以下:
其中的projectDependencyFactory为ProjectDependencyFactory对象,其createFromMap()方法以下:
可见,它实际上是依靠ProjectDependencyMapNotationConverter这个转换器实现将project转换为ProjectDependency的,而ProjectDependencyMapNotationConverter的定义很是简单:
显然,就是先经过projectFinder找到相应的Project, 而后经过factory建立ProjectDependency,其中的factory为DefaultProjectDependencyFactory, 其定义以下:
显然,就是根据传入的project和configuration名称,建立DefaultProjectDependency对象。
其实与configuration息息相关。
注意DefaultProjectDependency中的getBuildDependencies()方法:
TaskDependencyImpl是一个内部类,其定义以下:
其中findProjectConfiguration()方法以下:
这个方法的含义是,若是依赖project时指定了configuration(好比implementation project(":applemodule")时的implementation), 那就获取implementation这个configuration, 若是没用,那就使用default这个configuration.
再回到TaskDependencyImpl类中,注意以下两个调用:
这两个语句的真实含义以下:
其中的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依赖。
复制代码
可使用模块级 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…
封装此项目的全部产品风格属性。
产品风格表明但愿在单个设备,Google Play商店或存储库上共存的项目的不一样版本。 例如,能够为应用配置“演示”和“完整”产品风格,而且每种风格均可以指定不一样的功能,设备要求,资源和应用程序ID,同时共享公共源代码和资源。 所以,产品风格容许您经过仅更改它们之间不一样的组件和设置来输出项目的不一样版本。
配置产品风格相似于配置构建类型:将它们添加到模块的build.gradle文件的productFlavors块并配置所需的设置。 产品风格支持与BaseExtension.getDefaultConfig()块相同的属性 - 这是由于defaultConfig定义了一个ProductFlavor对象,该插件使用该对象做为全部其余风格的基本配置。 您配置的每一个flavor均可以覆盖defaultConfig中的任何默认值,例如applicationId。
使用Android插件3.0.0及更高版本时,每种风格必须属于一个维度。
配置产品风格时,Android插件会自动将它们与您的BuildType配置相结合,以建立构建变体。 若是插件建立了您不想要的某些构建变体,则可使用android.variantFilter过滤变体。
用于配置构建类型的DSL对象。
defaultConfig对象的DSL对象。
用于配置签名配置的DSL对象。
Java编译选项。
build.gradle 文件中的全部模块内容都可在官网查看。
build.gradle 文件中的全部模块内容都可在官网查看。
随着持续集成思想的普及,一次成功的构建可能分为 checkStyle,Lint,编译,单元测试,集成测试,代码裁剪,代码混淆,打包部署等多个步骤。若是项目中引用了第三方 lib,那么第三方 lib 会有版本迭代,甚至多个第三方 lib 可能又依赖了不一样版本的同一个第三方 lib,形成依赖版本冲突,事情会愈来愈复杂。咱们须要使每个 Commit 老是能构建出彻底相同的结果,Git 对于二进制文件的版本管理又不是那么驾轻就熟,手动构建经常会引入人为变数致使构建出错。因此构建过程自动化迫在眉睫。
Ant (Anothre Neat Tool) 2000年
Maven 2004年
Gradle 2007年
Google 基于 Gradle 经过 Android Gradle Plugin 提供了自动化构建的工具,对开发者隐藏了大量的繁琐的构建过程,暴露一些可被开发者配置的属性,大大的简化了 Android 项目管理的复杂度的同时又不失灵活性。 在这里列举的构建工具不止能够用来构建 Java 相关的项目。只要能表达出构建步骤,就可使用这些工具来进行项目构建。好比,你可使用 Gradle 来构建一个 iOS 的项目。
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:
可使用以下命令查看工程中的 Project
gradlew projects
复制代码
gradlew 是入口 Script, projects 其实是 Gradle 一个内置的 Task。 关于 Task 的概念,下面再解释。 运行上面的命令,结果以下图所示,能够看到,通常咱们开发时修改 **app **只是一个子项目,RootProject 其实是 app 的上级目录中的 TutorialAndroid。
Gradle 的构建过程分为如下几个阶段: initialization -> configuration -> execution
Task表示构建的单个原子工做,例如编译类或生成javadoc。 Task由一系列Action对象组成。 执行任务时,经过调用Action.execute(T)依次执行每一个操做。 能够经过调用Task.doFirst(org.gradle.api.Action)或Task.doLast(org.gradle.api.Action)向任务添加操做。
// 定义好 Task 以后,就能够经过 `gradlew simpleTask` 来运行指定的 Task
task simpleTask {
doLast {
println "This is a simple task."
}
}
复制代码
Gradle肯定要在执行的配置阶段建立和配置的任务子集。 子集由传递给gradle命令和当前目录的任务名称参数肯定。
build.gradle 中,如图所示就是在使用封装好的 Plugin。
根据上一步计算出的任务执行顺序去执行须要执行的 Tasks。 以上就是 Gradle 的工做过程。
随着项目中对 APM (Application Performance Management) 愈来愈关注,诸如像 Debug 日志,运行耗时监控等都会陆陆续续加入到源码中,随着功能的增多,这些监控日志代码在某种程度上会影响甚至是干扰业务代码的阅读,有没有一些能够自动化在代码中插入日志的方法,“插桩”就映入眼帘了,本质的思想都是 AOP,在编译或运行时动态注入代码。本文选了一种在编译期间修改字节码的方法,实如今方法执行先后插入日志代码的方式进行一些初步的试探,目的旨在学习这个流程。
由于是编译期间搞事情,因此首先要在编译期间找一个时间点,这也就是标题前半部分 Transform 的内容;找到“做案”地点后,接下来就是“做案对象”了,这里选择的是对编译后的 .class 字节码下手,要到的工具就是后半部分要介绍的 ASM 了。
官方出品的编译打包签名流程,咱们要搞事情的位置就是 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 是一个功能比较齐全的 Java 字节码操做与分析框架。它能被用来动态生成类或者加强既有类的功能。ASM 能够直接 产生二进制 class 文件,也能够在类被加载入 Java 虚拟机以前动态改变类的行为。
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 变成了什么样
上面咱们给每一句方法调用的先后都插入了一行日志打印,那么有没有想过,这样岂不是打乱了代码的行数,这样,万一crash了,定位堆栈岂不是乱套了。其实并否则,在上面visitMethodInsn中作的东西,其实都是在同一行中插入的代码.
若是咱们直接按上面的套路,将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若是直接这样使用,会大大拖慢编译时间,为了解决这个问题,摸索了一段时间后,也借鉴了Android编译器中Desugar等几个Transform的实现,发现咱们可使用增量编译,而且上面transform方法遍历处理每一个jar/class的流程,其实能够并发处理.
想要开启增量编译,咱们须要重写Transform的这个接口,返回true。
@Override
public boolean isIncremental() {
return true;
}
复制代码
虽然开启了增量编译,但也并不是每次编译过程都是支持增量的,毕竟一次clean build彻底没有增量的基础,因此,咱们须要检查当前编译是不是增量编译。
若是不是增量编译,则清空output目录,而后按照前面的方式,逐个class/jar处理 若是是增量编译,则要检查每一个文件的Status,Status分四种,而且对这四种文件的操做也不尽相同
@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便可。咱们一共须要作如下几步
一、新建一个自定义transform,添加到一个自定义gradle plugin中 二、继承HunterTransform实现自定义transform 三、实现自定义的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将会有自定义的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的构造函数,而后在这个构造函数的末尾,执行插入字节码的逻辑,咱们能够发现,字节码的指令是符合逆波兰式的,都是操做数在前,操做符在后。
因为一些ISP的LocalDNS的问题,用户常常会得到一个次优的DNS解析结果,致使网络访问缓慢,其中缘由无非三点,第一:ISP的LocalDNS缓存;第二:ISP为了节约成本,转发DNS请求到其余ISP;第三:ISP递归解析DNS时,可能因为NAT解析错误,致使出口IP不对。这些问题也促进了各大互联网公司推出本身的DNS服务,也就是HttpDNS,传统的DNS协议是经过UDP实现,而HttpDNS是经过Http协议访问本身搭建的DNS服务器。
而对于Android应用,咱们要如何接入HttpDNS服务呢?首先,你须要找一个能够用的HttpDNS服务器,好比腾讯云的HttpDNS服务器或者阿里云的HttpDNS服务器,这些服务都是让客户端提交一个域名,而后返回若干个IP解析结果给客户端,获得IP以后,若是客户端简单粗暴地将本地的网络请求的域名替代成IP,会面临不少问题:
一、Https如何进行域名验证 二、如何处理SNI的问题,一个服务器使用多个域名和证书,服务器不知道应该提供哪一个证书。 三、WebView中的资源请求要如何托管 四、第三方组件中的网络请求,咱们要如何为它们提供HttpDNS … 以上四点,腾讯云和阿里云的接入文档对前三点都给出了相应的解决方案,然而,不只仅第四点的问题没法解决,腾讯云和阿里云对其余几点的解决方案也都不算完美,由于它们都有一个共同问题,不能在一个地方统一处理全部网络DNS,须要逐个使用网络请求的地方去相应地解决这些问题,并且这种接入HttpDNS的方式对代码的侵入性太强,缺少可插拔的便捷性。
有没有其余侵入性更低的方式呢?接下来让咱们来探索几种经过Hook的方式来为Android应用提供全局的HttpDNS服务。
能够借助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层的Http请求是怎么发出的,能够分为两种方式,
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封装的第三方网络库,如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,接下来须要揣摩的问题有两个:
一、如何实现一个自定义的URLStreamHandlerFactory
二、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,接下来开始介绍咱们是如何处理的。
关于这个问题,咱们考虑过两种思路,第一种,使用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特性,以及流量监控。
简单介绍一下踩到的几个坑
一、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;
}
复制代码
二、Proxy的认证
OkHttp对Proxy的认证信息,是经过一个自定义的Authenticator接口获取的,而非从头部获取,因此在设置Proxy的认证信息时,须要为OkHttpClient添加一个Authenticator用于代理的认证。
三、死循环
若是你的HttpDNS的查询接口,是IP直连的,那么没有这个问题,能够跳过,若是是经过域名访问的,那须要注意,不要对这个域名进行HttpDNS解析,不然会陷入死循环。