HotFix原理介紹及使用總結(jié)

What is HotFix?

以補丁的方式動態(tài)修復(fù)緊急Bug礁芦,不再需要重新發(fā)布App,不再需要用戶重新下載贺嫂,覆蓋安裝(來自:安卓App熱補丁動態(tài)修復(fù)技術(shù)介紹)

HotFix框架匯總

QQ空間HotFix方案原理

首先HotFix原理是基于Android Dex分包方案的,而Dex分包方案的關(guān)鍵就是Android的ClassLoader體系。ClassLoader的繼承關(guān)系如下:

ClassLoader繼承關(guān)系

這里我們可以用的是PathClassLoaderDexClassLoader,接下來看看這兩個類的注釋:

  • 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ù):

  1. 改變BaseDexClassLoader中的dexElements數(shù)組并炮,將我們的patch.jar插入到dexElements數(shù)組的第一個位置默刚。
  2. 在打包的時候,我們要阻止類被打上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)步驟:

  1. 下載andfix-0.4.0.aar 0.4.0版本的aar文件到本地敲霍,然后將文件的擴展名改為zip俊马,用壓縮文件打開
  2. 因為其他文件夾都是空的丁存,我們只需要將jni文件夾的so文件和classes.jar(可以改下名字)導(dǎo)入到libs下面
  3. 按照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熱補丁輕量級方案

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末敞斋,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子疾牲,更是在濱河造成了極大的恐慌植捎,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件阳柔,死亡現(xiàn)場離奇詭異焰枢,居然都是意外死亡,警方通過查閱死者的電腦和手機舌剂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進店門济锄,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人霍转,你說我怎么就攤上這事荐绝。” “怎么了避消?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵低滩,是天一觀的道長。 經(jīng)常有香客問我沾谓,道長委造,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任均驶,我火速辦了婚禮,結(jié)果婚禮上枫虏,老公的妹妹穿的比我還像新娘妇穴。我一直安慰自己爬虱,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布腾它。 她就那樣靜靜地躺著跑筝,像睡著了一般。 火紅的嫁衣襯著肌膚如雪瞒滴。 梳的紋絲不亂的頭發(fā)上曲梗,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天,我揣著相機與錄音妓忍,去河邊找鬼虏两。 笑死,一個胖子當(dāng)著我的面吹牛世剖,可吹牛的內(nèi)容都是我干的定罢。 我是一名探鬼主播,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼旁瘫,長吁一口氣:“原來是場噩夢啊……” “哼祖凫!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起酬凳,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤惠况,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后宁仔,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體售滤,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年台诗,在試婚紗的時候發(fā)現(xiàn)自己被綠了完箩。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡拉队,死狀恐怖弊知,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情粱快,我是刑警寧澤秩彤,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站事哭,受9級特大地震影響漫雷,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜鳍咱,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一降盹、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧谤辜,春花似錦蓄坏、人聲如沸价捧。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽结蟋。三九已至,卻和暖如春渔彰,著一層夾襖步出監(jiān)牢的瞬間嵌屎,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工恍涂, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留宝惰,地道東北人。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓乳丰,卻偏偏與公主長得像掌测,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子产园,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,976評論 2 355

推薦閱讀更多精彩內(nèi)容