熱修復/熱更新技術原理

1 熱修復技術出現的背景

要說到熱修復的出現,主要還是因爲在中國Android應用沒有一個統一的應用商店,每個手機廠商基本都會定製自己的一款手機應用市場,不像國外的統一都使用Google Play。

中國用戶下載app都在不同的應用市場下載,沒有統一的應用市場導致的問題就是,如果應用出現bug需要緊急修復,按正常的發佈流程,需要從bug的修改、發佈應用到各個應用市場、提示用戶下載安裝修復這幾個流程。

在流程上看,有一些弊端:

  • 需要重新發布版本代價太大(可能只是修改小bug)

  • 用戶下載安裝覆蓋成本太高

  • bug修復不及時導致給用戶的體驗差

出現了以上問題,所以在中國熱修復技術就產生了。

2 熱修復技術簡介

什麼是熱修復?熱修復簡單來說就是一種補丁方案,我們只需要通過將補丁文件打包爲 .dex 推送給用戶,結合類加載機制在應用重啓的時候就可以修復好出現的bug。

優勢上是顯而易見的:

  • 無需重新發布版本,實時高效修復

  • 用戶無感知,無需下載安裝覆蓋應用,代價小

  • 修復成功率比較高

目前在市面上熱修復技術有阿里巴巴的 AndFixDexposed,騰訊QQ空間的超級補丁和微信的 Tinker 等。

這些熱修復技術框架的使用我不會在這裏講,而是要了解熱修復技術的底層實現原理。

3 插件化

在知道熱修復之前,你或許有聽說過插件化。那什麼是插件化?

3.1 什麼是插件化

如果你有關注一些比較大型的app,比如支付寶、美團等,可以發現app的一些入口是一個完全不同的功能,這些功能可能是主項目發佈後在後期動態加入進來的,而這種將其他apk集成到另一個apk的技術就是插件化。插件化的核心就是動態部署。

要注意的是,下面說的插件化方式也不是官方提供也不提倡的,和熱修復一樣在中國比較合適,要和Android提供的 Android App Bundles 區分。

3.2 插件化例子

現在我們實現一個功能:在我們的apk中通過插件化集成另一個apk,應用啓動的時候讓集成進來的apk生效(因爲是demo,所以我們直接就在項目中創建另一個apk)。

  • 創建一個插件名爲 plugin(注意這裏我們是選擇 Phone & Tablet Module 模擬一個插件apk),apk裏面寫一個類:

在這裏插入圖片描述

package com.example.plugin;

public class PluginClass {

    public String plugin() {
        return "I'm a plugin!";
    }
}

  • 生成apk,將這個apk放到主項目的 assets 目錄下(當然,實際的項目可能會從網絡下載或其他方式獲取插件apk):

在這裏插入圖片描述

  • 將apk加載進來:

那麼你可能會有疑問了:外部apk我們怎麼將它作爲插件加入到我們主項目中來呢?我們知道android的apk打包後都有一個或多個dex文件,而dex文件裏面是我們java文件的 .class 字節碼文件:

在這裏插入圖片描述

既然dex文件是我們一些 .class 字節碼文件,那麼同樣的也需要類加載器來加載,就是 DexClassLoader,我們可以通過它來獲取加載我們的apk中的那個 PluginClass 類。

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        File pluginApk = new File(getCacheDir() + "/plugin.apk");
        if (!pluginApk.exists()) {
            try(Source source = Okio.source(getAssets().open("plugin-debug.apk"));
                BufferedSink sink = Okio.buffer(Okio.sink(pluginApk))) {
                sink.writeAll(source);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        DexClassLoader dexClassLoader = new DexClassLoader(pluginApk.getPath(), getCacheDir().getPath(), null, null);
        String pluginPrint = "";
        try {
            Class<?> clazz = dexClassLoader.loadClass("com.example.plugin.PluginClass");
            Constructor<?> constructor = clazz.getDeclaredConstructor();
            Object pluginObj = constructor.newInstance();
            Method pluginMethod = clazz.getDeclaredMethod("plugin");
            pluginPrint = (String) pluginMethod.invoke(pluginObj);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }

        ((TextView) findViewById(R.id.tv_plugin_test)).setText(pluginPrint);
    }
}

