Hilt对象注入 | javassist插桩 研究
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")
)
}
}
遇到报错
在尝试使用’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方法的短标记形式'
}