天天看點

動态更新檔更新

最新github上開源了很多熱更新檔動态修複架構,大緻有:

<a target="_blank" href="https://github.com/dodola/hotfix">https://github.com/dodola/hotfix</a>

<a target="_blank" href="https://github.com/jasonross/nuwa">https://github.com/jasonross/nuwa</a>

<a target="_blank" href="https://github.com/bunnyblue/droidfix">https://github.com/bunnyblue/droidfix</a>

之前項目也使用過動态更新,用的技術也就是dex分包,分包替換政策。

有興趣的直接看這篇原理文章,加上上面架構的源碼基本就可以看懂了。當然了,本篇博文也會做個上述架構源碼的解析,以及在整個實作過程中用到的技術的解析。

對于熱修複的原理,如果你看了上面的兩篇文章,相信你已經大概明白了。重點需要知道的就是,android的classloader體系,android中加載類一般使用的是<code>pathclassloader</code>和<code>dexclassloader</code>,首先看下這兩個類的差別:

對于<code>pathclassloader</code>,從文檔上的注釋來看:

provides a simple {@link classloader} implementation that operates  on a list of files and directories in the local file system, but  does not attempt to load classes from the network. android uses  this class for its system class loader and for its application  class loader(s).

可以看出,android是使用這個類作為其系統類和應用類的加載器。并且對于這個類呢,隻能去加載已經安裝到android系統中的apk檔案。

對于<code>dexclassloader</code>,依然看下注釋:

a class loader that loads classes from {@code .jar} and  {@code .apk} files containing a {@code classes.dex} entry.  this can be used to execute code not installed as part of an application.

可以看出,該類呢,可以用來從.jar和.apk類型的檔案内部加載classes.dex檔案。可以用來執行非安裝的程式代碼。

ok,如果大家對于插件化有所了解,肯定對這個類不陌生,插件化一般就是提供一個apk(插件)檔案,然後在程式中load該apk,那麼如何加載apk中的類呢?其實就是通過這個dexclassloader,具體的代碼我們後面有描述。

ok,到這裡,大家隻需要明白,android使用pathclassloader作為其類加載器,dexclassloader可以從.jar和.apk類型的檔案内部加載classes.dex檔案就好了。

上面我們已經說了,android使用pathclassloader作為其類加載器,那麼熱修複的原理具體是?

ok,對于加載類,無非是給個classname,然後去findclass,我們看下源碼就明白了。 

<code>pathclassloader</code>和<code>dexclassloader</code>都繼承自<code>basedexclassloader</code>。在basedexclassloader中有如下源碼:

可以看出呢,basedexclassloader中有個pathlist對象,pathlist中包含一個dexfile的集合dexelements,而對于類加載呢,就是周遊這個集合,通過dexfile去尋找。

ok,通俗點說:

一個classloader可以包含多個dex檔案,每個dex檔案是一個element,多個dex檔案排列成一個有序的數組dexelements,當找類的時候,會按順序周遊dex檔案,然後從目前周遊的dex檔案中找類,如果找類則傳回,如果找不到從下一個dex檔案繼續查找。(來自:安卓app熱更新檔動态修複技術介紹)

那麼這樣的話,我們可以在這個dexelements中去做一些事情,比如,在這個數組的第一個元素放置我們的patch.jar,裡面包含修複過的類,這樣的話,當周遊findclass的時候,我們修複的類就會被查找到,進而替代有bug的類。

ok,對于<code>class_ispreverified</code>,還是帶大家理一下:

根據上面的文章,在虛拟機啟動的時候,當verify選項被打開的時候,如果static方法、private方法、構造函數等,其中的直接引用(第一層關系)到的類都在同一個dex檔案中,那麼該類就會被打上<code>class_ispreverified</code>标志。

那麼,我們要做的就是,阻止該類打上<code>class_ispreverified</code>的标志。

注意下,是阻止引用者的類,也就是說,假設你的app裡面有個類叫做<code>loadbugclass</code>,再其内部引用了<code>bugclass</code>。釋出過程中發現<code>bugclass</code>有編寫錯誤,那麼想要釋出一個新的<code>bugclass</code>類,那麼你就要阻止<code>loadbugclass</code>這個類打上<code>class_ispreverified</code>的标志。

也就是說,你在生成apk之前,就需要阻止相關類打上<code>class_ispreverified</code>的标志了。對于如何阻止,上面的文章說的很清楚,讓<code>loadbugclass</code>在構造方法中,去引用别的dex檔案,比如:hack.dex中的某個類即可。

ok,總結下:

其實就是兩件事:1、動态改變basedexclassloader對象間接引用的dexelements;2、在app打包的時候,阻止相關類去打上<code>class_ispreverified</code>标志。

如果你沒有看明白,沒事,多看幾遍,下面也會通過代碼來說明。

那麼,這裡拿具體的類來說:

大緻的流程是:在dx工具執行之前,将<code>loadbugclass.class</code>檔案呢,進行修改,再其構造中添加<code>system.out.println(dodola.hackdex.antilazyload.class)</code>,然後繼續打包的流程。注意:<code>antilazyload.class</code>這個類是獨立在hack.dex中。

ok,這裡大家可能會有2個疑問:

如何去修改一個類的class檔案

如何在dx之前去進行疑問1的操作

這裡我們使用javassist來操作,很簡單:

ok,首先我們建立幾個類:

注意下,這裡的package,我們要做的是,上述類正常編譯以後産生class檔案。比如:loadbugclass.class,我們在loadbugclass.class的構造中去添加一行:

下面看下操作類:

ok,點選run即可了,注意項目中導入javassist-*.jar的包。