上面的代碼非常簡單,就是讀取我們 assets 目錄的apk文件到本地,然後使用 DexClassLoader 將apk加載後使用反射調用。

3.3 新增界面、資源的插件

既然上面是通過反射才能夠拿到數據展示,那麼如果我的插件apk可能有界面Activity的怎麼辦?直接使用 Intent 跳轉?肯定是行不通的,運行時會提示清單文件沒有註冊Activity。那要怎麼做?可以提供代理Activity讓它轉發處理到插件apk的界面:

// 僞代碼
public class ProxyActivity extends AppCompatActivity {
	Object pluginActivity = DexClassLoader.loadClass("xxx");

	@Override
	protected void onCreate(@Nullable Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		pluginActivity.onCreate(savedInstanceState);
	}

	@Override
	protected void onStart() {
		super.onStart();
		pluginActivity.onStart();
	}
	...
}

不過上面的方式也會導致要寫很多的代理類。

還有另外一種方式就是通過欺騙系統,在系統檢查完清單文件發現Activity註冊後,在啓動清單文件註冊的Activity之前替換爲我們要啓動的Activity。不過這種方式說不準往後會被官方屏蔽也說不定,只使用於當前。

而新增在插件apk的資源又要怎麼獲取?需要重寫 getResources() 擴展:

@Override
public Resources getResources() {
	return new Resources(createAssetManager("插件apk本地目錄"), 
		super.getResources().getDisplayMetrics(), 
		super.getResources().getConfiguration());
}

private AssetManager createAssetManager(String dexPath) {
	try {
		// addAssetPath()是AssetManager的方法
		AssetManager am = AssetManager.class.newInstance();
		Method addAssetPath = am.getClass().getMethod("addAssetPath", String.class);
		addAssetPath.invoke(am, dexPath);
		return sm;
	} catch (Exception e) {
		e.printStackTrace();
	}
	return null;
}

4 熱修復技術

熱修復技術主要是處理我們上線發佈的版本需要緊急對bug進行修復,可能只是一些小改動,如果使用上面說的插件化的方式就不行了,插件化會替換掉所有的內容,這顯然不符合我們預期,我們只想要替換掉修復bug更改的一個或幾個java文件。

4.1 類加載機制

因爲熱修復技術需要了解類加載機制和反射相關原理和使用,所以首先還是需要有一個基本瞭解。在之前我寫了一篇文章可以作爲參考:

類加載機制和反射

4.2 PathClassLoader和DexClassLoader

在Android中主要涉及有幾個類加載器:

  • PathClassLoader

  • DexClassLoader

  • BaseDexClassLoader

PathClassLoaderDexClassLoader 都是 BaseDexClassLoader 的子類:

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);
	}
}

public class DexClassLoader extends BaseDexClassLoader {
	public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
		super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
	}
}

根據上面的源碼可以發現,PathClassLoaderDexClassLoader 的區別在於,DexClassLoader 多了一個 optimizedDirectory 參數,optimizedDirectory 可以是apk外部的文件目錄路徑。

這產生的具體區別就是,DexClassLoader 可用於加載指定路徑下的 .dex 文件,能支持動態插件化加載、熱修復;PathClassLoader 就只能用於加載已經安裝到系統中的apk文件中的 .dex 文件,不能從外部加載,也就不支持動態插件化加載、熱修復。

4.3 熱修復技術的原理

4.3.1 findClass()

上面分析了 PathClassLoaderDexClassLoader 的區別,發現它們都是 BaseDexClassLoader 的子類,具體的熱修復原理分析也是在這裏說明。

熱修復干預類加載機制是在 ClassLoader.findClass() 處理:

