android熱修復(fù)的pre-verify問題詳解及實踐

本篇文章繼續(xù)上一篇瓢捉,主要分析一下classloader方案在dalvik虛擬機中的pre-verify問題舶治。關(guān)于classloader方案的原理可以參考上一篇文章android熱修復(fù)相關(guān)之Multidex解析進行了解请祖。自從Multidex出現(xiàn)之后,QQ空間的一篇文章引發(fā)了classloader熱修復(fù)方案的浪潮因痛,包括開源的Nuwa,HotFix,Tinker等饮醇,很有價值的一篇文章安卓App熱補丁動態(tài)修復(fù)技術(shù)介紹。這篇文章比較清晰的闡述了pre-verify問題以及解決方案浪漠,但是我看完后還是有些疑惑的陕习,比如為什么當類A直接引用了類B后,就可以不被打上CLASS_VERIFIED標記址愿?這篇文章采用實踐加源碼的方式该镣,因為篇幅原因,具體的實踐操作過程可能不會特別詳細响谓,但是力求講清楚整個pre-verify的出現(xiàn)及解決過程损合,順便也可以了解到dalvik虛擬機的dexopt的大致流程。

image.png

盜個QQ空間的原理圖娘纷,原理很簡單,假設(shè)classes.dex中的Qzone.class有bug赖晶,我們通過動態(tài)加載patch.dex,并將patch.dex插入到Elements數(shù)組中,保證在classes.dex的前面捂贿。這樣一來,當出發(fā)Qzone.class的加載時厂僧,很明顯會加載到patch.class中的Qzone.class,而classes.dex中的Qzone.class是永遠加載不到的,從而達到熱修復(fù)的效果颜屠。從原理上分析沒有任何問題白魂,實踐一下看看:
首先,我們新建一個FixTest工程福荸,添加一個名為patch的module,核心代碼如下:

 public static void inject(Context context,String dexPath){
    try {
      Class<?> cl = Class.forName("dalvik.system.BaseDexClassLoader");
      Object originPathList=ReflectionUtils.getField(cl,DexUtils.class.getClassLoader(),"pathList");
      Object originElements=ReflectionUtils.getField(originPathList.getClass(),originPathList,"dexElements");
      String dexOpt=context.getDir("odex",0).getAbsolutePath();
      DexClassLoader dexClassLoader=new DexClassLoader(dexPath,dexOpt,dexOpt,DexUtils.class.getClassLoader());
      Object pathList=ReflectionUtils.getField(cl,dexClassLoader,"pathList");
      Object elements=ReflectionUtils.getField(pathList.getClass(),pathList,"dexElements");
      Object combineElements=combineArray(elements,originElements);
      ReflectionUtils.setFeild(originPathList.getClass(),originPathList,"dexElements",combineElements);
      Object object= ReflectionUtils.getField(originPathList.getClass(),originPathList,"dexElements");
      Log.i("ljj", "inject->length: "+Array.getLength(object));
    } catch (ClassNotFoundException e) {
      e.printStackTrace();
    } catch (IllegalAccessException e) {
      e.printStackTrace();
    } catch (NoSuchFieldException e) {
      e.printStackTrace();
    }
  }

代碼就是實現(xiàn)了上圖中的原理背传,將dexPath對應(yīng)的patch包插入到了PathClassLoader的Elements的前面呆瞻。我們在app的module中,引入patch径玖,進行測試,首先在app的Application中加入

 String dexPath = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "patch.dex";
 HotFix.inject(this, dexPath);

我們新建一個Test類痴脾,在MainActivity中用一個TextView顯示showText的結(jié)果,直接運行程序梳星,textView顯示“I am an error”赞赖。

public class Test {
  public String showText(){
    return "I am an error";
  }
}

假設(shè)現(xiàn)在發(fā)現(xiàn)顯示錯了,我們要顯示的是“I am a patch”冤灾,按照上面所說的前域,我們可以修改Test類,然后打包命名為patch.dex進行下發(fā)即可韵吨。至于patch.dex的生成匿垄,有很多種方式,我們直接修改showText方法后归粉,執(zhí)行g(shù)radle build椿疗,然后將/app/build/intermediates/classes/debug/包名/Test.class文件隨便拷貝到一個目錄,在目錄中建立包級文件夾糠悼,假設(shè)頂層文件夾為dex届榄,里層文件夾為com/ljj/fixtest/Test.class,調(diào)用dx命令

