天天看點

一起玩轉Android項目中的位元組碼(下)

上篇; https://blog.csdn.net/feiyu1947/article/details/84931252

如何驗證行号

上面我們給每一句方法調用的前後都插入了一行日志列印,那麼有沒有想過,這樣豈不是打亂了代碼的行數,這樣,萬一crash了,定位堆棧豈不是亂套了。其實并不然,在上面visitMethodInsn中做的東西,其實都是在同一行中插入的代碼,上面我們貼出來的代碼是這樣

private static void printTwo() {
    System.out.println("CALL printOne");
    printOne();
    System.out.println("RETURN printOne");
    System.out.println("CALL printOne");
    printOne();
    System.out.println("RETURN printOne");
}           

無論你用idea還是eclipse打開上面的class檔案,都是一行行展示的,但是其實class内部真實的行數應該是這樣

private static void printTwo() {
    System.out.println("CALL printOne"); printOne(); System.out.println("RETURN printOne");
    System.out.println("CALL printOne"); printOne(); System.out.println("RETURN printOne");
}           

idea下可以開啟一個選項,讓檢視class内容時,保留真正的行數

開啟後,你看到的是這樣

https://link.juejin.im/?target=http%3A%2F%2Fquinnchen.me%2Fimages%2Ftransform_line_2.png

我們可以發現,17行和18行,分别包含了三句代碼。

而開啟選項之前是這樣

https://link.juejin.im/?target=http%3A%2F%2Fquinnchen.me%2Fimages%2Ftransform_line_1.png

那麼如何開啟這個選項呢?Mac下

cmd + shift + A

輸入Registry,勾選這兩個選項

https://link.juejin.im/?target=http%3A%2F%2Fquinnchen.me%2Fimages%2Ftransform_line_3.png

其實無論位元組碼和ASM的代碼上看,class中的所有代碼,都是先聲明行号X,然後開始幾條位元組碼指令,這幾條位元組碼對應的代碼都在行号X中,直到聲明下一個新的行号。

ASM code

解析來介紹,如何寫出上面生成代碼的邏輯。首先,我們設想一下,如果要對某個class進行修改,那需要對位元組碼具體做什麼修改呢?最直覺的方法就是,先編譯生成目标class,然後看它的位元組碼和原來class的位元組碼有什麼差別(檢視位元組碼可以使用javap工具),但是這樣還不夠,其實我們最終并不是讀寫位元組碼,而是使用ASM來修改,我們這裡先做一個差別,bytecode vs ASM code,前者就是JVM意義的位元組碼,而後者是用ASM描述的bytecode,其實二者非常的接近,隻是ASM code用Java代碼來描述。是以,我們應該是對比ASM code,而不是對比bytecode。對比ASM code的diff,基本就是我們要做的修改。

而ASM也提供了一個這樣的類:ASMifier,它可以生成ASM code,但是,其實還有更快捷的工具,Intellij IDEA有一個插件

Asm Bytecode Outline

,可以檢視一個class檔案的bytecode和ASM code。

到此為止,貌似使用對比ASM code的方式,來實作位元組碼修改也不難,但是,這種方式隻是可以實作一些修改位元組碼的基礎場景,還有很多場景是需要對位元組碼有一些基礎知識才能做到,而且,要閱讀懂ASM code,也是需要一定位元組碼的的知識。是以,如果要開發位元組碼工程,還是需要學習一番位元組碼。

ClassWriter在Android上的坑

如果我們直接按上面的套路,将ASM應用到Android編譯插件中,會踩到一個坑,這個坑來自于ClassWriter,具體是因為ClassWriter其中的一個邏輯,尋找兩個類的共同父類。可以看看ClassWriter中的這個方法getCommonSuperClass,

/**
 * Returns the common super type of the two given types. The default
 * implementation of this method <i>loads</i> the two given classes and uses
 * the java.lang.Class methods to find the common super class. It can be
 * overridden to compute this common super type in other ways, in particular
 * without actually loading any class, or to take into account the class
 * that is currently being generated by this ClassWriter, which can of
 * course not be loaded since it is under construction.
 * 
 * @param type1
 *            the internal name of a class.
 * @param type2
 *            the internal name of another class.
 * @return the internal name of the common super class of the two given
 *         classes.
 */