首先拿到classpool對象,然後添加classpath,如果你有多個classpath可以多次調用。然後從classpath中找到loadbugclass,拿到其構造方法,在其最後插入一行代碼。ok,代碼很好懂。

ok,我們反編譯看下我們生成的class檔案:

動态更新檔更新

ok,關于javassist,如果有興趣的話,大家可以參考幾篇文章學習下:

<a target="_blank" href="http://www.ibm.com/developerworks/cn/java/j-dyn0916/">http://www.ibm.com/developerworks/cn/java/j-dyn0916/</a>

<a target="_blank" href="http://zhxing.iteye.com/blog/1703305">http://zhxing.iteye.com/blog/1703305</a>

将其源碼導入之後,打開app/build.gradle

你會發現,在執行dx之前,會先執行processwithjavassist這個任務。這個任務的作用呢,就和我們上面的代碼一緻了。而且源碼也給出了,大家自己看下。

ok,到這呢,你就可以點選run了。ok,有興趣的話,你可以反編譯去看看<code>dodola.hotfix.loadbugclass</code>這個類的構造方法中是否已經添加了改行代碼。

ok,到此我們已經能夠正常的安裝apk并且運作了。但是目前還未涉及到打更新檔的相關代碼。

ok,這裡就比較簡單了,動态改變一個對象的某個引用我們反射就可以完成了。

不過這裡需要注意的是,還記得我們之前說的,尋找class是周遊dexelements;然後我們的<code>antilazyload.class</code>實際上并不包含在apk的classes.dex中,并且根據上面描述的需要,我們需要将<code>antilazyload.class</code>這個類打成獨立的hack_dex.jar,注意不是普通的jar,必須經過dx工具進行轉化。

具體做法:

如果,你沒有辦法把那一個class檔案搞成jar,去百度一下…

ok,現在有了hack_dex.jar,這個是幹嘛的呢?

應該還記得,我們的app中部門類引用了<code>antilazyload.class</code>,那麼我們必須在應用啟動的時候,降這個hack_dex.jar插入到dexelements,否則肯定會出事故的。

那麼,application的oncreate方法裡面就很适合做這件事情,我們把hack_dex.jar放到assets目錄。

下面看hotfix的源碼:

ok,在app的私有目錄建立一個檔案,然後調用utils.preparedex将assets中的hackdex_dex.jar寫入該檔案。 

接下來hotfix.patch就是去反射去修改dexelements了。我們深入看下源碼:

ok,其實就是檔案的一個讀寫,将assets目錄的檔案,寫到app的私有目錄中的檔案。

下面主要看patch方法

這裡很據系統中classloader的類型做了下判斷,原理都是反射,我們看其中一個分支<code>hasdexclassloader()</code>;

首先查找類<code>dalvik.system.basedexclassloader</code>,如果找到則進入if體。

在injectaboveequalapilevel14中,根據context拿到pathclassloader,然後通過getpathlist(pathclassloader),拿到pathclassloader中的pathlist對象,在調用getdexelements通過pathlist取到dexelements對象。

ok,那麼我們的hack_dex.jar如何轉化為dexelements對象呢?

通過源碼可以看出,首先初始化了一個dexclassloader對象,前面我們說過dexclassloader的父類也是basedexclassloader,那麼我們可以通過和pathclassloader同樣的方式取得dexelements。

ok,到這裡,我們取得了,系統中pathclassloader對象的間接引用dexelements,以及我們的hack_dex.jar中的dexelements,接下來就是合并這兩個數組了。

可以看到上面的代碼使用的是combinearray方法。

合并完成後,将新的數組通過反射的方式設定給pathlist.

接下來看一下反射的細節:

其實都是取成員變量的過程,應該很容易懂~~

ok,這裡的兩個數組合并,隻需要注意一件事,将hack_dex.jar裡面的dexelements放到新數組前面即可。

到此,我們就完成了在應用啟動的時候,動态的将hack_dex.jar中包含的dexfile注入到classloader的dexelements中。這樣就不會查找不到antilazyload這個類了。

ok,那麼到此呢,還是沒有看到我們如何打更新檔,哈,其實呢,已經說過了,打更新檔的過程和我們注入hack_dex.jar是一緻的。

你現在運作hotfix的app項目,點選menu裡面的測試:

會彈出:<code>調用測試方法:bug class</code>

接下來就看如何完成熱修複。

ok,那麼我們假設bugclass這個類有錯誤,需要修複:

可以看到字元串變化了:bug class -&gt; fixed class .

然後,編譯,将這個類的class-&gt;jar-&gt;dex。步驟和上面是一緻的。

拿到path_dex.jar檔案。

正常情況下,這個玩意應該是下載下傳得到的,當然我們介紹原理,你可以直接将其放置到sdcard上。

然後在application的oncreate中進行讀取,我們這裡為了友善也放置到assets目錄,然後在application的oncreate中添加代碼:

其實就是添加了後面的3行,這裡需要說明一下,第一行依舊是複制到私有目錄,如果你是sdcard上,那麼操作基本是一緻的,這裡就别問:如果在sdcard或者網絡上怎麼處理~

ok,那麼再次運作我們的app。

動态更新檔更新

ok,最後說一下,說項目中有一個打更新檔的按鈕,在menu下,那麼你也可以不在application裡面添加我們最後的3行。

你運作app後,先點選<code>打更新檔</code>,然後點選<code>測試</code>也可以發現成功修複了。

如果先點選<code>測試</code>,再點選<code>打更新檔</code>,再<code>測試</code>是不會變化的,因為類一旦加載以後,不會重新再去重新加載了。

原文連結:http://blog.csdn.net/lmj623565791/article/details/49883661

繼續閱讀