一:熱修復(fù)相關(guān)
熱修復(fù)概念: 以補(bǔ)丁的方式動(dòng)態(tài)修復(fù)緊急Bug,不再需要重新發(fā)布App,不再需要用戶(hù)重新下載。
PathClassloader和DexClassLoader:
(1)PathClassloader作為其系統(tǒng)類(lèi)和應(yīng)用類(lèi)的加載器,只能去加載已經(jīng)安裝到Android系統(tǒng)中的apk文件。
(2)DexClassLoader可以用來(lái)從.jar和.apk類(lèi)型的文件內(nèi)部加載classes.dex文件。可以用來(lái)執(zhí)行非安裝的程序代碼。
(3)Android使用PathClassLoader作為其類(lèi)加載器,DexClassLoader可以從.jar和.apk類(lèi)型的文件內(nèi)部加載classes.dex文件。
熱修復(fù)原理:
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中有個(gè)pathList對(duì)象,pathList中包含一個(gè)DexFile的集合dexElements,而對(duì)于類(lèi)加載呢,就是遍歷這個(gè)集合,通過(guò)DexFile去尋找。
2 一個(gè)ClassLoader可以包含多個(gè)dex文件,每個(gè)dex文件是一個(gè)Element,多個(gè)dex文件排列成一個(gè)有序的數(shù)組dexElements,當(dāng)找類(lèi)的時(shí)候,會(huì)按順序遍歷dex文件,然后從當(dāng)前遍歷的dex文件中找類(lèi),如果找類(lèi)則返回,如果找不到從下一個(gè)dex文件繼續(xù)查找。
3 理論上,如果在不同的dex中有相同的類(lèi)存在,那么會(huì)優(yōu)先選擇排在前面的dex文件的類(lèi),如下圖:
4 把有問(wèn)題的類(lèi)打包到一個(gè)dex(patch.dex)中去,然后把這個(gè)dex插入到Elements的最前面,如下圖:
二:阻止相關(guān)類(lèi)打上CLASS_ISPREVERIFIED標(biāo)志
dex校驗(yàn): 如果兩個(gè)相關(guān)聯(lián)的類(lèi)在不同的dex中就會(huì)報(bào)錯(cuò),例如ClassA 引用了ClassB,但是發(fā)現(xiàn)這這兩個(gè)類(lèi)所在的dex不在一起,其中:
- ClassA 在classes.dex中
- ClassB 在patch.dex中
結(jié)果發(fā)生了錯(cuò)誤。
dex校驗(yàn)的前提: 如果引用者這個(gè)類(lèi)被打上了CLASS_ISPREVERIFIED標(biāo)志,那么就會(huì)進(jìn)行dex的校驗(yàn)。
相關(guān)類(lèi)打上CLASS_ISPREVERIFIED標(biāo)志的發(fā)生場(chǎng)景:
在虛擬機(jī)啟動(dòng)的時(shí)候,當(dāng)verify選項(xiàng)被打開(kāi)的時(shí)候,如果static方法、private方法、構(gòu)造函數(shù)等,其中的直接引用(第一層關(guān)系)到的類(lèi)都在同一個(gè)dex文件中,那么這個(gè)類(lèi)就會(huì)被打上CLASS_ISPREVERIFIED
下圖是class A 打上CLASS_ISPREVERIFIED標(biāo)志
下圖是class A 沒(méi)有打上CLASS_ISPREVERIFIED標(biāo)志
其中AntilazyLoad類(lèi)會(huì)被打包成單獨(dú)的hack.dex,這樣當(dāng)安裝apk的時(shí)候,classes.dex內(nèi)的類(lèi)都會(huì)引用一個(gè)在不相同dex中的AntilazyLoad類(lèi),這樣就防止了類(lèi)被打上CLASS_ISPREVERIFIED的標(biāo)志了,只要沒(méi)被打上這個(gè)標(biāo)志的類(lèi)都可以進(jìn)行打補(bǔ)丁操作。
在class文件中插入代碼來(lái)阻止相關(guān)類(lèi)打上CLASS_ISPREVERIFIED標(biāo)志:在dx工具執(zhí)行之前,將LoadBugClass.class文件呢,進(jìn)行修改,再其構(gòu)造中添加System.out.println(dodola.hackdex.AntilazyLoad.class),然后繼續(xù)打包的流程。
原始代碼
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)
在構(gòu)造函數(shù)中插入操作代碼(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");//項(xiàng)目的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包,然后寫(xiě)入App的私有目錄,最后把該jar對(duì)應(yīng)的dexElements文件插入到數(shù)組的最前面。
public class HotfixApplication extends Application
{
@Override
public void onCreate()
{
super.onCreate();
//創(chuàng)建jar對(duì)應(yīng)的文件
File dexPath = new File(getDir("dex", Context.MODE_PRIVATE), "hackdex_dex.jar");
//將asset文件中的jar寫(xiě)到App的私有目錄下面。
Utils.prepareDex(this.getApplicationContext(), dexPath, "hackdex_dex.jar");
// 把jar對(duì)應(yīng)的dexElements插入到dex數(shù)組最前面。
HotFix.patch(this, dexPath.getAbsolutePath(), "dodola.hackdex.AntilazyLoad");
try
{
this.getClassLoader().loadClass("dodola.hackdex.AntilazyLoad");
} catch (ClassNotFoundException e)
{
e.printStackTrace();
}
}
}
創(chuàng)建jar對(duì)應(yīng)的文件
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;
}
找相應(yīng)的ClassLoader進(jìn)行操作
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補(bǔ)丁插入到APP中,過(guò)程和插入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");
}
}
四:總結(jié)
(1)因?yàn)槲覀兊腜atch是以獨(dú)立的jar包,插入到APP的DexElements中,
所以如果APP中中的類(lèi)引用了Patch中的類(lèi),就會(huì)在校驗(yàn)時(shí)報(bào)錯(cuò)。因?yàn)楫?dāng)進(jìn)行dex校驗(yàn)時(shí),如果兩個(gè)相關(guān)聯(lián)的類(lèi)在不同的dex中就會(huì)報(bào)錯(cuò)。(LoadBugClass引用BugClass)。
(2) 為了防止上述錯(cuò)誤就要阻止Dex校驗(yàn),阻止Dex校驗(yàn)的方法是阻止相關(guān)類(lèi)打上CLASS_ISPREVERIFIED標(biāo)志。
(3) 阻止相關(guān)類(lèi)打上CLASS_ISPREVERIFIED標(biāo)志的做法是:在相關(guān)引用的類(lèi)(LoadBugClass.class)的構(gòu)造方法中,引用另外一個(gè)jar中類(lèi)AntilazyLoad.class
(4)因?yàn)?strong>AntilazyLoad.class在另一個(gè)jar中,所以需要把該jar對(duì)應(yīng)的dex插入到App中。并且在A(yíng)pplication中的onCreate()方法中將該類(lèi)加載進(jìn)來(lái)。
(5)把Patch.jar對(duì)應(yīng)的dexElement加載進(jìn)App中。因?yàn)镻atch.jar放在dex數(shù)組的第一個(gè)位置,所以首先被加載。即:如果在不同的dex中有相同的類(lèi)存在,那么會(huì)優(yōu)先選擇排在前面的dex文件的類(lèi)。