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

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

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

熱修復(fù)概念: 以補(bǔ)丁的方式動(dòng)態(tài)修復(fù)緊急Bug惊豺,不再需要重新發(fā)布App,不再需要用戶重新下載闰非。
PathClassloader和DexClassLoader:
(1)PathClassloader作為其系統(tǒng)類和應(yīng)用類的加載器绳慎,只能去加載已經(jīng)安裝到Android系統(tǒng)中的apk文件。
(2)DexClassLoader可以用來從.jar和.apk類型的文件內(nèi)部加載classes.dex文件肩杈。可以用來執(zhí)行非安裝的程序代碼解寝。
(3)Android使用PathClassLoader作為其類加載器扩然,DexClassLoader可以從.jar和.apk類型的文件內(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ì)于類加載呢,就是遍歷這個(gè)集合嘉抓,通過DexFile去尋找索守。
2 一個(gè)ClassLoader可以包含多個(gè)dex文件,每個(gè)dex文件是一個(gè)Element抑片,多個(gè)dex文件排列成一個(gè)有序的數(shù)組dexElements卵佛,當(dāng)找類的時(shí)候,會(huì)按順序遍歷dex文件敞斋,然后從當(dāng)前遍歷的dex文件中找類截汪,如果找類則返回,如果找不到從下一個(gè)dex文件繼續(xù)查找植捎。
3 理論上衙解,如果在不同的dex中有相同的類存在,那么會(huì)優(yōu)先選擇排在前面的dex文件的類焰枢,如下圖:

圖1

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

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

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

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

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

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

圖三

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

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

在class文件中插入代碼來阻止相關(guān)類打上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包赞警,然后寫入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寫到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中,過程和插入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中中的類引用了Patch中的類旁瘫,就會(huì)在校驗(yàn)時(shí)報(bào)錯(cuò)。因?yàn)楫?dāng)進(jìn)行dex校驗(yàn)時(shí)琼蚯,如果兩個(gè)相關(guān)聯(lián)的類在不同的dex中就會(huì)報(bào)錯(cuò)酬凳。(LoadBugClass引用BugClass)
(2) 為了防止上述錯(cuò)誤就要阻止Dex校驗(yàn)遭庶,阻止Dex校驗(yàn)的方法是阻止相關(guān)類打上CLASS_ISPREVERIFIED標(biāo)志宁仔。
(3) 阻止相關(guān)類打上CLASS_ISPREVERIFIED標(biāo)志的做法是:在相關(guān)引用的類(LoadBugClass.class)的構(gòu)造方法中,引用另外一個(gè)jar中類AntilazyLoad.class
(4)因?yàn)?strong>AntilazyLoad.class在另一個(gè)jar中峦睡,所以需要把該jar對(duì)應(yīng)的dex插入到App中翎苫。并且在Application中的onCreate()方法中將該類加載進(jìn)來。
(5)把Patch.jar對(duì)應(yīng)的dexElement加載進(jìn)App中赐俗。因?yàn)镻atch.jar放在dex數(shù)組的第一個(gè)位置,所以首先被加載弊知。即:如果在不同的dex中有相同的類存在阻逮,那么會(huì)優(yōu)先選擇排在前面的dex文件的類。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末秩彤,一起剝皮案震驚了整個(gè)濱河市叔扼,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌漫雷,老刑警劉巖瓜富,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異降盹,居然都是意外死亡与柑,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來价捧,“玉大人丑念,你說我怎么就攤上這事〗狍” “怎么了脯倚?”我有些...
    開封第一講書人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)嵌屎。 經(jīng)常有香客問我推正,道長(zhǎng),這世上最難降的妖魔是什么宝惰? 我笑而不...
    開封第一講書人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任植榕,我火速辦了婚禮,結(jié)果婚禮上掌测,老公的妹妹穿的比我還像新娘内贮。我一直安慰自己,他們只是感情好汞斧,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開白布夜郁。 她就那樣靜靜地躺著,像睡著了一般粘勒。 火紅的嫁衣襯著肌膚如雪竞端。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,031評(píng)論 1 285
  • 那天庙睡,我揣著相機(jī)與錄音事富,去河邊找鬼。 笑死乘陪,一個(gè)胖子當(dāng)著我的面吹牛统台,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播啡邑,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼贱勃,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了谤逼?” 一聲冷哼從身側(cè)響起贵扰,我...
    開封第一講書人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎流部,沒想到半個(gè)月后戚绕,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡枝冀,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年舞丛,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了耘子。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡瓷马,死狀恐怖拴还,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情欧聘,我是刑警寧澤片林,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站怀骤,受9級(jí)特大地震影響费封,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蒋伦,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一弓摘、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧痕届,春花似錦韧献、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至嚷炉,卻和暖如春渊啰,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背申屹。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工绘证, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人哗讥。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓嚷那,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親杆煞。 傳聞我的和親對(duì)象是個(gè)殘疾皇子魏宽,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345

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