protected String getCommonSuperClass(final String type1, final String type2) {
    Class<?> c, d;
    ClassLoader classLoader = getClass().getClassLoader();
    try {
        c = Class.forName(type1.replace('/', '.'), false, classLoader);
        d = Class.forName(type2.replace('/', '.'), false, classLoader);
    } catch (Exception e) {
        throw new RuntimeException(e.toString());
    }
    if (c.isAssignableFrom(d)) {
        return type1;
    }
    if (d.isAssignableFrom(c)) {
        return type2;
    }
    if (c.isInterface() || d.isInterface()) {
        return "java/lang/Object";
    } else {
        do {
            c = c.getSuperclass();
        } while (!c.isAssignableFrom(d));
        return c.getName().replace('.', '/');
    }
}           

這個方法用于尋找兩個類的共同父類,我們可以看到它是擷取目前class的classLoader加載兩個輸入的類型,而編譯期間使用的classloader并沒有加載Android項目中的代碼,是以我們需要一個自定義的ClassLoader,将前面提到的Transform中接收到的所有jar以及class,還有android.jar都添加到自定義ClassLoader中。(其實上面這個方法注釋中已經暗示了這個方法存在的一些問題)

如下

public static URLClassLoader getClassLoader(Collection<TransformInput> inputs,
                                            Collection<TransformInput> referencedInputs,
                                            Project project) throws MalformedURLException {
    ImmutableList.Builder<URL> urls = new ImmutableList.Builder<>();
    String androidJarPath  = getAndroidJarPath(project);
    File file = new File(androidJarPath);
    URL androidJarURL = file.toURI().toURL();
    urls.add(androidJarURL);
    for (TransformInput totalInputs : Iterables.concat(inputs, referencedInputs)) {
        for (DirectoryInput directoryInput : totalInputs.getDirectoryInputs()) {
            if (directoryInput.getFile().isDirectory()) {
                urls.add(directoryInput.getFile().toURI().toURL());
            }
        }
        for (JarInput jarInput : totalInputs.getJarInputs()) {
            if (jarInput.getFile().isFile()) {
                urls.add(jarInput.getFile().toURI().toURL());
            }
        }
    }
    ImmutableList<URL> allUrls = urls.build();
    URL[] classLoaderUrls = allUrls.toArray(new URL[allUrls.size()]);
    return new URLClassLoader(classLoaderUrls);
}           

但是,如果隻是替換了getCommonSuperClass中的Classloader,依然還有一個更深的坑,我們可以看看前面getCommonSuperClass的實作,它是如何尋找父類的呢?它是通過Class.forName加載某個類,然後再去尋找父類,但是,但是,android.jar中的類可不能随随便便加載的呀,android.jar對于Android工程來說隻是編譯時依賴,運作時是用Android機器上自己的android.jar。而且android.jar所有方法包括構造函數都是空實作,其中都隻有一行代碼

throw new RuntimeException("Stub!");           

這樣加載某個類時,它的靜态域就會被觸發,而如果有一個static的變量剛好在聲明時被初始化,而初始化中隻有一個RuntimeException,此時就會抛異常。

是以,我們不能通過這種方式來擷取父類,能否通過不需要加載class就能擷取它的父類的方式呢?謎底就在眼前,父類其實也是一個class的位元組碼中的一項資料,那麼我們就從位元組碼中查詢父類即可。最終實作是這樣。

