一、前言
在安卓中,难免会碰到需要动态插入代码,或者删除代码。这时候就需要用到自定义Transform任务,来对Class文件进行扫描和获取。
可以插入的代码场景有哪些?例如:打印方法的执行时间等。
可以删除的代码场景有哪些?例如:把代码中Log.e日志打印的代码去掉等
先看下代码前后效果,原始代码如下:
经过修改后(通过反编译apk得到源码):
接下来,让我们看看如何对以上两个场景进行代码的插入和删除。
二、工程准备
2.1 基础知识预备
1.需要用到Javassist来修改代码,不会的可以看看如何使用
如果你已经有了上面两个基础后,那我们就可以开始搭建工程了。
2.2 创建Transform
创建一个Transform用来对代码进行处理
流程如下:
- 从Transform中拿到我们需要处理的Class文件路径
- 从Class文件路径中找到Class文件结尾的文件名(因为可能有)
- 找到需要的Class文件对其代码进行插入和修改
- 除了代码的插入和修改,其他基本都是模板代码,不要纠结。😀
class MyTransform extends Transform {
def project
def pool = ClassPool.default
MyTransform(Project project) {
this.project = project
}
//任务名
@Override
public String getName() {
return "MyTransform";
}
//你想要处理的文件
@Override
public Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS;
}
//你想要处理的范围
@Override
public Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT;
}
//是否增量编译
@Override
public boolean isIncremental() {
return false;
}
@Override
public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation);
println "start transform"
//1.拿到需要的处理的class文件
transformInvocation.getInputs().each { allInput ->
//类最终生成为两种形式 1.文件夹(包含包名) 2.jar包
//1.1 先从文件夹中拿到我们需要Class文件
allInput.directoryInputs.each { dirInput ->
def preClassNamePath = dirInput.file.absolutePath
println "class文件路径"+preClassNamePath
//插入文件路径到Pool内存池
pool.insertClassPath(preClassNamePath)
findTarget(dirInput.file,preClassNamePath)
//1.2 获取输出的文件夹
def dest = transformInvocation.outputProvider.getContentLocation(
dirInput.name,
dirInput.contentTypes,
dirInput.scopes,
Format.DIRECTORY)
println "文件夹输出文件路径 " + dest
//记得把文件复制到下一个transform使用,不要下一个transform任务拿不到,也就生成不了APK
FileUtils.copyDirectory(dirInput.file, dest)
}
//1.3 在从jar包拿到需要的处理的class文件(注意:如果工程没有jar包,一般不需要从这里取)
allInput.jarInputs.each { jarInput ->
//1.4 获取输出的文件夹
def dest = transformInvocation.outputProvider.getContentLocation(
jarInput.name,
jarInput.contentTypes,
jarInput.scopes,
Format.JAR)
println "Jar包输出文件路径 " + dest
//把文件复制到下一个transform使用
FileUtils.copyFile(jarInput.file, dest)
}
}
}
/**
*找到class结尾的文件
* @param dir
* @param fileNamePath >>app\build\intermediates\javac\release\classes
*/
private void findTarget(File dir, String fileNamePath) {
if (dir.isDirectory()) {
dir.listFiles().each {
findTarget(it, fileNamePath)
}
}else {
def filePath = dir.absolutePath
if (filePath.endsWith(".class")) {
println "找到Class"+filePath
//修改文件
modify(filePath, fileNamePath)
}
}
}
private void modify(String filePath, String fileNamePath) {
//过滤没用的文件
if (filePath.contains('R$') || filePath.contains('R.class')
|| filePath.contains("BuildConfig.class")) {
return
}
println "开始修改Class"+filePath
//因为Javassist需要class包名也就是》》com.example.javassist.MainActivity
def className = filePath.replace(fileNamePath, "")
.replace("\\", ".") .replace("/", ".")
def name = className.replace(".class", "").substring(1)
println "包名为:" + name
//把class添加到pool中,才能修改class文件
project.android.bootClasspath.each {
pool.appendClassPath(it.absolutePath)
}
CtClass ctClass= pool.get(name)
//添加插入代码
addCode(ctClass, fileNamePath)
}
private void addCode(CtClass ctClass ,String fileName) {
//使class变成可修改
ctClass.defrost()
//获取class所有的方法
CtMethod[] methods = ctClass.getDeclaredMethods()
for (method in methods) {
println "method "+method.getName()+" 参数个数 "+method.getParameterTypes().length
if (method.getName().matches("hello")){
method.addLocalVariable("start",CtClass.longType);
method.insertBefore("{ start = System.currentTimeMillis();}");
method.insertAfter("{ " +
" long last = System.currentTimeMillis() - start;"+
"System.out.println(\" 方法耗时:\"+last);" +
"}");
}
}
for (method in methods){
println "deleteCodeInMethod start method"+method
deleteCodeInMethod(method)
}
//把修改的内容写入文件
ctClass.writeFile(fileName)
//释放内存
ctClass.detach()
}
private void deleteCodeInMethod(CtMethod method){
method.instrument(new ExprEditor(){
@Override
void edit(MethodCall m) throws CannotCompileException {
println("getClassName: "+ m.getClassName()+
" getMethodName: "+m.getMethodName() +
" line: " + m.getLineNumber());
if (m.getClassName().matches(".*Log") && m.getMethodName().matches("e")){
println "modify>>>>>"
m.replace("{\$_;}")
}
}
})
}
}
Transform注册
class MyPlugin implements Plugin<Project>{
@Override
void apply(Project project) {
println "this is a myplugin"
project.extensions.getByType(BaseExtension.class)
.registerTransform(new MyTransform(project))
}
}
2.3 核心代码
代码插入
循环Class中所有的方法,匹配到我们需要修改的方法,然后对方法进行代码插入
for (method in methods) {
println "method "+method.getName()+" 参数个数 "+method.getParameterTypes().length
if (method.getName().matches("hello")){
method.addLocalVariable("start",CtClass.longType);
method.insertBefore("{ start = System.currentTimeMillis();}");
method.insertAfter("{ " +
" long last = System.currentTimeMillis() - start;"+
"System.out.println(\" 方法耗时:\"+last);" +
"}");
}
}
代码修改
循环Class中所有的方法,匹配到我们需要修改的方法,然后对其方法的Body进行扫描,MethodCall 就是方法里每一行代码执行的回调,匹配出我们需要修改的代码,进行删除。
private void deleteCodeInMethod(CtMethod method){
method.instrument(new ExprEditor(){
@Override
void edit(MethodCall m) throws CannotCompileException {
println("getClassName: "+ m.getClassName()+
" getMethodName: "+m.getMethodName() +
" line: " + m.getLineNumber());
if (m.getClassName().matches(".*Log") && m.getMethodName().matches("e")){
println "modify>>>>>"
m.replace("{\$_;}")
}
}
})
}
————————————————
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.youkuaiyun.com/wumeixinjiazu/article/details/124968519