The book continues:

  • Gradle Pit Climbing Guide – Introduction
  • Gradle Pit Crawl Guide – First concepts, Grovvy syntax, common apis
  • Gradle Pit Crawl Guide – Understand plugins, Tasks, and build processes
  • Gradle Pit Crawl guide – Gradle core model, Hook functions, Ext extension properties, Project API

Gradle is a tool and a framework that needs to be used well. It is not enough to use Gradle. Some of the content needs to be further studied and studied. This part of the actual project is puzzling, how to understand the real. This article on this part of the content is also as far as possible a bit more in-depth, the rest of us still rely on their own, find more information to enrich their own

Dependency analysis tool

I think it’s worth starting with a few command-line analysis tools

1. Profile command

The gradle assembleDebug –profile command analyzes the project build process, looking at performance, looking at time taken, looking at each stage of the build, looking at method time taken, and looking at dependent version statistics. Although AS has a blue text after the build, click to see AS build performance analysis, but there is no such tool to clear

Profile tool generates ANALYSIS files in HTML format in the root directory /build/reports/ Profile. The content is clear and generous, easy to view, screenshot, and intercept the content for use as documents

Task Execution counts the total Execution time of a Task. Note that Gradle runs tasks in parallel with a maximum of 8 threads, so the total time is the sum of the time spent by each thread

Gradle Build –profile is a gradle build –profile. AssembleDebug is a gradle build –profile

Gradle now comes with a Task cache to speed up builds, so analyzing build performance should also be done on the first build after AS starts

Gradlew –profile –recompile-scripts –offline –rerun-tasks assembleDebug

Several parameters can be configured:

  • profile–> Enable performance check
  • recompile-scripts–> Recompile the script without caching
  • offline–> Enable offline compilation mode
  • return-taskRun all Gradle Tasks and ignore all optimizations

2. The dependencies command

1. gradlew dependencies
2. gradlew app:dependencies
3. gradlew app:dependencies --configuration implementation 
Copy the code

If you add module and implementation parameters to your dependencies command, you need to add the module and implementation parameters to your dependencies command. Adding a qualification is the dependency we added to the analysis Module ourselves

However, to be honest, there is too much information to read on the command line, so we have an official tool called Scan to help us see the results

3. Scan tool

Scan is a performance detection tool officially launched by Google to diagnose the process of application construction. It can analyze the build performance, see the time consumption of a single Task in detail, the time point and position in the entire build process, and also analyze the dependency version number, which is especially useful and a powerful weapon for us to analyze dependency conflicts

Run gradle build –scan in the root directory of your project and it will generate an analysis file in HTML format. It will get stuck asking yes/ ON and type yes

The analysis file is directly uploaded to the Scan official website, and the remote address is displayed at the end of the command line

The first run will allow you to register with the Scan website, and you will be able to read it after email confirmation. The goal here is to analyze dependency decisions (that is, dependency versions). Scan is sorted by dependency variants, and debugCompileClassPath is the dependency package in deDUG

4. Analyze specific dependencies

It is necessary to analyze a particular dependency, sometimes you don’t know which dependency ultimately determines the version of the dependency, and where, using Scan and command line

For example, rxAndroid relies on RxJava, and we will manually introduce a lower version of RxJava

implementation "IO. Reactivex. Rxjava2: rxandroid: 2.1.1"
implementation "IO. Reactivex. Rxjava2: rxjava: 2.0.0." "
Copy the code

1) Command line

gradle :app:dependencyInsight --configuration debugCompileClasspath --dependency rxjava

  • The top half begins by stating that the final resolution for RXJava is version 2.2.6
  • However, the 2.2.6 version relies on RxAndroid 2.1.1

2) Scan

In Scan, you can see the dependency analysis. Each dependency has a small mark in the upper right corner, which enables you to see the specific analysis process

Dependency management

In the early days of Gradle, every build was a puzzle. It was all luck, and most compilations failed because of dependency problems. Of course, this is much less the case now, but it is important to understand that dependencies exist in so many places that Gradle does not understand dependencies, you can not publish AAR to Maven, the problem is very difficult to fix

The solution of this article is that we are confused, a problem without a clue of this dilemma. In addition, dependency management is a core part of an automated build, so it’s important to master this

