DexClassLoader熱修復的入門到放棄

前提

寫這篇文章的目的也是爲了瞭解android源碼及hack技術,讀了這篇文章相信你也能夠了解到Dalvik的工做流程,apk的生成過程,及build.gradle中plugin中ApplicationPlugin的Task有哪些,如何經過hack技術來完成hotfix。有興趣的同窗也能夠看看groovy如何編寫Plugin,及如何優化dex來讓優化appjava

熱修復須要注意的幾個問題

  • 如何進行hack來達到熱修復
  • hack操做中須要使用哪些class
  • apk是生成的生命週期
  • gradle build 腳本(groovy)
    咱們先了解這些問題後再進行具體的操做步驟,來個按部就班。問題接下來會一一的詳解
如何進行hack來達到熱修復

爲何會有熱修復這個東西呢?你們都知道若是咱們的線上的app 因爲某種緣由crash?咱們這時候不能怨測試沒測好,後臺接口有變化什麼的,這不是解決問題的最終方式!要是之前咱們確定就是把從新上傳app到各大渠道,重新上線,這個過程嚴重的影響到咱們的用戶體驗很是很差,並且很耗時!做爲程序員如何經過代碼進行線上修復crash bug。。。呢?因此有了熱修復這個功能 bat 每家都有本身的開源熱修復庫?我這裏就講一下如何經過反射的方式來實現修復功能吧!也就是經過DexClassLoader。若是你們對其餘的開源庫想要了解的話能夠經過一下傳送門
AndFix
tinker
HotFix
Robust
我這裏也就講一下Dex的方式修復android

hack操做中須要使用哪些class

  • 顧名思義DexClassLoader這個必需要用到的
  • javaassist用於代碼的打樁(就是class文件代碼的植入,這裏不詳解了)
  • groovy一個android plugin插件開發語言 底下會說起到
    -

apk是生成的生命週期

咱們的項目如何在編譯的時候變成apk呢?git

  1. 第一步固然是把咱們的資源文件生成R.Java文件了
  2. 處理AIDL文件,生成對應的.java文件(固然,有不少工程沒有用到AIDL,那這個過程就能夠省了)
  3. 編譯Java文件,生成對應的.class文件
  4. 把.class文件轉化成Davik VM支持的.dex文件
  5. 打包生成未簽名的.apk文件
  6. 對未簽名.apk文件進行簽名
  7. 對簽名後的.apk文件進行對齊處理(不進行對齊處理是不能發佈到Google Market的)
    我感受仍是貼圖比較靠譜否則看文字沒有感受

build.png
build.png

這就是一個apk編譯所走的生命週期,可是咱們的build腳本到底走了哪些任務呢。若是想看的話能夠在咱們module中的build.gradle 加入以下代碼 便可在console中看到相應的任務程序員

tasks.whenTaskAdded { task ->
    println(task.name+"===")
}複製代碼

這個就是咱們apkbuild的時候的每個task。既然知道了這些task 那咱們如何才能知道這些task到底在後臺作了些什麼呢?github

gradle build 腳本(groovy)

時常見到殊不知道他在幹嗎的一句代碼,apply plugin: 'com.android.application'若是咱們把com.android.application代替爲com.android.library,那咱們的build目錄下的output那就是aar包了。組件化開發會用到這樣的切換想了解的能夠看看(組件化)編程

想要了解這句話幹嗎的那你必須的知道這個開發語言groovy,他是支持android studio的。咱們能夠自定義咱們想要的插件,在編譯的時候進行一些好玩的操做。這裏面能夠定義許多task,迴歸正題,這句代碼到底幹了哪些事情呢,那咱們就必須的瞭解這個源碼想要了解的同窗能夠看看這裏就很少說了,這裏面有咱們build中的全部taskapi

####app啓動過程
以上了解了這麼多,接下來就要進入正題了!如今說app的啓動過程,過程就不細說了,由於經歷了不少複雜的過程,我就說一下與DexClassLoader有關的事情吧。數組

app每次啓動fork一個進程但同時也會一樣會分配一個dalvik虛擬機供這個app運行,在dalvik中每次運行都須要讀取apk裏面的dex文件,這樣會耗費不少cpu資源,而後採用odex,把dex文件優化成odex文件,那麼odex操做給咱們熱修復帶來了哪些問題呢?咱們先把這個問題記錄下來,以後會分析具體緣由。啓動先說到這!!!app

如何進行熱修復

  • 經過上面瞭解了app啓動過程當中每次都要經過dvm來加載dex文件。
  • 一樣你們也知道了dex文件是由 .java->.class->dex 一步一步轉化來的
    瞭解了上面的兩個重要的東西,熱修復就是每次在咱們app啓動的時候加載咱們本身的patch.dex文件而不是加載以前的dex文件,這樣就能夠達到熱修復了(這時大概會有不少同窗困惑,dvm怎麼知道就用咱們的patch.dex而不用以前的呢?好問題 讓老夫徐徐道來)

