整理一下 Transform,仅供参考
注册 Transform
首先是注册 Transform appExtension.registerTransform
class PrimerPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
mPrimerTransform = new PrimerTransform()
if(isAppExtension(project)){
def appExtension = project.extensions.getByType(AppExtension)
appExtension.registerTransform(mPrimerTransform)
}
}
//当前是应用程序构建扩展(应用程序-可运行模块项目),区别于 LibraryExtension 库项目
boolean isAppExtension(Project project) {
try {
project.extensions.getByType(AppExtension)
} catch (Exception e) {
return false
}
return true
}
}
确定 Transform
Transform 有几个重要方法需要重写
- 明确构建过程处理的输入是什么
- 确定处理范围是什么
- 以及最重要的如何处理
其中关键是 transform 方法的重写
class PrimerTransform extends Transform {
PrimerTransform() {
}
//给当前 Transform 起一个响亮的名字
@Override
String getName() {
return PrimerTransform.class.getSimpleName()
}
/*
* 处理文件的类型
*
* CONTENT_CLASS: class 文件
* CONTENT_JARS:jar 文件
* CONTENT_RESOURCES:资源文件
* CONTENT_NATIVE_LIBS:so 库文件
* CONTENT_DEX:dex 文件
*/
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}
/***
* 作用范围
*
* PROJECT: 主项目
* SUB_PROJECTS: 子项目
* EXTERNAL_LIBRARIES: 外部库
*
*/
@Override
Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT
}
//是否增量编译,true:只处理上一次构建更改的文件,一定程度提高构建速度
@Override
boolean isIncremental() {
return false
}
//明确如何处理?处理什么? 一般使用 invocation.getInputs 获取所有参与构建的输入
@Override
void transform(TransformInvocation invocation) throws IOException {
TransformOutputProvider outputProvider = invocation.getOutputProvider()
//处理上一个 transform 的所有输入,包括目录和 jar 文件
invocation.getInputs().forEach(new Consumer<TransformInput>() {
@Override
void accept(TransformInput transformInput) {
//处理所有 jar
transformInput.getJarInputs().forEach(new Consumer<JarInput>() {
@Override
void accept(JarInput jarInput) {
//后去输出文件,对 jar 文件处理完毕之后需要把最终结果输出到该文件
File destFile = outputProvider.getContentLocation(jarInput.getName(),
jarInput.getContentTypes(),
jarInput.getScopes(),
Format.JAR)
try {
processJarFile(jarInput, destFile)
} catch (Exception e) {
CmdColorPrintUtils.outputRed("check plugin transform getInputs error = " + e)
}
}
})
//处理所有目录(目录下包含 class 文件)
transformInput.getDirectoryInputs().forEach(new Consumer<DirectoryInput>() {
@Override
void accept(DirectoryInput directoryInput) {
//后去输出文件,对 jar 文件处理完毕之后需要把最终结果输出到该文件
File destDir = outputProvider.getContentLocation(directoryInput.getName(),
directoryInput.getContentTypes(),
directoryInput.getScopes(),
Format.DIRECTORY)
//计算相对路径
Path sourcePath = directoryInput.getFile().toPath()
Path destPath = destDir.toPath()
try {
//遍历输入目录,
Files.walk(sourcePath)
.sorted(Comparator.naturalOrder())//按照自然顺序,也可以倒序
.forEach {
Path relativePath = sourcePath.relativize(it)
Path targetPath = destPath.resolve(relativePath)
//是目录,在该 Transform 路径下创建同名目录,否则进一步处理文件
if (Files.isDirectory(it)) {
Files.createDirectories(targetPath)
} else if (Files.isRegularFile(it)) {
processDirClassFile(it.toFile(), targetPath.toFile())
}
}
} catch (IOException e) {
CmdColorPrintUtils.outputRed("check plugin transform getDirectoryInputs error = " + e)
}
}
})
}
})
}
}
处理目录下的 class:
transformInput.getDirectoryInputs()
/**
*
* @param rawClassFile 目录下的 class 文件
* @param outputFile 下一个 transform 的输出文件
*/
private void processDirClassFile(File rawClassFile, File outputFile) {
//确保输出路径已创建,否则目录不存在导致下面文件创建失败
def parentFile = outputFile.getParentFile()
if (parentFile.isDirectory() && !parentFile.exists()) {
parentFile.mkdirs()
}
def rawInputStream = new FileInputStream(rawClassFile)
byte[] originBytes = IOUtils.toByteArray(rawInputStream)
//只处理 class 字节码文件,非 class 文件不做处理原样输出
if (!rawClassFile.getName().endsWith(".class")) {
Files.write(Paths.get(outputFile.getPath()),
originBytes,
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)
return
}
//byte[] -> asm -> byte[],通过 asm 操作最终获得新的字节数组,便是修改后的 classes 文件
byte[] newBytes = processClassFileByAsm(originBytes, rawClassFile.getName())
//使用 StandardOpenOption.CREATE:如果文件不存在则创建
//使用 StandardOpenOption.TRUNCATE_EXISTING:如果文件存在则覆盖(不要使用错了导致奇怪的打包问题)
Files.write(Paths.get(outputFile.getPath()), newBytes,
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)
}
处理 jar 文件:
transformInput.getJarInputs()
private void processJarFile(JarInput jarInput, File destFile) throws Exception {
//只处理 jar 文件(假如有非 jar 文件也别忘了原样输出)
def inFile = jarInput.getFile()
if (!inFile.getName().endsWith(".jar")) {
return
}
JarOutputStream outputStream = new JarOutputStream(new FileOutputStream(destFile))
JarFile inJarFile = new JarFile(jarInput.getFile())
def rawEntries = inJarFile.entries()
//jar 本省就是一个压缩包,遍历他的每一个 class
while (rawEntries.hasMoreElements()) {
def element = rawEntries.nextElement()
def elementName = element.getName()
outputStream.putNextEntry(new JarEntry(element.getName()))
def elementInStream = inJarFile.getInputStream(element)
//转成 byte 数组输入到 asm
byte[] originBytes = IOUtils.toByteArray(elementInStream)
//byte[] -> asm -> byte[],通过 asm 操作最终获得新的字节数组便是修改后的 classes 文件
//交给 asm 的,我们只处理 class 文件,别忘了 jar 里面什么都可能有
//这里只是简单的通过文件后缀判断,其实真正应该是通过 class 文件魔数 (magic number) 判断
if (elementName.endsWith(".class")) {
//这里面主要是实现自己的逻辑了,找到那个 class 文件是你需要处理的目标文件
if (CommUtils.trimEquals(AsmConstant.CLASS_PATH_GAME_MANAGER, elementName)) {
//拿到目标类的字节数组进行 asm 操作
checkGameManagerMethod(originBytes)
}
int index = elementName.lastIndexOf(".class")
if (index > 0) {
def className = elementName.substring(0, index).replaceAll("/", ".")
if (CommUtils.trimEquals(className, CheckPluginManager.sInstance.getGameActivityName())) {
CmdColorPrintUtils.outputGreen("找到 jar 里面需要处理的类:" + elementName)
//拿到目标类的字节数组进行 asm 操作
originBytes = asmProcess(originBytes)
}
}
}
outputStream.write(originBytes)
outputStream.closeEntry()
}
outputStream.close()
inJarFile.close()
Files.write(Paths.get(inFile.getPath()), IOUtils.toByteArray(new FileInputStream(destFile)),
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)
}
判断一个文件是否是 class 文件:
public boolean isClassFile(String filename) {
if (filename == null || filename.trim().isEmpty() || !filename.endsWith(".class")) {
return false;
}
try (DataInputStream dis = new DataInputStream(new FileInputStream(filename))) {
return dis.readInt() == 0xCAFEBABE;
} catch (IOException e) {
return false;
}
}
ASM 操作
Android 打包构建过程伴随着 class 字节码的转换,通常在其中引入字节码框架进一步处理字节码以满足需求,字节码框架有很多选择:
举一个简单的例子:
判断类里面是否有dispatchTouchEvent(MotionEvent ev)
方法
private void checkGameManagerMethod(byte[] originBytes) {
//拷贝数组,不影响原数组数据
def copyOf = Arrays.copyOf(originBytes, originBytes.length)
ClassReader classReader = new ClassReader(copyOf)
int option = 0
classReader.accept(new ClassVisitor(Opcodes.ASM9) {
@Override
MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
//不处理构造方法
if (!name.equals("<init>")) {
boolean isAbstractMethod = (access & ACC_ABSTRACT) != 0
boolean isNativeMethod = (access & ACC_NATIVE) != 0
//不处理 native 方法和抽象方法
if (!isAbstractMethod && !isNativeMethod) {
//判断该类里面是否有方法 dispatchTouchEvent(MotionEvent ev)
if (CommUtils.trimEquals(name, "dispatchTouchEvent") && CommUtils.trimEquals("(Landroid/view/MotionEvent;)V", descriptor)) {
CmdColorPrintUtils.outputGreen("找到 dispatchTouchEvent 方法")
}
}
}
return super.visitMethod(access, name, descriptor, signature, exceptions)
}
}, option)
}
修改类的静态代码块中某字符串常量
静态代码块的静态初始化方法<clinit>
,看 java 代码是这样的:
使用 jadx 查看 smali 代码是这样的:
除此之外还有一个实例初始化方法<init>
/**
* 访问静态代码块
*/
public class StaticBlockClassVisitor extends ClassVisitor {
private String mMapping, mRawName;
/**
* @param classVisitor
* @param rawName 映射前的值
* @param mapping 映射值
*/
public StaticBlockClassVisitor(ClassVisitor classVisitor, String rawName, String mapping) {
super(Opcodes.ASM9, classVisitor);
this.mMapping = mapping;
this.mRawName = rawName;
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
//<clinit> 就是类的静态构造方法
if ("<clinit>".equals(name)) {
return new MethodVisitor(Opcodes.ASM9, mv) {
@Override
public void visitLdcInsn(Object value) {
if (value instanceof String
&& value != null
&& mMapping != null
&& CommUtils.trimEquals((String) value, mRawName)) {
CmdColorPrintUtils.outputGreen("修改常量映射 :" + value + " -> " + mMapping);
//进行映射替换
super.visitLdcInsn(mMapping);
} else {
super.visitLdcInsn(value);
}
}
};
}
return mv;
}
}