BaseDexClassLoader.java

public class BaseDexClassLoader {
	private DexPathList pathList;

	@Override
	protected Class<?> findClass(String name) throws ClassNotFoundException {
		...
		Class c = pathList.findClass(name, suppressedExceptions);
		...
	}
}

DexPathList.java

public class DexPathList {
	private Element[] dexElements;

	public Class<?> findClass(String name, List<Throwable> suppressed) {
		for (Element element : dexElements) {
			Class<?> clazz = element.findClass(name, definingContext, suppressed);
			if (clazz != null) {
				return clazz;
			}
			...
			return null;
		}
	}	
}

Element.java

public class Element {
	public Class<?> findClass(String name, ClassLoader definingContext, List<Throwable> suppressed) {
		return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed) : null;
	}
}


DexFile.java

public final  class DexFile {
	public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
		return defineClass(name, loader, mCookie, this, suppressed);
	}

	private static class defineClass(String name, ClassLoader loader, Object cookie, DexFile dexFile, List<Throwable> suppressed) {
		Class result = null;
		try {
			// native method
			result = defineClassNative(name, loader, cookie, dexFile);
		} catch (NoClassDefFoundError e) {
			...
		} catch (ClassNotFoundException e) {
			...
		}
		return result;
	}
}

上面的流程先簡單梳理一下:

BaseDexClassLoader.findClass() -> DexPathList.findClass() -> Element.findClass() -> DexFile.loadClassBinaryName() -> DexFile.defineClass()

但是有幾個點我們沒有搞明白:DexPathList 是什麼?dexElements 是什麼?dexFile 又是什麼?接下來一步步分析。

4.3.2 DexPathList、dexElement、dexFile

首先看下 DexPathList 是什麼時候被初始化的:

public class BaseDexClassLoader {
	private DexPathList pathList;

	public BaseDexClassLoader(String dexPath, File optimizedDirectory, String librarySearchPath, ClassLoader parent) {
		this(dexPath, librarySearchPath, parent, null, false);	
	}

	public BaseDexClassLoader(String dexPath, String librarySearchPath, ClassLoader parent, ClassLoader[] sharedLibraryLoaders, boolean isTrusted) {
		super(parent);
		...
		// DexPathList在ClassLoader創建的時候被初始化
		this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted);
		...
	}
}

可以發現,DexPathList 是在 BaseDexClassLoader 創建的時候初始化,進去 DexPathList

public class DexPathList {
	private Element[] dexElements;