dx --dex --output=patch.dex  dex

將生成的patch.dex放到SDcard的根目錄。好的绢掰,至此一切準備工作都已經(jīng)完成痒蓬,我們在android5.0童擎,6.0滴劲,7.0上都能正常運行,但在android4.2的手機上當我們重啟時,報了這樣的異常:

01-02 00:56:37.674 11264-11264/com.ljj.fixtest E/AndroidRuntime: FATAL EXCEPTION: main
                                                                 java.lang.IllegalAccessError: Class ref in pre-verified class resolved to unexpected implementation
                                                                     at com.ljj.fixtest.MainActivity$1.onClick(MainActivity.java:20)
                                                                     at android.view.View.performClick(View.java:4299)
                                                                     at android.view.View$PerformClick.run(View.java:17576)
                                                                     at android.os.Handler.handleCallback(Handler.java:725)
                                                                     at android.os.Handler.dispatchMessage(Handler.java:92)
                                                                     at android.os.Looper.loop(Looper.java:153)
                                                                     at android.app.ActivityThread.main(ActivityThread.java:5356)
                                                                     at java.lang.reflect.Method.invokeNative(Native Method)
                                                                     at java.lang.reflect.Method.invoke(Method.java:511)
                                                                     at

直譯就是一個被標為pre-verify的class引用了一個ref類顾复,這個ref類被發(fā)現(xiàn)不是期待的實現(xiàn)方式班挖,也就是被換掉了,去看一下異常拋出的位置以及如何調(diào)用到這個位置的芯砸。
在本例中萧芙,MainActivity中引用到了Test類的showText方法,執(zhí)行MainActivity的onCreate方法時會嘗試解析Test類假丧。MainActivity的onCreate方法很簡單双揪。

public class MainActivity extends Activity {

  @Override
  protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    final TextView textView=(TextView) findViewById(R.id.mytv);
    textView.setText(new Test().showText());

  }
}

我們看一下反編譯后onCreate的smali代碼渔期。


image.png

而解釋器執(zhí)行到new-instance時拘哨,會觸發(fā)倦青,最終會調(diào)用到dvmResolvedClass方法

