實(shí)現(xiàn)熱修復(fù)以及其原理

簡(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這是本人代碼地址

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評(píng)論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,556評(píng)論 3 418
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 176,463評(píng)論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 63,009評(píng)論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,778評(píng)論 6 410
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 55,218評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,436評(píng)論 0 288
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,969評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,795評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,993評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,229評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,659評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 35,917評(píng)論 1 286
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,687評(píng)論 3 392
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,990評(píng)論 2 374

推薦閱讀更多精彩內(nèi)容