Depend on the classification

Gradle dependencies: Direct dependencies, project dependencies, local JAR, AAR dependencies, and transitive dependencies

  • Direct dependencies –> Dependencies imported directly in a project are called direct dependencies
implementation 'androidx. Core: the core - KTX: 1.3.2'
Copy the code
  • The App Module depends on the LIBS Module. Libs is a project dependency
  • Pass dependencies –> Dependencies inside, like rxAndroid also relies on RxJava, rxJava for the outer layer is passed dependencies. Passing dependencies is one of Gradle’s worst examples of how to do this. In any project where a dependency is not passed on top of a dependency, Gradle ends up with too many layers of dependency, which is time-consuming to analyze and can lead to compile failures. Some call this dependency hell
implementation "IO. Reactivex. Rxjava2: rxandroid: 2.1.1"--> internal dependency on implementation"io.reactivex.rxjava2:rxjava:x.x.x"
Copy the code

Version number rule

  • 1.3, 1.3.0 -beta3–> Fixed version number
  • [1.0.0, 1.3.0)–> >= 1.0.0 < 1.3.0,[)I can write all the symbols
    • [contain =
    • ) = not included
  • 1. +, [1.0,)— — > > = 1.0
  • The latest, integration, the latest release–> Latest version

Since Gradle does not have its own remote repository, it uses Maven, JCenter, and JVY remote repositories, so when adding remote dependencies, Gradle first looks for remote dependencies according to Maven POM rules. See Maven POM documentation for a more detailed version number

Add the dependent

Notice the difference between adding dependencies to each resource. It’s confusing, but there’s a pattern. Gradle depends on the Configuration object to complete, can be operated by code, currently do not see any actual application scenarios, should be because of my low level

1) JAR files

Module adds code from dependent JAR packages directly to its own project. For example, there is a dependency jar file in the module. Then the module uploads the JAR file to Maven through aar

Obviously, the class libraries in the JAR package are added directly to the build end product aarCopy the code

Because of this, JAR dependencies are prone to conflicts. If two AArs rely on the same JAR package, then for the project, there will be two libraries with exactly the same package name and class name. The solution to the conflict will be described below

There are generally two ways to add dependencies. Basically, everyone puts them directly into liBS. It’s too difficult to write them one by one

implementation files('hibernate.jar'.'libs/spring.jar')
implementation fileTree(dir: 'libs'.include: ['*.jar'])
Copy the code

For the Gradle Android Plugin, jar is thought of as a native code resource, implementation fileTree() is a declaration of a native Java resource path. In fact, this is essentially the same as relying on a Module project, the difference is the different resource type, one is file, the other is project

So let’s see

implementation project(':pickerview')
implementation files('hibernate.jar'.'libs/spring.jar')
Copy the code

2) So file

So files, like.java, are treated by Gradle as native code resources. You simply set the path to the.so resource

Generally, there are two ways:

  • One is to place the.so file in the default path of the Android plugin, so that the Android plugin will find the default path and importsofile
  • One is that we manually set the.so resource path
// Set the.so resource path
android{
   sourceSets {
        main {
            jniLibs.srcDirs = ['libs']}}}Copy the code

Note that so files need to be placed in the specific ABI directory, not directly in the LIbs directory, which is generally the case

X86 /x86_64/ armeabi-v7A/arm64-v8A these are called ABI, each of which corresponds to a set of CPU instructions in defaultConfig{… } to set which ABI to use

android{
    defaultConfig {
        ndk {
            abiFilters "armeabi"."armeabi-v7a"."x86"."mips"}}}Copy the code

3) ABI description

Android platform CPU has three categories: ARM, x86, MIPS, x86, MIPS has been completely eliminated, we integrate. So only consider the ARM platform architecture on the line

There are three main types of ARM platform architecture, and the following generation corresponds to the architecture:

  • armeabi:5th generation, 6th generation ARM processors, early Android phones, A5/7/8/9 cores are all based on this architecture
  • armeabiv-v7a:Seventh generation, 32-bit ARM processor architecture with floating point computing power. The A15/17 core uses this architecture, which is generally now rare and obsolete
  • arm64-v8a:The eighth-generation, 64-bit ARM processor architecture, which is used in the A32/35/53/57/72/73 core, generally falls within this range

