Android熱修復技術——QQ空間補丁方案解析(2)

接下來的幾篇博客我會用一個真實的demo來介紹如何實現(xiàn)熱修復。具體的內(nèi)容包括:

  • 如何打包補丁包
  • 如何將通過ClassLoader加載補丁包

1. 創(chuàng)建Demo

demo很簡單刹勃,創(chuàng)建一個只有一個Activity的demo:

package com.biyan.demo
public class MainActivity extends Activity {

    private Calculator mCal;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mCal = new Calculator();
    }
    public void click(View view) {
        Toast.makeText(this, String.valueOf(mCal.calculate()),Toast.LENGTH_SHORT).show();
    }
}
Public class Caculoator {
    public float calculate() {
        return 1 / 0;
    }
}

demo的代碼很簡單让禀,運行會出什么bug也很清楚了,在此就不演示了簿透。

2.創(chuàng)建補丁包

首先修復Calculator的bug铅碍。

package com.biyan.demo
Public class Caculoator {
    public float calculate() {
        return 1 / 1;
    }
}

重新編譯項目热鞍,在build目錄下找到Calculator.class文件,將其拷出來闷尿,準備打包塑径。放置在于Calculator包名相同的路徑下。


這里寫圖片描述

將其打成jar包:

jar -cvf patch.jar com

然后再將對應的jar包打成dex包:

dx --dex --output=patch_dex.jar patch.jar

dx是講jar包打成dex包的工具填具,安裝在path-android-sdk/build-tools/version(如24.0.0)/dx统舀。
patch_dex.jar就是補丁包,接下來將其安裝在sdCard中劳景,接下來應用從sdCard上加載該補丁包誉简。

3. 加載補丁

根據(jù)上一篇博客的介紹,加載補丁的思路如下:

  • 在Application的onCreate()方法中獲取應用本身的BaseDexClassLoader,然后通過反射得到對應的dexElements
  • 創(chuàng)建一個新的DexClassLoader實例盟广,然后加載sdCard上的補丁包闷串,然后通過同樣的方法得到對應的dexElements
  • 將兩個dexElements合并,然后再利用反射將合并后的dexElements賦值給應用本身的BaseDexClassLoader

接下來看下具體代碼:

package com.hotpatch.demo;

import android.app.Application;
import android.os.Environment;
import android.util.Log;
import java.io.File;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import dalvik.system.DexClassLoader;

/**
 * Created by hp on 2016/4/6.
 */
public class HotPatchApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();

        // 獲取補丁筋量,如果存在就執(zhí)行注入操作
        String dexPath = Environment.getExternalStorageDirectory().getAbsolutePath().concat("/patch_dex.jar");
        File file = new File(dexPath);
        if (file.exists()) {
            inject(dexPath);
        } else {
            Log.e("BugFixApplication", dexPath + "不存在");
        }
    }

    /**
     * 要注入的dex的路徑
     *
     * @param path
     */
    private void inject(String path) {
        try {
            // 獲取classes的dexElements
            Class<?> cl = Class.forName("dalvik.system.BaseDexClassLoader");
            Object pathList = getField(cl, "pathList", getClassLoader());
            Object baseElements = getField(pathList.getClass(), "dexElements", pathList);

            // 獲取patch_dex的dexElements(需要先加載dex)
            String dexopt = getDir("dexopt", 0).getAbsolutePath();
            DexClassLoader dexClassLoader = new DexClassLoader(path, dexopt, dexopt, getClassLoader());
            Object obj = getField(cl, "pathList", dexClassLoader);
            Object dexElements = getField(obj.getClass(), "dexElements", obj);

            // 合并兩個Elements
            Object combineElements = combineArray(dexElements, baseElements);

            // 將合并后的Element數(shù)組重新賦值給app的classLoader
            setField(pathList.getClass(), "dexElements", pathList, combineElements);

            //======== 以下是測試是否成功注入 =================
            Object object = getField(pathList.getClass(), "dexElements", pathList);
            int length = Array.getLength(object);
            Log.e("BugFixApplication", "length = " + length);

        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
    }

    /**
     * 通過反射獲取對象的屬性值
     */
    private Object getField(Class<?> cl, String fieldName, Object object) throws NoSuchFieldException, IllegalAccessException {
        Field field = cl.getDeclaredField(fieldName);
        field.setAccessible(true);
        return field.get(object);
    }

    /**
     * 通過反射設置對象的屬性值
     */
    private void setField(Class<?> cl, String fieldName, Object object, Object value) throws NoSuchFieldException, IllegalAccessException {
        Field field = cl.getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(object, value);
    }

    /**
     * 通過反射合并兩個數(shù)組
     */
    private Object combineArray(Object firstArr, Object secondArr) {
        int firstLength = Array.getLength(firstArr);
        int secondLength = Array.getLength(secondArr);
        int length = firstLength + secondLength;

        Class<?> componentType = firstArr.getClass().getComponentType();
        Object newArr = Array.newInstance(componentType, length);
        for (int i = 0; i < length; i++) {
            if (i < firstLength) {
                Array.set(newArr, i, Array.get(firstArr, i));
            } else {
                Array.set(newArr, i, Array.get(secondArr, i - firstLength));
            }
        }
        return newArr;
    }

}