HANDLE_OPCODE(OP_NEW_INSTANCE /*vAA, class@BBBB*/)
   {
       ClassObject* clazz;
       Object* newObj;

       EXPORT_PC();

       vdst = INST_AA(inst);
       ref = FETCH(1);
       ILOGV("|new-instance v%d,class@0x%04x", vdst, ref);
       clazz = dvmDexGetResolvedClass(methodClassDex, ref);
       if (clazz == NULL) {
           clazz = dvmResolveClass(curMethod->clazz, ref, false);
           if (clazz == NULL)
               GOTO_exceptionThrown();
       }

dvmResolvedClass方法位于/dalvik/vm/oo/Resolve.cpp中产镐。

ClassObject* dvmResolveClass(const ClassObject* referrer, u4 classIdx,
    bool fromUnverifiedConstant)
{
    DvmDex* pDvmDex = referrer->pDvmDex;
    ClassObject* resClass;
    const char* className;
    resClass = dvmDexGetResolvedClass(pDvmDex, classIdx);
    if (resClass != NULL)
        return resClass;
    className = dexStringByTypeIdx(pDvmDex->pDexFile, classIdx);
    if (className[0] != '\0' && className[1] == '\0') {
        /* primitive type */
        resClass = dvmFindPrimitiveClass(className[0]);
    } else {
        resClass = dvmFindClassNoInit(className, referrer->classLoader);
    }
    if (resClass != NULL) {
        if (!fromUnverifiedConstant &&
            IS_CLASS_FLAG_SET(referrer, CLASS_ISPREVERIFIED))
        {
            ClassObject* resClassCheck = resClass;
            if (dvmIsArrayClass(resClassCheck))
                resClassCheck = resClassCheck->elementClass;

            if (referrer->pDvmDex != resClassCheck->pDvmDex &&
                resClassCheck->classLoader != NULL) 
            {
                LOGW("Class resolved by unexpected DEX:"
                     " %s(%p):%p ref [%s] %s(%p):%p",
                    referrer->descriptor, referrer->classLoader,
                    referrer->pDvmDex,
                    resClass->descriptor, resClassCheck->descriptor,
                    resClassCheck->classLoader, resClassCheck->pDvmDex);
                LOGW("(%s had used a different %s during pre-verification)",
                    referrer->descriptor, resClass->descriptor);
                dvmThrowIllegalAccessError(
                    "Class ref in pre-verified class resolved to unexpected "
                    "implementation");
                return NULL;
            }
        }
      .........
    return resClass;
}

referrer是curMethod->clazz , 首先在dvmDexGetResolvedClass方法中判斷是否解析過該類,很明顯逃糟,該類是首次加載蓬豁,所以返回結(jié)果為空地粪,然后調(diào)用dvmFindClassNoInit方法用classloader去查找類蟆技,因為patch.dex已經(jīng)在之前反射注入到了elements中,所以此時resClass不為空旺聚,此時檢查MainActivity是否被打上了CLASS_ISPREVERIFIED砰粹,此時先給出結(jié)果碱璃,肯定是打上了的饭入,進而轉(zhuǎn)入到
if (referrer->pDvmDex != resClassCheck->pDvmDex && resClassCheck->classLoader != NULL)
這行代碼翻譯過來就是MainActivity所在的dex和Test所在的dex不是同一個且Test的類加載器不為空的情況下谐丢,就會拋出異常 "Class ref in pre-verified class resolved to unexpected implementation",現(xiàn)在大家都應(yīng)該清楚了這個異常具體的來源凭疮。
出現(xiàn)問題了执解,看看如何解決衰腌?盜用騰訊bugly的一張圖

image.png

當三個條件均滿足時右蕊,會拋出異常饶囚,解決方案大致上有以下四種萝风。

  • 修改fromUnverfiedConstant=true
    需要通過 native hook 攔截系統(tǒng)方法紫岩,更改方法的入口參數(shù)泉蝌,將 fromUnverifiedConstant 統(tǒng)一改為 true贪磺,風(fēng)險大粥鞋,幾乎無人采用瞄崇。
  • 禁止dexopt過程打上CLASS_ISPREVERIFIED標記
    Q-zone方案突破了此限制等浊,但是損失了性能摹蘑。
  • 補丁類與引用類放在同一個dex中
    Tinker等全量合成方案突破了此限制。
  • 使dvmDexGetResolvedClass返回不為null过咬,直接返回
    QFix的方案制妄,可參考這篇文章QFix探索之路—手Q熱補丁輕量級方案

各個方案都有各自的優(yōu)缺點耕捞,我們從學(xué)習(xí)的角度看,學(xué)習(xí)一下Q-zone方案的實現(xiàn)敞映。Q-zone方案的原理是在每個類的構(gòu)造方法中加入一行代碼,保證Hack.class在單獨的dex中振愿,選擇在構(gòu)造函數(shù)中進行可以不增加方法數(shù)埃疫。如下:

public class Test {
  public Test() {
    System.out.println(Hack.class);
  }
}

我們從源碼的角度看一下栓霜,為什么加入了這行代碼胳蛮,每個插入的類中都不會打上CLASS_ISPREVERIFIED了仅炊。
dexopt的過程是分為verify+optimize兩個步驟進行的抚垄,對于每個類的verify+optimize方法是在verifyAndOptimizeClass方法中進行的呆馁,源碼位置在:
/dalvik/vm/analysis/DexPrepare.cpp

static void verifyAndOptimizeClass(DexFile* pDexFile, ClassObject* clazz,
    const DexClassDef* pClassDef, bool doVerify, bool doOpt)
{
    ....
    if (doVerify) {
        if (dvmVerifyClass(clazz)) {
            /*
             * Set the "is preverified" flag in the DexClassDef.  We
             * do it here, rather than in the ClassObject structure,
             * because the DexClassDef is part of the odex file.
             */
            assert((clazz->accessFlags & JAVA_FLAGS_MASK) ==
                pClassDef->accessFlags);
            ((DexClassDef*)pClassDef)->accessFlags |= CLASS_ISPREVERIFIED;
            verified = true;
        } else {
            // TODO: log when in verbose mode
            LOGV("DexOpt: '%s' failed verification", classDescriptor);
        }
    }

    if (doOpt) {
        bool needVerify = (gDvm.dexOptMode == OPTIMIZE_MODE_VERIFIED ||
                           gDvm.dexOptMode == OPTIMIZE_MODE_FULL);
        if (!verified && needVerify) {
            LOGV("DexOpt: not optimizing '%s': not verified",
                classDescriptor);
        } else {
            dvmOptimizeClass(clazz, false);

            /* set the flag whether or not we actually changed anything */
            ((DexClassDef*)pClassDef)->accessFlags |= CLASS_ISOPTIMIZED;
        }
    }
}

很清晰,dvmVerifyClass如果校驗通過了纺腊,該clazz就會被打上CLASS_ISPREVERIFIED標記誓沸。接下來我們主要看dvmVerifyClass方法都干了什么壹粟。源碼位置:/dalvik/vm/analysis/DexVerify.cpp

bool dvmVerifyClass(ClassObject* clazz)
{
    int i;
    if (dvmIsClassVerified(clazz)) {
        LOGD("Ignoring duplicate verify attempt on %s", clazz->descriptor);
        return true;
    }
    for (i = 0; i < clazz->directMethodCount; i++) {
        if (!verifyMethod(&clazz->directMethods[i])) {
            LOG_VFY("Verifier rejected class %s", clazz->descriptor);
            return false;
        }
    }
    for (i = 0; i < clazz->virtualMethodCount; i++) {
        if (!verifyMethod(&clazz->virtualMethods[i])) {
            LOG_VFY("Verifier rejected class %s", clazz->descriptor);
            return false;
        }
    }
    return true;
}

在verifyMethod中會對Method的各個字段進行驗證煮寡,篇幅原因薇组,不進行逐層源碼追蹤了坐儿,在verifyMethod方法中貌矿,會調(diào)用dvmVerifyCodeFlow方法逛漫,接著調(diào)用doCodeVerification酌毡,會具體分析每一條指令枷踏,執(zhí)行必要的解析及驗證旭蠕。對于每一條指令掏熬,是調(diào)用verifyInstruction方法來驗證的。verifyInstruction方法的源碼位置:/dalvik/vm/CodeVerify.cpp讶坯。在verifyInstruction中辆琅,注意這段代碼。

  case OP_CONST_CLASS:
    case OP_CONST_CLASS_JUMBO:
        assert(gDvm.classJavaLangClass != NULL);
        /* make sure we can resolve the class; access check is important */
        resClass = dvmOptResolveClass(meth->clazz, decInsn.vB, &failure);
        if (resClass == NULL) {
            const char* badClassDesc = dexStringByTypeIdx(pDexFile, decInsn.vB);
            dvmLogUnableToResolveClass(badClassDesc, meth);
            LOG_VFY("VFY: unable to resolve const-class %d (%s) in %s",
                decInsn.vB, badClassDesc, meth->clazz->descriptor);
            assert(failure != VERIFY_ERROR_GENERIC);
        } else {
            setRegisterType(workLine, decInsn.vA,
                regTypeFromClass(gDvm.classJavaLangClass));
        }
        break;

為什么要關(guān)注OP_CONST_CLASS,因為我們插入的System.out.println(Hack.class);會生成const-class的dalvik指令这刷,可以通過dexdump或者反編譯apk來查看婉烟,此時會觸發(fā)dvmOptResolveClass的調(diào)用。dvmOptResolveClass函數(shù)會去查找Hack.class,由于我們的dex沒有Hack.class,肯定查不到暇屋,拋異常返回似袁,此時這個類的dvmVerifyClass過程會返回false,這個類也就沒有打上CLASS_ISPREVERIFIED咐刨,而verified為false昙衅,導(dǎo)致也不會進行optimize過程。

值得說明的是如果類沒有打上CLASS_ISPREVERIFIED定鸟,那么verify+optimize都會在類第一次加載時dvmInitClass中進行而涉,正常情況下每個類的verify+optimize只會在安裝時dexopt中進行一次啼县,verify過程非常重,會對類的所有方法的所有指令都進行校驗,如果短時間內(nèi),大量的類進行verify,耗時是比較嚴重的,尤其在應(yīng)用剛啟動的時候送挑,有可能造成白屏司澎。

至于我們?nèi)绾尾迦隨ystem.out.println(Hack.class),我們可以采用transformAPI+javaassist進行實現(xiàn)蛤铜。實現(xiàn)過程注意兩點:

  • Application不要插入Hack.class,因為application的構(gòu)造函數(shù)執(zhí)行時,我們還沒有注入hack.apk
  • 在注入patch.dex前注入hack.apk,否則會找不到類

pre-verify方案驗證demo腐芍,很簡單,直接運行app,然后將patch.dex放到sdcard的根目錄下即可。
Demo地址:https://github.com/jjlan/FixTest

參考:

  1. Android熱補丁動態(tài)修復(fù)技術(shù)(一):從Dex分包原理到熱補丁
  2. Android Classloader熱修復(fù)技術(shù)之百家齊放
  3. 安卓App熱補丁動態(tài)修復(fù)技術(shù)介紹
  4. QFix探索之路—手Q熱補丁輕量級方案

目前本人在公司負責熱修復(fù)相關(guān)的工作脑沿,主要是基于robust的熱修復(fù)相關(guān)工作韭邓。感興趣的同學(xué)歡迎進群交流诗力。


image.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市劝贸,隨后出現(xiàn)的幾起案子捌议,更是在濱河造成了極大的恐慌宫补,老刑警劉巖斋荞,帶你破解...
    沈念sama閱讀 206,723評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異还绘,居然都是意外死亡抚太,警方通過查閱死者的電腦和手機身冀,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評論 2 382
  • 文/潘曉璐 我一進店門剩愧,熙熙樓的掌柜王于貴愁眉苦臉地迎上來锦积,“玉大人丰介,你說我怎么就攤上這事垛叨』估猓” “怎么了?”我有些...
    開封第一講書人閱讀 152,998評論 0 344
  • 文/不壞的土叔 我叫張陵撬碟,是天一觀的道長诞挨。 經(jīng)常有香客問我,道長呢蛤,這世上最難降的妖魔是什么惶傻? 我笑而不...
    開封第一講書人閱讀 55,323評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮其障,結(jié)果婚禮上银室,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好粮揉,可當我...
    茶點故事閱讀 64,355評論 5 374
  • 文/花漫 我一把揭開白布巡李。 她就那樣靜靜地躺著,像睡著了一般扶认。 火紅的嫁衣襯著肌膚如雪侨拦。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,079評論 1 285
  • 那天辐宾,我揣著相機與錄音狱从,去河邊找鬼。 笑死叠纹,一個胖子當著我的面吹牛季研,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播誉察,決...
    沈念sama閱讀 38,389評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼与涡,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了持偏?” 一聲冷哼從身側(cè)響起驼卖,我...
    開封第一講書人閱讀 37,019評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎鸿秆,沒想到半個月后酌畜,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,519評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡卿叽,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,971評論 2 325
  • 正文 我和宋清朗相戀三年桥胞,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片考婴。...
    茶點故事閱讀 38,100評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡贩虾,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蕉扮,到底是詐尸還是另有隱情整胃,我是刑警寧澤,帶...
    沈念sama閱讀 33,738評論 4 324
  • 正文 年R本政府宣布喳钟,位于F島的核電站屁使,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏奔则。R本人自食惡果不足惜蛮寂,卻給世界環(huán)境...
    茶點故事閱讀 39,293評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望易茬。 院中可真熱鬧酬蹋,春花似錦及老、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至匕垫,卻和暖如春僧鲁,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背象泵。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評論 1 262
  • 我被黑心中介騙來泰國打工寞秃, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人偶惠。 一個月前我還...
    沈念sama閱讀 45,547評論 2 354
  • 正文 我出身青樓春寿,卻偏偏與公主長得像,于是被迫代替她去往敵國和親忽孽。 傳聞我的和親對象是個殘疾皇子绑改,可洞房花燭夜當晚...
    茶點故事閱讀 42,834評論 2 345

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