天天看點

android 熱更新檔工具,Android熱更新檔修複

一:熱修複相關

熱修複概念: 以更新檔的方式動态修複緊急Bug,不再需要重新釋出App,不再需要使用者重新下載下傳。

PathClassloader和DexClassLoader:

(1)PathClassloader作為其系統類和應用類的加載器,隻能去加載已經安裝到Android系統中的apk檔案。

(2)DexClassLoader可以用來從.jar和.apk類型的檔案内部加載classes.dex檔案。可以用來執行非安裝的程式代碼。

(3)Android使用PathClassLoader作為其類加載器,DexClassLoader可以從.jar和.apk類型的檔案内部加載classes.dex檔案。

熱修複原理:

PathClassLoader和DexClassLoader都繼承自BaseDexClassLoader

在BaseDexClassLoader中有如下源碼:

#BaseDexClassLoader

@Override

protected Class> findClass(String name) throws ClassNotFoundException {

Class clazz = pathList.findClass(name);

if (clazz == null) {

throw new ClassNotFoundException(name);

}

return clazz;

}

#DexPathList

public Class findClass(String name) {

for (Element element : dexElements) {

DexFile dex = element.dexFile;

if (dex != null) {

Class clazz = dex.loadClassBinaryName(name, definingContext);

if (clazz != null) {

return clazz;

}

}

}

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);1 n j

n

1 BaseDexClassLoader中有個pathList對象,pathList中包含一個DexFile的集合dexElements,而對于類加載呢,就是周遊這個集合,通過DexFile去尋找。

2 一個ClassLoader可以包含多個dex檔案,每個dex檔案是一個Element,多個dex檔案排列成一個有序的數組dexElements,當找類的時候,會按順序周遊dex檔案,然後從目前周遊的dex檔案中找類,如果找類則傳回,如果找不到從下一個dex檔案繼續查找。

3 理論上,如果在不同的dex中有相同的類存在,那麼會優先選擇排在前面的dex檔案的類,如下圖:

android 熱更新檔工具,Android熱更新檔修複

圖1

4 把有問題的類打包到一個dex(patch.dex)中去,然後把這個dex插入到Elements的最前面,如下圖:

android 熱更新檔工具,Android熱更新檔修複

圖2

二:阻止相關類打上CLASS_ISPREVERIFIED标志

dex校驗: 如果兩個相關聯的類在不同的dex中就會報錯,例如ClassA 引用了ClassB,但是發現這這兩個類所在的dex不在一起,其中:

ClassA 在classes.dex中

ClassB 在patch.dex中

結果發生了錯誤。

dex校驗的前提: 如果引用者這個類被打上了CLASS_ISPREVERIFIED标志,那麼就會進行dex的校驗。

相關類打上CLASS_ISPREVERIFIED标志的發生場景:

在虛拟機啟動的時候,當verify選項被打開的時候,如果static方法、private方法、構造函數等,其中的直接引用(第一層關系)到的類都在同一個dex檔案中,那麼這個類就會被打上CLASS_ISPREVERIFIED

下圖是class A 打上CLASS_ISPREVERIFIED标志

android 熱更新檔工具,Android熱更新檔修複

圖三

下圖是class A 沒有打上CLASS_ISPREVERIFIED标志

android 熱更新檔工具,Android熱更新檔修複

圖四

其中AntilazyLoad類會被打包成單獨的hack.dex,這樣當安裝apk的時候,classes.dex内的類都會引用一個在不相同dex中的AntilazyLoad類,這樣就防止了類被打上CLASS_ISPREVERIFIED的标志了,隻要沒被打上這個标志的類都可以進行打更新檔操作。

在class檔案中插入代碼來阻止相關類打上CLASS_ISPREVERIFIED标志:在dx工具執行之前,将LoadBugClass.class檔案呢,進行修改,再其構造中添加System.out.println(dodola.hackdex.AntilazyLoad.class),然後繼續打包的流程。

原始代碼

package dodola.hackdex;

public class AntilazyLoad

{

}

package dodola.hotfix;

public class BugClass

{

public String bug()

{

return "bug class";

}

}

package dodola.hotfix;

public class LoadBugClass

{

public String getBugString()

{

BugClass bugClass = new BugClass();

return bugClass.bug();

}

}

三:插入jar

插入代碼

System.out.println(dodola.hackdex.AntilazyLoad.class)

在構造函數中插入操作代碼(javassist)

package test;

import javassist.ClassPool;

import javassist.CtClass;

import javassist.CtConstructor;

public class InjectHack

{

public static void main(String[] args)

{

try

{

String path = "/Users/zhy/develop_work/eclipse_android/imooc/JavassistTest/";

ClassPool classes = ClassPool.getDefault();

classes.appendClassPath(path + "bin");//項目的bin目錄即可

CtClass c = classes.get("dodola.hotfix.LoadBugClass");

CtConstructor ctConstructor = c.getConstructors()[0];

ctConstructor

.insertAfter("System.out.println(dodola.hackdex.AntilazyLoad.class);");

c.writeFile(path + "/output");

} catch (Exception e)

{

e.printStackTrace();

}

}

}