	DexPathList(ClassLoader definingContext, String dexPath, String librarySearchPath, File optimizedDirectory, boolean isTrusted) {
		...
		this.definingContext = definingContext;
		// dexElements在DexPathList初始化的時候完成加載dex文件
		this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedException, definingContext, isTrusted);
	}

	private static List<File> splitDexPath(String path) {
		return splitPaths(path, false);
	}

	private static List<File> splitPaths(String searchPath, boolean directoryOnly) {
		List<File> result = new ArrayList<>();
		
		if (searchPath != null) {
			// File.pathSeparator在不同的系統是不一樣的
			// Windows系統:File.pathSeparator=";"
			// Unix、Linux系統:File.pathSeparator=":"
			// 這是系統環境變量Path分割拼接的路徑
			
			// 這裏的操作就是將我們的apk的dex文件目錄做拆分
			// new DexClassLoader(path, ...)
			// 也就是path可以是單個文件,也可以是一個目錄,根據不同的操作系統對目錄路徑拆分
			for (String path : searchPath.split(File.pathSeparator)) {
				if (directoryOnly) {
					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;
	}

	// 加載dex文件存儲到dexElements數組中
	private static Element[] makeDexElements(List<File> fiels, File optimizedDirectory, List<IOException> suppressedExceptions, ClassLoader loader, boolean isTrusted) {
		Element[] elements = new Element[files.size()];
		int elementsPos = 0;
		
		for (File file : files) {
			if (file.isDirectory()) {
				elements[elementsPos++] = new Element(file);
			} else if (file.isFile()) {
				String name = file.getName();

				DexFile dex = null;
				// DEX_SUFFIX = ".dex"
				// 如果是dex文件就加載,並且裝進Element存到dexElements
				if (name.endsWith(DEX_SUFFIX)) {
					try {
						dex = loadDexFile(file, optimizedDirectory, loader, elements);
						if (dex != null) {
							elements[elementsPos++] = new Element(dex, null);
						}
					} catch (IOException suppressed) {
						...
					}
				} else {
					try {
						// 不是dex文件同樣也處理加載
						dex = loadDexFile(file, optimizedDirectory, loader, elements);
					} catch (IOException suppressed) {
						...
					}

					// 無論是否爲dex文件都裝載進Element存到dexElements
					if (dex == null) {
						elements[elementsPos++] = new Element(file);
					} else {
						elements[elementsPos++] = new Element(dex, file);
					}
				}
			}
		}
		...
	}

	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);
		}
	}

	// 這個方法就是處理,如果不是dex文件,就幫你的文件加上後綴.dex
	private static String optimizedPathFor(File path, File optimizedDirectory) {
		String fileName = path.getName();
		if (!fileName.endsWith(DEX_SUFFIX)) {
			int lastDot = fileName.lastIndexOf(".");
			if (lastDot < 0) {
				fileName += DEX_SUFFIX;
			} else {
				StringBuilder sb = new StringBuilder(lastDot + 4);
				sb.append(fileName, 0, lastDot);
				sb.append(DEX_SUFFIX);
				fileName = sb.toString();
			}
		}
		return fileName;
	}
}

通過上面的源碼分析,dexElements 其實就是我們存放的 .dex 文件的數組,只不過將 .dex 封裝進Element。它同樣在ClassLoader創建出來的時候,將我們的文件通過 dexFile 加載出來。

4.4 分析干預類加載

經過上面的分析我們知道,既然主要的 .dex 加載完成的文件是存放在 dexElements 數組中,那麼它就是一個切入點。

有兩種方式可以干預:

  • 全量替換 dexElements 數組

  • 將我們修改的類文件先轉成dex文件,然後自己封裝放進Element,再將這個Element插入到 dexElements 數組前面

我們用一個例子來干預類加載達到熱修復:提供兩個按鈕,點擊 hotfix 按鈕替換 dexElements ,點擊 show text 按鈕顯示熱修復後的內容。

4.4.1 全量替換dexElement數組

// 未熱修復前的類
public class HotfixClass {

    public String hotfix() {
        return "I'm a original!";
    }
}

// 熱修復後的類
public class HotfixClass {

    public String hotfix() {
        return "I'm a hotfix!";
    }
}

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        final TextView tvText = findViewById(R.id.tv_text);
        Button btnShowText = findViewById(R.id.btn_show_text);
        Button btnHotFix = findViewById(R.id.btn_hotfix);

        btnShowText.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                HotfixClass hotfixClass = new HotfixClass();
                tvText.setText(hotfixClass.hotfix());
            }
        });

        btnHotFix.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                File hotfixFile = new File(getCacheDir() + "/hotfix.apk");
                try(Source source = Okio.source(getAssets().open("app-debug.apk"));
                    BufferedSink sink = Okio.buffer(Okio.sink(hotfixFile))) {
                    sink.writeAll(source);
                } catch (IOException e) {
                    e.printStackTrace();
                }

                try {
                    ClassLoader classLoader = getClassLoader();
                    Class<BaseDexClassLoader> loaderClass = BaseDexClassLoader.class;

                    // 獲取ClassLoader的DexPathList pathList成員變量
                    Field pathListFiled = loaderClass.getDeclaredField("pathList");
                    pathListFiled.setAccessible(true);
                    Object pathList = pathListFiled.get(classLoader);

                    // 獲取DexPathList的Element[] dexElements成員變量
                    Class<?> pathListClass = pathList.getClass();
                    Field dexElementsField = pathListClass.getDeclaredField("dexElements");
                    dexElementsField.setAccessible(true);

                    // 創建我們的類加載器,加載出我們熱修復需要的dexElements數組,替換舊的ClassLoader的dexElements
                    DexClassLoader hotfixClassLoader = new DexClassLoader(hotfixFile.getPath(), getCacheDir().getPath(), null, null);
                    Object newPathList = pathListFiled.get(hotfixClassLoader);
                    Object newDexElements = dexElementsField.get(newPathList);

                    dexElementsField.set(pathList, newDexElements);
                } catch (NoSuchFieldException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        });
    }
}