核心代碼就這么多烹吵,接下來運行一下程序碉熄。程序還是Crash了。年叮。具被。


DingTalk20170220205018
DingTalk20170220205018

原因是類預校驗問題引起的:

  • 在apk安裝的時候系統(tǒng)會將dex文件優(yōu)化成odex文件玻募,在優(yōu)化的過程中會涉及一個預校驗的過程
  • 如果一個類的static方法只损,private方法,override方法以及構造函數(shù)中引用了其他類七咧,而且這些類都屬于同一個dex文件跃惫,此時該類就會被打上CLASS_ISPREVERIFIED
  • 如果在運行時被打上CLASS_ISPREVERIFIED的類引用了其他dex的類,就會報錯
  • 所以MainActivityonCreate()方法中引用另一個dex的類就會出現(xiàn)上文中的問題
  • 正常的分包方案會保證相關類被打入同一個dex文件
  • 想要使得patch可以被正常加載艾栋,就必須保證類不會被打上CLASS_ISPREVERIFIED標記爆存。而要實現(xiàn)這個目的就必須要在分完包后的class中植入對其他dex文件中類的引用
  • 要在已經(jīng)編譯完成后的類中植入對其他類的引用,就需要操作字節(jié)碼蝗砾,慣用的方案是插樁先较。常見的工具有javaassist,asm等悼粮。

其實QQ空間補丁方案的關鍵就在于字節(jié)碼的注入而不是dex的注入闲勺。下一篇博客將會介紹字節(jié)碼注入的相關細節(jié)。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末扣猫,一起剝皮案震驚了整個濱河市菜循,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌申尤,老刑警劉巖癌幕,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異昧穿,居然都是意外死亡勺远,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進店門时鸵,熙熙樓的掌柜王于貴愁眉苦臉地迎上來胶逢,“玉大人,你說我怎么就攤上這事寥枝∠芩” “怎么了?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵囊拜,是天一觀的道長某筐。 經(jīng)常有香客問我,道長冠跷,這世上最難降的妖魔是什么南誊? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任身诺,我火速辦了婚禮,結果婚禮上抄囚,老公的妹妹穿的比我還像新娘霉赡。我一直安慰自己,他們只是感情好幔托,可當我...
    茶點故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布穴亏。 她就那樣靜靜地躺著,像睡著了一般重挑。 火紅的嫁衣襯著肌膚如雪嗓化。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天谬哀,我揣著相機與錄音刺覆,去河邊找鬼。 笑死史煎,一個胖子當著我的面吹牛谦屑,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播篇梭,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼氢橙,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了很洋?” 一聲冷哼從身側響起充蓝,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎喉磁,沒想到半個月后谓苟,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡协怒,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年涝焙,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片孕暇。...
    茶點故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡仑撞,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出妖滔,到底是詐尸還是另有隱情隧哮,我是刑警寧澤,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布座舍,位于F島的核電站沮翔,受9級特大地震影響,放射性物質發(fā)生泄漏曲秉。R本人自食惡果不足惜采蚀,卻給世界環(huán)境...
    茶點故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一疲牵、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧榆鼠,春花似錦纲爸、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至责静,卻和暖如春袁滥,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背灾螃。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留揩徊,地道東北人腰鬼。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像塑荒,于是被迫代替她去往敵國和親熄赡。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,925評論 2 344

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