如何将方法调用注入到android中的另一个方法中

时间:2018-01-31 12:19:43

标签: java android gradle kotlin bytecode

我有一个班级,

  • 一个名为something的字段,
  • 一个名为setSomething的setter方法,和
  • 一种名为onChange的方法,每次更改something时都应调用该方法。

我希望能够自由添加更多字段,并为所有字段提供相同的行为。

我不想手动拨打onChange因为,

  1. 很多样板,
  2. 代码将写在Kotlin中,所以我根本不想编写setter函数。
  3. 我能够想到的理想解决方案是以某种方式在编译时针对每个setter方法onChange之前注入return调用

    我看过注释处理,但显然课程在那个阶段没有实际编译,所以我必须重新生成整个课程?我并不完全理解这一点。

    另一个选项似乎是编写一个gradle插件,它将找到相关的类并修改它们的字节码。

    我实际上已经开始将这项工作作为一个纯Java项目(gradle插件是半完成的)并且能够找到类并注入方法调用。似乎无法成功地将结果写入类文件。

    这里有我所拥有的(使用BCEL):

    public class StateStoreInjector {
    
        public static void main(String[] args) {
            // Find all classes that extends StateStore
            Reflections reflections = new Reflections("tr.xip.statestore");
            Set<Class<? extends StateStore>> classes = reflections.getSubTypesOf(StateStore.class);
            for (Class c : classes) {
                try {
                    JavaClass clazz = Repository.lookupClass(c.getName());
                    JavaClass superClazz = Repository.lookupClass(StateStore.class.getName());
                    if (Repository.instanceOf(clazz, superClazz)) {
                        injectInClass(clazz, superClazz);
                    }
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                }
            }
        }
    
        private static void injectInClass(JavaClass clazz, JavaClass superClazz) {
            ClassGen classGen = new ClassGen(clazz);
            ConstantPoolGen cp = classGen.getConstantPool();
    
            // Find the onChange method
            Method onChangeMethod = null;
            for (Method m : superClazz.getMethods()) {
                if (m.getName().equals("onChange")) {
                    onChangeMethod = m;
                }
            }
    
            if (onChangeMethod == null) {
                throw new RuntimeException("onChange method not found");
            }
    
            ClassGen superClassGen = new ClassGen(superClazz);
            ConstantPoolGen superCp = superClassGen.getConstantPool();
    
            // Add onChange method ref to the class ConstantPool
            MethodGen onChangeMethodGen = new MethodGen(onChangeMethod, superClassGen.getClassName(), superCp);
            cp.addMethodref(onChangeMethodGen);
    
            // Loop through all methods to inject method invocations if applicable
            for (Method m : clazz.getMethods()) {
                // Skip methods with names shorter than 3 chars - we're looking for setters and setters would be min 4 chars
                if (m.getName().length() < 3) continue;
    
                // Check if the method actually starts with the keyword "set"
                boolean isSetMethod = m.getName().substring(0, 3).toUpperCase().equals("SET");
                // Get method name without the "set" keyword
                String methodName = m.getName().substring(3, m.getName().length());
    
                // Check that we actually have a field set by this setter - that this setter is "valid"
                boolean fieldWithSameNameExists = false;
                for (Field f : clazz.getFields()) {
                    if (f.getName().toUpperCase().equals(methodName.toUpperCase())) {
                        fieldWithSameNameExists = true;
                        break;
                    }
                }
    
                // Proceed with injection if criteria match
                Method newMethod = null;
                if (isSetMethod && fieldWithSameNameExists) {
                    newMethod = injectInMethod(m, onChangeMethodGen, classGen, cp);
                }
    
                // Injection returned. Do we have a new/modified method? Yes? Update and write class.
                if (newMethod != null) {
                    classGen.removeMethod(m);
                    classGen.addMethod(newMethod);
                    classGen.update();
                    try {
                        String packageName = clazz.getPackageName().replace(".", "/");
                        String className = clazz.getClassName();
                        className = className.substring(className.lastIndexOf(".") + 1, className.length());
                        clazz.dump(packageName + "/" + className + "Edited.class");
                    }
                    catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    
        private static Method injectInMethod(Method m, MethodGen onChangeMethodGen, ClassGen cg, ConstantPoolGen cp) {
            MethodGen methodGen = new MethodGen(m, cg.getClassName(), cp);
            InstructionList il = methodGen.getInstructionList();
    
            println(il.toString() + "pre insert ^");
    
            // Find the "return" instruction
            Instruction returnInstruction = null;
            for (Instruction i : il.getInstructions()) {
                if (i.getOpcode() == 177) returnInstruction = i;
            }
            // If found, insert onChange invocation instruction before the return instruction
            if (returnInstruction != null) {
                int index = cp.lookupMethodref(onChangeMethodGen); // Find the index of the onChange method in the CP
                il.insert(returnInstruction, new INVOKEVIRTUAL(index)); // Insert the new instruction
    
                println(il.toString() + "post insert ^");
    
                il.setPositions(); // Fix positions
    
                println(il.toString() + "post set pos ^");
    
                il.update();
                methodGen.update();
    
                return methodGen.getMethod();
            }
    
            return null;
        }
    
        private static void println(String message) {
            System.out.println(message);
        }
    }
    

    输入Java类:

    public class DummyStateStore extends StateStore {
        private int id = 4321;
    
        public void setId(int id) {
            this.id = id;
        }
    
        public int getId() {
            return id;
        }
    }
    

    父店类:

    public class StateStore {
    
        public void onChange() {
            // notifies all subscribers 
        }
    }
    

    输出(反编译)类文件:

    public class DummyStateStore extends StateStore {
        private int id = 4321;
    
        public DummyStateStore() {
        }
    
        public void setId(int id) {
            this.id = id;
        }
    
        public int getId() {
            return this.id;
        }
    }
    

    日志输出:

       0: aload_0[42](1)
       1: iload_1[27](1)
       2: putfield[181](3) 2
       5: return[177](1)
    pre insert ^
       0: aload_0[42](1)
       1: iload_1[27](1)
       2: putfield[181](3) 2
      -1: invokevirtual[182](3) 26
       5: return[177](1)
    post insert ^
       0: aload_0[42](1)
       1: iload_1[27](1)
       2: putfield[181](3) 2
       5: invokevirtual[182](3) 26
       8: return[177](1)
    post set pos ^
    

    (我通过调试代码检查了索引26, 是CP中的正确索引)

    现在,问题是:

    1. 为什么无法在反编译代码中看到调用,但它似乎被添加到指令列表中?我错过了什么?
    2. 我将在Android构建中导出修改后的类文件,以便将它们包含在最终的apk中?

1 个答案:

答案 0 :(得分:0)

你正在尝试使用反射,但是没有必要使用Kotlin,因为你可以创建更高阶函数(将函数作为输入的函数)。

您可以执行以下操作:

{{1}}