本博文是demo源码,详情参考下面链接
手把手教你实现一个 Gradle Tansform 实例
一文学会Android Gradle Transform基础使用
Gradle系列 (上篇) —Android自定义Gradle插件并在项目中使用
Gradle系列 (中篇) —在自定义Gradle插件中使用javassist往class中注入代码
plugins {
id 'groovy'
id 'maven-publish'
}
dependencies {
implementation gradleApi() //gradle sdk
implementation localGroovy() //groovy sdk
implementation 'com.android.tools.build:gradle:7.1.3'
implementation 'commons-codec:commons-codec:1.15'
implementation 'commons-io:commons-io:2.11.0'
implementation 'org.javassist:javassist:3.29.0-GA'
}
publishing {
publications {
myLibrary(MavenPublication) {
groupId = 'com.df' //groupId ,自行定义,组织名或公司名
artifactId = 'df.android' //artifactId,自行定义,项目名或模块名
version = '1.0.0' //插件版本号
from components.java
}
}
repositories {
// 本地仓库位于USER_HOME/.m2/repository
mavenLocal()
// 其他maven仓库
// maven { url uri('/Users/h__d/Desktop/1') }
}
}
implementation-class=com.jokerwan.demo.plugin.JokerWanPlugin
package com.jokerwan.demo.plugin
import com.android.build.gradle.AppExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
class JokerWanPlugin implements Plugin<Project> {
void apply(Project project) {
AppExtension appExtension = project.extensions.findByType(AppExtension.class)
appExtension.registerTransform(new JokerWanTransform(project))
// project.android.registerTransform(new JokerWanTransform(project))
}
}
package com.jokerwan.demo.plugin
import com.android.build.api.transform.*
import com.android.build.gradle.internal.pipeline.TransformManager
import org.apache.commons.codec.digest.DigestUtils
import org.apache.commons.io.FileUtils
import org.gradle.api.Project
class JokerWanTransform extends Transform {
private static Project project
private static final String NAME = "JokerWanAutoTrack"
JokerWanTransform(Project project) {
this.project = project
}
@Override
String getName() {
return NAME
}
/**
* 需要处理的数据类型,有两种枚举类型
* CLASSES 代表处理的 java 的 class 文件,RESOURCES 代表要处理 java 的资源
* @return
*/
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}
/**
* 指 Transform 要操作内容的范围,官方文档 Scope 有 7 种类型:
* 1. EXTERNAL_LIBRARIES 只有外部库
* 2. PROJECT 只有项目内容
* 3. PROJECT_LOCAL_DEPS 只有项目的本地依赖(本地jar)
* 4. PROVIDED_ONLY 只提供本地或远程依赖项
* 5. SUB_PROJECTS 只有子项目。
* 6. SUB_PROJECTS_LOCAL_DEPS 只有子项目的本地依赖项(本地jar)。
* 7. TESTED_CODE 由当前变量(包括依赖项)测试的代码
* @return
*/
@Override
Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT
}
@Override
boolean isIncremental() {
return false
}
static void printCopyRight() {
println()
println("******************************************************************************")
println("****** ******")
println("****** 欢迎使用 JokerWanTransform 编译插件 ******")
println("****** ******")
println("******************************************************************************")
println()
}
@Override
void transform(TransformInvocation transformInvocation) throws TransformException,
InterruptedException, IOException {
printCopyRight()
TransformOutputProvider outputProvider = transformInvocation.getOutputProvider()
// Transform 的 inputs 有两种类型,一种是目录,一种是 jar 包,要分开遍历
transformInvocation.inputs.each { TransformInput input ->
//遍历jar文件 对jar不操作,但是要输出到out路径
input.jarInputs.each { JarInput jarInput ->
// 处理jar
processJarInput(jarInput, outputProvider)
}
//遍历文件夹
input.directoryInputs.each { DirectoryInput directoryInput ->
// 处理源码文件
processDirectoryInput(directoryInput, outputProvider)
}
}
}
void processJarInput(JarInput jarInput, TransformOutputProvider outputProvider) {
// 重命名输出文件(同目录copyFile会冲突)
def jarName = jarInput.name
def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
if (jarName.endsWith(".jar")) {
jarName = jarName.substring(0, jarName.length() - 4)
}
File dest = outputProvider.getContentLocation(
jarName + md5Name,
jarInput.getContentTypes(),
jarInput.getScopes(),
Format.JAR
)
println("*****************")
println(" jarInput: " + jarInput.file.absolutePath + " ")
println(" jarName: " + jarName + " ")
println(" dest: " + dest.absolutePath + " ")
println("*****************")
// TODO do some transform
// 将 input 的目录复制到 output 指定目录
FileUtils.copyFile(jarInput.getFile(), dest)
}
void processDirectoryInput(DirectoryInput directoryInput, TransformOutputProvider outputProvider) {
String path = directoryInput.file.absolutePath
println("[processDirectoryInput] Begin to inject: $path")
// 执行注入逻辑
InjectByJavassit.inject(path, project)
// 获取output目录
File dest = outputProvider.getContentLocation(
directoryInput.getName(),
directoryInput.getContentTypes(),
directoryInput.getScopes(),
Format.DIRECTORY
)
println("[processDirectoryInput] Begin to inject: ${dest.absolutePath}")
// 将 input 的目录复制到 output 指定目录
FileUtils.copyDirectory(directoryInput.getFile(), dest)
}
}
import javassist.ClassPool
import javassist.CtClass
import javassist.CtMethod
import org.gradle.api.Project
class InjectByJavassit {
static void inject(String path, Project project) {
try {
File dir = new File(path)
if (dir.isDirectory()) {
dir.eachFileRecurse { File file ->
if (file.name.endsWith('Activity.class')) {
doInject(project, file, path)
}
}
}
} catch (Exception e) {
e.printStackTrace()
}
}
private static void doInject(Project project, File clsFile, String originPath) {
println("[Inject] DoInject clsFile: $clsFile.absolutePath")
println("[Inject] DoInject originPath: $originPath")
String cls = new File(originPath).relativePath(clsFile).replace('/', '.')
cls = cls.substring(0, cls.lastIndexOf('.class'))
println("[Inject] Cls: $cls")
ClassPool pool = ClassPool.getDefault()
// 将当前路径加入类池,不然找不到这个类
pool.appendClassPath(originPath)
// project.android.bootClasspath 加入android.jar,不然找不到android相关的所有类
// 为了能找到android相关的所有类,添加project.android.bootClasspath 加入android.jar,
pool.appendClassPath(project.android.bootClasspath[0].toString())
// 引入android.os.Bundle包,因为onCreate方法参数有Bundle
pool.importPackage('android.os.Bundle')
CtClass ctClass = pool.getCtClass(cls)
// 解冻
if (ctClass.isFrozen()) {
ctClass.defrost()
}
// 获取方法
CtMethod ctMethod = ctClass.getDeclaredMethod('onCreate')
String toastStr = 'android.widget.Toast.makeText(this, "I am the injected code", android.widget.Toast.LENGTH_SHORT).show();'
// 方法尾插入
ctMethod.insertAfter(toastStr) // 在方法开始注入代码
// ctMethod.insertAfter(injectCode) // 在方法结尾注入代码
// ctMethod.insertAt(18, injectCode) // 在class文件的某一行插入代码,前提是class包含行号信息
ctClass.writeFile(originPath)// 根据CtClass生成.class文件;
/**
* 将该class从ClassPool中删除
*
* ClassPool 会在内存中维护所有被它创建过的 CtClass,当 CtClass 数量过多时,会占用大量的内存,
* API中给出的解决方案是 有意识的调用CtClass的detach()方法以释放内存。
*/
ctClass.detach()
}
}
pluginManagement {
repositories {
mavenCentral()
gradlePluginPortal()
google()
mavenLocal() // 本地库
}
}
dependencyResolutionManagement {
// repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "TransformDemo"
include ':app'
include ':plugin'
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
dependencies {
classpath "com.df:df.android:1.0.0"
}
}
plugins {
id 'com.android.application' version '7.2.1' apply false
id 'com.android.library' version '7.2.1' apply false
}
task clean(type: Delete) {
delete rootProject.buildDir
}
plugins {
id 'com.android.application'
id 'com.jokerwan.android'
}
android {
compileSdk 32
defaultConfig {
applicationId "com.df.transformdemo"
minSdk 21
targetSdk 32
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'com.google.android.material:material:1.6.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}