動態加載patch.dex

  • 在 Android 中,App 安裝到手機後,apk 裏面的 class.dex 中的 class 均是經過 PathClassLoader 來加載的。
  • DexClassLoader 能夠用來加載 SD 卡上加載包含 class.dex 的 .jar 和 .apk 文件
  • DexClassLoader 和 PathClassLoader 的基類 BaseDexClassLoader 查找 class 是經過其內部的 DexPathList pathList 來查找的
  • DexPathList 內部有一個 Element[] dexElements 數組,其 findClass() 方法(源碼以下)的實現就是遍歷該數組,查找 class ,一旦找到須要的類,就直接返回,中止遍歷:
public Class findClass(String name, List
  
  
  

 
  
  suppressed) { for (Element element : dexElements) { DexFile dex = element.dexFile; if (dex != null) { Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed); if (clazz != null) { return clazz; } } } if (dexElementsSuppressedExceptions != null) { suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions)); } return null; } 

 複製代碼

經過上面的步驟是否是知道了咱們app每次啓動一個class是如何找到類的呢?如今知道了吧,DexClassLoader -> DexPathList -> Element[]
好的 如今應該有一些系統的瞭解了,經過上面的步驟能夠知道 每次查找類都是經過Element[]中查找的。若是找到就會return 而不會繼續找!這時候嘿嘿嘿咱們知道了他是如何findclass 的那咱們就能夠悄悄的幹些壞事了(這裏會有一些同窗會懵逼,Element[]是什麼鬼)ide

Element[]是什麼鬼

咱們在每次建立DexClassLoader時他的構造函數是這樣的

public DexClassLoader(String dexPath, String optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }複製代碼

從源碼能夠看出第一個參數顧名思義是dex路徑,第二個呢能夠看看源碼,第二個要傳一個路徑dex優化後odex的路徑。第三個呢就是父類嗎,直接getClassLoader()就好那麼咱們看看他的父類拿這些參數幹了些什麼

public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(parent);

        this.originalPath = dexPath;
        this.pathList =
            new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }複製代碼

父類也就作了一些初始化操做。最主要的是初始化了DexPathList這個類,而後咱們看看BaseDexClassLoader裏面的findClass作了些什麼呢源碼以下

@Override
protected Class
  
  
  

  findClass(String name) throws ClassNotFoundException {
        Class clazz = pathList.findClass(name);

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

        return clazz;
    }複製代碼

看到了嗎經過咱們構造函數初始化的DexPathList來查找的,上面咱們已經貼了DexPathList內部findclass的他是經過Element[]來拿到的。接下來咱們來看看DexPathList的構造函數

public DexPathList(ClassLoader definingContext, String dexPath,
            String libraryPath, File optimizedDirectory) {
        if (definingContext == null) {
            throw new NullPointerException("definingContext == null");
        }

        if (dexPath == null) {
            throw new NullPointerException("dexPath == null");
        }

        if (optimizedDirectory != null) {
            if (!optimizedDirectory.exists())  {
                throw new IllegalArgumentException(
                        "optimizedDirectory doesn't exist: "
                        + optimizedDirectory);
            }

            if (!(optimizedDirectory.canRead()
                            && optimizedDirectory.canWrite())) {
                throw new IllegalArgumentException(
                        "optimizedDirectory not readable/writable: "
                        + optimizedDirectory);
            }
        }

        this.definingContext = definingContext;
        this.dexElements =
            makeDexElements(splitDexPath(dexPath), optimizedDirectory);
        this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
    }複製代碼

好了這就是他的源碼了可看到在夠着函數中有一個很重要的一步就是對(makeDexElements這個方法)Element[]初始化 說了這麼多終於到這個地方了 這是什麼鬼,進入這個方法來看一下

