Insert code block of bytecode instrumentation (javassist) | object injection of IOC framework (Hilt) ~ research

Hilt object injection

The development idea of ​​using the IOC framework is that the creation of objects is no longer new, but through the IOC container to help us realize the instantiation and assignment of objects. In this way, the creation of object instances becomes easier to manage and can reduce object coupling.
In usage scenarios, template code creates instances, and local or global objects are shared.

There are three injection methods under the IOC framework:
view injection: such as ButterKnife
parameter injection: such as Arouter
object injection: such as Dagger2, Hilt

Before Hilt is applied to the project, perform essential configuration: 1. Import plug-in
of project projectbuild.gradlegradle

dependencies {
    
    
 // Hilt
  classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha'
}

build.gradle2, and then will be introduced in the app-module module to be applied

/**build.gradle*/
apply plugin: 'dagger.hilt.android.plugin'
apply plugin: 'kotlin-kapt'

compileOptions {
    
    
    sourceCompatibility = 1.8
    targetCompatibility = 1.8
}
kotlinOptions {
    
    
    jvmTarget = "1.8"
}


dependencies {
    
    
	// Hilt
    implementation "com.google.dagger:hilt-android:2.28-alpha"
    kapt "com.google.dagger:hilt-android-compiler:2.28-alpha"
    kapt 'androidx.hilt:hilt-compiler:1.0.0-alpha01'
}

3. The next step is to use annotation configuration in the application.

@HiltAndroidApp
public class MainApplication extends Application 
@AndroidEntryPoint
class MainActivity : AppCompatActivity() 
/**  MainModule.kt  */
@Module
@InstallIn(ApplicationComponent::class)
abstract class MainModule {
    
    

//    @ActivityScoped Activity作用域内单例
    // @Singleton 全局单例
    @Binds
    @Singleton
    abstract fun bindService(impl:LogPrintServiceImpl):ILogPrintService

//    @Provides
//    fun bindService():ILogPrintService {
    
    
//        return LogPrintServiceImpl(context)
//    }
}

interface ILogPrintService {
    
    
    fun logPrint()
}

class LogPrintServiceImpl @Inject constructor(@ApplicationContext val context:Context):ILogPrintService{
    
    
    override fun logPrint() {
    
    
        Toast.makeText(context, "~IOC依赖注入-对象注入方式~", Toast.LENGTH_SHORT).show()
    }
}

Finally, when applied to the project, the object instance injected by Hilt can be obtained by using the @Inject annotation. The annotation-based dependency injection framework makes the creation of object instances easier.

/**  MainActivity.kt  */
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    
    

    @set:Inject
    var iLogPrintService:ILogPrintService?=null
    override fun onCreate(savedInstanceState: Bundle?) {
    
    
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
		iLogService?.logPrint()

    }
}

MainActivity.kt and MainApplication.java are the java code generated by Hilt, under the app/build/generated/source/kapt/debugpath
insert image description here

  • The Hilt_MainActivity.java class generated during Hilt dependency injection compilation is the entry class for object injection.
  • The Hilt_MainApplication.java class generated during Hilt dependency injection compilation is the entry class for dependency injection.
  • The annotation @HiltAndroidApp is responsible for creating the ApplicationComponent component object, which will replace the parent class (for example, Application will be replaced with Hilt_MainApplication) at compile time Hilt_***.
  • The annotation @HiltEntryPoint is responsible for creating the ActivityComponent component object, which will replace the parent class (for example, AppCompatActivity will be replaced with Hilt_MainActivity) at compile time Hilt_***.
  • Then following up on the abstract class generated by Hilt compilation, you will find that in fact, the implementation method of dagger2 is encapsulated internally to realize Hilt's dependency injection.

javassist bytecode instrumentation

Using bytecode instrumentation technology, you can insert code blocks into any method under the Activity. Because through this technology, both the source code in the project and the .class files compiled with jar (aar) can be modified.
There are three modes of custom plug-in development:

custom plugin type custom description
buildSrc Creating a module of Java or Kotlin Library will put the source code of the plug-in in the buildSrc/src/main/groovy directory, which is only visible in this project. This method is suitable for plug-in definitions with complex logic.
jar package Create an independent groovy or java project, and package the project into a jar and publish it to the hosting platform for use. A jar package can contain multiple plug-in entries~
buildscript Write the custom plug-in source code in the buildscript closure of build.gradle, which is suitable for simple definition of logic. Because the definition here is only visible to the module to which the current build.gradle belongs.

