使用javassist和ASM修改class,並實現方法耗時檢測插件

題目出自鴻神玩安卓svip免費交流羣html

第一週:嘗試修改java字節碼java

1.選擇javassist或者asm嘗試修改一個java class,作任意修改便可[達標]。
2.嘗試給一個java class方法中的方法添加耗時。
3.嘗試在Android項目編譯階段給java class方法添加耗時檢測。android

1.javassist修改字極光jar包

以前在項目開發中爲了實現消息推送的各個平臺版本sdk(小米,華爲,OPPO,vivo,極光)。在寫這個多平臺推送的sdk過程當中,發現小米手機啓動時,小米推送和極光推送的服務都同時啓動了。致使後臺發起的推送收到了兩次(後天是全平臺推送的)。原本只要手機端只要啓動一個推送服務,結果應該只會收到一個推送。當時猜想多是註冊了某個廣播接收者而後在某些時候啓動了極光服務,如今從新回顧經過Android Studio的Analyze APK(build->Analyze APK)時,極光服務是經過provider啓動的,會有一些sdk會在provider中初始化,見你的Android庫是否還在Application中初始化?git

<provider android:name="cn.jpush.android.service.DownloadProvider" android:exported="true" android:authorities="com.wantu.kouzidashen.DownloadProvider" />
複製代碼
public class DownloadProvider extends ContentProvider {
...
    private void init() {
        try {
            if (a.d(this.getContext().getApplicationContext())) {
                JCoreInterface.register(this.getContext());
            }

        } catch (Throwable var1) {
        }
    }
}

public class JCoreInterface {
	...
	public static void register(Context var0) {
        Bundle var1 = new Bundle();
        i.a().b(var0, "intent.INIT", var1);
    }
}

public final class i {
    public final void b(Context var1, String var2, Bundle var3) {
	    try {
	        var1 = cn.jiguang.d.a.a(var1);
	        if (this.a(var1)) {
	            JCoreInterface.execute("SDK_MAIN", new j(this, var1, var2, var3), new int[0]);
	        }
	    } catch (Throwable var4) {
	        cn.jiguang.e.c.c("JServiceCommandHelper", "onAction failed", var4);
	    }
    }
}
複製代碼

在平時啓動極光服務經過JPushInterface.init()方法最終也會調用JCoreInterface.execute。所以爲了不在小米/華爲等自己具備推送平臺的手機在啓動時啓動了極光推送,須要設置一個flag標誌控制execute方法的執行:github

public class JCoreInterface{
	public static void execute{
		if(flag)return;//修改的代碼
		...
	}
}
複製代碼

在開始想經過JD-GUI來修改代碼,而後編譯成新的jar包。可是發現太難了,相關的Context環境沒有,並且極光的jar包是混淆過的,JD-GUI反編譯的最終效果不必定每一個都正確,會有一些文件不識別。
事實上咱們想要的效果只是修改個別文件,而後覆蓋相應的目錄便可,這樣改動最小。最終經過查詢,javassist(Java Programming Assistant)進入個人視野。
javassist是一個java字節碼編輯工具,能夠很簡單的修改class,操做方式優勢相似於反射接口調用。web

首先準備JD-GUIidea,而後下載javassist,在Android SDK目錄下的platforms/android-28下找出android.jar,而後下載極光的jar包, 咱們用idea新建一個java項目,而後新建libs目錄,而後加入javassist.jar。右鍵Add as Library加入到庫中。在src中新建一個Test類, 首先在JCoreInterface(在jpush-android-3.2.0.jar中)中加入JPUSH_IS_INIT靜態變量。編程

