Gradle

Gradle的架構

  • 在最下層的是底層Gradle框架,它主要提供一些基礎服務,如task的依賴,有向無環圖的構建等html

  • 上面的則是Google編譯工具團隊的Android Gradle plugin框架,它主要是在Gradle框架的基礎上,建立了不少與Android項目打包有關的task及artifactsjava

  • 最上面的則是開發者自定義的Plugin,通常是在Android Gradle plugin提供的task的基礎上,插入一些自定義的task,或者是增長Transform進行編譯時代碼注入android

Gradle Plugin以及Extension的多種用法,以及buildSrc及gradle插件調試的方法。

Plugin

語言選擇: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

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便可。咱們一共須要作如下幾步

一、新建一個自定義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這個類,其餘類無論它,那麼其餘類只會被普通的複製,而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,會面臨不少問題:

一、Https如何進行域名驗證 二、如何處理SNI的問題,一個服務器使用多個域名和證書,服務器不知道應該提供哪一個證書。 三、WebView中的資源請求要如何託管 四、第三方組件中的網絡請求,咱們要如何爲它們提供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,接下來須要揣摩的問題有兩個:

一、如何實現一個自定義的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,接下來開始介紹咱們是如何處理的。

如何全局處理全部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特性,以及流量監控。

簡單介紹一下踩到的幾個坑

一、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解析,不然會陷入死循環。

相關文章
相關標籤/搜索