Create buildSrc module

After the module is created (delete include ':buildSrc' from the project's settings.gradle), replace the build.gradle that configures the current buildSrc

// buildSrc/build.gradle
apply plugin: 'groovy'

repositories {
    
    
    google()
    jcenter()
}
dependencies {
    
    
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    //引入android plugin.相当于使用jetpack库
    implementation 'com.android.tools.build:gradle:3.4.2'
    //gradle api,相当于android sdk
    implementation gradleApi()
    //groovy库
    implementation localGroovy()

    implementation 'org.javassist:javassist:3.27.0-GA'
}
sourceCompatibility = "1.8"
targetCompatibility = "1.8"


Afterwards, create the directory and package name of the custom gradle plug-in, and the detailed specification is as shown in the screenshot

insert image description here

Custom plugin directory Catalog description
main/groovy This level of directory is the groovy folder directory, and the next level is to create the package name. A groovy file must be created under the created package name.
main/resources The resource directory where custom plugins are registered.
META-INF/gradle-plugins Define the plugin name in this directory and register the plugin. (such as okpatch.properties, okpatch is the plugin name)

Register plugin code

implementation-class =org.bagou.xiangs.plugin.OkPatchPlugin

Customize Transform and register the Transform implementation class

package org.bagou.xiangs.plugin
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.ProjectConfigurationException

class OkPatchPlugin implements Plugin<Project> {
    
    

    @Override
    void apply(Project project) {
    
    

        // 该方法是在配置执行时就会调用的。
        if (!project.plugins.hasPlugin("com.android.application")) {
    
    
            // 如果不是主工程模块,则抛出异常
            throw new ProjectConfigurationException("plugin:com.android.application must be apply", null)
        }

        // 注册自定义的Transform实现类
        project.android.registerTransform(new OkPatchPluginTransform(project))
    }
}

Next, to customize the gradle plug- in process, only how to customize and rewrite Transform is left . In customizing and rewriting Transform, we realize how to modify the class file bytecode. After the modification and compilation are completed, import and use it in the build.gradle of the application's project module .

apply plugin: 'okpatch'

Rewrite Transform

Several classes appeared in the process of rewriting Transform , and you need to be familiar with their functions. As the following code

// 自定义类OkPatchPluginTransform,继承并重写Transform中的相关方法
// getName()、getInputTypes()、getScopes()、isIncremental()
class OkPatchPluginTransform extends Transform {
    
    
	...@Override
    void transform(@NonNull TransformInvocation transformInvocation)
            throws TransformException, InterruptedException, IOException {
    
    
		// 重写transform方法,可实现对字节码进行插桩
		
	}
}

Familiar with TransformInvocation

The formal parameter TransformInvocation of the method in the code is a very critical interface. Two methods are defined in this interface

  • Collection<TransformInput> getInputs();
  • TransformOutputProvider getOutputProvider();

The first method obtains the TransformInput interface, which is an abstraction of the input file. Which encapsulates JarInput and DirectoryInput,

  • Collection<JarInput> getJarInputs();
  • Collection<DirectoryInput> getDirectoryInputs();

The second method can obtain TransformOutputProvider , and through this class, the following results can be obtained,

  • The content location at the specified scope, content type, and format collection.
  • If the Format format value is DIRECTORY , the obtained result is the directory address where the source code file is located.
  • If the Format format value is Jar , the obtained result is the address of the directory where the jar file to be created is located.
Related classes in TransformInput illustrate
JarInput Refers to the files in all local or remote Jar packages and aar packages involved in compilation.
DirectoryInput Refers to the source code files in all directories under the current project participating in the compilation.

When inheriting and rewriting Transform, you need to specify the range of bytecode processing. That is, bytecode files can only be obtained and processed within a certain scope.

class OkPatchPluginTransform extends Transform {
    
    
	...@Override
    Set<? super QualifiedContent.Scope> getScopes() {
    
    
        // 该transform 工作的作用域
        // 源码中:Set<Scope> SCOPE_FULL_PROJECT =
        //    Sets.immutableEnumSet(
        //           Scope.PROJECT,
        //           Scope.SUB_PROJECTS,
        //           Scope.EXTERNAL_LIBRARIES);
        return TransformManager.SCOPE_FULL_PROJECT // 复合作用域,是一个Set类型
    }
	...}
scope type illustrate
PROJECT Only files under the current project are processed.
SUB_PROJECTS Only files under subprojects are processed.
EXTERNAL_LIBRARIES Only handle external dependencies.
PROVIDED_ONLY Only process dependent libraries imported in the form of provided locally or remotely.
TESTED_CODE Only test code is processed.

Inherit and rewrite the source code of the groovy file of the method in the class Transform. The implementation of the instrumentation logic will be reflected in the following source code.

A CtClass object is converted into a class file through the writeFile(), toClass(), and toBytecode() methods,
then Javassist will freeze the CtClass object to prevent the CtClass object from being modified,
because a class can only be loaded once by the JVM.

/// 自定义gradle插件,实现Transform,完成插桩功能。自定义gradle插件执行优先级先于系统gradle插件!
// OkPatchPluginTransform.groovy
package org.bagou.xiangs.plugin

import com.android.annotations.NonNull
import com.android.build.api.transform.*
import com.android.build.gradle.internal.pipeline.TransformManager
import javassist.ClassPool
import javassist.CtClass
import javassist.bytecode.ClassFile
import org.apache.commons.codec.digest.DigestUtils
import org.apache.commons.io.FileUtils
import org.apache.commons.io.IOUtils
import org.gradle.api.Project

import java.util.jar.JarEntry
import java.util.jar.JarFile
import java.util.jar.JarOutputStream
/**在实现Transform类时,使用到的类要注意导包是否正确。*/
class OkPatchPluginTransform extends Transform {
    
    

    @Override
    String getName() {
    
    
        return "OkPatchPluginTransform" // 命名不重名于其他gradle即可
    }

    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
    
    
        // 表示接收到的输入数据类型
        return TransformManager.CONTENT_CLASS
    }

    @Override
    Set<? super QualifiedContent.Scope> getScopes() {
    
    
        // 该transform 工作的作用域
        return TransformManager.SCOPE_FULL_PROJECT
    }

    @Override
    boolean isIncremental() {
    
    
        // 是否增量变编译
        return false
    }

    private classPool = ClassPool.getDefault()
    OkPatchPluginTransform(Project project){
    
    
        // 将android.jar包添加到classPool中,以便能直接找到android相关的类
        classPool.appendClassPath(project.android.bootClasspath[0].toString())

		// 通过importPackage方式,以便由classPool.get(包名)直接获取实例对象
		// 且通过这种方式,相当于一次导包。在后面若要构建类,可免于写全类名
        classPool.importPackage("android.os.Bundle")
        classPool.importPackage("android.widget.Toast")
        classPool.importPackage("android.app.Activity")
        classPool.importPackage("android.util.Log")

    }


    @Override
    void transform(@NonNull TransformInvocation transformInvocation)
            throws TransformException, InterruptedException, IOException {
    
    
    	// 向工程中所有Activity的onCreate方法中打桩插入一段代码
        // 对项目中参与编译的.class,以及jar中的.class都做插桩处理
        def outputProvider = transformInvocation.outputProvider
		// transformInvocation.inputs,返回transform输入或输出的TransformInput容器
		// 然后通过TransformInput容器的迭代遍历,得到TransformInput实例。
		// 接下来可由TransformInput实例获得DIRECTORY和JAR格式的输入文件集合
        transformInvocation.inputs.each {
    
    _inputs->
            // 对_inputs中directory目录下的class进行遍历「DIRECTORY格式的输入文件集合」
            _inputs.directoryInputs.each {
    
     directory->
                handleDirectoryInputs(directory.file)
                def dest = outputProvider.getContentLocation(
                        directory.name, directory.contentTypes,
                        directory.scopes, Format.DIRECTORY
                )
				// 将修改过的字节码文件拷贝到原源码所在目录
                FileUtils.copyDirectory(directory.file, dest)
            }

            // 对_inputs中jar包下的class进行遍历「JAR格式的输入文件集合」
            _inputs.jarInputs.each {
    
    jar->
                def jarOutputFile = handleJarInputs(jar.file)
                def jarName = jar.name
                def md5 = DigestUtils.md5Hex(jar.file.absolutePath)
                if (jarName.endsWith(".jar")) {
    
    
                    jarName = jarName.substring(0,jarName.length()-4)
                }
                def dest = outputProvider.getContentLocation(
                        md5+jarName,jar.contentTypes,
                        jar.scopes,Format.JAR
                )
                // 将修改过的字节码文件拷贝到和原jar同级别所在目录
                FileUtils.copyFile(jarOutputFile, dest)

            }
        }

        classPool.clearImportedPackages()
    }

    // 处理 directory目录下的class文件
    void handleDirectoryInputs(File fileDir) {
    
    
        // required: 添加file地址到classPool
        classPool.appendClassPath(fileDir.absolutePath)
        if (fileDir.isDirectory()) {
    
     // 如果fileDir是文件目录
            fileDir.eachFileRecurse {
    
    file->
                def filePath = file.absolutePath
                if (ifModifyNeed(filePath)) {
    
    //判断是否满足class修改条件
                    // 为兼容jar包下class修改共用方法modifyClass(**),将file转化为FileInputStream
                    FileInputStream fis = new FileInputStream(file)
                    def ctClass = modifyClass(fis)
                    ctClass.writeFile(fileDir.name) // 修改完成后再写回去
                    ctClass.detach()
                }
            }
        }
    }

    // 处理 jar包下的class文件
    File handleJarInputs(File file) {
    
    
        // required: 添加file地址到classPool
        classPool.appendClassPath(file.absolutePath)

        JarFile jarInputFile = new JarFile(file) // 经过JarFile转换后,可获取jar包中子文件
        def entryFiles = jarInputFile.entries()


        File jarOutputFile = new File(file.parentFile, "temp_"+file.name)
        if (jarOutputFile.exists()) jarOutputFile.delete()
        JarOutputStream jarOutputStream = new JarOutputStream(new BufferedOutputStream(new FileOutputStream(jarOutputFile)))

        while (entryFiles.hasMoreElements()) {
    
    
            def nextEle = entryFiles.nextElement()
            def nextEleName = nextEle.name

            def jarEntry = new JarEntry(nextEleName)
            jarOutputStream.putNextEntry(jarEntry)

            def jarInputStream = jarInputFile.getInputStream(nextEle)
            if (!ifModifyNeed(nextEleName)) {
    
    //判断是否满足class修改条件
               jarOutputStream.write(IOUtils.toByteArray(jarInputStream))
                jarInputStream.close()
                continue
            }
            println('before....handleJarInputs-modifyClass')
            CtClass ctClass = modifyClass(jarInputStream)
            def bytecode = ctClass.toBytecode()
            ctClass.detach()
            jarInputStream.close()

            jarOutputStream.write(bytecode)
            jarOutputStream.flush()

        }

        jarInputFile.close()
        jarOutputStream.closeEntry()
        jarOutputStream.flush()
        jarOutputStream.close()
        return jarOutputFile

    }


    // class文件处理方法-共用
    CtClass modifyClass(InputStream fis) {
    
    
        // 通过输入流 获取 javassist 中的CtClass对象
        ClassFile classFile = new ClassFile(new DataInputStream(new BufferedInputStream(fis)))
        def ctClass = classPool.get(classFile.name)
        // 一个CtClass对象通过writeFile()、toClass()、toBytecode()方法被转换成class文件,
        // 那么Javassist就会将CtClass对象冻结起来,防止该CtClass对象被修改。
        if (ctClass.isFrozen())ctClass.defrost()

        // 开始执行修改逻辑
        // onCreate方法的参数 override fun onCreate(savedInstanceState: Bundle?)
        def bundle = classPool.get("android.os.Bundle")//获取到onCreate方法参数
        println(bundle)
        CtClass[] params = Arrays.asList(bundle).toArray() // 转化为反射入参数组

        def method = ctClass.getDeclaredMethod("onCreate", params)
        def message = "字节码插桩内容:"+classFile.name
        println('字节码插桩内容:'+message)
        method.insertBefore("android.widget.Toast.makeText(this, "+"\""+ message +"\""+", android.widget.Toast.LENGTH_SHORT).show();")//给每个方法的最后一行添加代码行
        method.insertAfter("Log.d(\"MainActivity\", \"override fun onCreate方法后......\");")//给每个方法的最后一行添加代码行
        return ctClass
    }

    boolean ifModifyNeed(String filePath) {
    
    
        return (
                filePath.contains("org/bagou/xiangs")
                        && filePath.endsWith("Activity.class")
                        && !filePath.contains("R.class")
                        && !filePath.contains('$')
                        && !filePath.contains('R$')
                        && !filePath.contains("BuildConfig.class")
        )
    }

}