public class ExtendClassWriter extends ClassWriter {
    public static final String TAG = "ExtendClassWriter";
    private static final String OBJECT = "java/lang/Object";
    private ClassLoader urlClassLoader;
    public ExtendClassWriter(ClassLoader urlClassLoader, int flags) {
        super(flags);
        this.urlClassLoader = urlClassLoader;
    }
    @Override
    protected String getCommonSuperClass(final String type1, final String type2) {
        if (type1 == null || type1.equals(OBJECT) || type2 == null || type2.equals(OBJECT)) {
            return OBJECT;
        }
        if (type1.equals(type2)) {
            return type1;
        }
        ClassReader type1ClassReader = getClassReader(type1);
        ClassReader type2ClassReader = getClassReader(type2);
        if (type1ClassReader == null || type2ClassReader == null) {
            return OBJECT;
        }
        if (isInterface(type1ClassReader)) {
            String interfaceName = type1;
            if (isImplements(interfaceName, type2ClassReader)) {
                return interfaceName;
            }
            if (isInterface(type2ClassReader)) {
                interfaceName = type2;
                if (isImplements(interfaceName, type1ClassReader)) {
                    return interfaceName;
                }
            }
            return OBJECT;
        }
        if (isInterface(type2ClassReader)) {
            String interfaceName = type2;
            if (isImplements(interfaceName, type1ClassReader)) {
                return interfaceName;
            }
            return OBJECT;
        }
        final Set<String> superClassNames = new HashSet<String>();
        superClassNames.add(type1);
        superClassNames.add(type2);
        String type1SuperClassName = type1ClassReader.getSuperName();
        if (!superClassNames.add(type1SuperClassName)) {
            return type1SuperClassName;
        }
        String type2SuperClassName = type2ClassReader.getSuperName();
        if (!superClassNames.add(type2SuperClassName)) {
            return type2SuperClassName;
        }
        while (type1SuperClassName != null || type2SuperClassName != null) {
            if (type1SuperClassName != null) {
                type1SuperClassName = getSuperClassName(type1SuperClassName);
                if (type1SuperClassName != null) {
                    if (!superClassNames.add(type1SuperClassName)) {
                        return type1SuperClassName;
                    }
                }
            }
            if (type2SuperClassName != null) {
                type2SuperClassName = getSuperClassName(type2SuperClassName);
                if (type2SuperClassName != null) {
                    if (!superClassNames.add(type2SuperClassName)) {
                        return type2SuperClassName;
                    }
                }
            }
        }
        return OBJECT;
    }
    private boolean isImplements(final String interfaceName, final ClassReader classReader) {
        ClassReader classInfo = classReader;
        while (classInfo != null) {
            final String[] interfaceNames = classInfo.getInterfaces();
            for (String name : interfaceNames) {
                if (name != null && name.equals(interfaceName)) {
                    return true;
                }
            }
            for (String name : interfaceNames) {
                if(name != null) {
                    final ClassReader interfaceInfo = getClassReader(name);
                    if (interfaceInfo != null) {
                        if (isImplements(interfaceName, interfaceInfo)) {
                            return true;
                        }
                    }
                }
            }
            final String superClassName = classInfo.getSuperName();
            if (superClassName == null || superClassName.equals(OBJECT)) {
                break;
            }
            classInfo = getClassReader(superClassName);
        }
        return false;
    }
    private boolean isInterface(final ClassReader classReader) {
        return (classReader.getAccess() & Opcodes.ACC_INTERFACE) != 0;
    }
    private String getSuperClassName(final String className) {
        final ClassReader classReader = getClassReader(className);
        if (classReader == null) {
            return null;
        }
        return classReader.getSuperName();
    }
    private ClassReader getClassReader(final String className) {
        InputStream inputStream = urlClassLoader.getResourceAsStream(className + ".class");
        try {
            if (inputStream != null) {
                return new ClassReader(inputStream);
            }
        } catch (IOException ignored) {
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException ignored) {
                }
            }
        }
        return null;
    }
}           

到此為止,我們介紹了在Android上實作修改位元組碼的兩個基礎技術Transform+ASM,介紹了其原理和應用,分析了性能優化以及在Android平台上的适配等。在此基礎上,我抽象出一個輪子,讓開發者寫位元組碼插件時,隻需要寫少量的ASM code即可,而不需關心Transform和ASM背後的很多細節。詳見

github.com/Leaking/Hun…

萬事俱備,隻欠寫一個插件來玩玩了,讓我們來看看幾個應用案例。

應用案例

先抛結論,修改位元組碼其實也有套路,一種是hack代碼調用,一種是hack代碼實作。

比如修改Android Framework(android.jar)的實作,你是沒辦法在編譯期間達到這個目的的,因為最終Android Framework的class在Android裝置上。是以這種情況下你需要從hack代碼調用入手,比如Log.i(TAG, “hello”),你不可能hack其中的實作,但是你可以把它hack成HackLog.i(TAG, “seeyou”)。

