Android熱補(bǔ)丁修復(fù)

參考1
參考2
參考3
參考4

一:熱修復(fù)相關(guān)

熱修復(fù)概念: 以補(bǔ)丁的方式動(dòng)態(tài)修復(fù)緊急Bug,不再需要重新發(fā)布App,不再需要用戶(hù)重新下載。
PathClassloader和DexClassLoader:
(1)PathClassloader作為其系統(tǒng)類(lèi)和應(yīng)用類(lèi)的加載器,只能去加載已經(jīng)安裝到Android系統(tǒng)中的apk文件。
(2)DexClassLoader可以用來(lái)從.jar和.apk類(lèi)型的文件內(nèi)部加載classes.dex文件。可以用來(lái)執(zhí)行非安裝的程序代碼。
(3)Android使用PathClassLoader作為其類(lèi)加載器,DexClassLoader可以從.jar和.apk類(lèi)型的文件內(nèi)部加載classes.dex文件。
熱修復(fù)原理:
PathClassLoader和DexClassLoader都繼承自BaseDexClassLoader
在BaseDexClassLoader中有如下源碼:

#BaseDexClassLoader
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
    Class clazz = pathList.findClass(name);

    if (clazz == null) {
        throw new ClassNotFoundException(name);
    }

    return clazz;
}

#DexPathList
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;
}

#DexFile
public Class loadClassBinaryName(String name, ClassLoader loader) {
    return defineClass(name, loader, mCookie);
}
private native static Class defineClass(String name, ClassLoader loader, int cookie);1  n j                                                                                                                         
n 

1 BaseDexClassLoader中有個(gè)pathList對(duì)象,pathList中包含一個(gè)DexFile的集合dexElements,而對(duì)于類(lèi)加載呢,就是遍歷這個(gè)集合,通過(guò)DexFile去尋找。
2 一個(gè)ClassLoader可以包含多個(gè)dex文件,每個(gè)dex文件是一個(gè)Element,多個(gè)dex文件排列成一個(gè)有序的數(shù)組dexElements,當(dāng)找類(lèi)的時(shí)候,會(huì)按順序遍歷dex文件,然后從當(dāng)前遍歷的dex文件中找類(lèi),如果找類(lèi)則返回,如果找不到從下一個(gè)dex文件繼續(xù)查找。
3 理論上,如果在不同的dex中有相同的類(lèi)存在,那么會(huì)優(yōu)先選擇排在前面的dex文件的類(lèi),如下圖:

圖1

4 把有問(wèn)題的類(lèi)打包到一個(gè)dex(patch.dex)中去,然后把這個(gè)dex插入到Elements的最前面,如下圖:

圖2
二:阻止相關(guān)類(lèi)打上CLASS_ISPREVERIFIED標(biāo)志

dex校驗(yàn): 如果兩個(gè)相關(guān)聯(lián)的類(lèi)在不同的dex中就會(huì)報(bào)錯(cuò),例如ClassA 引用了ClassB,但是發(fā)現(xiàn)這這兩個(gè)類(lèi)所在的dex不在一起,其中:

  1. ClassA 在classes.dex中
  2. ClassB 在patch.dex中
    結(jié)果發(fā)生了錯(cuò)誤。

dex校驗(yàn)的前提: 如果引用者這個(gè)類(lèi)被打上了CLASS_ISPREVERIFIED標(biāo)志,那么就會(huì)進(jìn)行dex的校驗(yàn)。

相關(guān)類(lèi)打上CLASS_ISPREVERIFIED標(biāo)志的發(fā)生場(chǎng)景:
在虛擬機(jī)啟動(dòng)的時(shí)候,當(dāng)verify選項(xiàng)被打開(kāi)的時(shí)候,如果static方法、private方法、構(gòu)造函數(shù)等,其中的直接引用(第一層關(guān)系)到的類(lèi)都在同一個(gè)dex文件中,那么這個(gè)類(lèi)就會(huì)被打上CLASS_ISPREVERIFIED
下圖是class A 打上CLASS_ISPREVERIFIED標(biāo)志

圖三

