一:熱修複相關
熱修複概念: 以更新檔的方式動态修複緊急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檔案的類,如下圖:
圖1
4 把有問題的類打包到一個dex(patch.dex)中去,然後把這個dex插入到Elements的最前面,如下圖:
圖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标志
圖三
下圖是class A 沒有打上CLASS_ISPREVERIFIED标志
圖四
其中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檔案的類。