AGP4 Transform 小结

整理一下 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;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值