而如果是要修改第三方依賴或者工程中寫的代碼,則可以直接hack代碼實作,但是,當如果你要插入的位元組碼比較多時,也可以通過一定技巧減少寫ASM code的量,你可以将大部分可以抽象的邏輯抽象到某個寫好的class中,然後ASM code隻需寫調用這個寫好的class的語句。

當然上面隻是目前按照我的經驗做的一點總結,還是有一些更複雜的情況要具體情況具體分析,比如在實作類似JakeWharton的

hugo

的功能時,在代碼開頭擷取方法參數名時我就遇到棘手的問題(用了一種二次掃描的方式解決了這個問題,可以移步項目首頁參考具體實作)。

我們這裡挑選OkHttp-Plugin的實作進行分析、示範如何使用Huntet架構開發一個位元組碼編譯插件。

使用OkHttp的人知道,OkHttp裡每一個OkHttp都可以設定自己獨立的Intercepter/Dns/EventListener(EventListener是okhttp3.11新增),但是需要對全局所有OkHttp設定統一的Intercepter/Dns/EventListener就很麻煩,需要一處處設定,而且一些第三方依賴中的OkHttp很大可能無法設定。曾經在官方repo提過這個問題的

issue

,沒有得到很好的回複,作者之一覺得如果是他,他會用依賴注入的方式來實作統一的Okhttp配置,但是這種方式隻能說可行但是不理想,背景在reddit發 

文章

安利自己Hunter這個輪子時,JakeWharton大佬竟然親自回答了,雖然面對大佬,不過還是要正面剛!争論一波之後,總結一下他的立場,大概如下

他覺得我說的好像這是okhttp的鍋,然而這其實是okhttp的一個feature,他覺得全局狀态是一種不好的編碼,是以在設計okhttp沒有提供全局Intercepter/Dns/EventListener的接口。而第三方依賴庫不能設定自定義Intercepter/Dns/EventListener這是它們的鍋。

但是,他的觀點我不完全同意,雖然全局狀态确實是一種不好的設計,但是,如果要做性能監控之類的功能,這就很難避免或多或少的全局侵入。(不過我确實措辭不當,說得這好像是Okhttp的鍋一樣)

言歸正傳,來看看我們要怎麼來對OkHttp動刀,請看以下代碼

public Builder(){
    this.dispatcher = new Dispatcher();
    this.protocols = OkHttpClient.DEFAULT_PROTOCOLS;
    this.connectionSpecs = OkHttpClient.DEFAULT_CONNECTION_SPECS;
    this.eventListenerFactory = EventListener.factory(EventListener.NONE);
    this.proxySelector = ProxySelector.getDefault();
    this.cookieJar = CookieJar.NO_COOKIES;
    this.socketFactory = SocketFactory.getDefault();
    this.hostnameVerifier = OkHostnameVerifier.INSTANCE;
    this.certificatePinner = CertificatePinner.DEFAULT;
    this.proxyAuthenticator = Authenticator.NONE;
    this.authenticator = Authenticator.NONE;
    this.connectionPool = new ConnectionPool();
    this.dns = Dns.SYSTEM;
    this.followSslRedirects = true;
    this.followRedirects = true;
    this.retryOnConnectionFailure = true;
    this.connectTimeout = 10000;
    this.readTimeout = 10000;
    this.writeTimeout = 10000;
    this.pingInterval = 0;
    this.eventListenerFactory = OkHttpHooker.globalEventFactory;
    this.dns = OkHttpHooker.globalDns;
    this.interceptors.addAll(OkHttpHooker.globalInterceptors);
    this.networkInterceptors.addAll(OkHttpHooker.globalNetworkInterceptors);
}           

這是OkhttpClient中内部類Builder的構造函數,我們的目标是在方法末尾加上四行代碼,這樣一來,所有的OkHttpClient都會擁有共同的Intercepter/Dns/EventListener。我們再來看看OkHttpHooker的實作