下圖是class A 沒(méi)有打上CLASS_ISPREVERIFIED標(biāo)志
圖四

其中AntilazyLoad類(lèi)會(huì)被打包成單獨(dú)的hack.dex,這樣當(dāng)安裝apk的時(shí)候,classes.dex內(nèi)的類(lèi)都會(huì)引用一個(gè)在不相同dex中的AntilazyLoad類(lèi),這樣就防止了類(lèi)被打上CLASS_ISPREVERIFIED的標(biāo)志了,只要沒(méi)被打上這個(gè)標(biāo)志的類(lèi)都可以進(jìn)行打補(bǔ)丁操作。

在class文件中插入代碼來(lái)阻止相關(guān)類(lèi)打上CLASS_ISPREVERIFIED標(biāo)志:在dx工具執(zhí)行之前,將LoadBugClass.class文件呢,進(jìn)行修改,再其構(gòu)造中添加System.out.println(dodola.hackdex.AntilazyLoad.class),然后繼續(xù)打包的流程。
原始代碼

package dodola.hackdex;
public class AntilazyLoad
{

}

package dodola.hotfix;
public class BugClass
{
    public String bug()
    {
        return "bug class";
    }
}

package dodola.hotfix;
public class LoadBugClass
{
    public String getBugString()
    {
        BugClass bugClass = new BugClass();
        return bugClass.bug();
    }
}

三:插入jar

插入代碼

System.out.println(dodola.hackdex.AntilazyLoad.class)

在構(gòu)造函數(shù)中插入操作代碼(javassist)

package test;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;

public class InjectHack
{
    public static void main(String[] args)
    {
        try
        {
            String path = "/Users/zhy/develop_work/eclipse_android/imooc/JavassistTest/";
            ClassPool classes = ClassPool.getDefault();
            classes.appendClassPath(path + "bin");//項(xiàng)目的bin目錄即可
            CtClass c = classes.get("dodola.hotfix.LoadBugClass");
            CtConstructor ctConstructor = c.getConstructors()[0];
            ctConstructor
                    .insertAfter("System.out.println(dodola.hackdex.AntilazyLoad.class);");
            c.writeFile(path + "/output");
        } catch (Exception e)
        {
            e.printStackTrace();
        }

    }
}

把AntilazyLoad.class打包成jar包,然后寫(xiě)入App的私有目錄,最后把該jar對(duì)應(yīng)的dexElements文件插入到數(shù)組的最前面。

public class HotfixApplication extends Application
{

    @Override
    public void onCreate()
    {
        super.onCreate();
       //創(chuàng)建jar對(duì)應(yīng)的文件
        File dexPath = new File(getDir("dex", Context.MODE_PRIVATE), "hackdex_dex.jar");
       //將asset文件中的jar寫(xiě)到App的私有目錄下面。
        Utils.prepareDex(this.getApplicationContext(), dexPath, "hackdex_dex.jar");
       // 把jar對(duì)應(yīng)的dexElements插入到dex數(shù)組最前面。
        HotFix.patch(this, dexPath.getAbsolutePath(), "dodola.hackdex.AntilazyLoad");
        try
        {
            this.getClassLoader().loadClass("dodola.hackdex.AntilazyLoad");
        } catch (ClassNotFoundException e)
        {
            e.printStackTrace();
        }

    }
}

創(chuàng)建jar對(duì)應(yīng)的文件

public class Utils {
    private static final int BUF_SIZE = 2048;

    public static boolean prepareDex(Context context, File dexInternalStoragePath, String dex_file) {
        BufferedInputStream bis = null;
        OutputStream dexWriter = null;
        bis = new BufferedInputStream(context.getAssets().open(dex_file));
        dexWriter = new BufferedOutputStream(new FileOutputStream(dexInternalStoragePath));
        byte[] buf = new byte[BUF_SIZE];
        int len;
        while ((len = bis.read(buf, 0, BUF_SIZE)) > 0) {
            dexWriter.write(buf, 0, len);
        }
        dexWriter.close();
        bis.close();
        return true;

}

找相應(yīng)的ClassLoader進(jìn)行操作

public final class HotFix
{
    public static void patch(Context context, String patchDexFile, String patchClassName)
    {
        if (patchDexFile != null && new File(patchDexFile).exists())
        {
            try
            {
                if (hasLexClassLoader())
                {
                    injectInAliyunOs(context, patchDexFile, patchClassName);
                } else if (hasDexClassLoader())
                {
                    injectAboveEqualApiLevel14(context, patchDexFile, patchClassName);
                } else
                {

                    injectBelowApiLevel14(context, patchDexFile, patchClassName);

                }
            } catch (Throwable th)
            {
            }
        }
    }
 }

Combine(合并)App的DexElements和AntilazyLoad.class的DexElements