Javassist bytecode processing classic reference articles

encountered an error

I have a problem when trying to use 'org.javassist:javassist:3.27.0-GA' for a custom gradle plugin.

FileSystemException

The gradle plugin in the current error project
is: classpath 'com.android.tools.build:gradle:3.4.2'
The gradle version is:distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip

Caused by: java.util.concurrent.ExecutionException: 
java.nio.file.FileSystemException: 
D:\android-studio\包名\build\intermediates\runtime_library_classes\debug\classes.jar: 
另一个程序正在使用此文件,进程无法访问。

solution

Delete (terminate) the process that occupies the classes.jar file. When an error is reported, there are three java.exe processes in the detailed information of the screenshot of the task management page. Then delete them all, and rebuild the project. After the build is successful, two java.exe processes are displayed.
insert image description here

In the IOC framework, when using DI (Dependency Injection), Hilt performs object injection.

There are three injection methods under the IOC framework:
view injection: such as ButterKnife
parameter injection: such as Arouter
object injection: such as Dagger2, Hilt

Hilt NoClassDefFoundError

The gradle plugin in the current error project
is: classpath 'com.android.tools.build:gradle:3.4.2'
The gradle version is:distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip

2022-05-18 09:40:42.941 27105-27105/? E/AndroidRuntime: FATAL EXCEPTION: main
    Process: 包名, PID: 27105
    java.lang.NoClassDefFoundError: Failed resolution of: Lorg/包名/MainActivity_GeneratedInjector;
        at 包名.Hilt_MainActivity.inject(Hilt_MainActivity.java:53)
        包名.Hilt_MainActivity.onCreate(Hilt_MainActivity.java:28)
        at 包名.MainActivity.onCreate(MainActivity.java:32)

