Welcome to follow my official account, scan the qr code below or search the official id: MXSZGG

This article is based on Android Gradle Plugin 3.0.1

  • preface
  • The task to write
    • The task statement
      • extension
    • Task content format
    • Task dependencies
  • The task of actual combat
    • install && launch apk
    • hook assets
  • Afterword.

preface

Tasks are functions and methods that developers encounter on a daily basis. They are the same concept. The concept of a task has been mentioned in Gradle’s basic postures for Android developers. For example transformClassesAndResourcesWithProguardForRelease task is to confuse the release source in the package.

Add the following dependencies to app/build.gradle:

CompileOnly ‘com. Android. Tools. Build: gradle: 3.0.1’

The source code for Gradle Plugin can be found in External Libraries.

How the Gradle source code is introduced will be described in the next section.

The task to write

The task statement

Task#create() : task #create()

task myTask
task myTask { configure closure }
task (myTask) { configure closure }
task (name: myTask) { configure closure }
Copy the code

Each task has its own name so that the developer can call it, such as calling the task above:

./gradlew myTask

But there is a question, if the current project app Module and a Module both contain a task named myTask, will there be a conflict, how to call them? The answer is no, and the call is as follows:

./gradlew app:myTask (call app Module’s myTask)

./gradlew a:myTask (call myTask on a Module)

ProjectName:taskName specifies the unique absolute path to call the task specified by Project.

extension

Task#create() specifies the name of a task.

  • Type: Default is DefaultTask. Similar to the parent class. This parameter is referred to later in this article.

  • DependsOn: Default is []. DependsOn (Object) – dependsOn(Task) – dependsOn(Object) Path). This parameter is referred to later in this article.

  • Action: Null by default. Equivalent to Action in task.dofirst {Action}.

    task (name: actionTest, action: new Action<Task>() {
      @Override
      void execute(Task task) {
        println 'hello'{}}})Copy the code

    Is equivalent to

    task (name: actionTest) {
    	doFirst {
    		println 'hello'}}Copy the code
  • Override: The default is false. Whether to replace an existing task.

  • Group: Null by default. Group type of the task.

  • Description: The default value is null. Task description.

  • ConstructorArgs: Default is null. Parameters passed to the Task constructor.

Most of the last four should not be used in the development process, readers who need to consult the documentation.

Task content format

  1. According to the official documentation and the previous article, if you want to add operations to a task, you can add them to a closure such as doLast {}/doFirst {}, for example:

    task myTask {
    	doFirst {
    		println 'The first thing myTask executes'
    	}
    	doLast {
    		println 'Last thing myTask executed'
    	}
    	// warning
    	// println 'Both the Configuration phase and the Execution phase'
    }
    Copy the code

Keep in mind that most of the writing is done in doLast{} or doFirst{} closures, because writing in task closures is also done in the Configuration phase.

  1. To improve task reuse, Gradle also supports writing tasks

2.1 Write the following code in build.gradle and mark the method you want to execute with @taskAction.

class GreetingTask extends DefaultTask {
	String greeting = 'hello from GreetingTask'
	
	@TaskAction
    def greet() {
        println greeting
    }
}
Copy the code

Compiling tasks in build.gradle calls GreetingTask:

// Use the default greeting
task (name: hello , type: GreetingTask)

// Customize the greeting
task (name: greeting , type: GreetingTask) {
	greeting = 'greetings from GreetingTask'
}
Copy the code

2.3 Invoking the Task —

./gradlew hello

> Task :app:hello
hello from GreetingTask

./gradlew greeting

> Task :app:greeting
greetings from GreetingTask
Copy the code

Not only should you understand the writing of the Task class, but you should also have a rough idea of what the type parameter means.

The @taskAction modifier and the doLast {} and doFirst {} closures are executed in the same order.

task (name: hello, type: GreetingTask) {
  doFirst {
    def list = getActions()
    for (int i = 0; i < list.size(); i++) {
      println list.get(i).displayName
    }
  }

  doLast {

  }
}
Copy the code

First declare doFirst {} and doLast {} closures; If you dig into the DefaultTask source code and trace it to the top subclass AbstractTask, you can see that all actions are stored internally using actions and exposed via getAction(). Describable implements getDisplayName(), which is used to specify ContextAwareTaskAction (ContextAwareTaskAction). So you can get the name of the Action directly through displayName.

To complete the task, try it on the command line

./gradlew hello

> Task :app:hello
Execute doFirst {} action
Execute greet
Execute doLast {} action
Copy the code

Gradle automatically sets setter and getter methods for variables, so when a Gradle has a getXxx() method, you can use the XXX variable directly. If this detail is not clear, I suggest reviewing the appendix of the previous article.

Task dependencies

DependsOn Dependencies are commonly used to specify task execution order (see task Dependencies and task Ordering).

task a {
	doLast {
		println 'a'
	}
}

task b {
	dependsOn('a')
	doFirst {
		println 'b'}}Copy the code

Write the above code in the app build.gradle file. When executing task B, it will print the following message:

./gradlew task app:b

> Task :app:a

a

> Task :app:b

b

As you can see, task B will execute Task A first because task B depends on Task A.

Experienced developers who try commands like assembleDebug on the command line will see that their execution depends on many other tasks. So try it on the command line./gradlew assembleDebug to see the output.

The task of actual combat

install && launch apk

Com.android. application comes with installDebug task, developers can use installDebug to install the current project APK:

./gradlew installDebug

> Task :app:installDebug

Installing APK ‘app-debug.apk’ on ‘xxxxx’ for app:debug Installed on 1 device.

However, there seems to be some disappointment, such as the developers wanting the app to be able to launch when they install it. So how to do that?

