Android熱修復原理分析

什麼是熱修復

熱修復:讓應用能夠在無需重新安裝的情況實現更新,幫助應用快速建立動態修復能力。

​ 早期遇到Bug我們一般會緊急發佈了一個版本。然而這個Bug可能就是簡簡單單的一行代碼,爲了這一行代碼,進行全量或者增量更新迭代一個版本,未免有點大材小用了。而且新版本的普及需要時間,以Android用戶的升級習慣,即使是相對活躍的微信也需要10天以上的時間去覆蓋50%的用戶。使用熱修復技術,能做到1天覆蓋70%以上。這也是基於補丁體積較小,可以直接使用移動網絡下載更新。
熱修復開發流程
在這裏插入圖片描述

​ 目前Android業內,熱修復技術百花齊放,各大廠都推出了自己的熱修復方案,使用的技術方案也各有所異。

主要實現原理如下:
底層替換方法(Native層hook java代碼替換)
instant run 方法
基於類加載機制

各大公司推出的熱修復框架比較

在這裏插入圖片描述

AndFix(廢棄不更新了)

在native動態替換java層的方法,通過native層hook java層的代碼。
在這裏插入圖片描述

RoBus(美團)

對每個函數都在編譯打包階段自動的插入了一段代碼。類似於代理,將方法執行的代碼重定向到其他方法中。
在這裏插入圖片描述

//編寫的代碼
@Modify//改動代碼後手動註解用於補丁包生成
public long getIndex(){
	return 1000;
} 
//經過插樁後實際執行的代碼
public long getIndex(){
	if(changeQuickRedirect!=null){
		return 修復實現
	}
	return 1000
}

Tinker

Tinker通過計算對比指定的Base Apk中的dex與修改後的Apk中的dex的區別,補丁包中的內容即爲兩者差分的描述。運行時將Base Apk中的dex與補丁包進行合成,重啓後加載全新的合成後的dex文件。
在這裏插入圖片描述

QZone

QQ空間基於的是dex分包方案。把BUG方法修復以後,放到一個單獨的dex補丁文件,讓程序運行期間加載dex補丁,執行修復後的方法。如何做到這一點?
在Android中所有我們運行期間需要的類都是由ClassLoader(類加載器)進行加載。
因此讓ClassLoader加載全新的類替換掉出現Bug的類即可完成熱修復。

在這裏插入圖片描述
其中QZone超級補丁基於的是dex分包方案,而dex分包是基於Java的類加載機制ClassLoader
在這裏插入圖片描述

ClassLoader介紹

​ 任何一個 Java 程序都是由一個或多個 class 文件組成,在程序運行時,需要將 class 文件加載到虛擬機 中才可以使用,負責加載這些 class 文件的就是 Java 的類加載機制。ClassLoader 的作用簡單來說就是加載 class 文件,提供給程序運行時使用。每個 Class 對象的內部都有一個classLoader字段來標識自己是由哪個 ClassLoader 加載的。

class Class<T> {
  ...
  private transient ClassLoader classLoader;
  ...
}

​ ClassLoader是一個抽象類,而它的主要實現類主要有:

  • BootClassLoader

    用於加載Android Framework層class文件。

  • PathClassLoader

    用於Android應用程序類加載器。可以加載指定的dex,以及jar、zip、apk中的classes.dex

  • DexClassLoader

    用於加載指定的dex,以及jar、zip、apk中的classes.dex

很多博客裏說PathClassLoader只能加載已安裝的apk的dex,但是實際上PathClassLoaderDexClassLoader一樣都能夠加載sdcard中的dex。

Log.e(TAG, "Activity.class 由:" + Activity.class.getClassLoader() +" 加載");
Log.e(TAG, "MainActivity.class 由:" + MainActivity.class.getClassLoader() +" 加載");


//輸出:
Activity.class 由:java.lang.BootClassLoader@d3052a9 加載

MainActivity.class 由:dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.enjoy.enjoyfix-1/base.apk"],nativeLibraryDirectories=[/data/app/com.enjoy.enjoyfix-1/lib/x86, /system/lib, /vendor/lib]]] 加載

​ 它們之間的關係如下:

PathClassLoaderDexClassLoader的共同父類是BaseDexClassLoader

public class DexClassLoader extends BaseDexClassLoader {
	
    public DexClassLoader(String dexPath, String optimizedDirectory,
		String librarySearchPath, ClassLoader parent) {
		super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
	}
}

public class PathClassLoader extends BaseDexClassLoader {

    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }

	public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent){
		 super(dexPath, null, librarySearchPath, parent);
	}
}

​ 可以看到兩者唯一的區別在於:創建DexClassLoader需要傳遞一個optimizedDirectory參數,並且會將其創建爲File對象傳給super,而PathClassLoader則直接給到null。因此兩者都可以加載指定的dex,以及jar、zip、apk中的classes.dex

PathClassLoader pathClassLoader = new PathClassLoader("/sdcard/xx.dex", getClassLoader());

File dexOutputDir = context.getCodeCacheDir();
DexClassLoader dexClassLoader = new DexClassLoader("/sdcard/xx.dex",dexOutputDir.getAbsolutePath(), null,getClassLoader());

optimizedDirectory參數爲odex的目錄。實際上Android中的ClassLoader在加載dex時,會首先經過dexopt對dex執行優化,產生odex文件。optimizedDirectory爲null時的默認路徑爲:***/data/dalvik-cache***。並且處於安全考慮,此目錄需要使用app私有目錄,如:getCodeCacheDir()

