热修复实现原理——native hook
一、native hook简介
直接在native层进行方法的结构体信息对换,从而实现完美的方法新旧替换,从而实现热修复功能。例如AndFix采用native hook的方式,以Field为切入点,直接使用dalvik_replaceMethod替换class中方法的实现。由于它并没有整体替换class, 而field在class中的相对地址在class加载时已确定,所以AndFix无法支持新增或者删除filed的情况(通过替换init与clinit只可以修改field的数值)。
在dalvik上的实现略有不同,是通过jni bridge来指向补丁的方法。
二、修复过程(以AndFix为例)
I、Java层
java 层的功能就是找到补丁文件,根据补丁中的注解找到将要替换的方法然后交给jni层去处理替换方法的操作
1、application 初始化
public class MainApplication extends Application {
private static final String TAG = " andrew";
private static final String APATCH_PATH = "/out.apatch";
private static final String DIR = "apatch";//补丁文件夹
/**
* patch manager
*/
private PatchManager mPatchManager;
@Override
public void onCreate() {
super.onCreate();
// initialize
mPatchManager = new PatchManager(this);
mPatchManager.init("1.0");
Log.d(TAG, "inited.");
// load patch
mPatchManager.loadPatch();
try {
// .apatch file path
String patchFileString = Environment.getExternalStorageDirectory()
.getAbsolutePath() + APATCH_PATH;
mPatchManager.addPatch(patchFileString);
Log.d(TAG, "apatch:" + patchFileString + " added.");
//复制且加载补丁成功后,删除下载的补丁
File f = new File(this.getFilesDir(), DIR + APATCH_PATH);
if (f.exists()) {
boolean result = new File(patchFileString).delete();
if (!result)
Log.e(TAG, patchFileString + " delete fail");
}
} catch (IOException e) {
Log.e(TAG, "", e);
}
}
}
2、实例化PatchManager
//SP_VERSION 更多象征app的版本,该值不变时,打补丁;改变时,清空补丁
// patch extension
private static final String SUFFIX = ".apatch";//后缀名
private static final String DIR = "apatch";//补丁文件夹
private static final String SP_NAME = "_andfix_";
private static final String SP_VERSION = "version";//热更新补丁时,版本不变,自动加载补丁;apk完整更新发布时,版本提升,本地会自动删除以前加载在apatch文件夹里的补丁,防止二次载入过时补丁
/**
* context
*/
private final Context mContext;
/**
* AndFix manager
*/
private final AndFixManager mAndFixManager;
/**
* patch directory
*/
private final File mPatchDir;
/**
* patchs
*/
private final SortedSet mPatchs;
/**
* classloaders
*/
private final Map mLoaders;
/**
* @param context context
*/
public PatchManager(Context context) {
mContext = context;
mAndFixManager = new AndFixManager(mContext);//初始化AndFixManager
mPatchDir = new File(mContext.getFilesDir(), DIR);//初始化存放patch补丁文件的文件夹, data/data/包名/files/patch
mPatchs = new ConcurrentSkipListSet();//初始化存在Patch类的集合,此类适合大并发
mLoaders = new ConcurrentHashMap();//初始化存放类对应的类加载器集合
}
3、初始化AndFixManager
此处 主要在native层进行 ;一件事是判断当前环境是否支持热修复,一件事是初始化修复包安全校验的工作
4、初始化PatchManager
就是从SharedPreferences读取以前存的版本和你传过来的版本进行比对,如果两者版本不一致就删除本地patch,否则调用initPatchs()这个方法
/**
* initialize
*
* @param appVersion App version
*/
public void init(String appVersion) {
if (!mPatchDir.exists() && !mPatchDir.mkdirs()) {// make directory fail
Log.e(TAG, "patch dir create error.");
return;
} else if (!mPatchDir.isDirectory()) {//如果遇到同名的文件,则将该同名文件删除
mPatchDir.delete();
return;
}
//在该文件下放入一个名为_andfix_的SharedPreferences文件
SharedPreferences sp = mContext.getSharedPreferences(SP_NAME,
Context.MODE_PRIVATE);//存储关于patch文件的信息
//根据你传入的版本号和之前的对比,做不同的处理
String ver = sp.getString(SP_VERSION, null);
//根据版本号加载补丁文件,版本号不同清空缓存目录
if (ver == null || !ver.equalsIgnoreCase(appVersion)) {
cleanPatch();//删除本地patch文件
sp.edit().putString(SP_VERSION, appVersion).commit();//并把传入的版本号保存
} else {
initPatchs();//初始化patch列表,把本地的patch文件加载到内存
}
}
5、加载patch文件到内存
把扩展名为.apatch的文件加载到内存,初始化对应的Patch,并把刚初始化的Patch加入到我们之前看到的Patch集合mPatchs中
private void initPatchs() {
File[] files = mPatchDir.listFiles();
for (File file : files) {
addPatch(file);
}
}
/**
* add patch file
*
* @param file
* @return patch
*/
private Patch addPatch(File file) {
Patch patch = null;
if (file.getName().endsWith(SUFFIX)) {
try {
patch = new Patch(file);//实例化Patch对象
mPatchs.add(patch);//把patch实例存储到内存的集合中,在PatchManager实例化集合
} catch (IOException e) {
Log.e(TAG, "addPatch", e);
}
}
return patch;
}
/**
Patch文件的实例化
*/
public class Patch implements Comparable {
private static final String ENTRY_NAME = "META-INF/PATCH.MF";
private static final String CLASSES = "-Classes";
private static final String PATCH_CLASSES = "Patch-Classes";
private static final String CREATED_TIME = "Created-Time";
private static final String PATCH_NAME = "Patch-Name";
/**
* patch file
*/
private final File mFile;
/**
* name
*/
private String mName;
/**
* create time
*/
private Date mTime;
/**
* classes of patch
*/
private Map> mClassesMap;
public Patch(File file) throws IOException {
mFile = file;
init();
}
@SuppressWarnings("deprecation")
private void init() throws IOException {
JarFile jarFile = null;
InputStream inputStream = null;
try {
jarFile = new JarFile(mFile);//使用JarFile读取Patch文件
JarEntry entry = jarFile.getJarEntry(ENTRY_NAME);//获取META-INF/PATCH.MF文件
inputStream = jarFile.getInputStream(entry);
Manifest manifest = new Manifest(inputStream);
Attributes main = manifest.getMainAttributes();
mName = main.getValue(PATCH_NAME);//获取PATCH.MF属性Patch-Name
mTime = new Date(main.getValue(CREATED_TIME));//获取PATCH.MF属性Created-Time
mClassesMap = new HashMap>();
Attributes.Name attrName;
String name;
List strings;
for (Iterator it = main.keySet().iterator(); it.hasNext();) {
attrName = (Attributes.Name) it.next();
name = attrName.toString();
//判断name的后缀是否是-Classes,并把name对应的值加入到集合中,对应的值就是class类名的列表
if (name.endsWith(CLASSES)) {
strings = Arrays.asList(main.getValue(attrName).split(","));
if (name.equalsIgnoreCase(PATCH_CLASSES)) {
mClassesMap.put(mName, strings);
} else {
mClassesMap.put(
name.trim().substring(0, name.length() - 8),// remove
// "-Classes"
strings);
}
}
}
} finally {
if (jarFile != null) {
jarFile.close();
}
if (inputStream != null) {
inputStream.close();
}
}
}
public String getName() {
return mName;
}
public File getFile() {
return mFile;
}
public Set getPatchNames() {
return mClassesMap.keySet();
}
public List getClasses(String patchName) {
return mClassesMap.get(patchName);
}
public Date getTime() {
return mTime;
}
@Override
public int compareTo(Patch another) {
return mTime.compareTo(another.getTime());
}
}
6、对比查找不同
调用PatchManager. loadPatch;遍历mPatchs中每个patch的每个类,mPatchs就是上文介绍的存储patch的一个集合。根据补丁名找到对应的类,做为参数传给fix()
/**
* load patch,call when application start
*/
public void loadPatch() {
mLoaders.put("*", mContext.getClassLoader());// wildcard
Set patchNames;
List classes;
for (Patch patch : mPatchs) {
patchNames = patch.getPatchNames();
for (String patchName : patchNames) {
classes = patch.getClasses(patchName);
mAndFixManager.fix(patch.getFile(), mContext.getClassLoader(),
classes);
}
}
}
7、跟修改情况调用Native
/**
* fix
*
* @param file
* patch file
* @param classLoader
* classloader of class that will be fixed
* @param classes
* classes will be fixed
*/
public synchronized void fix(File file, ClassLoader classLoader,
List classes) {
if (!mSupport) {
return;
}
//判断patch文件的签名
if (!mSecurityChecker.verifyApk(file)) {// security check fail
return;
}
try {
File optfile = new File(mOptDir, file.getName());
boolean saveFingerprint = true;
if (optfile.exists()) {
// need to verify fingerprint when the optimize file exist,
// prevent someone attack on jailbreak device with
// Vulnerability-Parasyte.
// btw:exaggerated android Vulnerability-Parasyte
// http://secauo.com/Exaggerated-Android-Vulnerability-Parasyte.html
if (mSecurityChecker.verifyOpt(optfile)) {
saveFingerprint = false;
} else if (!optfile.delete()) {
return;
}
}
//加载patch文件中的dex
final DexFile dexFile = DexFile.loadDex(file.getAbsolutePath(),
optfile.getAbsolutePath(), Context.MODE_PRIVATE);
if (saveFingerprint) {
mSecurityChecker.saveOptSig(optfile);
}
ClassLoader patchClassLoader = new ClassLoader(classLoader) {
@Override
protected Class findClass(String className)
throws ClassNotFoundException {
Class clazz = dexFile.loadClass(className, this);
if (clazz == null
&& className.startsWith("com.alipay.euler.andfix")) {
return Class.forName(className);// annotation’s class
// not found
}
if (clazz == null) {
throw new ClassNotFoundException(className);
}
return clazz;
}
};
Enumeration entrys = dexFile.entries();
Class clazz = null;
while (entrys.hasMoreElements()) {
String entry = entrys.nextElement();
if (classes != null && !classes.contains(entry)) {
continue;// skip, not need fix
}
clazz = dexFile.loadClass(entry, patchClassLoader);//获取有bug的类文件
if (clazz != null) {
fixClass(clazz, classLoader);
}
}
} catch (IOException e) {
Log.e(TAG, "pacth", e);
}
}
/**
* fix class
*
* @param clazz
* class
*/
private void fixClass(Class clazz, ClassLoader classLoader) {
//使用反射获取这个类中所有的方法
Method[] methods = clazz.getDeclaredMethods();
//MethodReplace是这个库自定义的Annotation,标记哪个方法需要被替换
MethodReplace methodReplace;
String clz;
String meth;
for (Method method : methods) {
//获取此方法的注解,因为有bug的方法在生成的patch的类中的方法都是有注解的
//还记得对比过程中生成的Annotation注解吗
//这里通过注解找到需要替换掉的方法
methodReplace = method.getAnnotation(MethodReplace.class);
if (methodReplace == null)
continue;
clz = methodReplace.clazz();//获取注解中clazz的值,标记的类
meth = methodReplace.method();//获取注解中method的值,需要替换的方法
if (!isEmpty(clz) && !isEmpty(meth)) {
//所有找到的方法,循环替换
replaceMethod(classLoader, clz, meth, method);
}
}
}
/**
* replace method
*
* @param classLoader classloader
* @param clz class
* @param meth name of target method
* @param method source method
*/
private void replaceMethod(ClassLoader classLoader, String clz,
String meth, Method method) {
try {
String key = clz + "@" + classLoader.toString();
Class clazz = mFixedClass.get(key);//判断此类是否被fix
if (clazz == null) {// class not load
Class clzz = classLoader.loadClass(clz);
// initialize target class
clazz = AndFix.initTargetClass(clzz);//初始化class
}
if (clazz != null) {// initialize class OK
mFixedClass.put(key, clazz);
Method src = clazz.getDeclaredMethod(meth,
method.getParameterTypes());//根据反射获取到有bug的类的方法(有bug的apk)
AndFix.addReplaceMethod(src, method);//src是有bug的方法,method是补丁方法
}
} catch (Exception e) {
Log.e(TAG, "replaceMethod", e);
}
}
//调用jni替换,src是有bug的方法,method是补丁方法
private static native boolean setup(boolean isArt, int apilevel);
private static native void replaceMethod(Method dest, Method src);
private static native void setFieldFlag(Field field);
public static void addReplaceMethod(Method src, Method dest) {
try {
replaceMethod(src, dest);//调用了native方法,next code
initFields(dest.getDeclaringClass());
} catch (Throwable e) {
Log.e(TAG, "addReplaceMethod", e);
}
}
II、Native层
由于android 4.4 之后新增应用运行模式 此处分两种
在native层中会做art和dalvik虚拟机的区分处理工作,Java hook大致的逻辑都是一致的:
1、dalvik 模式
- 在libdvm.so动态获取dvmDecodeIndirectRef函数指针和获取dvmThreadSelf函数指针。
- 调用dest的 Method.getDeclaringClass方法获取method的类对象clazz。
- 调用dvmDecodeIndirectRef方法,获取clazz的ClassObject*
- 通关 env->FromReflectedMethod方法获取dest的Method结构体函数的指针
- 替换method结构体的成员数据
2、art 模式
- art模式中,我们直接通过 env->FromReflectedMethod获取到ArtMethod函数指针。
- 然后直接替换ArtMethod结构体的成员数据指针
三、native hook 优劣
因为是动态的,所以不需要重启应用就可以生效
支持ART与Dalvik
与multidex方案相比,性能会有所提升(Multi Dex需要修改所有class的class_ispreverified标志位,导致运行时性能有所损失)
支持新增加方法
-
支持在新增方法中新增局部变量
支持的补丁场景相对有限,仅仅可以使用它来修复特定问题(兼容性较差)一般只用来修复方法。类的成员字段不能修改