private static Element[] makeDexElements(ArrayList
  
  
  

 
  
  files, File optimizedDirectory) { ArrayList 
 
  
    elements = new ArrayList 
   
     (); /* * Open all files and load the (direct or contained) dex files * up front. */ for (File file : files) { ZipFile zip = null; DexFile dex = null; String name = file.getName(); if (name.endsWith(DEX_SUFFIX)) { // Raw dex file (not inside a zip/jar). try { dex = loadDexFile(file, optimizedDirectory); } catch (IOException ex) { System.logE("Unable to load dex file: " + file, ex); } } else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX) || name.endsWith(ZIP_SUFFIX)) { try { zip = new ZipFile(file); } catch (IOException ex) { /* * Note: ZipException (a subclass of IOException) * might get thrown by the ZipFile constructor * (e.g. if the file isn't actually a zip/jar * file). */ System.logE("Unable to open zip file: " + file, ex); } try { dex = loadDexFile(file, optimizedDirectory); } catch (IOException ignored) { /* * IOException might get thrown "legitimately" by * the DexFile constructor if the zip file turns * out to be resource-only (that is, no * classes.dex file in it). Safe to just ignore * the exception here, and let dex == null. */ } } else { System.logW("Unknown file type for: " + file); } if ((zip != null) || (dex != null)) { elements.add(new Element(file, zip, dex)); } } return elements.toArray(new Element[elements.size()]); } 
    
   

 複製代碼

代碼有點多哈...不要着急 我來慢慢說,首選呢構造函數傳進來了一個file數組不管是jar文件仍是apk文件咱們在這一步都是吧他們轉換成dex文件至關於作了一個操做把patch.jar 改爲patch.dex而後轉存到最初咱們傳進來的那個optimizedDirectory文件夾下。而後咱們的Element這個類是個靜態內部類,能夠看看下面的源碼的構造函數

public Element(File file, ZipFile zipFile, DexFile dexFile) {
            this.file = file;
            this.zipFile = zipFile;
            this.dexFile = dexFile;
        }複製代碼

能夠看到他傳入了這些參數。好了如今知道了這是什麼鬼了吧,一系列的源碼恐怕會看的頭暈腦脹的吧。反正知道了Element就是存儲咱們dex文件的每次findclass的時候從這裏面取得,知道這個就好了。

插入咱們須要的加載的patch.dex

如今知道往哪裏插入咱們的dex文件了吧,只要在咱們app啓動的時候,把咱們的dex文件加載到Element[]數組最前面就好了,每次findclass的時候確定先查找咱們的dex了。這樣不就能夠達到熱修復了嗎!

  • 第一步建立一個咱們的DexClassLoader 把咱們的patch.dex(或.jar)文件傳進去
  • 第二步經過反射拿到咱們建立的DexClassLoader裏面的DexPathList裏面的Element[]
  • 拿到apk的DexClassLoader(getClassLoader()這個方法就能夠拿到)而後一樣反射的方式拿到DexPathList裏面的Element[]。
  • 最關鍵的一部就是把咱們patch的Element[]和apk的Element[]合併在一塊兒而後經過反射修改apk裏面的Element[](別合併錯了,要把咱們的數據插入最前面)
  • 以上步驟要在Application生命週期中的attachBaseContext進行執行否則,在onCreate裏面執行的話app就已經初始化好了
    這裏我就用Nuva熱修復的代碼來舉例吧,他這邊寫的很詳細的 git傳送門哈哈哈博主已棄坑 放棄維護了
DexClassLoader dexClassLoader = new DexClassLoader(dexPath, defaultDexOptPath, dexPath, getPathClassLoader());
        Object baseDexElements = getDexElements(getPathList(getPathClassLoader()));
        Object newDexElements = getDexElements(getPathList(dexClassLoader));
        Object allDexElements = combineArray(newDexElements, baseDexElements);
        Object pathList = getPathList(getPathClassLoader());
        ReflectionUtils.setField(pathList, pathList.getClass(), "dexElements", allDexElements);複製代碼

這就是我所說的那四步。

大功告成是否是很帶勁吧。md終於把咱們的dex文件插入了。嘿嘿嘿黑科技啊,熱修復原來如此簡單。別高興的太早。接下來重點來了

坑1(CLASS_ISPREVERIFIED)預約義

這時候你運行項目的時候會發現app 掛了 哈哈哈 真是日了狗了,不出意外的話會報一下錯誤class ref in pre-verified class resolved to unexpected implementation 這個就是上面所說的odex操做帶來的麻煩。
出問題嗎?固然要慢慢解決了。先了解一下odex吧

  • 在apk安裝的時候系統會將dex文件優化成odex文件,在優化的過程當中會涉及一個預校驗的過程
  • 若是一個類的static方法,private方法,override方法以及構造函數中引用了其餘類,並且這些類都屬於同一個dex文件,此時該類就會被打上CLASS_ISPREVERIFIED
  • 若是在運行時被打上CLASS_ISPREVERIFIED的類引用了其餘dex的類,就會報錯
  • 因此你的類中引用另外一個dex的類就會出現上文中的問題
  • 正常的分包方案會保證相關類被打入同一個dex文件
  • 想要使得patch能夠被正常加載,就必須保證類不會被打上CLASS_ISPREVERIFIED標記。而要實現這個目的就必需要在分完包後的class中植入對其餘dex文件中類的引用
  • 要在已經編譯完成後的類中植入對其餘類的引用,就須要操做字節碼,慣用的方案是插樁。常見的工具備javaassist,asm等

這時候你們就要了解這個了javaassist,一個代碼植入庫,幾個簡單的api你們看看都會

/**
     * 植入代碼
     * @param buildDir 是項目的build class目錄,就是咱們須要注入的class所在地
     * @param lib 這個是hackdex的目錄,就是AntilazyLoad類的class文件所在地
     */
    public static void process(String buildDir, String lib) {
        System.out.println(buildDir)
        println(lib);
        ClassPool classes = ClassPool.getDefault()
        classes.appendClassPath(buildDir)
        classes.appendClassPath(lib)

        // 將須要關聯的類的構造方法中插入引用代碼
        CtClass c = classes.getCtClass("cn.jiajixin.nuwasample.Hello.Hello")
        if (c.isFrozen()) {
            c.defrost()
        }
        println("====添加構造方法====")
        def constructor = c.getConstructors()[0];
        constructor.insertBefore("System.out.println(com.cuieney.hookdex.AntilazyLoad.class);")
        c.writeFile(buildDir)


    }複製代碼

若是咱們想不被打上標記就只能這樣了,就是經過這個方法,讓如今這個類Hello在當dex裏面引用其餘的dex文件裏面的AntilazyLoad.class簡單的說就是對其餘dex文件有依賴就不會被打上標記。

那麼咱們這個代碼段改在哪裏運行呢。好問題!!!這也是重點。不知道老鐵們還記得上面的代碼嗎。apk的生成過程的生命週期,就是在build的是那幾個步驟。咱們須要在.class文件編程成.dex文件前 進行代碼植入。這樣是否是很完美呢。那咱們從哪裏下手呢。固然是咱們的build.gradle文件下手,咱們編譯項目的時候每次是否是都是在這裏進行操做的

這裏要用到一個新的姿式哦(不對是知識哈哈哈)groovy這個語言plugin插件語言。咱們原生的android studio 是對groovy支持的。在咱們的項目中建立一個buildsrc項目,必定要這個名字。而後咱們在項目中建立一個類patch.groovy 目錄結構以下不用的都刪了。

Screen Shot 2017-06-27 at 11.23.21 AM.png
Screen Shot 2017-06-27 at 11.23.21 AM.png

而後咱們在咱們的app的build.gradle裏面作一下操做

task ('processWithJavassist') << {
    String classPath = file('build/intermediates/classes/debug')//項目編譯class所在目錄
    com.cuieney.groovy.PatchClass.process(classPath, project(':hookdex').buildDir
            .absolutePath + '/intermediates/classes/debug')//第二個參數是hackdex的class所在目錄

}複製代碼

這個是執行代碼植入操做project(':hookdex')這個使咱們植入的類的module
可是我麼這個task你得保證在.class 到 .dex文件之間操做,咱們怎麼保證呢?接下來見證奇蹟的時候到了在build.g裏在添加以下代碼

applicationVariants.all { variant ->
        variant.dex.dependsOn << processWithJavassist //在執行dx命令以前將代碼打入到class中
    }複製代碼

這樣就完成了咱們的代碼植入操做 哈哈哈哈 牛逼不牛逼不

but 你在植入代碼以前必定要把咱們的植入的類的dex提早插入到Element[]裏面否則 會報找不到這個類的。 而後在只要真正的patch.dex 咱們的補丁。

補丁製做

  • 將class文件打入一個jar包中 jar cvf path.jar xxxx.java
  • 將jar包轉換成dex的jar包 dx --dex --output=path_dex.jar path.jar
  • 用adb將你的path_dex.jar push到你的dexpath中。每次app啓動吧這個補丁打入就好

坑2 以上代碼植入在高版本的gradle不行

包如下錯誤Gradle1.40 裏TransformAPI沒法打包的狀況,只兼容Gradle1.3-Gradle2.1.0版本
哈哈哈我也沒則,目前RocooFix這個項目博主經過一種新的方式進行了代碼植入(以前咱們經過植入代碼來完成避免打上標誌,他則是反其道而行,PatchClassLoader每次加載apk裏面的dex時,把標誌去了這樣也能夠防止出現以前那種crash 只能說牛逼牛逼,裏面代碼還在研究...)有興趣的同窗能夠看一下。

ending

說了這麼多,其實網上這種帖子不少,本身只是想系統的整理一下,其實在這個過程本身學到了不少,不管是源碼仍是各方面的擴展知識吧,對本身都有很大的提高,不論老鐵們看沒看玩,但願此次分享給你們帶來的知識的提高。 stay hungry stay foolish

下一篇文章
手把手教你寫熱修復(HOTFIX)

相關文章
相關標籤/搜索