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