public class OkHttpHooker {
    public static EventListener.Factory globalEventFactory = new EventListener.Factory() {
        public EventListener create(Call call) {
            return EventListener.NONE;
        }
    };;
    public static Dns globalDns = Dns.SYSTEM;
    public static List<Interceptor> globalInterceptors = new ArrayList<>();
    public static List<Interceptor> globalNetworkInterceptors = new ArrayList<>();
    public static void installEventListenerFactory(EventListener.Factory factory) {
        globalEventFactory = factory;
    }
    public static void installDns(Dns dns) {
        globalDns = dns;
    }
    public static void installInterceptor(Interceptor interceptor) {
        if(interceptor != null)
            globalInterceptors.add(interceptor);
    }
    public static void installNetworkInterceptors(Interceptor networkInterceptor) {
        if(networkInterceptor != null)
            globalNetworkInterceptors.add(networkInterceptor);
    }
}           

這樣,隻需要為OkHttpHooker預先install好幾個全局的Intercepter/Dns/EventListener即可。

那麼,如何來實作上面OkhttpClient内部Builder中插入四行代碼呢?

首先,我們通過Hunter的架構,可以隐藏掉Transform和ASM絕大部分細節,我們隻需把注意力放在寫ClassVisitor以及MethodVisitor即可。我們一共需要做以下幾步

1、建立一個自定義transform,添加到一個自定義gradle plugin中

2、繼承HunterTransform實作自定義transform

3、實作自定義的ClassVisitor,并依情況實作自定義MethodVisitor

其中第一步文章講解transform一部分有講到,基本是一樣簡短的寫法,我們從第二步講起

繼承HunterTransform,就可以讓你的transform具備并發、增量的功能。

final class OkHttpHunterTransform extends HunterTransform {
    private Project project;
    private OkHttpHunterExtension okHttpHunterExtension;
    public OkHttpHunterTransform(Project project) {
        super(project);
        this.project = project;
        //依情況而定,看看你需不需要有插件擴充
        project.getExtensions().create("okHttpHunterExt", OkHttpHunterExtension.class);
        //必須的一步,繼承BaseWeaver,幫你隐藏ASM細節
        this.bytecodeWeaver = new OkHttpWeaver();
    }
    @Override
    public void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {
        okHttpHunterExtension = (OkHttpHunterExtension) project.getExtensions().getByName("okHttpHunterExt");
        super.transform(context, inputs, referencedInputs, outputProvider, isIncremental);
    }
    // 用于控制修改位元組碼在哪些debug包還是release包下發揮作用,或者完全打開/關閉
    @Override
    protected RunVariant getRunVariant() {
        return okHttpHunterExtension.runVariant;
    }
}
//BaseWeaver幫你隐藏了ASM的很多複雜邏輯
public final class OkHttpWeaver extends BaseWeaver {
    @Override
    protected ClassVisitor wrapClassWriter(ClassWriter classWriter) {
        return new OkHttpClassAdapter(classWriter);
    }
}
//插件擴充
public class OkHttpHunterExtension {
    public RunVariant runVariant = RunVariant.ALWAYS;
    @Override
    public String toString() {
        return "OkHttpHunterExtension{" +
                "runVariant=" + runVariant +
                '}';
    }
}           

好了,Transform寫起來就變得這麼簡單,接下來看自定義ClassVisitor,它在OkHttpWeaver傳回。

我們建立一個ClassVisitor(自定義ClassVisitor是為了代理ClassWriter,前面講過)

public final class OkHttpClassAdapter extends ClassVisitor{
    private String className;
    OkHttpClassAdapter(final ClassVisitor cv) {
        super(Opcodes.ASM5, cv);
    }
    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        super.visit(version, access, name, signature, superName, interfaces);
        this.className = name;
    }
    @Override
    public MethodVisitor visitMethod(final int access, final String name,
                                     final String desc, final String signature, final String[] exceptions) {
        MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
        if(className.equals("okhttp3/OkHttpClient$Builder")) {
            return mv == null ? null : new OkHttpMethodAdapter(className + File.separator + name, access, desc, mv);
        } else {
            return mv;
        }
    }
}           

我們尋找出

okhttp3/OkHttpClient$Builder

這個類,其他類不管它,那麼其他類隻會被普通的複制,而

okhttp3/OkHttpClient$Builder

