Robust 源代码分析之gradle-plugin
工作原理简介
- 编译期间通过代码插桩,在每个方法的最前面插入一段代。这段代码的功能是:如果有热修复代码就走热修逻辑返回,不再走原方法逻辑。
- 运行期间,通过新建ClassLoader的方式,加载包含热修复代码的Dex。
编译期做了哪些工作
gradle脚本插件的入口方法在RobustTransform的apply方法中
void apply(Project target) {project = targetrobust = new XmlSlurper().parse(new File("${project.projectDir}/${Constants.ROBUST_XML}"))logger = project.loggerinitConfig()//isForceInsert 是true的话,则强制执行插入if (!isForceInsert) {if (!isDebugTask) {project.android.registerTransform(this)project.afterEvaluate(new RobustApkHashAction())logger.quiet "Register robust transform successful !!!"}} else {project.android.registerTransform(this)project.afterEvaluate(new RobustApkHashAction())}
}
复制代码
这个方法做了这些事情:
- 读取配置文件,初始化配置
- 如果是强制插入,则插入代码
- 如果是非强制插入,则Debug打包模式下不插入代码
初始化配置的代码如下:
def initConfig() {hotfixPackageList = new ArrayList<>()hotfixMethodList = new ArrayList<>()exceptPackageList = new ArrayList<>()exceptMethodList = new ArrayList<>()isHotfixMethodLevel = false;isExceptMethodLevel = false;/*对文件进行解析*/for (name in robust.packname.name) {hotfixPackageList.add(name.text());}for (name in robust.exceptPackname.name) {exceptPackageList.add(name.text());}for (name in robust.hotfixMethod.name) {hotfixMethodList.add(name.text());}for (name in robust.exceptMethod.name) {exceptMethodList.add(name.text());}if (null != robust.switch.filterMethod && "true".equals(String.valueOf(robust.switch.turnOnHotfixMethod.text()))) {isHotfixMethodLevel = true;}if (null != robust.switch.useAsm && "false".equals(String.valueOf(robust.switch.useAsm.text()))) {useASM = false;}else {//默认使用asmuseASM = true;}if (null != robust.switch.filterMethod && "true".equals(String.valueOf(robust.switch.turnOnExceptMethod.text()))) {isExceptMethodLevel = true;}if (robust.switch.forceInsert != null && "true".equals(String.valueOf(robust.switch.forceInsert.text())))isForceInsert = trueelseisForceInsert = false}
复制代码
分别读取了如下配置
- packname 需要插入代码的包名或者类名
- exceptPackname 不需要插入代码的包名或者类名
- hotfixMethod 需要插入代码的方法名
- exceptMethod 不需要插入代码的方法名
- filterMethod与turnOnHotfixMethod 与hotfixMethod配合插桩
- useAsm 是否使用Asm进行插入
- filterMethod与turnOnExceptMethod 与exceptMethod配合过滤方法
- forceInsert 强制插入,Debug是否也进行插入代码
如果需要插入代码,则将RobustTransform与RobustApkHashAction注册
RobustTransform
void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {logger.quiet '================robust start================'def startTime = System.currentTimeMillis()outputProvider.deleteAll()File jarFile = outputProvider.getContentLocation("main", getOutputTypes(), getScopes(),Format.JAR);if(!jarFile.getParentFile().exists()){jarFile.getParentFile().mkdirs();}if(jarFile.exists()){jarFile.delete();}ClassPool classPool = new ClassPool()project.android.bootClasspath.each {classPool.appendClassPath((String) it.absolutePath)}def box = ConvertUtils.toCtClasses(inputs, classPool)def cost = (System.currentTimeMillis() - startTime) / 1000
// logger.quiet "check all class cost $cost second, class count: ${box.size()}"if(useASM){insertcodeStrategy=new AsmInsertImpl(hotfixPackageList,hotfixMethodList,exceptPackageList,exceptMethodList,isHotfixMethodLevel,isExceptMethodLevel);}else {insertcodeStrategy=new JavaAssistInsertImpl(hotfixPackageList,hotfixMethodList,exceptPackageList,exceptMethodList,isHotfixMethodLevel,isExceptMethodLevel);}insertcodeStrategy.insertCode(box, jarFile);writeMap2File(insertcodeStrategy.methodMap, Constants.METHOD_MAP_OUT_PATH)
}
复制代码
这个Transform如下工作
- 把所有需要打包的类放在一个列表里
- 根据配置对这些类遍历进行选择性插桩
- 将插桩完成的类输出到Transform的输出jar包中
- 输出插桩的方法与此方法的id(这个id就是自增1生成的),到一个Map中
- 将这个Map输出到 /outputs/robust/methodsMap.robust中
除了第二步骤,相对都比较简单,我们忽略其他步骤,主要看下第2步骤的实现
根据配置对这些类遍历进行选择性插桩
if(useASM){insertcodeStrategy=new AsmInsertImpl(hotfixPackageList,hotfixMethodList,exceptPackageList,exceptMethodList,isHotfixMethodLevel,isExceptMethodLevel);
}else {insertcodeStrategy=new JavaAssistInsertImpl(hotfixPackageList,hotfixMethodList,exceptPackageList,exceptMethodList,isHotfixMethodLevel,isExceptMethodLevel);
}
insertcodeStrategy.insertCode(box, jarFile);
复制代码
这里的插桩逻辑可以根据配置选择是使用asm还是javaassit,我们详细的看下ASM的实现
protected void insertCode(List<CtClass> box, File jarFile) throws IOException, CannotCompileException {ZipOutputStream outStream = new JarOutputStream(new FileOutputStream(jarFile));//get every class in the box ,ready to insert codefor (CtClass ctClass : box) {//change modifier to public ,so all the class in the apk will be public ,you will be able to access it in the patchctClass.setModifiers(AccessFlag.setPublic(ctClass.getModifiers()));if (isNeedInsertClass(ctClass.getName()) && !(ctClass.isInterface() || ctClass.getDeclaredMethods().length < 1)) {//only insert code into specific classeszipFile(transformCode(ctClass.toBytecode(), ctClass.getName().replaceAll("\\.", "/")), outStream, ctClass.getName().replaceAll("\\.", "/") + ".class");} else {zipFile(ctClass.toBytecode(), outStream, ctClass.getName().replaceAll("\\.", "/") + ".class");}}outStream.close();
}
复制代码
- 这里把所有类的修饰符都变成了public
- 如果类需要插桩(isNeedInsertClass返回true的基础上还要求不能是接口以及累的方法数大于1),则把插桩后的类输出到jar包中,否则把原始类输出到jar包中
类是否需要插桩的判断 isNeedInsertClass
protected boolean isNeedInsertClass(String className) {//这样子可以在需要埋点的剔除指定的类for (String exceptName : exceptPackageList) {if (className.startsWith(exceptName)) {return false;}}for (String name : hotfixPackageList) {if (className.startsWith(name)) {return true;}}return false;
}
复制代码
代码十分简单,如果这个类的名字在exceptPackageList中,则返回false;如果这个类的名字在hotfixPackageList中,则返回true
对类插桩 transformCode
public byte[] transformCode(byte[] b1, String className) throws IOException {ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);ClassReader cr = new ClassReader(b1);ClassNode classNode = new ClassNode();Map<String, Boolean> methodInstructionTypeMap = new HashMap<>();cr.accept(classNode, 0);final List<MethodNode> methods = classNode.methods;for (MethodNode m : methods) {InsnList inList = m.instructions;boolean isMethodInvoke = false;for (int i = 0; i < inList.size(); i++) {if (inList.get(i).getType() == AbstractInsnNode.METHOD_INSN) {isMethodInvoke = true;}}methodInstructionTypeMap.put(m.name + m.desc, isMethodInvoke);}InsertMethodBodyAdapter insertMethodBodyAdapter = new InsertMethodBodyAdapter(cw, className, methodInstructionTypeMap);cr.accept(insertMethodBodyAdapter, ClassReader.EXPAND_FRAMES);return cw.toByteArray();
}
复制代码
这段代码是把这个类中的所有方法列出来,然后把方法名字和方法的descriptor拼接起来作为key值,这个方法内部是否有方法调用指令做为value值存储起来,传递给InsertMethodBodyAdapter去对方法进行插桩
InsertMethodBodyAdapter是这样事儿的
public InsertMethodBodyAdapter(ClassWriter cw, String className, Map<String, Boolean> methodInstructionTypeMap) {super(Opcodes.ASM5, cw);this.classWriter = cw;this.className = className;this.methodInstructionTypeMap = methodInstructionTypeMap;//insert the fieldclassWriter.visitField(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, Constants.INSERT_FIELD_NAME, Type.getDescriptor(ChangeQuickRedirect.class), null, null);
}@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {if (isProtect(access)) {access = setPublic(access);}MethodVisitor mv = super.visitMethod(access, name,desc, signature, exceptions);if (!isQualifiedMethod(access, name, desc, methodInstructionTypeMap)) {return mv;}//record method numbermethodMap.put(className.replace('/', '.') + "." + name + "(" + parameters.toString() + ")", insertMethodCount.incrementAndGet());return new MethodBodyInsertor(mv, className, desc, isStatic(access), String.valueOf(insertMethodCount.get()), name, access);
}
复制代码
InsertMethodBodyAdapter显示对类插入了一个静态的ChangeQuickRedirect类型的字段changeQuickRedirect,然后就开始了对类的每个方法的访问,访问前判断如果方法是protected 那么就把方法修改为public 接着去判断是否方法需要插桩,下列情况的方法不进行插桩(isQualifiedMethod方法的逻辑,此处代码就不贴了)
- 方法是synthetic并且不是private的
- 方法是abstract,native,interface,decprecated的
- 方法在exceptMethodList中的
- 方法内没有调用其他方法的
如果需要插桩,用 MethodBodyInsertor对这个方法进行插桩
对方法插桩 transformCode
@Override
public void visitCode() {//insert code hereRobustAsmUtils.createInsertCode(this, className, paramsTypeClass, returnType, isStatic, Integer.valueOf(methodId));
}public static void createInsertCode(GeneratorAdapter mv, String className, List<Type> args, Type returnType, boolean isStatic, int methodId) {prepareMethodParameters(mv, className, args, returnType, isStatic, methodId);//开始调用mv.visitMethodInsn(Opcodes.INVOKESTATIC,PROXYCLASSNAME,"proxy","([Ljava/lang/Object;Ljava/lang/Object;" + REDIRECTCLASSNAME + "ZI[Ljava/lang/Class;Ljava/lang/Class;)Lcom/meituan/robust/PatchProxyResult;",false);int local = mv.newLocal(Type.getType("Lcom/meituan/robust/PatchProxyResult;"));mv.storeLocal(local);mv.loadLocal(local);mv.visitFieldInsn(Opcodes.GETFIELD, "com/meituan/robust/PatchProxyResult", "isSupported", "Z");// if isSupportedLabel l1 = new Label();mv.visitJumpInsn(Opcodes.IFEQ, l1);//判断是否有返回值,代码不同if ("V".equals(returnType.getDescriptor())) {mv.visitInsn(Opcodes.RETURN);} else {mv.loadLocal(local);mv.visitFieldInsn(Opcodes.GETFIELD, "com/meituan/robust/PatchProxyResult", "result", "Ljava/lang/Object;");//强制转化类型if (!castPrimateToObj(mv, returnType.getDescriptor())) {//这里需要注意,如果是数组类型的直接使用即可,如果非数组类型,就得去除前缀了,还有最终是没有结束符;//比如:Ljava/lang/String; ==》 java/lang/StringString newTypeStr = null;int len = returnType.getDescriptor().length();if (returnType.getDescriptor().startsWith("[")) {newTypeStr = returnType.getDescriptor().substring(0, len);} else {newTypeStr = returnType.getDescriptor().substring(1, len - 1);}mv.visitTypeInsn(Opcodes.CHECKCAST, newTypeStr);}//这里还需要做返回类型不同返回指令也不同mv.visitInsn(getReturnTypeCode(returnType.getDescriptor()));}mv.visitLabel(l1);
}
复制代码
终于来到对方法进行插桩的位置 RobustAsmUtils.createInsertCode,不过这段代码很ASM,在这里就不讲怎么使用ASM了,这段代码的大概意思是
把参数入操作栈,调用PatchProxy的proxy方法,这个方法会检测是否有热修代码,有的话则执行热修代码,没有则返回,这个方法的返回值PatchProxyResult,PatchProxyResult有两个字段,isSupported与result, result在isSupported为true的情况下为热修代码的返回值
检查PatchProxyResult的isSupported的值,如果为false,则走原方法代码逻辑,如果为true,分两种情况
无返回值,直接返回
有返回值,从PatchProxyResult的result取出结果返回
最终代码就是下面这种样子
//不带返回值的
private void sayHello() {if (!PatchProxy.proxy(new Object[0], this, n, false, 3, new Class[0], Void.TYPE).isSupported) {delegate.sayHello((Activity) this, 1);}
}
//带返回值的
private boolean isSpeakEnable() {PatchProxyResult proxy = PatchProxy.proxy(new Object[0], this, n, false, 2, new Class[0], Boolean.TYPE);return proxy.isSupported ? ((Boolean) proxy.result).booleanValue() : delegate.isSpeakEnable();
}
复制代码
至此插桩就完成了,回到最开始的RobustApkHashAction
RobustApkHashAction
这段代码就不贴了,因为真的是很简单,就是把代码res文件,dex文件,javaResource文件,jni文件,assets文件添加到一个zip包中,计算一下这个zip文件的hash值,然后在assets文件中创建一个robust.apkhash文件,把这个值写入即可
至此在打我们的应用包时,robust所做的工作就完成了,整体来看是比较简单的,流程也很清晰,唯一代码的阅读难点可能就是在方法插入代码时,需要一些ASM相关的知识以及需要懂一些jvm运行时的一些知识,才能读懂在方法中插入的指令有哪些作用以及为什么如此插入。
后续
后续还会针对robust写一篇解析文章,主要是针对打patch包时,robust做了些什么,robust加载patch包的代码非常简单,就不做分析了,另外,后续还会写一下字节码插桩相关内容的文章
转载于:https://juejin.im/post/5cc7f0915188252da4250717
Robust 源代码分析之gradle-plugin相关推荐
- 【Android 修炼手册】Gradle 篇 -- Android Gradle Plugin 主要 Task 分析
上文回顾 上篇文章里讲了 android gradle plugin 的整体流程,引入插件以后生成了很多 Task,这篇文章就谈谈生成的这些 Task 都有什么用处,以及一些主要 Task 的实现 预 ...
- 【Android 修炼手册】Gradle 篇 -- Android Gradle Plugin 主要流程分析
预备知识 理解 gradle 的基本开发 了解 gradle task 和 plugin 使用及开发 了解 android gradle plugin 的使用 看完本文可以达到什么程度 了解 andr ...
- Android Gradle Plugin 源码解析(上)
一.源码依赖 本文基于: android gradle plugin版本: com.android.tools.build:gradle:2.3.0 gradle 版本:4.1 Gradle源码总共3 ...
- Android Studio——[The ‘kotlin-android-extensions‘ Gradle plugin is deprecated.]解决方案
问题描述 问题分析 出现这个提示是因为'kotlin-android-extensions'是android studio不赞成使用的. 解决方案 修改build.gradle sync now 参考 ...
- struts2请求过程源代码分析
struts2请求过程源代码分析 Struts2是Struts社区和WebWork社区的共同成果.我们甚至能够说,Struts2是WebWork的升级版.他採用的正是WebWork的核心,所以.Str ...
- 你绝对能看懂的Kafka源代码分析-Kafka Producer设计分析
之前我写了<Kafka入门教程轻松学>系列文章,半年来有1万多的阅读量,虽然不算很多,但看到很多朋友的支持,也给了我继续写下去的动力.接下来我会再写一个系列文章----<你绝对能看懂 ...
- 如何看懂源代码--(分析源代码方法) 1
如何看懂源代码--(分析源代码方法) -- 转载 作者: fandyst 出处: http://www.cnblogs.com/todototry/ 原文: https://www.cnblogs. ...
- Android 热修复 Tinker Gradle Plugin解析
本文已在我的公众号hongyangAndroid原创首发. 转载请标明出处: http://blog.csdn.net/lmj623565791/article/details/72667669 本文 ...
- OkHttp从使用到源代码分析(2)-请求的使用方法
之前说到OKHttp网络请求支持两种方式:同步请求和异步请求,同时又存在get和post请求,那么就是2*2,一共四种情况,接下来就分别介绍下这四种请求的使用和区别 在gradle中指定Java版本 ...
最新文章
- 使用Jupyter notebooks上传文件夹或大量数据到服务器 有解压缩ZIP
- springboot知识
- QML on Android 在小米5s手机上中文字体显示异常
- JAVA序列化和反序列化
- Linux命令TOP TEN
- 计组之中央处理器:5、微程序控制器(组成、原理、概念对比)
- 【Siddhi 5】Siddhi 自定义函数@Extension使用
- gmat模考_国外GMAT高分学霸们都在用什么复习资料?(模考篇)
- 语言密码加密变星号_为什么汉字不能设成密码,你想过吗?
- 在Exchange Server 2007中修改邮件接受域
- xvid-core1.1.2编译方法(vc6,vs2005)
- 爬取学校教务网课表与成绩 java版
- 极限编程缺点_极限编程(XP)的优缺点是什么?
- mysql execute stmt_25.2.7.10. mysql_stmt_execute()
- 马云回应豪宅谣言;淘宝上线了三架波音747进行拍卖;迪拜投1.4亿美元建模拟火星丨价值早报
- 计算机上的小键盘,电脑小键盘关闭方法有哪些 小键盘上的六个键都有什么用...
- iOS基础 关于UIKit框架
- linux中安装无线网卡驱动
- 第三方登录mysql表_浅谈数据库用户表结构设计,第三方登录
- 获取浏览器和屏幕各种高度宽度
热门文章
- python银行系统-python银行系统实现源码
- python银行系统-python 银行系统
- python中一共有多少个关键字-python – 搜索多个关键字的字符串列表
- python课程费用-上海Python数据分析课程
- python app教程-Python zipapp打包教程(超级详细)
- 错误:cl: 命令行 error D8021 :无效的数值参数“/Wno-cpp”
- LeetCode Find the Difference
- 题目1254:N皇后问题(DFS)
- 电脑内存和磁盘空间有什么区别与联系
- 解决svn working copy locked问题