把AntilazyLoad.class打包成jar包,然後寫入App的私有目錄,最後把該jar對應的dexElements檔案插入到數組的最前面。

public class HotfixApplication extends Application

{

@Override

public void onCreate()

{

super.onCreate();

//建立jar對應的檔案

File dexPath = new File(getDir("dex", Context.MODE_PRIVATE), "hackdex_dex.jar");

//将asset檔案中的jar寫到App的私有目錄下面。

Utils.prepareDex(this.getApplicationContext(), dexPath, "hackdex_dex.jar");

// 把jar對應的dexElements插入到dex數組最前面。

HotFix.patch(this, dexPath.getAbsolutePath(), "dodola.hackdex.AntilazyLoad");

try

{

this.getClassLoader().loadClass("dodola.hackdex.AntilazyLoad");

} catch (ClassNotFoundException e)

{

e.printStackTrace();

}

}

}

建立jar對應的檔案

public class Utils {

private static final int BUF_SIZE = 2048;

public static boolean prepareDex(Context context, File dexInternalStoragePath, String dex_file) {

BufferedInputStream bis = null;

OutputStream dexWriter = null;

bis = new BufferedInputStream(context.getAssets().open(dex_file));

dexWriter = new BufferedOutputStream(new FileOutputStream(dexInternalStoragePath));

byte[] buf = new byte[BUF_SIZE];

int len;

while ((len = bis.read(buf, 0, BUF_SIZE)) > 0) {

dexWriter.write(buf, 0, len);

}

dexWriter.close();

bis.close();

return true;

}

找相應的ClassLoader進行操作

public final class HotFix

{

public static void patch(Context context, String patchDexFile, String patchClassName)

{

if (patchDexFile != null && new File(patchDexFile).exists())

{

try

{

if (hasLexClassLoader())

{

injectInAliyunOs(context, patchDexFile, patchClassName);

} else if (hasDexClassLoader())

{

injectAboveEqualApiLevel14(context, patchDexFile, patchClassName);

} else

{

injectBelowApiLevel14(context, patchDexFile, patchClassName);

}

} catch (Throwable th)

{

}

}

}

}

Combine(合并)App的DexElements和AntilazyLoad.class的DexElements

private static boolean hasDexClassLoader()

{

try

{

Class.forName("dalvik.system.BaseDexClassLoader");

return true;

} catch (ClassNotFoundException e)

{

return false;

}

}

private static void injectAboveEqualApiLevel14(Context context, String str, String str2)

throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException

{

PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();

Object a = combineArray(getDexElements(getPathList(pathClassLoader)),

getDexElements(getPathList(

new DexClassLoader(str, context.getDir("dex", 0).getAbsolutePath(), str, context.getClassLoader()))));

Object a2 = getPathList(pathClassLoader);

setField(a2, a2.getClass(), "dexElements", a);

pathClassLoader.loadClass(str2);

}

将Patch.jar更新檔插入到APP中,過程和插入AntilazyLoad.class一樣

public class HotfixApplication extends Application

{

@Override

public void onCreate()

{

super.onCreate();

File dexPath = new File(getDir("dex", Context.MODE_PRIVATE), "hackdex_dex.jar");

Utils.prepareDex(this.getApplicationContext(), dexPath, "hack_dex.jar");

HotFix.patch(this, dexPath.getAbsolutePath(), "dodola.hackdex.AntilazyLoad");

try

{

this.getClassLoader().loadClass("dodola.hackdex.AntilazyLoad");

} catch (ClassNotFoundException e)

{

e.printStackTrace();

}

dexPath = new File(getDir("dex", Context.MODE_PRIVATE), "path_dex.jar");

Utils.prepareDex(this.getApplicationContext(), dexPath, "path_dex.jar");

HotFix.patch(this, dexPath.getAbsolutePath(), "dodola.hotfix.BugClass");

}

}

四:總結

(1)因為我們的Patch是以獨立的jar包,插入到APP的DexElements中,

是以如果APP中中的類引用了Patch中的類,就會在校驗時報錯。因為當進行dex校驗時,如果兩個相關聯的類在不同的dex中就會報錯。(LoadBugClass引用BugClass)。

(2) 為了防止上述錯誤就要阻止Dex校驗,阻止Dex校驗的方法是阻止相關類打上CLASS_ISPREVERIFIED标志。

(3) 阻止相關類打上CLASS_ISPREVERIFIED标志的做法是:在相關引用的類(LoadBugClass.class)的構造方法中,引用另外一個jar中類AntilazyLoad.class

(4)因為AntilazyLoad.class在另一個jar中,是以需要把該jar對應的dex插入到App中。并且在Application中的onCreate()方法中将該類加載進來。

(5)把Patch.jar對應的dexElement加載進App中。因為Patch.jar放在dex數組的第一個位置,是以首先被加載。即:如果在不同的dex中有相同的類存在,那麼會優先選擇排在前面的dex檔案的類。