This we can see which phone CPU, which model, will advertise the use of AXX ARM core

If you want to work with older phones, you can add Armeabi. The downside of Armeabi is that it is slow to implement. Since 2020, Google Paly has made 64-bit SO mandatory. According to the official release, it is likely that soon new cpus will all be 64-bit and implement 64-bit instruction set designs

Maybe you still have a clear understanding, so let’s take a look at the development history of ARM architecture

Development history of ARM architecture:

The one on the left is the architecture, and the one on the right is the processor, or core

  • V1 architecture 1985
  • V2 architecture 1986
  • The V3 architecture 1990
  • The V4 architecture 1993
  • V5 architecture 1998
  • V6 architecture 2001
  • V7 architecture 2004
  • V8 architecture 2011

4) Remote dependency

Gradle does not have its own remote repository. It uses Maven, Jcenter, and Jvy libraries, so to add a remote dependency, you must first declare which remote repository to use

Every Gradle script declares a remote repository to use, which is why the root script uses Allprojects {… } declare the remote repository address for each subproject

buildscript {
    ext.kotlin_version = "1.4.10"
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath "Com. Android. Tools. Build: gradle: 4.0.1." "
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}

allprojects {
    repositories {
        google()
        jcenter()
    }
}
Copy the code

It would be better to write it this way (○ ‘3’ ○)

dependencies {

    androidTestImplementation([
            'androidx. Test. Ext: junit: 1.1.2'.'androidx. Test. Espresso: espresso - core: 3.3.0'
    ])
    
    implementation([
            'androidx. Appcompat: appcompat: 1.2.0'."IO. Reactivex. Rxjava2: rxandroid: 2.1.1"])}Copy the code

Or someone in ext{… } so I’m going to write implementation, and then I’m going to write each in the script and I’m not going to write the code, but I don’t think there are so many dependencies, I don’t think there’s a need to do that, it’s not good to look at dependencies

5) AAR file

Gradle regards the AAR file as a remote dependency, so it is also in repositories{… }, but setting up the aar repository is a bit more complicated

If the App Module relies on a local AAR, this is fine

android{
	...
}

