What is HotFix?
以補丁的方式動態修復緊急Bug,不再需要重新發布App,不再需要用戶重新下載,覆蓋安裝(來自:安卓App熱補丁動態修復技術介紹)
HotFix框架匯總
-
QQ空間熱修復方案
-
native hook的方案
-
微信熱修復方案
手機QQ熱修復方案
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).
*
* <p>This class also contains methods to use these lists to look up
* classes and resources.</p>
*/
/*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.
* <p>
* 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<Patch>();
//初始化類對應的classLoader集合
mLoaders = new ConcurrentHashMap<String, ClassLoader>();
}
//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<String> patchNames;
List<String> 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
// 替換用于檢查遞歸調用<clinit>的線程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熱補丁輕量級方案