Java层热修复框架实践
来源:互联网 发布:一人能做好淘宝网店吗 编辑:程序博客网 时间:2024/05/28 18:42
结合上一篇研究的内容,我们在这一篇实现一个简单的HotFix框架。
上一篇有一个重要的内容没有讲,就是在实现方法的替换后,原来的方法中的内存就会被覆盖,如果我们还想要调用原来的方法怎么办呢?所以我们需要找个地方把原来的方法存起来,不过在具体实现的时候,会遇到一个问题,就是 Java的非static,非private的方法默认是虚方法,在调用这个方法的时候会有一个类似查找虚函数表的过程:
mirror::Object* receiver = nullptr;if (!m->IsStatic()) { // Check that the receiver is non-null and an instance of the field's declaring class. receiver = soa.Decode<mirror::Object*>(javaReceiver); if (!VerifyObjectIsClass(receiver, declaring_class)) { return NULL; } // Find the actual implementation of the virtual method. m = receiver->GetClass()->FindVirtualMethodForVirtualOrInterface(m);}
在调用的时候,如果不是static的方法,会去查找这个方法的真正实现;我们直接把原方法做了备份之后,去调用备份的那个方法,如果此方法是public的,则会查找到原来的那个函数,于是就无限循环了;我们只需要阻止这个过程,查看 FindVirtualMethodForVirtualOrInterface 这个方法的实现就知道,只要方法是 invoke-direct 进行调用的,就会直接返回原方法,这些方法包括:构造函数,private的方法(见https://source.android.com/devices/tech/dalvik/dalvik-bytecode.html) 因此,我们手动把这个备份的方法属性修改为private即可解决这个问题。
核心问题都解决了,下面我们把这些内容整合到一个HotFix的核心类中:
package com.amuro.hotfix;import android.os.Build;import android.util.Pair;import java.lang.reflect.AccessibleObject;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.lang.reflect.Method;import java.lang.reflect.Modifier;import java.nio.Buffer;import java.nio.ByteBuffer;import java.nio.ByteOrder;import java.util.Map;import java.util.concurrent.ConcurrentHashMap;/** * Created by Amuro on 2017/9/27. */public class Hotfix{ private static class Memory { static byte peekByte(long address) { return (Byte) ReflectUtils.invokeStaticMethod( "libcore.io.Memory", "peekByte", new Class[]{long.class}, new Object[]{address} ); } static void pokeByte(long address, byte value) { ReflectUtils.invokeStaticMethod( "libcore.io.Memory", "pokeByte", new Class[]{long.class, byte.class}, new Object[]{address, value} ); } static void memcpy(long dst, long src, long length) { for (long i = 0; i < length; i++) { pokeByte(dst, peekByte(src)); dst++; src++; } } } private static class Unsafe { static final String UNSAFE_CLASS_NAME = "sun.misc.Unsafe"; static Object UNSAFE_CLASS_INSTANCE = ReflectUtils.getStaticFieldObj(UNSAFE_CLASS_NAME, "THE_ONE"); static long getObjectAddress(Object obj) { Object[] args = {obj}; Integer baseOffset = (Integer) ReflectUtils.invokeMethod( UNSAFE_CLASS_NAME, UNSAFE_CLASS_INSTANCE, "arrayBaseOffset", new Class[]{Class.class}, new Object[]{Object[].class} ); long result = ((Number) ReflectUtils.invokeMethod( UNSAFE_CLASS_NAME, UNSAFE_CLASS_INSTANCE, "getInt", new Class[]{Object.class, long.class}, new Object[]{args, baseOffset.longValue()} )).longValue(); return result; } } private static class MethodDecoder { static long sMethodSize = -1; public static void ruler1() { } public static void ruler2() { } static long getMethodAddress(Method method) { Object mirrorMethod = ReflectUtils.getFieldObj( Method.class.getSuperclass().getName(), method, "artMethod" ); if (mirrorMethod.getClass().equals(Long.class)) { return (Long) mirrorMethod; } return Unsafe.getObjectAddress(mirrorMethod); } static long getArtMethodSize() { if (sMethodSize > 0) { return sMethodSize; } try { Method m1 = MethodDecoder.class.getDeclaredMethod("ruler1"); Method m2 = MethodDecoder.class.getDeclaredMethod("ruler2"); sMethodSize = getMethodAddress(m2) - getMethodAddress(m1); } catch (Exception e) { e.printStackTrace(); } return sMethodSize; } } private static Map<Pair<String, String>, Method> sBackups = new ConcurrentHashMap<>(); protected static void hook(Method origin, Method replace) { // 1. backup Method backUpMethod = backUp(origin, replace); sBackups.put( Pair.create(replace.getDeclaringClass().getName(), replace.getName()), backUpMethod ); // 2. replace method long addressOrigin = MethodDecoder.getMethodAddress(origin); long addressReplace = MethodDecoder.getMethodAddress(replace); Memory.memcpy( addressOrigin, addressReplace, MethodDecoder.getArtMethodSize()); } protected static Object callOrigin(Object receiver, Object... params) { StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); StackTraceElement currentStack = stackTrace[4]; Method method = sBackups.get( Pair.create(currentStack.getClassName(), currentStack.getMethodName())); try { return method.invoke(receiver, params); } catch (Exception e) { e.printStackTrace(); } return null; } private static Method backUp(Method origin, Method replace) { try { if (Build.VERSION.SDK_INT < 23) { Class<?> artMethodClass = Class.forName("java.lang.reflect.ArtMethod"); Field accessFlagsField = artMethodClass.getDeclaredField("accessFlags"); accessFlagsField.setAccessible(true); Constructor<?> artMethodConstructor = artMethodClass.getDeclaredConstructor(); artMethodConstructor.setAccessible(true); Object newArtMethod = artMethodConstructor.newInstance(); Constructor<Method> methodConstructor = Method.class.getDeclaredConstructor(artMethodClass); Method newMethod = methodConstructor.newInstance(newArtMethod); newMethod.setAccessible(true); Memory.memcpy(MethodDecoder.getMethodAddress(newMethod), MethodDecoder.getMethodAddress(origin), MethodDecoder.getArtMethodSize()); Integer accessFlags = (Integer) accessFlagsField.get(newArtMethod); accessFlags &= ~Modifier.PUBLIC; accessFlags |= Modifier.PRIVATE; accessFlagsField.set(newArtMethod, accessFlags); return newMethod; } else { // AbstractMethod Class<?> abstractMethodClass = Method.class.getSuperclass(); Field accessFlagsField = abstractMethodClass.getDeclaredField("accessFlags"); Field artMethodField = abstractMethodClass.getDeclaredField("artMethod"); accessFlagsField.setAccessible(true); artMethodField.setAccessible(true); // make the construct accessible, we can not just use `setAccessible` Constructor<Method> methodConstructor = Method.class.getDeclaredConstructor(); Field override = AccessibleObject.class.getDeclaredField( Build.VERSION.SDK_INT == Build.VERSION_CODES.M ? "flag" : "override"); override.setAccessible(true); override.set(methodConstructor, true); // clone the origin method Method newMethod = methodConstructor.newInstance(); newMethod.setAccessible(true); for (Field field : abstractMethodClass.getDeclaredFields()) { field.setAccessible(true); field.set(newMethod, field.get(origin)); } // allocate new artMethod struct, we can not use memory managed by JVM int artMethodSize = (int) MethodDecoder.getArtMethodSize(); ByteBuffer artMethod = ByteBuffer.allocateDirect(artMethodSize); Long artMethodAddress; int ACC_FLAG_OFFSET; if (Build.VERSION.SDK_INT < 24) { // Below Android N, the jdk implementation is not openjdk artMethodAddress = (Long) ReflectUtils.getFieldObj( Buffer.class.getName(), artMethod, "effectiveDirectAddress" ); // http://androidxref.com/6.0.0_r1/xref/art/runtime/art_method.h // GCRoot * 3, sizeof(GCRoot) = sizeof(mirror::CompressedReference) = sizeof(mirror::ObjectReference) = sizeof(uint32_t) = 4 ACC_FLAG_OFFSET = 12; } else { artMethodAddress = (Long) ReflectUtils.invokeMethod( artMethod.getClass().getName(), artMethod, "address", null, null );// (Long) Reflection.call(artMethod.getClass(), null, "address", artMethod, null, null); // http://androidxref.com/7.0.0_r1/xref/art/runtime/art_method.h // sizeof(GCRoot) = 4 ACC_FLAG_OFFSET = 4; } Memory.memcpy( artMethodAddress, MethodDecoder.getMethodAddress(origin), artMethodSize); byte[] newMethodBytes = new byte[artMethodSize]; artMethod.get(newMethodBytes); // replace the artMethod of our new method artMethodField.set(newMethod, artMethodAddress); // modify the access flag of the new method Integer accessFlags = (Integer) accessFlagsField.get(origin); int privateAccFlag = accessFlags & ~Modifier.PUBLIC | Modifier.PRIVATE; accessFlagsField.set(newMethod, privateAccFlag); // 1. try big endian artMethod.order(ByteOrder.BIG_ENDIAN); int nativeAccFlags = artMethod.getInt(ACC_FLAG_OFFSET); if (nativeAccFlags == accessFlags) { // hit! artMethod.putInt(ACC_FLAG_OFFSET, privateAccFlag); } else { // 2. try little endian artMethod.order(ByteOrder.LITTLE_ENDIAN); nativeAccFlags = artMethod.getInt(ACC_FLAG_OFFSET); if (nativeAccFlags == accessFlags) { artMethod.putInt(ACC_FLAG_OFFSET, privateAccFlag); } else { // the offset is error! throw new RuntimeException("native set access flags error!"); } } return newMethod; } } catch (Exception e) { return null; } }}
这里最复杂的就是backup方法了,主要就是把原来method的public属性改成private的,避免上述的死循环问题。感兴趣的童鞋可以去翻Method的父类AbstractMethod的源码,大量的反射都来自源码的阅读理解。
好了,核心的工具类搞定了,下面就是怎么弄出我们的补丁和加载补丁了。之前我们分析AndFix的补丁知道,AndFix的补丁本质就是配置文件加一个dex文件,这个dex的本质可以总结为以下两点:
1.在两个apk对比时AndFix找到了方法发生变动的某个类,这里设有问题的类是A,patch后的是B,但两者的名字是一模一样的;
2.按照一定的命名规则把B中的类名做一个修改,然后找到发生变化的方法,在方法上加上注解;
3.把这些被修改过的类做成一个dex文件。
知道了补丁的本质,我们也可以手工写出这样的补丁类,然后自己打包成dex用就可以了,这里举个例子,先写个有问题的类:
注意包名是cn.cmgame.demo。
package cn.cmgame.demo;import android.support.v7.app.AppCompatActivity;import android.os.Bundle;import android.view.View;import android.widget.TextView;import android.widget.Toast;import com.amuro.hotfix.HotfixManager;public class MainActivity extends AppCompatActivity{ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); findViewById(R.id.bt_mk_bug).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { callBug(); } }); findViewById(R.id.bt_fix_bug).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { doFix(); } }); } private void callBug() { showToast("I'm a bug"); } private void doFix() { try { HotfixManager.getInstance().init(this); HotfixManager.getInstance().fix(); } catch (Exception e) { e.printStackTrace(); } } private void showToast(String msg) { Toast.makeText(this, msg, Toast.LENGTH_SHORT).show(); }}
然后我们自己定义一个规则写一个补丁类,我这里的规则是原包名下加一个fix包,类名加一个_FIX后缀,然后仿照AndFix写一个要被replace的注解:
package com.amuro.hotfix;/** * Created by Amuro on 2017/9/29. */public @interface MethodReplace{ String className(); String methodName();}
万事俱备,我们可以把这个补丁类写出来了:
package cn.cmgame.demo.fix;import android.os.Bundle;import android.support.v7.app.AppCompatActivity;import android.view.View;import android.widget.Toast;import com.amuro.hotfix.HotfixManager;import com.amuro.hotfix.MethodReplace;import cn.cmgame.demo.R;public class MainActivity_FIX extends AppCompatActivity{ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); findViewById(R.id.bt_mk_bug).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { callBug(); } }); findViewById(R.id.bt_fix_bug).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { doFix(); } }); } @MethodReplace(className = "cn.cmgame.demo.MainActivity", methodName = "callBug") private void callBug() { showToast("bug has been fixed"); HotfixManager.getInstance().callOrigin(this, new Object[]{}); } private void doFix() { try { HotfixManager.getInstance().init(this); HotfixManager.getInstance().fix(); } catch (Exception e) { e.printStackTrace(); } } private void showToast(String msg) { Toast.makeText(this, msg, Toast.LENGTH_SHORT).show(); }}
编译生成class文件,提取这个文件用dx工具打成dex文件,我们的补丁文件就ok了,下面就是怎么加载这个补丁了,这里我们要用到插件化中的动态加载技术,不太熟的同学可以找我之前的blog看,这里不再赘述。根据框架的设计原则,我们也封装了一个外观类:
package com.amuro.hotfix;import android.content.Context;import android.os.Environment;import java.io.File;import java.io.IOException;import java.lang.reflect.Array;import java.lang.reflect.Method;import java.util.Enumeration;import dalvik.system.DexClassLoader;import dalvik.system.DexFile;/** * Created by Amuro on 2017/9/28. */public class HotfixManager{ private HotfixManager(){} private static HotfixManager instance = new HotfixManager(); public static HotfixManager getInstance() { return instance; } private static final String SD_CARD_PATH = Environment.getExternalStorageDirectory().getAbsolutePath(); private static final String PATCH_PATH = "sdk_patch"; private Context appContext; private DexClassLoader patchClassLoader; private DexFile[] dexFiles; public void init(Context context) { this.appContext = context.getApplicationContext(); try { File patchDir = FileUtil.getDir(SD_CARD_PATH + File.separator + PATCH_PATH); String dexPath = patchDir.getAbsolutePath() + File.separator + "patch.jar"; File optDir = FileUtil.getDir(appContext.getCacheDir().getAbsolutePath() + File.separator + "patch/opt" ); File soDir = FileUtil.getDir(appContext.getCacheDir().getAbsolutePath() + File.separator + "patch/so" ); patchClassLoader = new DexClassLoader( dexPath, optDir.getAbsolutePath(), soDir.getAbsolutePath(), appContext.getClassLoader()); //reflect the dexFile for traverse all of the class in the patch Object dexPathList = ReflectUtils.getFieldObj( "dalvik.system.BaseDexClassLoader", patchClassLoader, "pathList"); Object dexElements = ReflectUtils.getFieldObj( "dalvik.system.DexPathList", dexPathList, "dexElements"); int length = Array.getLength(dexElements); dexFiles = new DexFile[length]; for(int i = 0; i < length; i++) { Object element = Array.get(dexElements, i); dexFiles[i] = (DexFile) ReflectUtils.getFieldObj( "dalvik.system.DexPathList$Element", element, "dexFile" ); } } catch (Exception e) { e.printStackTrace(); } } public void fix() { try { //traverse the dexFile and hook all of the methods for(DexFile dexFile : dexFiles) { Enumeration<String> entries = dexFile.entries(); Class<?> clazz = null; while (entries.hasMoreElements()) { String entry = entries.nextElement(); clazz = dexFile.loadClass(entry, patchClassLoader); if (clazz != null) { Method[] methods = clazz.getDeclaredMethods(); for (Method replace : methods) { MethodReplace mr = replace.getAnnotation(MethodReplace.class); if (mr == null) { continue; } Method origin = Class.forName(mr.className()).getDeclaredMethod(mr.methodName(), replace.getParameterTypes()); Hotfix.hook(origin, replace); } } } } } catch (Exception e) { e.printStackTrace(); } } public void callOrigin(Object receiver, Object[] params) { Hotfix.callOrigin(receiver, params); }}
这里用到了一点别的东西,因为我们要遍历出补丁中所有的类,classLoader本身没有开放这个api,所以需要阅读一下BaseDexClassLoader的源码,从中找到具备这个能力的DexFile类文件,实现这个功能,具体的请参考代码中的注释。遍历类的时候,通过反射获取加了注解的方法,然后从中取出原来的方法进行backup和替换。
好了,一个简单的热修复框架就完成了,这里还没有对签名做处理,具体的可以直接复用AndFix的源码,对于sdk来说,没有so就可以大大增加兼容性,减小集成的难度。当然这里只是抛砖引玉,真正要做一个商用的热修复框架需要解决的问题还有很多很多,而且目前的框架只能实现方法级的替换,对于资源等其他大量内容无法实现修复,局限性很大,这块研究我还会继续深入下去,感谢大家收看。
- Java层热修复框架实践
- Sophix热修复实践
- 阿里SopHix热修复框架操作实践基础步骤
- Android热修复之AndFix原理探索(黑科技热修复的Java层实现)
- 热修复框架Nuwa
- Android热修复大白话版(Java层)
- android热修复实践-andfix
- java热修复实例
- Android RocooFix 热修复框架
- Android AndFix 热修复框架
- Android RocooFix 热修复框架
- Android RocooFix 热修复框架
- Android RocooFix 热修复框架
- Android RocooFix 热修复框架
- Android AndFix 热修复框架
- Android热修复框架andfix
- 接入热修复框架TinKer
- Android RocooFix 热修复框架
- Gray Code
- hdu2036 求多边形面积(向量叉乘)
- httpd
- 一个平凡的工作日
- vue-router 报错"Uncaught SyntaxError: Unexpected token }"
- Java层热修复框架实践
- bzoj2101[Usaco2010 Dec]Treasure Chest 藏宝箱 DP
- 从Mac/OS和iOS开放源码浅谈UNIX家谱
- HTML5应用之文件拖拽上传
- Jsonp最简单的使用方法
- Generate Parentheses一道普通dfs题目
- 系统分区和磁盘管理
- Python3:《机器学习实战》之支持向量机(1)算法概述
- ROS(1和2)机器人操作系统相关书籍、资料和学习路径