What is HotFix?
以補丁的方式動態(tài)修復(fù)緊急Bug礁芦,不再需要重新發(fā)布App,不再需要用戶重新下載贺嫂,覆蓋安裝(來自:安卓App熱補丁動態(tài)修復(fù)技術(shù)介紹)
HotFix框架匯總
-
QQ空間熱修復(fù)方案
-
native hook的方案
-
微信熱修復(fù)方案
手機QQ熱修復(fù)方案
QQ空間HotFix方案原理
首先HotFix原理是基于Android Dex分包方案的,而Dex分包方案的關(guān)鍵就是Android的ClassLoader體系。ClassLoader的繼承關(guān)系如下:
這里我們可以用的是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).
*/
這個類被用作系統(tǒng)類加載器和應(yīng)用類(已安裝的應(yīng)用)加載器。
- 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,可以用來執(zhí)行沒有安裝的程序代碼袋坑。
通過上面的兩個注釋可以清楚這兩個類的作用了仗处,很顯然我們要用的是DexClassLoader,對插件化了解的小伙伴們對這個類肯定不會陌生的,對插件化不了解的也沒關(guān)系婆誓。下面會更詳細的介紹吃环。
我們知道了PathClassLoader和DexClassLoader的應(yīng)用場景,接下來看一下是如何加載類的洋幻,看上面的繼承關(guān)系這里兩個類都是繼承自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;
}
}
這個方法里面有調(diào)用了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);
}
好了厂庇,關(guān)聯(lián)的代碼全部貼上了渠啊,理解起來并不難,總結(jié)一下流程:BaseDexClassLoader中有一個DexPathList對象pathList权旷,pathList中有個Element數(shù)組dexElements(Element是DexPathList的靜態(tài)內(nèi)部類替蛉,在Element中會保存DexFile的對象),然后遍歷Element數(shù)組拄氯,通過DexFile對象去查找類躲查。
更通俗的說:
一個ClassLoader可以包含多個dex文件,每個dex文件是一個Element译柏,多個dex文件排列成一個有序的數(shù)組dexElements镣煮,當(dāng)找類的時候,會按順序遍歷dex文件鄙麦,然后從當(dāng)前遍歷的dex文件中找類典唇,如果找類則返回,如果找不到從下一個dex文件繼續(xù)查找胯府。(出自安卓App熱補丁動態(tài)修復(fù)技術(shù)介紹)
so介衔,通過上面介紹,我們可以將patch.jar(補丁包)骂因,放在dexElements數(shù)組的第一個元素炎咖,這樣優(yōu)先找到我們patch.jar中的新類去替換之前存在bug的類。
方案有了寒波,但是我們還差一個步驟乘盼,就是防止類被打上CLASS_ISPREVERIFIED的標(biāo)記
解釋一下:在apk安裝的時候,虛擬機會將dex優(yōu)化成odex后才拿去執(zhí)行俄烁。在這個過程中會對所有class一個校驗绸栅。校驗方式:假設(shè)A該類在它的static方法,private方法猴娩,構(gòu)造函數(shù)阴幌,override方法中直接引用到B類勺阐。如果A類和B類在同一個dex中,那么A類就會被打上CLASS_ISPREVERIFIED標(biāo)記矛双。A類如果還引用了一個C類渊抽,而C類在其他dex中,那么A類并不會被打上標(biāo)記议忽。換句話說懒闷,只要在static方法,構(gòu)造方法栈幸,private方法愤估,override方法中直接引用了其他dex中的類,那么這個類就不會被打上CLASS_ISPREVERIFIED標(biāo)記速址。(引用自Android熱補丁動態(tài)修復(fù)技術(shù)(二):實戰(zhàn)玩焰!CLASS_ISPREVERIFIED問題!)
O..O..OK芍锚,現(xiàn)在很清楚了昔园,實現(xiàn)QQ空間熱修復(fù)方案,我們需要完成兩個任務(wù):
- 改變BaseDexClassLoader中的dexElements數(shù)組并炮,將我們的patch.jar插入到dexElements數(shù)組的第一個位置默刚。
- 在打包的時候,我們要阻止類被打上CLASS_ISPREVERIFIED標(biāo)記
AndFix修復(fù)方案原理
AndFix的原理需要從源碼來一步一步的分析逃魄,接下來按照AndFix的使用步驟來分析源碼荤西,從而引出原理,一共分為兩層:1.Java層 2.Native層(關(guān)鍵步驟)伍俘。
Java層
首先是patchManager = new PatchManager(context);
邪锌,來看下PatchManager的構(gòu)造方法:
/**
* @param context
* context
*/
public PatchManager(Context context) {
mContext = context;
//初始化AndFixManager()
mAndFixManager = new AndFixManager(mContext);
//初始化緩存Patch的文件夾
mPatchDir = new File(mContext.getFilesDir(), DIR);
//初始化存在patch類的集合,即需要修復(fù)類的集合
mPatchs = new ConcurrentSkipListSet<Patch>();
//初始化類對應(yīng)的classLoader集合
mLoaders = new ConcurrentHashMap<String, ClassLoader>();
}
//AndFixManager.java
public AndFixManager(Context context) {
mContext = context;
//判斷是否支持當(dāng)前機型
mSupport = Compat.isSupport();
if (mSupport) {
//初始化安全檢查的類
mSecurityChecker = new SecurityChecker(mContext);
//初始化優(yōu)化的文件夾(該文件夾會存放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;
}
}
}
構(gòu)造方法里的代碼都加了注釋很清晰秃流,接下來看patchManager.init(appversion);//current version
方法:
/**
* initialize
*
* @param appVersion
* App version
*/
public void init(String appVersion) {
//判斷是否存在構(gòu)造方法中創(chuàng)建的文件夾
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)
方法,這個方法在加載補丁的時候調(diào)用:
/**
* 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;
}
//將補丁復(fù)制一份到緩存文件夾
FileUtil.copyFile(src, dest);// copy to patch's directory
//這里也是調(diào)用的上面的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) {
//獲取補丁內(nèi)Class的集合
classes = patch.getClasses(patchName);
//重點方法:修復(fù)的方法
mAndFixManager.fix(patch.getFile(), mContext.getClassLoader(), classes);
}
}
}
源碼里好幾個loadPatch()重載的方法,這里只列出一個糖赔,其他接收參數(shù)和內(nèi)部實現(xiàn)略有不同萍丐,但最終都去調(diào)用了mAndFixManger.fix(...)方法,而fix()方法開始是一堆的驗證放典,文件校驗之類的安全檢查逝变,在這就不貼了基茵,最后調(diào)用了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();
//找到需要替換的方法后調(diào)用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();
// 根據(jù)key,查找緩存中的數(shù)據(jù)宴咧,該緩存記錄了已經(jīng)被修復(fù)過的class對象根灯。
Class<?> clazz = mFixedClass.get(key);
if (clazz == null) {// class not load
// initialize target class
//找不到則表示該class沒有被修復(fù)過,則通過類加載器去加載掺栅。
Class<?> clzz = classLoader.loadClass(clz);
// 通過C層烙肺,改寫accessFlags,把需要替換的類的所有方法(Field)改成了public
clazz = AndFix.initTargetClass(clzz);
}
if (clazz != null) {// initialize class OK
mFixedClass.put(key, clazz);
// 反射得到修復(fù)前老的Method對象
Method src = clazz.getDeclaredMethod(meth,method.getParameterTypes());
AndFix.addReplaceMethod(src, method);
}
} catch (Exception e) {
Log.e(TAG, "replaceMethod", e);
}
}
AndFix.addReplaceMethod(src,method)方法調(diào)用了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熱修復(fù)原理。
//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);
}
}
根據(jù)不同的API版本執(zhí)行不同的方法怎栽,來看5.0的替換方法:
//art_method_replace_5_0.cpp
void replace_5_0(JNIEnv* env, jobject src, jobject dest) {
// 通過jni.h中的FromReflectedMethod方法反射得到源方法和替換方法的ArtMethod指針(ArtMethod的數(shù)據(jù)結(jié)構(gòu)定義在頭文件中,接下來會分析)
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
// 替換用于檢查遞歸調(diào)用<clinit>的線程id
dmeth->declaring_class_->clinit_thread_id_ =
smeth->declaring_class_->clinit_thread_id_;
// 把目標(biāo)方法所在類的初始化狀態(tài)值設(shè)置成源方法的狀態(tài)值-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運行時執(zhí)行方法的兩種模式(compiled_code熏瞄,interpreter),Andfix根據(jù)方法不同的調(diào)用機制通過這兩個指針做方法替換
//方法執(zhí)行方式為本地機器指令的指針入口
smeth->entry_point_from_compiled_code_ = dmeth->entry_point_from_compiled_code_;
// 方法執(zhí)行方式為解釋執(zhí)行的指針入口
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調(diào)用方式强饮,本地機器指令入口
// 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方式調(diào)用方法 解釋執(zhí)行入口
// 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_;
};
}
代碼就分析這里,通過代碼我們來總結(jié)一下:
- java層:實現(xiàn)加載補丁文件为黎,安全驗證等操作邮丰,然后根據(jù)補丁中的注解找到將要替換的方法然后交給native層去處理替換方法的操作。
- native層:利用java hook的技術(shù)來替換要修復(fù)的方法
so...so...so铭乾,AndFix原理就是:在Native層使用指針替換的方式替換bug方法剪廉,以達到修復(fù)bug的目的。
微信熱補丁原理
微信的原理詳細見這篇文章:微信Android熱補丁實踐演進之路
Eclipse使用HotFix
Eclipse上使用HotFix炕檩,這個問題我研究了一個多星期斗蒋,一開始的思路是使用QQ空間的方案,但是在插樁那一步卡住了~不知道要怎么注入代碼(如果有大神實現(xiàn)了笛质,可以留言泉沾,小弟感激不盡),后來又去研究AndFix妇押,果然沒讓我失望跷究,終于在eclipse上實現(xiàn)了AndFix的熱補丁。
下面來說一下具體的實現(xiàn)步驟:
- 下載andfix-0.4.0.aar 0.4.0版本的aar文件到本地敲霍,然后將文件的擴展名改為zip俊马,用壓縮文件打開
- 因為其他文件夾都是空的丁存,我們只需要將jni文件夾的so文件和classes.jar(可以改下名字)導(dǎo)入到libs下面
- 按照AndFixgithub上的使用教程,集成api就可以了
目前eclipse只寫了demo柴我,混淆還未測試柱嫌,機型也未做測試。
RoccoFix使用問題
附上一個RoccoFix使用demo屯换,里面有使用步驟的視頻编丘,更加可視化,而且地址里面有很多問題的解決辦法彤悔,demo中還有在線補丁流程思路嘉抓。地址:https://github.com/shoyu666/derocoodemo
UPDATE
今天看到了這篇文章Android Patch 方案與持續(xù)交付,覺得很吊的樣子~希望能開源出來晕窑。
參考
安卓App熱補丁動態(tài)修復(fù)技術(shù)介紹
Android dex分包方案
Android 熱補丁動態(tài)修復(fù)框架小結(jié)
Android熱補丁動態(tài)修復(fù)技術(shù)(一):從Dex分包原理到熱補丁
Android熱補丁動態(tài)修復(fù)技術(shù)(二):實戰(zhàn)抑片!CLASS_ISPREVERIFIED問題!
Android熱補丁動態(tài)修復(fù)技術(shù)(三)—— 使用Javassist注入字節(jié)碼杨赤,完成熱補丁框架雛形(可使用)
Android熱補丁動態(tài)修復(fù)技術(shù)(四):自動化生成補丁——解決混淆問題
AndFix使用說明
向每一個錯誤致敬—— AndFix學(xué)習(xí)記錄
微信Android熱補丁實踐演進之路
Android熱補丁之AndFix原理解析
各大熱補丁方案分析和比較
從AndFix源碼分析JNI Hook熱修復(fù)原理
Android Patch 方案與持續(xù)交付
QFix探索之路——手Q熱補丁輕量級方案