您现在的位置是:首页 > 文章详情

一起玩转Android项目中的字节码(下)

日期:2018-12-08点击:405

上篇;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内容时,保留真正的行数

开启后,你看到的是这样

我们可以发现,17行和18行,分别包含了三句代码。

而开启选项之前是这样

那么如何开启这个选项呢?Mac下cmd + shift + A输入Registry,勾选这两个选项

其实无论字节码和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项目主页,欢迎使用Hunter框架开发插件,以及使用现有的几个插件,也欢迎提issue,欢迎star/fork。

现在加Android开发群;701740775,可免费领取一份最新Android高级架构技术体系大纲和视频资料,以及五年积累整理的所有面试资源笔记。加群请备注csdn领取xx资料

原文链接:https://yq.aliyun.com/articles/687835
关注公众号

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。

持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。

文章评论

共有0条评论来说两句吧...

文章二维码

扫描即可查看该文章

点击排行

推荐阅读

最新文章