當(dāng)我們開發(fā)完一個(gè)APP,打包成了apk裝進(jìn)了手機(jī),然后啟動(dòng)和使用APP,這一個(gè)過程中,必定會(huì)使用各種的類和方法,有系統(tǒng)的有自己的,那這些類都是如何加載成功,供我們使用的呢?
例如有一個(gè)A類,我們使用的時(shí)候,一般是new A()創(chuàng)建個(gè)對(duì)象,然后使用,到底是怎么new的?其實(shí)就是Android的類加載幫我們做的。
apk的組成
懂a(chǎn)pk的打包流程或者反編譯過apk的都知道,apk里面會(huì)有一個(gè)或者多個(gè)的dex文件,這些都是我們的代碼,但是是經(jīng)過處理的代碼,把我們的代碼轉(zhuǎn)化成電腦能懂的代碼,其實(shí)就是字節(jié)碼。
安裝apk
這里需要分一下版本,不同的版本安裝機(jī)制有點(diǎn)區(qū)別。
Android N(7.0)以上的:
安裝apk的時(shí)候,不進(jìn)行任何的預(yù)編譯(提高安裝速度);
運(yùn)行的過程中解析執(zhí)行,并且對(duì)經(jīng)常使用的方法進(jìn)行優(yōu)化,就是即時(shí)編譯(JIT just in time),經(jīng)過JIT處理的代碼,都會(huì)記錄在一個(gè)profile配置文件里;
最后在手機(jī)閑的時(shí)候,有一個(gè)編譯守護(hù)進(jìn)程,會(huì)對(duì)profile里面的方法進(jìn)行預(yù)先編譯(AOT),把這些代碼轉(zhuǎn)化為本地機(jī)器碼。
Android L(5.0)- Android N:
安裝時(shí)直接使用預(yù)先編譯(AOT),就是把所有代碼一次性轉(zhuǎn)化為本地機(jī)器碼,當(dāng)需要使用時(shí)就可以直接使用了。但是缺點(diǎn)就是第一次安裝的時(shí)候十分的慢,因?yàn)橐淮涡赞D(zhuǎn)化全部代碼很耗時(shí)。
Android 2.2-4.4:這部分都是使用JIT,就是說都是在運(yùn)行的時(shí)候,需要用什么,就加載什么,好處就是安裝賊快,但缺點(diǎn)也明顯,每次運(yùn)行都需要重新編譯,浪費(fèi)資源,例如電量。
關(guān)鍵類:ClassLoader

