簡(jiǎn)要說(shuō)一下熱修復(fù)的背景
當(dāng)我們發(fā)現(xiàn)有bug,然后需要去解決這些bug,這個(gè)時(shí)候又要進(jìn)行發(fā)包提醒用戶下載或者強(qiáng)制用戶更新這就很容易失去用戶,所以我們可以采用熱修復(fù) 插件化等技術(shù)在用戶毫無(wú)感覺(jué)的情況下更新。
我們首先說(shuō)一下類加載他是怎么工作的了
...
getClassLoader().loadClass(全類名徑)
/libcore/ojluni/src/main/java/java/lang/ClassLoader.java
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
// during the entire class loading process.
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 {
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;
}
...
首先會(huì)檢查類是不是已經(jīng)被加載了,第一次進(jìn)來(lái)肯定沒(méi)有加載的這個(gè)時(shí)候就會(huì)走
/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
里面的findClass(name);
...
@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;
}
...
接下來(lái)就會(huì)通過(guò)pathList.findClass(name, suppressedExceptions);
這個(gè)時(shí)候就讓外面來(lái)查看一下pathList究竟是什么
...
private final DexPathList pathList;
public BaseDexClassLoader(ByteBuffer[] dexFiles, ClassLoader parent) // TODO We should support giving this a library search path maybe.
super(parent);
this.pathList = new DexPathList(this, dexFiles);
}
...
看到是在構(gòu)造函數(shù)中進(jìn)行實(shí)例化的接下來(lái)外面看一下
/libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
...
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;
}
...
看到?jīng)]有其是在Element 里面查找的
接下來(lái)我們就來(lái)看看dexElements數(shù)組是什么時(shí)候賦值的
...
new PathClassLoader()
/libcore/dalvik/src/main/java/dalvik/system/PathClassLoader.java
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
到其父類/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
...
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
this(dexPath, optimizedDirectory, librarySearchPath, parent, false);
}
...
接下來(lái)就解釋一下者四個(gè)參數(shù)
dexPath,指的是在Androdi包含類和資源的jar/apk類型的文件集合,指的是包含dex文件。如果有多個(gè)文件者用“:”分隔開(kāi),用代碼就是File.pathSeparator。
optimizedDirectory,指的是odex優(yōu)化文件存放的路徑,可以為null,那么就采用默認(rèn)的系統(tǒng)路徑即/data/dalvik-cache。
libraryPath,包含 C/C++ 庫(kù)的路徑集合,多個(gè)路徑用文件分隔符分隔分割,可以為null。
parent,parent類加載器
接下來(lái)我們繼續(xù)看源碼
...
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();
}
}
...
接下來(lái)
/libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
...
DexPathList(ClassLoader definingContext, String dexPath,
String librarySearchPath, File optimizedDirectory, boolean isTrusted) {
...
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions, definingContext, isTrusted);
....
}
...
看到這里我們知道了之前dexElements是怎么的來(lái)的
接下來(lái)我們看一下
來(lái)看看splitDexPath到底做什么了。
...
private static List<File> splitPaths(String searchPath, boolean directoriesOnly) {
if (searchPath != null) {
for (String path : searchPath.split(File.pathSeparator)) {
if (directoriesOnly) {
try {
StructStat sb = Libcore.os.stat(path);
if (!S_ISDIR(sb.st_mode)) {
continue;
}
} catch (ErrnoException ignored) {
continue;
}
}
result.add(new File(path));
}
}
return result;
}
...
看到searchPath.split(File.pathSeparator)可以看出根據(jù):來(lái)截取字符串,就是多個(gè)dexpath之間用:分割,然后變成file,
被加進(jìn)去 List<File> result 里面
然后我們makeDexElements進(jìn)入這個(gè)方法里看是怎么生成dexElements
...
private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
List<IOException> suppressedExceptions, ClassLoader loader, boolean isTrusted) {
Element[] elements = new Element[files.size()];
int elementsPos = 0;
/*
* Open all files and load the (direct or contained) dex files up front.
/
for (File file : files) {
if (file.isDirectory()) {
// We support directories for looking up resources. Looking up resources in
// directories is useful for running libcore tests.
elements[elementsPos++] = new Element(file);
} else if (file.isFile()) {
String name = file.getName();
DexFile dex = null;
if (name.endsWith(DEX_SUFFIX)) {
// Raw dex file (not inside a zip/jar).
try {
dex = loadDexFile(file, optimizedDirectory, loader, elements);
if (dex != null) {
elements[elementsPos++] = new Element(dex, null);
}
} catch (IOException suppressed) {
System.logE("Unable to load dex file: " + file, suppressed);
suppressedExceptions.add(suppressed);
}
} else {
try {
dex = loadDexFile(file, optimizedDirectory, loader, elements);
} catch (IOException suppressed) {
/
* IOException might get thrown "legitimately" by the DexFile constructor if
* the zip file turns out to be resource-only (that is, no classes.dex file
* in it).
* Let dex == null and hang on to the exception to add to the tea-leaves for
* when findClass returns null.
/
suppressedExceptions.add(suppressed);
}
if (dex == null) {
elements[elementsPos++] = new Element(file);
} else {
elements[elementsPos++] = new Element(dex, file);
}
}
if (dex != null && isTrusted) {
dex.setTrusted();
}
} else {
System.logW("ClassLoader referenced unknown path: " + file);
}
}
if (elementsPos != elements.length) {
elements = Arrays.copyOf(elements, elementsPos);
}
return elements;
}
...
可以看到他首先會(huì)建一個(gè)數(shù)組以我們有多少文件來(lái)設(shè)置大小
private static final String DEX_SUFFIX = ".dex";
接者就開(kāi)始循環(huán)遍歷我們可以看到他有兩種一種是以 if (name.endsWith(DEX_SUFFIX))以dex文件
其實(shí)不管是以dex為結(jié)尾還是不以dex為結(jié)尾他們都會(huì)走
dex = loadDexFile(file, optimizedDirectory, loader, elements);
...
private static DexFile loadDexFile(File file, File optimizedDirectory, ClassLoader loader,
Element[] elements)
throws IOException {
if (optimizedDirectory == null) {
return new DexFile(file, loader, elements);
} else {
String optimizedPath = optimizedPathFor(file, optimizedDirectory);
return DexFile.loadDex(file.getPath(), optimizedPath, 0, loader, elements);
}
}
...
可以看出其是根據(jù)optimizedDirectory(這個(gè)參數(shù)的作用我在上面有做解釋)是否為null,
如果為null我們就直接new DexFile(file, loader, elements);否則DexFile.loadDex(file.getPath(), optimizedPath, 0, loader, elements);
/libcore/dalvik/src/main/java/dalvik/system/DexFile.java
...
static DexFile loadDex(String sourcePathName, String outputPathName,
int flags, ClassLoader loader, DexPathList.Element[] elements) throws IOException {
return new DexFile(sourcePathName, outputPathName, flags, loader, elements);
}
...
我們點(diǎn)進(jìn)去發(fā)現(xiàn)他也是實(shí)例化new DexFile唯一的區(qū)別就是參數(shù)個(gè)數(shù)不一樣了
接下來(lái)我們看一下
看這兩個(gè)他們的調(diào)用時(shí)機(jī)是不一樣的當(dāng)我們有傳optimizedDirectory這個(gè)路徑時(shí)。
mCookie = openDexFile(sourceName, outputName, flags, loader, elements);
如果沒(méi)有就 mCookie = openDexFile(fileName, null, 0, loader, elements);可見(jiàn)其為null
...
private static Object openDexFile(String sourceName, String outputName, int flags,
ClassLoader loader, DexPathList.Element[] elements) throws IOException {
// Use absolute paths to enable the use of relative paths when testing on host.
return openDexFileNative(new File(sourceName).getAbsolutePath(),
(outputName == null)
? null
: new File(outputName).getAbsolutePath(),
flags,
loader,
elements);
}
...
可見(jiàn)Native層里面會(huì)把我們傳進(jìn)去的文件進(jìn)行解壓然后就會(huì)將那些解析的類返回出來(lái)外面就保存到Element 數(shù)組里面去了。然后外面就可以找到了。
然后說(shuō)一下PathClassLoader,DexClassLoader 的區(qū)別
PathClassLoader只能加載已安裝的apk下dex文件
DexClassLoader 可以加載dex/jar/apk/zip也可以從SD目錄下的加載)
為什么其實(shí)就是因?yàn)镈exClassLoader 可以傳optimizedDirectory(指的是odex優(yōu)化文件存放的路徑)路徑
帶那么看一下源碼
/libcore/dalvik/src/main/java/dalvik/system/PathClassLoader.java
...
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
...
他的構(gòu)造函數(shù)只有兩個(gè)使用可以清楚看到不能傳optimizedDirectory
現(xiàn)在我先來(lái)說(shuō)說(shuō)art和Dalvik虛擬機(jī)
Dalvik中的應(yīng)用每次運(yùn)行時(shí)字節(jié)碼都需要通過(guò)jit編譯器編譯成機(jī)器碼(可以看成是及時(shí)編譯這樣會(huì)大大拉低效率)】
而art在系統(tǒng)安裝應(yīng)用程序的時(shí)候就進(jìn)行dex2oat預(yù)編譯,把多個(gè)dex合并為一個(gè)oat文件,供android設(shè)備使用
這樣子的就是將字節(jié)碼預(yù)先編譯成機(jī)器碼直接存放在本地,等需要用的時(shí)候就來(lái)取,這樣子就不會(huì)像dav那樣每次都要進(jìn)行編譯從而提高了效率。
接下來(lái)我來(lái)說(shuō)說(shuō)PathClassLoader是在什么時(shí)候被實(shí)例化了
說(shuō)到這個(gè)我就簡(jiǎn)單說(shuō)一我們的應(yīng)用進(jìn)程都是由ActivityManagerService來(lái)請(qǐng)求Zygote來(lái)創(chuàng)建出來(lái)的,接著我們activity啟動(dòng)通過(guò)ams最后回到我們ActivyThread類里面的
handleLaunchActivity那么我們就從這個(gè)方法開(kāi)始說(shuō)起。
然后回跳到performLaunchActivity
然后 Application app = r.packageInfo.makeApplication(false, mInstrumentation);
接著 getClassLoader();
...
public ClassLoader getClassLoader() {
synchronized (this) {
if (mClassLoader == null) {
createOrUpdateClassLoaderLocked(null /addedPaths/);
}
return mClassLoader;
}
}
...
接著會(huì)走著里面的
...
createOrUpdateClassLoaderLocked(null /addedPaths/);{
if (!mIncludeCode) {
if (mClassLoader == null) {
StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
mClassLoader = ApplicationLoaders.getDefault().getClassLoader(
"" / codePath /, mApplicationInfo.targetSdkVersion, isBundledApp,
librarySearchPath, libraryPermittedPath, mBaseClassLoader,
null / classLoaderName */);
StrictMode.setThreadPolicy(oldPolicy);
mAppComponentFactory = AppComponentFactory.DEFAULT;
}
}
getClassLoader(){
ClassLoaderFactory.createClassLoader(
zip, null, parent, classLoaderName);接著會(huì)走這里面的方法
}
com.android.internal.os.ClassLoaderFactory這個(gè)類中
*/
public static ClassLoader createClassLoader(String dexPath,
String librarySearchPath, ClassLoader parent, String classloaderName) {
if (isPathClassLoaderName(classloaderName)) {
return new PathClassLoader(dexPath, librarySearchPath, parent);
} else if (isDelegateLastClassLoaderName(classloaderName)) {
return new DelegateLastClassLoader(dexPath, librarySearchPath, parent);
}
throw new AssertionError("Invalid classLoaderName: " + classloaderName);
}
...
看到這就會(huì)就行實(shí)例化完成了
對(duì)了還有一點(diǎn)我們的librarySearchPath, 這個(gè)存放so路徑是怎么來(lái)
原來(lái)在之前我們走了createOrUpdateClassLoaderLocked這個(gè)方法的時(shí)候會(huì)有
final List<String> libPaths = new ArrayList<>(10);
然后會(huì)走一個(gè)方法這個(gè)方法主要會(huì)把系統(tǒng)的環(huán)境路徑以及apk的安裝目錄下data/app/package/lib/armbeaiv7a存放到outLibPaths下
...
public static void makePaths(ActivityThread activityThread,
boolean isBundledApp,
ApplicationInfo aInfo,
List<String> outZipPaths,
List<String> outLibPaths) {
if (aInfo.primaryCpuAbi != null) {
// Add fake libs into the library search path if we target prior to N.
if (aInfo.targetSdkVersion < Build.VERSION_CODES.N) {
outLibPaths.add("/system/fake-libs" +
(VMRuntime.is64BitAbi(aInfo.primaryCpuAbi) ? "64" : ""));
}
for (String apk : outZipPaths) {
outLibPaths.add(apk + "!/lib/" + aInfo.primaryCpuAbi);
}
}
}
...
// final String librarySearchPath = TextUtils.join(File.pathSeparator, libPaths);通過(guò)這個(gè)就直接把我們剛剛保存到outLibPaths變成字符串以;分割開(kāi)來(lái)。
接下來(lái)我將一下so的加載流程
...
System.loadLibrary("");
public static void loadLibrary(String libname) {
Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname);
}
...
java.lang.Runtime里面的loadLibrary0下
String filename = loader.findLibrary(libraryName);
會(huì)去我們的PathClassLoader可是里面沒(méi)有這個(gè)方法那么我們就去父類
/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
...
@Override
public String findLibrary(String name) {
return pathList.findLibrary(name);
}
...
又會(huì)走到
/libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
...
//libraryName “native-lib”
public String findLibrary(String libraryName) {
String fileName = System.mapLibraryName(libraryName);//如果之前加載過(guò)了 絕對(duì)路徑返回給你
for (NativeLibraryElement element : nativeLibraryPathElements) {
String path = element.findNativeLibrary(fileName);
if (path != null) {
return path;
}
}
return null;
}
public String findNativeLibrary(String name) {
maybeInit();
if (zipDir == null) {
String entryPath = new File(path, name).getPath();
if (IoUtils.canOpenReadOnly(entryPath)) {
return entryPath;
}
} else if (urlHandler != null) {
// Having a urlHandler means the element has a zip file.
// In this case Android supports loading the library iff
// it is stored in the zip uncompressed.
String entryName = zipDir + '/' + name;
if (urlHandler.isEntryStored(entryName)) {
return path.getPath() + zipSeparator + entryName;
}
}
return null;
}
...
主要是為了返回so的絕對(duì)路徑,其中path實(shí)例化NativeLibraryElement最后其實(shí)是我們之前實(shí)例化PathClassLoader的時(shí)候路徑
這樣走完會(huì)放回我們
synchronized void loadLibrary0(ClassLoader loader, String libname) {
nativeLoad(filename, loader);走完這個(gè)我們java層的so文件就跟蹤完成了
}
所以我們要熱修復(fù)so只要比錯(cuò)誤的so快加載就可以了我們可以傳我們so所放的地方的絕對(duì)路徑來(lái)
System.load();
而我們要修復(fù)代碼bug其實(shí)就很簡(jiǎn)單
只要把我們從服務(wù)端獲下載下來(lái)正確的classes2.dex
然后通過(guò)DexClassLoader加載 然后我們把他和原來(lái)的dexElements進(jìn)行合并,其實(shí)就是插在原來(lái)的前面
因?yàn)槲覀兗虞d類有一個(gè)機(jī)制(雙親委托模式)。只要找到該類就不會(huì)往下走了
代碼在https://github.com/yang1992yff/fixBug這是本人代碼地址