一:熱修復(fù)相關(guān)
熱修復(fù)概念: 以補(bǔ)丁的方式動(dòng)態(tài)修復(fù)緊急Bug惊豺,不再需要重新發(fā)布App,不再需要用戶重新下載闰非。
PathClassloader和DexClassLoader:
(1)PathClassloader作為其系統(tǒng)類和應(yīng)用類的加載器绳慎,只能去加載已經(jīng)安裝到Android系統(tǒng)中的apk文件。
(2)DexClassLoader可以用來從.jar和.apk類型的文件內(nèi)部加載classes.dex文件肩杈。可以用來執(zhí)行非安裝的程序代碼解寝。
(3)Android使用PathClassLoader作為其類加載器扩然,DexClassLoader可以從.jar和.apk類型的文件內(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ì)于類加載呢,就是遍歷這個(gè)集合嘉抓,通過DexFile去尋找索守。
2 一個(gè)ClassLoader可以包含多個(gè)dex文件,每個(gè)dex文件是一個(gè)Element抑片,多個(gè)dex文件排列成一個(gè)有序的數(shù)組dexElements卵佛,當(dāng)找類的時(shí)候,會(huì)按順序遍歷dex文件敞斋,然后從當(dāng)前遍歷的dex文件中找類截汪,如果找類則返回,如果找不到從下一個(gè)dex文件繼續(xù)查找植捎。
3 理論上衙解,如果在不同的dex中有相同的類存在,那么會(huì)優(yōu)先選擇排在前面的dex文件的類焰枢,如下圖:
4 把有問題的類打包到一個(gè)dex(patch.dex)中去蚓峦,然后把這個(gè)dex插入到Elements的最前面,如下圖:
二:阻止相關(guān)類打上CLASS_ISPREVERIFIED標(biāo)志
dex校驗(yàn): 如果兩個(gè)相關(guān)聯(lián)的類在不同的dex中就會(huì)報(bào)錯(cuò)济锄,例如ClassA 引用了ClassB暑椰,但是發(fā)現(xiàn)這這兩個(gè)類所在的dex不在一起,其中:
- ClassA 在classes.dex中
- ClassB 在patch.dex中
結(jié)果發(fā)生了錯(cuò)誤荐绝。
dex校驗(yàn)的前提: 如果引用者這個(gè)類被打上了CLASS_ISPREVERIFIED標(biāo)志一汽,那么就會(huì)進(jìn)行dex的校驗(yàn)。
相關(guān)類打上CLASS_ISPREVERIFIED標(biāo)志的發(fā)生場(chǎng)景:
在虛擬機(jī)啟動(dòng)的時(shí)候低滩,當(dāng)verify選項(xiàng)被打開的時(shí)候召夹,如果static方法、private方法恕沫、構(gòu)造函數(shù)等戳鹅,其中的直接引用(第一層關(guān)系)到的類都在同一個(gè)dex文件中,那么這個(gè)類就會(huì)被打上CLASS_ISPREVERIFIED
下圖是class A 打上CLASS_ISPREVERIFIED標(biāo)志
下圖是class A 沒有打上CLASS_ISPREVERIFIED標(biāo)志
其中AntilazyLoad類會(huì)被打包成單獨(dú)的hack.dex昏兆,這樣當(dāng)安裝apk的時(shí)候枫虏,classes.dex內(nèi)的類都會(huì)引用一個(gè)在不相同dex中的AntilazyLoad類妇穴,這樣就防止了類被打上CLASS_ISPREVERIFIED的標(biāo)志了,只要沒被打上這個(gè)標(biāo)志的類都可以進(jìn)行打補(bǔ)丁操作隶债。
在class文件中插入代碼來阻止相關(guān)類打上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包赞警,然后寫入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寫到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中,過程和插入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中中的類引用了Patch中的類旁瘫,就會(huì)在校驗(yàn)時(shí)報(bào)錯(cuò)。因?yàn)楫?dāng)進(jìn)行dex校驗(yàn)時(shí)琼蚯,如果兩個(gè)相關(guān)聯(lián)的類在不同的dex中就會(huì)報(bào)錯(cuò)酬凳。(LoadBugClass引用BugClass)。
(2) 為了防止上述錯(cuò)誤就要阻止Dex校驗(yàn)遭庶,阻止Dex校驗(yàn)的方法是阻止相關(guān)類打上CLASS_ISPREVERIFIED標(biāo)志宁仔。
(3) 阻止相關(guān)類打上CLASS_ISPREVERIFIED標(biāo)志的做法是:在相關(guān)引用的類(LoadBugClass.class)的構(gòu)造方法中,引用另外一個(gè)jar中類AntilazyLoad.class
(4)因?yàn)?strong>AntilazyLoad.class在另一個(gè)jar中峦睡,所以需要把該jar對(duì)應(yīng)的dex插入到App中翎苫。并且在Application中的onCreate()方法中將該類加載進(jìn)來。
(5)把Patch.jar對(duì)應(yīng)的dexElement加載進(jìn)App中赐俗。因?yàn)镻atch.jar放在dex數(shù)組的第一個(gè)位置,所以首先被加載弊知。即:如果在不同的dex中有相同的類存在阻逮,那么會(huì)優(yōu)先選擇排在前面的dex文件的類。