我們先了解下類加載的所有類關(guān)系。
①BootClassLoader:用來加載系統(tǒng)framework層的class文件。
②BaseDexClassLoader:衍生出PathClassLoader和DexClassLoader。
③ PathClassLoader:Android應(yīng)用的類加載器,也就是我們寫的類,都由這個(gè)來加載。
④DexClassLoader:這個(gè)是加載一些額外的動(dòng)態(tài)類。
類加載
安裝成功了,打開APP,每當(dāng)我們使用類,創(chuàng)建對(duì)象的時(shí)候,類加載器都會(huì)幫我們從代碼里面找出我們要的那個(gè)類,然后加載給我們用。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
//交給父類加載器去加載 遞歸
c = parent.loadClass(name, false);
} else {
//這里是如果父類加載器是null(也就是bootstrap),那這個(gè)方法會(huì)去查找name指向的這個(gè)類是不是由bootstrap加載了,是的話就返回class對(duì)象,不是的話返回null
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
return c;
}
可以看到,加載一個(gè)類,會(huì)分為2步
1、Class<?> c = findLoadedClass(name);找緩存,如果已經(jīng)被加載過了,就直接return返回。
2、如果沒有緩存,就根據(jù)變量parent是否為null來判斷邏輯,如果不為null,就直接調(diào)用parent的loadClass方法;如果為null,就調(diào)用findBootStrapClassOrNull方法。
這里值得注意的是這個(gè)parent,其實(shí)這里使用了雙親委托機(jī)制(先把任務(wù)交給父類去處理,直到?jīng)]有父類或者父類處理不了,才自己去嘗試處理),我們的類需要加載的時(shí)候,是由pathClassLoader處理的,但是!這里雙親委托說的父類,指的不是BaseDexClassLoader,而是BootClassLoader。
所以這里的邏輯理解應(yīng)該是:
先判斷當(dāng)前加載器是否有父類,沒有就從Bootstrap里面找,如果沒有加載過就自己去執(zhí)行findClass方法去加載。
如果有父類,就根據(jù)雙親委托機(jī)制,遞歸加載,如果都沒有加載過,最后也是交給自己去執(zhí)行findClass。
那findClass又做了什么?
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
ClassLoader中的findClass其實(shí)是空實(shí)現(xiàn),也就是說實(shí)現(xiàn)交給了子類去實(shí)現(xiàn)。那再找BaseDexClassLoader中的。
@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;
}
重點(diǎn)就這一句:Class c = pathList.findClass(name, suppressedExceptions);
從變量pathList中,根據(jù)name來findClass,并且把結(jié)果return。那pathList是什么?先看定義
private final DexPathList pathList;
再看賦值
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent, boolean isTrusted) {
super(parent);
this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted);
if (reporter != null) {
reportClassLoaderChain();
}
}
可以看到,是一個(gè)DexPathList的對(duì)象,而且是在BaseDexClassLoader的構(gòu)造函數(shù)里賦值的,而構(gòu)造函數(shù)中的形參dexPath,其實(shí)就是dex文件的路徑。dex文件是什么?就是開頭講的apk里面的我們寫的代碼。繼續(xù)看DexPathList的實(shí)現(xiàn):
public DexPathList(ClassLoader definingContext, String dexPath,
String librarySearchPath, File optimizedDirectory) {
this(definingContext, dexPath, librarySearchPath, optimizedDirectory, false);
}
DexPathList(ClassLoader definingContext, String dexPath,
String librarySearchPath, File optimizedDirectory, boolean isTrusted) {
···省略部分代碼···
this.definingContext = definingContext;
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
// save dexPath for BaseDexClassLoader
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions, definingContext, isTrusted);
···省略部分代碼···
}
通過makeDexElements,把dexPath路徑上的文件拆分,變成一個(gè)Element數(shù)組。
private Element[] dexElements;
也就是說,DexPathList類型的pathList對(duì)象里面,有一個(gè)dexElements數(shù)組,存放的就是我們dex文件里面的所有代碼的類。那再看回pathList的findClass方法:
//DexPathList類:
public Class<?> findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
Class<?> clazz = element.findClass(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
//Element類
public Class<?> findClass(String name, ClassLoader definingContext,
List<Throwable> suppressed) {
return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
: null;
}
遍歷Element數(shù)組,再根據(jù)Element對(duì)象內(nèi)的dexFile和loadClassBinaryName得到的Class,就可以用了,也就是說,我們new的對(duì)象,已經(jīng)成功了。
雙親委托的作用
第一、避免了重復(fù)加載類,交由父類先處理,可以知道是否被加載過。
第二、為了安全,因?yàn)楦鞣N各樣的對(duì)象都有類加載器來加載,有系統(tǒng)的,有我們自己的,而系統(tǒng)的必須優(yōu)先度高并且不可改,不然就會(huì)有安全性問題。所以父類加載完系統(tǒng)對(duì)象,就算我們?cè)诖a里自己寫一個(gè)去修改實(shí)現(xiàn),也是沒用。
APP熱修復(fù)
通過上面的知識(shí)點(diǎn),了解到了一個(gè)apk是怎么安裝并啟動(dòng)加載到ART虛擬機(jī)里面的了。既然類的加載,是一個(gè)Element數(shù)組的遍歷,而Element存放的又是dexFile。
那也就是說:如果APP某個(gè)類有bug,我們只需要修復(fù)這個(gè)類的bug,然后生成一個(gè)dex文件,用戶下載后,根據(jù)邏輯把這個(gè)dex文件放在Element數(shù)組的第一位,那么根據(jù)類加載的邏輯,修復(fù)后的類會(huì)先加載,而后面有bug的類,由于類名一樣,所以就不會(huì)再加載了,達(dá)到了問題被修復(fù),而不需要重新發(fā)包的目的。
簡單例子實(shí)現(xiàn)
增加入口,確保第一時(shí)間把這個(gè)新的類被加載器加載,不然由于同名的原因,如果另一個(gè)同名的類先加載了,那這個(gè)就無法修復(fù)了。
public class MyApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
//執(zhí)行熱修復(fù)。 插入補(bǔ)丁dex
Hotfix.installPatch(this,new File("/sdcard/bugFix.dex"));
}
}
根據(jù)版本,分別處理
public static void installPatch(Application application, File patch) {
//1、獲得classloader,PathClassLoader
ClassLoader classLoader = application.getClassLoader();
List<File> files = new ArrayList<>();
if (patch.exists()) {
files.add(patch);
}
File dexOptDir = application.getCacheDir();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
try {
NewClassLoaderInjector.inject(application, classLoader, files);
} catch (Throwable throwable) {
throwable.printStackTrace();
}
} else {
try {
//23 6.0及以上
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
V23.install(classLoader, files, dexOptDir);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
V19.install(classLoader, files, dexOptDir); //4.4以上
} else { // >= 14
V14.install(classLoader, files, dexOptDir);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
處理無非就是利用反射,找到BaseDexClassLoader中的pathList,然后找到pathList中的makePathElements方法,得到補(bǔ)丁創(chuàng)建的 Element[],最后合并2個(gè)Element數(shù)組并修改 classLoader中 pathList的 dexelements。
private static final class V23 {
private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
File optimizedDirectory)
throws IllegalArgumentException, IllegalAccessException,
NoSuchFieldException, InvocationTargetException, NoSuchMethodException,
IOException {
//找到 pathList
Field pathListField = ShareReflectUtil.findField(loader, "pathList");
Object dexPathList = pathListField.get(loader);
ArrayList<IOException> suppressedExceptions = new ArrayList<>();
// 從 pathList找到 makePathElements 方法并執(zhí)行
// 得到補(bǔ)丁創(chuàng)建的 Element[]
Object[] patchElements = makePathElements(dexPathList,
new ArrayList<>(additionalClassPathEntries), optimizedDirectory,
suppressedExceptions);
//將原本的 dexElements 與 makePathElements生成的數(shù)組合并
ShareReflectUtil.expandFieldArray(dexPathList, "dexElements", patchElements);
if (suppressedExceptions.size() > 0) {
for (IOException e : suppressedExceptions) {
Log.w(TAG, "Exception in makePathElement", e);
throw e;
}
}
}
/**
* 把dex轉(zhuǎn)化為Element數(shù)組
*/
private static Object[] makePathElements(
Object dexPathList, ArrayList<File> files, File optimizedDirectory,
ArrayList<IOException> suppressedExceptions)
throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
//通過閱讀android6、7、8、9源碼,都存在makePathElements方法
Method makePathElements = ShareReflectUtil.findMethod(dexPathList, "makePathElements",
List.class, File.class,
List.class);
return (Object[]) makePathElements.invoke(dexPathList, files, optimizedDirectory,
suppressedExceptions);
}
}
代碼太多,不全放了。
然后MainActivity寫點(diǎn)bug,為了方便,我新建了一個(gè)類來拋出bug。
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ExceptionBug.test();
}
}
public class ExceptionBug {
public static void test() {
throw new UnsupportedOperationException("this is a exception");
}
}
這樣就能保證出bug了。
修復(fù)
public class ExceptionBug {
public static void test() {
//throw new UnsupportedOperationException("this is a exception");
}
}
注釋掉異常的拋出。重新編譯項(xiàng)目,注意只是編譯項(xiàng)目,不是重新運(yùn)行到手機(jī)。找到這個(gè)類的class文件。編譯成dex文件。位置在app-build-intermediates-javac
編譯
找到工具:/Users/chenjy/Library/Android/sdk/build-tools/29.0.3