// Declare the local AAR file address
repositories {
	flatDir {
		dirs 'libs'}}dependencies {
    implementation fileTree(dir: "libs".include: ["*.jar"]) 
    // Add local AAR file dependencies
    implementation(name: 'easeui', ext: 'aar')
Copy the code

If lib provides a dependency on an AAR file for another module, then all modules in the dependency chain must write the aar file repository address. We are all scripting allprojects{… } to add aar warehouse by iotting the aar warehouse address to the module of the aar file

allprojects {
    repositories {
        google()
        jcenter()
        flatDir {
            dirs '.. /animal/libs'
        }
    }
}

implementation(name: 'easeui', ext: 'aar')
Copy the code

Eas eas Eas EAS EAS EAS EAS EAS EAS EAS EAS EAS EAS EAS EAS EAS

Or, if you’re too bothered, create a liBS folder in the project root directory, put all the AArs in it, and set it up in the same way as the script

What is the core of dependency management

So you’re familiar with remote dependencies, do you know how dependency import is managed?

implementation "IO. Reactivex. Rxjava2: rxandroid: 2.1.1"
implementation "IO. Reactivex. Rxjava2: rxjava: 2.0.0." "
Copy the code

Gradle manages dependencies on two Classpath compileClasspath and runtimeClasspath

  • compileClasspath–> Code and classes that can be used at compile time. When a component is compiled, Gradle puts it in compileClasspath
  • runtimeClasspath–> Code, classes that can be used at runtime. When a component participates in packaging, Gradle places it in runtimeClasspath
  1. Compile time –> Are you familiar with it? If not, I’ll say it here. What is compile-time, when we write a remote dependency, and AS writes the code down, and we write the code instead of using the class library, and we write the code ourselves, while the code is still being written, and anything that isn’t compiled to.class is compile-time

  2. Runtime –> Are you familiar with it? If not, let me say something about it. What is run-time when we write our code, compile it to.class, install it on the machine, and run it

  3. CompileClasspath –> contains code, class libraries that you can use when writing code. If the path doesn’t have the library we want to call, it doesn’t matter if we drop the remote dependency code

  4. RuntimeClasspath –> contains code and libraries that can be found after the app runs. Even if you write your remote dependencies to compileClasspath, you can call the library, but if you don’t write to runtimeClasspath, your app will still not find the library and crash

  5. Implementation –> These are just different ways to manage dependent libraries. The core logic is to decide whether to put remote code into compileClasspath or compileClasspath, or both, or just one and not the other

Use implementation and API to understand compileClasspath and runtimeClasspath

Some of you may not be familiar with apis, but that’s okay, just look back, okay

For example, A depends on B, B depends on C, A, B, C are project modules

  1. If A implementation B, B implementation C, then
    • C’s library can be used freely in B
    • C’s library cannot be used in A, and the IED indicates that the corresponding class cannot be found
    • That’s because implementation introduces dependencies that will add C to B’s compileClasspath and runtimeClasspath, and C to A’s runtimeClasspath
    • You can’t use THE A project’s compileClasspath because you didn’t add A’s runtimeClasspath, but you can pack C’s libraries into APK
  2. So if A implementation B, B API C, then
    • C’s library can be used freely in B
    • C’s library can also be used freely in A
    • This is because the API introduces dependencies that will add C to B’s compileClasspath and runtimeClasspath, and will add C to A’s compileClasspath and runtimeClasspath
    • Since C joins A’s compileClasspath and A’s runtimeClasspath, A projects can use C libraries and package them into APK
  3. If B compileOnly C, A cannot call C’s code and C’s code is not packaged into APK
  4. If B runtimeOnly C, then neither A nor B can call C’s code, but C’s code is packaged into APK

You can try this out for yourself

Dig into compileClasspath, runtimeClasspath

CompileClasspath and runtimeClasspath are paths to each dependency level. For example, ABC is a module project. Each module has its own compileClasspath and runtimeClasspath to manage dependencies at this level. The same goes for remote dependencies, such as RxAndroid’s remote Java. For rxAndroid remote dependencies, there are paths to compileClasspath and runtimeClasspath to manage your dependencies. Finally, in the build phase, Gradle merges compileClasspath and runtimeClasspath level by level to determine which dependencies can be used when writing code and which dependencies can be packed into APK

This is easy to understand for local code dependencies such as the Module project, but a little confusing for remote AAR dependencies. Thanks to the Baidu team’s article: Gradle and Android building primer

When you upload an AAR using the Maven specification, not only do you upload the AAR binary, but you also upload a pop.xml file in which the remote dependencies used in the AAR are recorded. The POM is an XML file.

Obviously, the AAR file we uploaded to Maven does not contain the remote dependencies that we relied on when we wrote the AAR. These remote dependencies are in the form of XML files. The POM file in the figure above has two remote dependencies, one for the Complie compilation phase. For the Runtime runtime phase, when we add the AAR on Maven to the AS, the AS automatically parses the POM file in the AAR to download the relevant remote dependency code

The rule for managing child dependencies, whether project dependencies or remote dependencies, is the same as that for managing child dependencies. If we understand the role of two paths, we can clearly know whether code can be used across projects

For example: RxAndroid has a dependency on RxJava, we add rxAndroid dependency, RxJava code can not use, try to use, why, I went to see rxAndroid source code, the original RxAndroid is API RxJava

The Scope of Gradle

Gradle is actually based on Maven’s Scope, Scope is implementation, API, etc. It operates when a dependency can be used. With path, it can be used when a dependency is compiled. It’s still available at run time

Gradle’s Scope design is based on two dimensions:

  • One is compile-time or run-time, which affects the results of dependencies added to the path.Implementation, APIThe two goods
  • The other is to build types in conjunction with buildType, as you can see from the autoprompt

Compile has been deprecated and replaced with implementation

There are other scopes, these are also common:

  • compileOnlyIt is valid only at compile time and does not appear in the final product
  • runtimeOnlyValid only at run time and will appear in the compiled product
  • annotationProcessorAnnotation processor dependency

Gradle scopes can not only affect the compilePath and runtimePath of child projects, but also remember dependency passing to affect the compilePath and runtimePath of parent projects. So let’s do a little bit of implementation and API. Gradle Scope operates on a path. It can be a project’s own path or a parent project’s path. The difference is the Scope. This, combined with Gradle dependency passing, influences which files are available when APK is finally packaged

Scope in POM file

  • Maven POM – document

As mentioned above, the subproject packages the AAR and uploads Maven. Remote dependencies in the subproject do not package the code into the final AAR file, but generate a description file pom.xml that describes how the AAR uses remote dependencies. Each dependency has a scope attribute, which, like implementation and API, also affects the location of the dependency in the path. The details will be mentioned in the next section. Here are the types of scope attribute:

This element refers to the classpath of the task at hand (compile and run time, test, etc.) and how do you limit transitivity of dependencies

There are five scopes:

  • compile– This is the default range and is used if not specified. Compile dependencies are available on all classpath. In addition, these dependencies are propagated to related projects
  • provided– This is much like compilation, but indicates that you want the JDK or container to provide it at run time. It is only available on the compile and test classpath and is not passable
  • runtime– This scope indicates that dependencies are not required for compilation, but for execution. It is in the runtime and test classpath, not in the compile classpath
  • test– This scope indicates that the dependency is not required for normal use of the application and is only available during test compilation and execution. It’s not transitive
  • system– This scope is similar to that of the PROVIDED, except that it must provide the JAR that contains it explicitly. The artifact is always available and is not looked up in the repository

Impact of Implementation and API on POM. XML scope when subproject package and upload AAR

Or above RXAndroid, RXJava example, or with the above kind of graph to say the matter

Gradle Scope and Maven have the same core content, but with some changes, testCompile corresponds to the scope of Maven

In a nutshell:

  • So rxAndroid is implementation RxJava, so that scope is Runtime
  • Rxandroid subproject rxJava API, then this scope is compile

If rxAndroid implementation rxJava, and then package aar upload Maven, and we rely on rxAndroid remote dependency library, guess what, we will get an error saying that the rxJava class is not found

The scope of rxJava is runtime in the pop.xml file, so the code will take effect only when the APK is packaged. The rxJava class library will not be found when the code is compiled and written. Unless we add rXJava remote dependencies ourselves to the project

So rxAndroid based on practical considerations, ultimately API RxJava, so we introduce rxAndroid remote dependencies will not be reported to find classes, or really bad, developers hate this hassle

Depend on the resolution

Dependency resolution refers to “the question of which build system should build during compilation if there are multiple versions of a dependency”

Dependency resolution deals with remote dependency version conflicts. Let’s take a look at a typical case:

A, B, and C are all local subproject modules, and D is A remote dependency

  • Compile time: B uses version 1.0 of D, C uses version 1.1 of D, there is no conflict between B and C
  • When packaging: Only one version of the code ends up in APK, which is a conflict for Gradle

Gradle has its own way of doing this, and it doesn’t consider any other means of enforcing a version number. By default, Gradle uses the latest version of remote dependencies, which you can see when you use Scan to analyze the dependencies

The system is dead, and HERE I have encountered a pit, which is incompatible with the previous version. The old version had the argument constructor, but the new version has the argument constructor removed. Gradle default resolution version won’t solve this problem, this can only rely on you to coordinate, who change, it is also reminds us, be sure to write functional components in the company, and compatible version, otherwise it is easy to out of the puzzling problem, this kind of problem and difficult to locate, found that most of the time is buried themselves in the hole

Forced dependent version

1) isFoce