public class Test {
    public static void main(String[] args) {
        ClassPool pool = ClassPool.getDefault();
        try {
            pool.insertClassPath("/xxx/JavassistTest/libs/jcore-android-1.2.7.jar");
            pool.insertClassPath("/xxx/JavassistTest/libs/jpush-android-3.2.0.jar");
            pool.insertClassPath("/xxx/JavassistTest/libs/android.jar");
            CtClass c = pool.get("cn.jpush.android.api.JPushInterface");//找到JPushInterface類
            CtField bField = new CtField(CtClass.booleanType,"JPUSH_IS_INIT",c2);//添加JPUSH_IS_INIT靜態變量
            bField.setModifiers(Modifier.PUBLIC|Modifier.STATIC);
            c.addField(bField);
            CtMethod initMethod = c.getDeclaredMethod("init");//在init方法最前面插入代碼 JPUSH_IS_INIT = true;
            initMethod.insertBefore("JPUSH_IS_INIT = true;");

            CtMethod stopMethod = c.getDeclaredMethod("stopPush");////stopPush方法中 JPUSH_IS_INIT = true;
            stopMethod.insertBefore("JPUSH_IS_INIT = false;");
            c.writeFile("jpush-android"); //輸出目錄jpush-android
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
複製代碼

在當前工程jpush-android目錄下,咱們找到了cn/jpush/android/api/JPushInterface.class,idea打開反編譯以下:api

public class JPushInterface {
    ...
    public static boolean JPUSH_IS_INIT;
    ...

    public static void init(Context var0) {
        JPUSH_IS_INIT = true;
        ...
    }

   ...

    public static void stopPush(Context var0) {
        JPUSH_IS_INIT = false;
        g.a("JPushInterface", "action:stopPush");
        ...
    }
複製代碼

至此JPushInterface已經加入了JPUSH_IS_INIT標誌,而且在initstopPush中進行修改。接着須要修改jcore-android-1.2.7.jarJCoreInterface.execute方法。在此以前,須要將當前的JPushInterface.class覆蓋到jpush-android-3.2.0.jar中。拷貝一份命名爲jpush-android-3.2.0-fix.jar,經過360壓縮軟件打開,將修改JPushInterface.class文件覆蓋到對應目錄便可。接着就能夠修改JCoreInterface了,代碼以下:app

ClassPool pool = ClassPool.getDefault();
try {
    pool.insertClassPath("/xxx/JavassistTest/libs/jcore-android-1.2.7.jar");
    pool.insertClassPath("/xxx/JavassistTest/libs/jpush-android-3.2.0-fix.jar");
    pool.insertClassPath("/xxx/JavassistTest/libs/android.jar");
    CtClass c = pool.get("cn.jiguang.api.JCoreInterface");
    CtMethod method = c.getDeclaredMethod("execute");
    method.insertBefore("if(!cn.jpush.android.api.JPushInterface.JPUSH_IS_INIT)return;");//JPUSH_IS_INIT爲false,直接return返回
    c.writeFile("jpush-android");
} catch (Exception e) {
    e.printStackTrace();
}
複製代碼

最後獲得修改後的JCoreInterface,反編譯以下:框架

package cn.jiguang.api;
...
public class JCoreInterface {
 	public static void execute(String var0, Runnable var1, int... var2) {
        if (JPushInterface.JPUSH_IS_INIT) {
            cn.jiguang.d.h.i.a(var0, var1);
        }
    }
}
複製代碼

稍微與修改時候的代碼有所不一樣,可是總體的邏輯是正確的。而後一樣經過壓縮軟件覆蓋修改,咱們就實現了可控制啓動的極光推送jar包。

2. ASM添加方法耗時檢測

ASM是一款字節碼操做與分析的開源框架,能夠經過二進制形式(內存)修改已有class或者動態生成class。它提供了許多api用於字節碼轉換構建與分析。較於javassistASM相對複雜,門檻較高。ASM操做基於指令級別,提供了多種修改和分析API,小而快速,強大。

因爲ASM操做字節碼是基於指令的,所以要對jvm要有必定了解,推薦你們閱讀《深刻理解Java虛擬機》《本身動手寫Java虛擬機》,而《本身動手寫Java虛擬機》實踐性強,你們能夠經過go語言編程的形式學習Java虛擬機。
ASMapi主要有如下關鍵類:
ClassReader: 用於解析class文件,經過accept接收ClassVisitor對象訪問具體的字段,方法等.
ClassVisitor:class訪問者.
ClassWriter: 繼承自ClassVisitor,用於修改或生成class,一般配合ClassReaderClassVisitor修改class.
這裏在asm4-guide第63頁經過LocalVariablesSorter爲方法添加耗時檢測

public class MethodLogAdapter extends ClassVisitor {
    public MethodLogAdapter(int api) {
        super(api);
    }

    private String owner;
    private boolean isInterface;
    public boolean changed; //是否修改過

    public MethodLogAdapter(ClassVisitor cv) {
        super(ASM4, cv);
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        cv.visit(version, access, name, signature, superName, interfaces);
        owner = name;
        isInterface = (access & ACC_INTERFACE) != 0;
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        MethodVisitor mv = cv.visitMethod(access, name, desc, signature,
                exceptions);
        if (!isInterface && mv != null && !name.equals("<init>")) {
            mv = new MethodLogAdapter.LogMethodAdapter(access, name, desc, mv);
        }
        return mv;
    }


    class LogMethodAdapter extends LocalVariablesSorter {
        private int time;
        private String name;
        private boolean hasMethodLog;//是否具備MethodLog註解

        public LogMethodAdapter(int access, String name, String desc, MethodVisitor mv) {
            super(ASM4, access, desc, mv);
            this.name = name;
        }

        @Override
        public void visitCode() {
            super.visitCode();
            if (hasMethodLog) {
                mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
                        "nanoTime", "()J");
                time = newLocal(Type.LONG_TYPE);//聲明臨時變量time
                mv.visitVarInsn(LSTORE, time);//將返回的時間戳保存到臨時變量
            }
        }

        @Override
        public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
            hasMethodLog = "Lannotations/MethodLog;".equals(descriptor);
            if (!changed && hasMethodLog) changed = true;
            return super.visitAnnotation(descriptor, visible);
        }

        @Override
        public void visitInsn(int opcode) {
            if ((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) {
                if (hasMethodLog) {
                    mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
                            "nanoTime", "()J");
                    mv.visitVarInsn(LLOAD, time);//加載time臨時變量
                    mv.visitInsn(LSUB);//與當前時間戳相減
                    mv.visitVarInsn(LSTORE, 3);
                    Label l3 = new Label();
                    mv.visitLabel(l3);
                    //如下是將方法耗時打印出來 Log.i("當前類名","方法名:"+time)
                    mv.visitLdcInsn(owner);
                    mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
                    mv.visitInsn(DUP);
                    mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
                    mv.visitLdcInsn(name + ":");
                    mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
                    mv.visitVarInsn(LLOAD, 3);
                    mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
                    mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
                    mv.visitMethodInsn(INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;Ljava/lang/String;)V", false);
                }
            }
            super.visitInsn(opcode);
        }

        @Override
        public void visitMaxs(int maxStack, int maxLocals) {
            super.visitMaxs(maxStack + 4, maxLocals);
        }
    }
}

複製代碼

而後配合ClassReaderClassWriter修改class,給TestActivity添加方法耗時檢測

public class AsmTest {
    public static void main(String[] args) {
        try {
            changeTest();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    //class文件信息讀取
    private static void changeTest() throws Exception {
        String classPath = "out/production/ClassEditTest/test/TestActivity.class";
        ClassReader reader = new ClassReader(new FileInputStream(new File(classPath)));
        ClassWriter cw = new ClassWriter(reader,ClassWriter.COMPUTE_MAXS);
        MethodLogAdapter adapter = new MethodLogAdapter(cw);
        reader.accept(adapter,ClassReader.EXPAND_FRAMES);
        System.out.println(adapter.changed);
        byte[] bytes = cw.toByteArray();
        FileOutputStream fos = new FileOutputStream(new File("test.class"));
        fos.write(bytes);

    }
}
複製代碼

對比原先和修改後的代碼以下

public class TestActivity {//修改前

    @MethodLog
    public void test() {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void test2() {
       Log.i("test","test123");
    }
}
複製代碼
public class TestActivity {//修改後
    public TestActivity() {
    }

    @MethodLog
    public void test() {
        long var1 = System.nanoTime();

        try {
            Thread.sleep(100L);
        } catch (InterruptedException var5) {
            var5.printStackTrace();
        }

        long var3 = System.nanoTime() - var1;
        Log.i("test/TestActivity", "test:" + var3);
    }

    public void test2() {
        this.test();
    }
}
複製代碼

3. 使用Transform在Android編譯階段添加方法耗時

以前經過Transform實現了簡易版路由框架,不過是經過javassist實現的,雖然實現更簡單,可是不如ASM操做快速,因此本次經過ASM實現。
一樣的建立一個名稱爲buildSrc(注意大小寫)的Android Library,這樣咱們的插件直接可使用了,具體如何實現插件能夠參照基於Transform實現更高效的組件化路由框架的配置方式。
添加MethodLogTransform處理方法耗時

class MethodLogTransform extends Transform {
    @Override
    String getName() {
        return "MethodLog"
    }

	...
    @Override
    void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {
        for (TransformInput input : inputs) {
            for (DirectoryInput dirInput : input.directoryInputs) {//目錄中的class文件
                readClassWithPath(dirInput.file)
                File dest = outputProvider.getContentLocation(dirInput.name,
                        dirInput.contentTypes,
                        dirInput.scopes,
                        Format.DIRECTORY)
                FileUtils.copyDirectory(dirInput.file, dest)
            }
            for (JarInput jarInput : input.jarInputs) {//jar(第三方庫,module)
                if (jarInput.scopes.contains(QualifiedContent.Scope.SUB_PROJECTS)) {//module library
					//todo 爲jar包添加耗時
                }
                copyFile(jarInput, outputProvider)
            }
        }
    }
    //
    void readClassWithPath(File dir) {//從編譯class文件目錄找到註解
        def root = dir.absolutePath
        dir.eachFileRecurse { File file ->
            def filePath = file.absolutePath
            if (!filePath.endsWith(".class")) return
            def className = getClassName(root, filePath)
            if (isSystemClass(className)) return
            hookClass(filePath, className)
        }
    }

    void hookClass(String filePath, String className) {
        ClassReader reader = new ClassReader(new FileInputStream(new File(filePath)))
        ClassWriter cw = new ClassWriter(reader,ClassWriter.COMPUTE_MAXS)
        MethodLogAdapter adapter = new MethodLogAdapter(cw)
        reader.accept(adapter,ClassReader.EXPAND_FRAMES)
        System.out.println(adapter.changed)
        if(adapter.changed){
            byte[] bytes = cw.toByteArray()
            FileOutputStream fos = new FileOutputStream(new File(filePath))
            fos.write(bytes)
        }

    }

    ...
}
複製代碼

項目地址

github.com/iamyours/AS…

相關文章
相關標籤/搜索