将會有自定義的MethodVisitor來處理

我們來看看這個MethodVisitor的實作

public final class OkHttpMethodAdapter extends LocalVariablesSorter implements Opcodes {
    private boolean defaultOkhttpClientBuilderInitMethod = false;
    OkHttpMethodAdapter(String name, int access, String desc, MethodVisitor mv) {
        super(Opcodes.ASM5, access, desc, mv);
        if ("okhttp3/OkHttpClient$Builder/<init>".equals(name) && "()V".equals(desc)) {
            defaultOkhttpClientBuilderInitMethod = true;
        }
    }
    @Override
    public void visitInsn(int opcode) {
        if(defaultOkhttpClientBuilderInitMethod) {
            if ((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) {
                //EventListenFactory
                mv.visitVarInsn(ALOAD, 0);
                mv.visitFieldInsn(GETSTATIC, "com/hunter/library/okhttp/OkHttpHooker", "globalEventFactory", "Lokhttp3/EventListener$Factory;");
                mv.visitFieldInsn(PUTFIELD, "okhttp3/OkHttpClient$Builder", "eventListenerFactory", "Lokhttp3/EventListener$Factory;");
                //Dns
                mv.visitVarInsn(ALOAD, 0);
                mv.visitFieldInsn(GETSTATIC, "com/hunter/library/okhttp/OkHttpHooker", "globalDns", "Lokhttp3/Dns;");
                mv.visitFieldInsn(PUTFIELD, "okhttp3/OkHttpClient$Builder", "dns", "Lokhttp3/Dns;");
                //Interceptor
                mv.visitVarInsn(ALOAD, 0);
                mv.visitFieldInsn(GETFIELD, "okhttp3/OkHttpClient$Builder", "interceptors", "Ljava/util/List;");
                mv.visitFieldInsn(GETSTATIC, "com/hunter/library/okhttp/OkHttpHooker", "globalInterceptors", "Ljava/util/List;");
                mv.visitMethodInsn(INVOKEINTERFACE, "java/util/List", "addAll", "(Ljava/util/Collection;)Z", true);
                mv.visitInsn(POP);
                //NetworkInterceptor
                mv.visitVarInsn(ALOAD, 0);
                mv.visitFieldInsn(GETFIELD, "okhttp3/OkHttpClient$Builder", "networkInterceptors", "Ljava/util/List;");
                mv.visitFieldInsn(GETSTATIC, "com/hunter/library/okhttp/OkHttpHooker", "globalNetworkInterceptors", "Ljava/util/List;");
                mv.visitMethodInsn(INVOKEINTERFACE, "java/util/List", "addAll", "(Ljava/util/Collection;)Z", true);
                mv.visitInsn(POP);
            }
        }
        super.visitInsn(opcode);
    }
}           

首先,我們先找出

okhttp3/OkHttpClient$Builder

的構造函數,然後在這個構造函數的末尾,執行插入位元組碼的邏輯,我們可以發現,位元組碼的指令是符合逆波蘭式的,都是操作數在前,操作符在後。

至此,我們隻需要釋出插件,然後apply到我們的項目中即可。

借助Hunter架構,我們很輕松就成功hack了Okhttp,我們就可以用全局統一的Intercepter/Dns/EventListener來監控我們APP的網絡了。

講到這裡,就完整得介紹了如何使用Hunter架構開發一個位元組碼編譯插件,對第三方依賴庫為所欲為。如果對于代碼還有疑惑,可以移步項目首頁,參考完整代碼,以及其他幾個插件的實作。有時間再寫文章介紹其他幾個插件的具體實作。

總結

這篇文章寫到這裡差不多了,全文主要圍繞Hunter展開介紹,分析了如何開發一個高效的修改位元組碼的編譯插件,以及ASM位元組碼技術的一些相關工作流和開發套路。

也歡迎大家前往

Hunter

項目首頁,歡迎使用

架構開發插件,以及使用現有的幾個插件,也歡迎提issue,歡迎star/fork。

現在加Android開發群;701740775,可免費領取一份最新Android進階架構技術體系大綱和視訊資料,以及五年積累整理的所有面試資源筆記。加群請備注csdn領取xx資料