上篇; 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資料