工作原理简介
- 编译期间通过代码插桩,在每个方法的最前面插入一段代。这段代码的功能是:如果有热修复代码就走热修逻辑返回,不再走原方法逻辑。
- 运行期间,通过新建ClassLoader的方式,加载包含热修复代码的Dex。
编译期做了哪些工作
gradle脚本插件的入口方法在RobustTransform的apply方法中
void apply(Project target) {
project = target
robust = new XmlSlurper().parse(new File("${project.projectDir}/${Constants.ROBUST_XML}"))
logger = project.logger
initConfig()
//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 {
//默认使用asm
useASM = 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 = true
else
isForceInsert = 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 code
for (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 patch
ctClass.setModifiers(AccessFlag.setPublic(ctClass.getModifiers()));
if (isNeedInsertClass(ctClass.getName()) && !(ctClass.isInterface() || ctClass.getDeclaredMethods().length < 1)) {
//only insert code into specific classes
zipFile(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 field
classWriter.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 number
methodMap.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 here
RobustAsmUtils.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 isSupported
Label 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/String
String 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包的代码非常简单,就不做分析了,另外,后续还会写一下字节码插桩相关内容的文章