solution

Locally modify the current gradle version number to distributionUrl=file:///C:/Users/Administrator/.gradle/wrapper/dists/gradle-6.4.1-all.zip
(this error occurs when gradle is not completely downloaded) or use vpn to execute the build downloaddistributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip

Then please confirm the following configurations of Settings and Project Structure under your own AS ,

insert image description here
insert image description here

GradleException: ‘buildSrc’

* Exception is:
org.gradle.api.GradleException: 'buildSrc' cannot be used as a project name as it is a reserved name
	at org.gradle.initialization.DefaultSettingsLoader.lambda$validate$0(DefaultSettingsLoader.java:146)
	at org.gradle.initialization.DefaultSettingsLoader.validate(DefaultSettingsLoader.java:142)
	at org.gradle.initialization.DefaultSettingsLoader.findSettingsAndLoadIfAppropriate

solution

When creating the buildSrc module, ide will automatically introduce the module into settings.gradle, so the above error will be reported. Just delete include ':buildSrc' in settings.gradle.

Android studio Connection refused: connect

solution

The first step is to turn off the proxy of AS and select no proxy.

insert image description here
The second step is to delete .gradlethe directory gradle.propertiesand rebuild. That's it~
insert image description here
When the proxy is configured intentionally or unintentionally, AS will (I am under the default installation directory/.gradle here) generate a proxy file, and then the studio will read the file every time it compiles.