把這個(gè)添加到配置環(huán)境里面去。然后回到ExceptionBug.class文件所在的目錄。
然后輸入命令:dx --dex --output=bugFix.dex com/cjy/hotfixdemo/ExceptionBug.class
執(zhí)行命令后就會(huì)生成這個(gè)dex文件了,然后把文件放到MyApplication指定的位置那里,也就是/sdcard/bugFix.dex,放到sdcard里。
再重新打開,就會(huì)發(fā)現(xiàn),沒拋出異常了。
值得注意的問題
1、AndroidQ(10.0)以上,熱修復(fù)的dex文件,不要放到sdcard中,因?yàn)橥獠看鎯?chǔ)的訪問權(quán)限改了,只能看到自己的,所以應(yīng)該放到私有目錄下,或者application中加入android:requestLegacyExternalStorage="true"。
2、修復(fù)的代碼一定要先于bug代碼被加載,所以這里我直接在application中就調(diào)用了,如果先啟動(dòng)的是MAinActivity,已經(jīng)加載過了ExceptionBug這個(gè)類,之后跳轉(zhuǎn)到activity2,再去熱修復(fù)ExceptionBug類就不行了。
3、這個(gè)只是個(gè)簡單的例子,實(shí)際的熱修復(fù)沒這么簡單,這里只不過是通過熱修復(fù)的例子,來解釋實(shí)現(xiàn)類加載的流程。