編輯:關於Android編程
針對app線上修復技術,目前有好幾種解決方案,開源界往往一個方案會有好幾種實現。重復的實現會有造輪子之嫌,但分析解決方案在技術上的探索和衍變,這輪子還是值得去推動的
Hot Fix技術,簡單來說就是針對線上已發布app出現了bug,在不推送新版本的情況下通過發布修復補丁進行修復。通常是剛上線的app,需要快速線上修復bug,類似的技術就叫做熱修復或熱補丁。
現在有兩種熱修復解決方案:
1、JNI Hook方式解決方案 : c++動態修改方法指針,安裝補丁後立刻生效;
2、classloader類加載極致:程序啟動過程加載,安裝補丁之後需要重新啟動才能生效;
目前,從技術解決方案上來說,有以下幾種思路:
*Dexposed:JNI Hook
來自阿裡手淘團隊,白衣(花名)基於Xposed實現了Dexposed,在此基礎上手淘團隊推出了HotPatch二方庫。
*AndFix:JNI Hook
出自阿裡支付寶技術團隊,同樣是對方法的hook,但未基於Dexposed去實現,避免了在art上運行時存在兼容性問題。
*基於ClassLoader
QQ空間終端開發團隊提供了技術思路,目前基於此實現的熱門的開源項目有Nuwa,HotFix,DroidFix,這三種方案的原理卻徊然不同,各有優缺點。
熱修復 == 動態替換 == 動態加載
得出上面的等式,是因為熱修復一般來說就是增發patch文件,避免用戶調用錯誤代碼,並不是直接修改了原來的代碼。這相當於是對問題文件做了動態替換,而要實現動態替換就是避免默認的加載,改變成動態地加載替換文件。
動態加載的基礎是ClassLoader,Java程序在運行時加載對應的類是通過ClassLoader來實現的, Java 類可以被動態加載到 Java 虛擬機中並執行。所以ClassLoader所做的工作實質就是把類文件從硬盤讀取到內存中。
*類加載器的樹狀結構:在JVM中,所有類加載器實例按樹狀結構組織,根結點為引導類加載器。除根結點外的所有類加載器都有一個非空的父類加載器,從而構成樹狀結構;
*雙親委托(代理)模型:當類加載器收到加載類或資源的請求時,通常都是先委托給父類加載器加載,也就是說只有當父類加載器找不到指定類或資源時,自身才會執行實際的類加載過程;
代理模式是為了保證 Java 核心庫的類型安全。通過代理模式,對於 Java 核心庫的類的加載工作由bootClassLoader來統一完成,保證了 Java 應用所使用的都是同一個版本的 Java 核心庫的類,是互相兼容的。
類的判等:即使類完全相同(名稱相同、字節碼相同),不同類加載器實例加載的類對象也是不相等的;
這條規則是Java類加載機制中非常核心的規則,它保證了類加載機制實現“類隔離”、“保護JDK中的基礎類”等目標。
Android的Dalvik/ART虛擬機如同標准JAVA的JVM虛擬機一樣,在運行程序時首先需要將對應的類加載到內存中。因此可以利用這一點,在程序運行時手動加載Class,從而達到代碼中動態加載可執行文件的目的。
在Android系統啟動的時候會創建一個Boot類型的ClassLoader實例,用於加載一些系統Framework層級需要的類。由於Android應用裡也需要用到一些系統的類,所以APP啟動的時候也會把這個Boot類型的ClassLoader傳進來。
此外,APP也有自己的類,這些類保存在APK的dex文件裡面,所以APP啟動的時候,也會創建一個自己的ClassLoader實例,用於加載自己dex文件中的類。
下面實際驗證看看:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ClassLoader classLoader = getClassLoader();
Log.i("ClassLoader" , "classLoader " + classLoader.toString());
while (classLoader.getParent() != null) {
classLoader = classLoader.getParent();
if (classLoader != null) {
Log.i("ClassLoader", "classLoaderParent " + classLoader.toString());
}
}
}
輸出結果為:
I/ClassLoader: classLoader dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.sunteng.classloader-1/base.apk"],nativeLibraryDirectories=[/vendor/lib, /system/lib]]]
I/ClassLoader: classLoaderParent java.lang.BootClassLoader@2d0a3af7
可以看見有2個Classloader實例,一個是BootClassLoader(系統啟動的時候創建的),另一個是PathClassLoader(應用啟動時創建的,用於加載當前已安裝app裡面的類)。
Android經常使用的是PathClassLoader和DexClassLoader
官方注釋:一個簡單的ClassLoader的實現,工作在本地文件系統中的文件和目錄的列表上,但不嘗試從網絡加載類。 Android使用這個類為它的系統類加載器和應用類加載器。
官方注釋:一個ClassLoader的實現,從.jar和.apk文件內部加載classes.dex。這可以用於執行非安裝程序作為已安裝應用程序的一部分的代碼。
#BaseDexClassLoader
@Override
protected Class findClass(String name) throws ClassNotFoundException {
List suppressedExceptions = new ArrayList();
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}
看源碼可知,BaseDexClassLoader將findClass方法委托給了pathList對象的findClass方法,pathList對象是在BaseDexClassLoader的構造函數中new出來的,它的類型是DexPathList。看下DexPathList.findClass源碼是如何做的:
#DexPathList
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;
}
#DexFile
public Class loadClassBinaryName(String name, ClassLoader loader) {
return defineClass(name, loader, mCookie);
}
private native static Class defineClass(String name, ClassLoader loader, int cookie);
直接就是遍歷dexElements列表,然後通過調用element.dexFile對象上的loadClassBinaryName方法來加載類,如果返回值不是null,就表示加載類成功,會將這個Class對象返回。而且dexElements對象是在DexPathList類的構造函數中完成初始化的。
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions);
makeDexElements所做的事情就是遍歷我們傳遞來的dexPath,然後一次加載每個dex文件。可以看出,BaseDexClassLoader中有個pathList對象,pathList中包含一個DexFile的集合dexElements,而對於類加載,就是遍歷這個集合,通過DexFile去尋找。 這樣的話,我們可以在這個dexElements中去做一些事情,比如在這個數組的第一個元素放置我們的patch.jar,裡面包含修復過的類。當遍歷findClass的時候,修復的類就會被查找到,從而替代有bug的類。
標准JVM中,ClassLoader是用defineClass加載類的,而Android中defineClass被棄用了,改用了loadClass方法,而且加載類的過程也挪到了DexFile中,在DexFile中加載類的具體方法也叫defineClass一個ClassLoader可以包含多個dex文件,每個dex文件是一個Element,多個dex文件排列成一個有序的數組dexElements,當找類的時候,會按順序遍歷dex文件,然後從當前遍歷的dex文件中找類,如果找類則返回,如果找不到從下一個dex文件繼續查找
ClassLoader特性
使用ClassLoader的一個特點就是,當ClassLoader在成功加載某個類之後,會把得到類的實例緩存起來。下次再請求加載該類的時候,ClassLoader會直接使用緩存的類的實例,而不會嘗試再次加載。也就是說,如果程序不重新啟動,加載過一次的類就無法重新加載。除了使用ClassLoader外,還可以使用jni hook的方式修改程序的執行代碼。後者做的已經是Native層級的工作了,直接修改應用運行時的內存地址,所以使用jni hook的方式時,不用重新應用就能生效。 而阿裡的dexposed和AndFix采用了jni hook方案 **Android程序比起一般Java程序在使用動態加載時麻煩在哪裡** 使用ClassLoader動態加載一個外部的類是非常容易的事情,所以很容易就能實現動態加載新的可執行代碼的功能,但是比起一般的Java程序,在Android程序中使用動態加載主要有兩個麻煩的問題: * Android中許多組件類(如Activity、Service等)是需要在Manifest文件裡面注冊後才能工作的(系統會檢查該組件有沒有注冊),所以即使動態加載了一個新的組件類進來,沒有注冊的話還是無法工作; * Res資源是Android開發中經常用到的,而Android是把這些資源用對應的R.id注冊好,運行時通過這些ID從Resource實例中獲取對應的資源。如果是運行時動態加載進來的新類,那類裡面用到R.id的地方將會拋出找不到資源或者用錯資源的異常,因為新類的資源ID根本和現有的Resource實例中保存的資源ID對不上; 說到底,一個Android程序和標准的Java程序最大的區別就在於他們的上下文環境(Context)不同。如果使用ClassLoader來動態升級APP或者動態修復BUG,都需要重新啟動APP才能生效。
Android中context可以給程序提供組件需要用到的功能,也可以提供一些主題、Res等資源,而現在的各種Android動態加載框架中,核心要解決的東西也正是如何給外部的新類提供上下文環境的問題。
就一個理念:只有適合當前情況的才是最好的。
前面關於Android中ClassLoader的介紹,Android使用PathClassLoader作為其類加載器,DexClassLoader可以從.jar和.apk類型的文件內部加載classes.dex文件。
如果大家對於插件化有所了解,其實Android應用的插件化,就可以利用DexClassLoader來動態加載非安裝應用的類來實現,當然也就可以做到只有單用戶點擊相應插件模塊,才會從網絡獲取相應插件文件,再通過DexClassLoader實現類加載。
而熱修復可以利用BaseDexClassLoader中的pathList對象,pathList中包含一個DexFile的集合dexElements,我們可以在這個dexElements中去做一些事情,比如在這個數組的第一個元素放置我們的patch.jar,裡面包含修復過的類。
這樣的話,當遍歷findClass的時候,我們修復的類就會被查找到,從而替代有bug的類。不過這樣處理還存在一個CLASS_ISPREVERIFIED的問題。
上面分析了Android中的類的加載的流程,可以看出:
* DexPathList對象中的dexElements列表是類加載的一個核心,一個類如果能被成功加載,那麼它的dex一定會出現在dexElements所對應的dex文件中。
* exElements中出現的順序也很重要,在dexElements前面出現的dex會被優先加載,一旦Class被加載成功,就會立即返回。
* 我們的如果想做hot fix,一定要保證我們的pacth dex文件出現在dexElements列表的前面。
要實現熱修復,就需要我們在運行時去更改PathClassLoader.pathList.dexElements,由於這些屬性都是private的,因此需要通過反射來修改。
另外,構造我們自己的dex文件所對應的dexElements數組的時候,我們也可以采取一個比較取巧的方式:
* 通過構造一個DexClassLoader對象來加載我們的dex文件
* 調用一次dexClassLoader.loadClass(dummyClassName)方法
* 這樣dexClassLoader.pathList.dexElements中就會包含我們的dex
通過把dexClassLoader.pathList.dexElements插入到系統默認的classLoader.pathList.dexElements列表前面,就可以讓系統優先加載我們的dex中的類,從而可以實現熱修復了。
通過分析三者的差異化對比,以及思考到底什麼才是合適的,通過hook方法的方式實現起來確實最直接,但是問題卻也很明顯,首先成功覆蓋率和穩定性是個問題,而且操作起來復雜性比較高。
而通過classloader考慮的是從系統動態加載的特性入手,所以理所當然以局限於系統的特性,比如由於對於已經加載的類,類加載器不會再調用loadClass方法,所以想要修復要等到下次啟動程序才行。
Android項目中,動態加載技術按照加載的可執行文件的不同大致可以分為兩種:
1.動態加載so庫;
2.動態加載dex/jar/apk文件(通常都是這種)
所以理解起來就是:
1.動態調用外部的Dex文件則是完全沒有問題的。
2.在APK文件中往往有一個或者多個Dex文件,我們寫的每一句代碼都會被編譯到這些文件裡面。
3.Android應用運行的時候就是通過執行這些Dex文件完成應用的功能的。
4.雖然一個APK一旦構建出來,我們是無法更換裡面的Dex文件,但是我們可以通過加載外部的Dex文件來實現。
外部文件可以放在外部存儲,或者從網絡下載。
因此最極端的情況就是,直接把APK自身帶有的Dex文件當做空殼,只是作為一個程序的入口,所有的功能都通過從服務器下載最新的Dex文件完成。
當然,一般來說只要利用Android動態加載技術,通過動態加載新的dex的方式,完成對有bug類的“替換”,來達到避免調用存在bug的代碼,這也就是所謂的Hot Fix。
總體的思路就是這樣,至於具體的實現,就有很多環節需要細化的,因為Android本身也有很多自身的特性。
接下來就是考慮實際編碼實現了。
在我們進行android開發的時候雖然官方提供了形形色色的控件,但是有的時候根據不同業務需求我們找不到官方的控件支持,那麼這個時候就需要我們自己去定義控件來適應不同的需求
最近閒著沒事做了一個Android小程序,具體如下:效果圖:原始界面點擊按鈕運行 運行後界面實現代碼:public class Mai
由於本人所作的項目需要用到這種列表式的收縮與展開,因此,就好好研究了有關這方面的一些知識,當然,也借鑒了網上一些成功的案列。下面就是我模擬測試的一個展示界面。 實現上面的
本篇文章致那些從零開始學 Android 的或者正要學習還沒有勇氣出發的人, 希望通過我的經歷能夠讓你在學習的道路中堅持下來。我的第一份工作畢業之際通過學校的校招找到了一