Groovy language compared with Java

  • The Groovy language is a dynamic language based on the JVM virtual machine, Java is a static language, and Groovy is fully compatible with Java.
  • The Groovy def keyword, the def keyword is used to define untyped variables or functions with dynamic return types in Groovy.
  • The semicolon is not required in Groovy syntax (this feature is the same as kotlin), but the Java semicolon is required.
  • Both single quotes and double quotes can define a string in Groovy syntax. Single quotes cannot perform operations on expressions in strings, but double quotes can. Java single quotes define characters and double quotes define strings.
  • The Groovy language declares a List collection using square brackets, and Java declares the List collection using curly braces. ( Groovy访问元素list[0]如范围索引1..3,-1表示右侧第一个等, Java access element list.get(0))
  • The Groovy language uses square brackets ( ) when declaring a map 访问map[key]、map.key,遍历map.each{item->...}, and Java uses curly braces.
  • When Groovy syntax calls a method, the parentheses can be left out. Java is required.
  • The return of Groovy grammar is not necessary, this is the same as kotlin.
  • The closure of Groovy syntax has something to say (same as kotlin)
/** Groovy闭包的演变过程 */
def closureMethod () {
     
     
	def list = [1,2,3,4,5,6]
	// 呆板写法
	list.each({
     
     println it}) 
	list.each({
     
      // 格式化下
	    println it
	})

	// 演进 - Groovy规定,若方法中最后一个参数是闭包,可放到方法外面
	list.each(){
     
     
		println it
	}

	// 再次演变 - 方法后的括号能省略
	list.each{
     
     
		println it
	}
}

