簡要說一下熱修復(fù)的背景
當(dāng)我們發(fā)現(xiàn)有bug,然后需要去解決這些bug,這個時候又要進(jìn)行發(fā)包提醒用戶下載或者強制用戶更新這就很容易失去用戶,所以我們可以采用熱修復(fù) 插件化等技術(shù)在用戶毫無感覺的情況下更新。
我們首先說一下類加載他是怎么工作的了
...
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;
}
...
首先會檢查類是不是已經(jīng)被加載了,第一次進(jìn)來肯定沒有加載的這個時候就會走
/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;
}
...
接下來就會通過pathList.findClass(name, suppressedExceptions);
這個時候就讓外面來查看一下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)行實例化的接下來外面看一下
/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 里面查找的
接下來我們就來看看dexElements數(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);
}
...
接下來就解釋一下者四個參數(shù)
dexPath,指的是在Androdi包含類和資源的jar/apk類型的文件集合,指的是包含dex文件。如果有多個文件者用“:”分隔開,用代碼就是File.pathSeparator。
optimizedDirectory,指的是odex優(yōu)化文件存放的路徑,可以為null,那么就采用默認(rèn)的系統(tǒng)路徑即/data/dalvik-cache。
libraryPath,包含 C/C++ 庫的路徑集合,多個路徑用文件分隔符分隔分割,可以為null。
parent,parent類加載器
接下來我們繼續(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();
}
}
...
接下來
/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是怎么的來的
接下來我們看一下
來看看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ù):來截取字符串,就是多個dexpath之間用:分割,然后變成file,
被加進(jìn)去 List<File> result 里面
然后我們makeDexElements進(jìn)入這個方法里看是怎么生成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;
}
...
可以看到他首先會建一個數(shù)組以我們有多少文件來設(shè)置大小
private static final String DEX_SUFFIX = ".dex";
接者就開始循環(huán)遍歷我們可以看到他有兩種一種是以 if (name.endsWith(DEX_SUFFIX))以dex文件
其實不管是以dex為結(jié)尾還是不以dex為結(jié)尾他們都會走
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(這個參數(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);
}
...
我們點進(jìn)去發(fā)現(xiàn)他也是實例化new DexFile唯一的區(qū)別就是參數(shù)個數(shù)不一樣了
接下來我們看一下
看這兩個他們的調(diào)用時機是不一樣的當(dāng)我們有傳optimizedDirectory這個路徑時。
mCookie = openDexFile(sourceName, outputName, flags, loader, elements);
如果沒有就 mCookie = openDexFile(fileName, null, 0, loader, elements);可見其為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);
}
...
可見Native層里面會把我們傳進(jìn)去的文件進(jìn)行解壓然后就會將那些解析的類返回出來外面就保存到Element 數(shù)組里面去了。然后外面就可以找到了。
然后說一下PathClassLoader,DexClassLoader 的區(qū)別
PathClassLoader只能加載已安裝的apk下dex文件
DexClassLoader 可以加載dex/jar/apk/zip也可以從SD目錄下的加載)
為什么其實就是因為DexClassLoader 可以傳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ù)只有兩個使用可以清楚看到不能傳optimizedDirectory
現(xiàn)在我先來說說art和Dalvik虛擬機
Dalvik中的應(yīng)用每次運行時字節(jié)碼都需要通過jit編譯器編譯成機器碼(可以看成是及時編譯這樣會大大拉低效率)】
而art在系統(tǒng)安裝應(yīng)用程序的時候就進(jìn)行dex2oat預(yù)編譯,把多個dex合并為一個oat文件,供android設(shè)備使用
這樣子的就是將字節(jié)碼預(yù)先編譯成機器碼直接存放在本地,等需要用的時候就來取,這樣子就不會像dav那樣每次都要進(jìn)行編譯從而提高了效率。
接下來我來說說PathClassLoader是在什么時候被實例化了
說到這個我就簡單說一我們的應(yīng)用進(jìn)程都是由ActivityManagerService來請求Zygote來創(chuàng)建出來的,接著我們activity啟動通過ams最后回到我們ActivyThread類里面的
handleLaunchActivity那么我們就從這個方法開始說起。
然后回跳到performLaunchActivity
然后 Application app = r.packageInfo.makeApplication(false, mInstrumentation);
接著 getClassLoader();
...
public ClassLoader getClassLoader() {
synchronized (this) {
if (mClassLoader == null) {
createOrUpdateClassLoaderLocked(null /addedPaths/);
}
return mClassLoader;
}
}
...
接著會走著里面的
...
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);接著會走這里面的方法
}
com.android.internal.os.ClassLoaderFactory這個類中
*/
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);
}
...
看到這就會就行實例化完成了
對了還有一點我們的librarySearchPath, 這個存放so路徑是怎么來
原來在之前我們走了createOrUpdateClassLoaderLocked這個方法的時候會有
final List<String> libPaths = new ArrayList<>(10);
然后會走一個方法這個方法主要會把系統(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);通過這個就直接把我們剛剛保存到outLibPaths變成字符串以;分割開來。
接下來我將一下so的加載流程
...
System.loadLibrary("");
public static void loadLibrary(String libname) {
Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname);
}
...
java.lang.Runtime里面的loadLibrary0下
String filename = loader.findLibrary(libraryName);
會去我們的PathClassLoader可是里面沒有這個方法那么我們就去父類
/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
...
@Override
public String findLibrary(String name) {
return pathList.findLibrary(name);
}
...
又會走到
/libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
...
//libraryName “native-lib”
public String findLibrary(String libraryName) {
String fileName = System.mapLibraryName(libraryName);//如果之前加載過了 絕對路徑返回給你
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的絕對路徑,其中path實例化NativeLibraryElement最后其實是我們之前實例化PathClassLoader的時候路徑
這樣走完會放回我們
synchronized void loadLibrary0(ClassLoader loader, String libname) {
nativeLoad(filename, loader);走完這個我們java層的so文件就跟蹤完成了
}
所以我們要熱修復(fù)so只要比錯誤的so快加載就可以了我們可以傳我們so所放的地方的絕對路徑來
System.load();
而我們要修復(fù)代碼bug其實就很簡單
只要把我們從服務(wù)端獲下載下來正確的classes2.dex
然后通過DexClassLoader加載 然后我們把他和原來的dexElements進(jìn)行合并,其實就是插在原來的前面
因為我們加載類有一個機制(雙親委托模式)。只要找到該類就不會往下走了
代碼在https://github.com/yang1992yff/fixBug這是本人代碼地址