The isFoce tag enforces the use of dependencies for that tag version

dependencies {
    implementation("IO. Reactivex. Rxjava2: rxjava: 2.2.6." ") {
        isForce = true
    }
    implementation("IO. Reactivex. Rxjava2: rxjava: 2.2.10")}// Dependency resolution uses version 2.2.6
Copy the code

Note:

  • If you write isForce multiple times for the same dependency in the same Module, only the first dependency will take effect
  • IsForce applies only to the current module. Isforces in different modules are independent of each other
  • IsForce is cross-module, these modules depend on each other, so the isForce of app shall prevail, because the app script is executed first
  • The version of isForce is not reflected in the POM file, so it must be clear that isForce must be used with caution

IsForce appears very early, the actual use of many problems, due to improper writing can easily cause the build failure, now it is recommended not to use

The biggest problem with isForce is this:

The version of force cannot be lower than that of the main app module, or an error will be reported. For example, if the app module depends on lib, a compilation error will occur if rxjava:2.2.10 is introduced in app and lib forces rxjava:2.2.6

Encounter this problem:

  • Remove force from lib
  • You can force the dependency again in the app, regardless of the high or low version

2) strictly

Strictly is a strongly dependent version constraint, which is now officially recommended and available!! shorthand

dependencies {
    implementation("IO. Reactivex. Rxjava2: rxjava: 2.2.0!!") 
    implementation("io.reactivex.rxjava2:rxjava") {
        version {
            strictly("2.2.0")}}}Copy the code

