Java层热修复框架实践

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值