昨日,樂視超級(jí)汽車戰(zhàn)略合作伙伴法拉第未來(Faraday Future,簡稱FF)在美國拉斯維加斯發(fā)布旗下首款量產(chǎn)車FF 91。根據(jù)現(xiàn)場介紹,F(xiàn)F 91續(xù)航超800公里,0-60 英里/小時(shí)加速時(shí)間僅為2.39 秒,加速性能超越 Ferrari 488 GTB、TESLA Model S P100D 在內(nèi)的頂級(jí)跑車和同級(jí)別豪華車。此外,發(fā)布會(huì)演示環(huán)節(jié)還通過樂視超級(jí)手機(jī),無需人工復(fù)雜操作即可作為車輛鑰匙及車主識(shí)別功能,為用戶提供個(gè)性化需求及強(qiáng)大的交互體驗(yàn)。 本篇來自 SAHADEV 的投稿,詳細(xì)介紹了如何實(shí)現(xiàn)熱修復(fù),作者原文中有很多熱修復(fù)相關(guān)博文推薦,由于篇幅原因,我這里沒有貼出,感興趣的朋友可以點(diǎn)擊最后 閱讀原文 查看。 SAHADEV 的博客地址:
熱修復(fù)技術(shù)自從QQ空間團(tuán)隊(duì)搞出來之后便漸漸趨于成熟。這里我們主要介紹如何一步步手動(dòng)實(shí)現(xiàn)基本的熱修復(fù)功能,無需使用第三方框架。 本文示例所用到的任何資源都已開源,項(xiàng)目中包含工程中所用到代碼、示例圖片、說明文檔。項(xiàng)目地址為:
我們在這部分主要做的流程有: 1. 編寫基本的Java文件并編譯為 .class 文件。 2. 將 .class 文件轉(zhuǎn)為 .dex 文件。 3. 將轉(zhuǎn)好的 dex文件 放入創(chuàng)建好的Android工程內(nèi)并在啟動(dòng)時(shí)將其寫入本地。 4. 加載解壓后的 .dex 文件中的類,并調(diào)用其方法進(jìn)行測試。
編寫基本的Java文件并編譯為.class文件 首先我們在一個(gè)工程目錄下開始創(chuàng)建并編寫我們的Java文件,你可能會(huì)選擇各種IDE來做這件事,但我在這里勸你不要這么做,因?yàn)橛锌釉诘饶?。等把基本流程搞清楚可以再選擇更進(jìn)階的方法。這里我們可以選擇文本編輯器比如EditPlus來對(duì)Java文件進(jìn)行編輯。 新建一個(gè)Java文件,并命名為:ClassStudent.java,并在java文件內(nèi)鍵入以下代碼: public class ClassStudent { private String name; public ClassStudent() {} public void setName(String name) { this.name = name; } public String getName(){ return this.name + '.Mr'; }}
在文件創(chuàng)建好之后,對(duì)Java文件進(jìn)行編譯: 將.class文件轉(zhuǎn)為.dex文件 好,現(xiàn)在我們使用class文件生成對(duì)應(yīng)的dex文件。生成dex文件所需要的工具為dx,dx工具位于sdk的 build-tools 文件夾內(nèi),如下圖所示:
dx工具的基本用法是: dx --dex [--output=file>] [file>.class | file>.{zip,jar,apk} | directory>]
我們使用以下命令生成dex文件: dx --dex --output=user.dex ClassStudent.class 這里我為了防止出錯(cuò),提前在當(dāng)前目錄下新建好了 user.dex 文件。上述命令依賴編譯.class文件的JDK版本,如果使用的是JDK8編譯的class會(huì)提示以下問題: PARSE ERROR:unsupported class file version 52.0...while parsing ClassStudent.class1 error; aborting 這里的52.0意味著class文件不被支持,需要使用JDK8以下的版本進(jìn)行編譯,但是dx所需的環(huán)境還是需要為JDK8的,這里我編譯class文件使用的是JDK7,請注意。 上面我們提到了為什么先不要在ClassStudent中使用包名,因?yàn)樵趫?zhí)行dx的時(shí)候會(huì)報(bào)以下異常,這是因?yàn)橐韵碌诙?xiàng)條件沒有通過,該代碼位于 com.Android.dx.cf.direct.DirectClassFile 文件內(nèi): 運(yùn)行截圖如下所示: 好了,到此為止我們的目錄應(yīng)該如下: 寫入dex到本地磁盤 接下來將生成好的user.dex文件放入Android工程的res\raw文件夾下: 在系統(tǒng)啟動(dòng)時(shí)將其寫入到磁盤,這里不再貼出具體的寫入代碼,項(xiàng)目的MainActivity中包含了此部分代碼。 加載dex中的類并測試 在寫入完畢之后使用DexClassLoader對(duì)其進(jìn)行加載。DexClassLoader 的構(gòu)造方法需要4個(gè)參數(shù),這里對(duì)這4個(gè)參數(shù)進(jìn)行簡要說明:
在解釋完畢 DexClassLoader 的構(gòu)造參數(shù)之后,我們開始對(duì)剛剛的dex文件進(jìn)行加載: 接來下開始load我們剛剛寫入在dex文件中的 ClassStudent 類: Class?> aClass = dexClassLoader.loadClass('ClassStudent'); 然后我們對(duì)其進(jìn)行初始化,并調(diào)用相關(guān)的 get/set 方法對(duì)其進(jìn)行驗(yàn)證,在這里我傳給 ClassStudent 對(duì)象一個(gè)字符串,然后調(diào)用它的get方法獲取在方法內(nèi)合并后的字符串: Object instance = aClass.newInstance(); Method method = aClass.getMethod('setName', String.class);method.invoke(instance, 'Sahadev'); Method getNameMethod = aClass.getMethod('getName'); Object invoke = getNameMethod.invoke(instance); 最后我們實(shí)現(xiàn)的代碼可能是這樣的: 最后附上我們的運(yùn)行截圖: 本節(jié)內(nèi)容是為了給下節(jié)內(nèi)容做知識(shí)鋪墊,所以如果要需要了解熱修復(fù)技術(shù),本節(jié)內(nèi)容的知識(shí)點(diǎn)必不可少。 一個(gè)類在被加載到內(nèi)存之前要經(jīng)過加載、驗(yàn)證、準(zhǔn)備等過程。經(jīng)過這些過程之后,虛擬機(jī)才會(huì)從方法區(qū)將代表類的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)轉(zhuǎn)換為內(nèi)存中的Class。 我們這節(jié)內(nèi)容的重點(diǎn)在于一個(gè)類是如何被加載的,所以我們從類的加載入口開始。 類的加載是由虛擬機(jī)觸發(fā)的,類的加載入口位于 ClassLoader 的 loadClassInternal() 方法: 這段方法還有段注釋說明:這個(gè)方法由虛擬機(jī)調(diào)用用來加載一個(gè)類。我們看到這個(gè)類的內(nèi)部最后調(diào)用了 loadClass() 方法。那我們進(jìn)入 loadClass() 方法看看: public Class?> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false);} loadClass() 方法方法內(nèi)部調(diào)用了 loadClass() 的重載方法: loadClass() 方法大概做了以下工作:
好,ClassLoader 的 findClass() 方法是個(gè)空方法,所以這個(gè)過程一般是由子加載器實(shí)現(xiàn)的。Java的加載器這么設(shè)計(jì)是有一定的淵源的,感興趣的讀者可以自行查找書籍了解。 protected Class?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name);} 在Android中,ClassLoader 的直接子類是 BaseDexClassLoader,我們看一下 BaseDexClassLoader 的 findClass() 實(shí)現(xiàn):
看到這里我們可以知道,Android中類的查找是通過這個(gè) pathList 進(jìn)行查找的,而 pathList 又是個(gè)什么鬼呢? 在 BaseDexClassLoader 中聲明了以下變量: /** structured lists of path elements */ private final DexPathList pathList; 所以我們可以看看 DexPathList 的 findClass() 方法做了什么: 這里通過遍歷 dexElements 中的 Element對(duì)象 進(jìn)行查找,最終走的是 DexFile 的 loadClassBinaryName() 方法: 到此為止,我們就將類的加載過程梳理完了。 本節(jié)主要依賴文章:
中的未實(shí)現(xiàn)代碼實(shí)現(xiàn),實(shí)現(xiàn)思路也源自該文章,在閱讀本文之前可以先行了解。 這一節(jié)我們主要實(shí)現(xiàn)的流程有:
創(chuàng)建工程內(nèi)的ClassStudent 上面演示了如何加載外部的Class,為了起到熱修復(fù)效果,那么我們需要在工程內(nèi)有一個(gè)被替換的類,被替換的ClassStudent類內(nèi)容如下: 外部的ClassStudent類的內(nèi)容如下: 這兩個(gè)類除了在 getName() 方法返回之處有差別之外,其它地方一模一樣,不過這足可以讓我們說明情況。 我們這里要實(shí)現(xiàn)的目的: 我們默認(rèn)調(diào)用getName()方法返回的是“xxxx.Miss”,如果熱修復(fù)成功,那么再使用該方法的話,返回的則會(huì)是“xxxx.Mr”。 對(duì)含有包名的類再次編譯 因?yàn)榈谝还?jié)中專門聲明了不可以對(duì)類聲明包名,但是這樣在Android工程中無法引用到該類,所以把不能聲明包名的問題解決了一下。 不能聲明包名的主要原因是在編譯Java文件時(shí),沒有正確的使用命令。對(duì)含有包名的Java文件應(yīng)當(dāng)使用以下命令: javac -d ./ ClassStudent.java 經(jīng)過上面命令編譯后的.class文件便可以順利通過dx工具的轉(zhuǎn)換。 我們還是按照第一節(jié)的步驟將轉(zhuǎn)換后的user.dex文件放入工程中并寫入本地磁盤,以便稍后使用。 替換工程內(nèi)的類文件在開始之前還是再回顧一下實(shí)現(xiàn)思路(詳見上一大章節(jié)):類在使用之前必須要經(jīng)過加載器的加載才能夠使用,在加載類時(shí)會(huì)調(diào)用自身的 findClass() 方法進(jìn)行查找。然而在Android中類的查找使用的是 BaseDexClassLoader,BaseDexClassLoader 對(duì) findClass() 方法進(jìn)行了重寫。 我們可以得知類的查找是通過遍歷 dexElements 來進(jìn)行查找的。所以為了實(shí)現(xiàn)替換效果,我們需要將 DexClassLoader 中的 Element對(duì)象 放到 dexElements數(shù)組 的 第0個(gè)位置,這樣才能在 BaseDexClassLoader 查找類時(shí)先找到 DexClassLoader 所用的 user.dex 中的類。 類的加載是從上而下加載的,所以就算是 DexClassLoader 加載了外部的類,但是在系統(tǒng)使用類的時(shí)候還是會(huì)先在 ClassLoader 中查找,如果找不到則會(huì)在 BaseDexClassLoader 中查找,如果再找不到,就會(huì)進(jìn)入 PathClassLoader 中查找,最后才會(huì)使用 DexClassLoader 進(jìn)行查找,所以按照這個(gè)流程外部類是無法正常發(fā)揮作用的。所以我們的目的就是在查找工程內(nèi)的類之前,先讓加載器去外部的dex中查找。 好了,再次梳理了思路之后,我們接下來對(duì)思路進(jìn)行實(shí)踐。 下面的方法是我們主要的注入方法: 這段代碼的核心在于將 DexClassLoader 中的 dexElements 與 PathClassLoader 中的 dexElements 進(jìn)行合并,然后將合并后的 dexElements 替換 原先的dexElements。最后我們在使用 ClassStudent類 的時(shí)候便可以直接使用外部的ClassStudent,而不會(huì)再加載默認(rèn)的ClassStudent類。 首先我們通過 classLoader 獲取各自的 pathList 對(duì)象: 在使用以上反射的時(shí)候要注意,pathList屬性 屬于 基類BaseDexClassLoader。所以如果直接獲取 DexClassLoader 或者 PathClassLoader的pathList 屬性的話,會(huì)得到null。 其次是獲取 pathList 對(duì)應(yīng)的 dexElements,這里要注意 dexElements 是個(gè) 數(shù)組對(duì)象: 接下來我們將兩個(gè)數(shù)組對(duì)象合并成為一個(gè): 上面這段代碼我們根據(jù)數(shù)組對(duì)象的類型創(chuàng)建了一個(gè)新的大小為2的新數(shù)組,并將兩個(gè)數(shù)組的第一個(gè)元素取出,將代表 外部dex 的 dexElement 放在了第0個(gè)位置。這樣便可以確保在查找類時(shí)優(yōu)先從外部的dex中查找。 最后將原先的dexElements覆蓋: 驗(yàn)證替換結(jié)果 好,我們做完以上的工作之后,寫一段代碼來進(jìn)行驗(yàn)證: 如果我們沒有替換成功的話,那么這里默認(rèn)使用的是內(nèi)部的ClassStudent,getName()返回的會(huì)是Lavon.Miss。 如果我們替換成功的話,那么這里默認(rèn)使用的是外部的ClassStudent,getName()返回的則會(huì)是Lavon.Mr。 我們實(shí)際運(yùn)行看下效果: 這說明我們已經(jīng)完成了基本的熱修復(fù)。 |
|