This enforces the use of version 2.2.0 dependencies

3) constraints

Constraints is an alternative to the new version of Strictly, which is the same thing as Strictly

dependencies {
    implementation("IO. Reactivex. Rxjava2: rxjava: 2.2.6!!")
    constraints {
        implementation("IO. Reactivex. Rxjava2: rxjava: 2.2.6!!")}}Copy the code

However, there is also a problem. The version of CONSTRAINTS cannot be lower than the others, or it will also report an error. The following code is problematic and will report an error

dependencies {
    implementation("IO. Reactivex. Rxjava2: rxjava: 2.2.4." ")

	constraints {
		implementation("IO. Reactivex. Rxjava2: rxjava: 2.2.0!!")}}Copy the code

4) fun

These several forced version dependency have problems, improper Settings will report errors, to tell the truth, I think or go according to the system default, choose the latest version number is good, so complicated to say bad to dig holes for themselves

The jar package conflict

As mentioned above, the.jar file is added to the final build as code, and the AAR contains the code in the.jar directly. Local projects and remote dependencies will collide if they import the same.jar and will prompt you for resources with the same package name and class name. You can use compileOnly instead of implementation where convenient

Remote dependency conflict

Gradle in implementation {… } provides a exclude setting to ignore specified dependencies. Ignored dependencies are treated as if they never existed

Familiarize yourself with what group, module, and version refer to

implementation("IO. Reactivex. Rxjava2: rxandroid: 2.1.1")

group = io.reactivex.rxjava2
module = rxandroid
version = 2.1.1
Copy the code

Normally we could write it this way

 implementation('3.1' org. Hibernate: hibernate.) {
        exclude module: 'cglib' 
        exclude group: 'org.jmock' 
        exclude group: 'org.unwanted', module: 'iAmBuggy' 
    }
Copy the code

In general, exclude, force, and transitive are used to resolve remote dependency conflicts. Force is not recommended. The following are some examples:

configurations.all {
    resolutionStrategy.eachDependency { DependencyResolveDetails details ->
        def requested = details.requested
        if (requested.group= ='com.android.support') {
            if(! requested.name.startsWith("multidex")) {
                details.useVersion '26.1.0'}}}}Copy the code

Depend on the transfer

I’m going to use implementation and API to tell you how to compileClasspath and runtimeClasspath. A depends on B, B depends on C, and C is passed to A as A dependency. Gradle merges the path layer by layer to know which code should be added to the APK file. This is the mechanism that makes the code complete and not lacking, even though this can lead to dependency conflicts and performance loss

We implementa remote dependency, dependency passing is enabled by default. There is, of course, room for dependency passing, and the system provides transitive so that we can choose whether or not to pass the dependency

Import remote dependencies, and transitive defaults to true, as shown below

implementation("IO. Reactivex. Rxjava2: rxandroid: 2.1.1"){
	transitive(true)}Copy the code

Now let’s write transitive as false

Ex. : App implementation Libs, LIBS API RxAndroid, so we can also use RxAndroid API in app, After setting the liBS API rxAndroid to transitive(false), the rxAndroid library is no longer available in the app because dependencies cannot be passed

api("IO. Reactivex. Rxjava2: rxandroid: 2.1.1"){
	transitive(false)}Copy the code

I do not know the application scenarios of transitive, but I think you can use it in open source libraries, such as Google official components, so that everyone can use it. – Dependencies will not be pushed into APK (except for app Module)

variant