Firstly, analyze the feasibility of the problem. — adb install -r app-debug.apk and ADB shell am start -n So the key is how to call the command line code from Gradle and how to get the package name/first Activity information.

  1. The developer’s intuition also tells us that the Gradle development documentation contains information about command line calls, and that it is only necessary to use the exec {} closure.

  2. How to get the package name/first Activity information? This can be obtained from androidmanifest.xml. As some experienced developers know, the androidmanifest.xml file that gets pushed into APK is not the androidmanifest.xml file that we normally write. But the apk compiled in Project/app/build/intermediates/manifests/full/debug/package under the AndroidManifest. XML (and, of course, if is the Release package, Should be a Project/app/build/intermediates/manifests/full/release/package).

    • The package name is the applicationId in the defaultConfig closure of the Android closure.

    • Target Activity is containing the action for android. The intent. The action. The MAIN Activity.

With that in mind, it’s easy to understand the following:

task installAndRun(dependsOn: 'assembleDebug') {
  doFirst {
    exec {
      workingDir "${buildDir}/outputs/apk/debug"
      commandLine 'adb'.'install'.'-r'.'app-debug.apk'
    }
    exec {
      def path = "${buildDir}/intermediates/manifests/full/debug/AndroidManifest.xml"// def parser = new XmlParser(false.false). The parse (new File (path)) / / under the application of each activity node parser. The application. The activity. Each {activity - > / / activity For each intent-filter node activity.'intent-filter'.each {filter - > / / intent - filter under the action of the nodes in the @ android: name contains android. The intent. The action. The MAINif (filter.action.@"android:name".contains("android.intent.action.MAIN")) {
            def targetActivity = activity.@"android:name"
            commandLine 'adb'.'shell'.'am'.'start'.'-n'."${android.defaultConfig.applicationId}/${targetActivity}"
          }
        }
      }
    }
  }
}
Copy the code
  1. To install APK, you must have an APK, so you must rely on the assembleDebug Task.

    InstallDebug task also relies on assembleDebug task

    task showInstallDepends {
      doFirst {
        println project.tasks.findByName("installDebug").dependsOn
      }
    }
    Copy the code

    ./gradlew showInstallDepends

    > Configure project :app

    [task ‘installDebug’ input files, assembleDebug]

  2. Several parameters in the exec closure are mentioned below

    2.1 workingDir: indicates the working environment. The parameter is in File format. The default is the current project directory.

    2.2 commandLine: A command that needs to be executed on the CLI. The parameters are in the List format.

  3. As mentioned in the previous article —

    In plain English, they are closures, fixed formats, and because they are fixed, tasks can read the data and do the corresponding things.

    This is nicely illustrated in line 8 of the second Exec closure, Directly obtained by {android. DefaultConfig. ApplicationId} to Gradle file android defaultConfig under the closure of the closure of applicationId values. This gives you the package name of the current application.

    Of course, in addition to Gradle being able to call the command line, Groovy is actually able to call the command line, but I won’t extend it here.

  4. As for the first start of the Activity, be sure it is the action for android. The intent. The action. The MAIN Activity, The problem then becomes how to find the Activity in androidmanifest.xml. As an experienced driver, you should be aware that Groovy provides an XML parsing API. It is left to readers to explore the growth of the source code.

  5. In addition to the above information, what else is needed? You also need to know something about gradle builds — for example, the debug package ends up in ${buildDir}/outputs/apk/debug; For example, the Androidmanifest.xml in the Debug package is not the same as the androidmanifest.xml that was written in everyday development (although it might be the same), But ${buildDir} / intermediates/manifests/full/debug AndroidManifest. XML. ${buildDir}(Build folder) is very important because Gradle builds apK with output files in this folder, so check it out.

After this, you can type the following command on the command line:

./gradlew installAndRun

> Task :app:installAndRun

[ 4%] /data/local/tmp/app-debug.apk

[ 8%] /data/local/tmp/app-debug.apk

[ 12%] /data/local/tmp/app-debug.apk …

Success

Starting: Intent {com.test.Test/TestActivity}

This completes a task authoring to install and start APK.

hook assets

The task above doesn’t seem to have much to do with the Android build process, so let’s go deeper and change the packaged file through the hook native Task implementation. Insert an image into assets during the packaging process.

Even though it doesn’t seem to work at all

In the packaging process, there is a task named packageDebug that packages files to generate APK —

Next, type the following command on the command line:

./gradlew help –task “packageDebug”

Type

PackageApplication (com.android.build.gradle.tasks.PackageApplication)

As you can see, the type of this task is PackageApplication —

Take a look at its parent, PackageAndroidArtifact:

The task has a field of type FileCollection Assets. This is the asset that is finally entered into the APK. So it’s not hard to write the following code —

task hookAssets {
  afterEvaluate {
    tasks.findByName("packageDebug").doFirst { task ->
      copy {
        from "${projectDir.absolutePath}/test.png"
        into "${task.assets.asPath}"}}}}Copy the code
  1. In the projectafterEvaluateAfter findingpackageDebug task
  2. Put one in your app directorytest.png, the use ofcopy {}Closures,fromThe entered parameter istest.pngThe path,intoThe parameter to fill in is the path to the output, i.eassetsThe path.

Can see the/app/build/intermediates/assets/debug/entities under test. The PNG

Similarly, unzip the APK file to see —

This is the end of a solid Gradle Task Hook process.

Afterword.

If Gradle Task is a function, then Gradle Plugin is a function library. In the next section, I will explain Gradle Plugin.

Of course, if you have any questions, please join my wechat group. If the QR code fails, check the end of my latest article.