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就可以大大增加兼容性,减小集成的难度。当然这里只是抛砖引玉,真正要做一个商用的热修复框架需要解决的问题还有很多很多,而且目前的框架只能实现方法级的替换,对于资源等其他大量内容无法实现修复,局限性很大,这块研究我还会继续深入下去,感谢大家收看。

原创粉丝点击