上面代碼操作步驟如下:

  • 通過反射獲取 BaseDexClassLoader 裏面的 pathList 成員變量

  • 再通過 pathList 反射獲取 dexElements 成員變量

  • 自己創建一個ClassLoader加載我們自己的補丁文件生成 dexElements

  • 替換舊的 dexElements

4.4.2 存在的問題

看起來沒問題也運行正常,但實際上有一些弊端:

  • 如果把程序kill掉,重新啓動apk後直接點擊 show text 熱更新失效了(因爲類加載機制這時候加載的是你沒修改的那個類)

  • 我爲了修復這個bug,替換了整個apk而不是隻替換要修復的 HotfixClass

  • 熱更新要生效,需要重啓apk(要讓熱更新生效只能如此)

4.4.2.1 解決程序kill掉後熱修復失效問題

第二個問題是因爲沒有及時的將我們熱修復的補丁文件加到 dexElements 中,可以將它提前到 Application.attachBaseContext() 執行:

public class HotfixApplication extends Application {

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        File hotfixFile = new File(getCacheDir() + "/hotfix.apk");
        if (!hotfixFile.exists()) {
            return;
        }
        
        try {
            ClassLoader classLoader = getClassLoader();
            Class<BaseDexClassLoader> loaderClass = BaseDexClassLoader.class;

            // 獲取ClassLoader的DexPathList pathList成員變量
            Field pathListFiled = loaderClass.getDeclaredField("pathList");
            pathListFiled.setAccessible(true);
            Object pathList = pathListFiled.get(classLoader);

            // 獲取DexPathList的Element[] dexElements成員變量
            Class<?> pathListClass = pathList.getClass();
            Field dexElementsField = pathListClass.getDeclaredField("dexElements");
            dexElementsField.setAccessible(true);

            // 創建我們的類加載器,加載出我們熱修復需要的dexElements數組,替換舊的ClassLoader的dexElements
            DexClassLoader hotfixClassLoader = new DexClassLoader(hotfixFile.getPath(), getCacheDir().getPath(), null, null);
            Object newPathList = pathListFiled.get(hotfixClassLoader);
            Object newDexElements = dexElementsField.get(newPathList);

            dexElementsField.set(pathList, newDexElements);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

4.4.2.2 解決替換整個apk而不是替換修復的類文件問題

第一個問題的解決方案就是我們只將我們要修改的類編譯爲 .dex 文件,然後插入到 dexElements 前面。

.class 文件編譯爲 .dex 需要使用到 d8 工具,這個工具存放在我們的sdk build-tools/xxx版本/d8

  • HotfixClass.java 使用命令 javac HotfixClass.java 編譯爲 HotfixClass.class

  • HotfixClass.class 轉成 .dex 文件,執行命令:

// Windows
d8.bat HotfixClass.class
// Unix、Linux
./d8 HotfixClass.class

輸出:classes.dex
  • 使用 classes.dex 作爲熱修復的補丁文件

因爲修改的是一個 .dex 文件,如果還是使用上面的方案去全量替換 dexElements 將會導致應用崩潰,因爲你是把整個apk替換稱這個只有一個補丁 .dex 文件。

所以也就需要第二種熱修復方案。

4.4.3 修改文件轉成dex文件封裝爲Element插入到dexElement數組前面

public class HotfixApplication extends Application {

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        File hotfixFile = new File(getCacheDir() + "/hotfix.dex");
        if (!hotfixFile.exists()) {
            return;
        }

        try {
            ClassLoader classLoader = getClassLoader();
            Class<BaseDexClassLoader> loaderClass = BaseDexClassLoader.class;

            // 獲取ClassLoader的DexPathList pathList成員變量
            Field pathListFiled = loaderClass.getDeclaredField("pathList");
            pathListFiled.setAccessible(true);
            Object pathList = pathListFiled.get(classLoader);

            // 獲取DexPathList的Element[] dexElements成員變量
            Class<?> pathListClass = pathList.getClass();
            Field dexElementsField = pathListClass.getDeclaredField("dexElements");
            dexElementsField.setAccessible(true);
            Object dexElements = dexElementsField.get(pathList);

            // 創建我們的類加載器,加載出我們熱修復需要的dexElements數組
            DexClassLoader hotfixClassLoader = new DexClassLoader(hotfixFile.getPath(), getCacheDir().getPath(), null, null);
            Object newPathList = pathListFiled.get(hotfixClassLoader);
            Object newDexElements = dexElementsField.get(newPathList);
            
            int oldLength = Array.getLength(dexElements);
            int newLength = Array.getLength(newDexElements);
            Object concatDexElements = Array.newInstance(dexElements.getClass().getComponentType(), oldLength + newLength);
            for (int i = 0; i < newLength; i++) {
                Array.set(concatDexElements, i, Array.get(newDexElements, i));
            }
            for (int i = 0; i < oldLength; i++) {
                Array.set(concatDexElements, newLength + i, Array.get(dexElements, i));
            }
            dexElementsField.set(pathList, concatDexElements);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

通過將一個或多個需要修復的類文件打包成 .dex 文件的方式,在實際的項目開發當中要使用到熱修復,我們就可以將我們要修改的補丁文件打成 .dex 文件,通過網絡的方式推給apk,讓apk在下次啓動的時候生效。

需要注意的是,我們自己熱修復的 .dex 文件封裝的Element要插入 dexElements 數組前面而不是後面。

首先第一個原因就是 dexElements 是順序遍歷循環的:

public class DexPathList {
	private Element[] dexElements;

	public Class<?> findClass(String name, List<Throwable> suppressed) {
		// 順序循環遍歷
		for (Element element : dexElements) {
			Class<?> clazz = element.findClass(name, definingContext, suppressed);
			if (clazz != null) {
				return clazz;
			}
			...
			return null;
		}
	}	
}

第二個原因是因爲類加載機制在首次加載到這個類後,下一次獲取會去查找緩存那個之前已加載過的類:

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
        // 第二次及往後查找緩存獲取c != null,直接就會返回
        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) {
                // 第一次加載會遍歷到dexElements
                c = findClass(name);
            }
        }
        return c;
}

5 總結

5.1 熱修復原理的簡單說明

簡單來說,熱修復的原理就是:

  • ClassLoader的dex文件替換

  • 直接修改字節碼

5.2 初始化流程和從dex文件查找類流程

初始化流程:

創建 BaseDexClassLoader -> 創建 DexPathList -> DexPathList.splitDexPath() 獲取dex文件 -> DexPathList.makeDexElements() 加載dex文件封裝爲Element存儲到 dexElements 數組

從dex文件查找類流程:

BaseDexClassLoader.findClass() -> DexPathList.findClass() -> 遍歷 dexElements 調用 Element.findClass() -> DexFile.loadClassBinaryName() -> DexFile.defineClass()

5.3 插件化和熱修復的區別

區別有兩點:

  • 插件化的內容在原App中沒有,而熱修復是在原App中的內容做改動

  • 插件化在代碼中有固定的入口,而熱修復則可能改變任何一個位置的代碼

發佈了199 篇原創文章 · 獲贊 7 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章