(In the gradle file) When the Groovy language defines a task, (script is code, code is also script)

// build.gradle
// 每个任务task,都是project的一个属性
task customTask1 {
     
     
	doFirst {
     
     
		println 'customTask1方法第一个执行到的方法'
		def date = new Date()
		def datef = date.format('yyy-MM-dd)
		println "脚本即代码,代码也是脚本。当记得这一点才能时刻使用Groovy、Java和Gradle的任何语法和API来完成你想要做的事情。像这里,当前已格式化的日期:${datef}"
	}
	doLast {
     
     
		println 'customTask1方法最后一个执行到的方法'
	}
}

tasks.create ('customTask2') {
     
     
	doFirst {
     
     
		println 'customTask2方法第一个执行到的方法'
	}
	doLast {
     
     
		println 'customTask2方法最后一个执行到的方法'
	}
}
// 通过任务名称访问方法(其实就是动态赋一个新的原子方法)
customTask2.doFirst {
     
     
	print '查看在project中是否有task=customTask2 = '
	println project.hasProperty('customTask2')
}

(In the gradle file) There are about 5 ways for Groovy to create tasks


/**我们创建Task任务都会成为Project的一个属性,属性名就是任务名*/
task newOwnerTask5
// 扩展任务属性
newOwnerTask5.description = '扩展任务属性-描述'
newOwnerTask5.group = BasePlugin.BUILD_GROUP
newOwnerTask5.doFirst {
     
     
	println "我们创建Task任务都会成为Project的一个属性,属性名就是任务名"
}
tasks['newOwnerTask5'].doFirst {
     
     
	println "任务都是通过TaskContanier创建的,TaskContanier是我们创建任务的集合,在Project中我们可以用通过tasks属性访问TaskContanier。所以可以以访问集合元素方式访问已创建的任务。"
}
// 第一种:直接以一个任务名称,作为创建任务的方式
def Task newOwnerTask1 = task(newOwnerTask1)
newOwnerTask1.doFirst {
     
     
	println '创建方法的原型为:Task task(String name) throws InvalidUserDataException'
}

// 第二种:以一个任务名+一个对该任务配置的map对象来创建任务 [和第一种大同小异]
def Task newOwnerTask2 = task(newOwnerTask2, group:BasePlugin.BUILD_GROUP)
newOwnerTask2.doFirst {
     
     
	println '创建方法的原型为:Task task(String name, Map<String,?> arg) throws InvalidUserDataException'
	println "任务分组:${newOwnerTask2.group}"
}
// 第三种:以一个任务名+闭包配置
task newOwnerTask3 {
     
     
	description '演示任务的创建'
	doFirst {
     
     
		println "任务的描述:${description}"
		println "创建方法的原型:Task task(String name, Closure closure)"
	}
}
// 第四种:tasks是Project对象的属性,其类型是TaskContainer,
// 因此下面的创建方式tasks可替换为TaskContainer来创建task任务
//【这种创建方式,发生在Project对象源码中创建任务对象】
tasks.create("newOwnerTask4") {
     
     
	description '演示任务的创建'
	doFirst {
     
     
		println "任务的描述:${description}"
		println "创建方法的原型:Task create(String name, Closure closure) throws InvalidUserDataException"
	}
}
// 白送一种任务创建形式
task (helloFlag).doLast {
     
     
	println '<< 作为操作符,在gradle的Task上是doLast方法的短标记形式'
}

Guess you like

Origin blog.csdn.net/u012827205/article/details/124901269