工作原理简介

  1. 编译期间通过代码插桩,在每个方法的最前面插入一段代。这段代码的功能是:如果有热修复代码就走热修逻辑返回,不再走原方法逻辑。
  2. 运行期间,通过新建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())}
}
复制代码

这个方法做了这些事情:

  1. 读取配置文件,初始化配置
  2. 如果是强制插入,则插入代码
  3. 如果是非强制插入,则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如下工作

  1. 把所有需要打包的类放在一个列表里
  2. 根据配置对这些类遍历进行选择性插桩
  3. 将插桩完成的类输出到Transform的输出jar包中
  4. 输出插桩的方法与此方法的id(这个id就是自增1生成的),到一个Map中
  5. 将这个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();
}
复制代码
  1. 这里把所有类的修饰符都变成了public
  2. 如果类需要插桩(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方法的逻辑,此处代码就不贴了)

  1. 方法是synthetic并且不是private的
  2. 方法是abstract,native,interface,decprecated的
  3. 方法在exceptMethodList中的
  4. 方法内没有调用其他方法的

如果需要插桩,用 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了,这段代码的大概意思是

  1. 把参数入操作栈,调用PatchProxy的proxy方法,这个方法会检测是否有热修代码,有的话则执行热修代码,没有则返回,这个方法的返回值PatchProxyResult,PatchProxyResult有两个字段,isSupported与result, result在isSupported为true的情况下为热修代码的返回值

  2. 检查PatchProxyResult的isSupported的值,如果为false,则走原方法代码逻辑,如果为true,分两种情况

  3. 无返回值,直接返回

  4. 有返回值,从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相关推荐

  1. 【Android 修炼手册】Gradle 篇 -- Android Gradle Plugin 主要 Task 分析

    上文回顾 上篇文章里讲了 android gradle plugin 的整体流程,引入插件以后生成了很多 Task,这篇文章就谈谈生成的这些 Task 都有什么用处,以及一些主要 Task 的实现 预 ...

  2. 【Android 修炼手册】Gradle 篇 -- Android Gradle Plugin 主要流程分析

    预备知识 理解 gradle 的基本开发 了解 gradle task 和 plugin 使用及开发 了解 android gradle plugin 的使用 看完本文可以达到什么程度 了解 andr ...

  3. Android Gradle Plugin 源码解析(上)

    一.源码依赖 本文基于: android gradle plugin版本: com.android.tools.build:gradle:2.3.0 gradle 版本:4.1 Gradle源码总共3 ...

  4. Android Studio——[The ‘kotlin-android-extensions‘ Gradle plugin is deprecated.]解决方案

    问题描述 问题分析 出现这个提示是因为'kotlin-android-extensions'是android studio不赞成使用的. 解决方案 修改build.gradle sync now 参考 ...

  5. struts2请求过程源代码分析

    struts2请求过程源代码分析 Struts2是Struts社区和WebWork社区的共同成果.我们甚至能够说,Struts2是WebWork的升级版.他採用的正是WebWork的核心,所以.Str ...

  6. 你绝对能看懂的Kafka源代码分析-Kafka Producer设计分析

    之前我写了<Kafka入门教程轻松学>系列文章,半年来有1万多的阅读量,虽然不算很多,但看到很多朋友的支持,也给了我继续写下去的动力.接下来我会再写一个系列文章----<你绝对能看懂 ...

  7. 如何看懂源代码--(分析源代码方法) 1

    如何看懂源代码--(分析源代码方法)  -- 转载 作者: fandyst 出处: http://www.cnblogs.com/todototry/ 原文: https://www.cnblogs. ...

  8. Android 热修复 Tinker Gradle Plugin解析

    本文已在我的公众号hongyangAndroid原创首发. 转载请标明出处: http://blog.csdn.net/lmj623565791/article/details/72667669 本文 ...

  9. OkHttp从使用到源代码分析(2)-请求的使用方法

    之前说到OKHttp网络请求支持两种方式:同步请求和异步请求,同时又存在get和post请求,那么就是2*2,一共四种情况,接下来就分别介绍下这四种请求的使用和区别 在gradle中指定Java版本 ...

最新文章

  1. 使用Jupyter notebooks上传文件夹或大量数据到服务器 有解压缩ZIP
  2. springboot知识
  3. QML on Android 在小米5s手机上中文字体显示异常
  4. JAVA序列化和反序列化
  5. Linux命令TOP TEN
  6. 计组之中央处理器:5、微程序控制器(组成、原理、概念对比)
  7. 【Siddhi 5】Siddhi 自定义函数@Extension使用
  8. gmat模考_国外GMAT高分学霸们都在用什么复习资料?(模考篇)
  9. 语言密码加密变星号_为什么汉字不能设成密码,你想过吗?
  10. 在Exchange Server 2007中修改邮件接受域
  11. xvid-core1.1.2编译方法(vc6,vs2005)
  12. 爬取学校教务网课表与成绩 java版
  13. 极限编程缺点_极限编程(XP)的优缺点是什么?
  14. mysql execute stmt_25.2.7.10. mysql_stmt_execute()
  15. 马云回应豪宅谣言;淘宝上线了三架波音747进行拍卖;迪拜投1.4亿美元建模拟火星丨价值早报
  16. 计算机上的小键盘,电脑小键盘关闭方法有哪些 小键盘上的六个键都有什么用...
  17. iOS基础 关于UIKit框架
  18. linux中安装无线网卡驱动
  19. 第三方登录mysql表_浅谈数据库用户表结构设计,第三方登录
  20. 获取浏览器和屏幕各种高度宽度

热门文章

  1. python银行系统-python银行系统实现源码
  2. python银行系统-python 银行系统
  3. python中一共有多少个关键字-python – 搜索多个关键字的字符串列表
  4. python课程费用-上海Python数据分析课程
  5. python app教程-Python zipapp打包教程(超级详细)
  6. 错误:cl: 命令行 error D8021 :无效的数值参数“/Wno-cpp”
  7. LeetCode Find the Difference
  8. 题目1254:N皇后问题(DFS)
  9. 电脑内存和磁盘空间有什么区别与联系
  10. 解决svn working copy locked问题