What is HotFix?
以补丁的方式动态修复紧急Bug,不再需要重新发布App,不再需要用户重新下载,覆盖安装(来自:安卓App热补丁动态修复技术介绍)
HotFix框架汇总
-
QQ空间热修复方案
- RocooFix
- Nuwa
- HotFix
-
native hook的方案
- AndFix
- 阿里百川(未开源)
-
微信热修复方案
- Tinker_imitator(微信热更新方案实践)
tinker(微信官方)
手机QQ热修复方案
QFix:推荐文章QFix探索之路——手Q热补丁轻量级方案
QQ空间HotFix方案原理
首先HotFix原理是基于Android Dex分包方案的,而Dex分包方案的关键就是Android的ClassLoader体系。ClassLoader的继承关系如下:
这里我们可以用的是 PathClassLoader和 DexClassLoader,接下来看看这两个类的注释:
- PatchClassLoader
/**
* Provides a simple {@link ClassLoader} implementation that operates on a list
* of files and directories in the local file system, but does not attempt to
* load classes from the network. Android uses this class for its system class
* loader and for its application class loader(s).
*/
这个类被用作系统类加载器和应用类(已安装的应用)加载器。
- DexClassLoader
/**
* A class loader that loads classes from {@code .jar} and {@code .apk} files
* containing a {@code classes.dex} entry. This can be used to execute code not
* installed as part of an application.
*/
注释可以看出,这个类是可以用来从.jar文件和.apk文件中加载classed.dex,可以用来执行没有安装的程序代码。
通过上面的两个注释可以清楚这两个类的作用了,很显然我们要用的是DexClassLoader,对插件化了解的小伙伴们对这个类肯定不会陌生的,对插件化不了解的也没关系。下面会更详细的介绍。
我们知道了PathClassLoader和DexClassLoader的应用场景,接下来看一下是如何加载类的,看上面的继承关系这里两个类都是继承自BaseDexClassLoader,所以查找类的方法也在BaseDexClassLoader中,下面是部分源码:
/**
* Base class for common functionality between various dex-based
* {@link ClassLoader} implementations.
*/
public class BaseDexClassLoader extends ClassLoader {
/** structured lists of path elements */
private final DexPathList pathList;
//...some code
@Override
protected Class> findClass(String name) throws ClassNotFoundException {
Class clazz = pathList.findClass(name);
if (clazz == null) {
throw new ClassNotFoundException(name);
}
return clazz;
}
//...some code
}
可以看到在findClass()方法中用到了pathList.findClass(name)
,而pathList的类型是DexPathList,下面看一下DexPathList的findClass()方法源码:
/**
* A pair of lists of entries, associated with a {@code ClassLoader}.
* One of the lists is a dex/resource path — typically referred
* to as a "class path" — list, and the other names directories
* containing native code libraries. Class path entries may be any of:
* a {@code .jar} or {@code .zip} file containing an optional
* top-level {@code classes.dex} file as well as arbitrary resources,
* or a plain {@code .dex} file (with no possibility of associated
* resources).
*
* This class also contains methods to use these lists to look up
* classes and resources.
*/
/*package*/ final class DexPathList {
/** list of dex/resource (class path) elements */
private final Element[] dexElements;
/**
* Finds the named class in one of the dex files pointed at by
* this instance. This will find the one in the earliest listed
* path element. If the class is found but has not yet been
* defined, then this method will define it in the defining
* context that this instance was constructed with.
*
* @return the named class or {@code null} if the class is not
* found in any of the dex files
*/
public Class findClass(String name) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext);
if (clazz != null) {
return clazz;
}
}
}
return null;
}
}
这个方法里面有调用了dex.loadClassBinaryName(name, definingContext)
,然后我们来看一下DexFile的这个方法:
/**
* Manipulates DEX files. The class is similar in principle to
* {@link java.util.zip.ZipFile}. It is used primarily by class loaders.
*
* Note we don't directly open and read the DEX file here. They're memory-mapped
* read-only by the VM.
*/
public final class DexFile {
/**
* See {@link #loadClass(String, ClassLoader)}.
*
* This takes a "binary" class name to better match ClassLoader semantics.
*
* @hide
*/
public Class loadClassBinaryName(String name, ClassLoader loader){
return defineClass(name, loader, mCookie);
}
private native static Class defineClass(String name, ClassLoader loader, int cookie);
}
好了,关联的代码全部贴上了,理解起来并不难,总结一下流程:BaseDexClassLoader中有一个DexPathList对象pathList,pathList中有个Element数组dexElements(Element是DexPathList的静态内部类,在Element中会保存DexFile的对象),然后遍历Element数组,通过DexFile对象去查找类。
更通俗的说:
一个ClassLoader可以包含多个dex文件,每个dex文件是一个Element,多个dex文件排列成一个有序的数组dexElements,当找类的时候,会按顺序遍历dex文件,然后从当前遍历的dex文件中找类,如果找类则返回,如果找不到从下一个dex文件继续查找。(出自安卓App热补丁动态修复技术介绍)
so,通过上面介绍,我们可以将patch.jar(补丁包),放在dexElements数组的第一个元素,这样优先找到我们patch.jar中的新类去替换之前存在bug的类。
方案有了,但是我们还差一个步骤,就是防止类被打上CLASS_ISPREVERIFIED的标记
解释一下:在apk安装的时候,虚拟机会将dex优化成odex后才拿去执行。在这个过程中会对所有class一个校验。校验方式:假设A该类在它的static方法,private方法,构造函数,override方法中直接引用到B类。如果A类和B类在同一个dex中,那么A类就会被打上CLASS_ISPREVERIFIED标记。A类如果还引用了一个C类,而C类在其他dex中,那么A类并不会被打上标记。换句话说,只要在static方法,构造方法,private方法,override方法中直接引用了其他dex中的类,那么这个类就不会被打上CLASS_ISPREVERIFIED标记。(引用自Android热补丁动态修复技术(二):实战!CLASS_ISPREVERIFIED问题!)
O..O..OK,现在很清楚了,实现QQ空间热修复方案,我们需要完成两个任务:
- 改变BaseDexClassLoader中的dexElements数组,将我们的patch.jar插入到dexElements数组的第一个位置。
- 在打包的时候,我们要阻止类被打上CLASS_ISPREVERIFIED标记
AndFix修复方案原理
AndFix的原理需要从源码来一步一步的分析,接下来按照AndFix的使用步骤来分析源码,从而引出原理,一共分为两层:1.Java层 2.Native层(关键步骤)。
Java层
首先是patchManager = new PatchManager(context);
,来看下PatchManager的构造方法:
/**
* @param context
* context
*/
public PatchManager(Context context) {
mContext = context;
//初始化AndFixManager()
mAndFixManager = new AndFixManager(mContext);
//初始化缓存Patch的文件夹
mPatchDir = new File(mContext.getFilesDir(), DIR);
//初始化存在patch类的集合,即需要修复类的集合
mPatchs = new ConcurrentSkipListSet();
//初始化类对应的classLoader集合
mLoaders = new ConcurrentHashMap();
}
//AndFixManager.java
public AndFixManager(Context context) {
mContext = context;
//判断是否支持当前机型
mSupport = Compat.isSupport();
if (mSupport) {
//初始化安全检查的类
mSecurityChecker = new SecurityChecker(mContext);
//初始化优化的文件夹(该文件夹会存放MD5值,安全检查时候会用)
mOptDir = new File(mContext.getFilesDir(), DIR);
if (!mOptDir.exists() && !mOptDir.mkdirs()) {// make directory fail
mSupport = false;
Log.e(TAG, "opt dir create error.");
} else if (!mOptDir.isDirectory()) {// not directory
mOptDir.delete();
mSupport = false;
}
}
}
构造方法里的代码都加了注释很清晰,接下来看patchManager.init(appversion);//current version
方法:
/**
* 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()) {// not directory
mPatchDir.delete();
return;
}
//获取SharedPreferences对象,用来缓存版本号
SharedPreferences sp = mContext.getSharedPreferences(SP_NAME,Context.MODE_PRIVATE);
String ver = sp.getString(SP_VERSION, null);
//如果没有缓存的版本号或者版本号不一致
if (ver == null || !ver.equalsIgnoreCase(appVersion)) {
// 清除缓存文件夹里面的所有文件
cleanPatch();
//缓存新的版本号
sp.edit().putString(SP_VERSION, appVersion).commit();
} else {
initPatchs();
}
}
/**
* 清除缓存文件夹里面的所有文件
*
*/
private void cleanPatch() {
File[] files = mPatchDir.listFiles();
for (File file : files) {
//AndFixManager的方法,移除缓存的MD5指纹
mAndFixManager.removeOptFile(file);
if (!FileUtil.deleteFile(file)) {
Log.e(TAG, file.getName() + " delete error.");
}
}
}
/**
*初始化补丁文件
*/
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类会将文件中的信息解析出来
patch = new Patch(file);
//添加到集合中
mPatchs.add(patch);
} catch (IOException e) {
Log.e(TAG, "addPatch", e);
}
}
return patch;
}
接下来先分析一下另一个addPatch(String path)
方法,这个方法在加载补丁的时候调用:
/**
* add patch at runtime
*
* @param path
* patch path
* @throws IOException
*/
public void addPatch(String path) throws IOException {
File src = new File(path);
File dest = new File(mPatchDir, src.getName());
if(!src.exists()){
throw new FileNotFoundException(path);
}
if (dest.exists()) {
Log.d(TAG, "patch [" + path + "] has be loaded.");
return;
}
//将补丁复制一份到缓存文件夹
FileUtil.copyFile(src, dest);// copy to patch's directory
//这里也是调用的上面的addPatch(File file)方法
Patch patch = addPatch(dest);
if (patch != null) {
//加载补丁
loadPatch(patch);
}
}
好了,重点的方法终于要来了~激动么?来看patchManager.loadPatch();
方法:
/**
* 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) {
//获取补丁内Class的集合
classes = patch.getClasses(patchName);
//重点方法:修复的方法
mAndFixManager.fix(patch.getFile(), mContext.getClassLoader(), classes);
}
}
}
源码里好几个loadPatch()重载的方法,这里只列出一个,其他接收参数和内部实现略有不同,但最终都去调用了mAndFixManger.fix(...)方法,而fix()方法开始是一堆的验证,文件校验之类的安全检查,在这就不贴了,最后调用了fixClass(Class> clazz, ClassLoader classLoader)
方法,直接贴这个方法:
/**
* fix class
*
* @param clazz
* class
*/
private void fixClass(Class> clazz, ClassLoader classLoader) {
// 反射找到clazz中的所有方法
Method[] methods = clazz.getDeclaredMethods();
//注解
MethodReplace methodReplace;
String clz;
String meth;
for (Method method : methods) {
//遍历所有方法,找到有MethodReplace注解的方法,即需要替换的方法
methodReplace = method.getAnnotation(MethodReplace.class);
if (methodReplace == null)
continue;
clz = methodReplace.clazz();
meth = methodReplace.method();
//找到需要替换的方法后调用replaceMethod替换方法
if (!isEmpty(clz) && !isEmpty(meth)) {
replaceMethod(classLoader, clz, meth, method);
}
}
}
replaceMethod(ClassLoader classLoader, String clz, String meth, Method 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();
// 根据key,查找缓存中的数据,该缓存记录了已经被修复过的class对象。
Class> clazz = mFixedClass.get(key);
if (clazz == null) {// class not load
// initialize target class
//找不到则表示该class没有被修复过,则通过类加载器去加载。
Class> clzz = classLoader.loadClass(clz);
// 通过C层,改写accessFlags,把需要替换的类的所有方法(Field)改成了public
clazz = AndFix.initTargetClass(clzz);
}
if (clazz != null) {// initialize class OK
mFixedClass.put(key, clazz);
// 反射得到修复前老的Method对象
Method src = clazz.getDeclaredMethod(meth,method.getParameterTypes());
AndFix.addReplaceMethod(src, method);
}
} catch (Exception e) {
Log.e(TAG, "replaceMethod", e);
}
}
AndFix.addReplaceMethod(src,method)方法调用了native的replaceMethod()方法:
/**
* replace method's body
*
* @param src
* source method
* @param dest
* target method
*
*/
public static void addReplaceMethod(Method src, Method dest) {
try {
replaceMethod(src, dest);
initFields(dest.getDeclaringClass());
} catch (Throwable e) {
Log.e(TAG, "addReplaceMethod", e);
}
}
private static native void replaceMethod(Method dest, Method src);
Native层
接下来是Native层的分析,由于自己对c代码不是太了解,所以Native层分析来自从AndFix源码分析JNI Hook热修复原理。
//andfix.cpp
static void replaceMethod(JNIEnv* env, jclass clazz, jobject src, jobject dest) {
if (isArt) {
art_replaceMethod(env, src, dest);
} else {
dalvik_replaceMethod(env, src, dest);
}
}
从代码看来Art和dalvik的处理逻辑不一样,这里只分析一下Art:
//art_method_replace.cpp
extern void __attribute__ ((visibility ("hidden"))) art_replaceMethod(
JNIEnv* env, jobject src, jobject dest) {
if (apilevel > 22) {
replace_6_0(env, src, dest);
} else if (apilevel > 21) {
replace_5_1(env, src, dest);
} else {
replace_5_0(env, src, dest);
}
}
根据不同的API版本执行不同的方法,来看5.0的替换方法:
//art_method_replace_5_0.cpp
void replace_5_0(JNIEnv* env, jobject src, jobject dest) {
// 通过jni.h中的FromReflectedMethod方法反射得到源方法和替换方法的ArtMethod指针(ArtMethod的数据结构定义在头文件中,接下来会分析)
art::mirror::ArtMethod* smeth =
(art::mirror::ArtMethod*) env->FromReflectedMethod(src);
art::mirror::ArtMethod* dmeth =
(art::mirror::ArtMethod*) env->FromReflectedMethod(dest);
// 替换方法所在类的类加载器
dmeth->declaring_class_->class_loader_ =
smeth->declaring_class_->class_loader_; //for plugin classloader
// 替换用于检查递归调用的线程id
dmeth->declaring_class_->clinit_thread_id_ =
smeth->declaring_class_->clinit_thread_id_;
// 把目标方法所在类的初始化状态值设置成源方法的状态值-1
dmeth->declaring_class_->status_ = smeth->declaring_class_->status_-1;
// 把原方法的各种属性都改成补丁方法的
smeth->declaring_class_ = dmeth->declaring_class_;
smeth->access_flags_ = dmeth->access_flags_;
smeth->frame_size_in_bytes_ = dmeth->frame_size_in_bytes_;
smeth->dex_cache_initialized_static_storage_ = dmeth->dex_cache_initialized_static_storage_;
smeth->dex_cache_resolved_types_ = dmeth->dex_cache_resolved_types_;
smeth->dex_cache_resolved_methods_ = dmeth->dex_cache_resolved_methods_;
smeth->vmap_table_ = dmeth->vmap_table_;
smeth->core_spill_mask_ = dmeth->core_spill_mask_;
smeth->fp_spill_mask_ = dmeth->fp_spill_mask_;
smeth->mapping_table_ = dmeth->mapping_table_;
smeth->code_item_offset_ = dmeth->code_item_offset_;
// 最重要的两个方法指针替换,下面两个entry_point指针代表了ART运行时执行方法的两种模式(compiled_code,interpreter),Andfix根据方法不同的调用机制通过这两个指针做方法替换
//方法执行方式为本地机器指令的指针入口
smeth->entry_point_from_compiled_code_ = dmeth->entry_point_from_compiled_code_;
// 方法执行方式为解释执行的指针入口
smeth->entry_point_from_interpreter_ = dmeth->entry_point_from_interpreter_;
smeth->native_method_ = dmeth->native_method_;
smeth->method_index_ = dmeth->method_index_;
smeth->method_dex_index_ = dmeth->method_dex_index_;
LOGD("replace_5_0: %d , %d", smeth->entry_point_from_compiled_code_, dmeth->entry_point_from_compiled_code_);
}
在头文件art_5_0.h中找到了ArtMethod的定义,也看到了上面代码中替换的所有变量的定义
// art_5_0.h
class ArtMethod: public Object {
public:
// Field order required by test "ValidateFieldOrderOfJavaCppUnionClasses".
// The class we are a part of
Class* declaring_class_;
// short cuts to declaring_class_->dex_cache_ member for fast compiled code access
void* dex_cache_initialized_static_storage_;
// short cuts to declaring_class_->dex_cache_ member for fast compiled code access
void* dex_cache_resolved_methods_;
// short cuts to declaring_class_->dex_cache_ member for fast compiled code access
void* dex_cache_resolved_types_;
// short cuts to declaring_class_->dex_cache_ member for fast compiled code access
void* dex_cache_strings_;
// Access flags; low 16 bits are defined by spec.
uint32_t access_flags_;
// Offset to the CodeItem.
uint32_t code_item_offset_;
// Architecture-dependent register spill mask
uint32_t core_spill_mask_;
// compiled_code调用方式,本地机器指令入口
// Compiled code associated with this method for callers from managed code.
// May be compiled managed code or a bridge for invoking a native method.
// TODO: Break apart this into portable and quick. const void* entry_point_from_compiled_code_;
// 通过interpreter方式调用方法 解释执行入口
// Called by the interpreter to execute this method.
void* entry_point_from_interpreter_;
// Architecture-dependent register spill mask
uint32_t fp_spill_mask_;
// Total size in bytes of the frame
size_t frame_size_in_bytes_;
// Garbage collection map of native PC offsets (quick) or dex PCs (portable) to reference bitmaps.
const uint8_t* gc_map_;
// Mapping from native pc to dex pc
const uint32_t* mapping_table_;
// Index into method_ids of the dex file associated with this method
uint32_t method_dex_index_;
// For concrete virtual methods, this is the offset of the method in Class::vtable_.
//
// For abstract methods in an interface class, this is the offset of the method in
// "iftable_->Get(n)->GetMethodArray()".
//
// For static and direct methods this is the index in the direct methods table.
uint32_t method_index_;
// The target native method registered with this method
const void* native_method_;
// When a register is promoted into a register, the spill mask holds which registers hold dex
// registers. The first promoted register's corresponding dex register is vmap_table_[1], the Nth
// is vmap_table_[N]. vmap_table_[0] holds the length of the table.
const uint16_t* vmap_table_;
static void* java_lang_reflect_ArtMethod_;
};
}
代码就分析这里,通过代码我们来总结一下:
- java层:实现加载补丁文件,安全验证等操作,然后根据补丁中的注解找到将要替换的方法然后交给native层去处理替换方法的操作。
- native层:利用java hook的技术来替换要修复的方法
so...so...so,AndFix原理就是:在Native层使用指针替换的方式替换bug方法,以达到修复bug的目的。
微信热补丁原理
微信的原理详细见这篇文章:微信Android热补丁实践演进之路
Eclipse使用HotFix
Eclipse上使用HotFix,这个问题我研究了一个多星期,一开始的思路是使用QQ空间的方案,但是在插桩那一步卡住了~不知道要怎么注入代码(如果有大神实现了,可以留言,小弟感激不尽),后来又去研究AndFix,果然没让我失望,终于在eclipse上实现了AndFix的热补丁。
下面来说一下具体的实现步骤:
- 下载andfix-0.4.0.aar 0.4.0版本的aar文件到本地,然后将文件的扩展名改为zip,用压缩文件打开
- 因为其他文件夹都是空的,我们只需要将jni文件夹的so文件和classes.jar(可以改下名字)导入到libs下面
- 按照AndFixgithub上的使用教程,集成api就可以了
目前eclipse只写了demo,混淆还未测试,机型也未做测试。
RoccoFix使用问题
附上一个RoccoFix使用demo,里面有使用步骤的视频,更加可视化,而且地址里面有很多问题的解决办法,demo中还有在线补丁流程思路。地址:https://github.com/shoyu666/derocoodemo
UPDATE
今天看到了这篇文章Android Patch 方案与持续交付,觉得很吊的样子~希望能开源出来。
参考
安卓App热补丁动态修复技术介绍
Android dex分包方案
Android 热补丁动态修复框架小结
Android热补丁动态修复技术(一):从Dex分包原理到热补丁
Android热补丁动态修复技术(二):实战!CLASS_ISPREVERIFIED问题!
Android热补丁动态修复技术(三)—— 使用Javassist注入字节码,完成热补丁框架雏形(可使用)
Android热补丁动态修复技术(四):自动化生成补丁——解决混淆问题
AndFix使用说明
向每一个错误致敬—— AndFix学习记录
微信Android热补丁实践演进之路
Android热补丁之AndFix原理解析
各大热补丁方案分析和比较
从AndFix源码分析JNI Hook热修复原理
Android Patch 方案与持续交付
QFix探索之路——手Q热补丁轻量级方案