 private static boolean hasDexClassLoader()
{
    try
    {
        Class.forName("dalvik.system.BaseDexClassLoader");
        return true;
    } catch (ClassNotFoundException e)
    {
        return false;
    }
}


 private static void injectAboveEqualApiLevel14(Context context, String str, String str2)
            throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException
{
    PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
    Object a = combineArray(getDexElements(getPathList(pathClassLoader)),
            getDexElements(getPathList(
                    new DexClassLoader(str, context.getDir("dex", 0).getAbsolutePath(), str, context.getClassLoader()))));
    Object a2 = getPathList(pathClassLoader);
    setField(a2, a2.getClass(), "dexElements", a);
    pathClassLoader.loadClass(str2);
}

將Patch.jar補(bǔ)丁插入到APP中,過(guò)程和插入AntilazyLoad.class一樣

public class HotfixApplication extends Application
{

    @Override
    public void onCreate()
    {
        super.onCreate();
        File dexPath = new File(getDir("dex", Context.MODE_PRIVATE), "hackdex_dex.jar");
        Utils.prepareDex(this.getApplicationContext(), dexPath, "hack_dex.jar");
        HotFix.patch(this, dexPath.getAbsolutePath(), "dodola.hackdex.AntilazyLoad");
        try
        {
            this.getClassLoader().loadClass("dodola.hackdex.AntilazyLoad");
        } catch (ClassNotFoundException e)
        {
            e.printStackTrace();
        }

        dexPath = new File(getDir("dex", Context.MODE_PRIVATE), "path_dex.jar");
        Utils.prepareDex(this.getApplicationContext(), dexPath, "path_dex.jar");
        HotFix.patch(this, dexPath.getAbsolutePath(), "dodola.hotfix.BugClass");

    }
}

四:總結(jié)

(1)因?yàn)槲覀兊腜atch是以獨(dú)立的jar包,插入到APP的DexElements中,
所以如果APP中中的類(lèi)引用了Patch中的類(lèi),就會(huì)在校驗(yàn)時(shí)報(bào)錯(cuò)。因?yàn)楫?dāng)進(jìn)行dex校驗(yàn)時(shí),如果兩個(gè)相關(guān)聯(lián)的類(lèi)在不同的dex中就會(huì)報(bào)錯(cuò)。(LoadBugClass引用BugClass)
(2) 為了防止上述錯(cuò)誤就要阻止Dex校驗(yàn),阻止Dex校驗(yàn)的方法是阻止相關(guān)類(lèi)打上CLASS_ISPREVERIFIED標(biāo)志。
(3) 阻止相關(guān)類(lèi)打上CLASS_ISPREVERIFIED標(biāo)志的做法是:在相關(guān)引用的類(lèi)(LoadBugClass.class)的構(gòu)造方法中,引用另外一個(gè)jar中類(lèi)AntilazyLoad.class
(4)因?yàn)?strong>AntilazyLoad.class在另一個(gè)jar中,所以需要把該jar對(duì)應(yīng)的dex插入到App中。并且在A(yíng)pplication中的onCreate()方法中將該類(lèi)加載進(jìn)來(lái)。
(5)把Patch.jar對(duì)應(yīng)的dexElement加載進(jìn)App中。因?yàn)镻atch.jar放在dex數(shù)組的第一個(gè)位置,所以首先被加載。即:如果在不同的dex中有相同的類(lèi)存在,那么會(huì)優(yōu)先選擇排在前面的dex文件的類(lèi)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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