字节码插桩(javassist)之插入代码块|IOC框架(Hilt)之对象注入~研究

Hilt对象注入

使用IOC框架的开发思想就是,创建对象不再new,而是通过IOC容器帮助我们来实现对象的实例化并赋值使用。这样对象实例的创建变的容易管理,并能降低对象耦合度。
使用场景上,模板代码创建实例,局部或全局对象共享。

IOC框架下有三种注入方式:
view注入: 如ButterKnife
参数注入: 如Arouter
对象注入: 如Dagger2,Hilt

在Hilt应用到项目前,进行必不可少的配置:
1,project工程的build.gradle引入gradle插件

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

2,然后在将要应用到的app-module模块中将build.gradle引入

/**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,接下来是应用程序中使用注解配置。

@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()
    }
}

最后,应用到项目中,使用@Inject注解即可获得由Hilt注入的对象实例,基于注解的依赖注入框架,使得对象实例的创建更为简单.

/**  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和MainApplication.java由Hilt生成的java代码,在路径app/build/generated/source/kapt/debug下面
在这里插入图片描述

  • 在Hilt依赖注入编译时生成的Hilt_MainActivity.java类,是对象注入的入口类。
  • 在Hilt依赖注入编译时生成的Hilt_MainApplication.java类,是依赖注入的入口类。
  • 注解@HiltAndroidApp负责创建ApplicationComponent组件对象,在编译时会将父类(如这里会将Application替换成Hilt_MainApplication)替换成Hilt_***
  • 注解@HiltEntryPoint负责创建ActivityComponent组件对象,在编译时会将父类(如这里会将AppCompatActivity替换成Hilt_MainActivity)替换成Hilt_***
  • 然后跟进Hilt编译生成的抽象类中会发现,其实内部则是封装了dagger2的实现方式,来实现Hilt的依赖注入。

javassist字节码插桩

使用字节码插桩的技术,可以向Activity下任何方法中插入代码块。因为通过该技术,工程内源码和以jar(aar)参与编译之后的.class文件都能够被修改。
自定义插件开发有以下三种模式:

自定义插件类型 自定义说明
buildSrc 创建Java or Kotlin Library的module,会将插件的源码放到buildSrc/src/main/groovy目录下,且仅在本工程中可见。该方式适用于逻辑较复杂的插件定义。
jar包 创建独立的groovy或java项目,并把项目打包成jar发布到托管平台,以供使用。一个jar包可包含多个插件入口~
buildscript 将自定义的插件源码写在build.gradle的buildscript闭包中,适用于逻辑简单定义。因为定义在这里仅对当前build.gradle所属module可见。

创建buildSrc的module

module创建完成后(从工程的settings.gradle中删除include ‘:buildSrc’),替换配置当前buildSrc的build.gradle

// 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"


之后新建自定义gradle插件的目录、包名,详尽规范如截图

在这里插入图片描述

自定义插件目录 目录说明
main/groovy 这一级目录是groovy文件夹目录,下一级则是创建包名 。在已创建包名下必须创建groovy文件。
main/resources 自定义插件注册所在的资源目录。
META-INF/gradle-plugins 在该目录下定义插件名称,并注册插件。(如okpatch.properties,okpatch是插件名称)

注册插件代码

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

自定义Transform并注册该Transform实现类

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))
    }
}

接下来自定义gradle插件过程,就只剩下了如何自定义重写Transform。在自定义并重写Transform中,即是我们实现如何修改class文件字节码。修改且编译完成后,在应用的项目模块的build.gradle中引入并使用。

apply plugin: 'okpatch'

重写Transform

重写Transform过程中出现了几个类,需要熟悉他们的作用。如下面代码

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

熟悉TransformInvocation

代码中方法的形参TransformInvocation是非常关键的接口。该接口中定义了两个方法

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

第一个方法可获得TransformInput接口,它是对输入文件的一个抽象。其中封装了JarInput和DirectoryInput,

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

第二个方法可获得TransformOutputProvider,并通过该类可获得如下结果,

  • 在已指定范围、内容类型和格式集合的内容位置。
  • 如果Format格式值是DIRECTORY,则获得的结果是源码文件所在的目录地址。
  • 如果Format格式值是Jar,则获得的结果是要创建的jar文件所在的目录地址。
TransformInput中相关类 说明
JarInput 指的是参与编译的所有本地或者远程Jar包和aar包中文件。
DirectoryInput 指的是参与编译的当前工程下的所有目录下的源码文件。

在继承重写Transform时,需要指定处理字节码的范围。即只能在某个作用域内获得并处理字节码文件。

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类型
    }
	...}
作用域类型 说明
PROJECT 仅处理当前项目下的文件。
SUB_PROJECTS 仅处理子项目下的文件。
EXTERNAL_LIBRARIES 仅处理外部的依赖库。
PROVIDED_ONLY 仅处理本地或远程以provided形式引入的依赖库。
TESTED_CODE 仅处理测试代码。

继承并重写类Transform中方法的groovy文件源码,对于插桩逻辑的实现将体现在下面源码中,

一个CtClass对象通过writeFile()、toClass()、toBytecode()方法被转换成class文件,
那么Javassist就会将CtClass对象冻结起来,防止该CtClass对象被修改,
因为一个类只能被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字节码处理经典使用参考文章

遇到报错

在尝试使用’org.javassist:javassist:3.27.0-GA’进行自定义gradle插件时,遇到问题。

FileSystemException

当前报错的项目工程中
gradle插件是:classpath 'com.android.tools.build:gradle:3.4.2'
gradle版本是 :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: 
另一个程序正在使用此文件,进程无法访问。

解决方案

删除(终止)占用该classes.jar文件的进程。报错时,在任务管理页面截图的详细信息中java.exe进程有三个。然后全部删除,并重新构建工程,构建成功后显示两个java.exe进程。
在这里插入图片描述

在IOC框架研究使用DI(依赖注入),Hilt 进行对象注入时。

IOC框架下有三种注入方式:
view注入: 如ButterKnife
参数注入: 如Arouter
对象注入: 如Dagger2,Hilt

Hilt NoClassDefFoundError

当前报错的项目工程中
gradle插件是:classpath 'com.android.tools.build:gradle:3.4.2'
gradle版本是 :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)

解决方案

本地修改当前gradle版本号到distributionUrl=file:///C:/Users/Administrator/.gradle/wrapper/dists/gradle-6.4.1-all.zip
(这个报错出现在gradle未下载完全导致)或者使用vpn执行构建下载distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip

然后请确认以下自己AS下Settings和Project Structure的配置,

在这里插入图片描述
在这里插入图片描述

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

解决方案

在创建buildSrc该module时,ide会自动将该module引入到setttings.gradle中,因此会报上面错误。在settings.gradle中删除include ':buildSrc’即可。

Android studio Connection refused: connect

解决方案

第一步关掉AS的代理,选中no proxy。

在这里插入图片描述
第二步删除.gradle目录下的gradle.properties,并重新构建。即可~
在这里插入图片描述
当有意无意中配置过一次代理后,AS就会(我这里是默认的安装目录/.gradle下)生成一个代理文件,而后studio每次编译都会去读取该文件。

Groovy语言与Java相较

  • Groovy语言是基于JVM虚拟机的动态语言,Java是静态语言,Groovy完全兼容Java。
  • Groovy def关键字,def关键字用于定义Groovy中的无类型变量或动态返回类型的函数。
  • Groovy语法上分号不是必须的(该特点和kotlin一样),Java分号是必须的。
  • Groovy语法上单引号和双引号都能定义一个字符串,单引号不能对字符串中表达式做运算,双引号可以。Java单引号定义字符,双引号定义字符串。
  • Groovy语言声明一个List集合使用中括号,Java声明List集合使用大括号。(Groovy访问元素list[0]如范围索引1..3,-1表示右侧第一个等,Java访问元素list.get(0))
  • Groovy语言在声明map时使用中括号(访问map[key]、map.key,遍历map.each{item->...}),Java使用大括号。
  • Groovy语法在执行调用一个方法时,括号可以不写。Java则是必须的。
  • Groovy语法的return不是必须的,这个和kotlin一样。
  • Groovy语法的闭包有话说(与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
	}
}

(在gradle文件中)Groovy语言在定义一个任务时,(脚本即代码,代码也是脚本)

// 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')
}

(在gradle文件中)Groovy创建任务的方式大概有5种


/**我们创建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方法的短标记形式'
}

猜你喜欢

转载自blog.csdn.net/u012827205/article/details/124901269