• 字节码打桩插入代码块|IOC框架之对象注入~研究


    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'
    }
    
    • 1
    • 2
    • 3
    • 4

    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'
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    3,接下来是应用程序中使用注解配置。

    @HiltAndroidApp
    public class MainApplication extends Application 
    
    • 1
    • 2
    @AndroidEntryPoint
    class MainActivity : AppCompatActivity() 
    
    • 1
    • 2
    /**  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()
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    最后,应用到项目中,使用@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()
    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    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"
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

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

    在这里插入图片描述

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

    注册插件代码

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

    自定义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))
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

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

    apply plugin: 'okpatch'
    
    • 1

    重写Transform

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

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

    熟悉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类型
        }
    	...}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    作用域类型说明
    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")
            )
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200

    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: 
    另一个程序正在使用此文件,进程无法访问。
    
    • 1
    • 2
    • 3
    • 4

    解决方案

    删除(终止)占用该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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    解决方案

    本地修改当前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
    
    • 1
    • 2
    • 3
    • 4
    • 5

    解决方案

    在创建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
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    (在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')
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    (在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方法的短标记形式'
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
  • 相关阅读:
    Oracle 中的索引
    解决navicat连接oracle19c数据库缺少oci.dll
    Python在地球科学领域中的数据处理、科学计算、数学建模、数据挖掘和数据可视化
    SpringBoot和SpringCloud的区别,使用微服务的好处和缺点
    怎么选择好的游戏平台开发商?
    “蔚来杯“2022牛客暑期多校训练营2
    C# 二十年语法变迁之 C# 7参考
    实战攻防演练-Linux写入ssh密钥,利用密钥登录
    学习linux从0到工程师(命令)-4
    NFT营销如何赋能品牌破圈增长?
  • 原文地址:https://blog.csdn.net/u012827205/article/details/124901269