在API 26源碼中,將DexClassLoader的optimizedDirectory標記爲了 deprecated 棄用,實現也變爲了:

 public DexClassLoader(String dexPath, String optimizedDirectory,
 					String librarySearchPath, ClassLoader parent) {
 	super(dexPath, null, librarySearchPath, parent);
 }

和PathClassLoader一摸一樣了!

雙親委託機制

​ 創建ClassLoader需要接收一個ClassLoader parent參數。這個parent爲父類加載。即:某個類加載器在接到加載類的請求時,首先將加載任務委託給父類加載器,依次遞歸,如果父類加載器可以完成類加載任務,就成功返回;只有父類加載器無法完成此加載任務時,才自己去加載。這就是雙親委託機制

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
	
    // 檢查class是否有被加載  
	Class c = findLoadedClass(name);
	if (c == null) {
		long t0 = System.nanoTime();
		try {
			if (parent != null) {
                //如果parent不爲null,則調用parent的loadClass進行加載  
				c = parent.loadClass(name, false);
            } else {
                //parent爲null,則調用BootClassLoader進行加載  
                c = findBootstrapClassOrNull(name);
            }
        } catch (ClassNotFoundException e) {
		
        }

        if (c == null) {
            // 如果都找不到就自己查找
			long t1 = System.nanoTime();
            c = findClass(name);
        }
	}
	return c;
}

因此我們自己創建的ClassLoader: new PathClassLoader("/sdcard/xx.dex", getClassLoader());並不僅僅只能獲得 xx.dex中的Class,還能夠獲得其父ClassLoader中加載的Class。

findClass

​ 在所有父ClassLoader無法加載Class時,則會調用自己的findClass方法。findClass在ClassLoader中的定義爲:

protected Class<?> findClass(String name) throws ClassNotFoundException {
	throw new ClassNotFoundException(name);
}

​ 其實任何ClassLoader子類,都可以重寫loadClassfindClass。一般如果你不想使用雙親委託,則重寫loadClass修改其實現。而重寫findClass則表示在雙親委託下,父ClassLoader都找不到Class的情況下,定義自己如何去查找一個Class。而我們的PathClassLoader會自己負責加載MainActivity這樣的程序中自己編寫的類,利用雙親委託父ClassLoader加載Framework中的Activity。說明PathClassLoader並沒有重寫loadClass,因此我們可以來看看PathClassLoader中的 findClass 是如何實現的。

public BaseDexClassLoader(String dexPath, File optimizedDirectory,String 	
						librarySearchPath, ClassLoader parent) {
	super(parent);
	this.pathList = new DexPathList(this, dexPath, librarySearchPath, 		
                                    optimizedDirectory);
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
	List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
    //查找指定的class
    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;
}

​ 實現非常簡單,從pathList中查找class。繼續查看DexPathList

public DexPathList(ClassLoader definingContext, String dexPath,
            String librarySearchPath, File optimizedDirectory) {
	//.........
    // splitDexPath 實現爲返回 List<File>.add(dexPath)
    // makeDexElements 會去 List<File>.add(dexPath) 中使用DexFile加載dex文件返回 Element數組
    this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                           suppressedExceptions, definingContext);
	//.........
    
}

public Class findClass(String name, List<Throwable> suppressed) {
     //從element中獲得代表Dex的 DexFile
	for (Element element : dexElements) {
		DexFile dex = element.dexFile;
		if (dex != null) {
            //查找class
        	Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
            if (clazz != null) {
            	return clazz;
        	}
    	}
    }
    if (dexElementsSuppressedExceptions != null) {
    	suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
    }
	return null;
}

在這裏插入圖片描述

熱修復

PathClassLoader中存在一個Element數組,Element類中存在一個dexFile成員表示dex文件,即:APK中有X個dex,則Element數組就有X個元素。
在這裏插入圖片描述
​ 而對於類的查找,由代碼for (Element element : dexElements)得知,會由數組從前往後進行查找。
在這裏插入圖片描述
​ 在PathClassLoader中的Element數組爲:[patch.dex , classes.dex , classes2.dex]。如果存在Key.class位於patch.dex與classes2.dex中都存在一份,當進行類查找時,循環獲得dexElements中的DexFile,查找到了Key.class則立即返回,不會再管後續的element中的DexFile是否能加載到Key.class了。
​ 因此,可以將出現Bug的class單獨的製作一份patch.dex文件(補丁包),然後在程序啓動時,從服務器下載patch.dex保存到某個路徑,再通過patch.dex的文件路徑,用其創建Element對象,然後將這個Element對象插入到我們程序的類加載器PathClassLoaderpathList中的dexElements數組頭部。這樣在加載出現Bug的class時會優先加載patch.dex中的修復類,從而解決Bug。QQ空間熱修復的原理就是這樣,利用反射Hook了PathClassLoader中pathList的dexElements數組。

總結:熱修復流程

1.獲取到當前應用的PathClassloader;
2反射獲取到DexPathList屬性對象pathList;
3.反射修改pathList的dexElements

a.把補丁包patch.dex轉化爲Element[] (patch)
b.獲得pathList的dexElements屬性(old)
c.patch+old合併,並反射賦值給pathList的dexElements

參考技術:
QQ空間安卓App熱補丁動態修復技術介紹
Android技術——ASM字節碼插樁

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章