ClassLoader
我們知道一個java程序來是由多個class
類組成的于样,我們在運行程序的過程中需要通過ClassLoader
將class
類載入到JVM
中才可以正常運行屡律。
而Android程序需要正常運行,也同樣需要有ClassLoader
機制將class類加載到Android 的 Dalvik
(5.0之前版本)/ART
(5.0增加的)中浅萧,只不過它和java
中的ClassLoader
不一樣在于Android的apk
打包逐沙,是將class
文件打包成一個或者多個 dex
文件(由于Android 65k
問題,使用 MultiDex
就會生成多個 dex
文件)洼畅,再由BaseDexClassLoader
來進行處理吩案。
在安裝apk
的過程中,會有一個驗證優(yōu)化dex
的機制帝簇,叫做DexOpt
徘郭,這個過程會生成一個odex
文件( odex 文件也屬于dex文件
),即Optimised Dex
丧肴。執(zhí)行odex
的效率會比直接執(zhí)行dex
文件的效率要高很多残揉。運行Apk的時候,直接加載odex
文件闪湾,從而避免重復(fù)驗證和優(yōu)化冲甘,加快了Apk的響應(yīng)時間。
注意:Dalvik/ART 無法像 JVM 那樣直接加載 class 文件和 jar 文件中的 class途样,需要通過工具來優(yōu)化轉(zhuǎn)換成 Dalvik byte code 才行江醇,只能通過 dex 或者包含 dex 的jar、apk 文件來加載
dex生成方法
你可以直接在編譯工程后何暇,在app/build/intermediates/classes
中拿到你需要的class
陶夜,然后再通過dx
命令生成dex
文件
dx --dex --output=/Users/test/test.dex multi/shengyuan/com/mytestdemo/test.class
雙親委派機制
類加載器雙親委派模型的工作過程是:如果一個類加載器收到一個類加載的請求,它首先將這個請求委派給父類加載器去完成裆站,每一個層次類加載器都是如此条辟,則所有的類加載請求都會傳送到頂層的啟動類加載器,只有父加載器無法完成這個加載請求(即它的搜索范圍中沒有找到所要的類)宏胯,子類才嘗試加載羽嫡。
雙親委派模式優(yōu)勢
采用雙親委派模式的是好處是Java類隨著它的類加載器一起具備了一種帶有優(yōu)先級的層次關(guān)系,通過這種層級關(guān)可以避免類的重復(fù)加載肩袍,當父親已經(jīng)加載了該類時杭棵,就沒有必要子ClassLoader再加載一次。其次是考慮到安全因素氛赐,java核心api中定義類型不會被隨意替換魂爪,假設(shè)通過網(wǎng)絡(luò)傳遞一個名為java.lang.Integer的類先舷,通過雙親委托模式傳遞到啟動類加載器,而啟動類加載器在核心Java API發(fā)現(xiàn)這個名字的類滓侍,發(fā)現(xiàn)該類已被加載蒋川,并不會重新加載網(wǎng)絡(luò)傳遞的過來的java.lang.Integer,而直接返回已加載過的Integer.class撩笆,這樣便可以防止核心API庫被隨意篡改捺球。
Android中的ClassLoader根據(jù)用途可分為一下幾種:
- BootClassLoader:主要用于加載系統(tǒng)的類,包括
java
和android
系統(tǒng)的類庫浇衬,和JVM
中不同懒构,BootClassLoader是ClassLoader
內(nèi)部類,是由Java
實現(xiàn)的耘擂,它也是所有系統(tǒng)ClassLoader
的父ClassLoader - PathClassLoader:用于加載Android系統(tǒng)類和開發(fā)編寫應(yīng)用的類胆剧,只能加載已經(jīng)安裝應(yīng)用的
dex
或apk
文件,也是getSystemClassLoader
的返回對象 - DexClassLoader:可以用于加載任意路徑的
zip
醉冤、dex
秩霍、jar
或者apk
文件,也是進行安卓動態(tài)加載的基礎(chǔ)
DexClassLoader類
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
}
}
PathClassLoader類
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);
}
}
BaseDexClassLoader#BaseDexClassLoader方法
PathClassLoader
蚁阳、DexClassLoader
均繼承BaseDexClassLoader
铃绒,所以其super
方法均調(diào)用到了BaseDexClassLoader
構(gòu)造方法
參數(shù)詳解:
dexPath
待解析文件所在的全路徑,classloader
將在該路徑中指定的dex
文件尋找指定目標類optimzedDirectory
優(yōu)化路徑螺捐,指的是虛擬機對于apk中的dex文件進行優(yōu)化后生成文件存放的路徑颠悬,如dalvik虛擬機生成的ODEX
文件路徑和ART
虛擬機生成的OAT
文件路徑。
這個路徑必須是當前app的內(nèi)部存儲路徑定血,Google認為如果放在公有的路徑下赔癌,存在被惡意注入的危險
注意:PathClassLoader沒有將optimizedDirectory置為Null,也就是沒設(shè)置優(yōu)化后的存放路徑。其實optimizedDirectory為null時的默認路徑就是/data/dalvik-cache 目錄澜沟。 PathClassLoader是用來加載Android系統(tǒng)類和應(yīng)用的類灾票,并且不建議開發(fā)者使用。
libraryPath
指定native
(即so
加載路徑)層代碼存放路徑parent
當前ClassLoader的parent
茫虽,和java
中classloader
的parent
含義一樣
public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) {
super(parent);
this.originalPath = dexPath;
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
ClassLoader#loadClass方法
通過該方法你就能發(fā)現(xiàn)雙親委派機制的妙處了
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// 1 通過調(diào)用c層findLoadedClass檢查該類是否被加載過刊苍,若加載過則返回class對象(緩存機制)
Class c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//2 各種類型的類加載器在構(gòu)造時都會傳入一個parent類加載器
//2 若parent類不為空,則調(diào)用parent類的loadClass方法
c = parent.loadClass(name, false);
} else {
//3 查閱了PathClassLoader濒析、DexClassLoader并沒有重寫該方法正什,默認是返回null
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
//4 如果父ClassLoader不能加載該類才由自己去加載,這個方法從本ClassLoader的搜索路徑中查找該類
long t1 = System.nanoTime();
c = findClass(name);
}
}
return c;
}
BaseDexClassLoader#findClass方法
DexClassLoader
、PathClassLoader
通過繼承BaseDexClassLoader
從而使用其父類findClass
方法号杏,在ClassLoader#loadClass
方法中第3
步進入
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
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;
}
DexPathList#findClass方法
從ClassLoader#loadClass
方法中我們可以知道埠忘,當走到第2
步即會走到如下方法,通過對已構(gòu)建好的dexElements
進行遍歷馒索,通過dex.loadClassBinaryName
方法load
對應(yīng)的class
類莹妒,所以這里是一個熱修復(fù)的點,你可以將需要熱修復(fù)的dex
文件插入到dexElements
數(shù)組前面绰上,這樣遍歷的時候查到你最新插入的則返回旨怠,從而實現(xiàn)動態(tài)替換有問題類
public Class findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
//調(diào)用到c層defineClassNative方法進行查找
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
DexPathList#makeElements
BaseDexClassLoader
的構(gòu)造方法中對DexPathList
進行實例化,在DexPathList
構(gòu)造方法中調(diào)用makeElements
生成dexElements
數(shù)組蜈块,首先會根據(jù)傳入的dexPath
鉴腻,生成一個file類型的list容器,然后傳入后進行遍歷加載百揭,通過調(diào)用DexFile
中的loadDexFile
對dexFile
文件進行加載
private static Element[] makeElements(List<File> files, File optimizedDirectory,
List<IOException> suppressedExceptions,
boolean ignoreDexFiles,
ClassLoader loader) {
Element[] elements = new Element[files.size()];
int elementsPos = 0;
//遍歷所有文件爽哎,并提取 dex 文件。
for (File file : files) {
File zip = null;
File dir = new File("");
DexFile dex = null;
String path = file.getPath();
String name = file.getName();
if (path.contains(zipSeparator)) {
String split[] = path.split(zipSeparator, 2);
zip = new File(split[0]);
dir = new File(split[1]);
} else if (file.isDirectory()) {
//為文件夾時器一,直接存儲
elements[elementsPos++] = new Element(file, true, null, null);
} else if (file.isFile()) {
if (!ignoreDexFiles && name.endsWith(DEX_SUFFIX)) {
// loadDexFile 的作用是:根據(jù) file 獲取對應(yīng)的 DexFile 對象课锌。
try {
dex = loadDexFile(file, optimizedDirectory, loader, elements);
} catch (IOException suppressed) {
System.logE("Unable to load dex file: " + file, suppressed);
suppressedExceptions.add(suppressed);
}
} else {
// 非 dex 文件,那么 zip 表示包含 dex 文件的壓縮文件祈秕,如 .apk渺贤,.jar 文件等
zip = file;
if (!ignoreDexFiles) {
try {
dex = loadDexFile(file, optimizedDirectory, loader, elements);
} catch (IOException suppressed) {
suppressedExceptions.add(suppressed);
}
}
}
} else {
System.logW("ClassLoader referenced unknown path: " + file);
}
if ((zip != null) || (dex != null)) {
elements[elementsPos++] = new Element(dir, false, zip, dex);
}
}
if (elementsPos != elements.length) {
elements = Arrays.copyOf(elements, elementsPos);
}
return elements;
}
插件化
看到這里,你應(yīng)該大概理解了classloader
加載流程请毛,其實java
這層的classloader
代碼量并不多志鞍,主要集中在c
層,但是我們在java
層進行hook
便可實現(xiàn)熱修復(fù)方仿。
結(jié)合網(wǎng)上的資料及源碼的閱讀一共有三種方案
方案1:向dexElements
進行插入新的dex
(目前最常見的方式)
從上面的ClassLoader#loadClass
方法你就會知道固棚,初始化的時候會進入BaseDexClassLoader#findClass
方法中通過遍歷dexElements
進行查找dex
文件,因為dexElements
是一個數(shù)組仙蚜,所以我們可以通過反射的形式此洲,將需要熱修復(fù)的dex
文件插入到數(shù)組首部
,這樣遍歷數(shù)組的時候就會優(yōu)先讀取你插入的dex
鳍征,從而實現(xiàn)熱修復(fù)黍翎。
DexClassLoader不是允許你加載外部dex嗎?用DexClassLoader#loadClass不就行了
我們知道DexClassLoader
是允許你加載外部dex
文件的艳丛,所以網(wǎng)上有一些例子介紹通過DexClassLoader#loadClass
可以加載到你的dex
文件中的方法匣掸,那么有一些網(wǎng)友就會有疑問,我直接通過調(diào)用DexClassLoader#loadClass
去獲取我傳入的外部dex
文件中的class
氮双,不就行了碰酝,這樣確實是可以的,但是它僅適用于新增的類戴差,而不能去替換舊的類送爸,因為通過上面的dexElements
數(shù)組的生成以及委派雙親機制
,你就會知道它的父類是先去把你應(yīng)用類組裝進來,當你調(diào)用DexClassLoader
去loadClass
時袭厂,是先委派父類去loadClass
墨吓,如果查找不到才會到子類自行查找,也就是說應(yīng)用中本來就已經(jīng)存在B.class
了纹磺,那么父類loadClass
會直接返回帖烘,而你真正需要返回的其實是子類中的B.class
,所以才說只適用于新增的類橄杨,你不通過一些手段修改源碼層秘症,是無法實現(xiàn)替換類的。
方案2:在ActivityThread中替換LoadedApk的mClassLoader對象
小編在開發(fā)MPlugin的時候式矫,使用了下面的方法乡摹,但發(fā)現(xiàn)當你插件apk中進行跳轉(zhuǎn)的下一個頁面的時候,若引了第三方的庫采转,會拋出無法載入該第三方庫控件異常聪廉。
實現(xiàn)代碼如下:
public static void loadApkClassLoader(Context context,DexClassLoader dLoader){
try{
// 配置動態(tài)加載環(huán)境
Object currentActivityThread = RefInvoke.invokeStaticMethod(
"android.app.ActivityThread", "currentActivityThread",
new Class[] {}, new Object[] {});//獲取主線程對象 http://blog.csdn.net/myarrow/article/details/14223493
String packageName = context.getPackageName();//當前apk的包名
ArrayMap mPackages = (ArrayMap) RefInvoke.getFieldOjbect(
"android.app.ActivityThread", currentActivityThread,
"mPackages");
WeakReference wr = (WeakReference) mPackages.get(packageName);
RefInvoke.setFieldOjbect("android.app.LoadedApk", "mClassLoader",
wr.get(), dLoader);
}catch(Exception e){
e.printStackTrace();
}
}
方案3:通過自定義ClassLoader實現(xiàn)class攔截替換
我們知道PathClassLoader
是加載已安裝的apk
的dex
,那我們可以
在 PathClassLoader
和 BootClassLoader
之間插入一個 自定義的MyClassLoader
氏义,而我們通過ClassLoader#loadClass
方法中的第2
步知道锄列,若parent
不為空,會調(diào)用parent.loadClass
方法惯悠,固我們可以在MyClassLoader
中重寫loadClass
方法邻邮,在這個里面做一個判斷去攔截替換掉我們需要修復(fù)的class
。
如何拿到我們需要修復(fù)的class呢克婶?
我當時首先想到的是通過DexClassLoader
直接去loadClass
來獲得需要熱修復(fù)的Class
筒严,但是通過ClassLoader#loadClass
方法分析,可以知道加載查找class
的第1
步是調(diào)用findLoadedClass
情萤,這個方法主要作用是檢查該類是否被加載過鸭蛙,如果加載過則直接返回,所以如果你想通過DexClassLoader
直接去loadClass
來獲得你需要熱修復(fù)的Class
筋岛,是不可能完成替換的(熱修復(fù))娶视,因為你調(diào)用DexClassLoader.loadClass
已經(jīng)屬于首次加載了,那么意味著下次加載就直接在findLoadedClass
方法中返回class
了睁宰,是不會再往下走肪获,從而MyClassLoader#loadClass
方法也不可能會被回調(diào),也就無法實現(xiàn)修復(fù)柒傻。
通過BaseDexClassLoader#findClass
方法你就會知道孝赫,這個方法在父ClassLoader
不能加載該類的時候才由自己去加載,我們可以通過這個方法來獲得我們的class
红符,因為你調(diào)用這個方法的話青柄,是不會被緩存起來伐债。也就不存在ClassLoader#loadClass
中的第1
步就查找到就被返回。
方案3代碼:
public class HookUtil {
/**
* 在 PathClassLoader 和 BootClassLoader 之間插入一個 自定義的MyClassLoader
* @param classLoader
* @param newParent
*/
public static void injectParent(ClassLoader classLoader, ClassLoader newParent) {
try {
Field parentField = ClassLoader.class.getDeclaredField("parent");
parentField.setAccessible(true);
parentField.set(classLoader, newParent);
} catch (IllegalArgumentException e) {
throw new RuntimeException(e);
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
/**
* 反射調(diào)用findClass方法獲取dex中的class類
* @param context
* @param dexPath
* @param className
*/
public static void hookFindClass(Context context,String dexPath,String className){
DexClassLoader dexClassLoader = new DexClassLoader(dexPath, context.getDir("dex",context.MODE_PRIVATE).getAbsolutePath(),null, context.getClassLoader());
try {
Class<?> herosClass = dexClassLoader.getClass().getSuperclass();
Method m1 = herosClass.getDeclaredMethod("findClass", String.class);
m1.setAccessible(true);
Class newClass = (Class) m1.invoke(dexClassLoader, className);
ClassLoader pathClassLoader = MyApplication.getContext().getClassLoader();
MyClassLoader myClassLoader = new MyClassLoader(pathClassLoader.getParent());
myClassLoader.registerClass(className, newClass);
injectParent(pathClassLoader, myClassLoader);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
public class MyClassLoader extends ClassLoader {
public Map<String,Class> myclassMap;
public MyClassLoader(ClassLoader parent) {
super(parent);
myclassMap = new HashMap<>();
}
/**
* 注冊類名以及對應(yīng)的類
* @param className
* @param myclass
*/
public void registerClass(String className,Class myclass){
myclassMap.put(className,myclass);
}
/**
* 移除對應(yīng)的類
* @param className
*/
public void removeClass(String className){
myclassMap.remove(className);
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
Class myclass = myclassMap.get(name);
//重寫父類loadClass方法致开,實現(xiàn)攔截
if(myclass!=null){
return myclass;
}else{
return super.loadClass(name, resolve);
}
}
}
關(guān)于CLASS_ISPREVERIFIED標記
因為在 Dalvik
虛擬機下峰锁,執(zhí)行 dexopt
時,會對類進行掃描喇喉,如果類里面所有直接依賴的類都在同一個 dex 文件中祖今,那么這個類就會被打上 CLASS_ISPREVERIFIED
標記,如果一個類有 CLASS_ISPREVERIFIED
標記拣技,那么在熱修復(fù)時,它加載了其他 dex 文件中的類耍目,會報經(jīng)典的Class ref in pre-verified class resolved to unexpected implementation
異常
通過源碼搜索并沒有找到CLASS_ISPREVERIFIED
標記這個關(guān)鍵詞膏斤,通過在android7.0、8.0
上進行熱修復(fù)邪驮,也沒有遇到這個異常莫辨,猜測這個問題只屬于android5.0以前(關(guān)于解決方法網(wǎng)上有很多,本文就不講述了)毅访,因為android5.0后新增了art
沮榜。
最后
看到這里相信java層的ClassLoader機制你已經(jīng)熟悉得差不多了,相對于插件化而言你已經(jīng)前進了一步喻粹,但仍有一些問題需要去思考解決的蟆融,比如解決資源加載、混淆守呜、加殼等問題型酥,為了更好的完善熱修復(fù)機制,你也可以去閱讀下c層的邏輯查乒,盡管熱修復(fù)帶來了很多便利弥喉,但個人也并不是太認同熱修復(fù)的使用,畢竟是通過hook去修改源碼層玛迄,因為android的碎片化問題由境,很難確保你的hook能正常使用且不引發(fā)別的問題。
注意:本文源碼閱讀及案例測試是基于android7.0蓖议、8.0編寫的虏杰,案例經(jīng)過實測是可行的