Android Studio 2.0開始支持 Instant Run 特性锋拖, 使得在開發(fā)過程中能快速將代碼變化更新到設備上韩肝。之前触菜,更新代碼之后需要先編譯一個完整的新Apk,卸載設備上已安裝的這個 Apk (若有)哀峻,再 push 到設備安裝玫氢,再啟動。有了 Instant Run 特性之后谜诫,只需要 push 一些增量到設備上漾峡,直接執(zhí)行,可以為開發(fā)人員節(jié)省大量時間喻旷。當然 Instant Run 特征只在 debug 時有效生逸,對發(fā)布 release 版沒有任何影響。
對于InstantRun不了解的同學可以去查看官方文檔
Instant Run 通過 hot swap且预, warm swap槽袄, code swap 三種 swap 來實現(xiàn)。Android Studio 會根據(jù)代碼的改變自動決定 push 哪種 swap 到設備上锋谐,并根據(jù)不同的 swap 執(zhí)行不同的行為遍尺。
代碼改變內(nèi)容 | Instant Run 行為 |
---|---|
修改一個實例方法或者一個靜態(tài)方法的實現(xiàn) | hot swap: 這是最快的情況,下次調(diào)用該方法時直接使用更新后的方法 |
修改或者刪除一個資源 | warm swap: App 保護運行狀態(tài)涮拗,但是會自動重啟 activity乾戏, 所以屏幕會閃一下 |
增加迂苛、刪除或修改((1)注解 (2)成員變量/靜態(tài)變量/方法簽名)修改類的繼承關系、實現(xiàn)的接口修改類的靜態(tài)代碼塊利用動態(tài)資源ID改變資源順序 | cold swap(Api level >= 21): 需要重啟App若Api level < 21鼓择, 則需要重新編譯整個app |
修改 AndroidManifest.xml修改被 AndroidManifest.xml 引用的資源修改 widget UI | 需要重新編譯整個App |
接下來我們就以一個簡單的例子來介紹InstantRun的原理三幻。
1 運行demo
首先我們來創(chuàng)建一個簡單的demo,demo很簡單呐能,只有一個activity念搬,activity中有一個button:
public class MainActivity extends AppCompatActivity implements View.OnClickListener{
private Button btn;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
this.btn = (Button) findViewById(R.id.btn);
this.btn.setOnClickListener(this);
}
@Override
public void onClick(View view) {
Toast.makeText(this, "click", Toast.LENGTH_SHORT).show();
}
}
運行該demo,效果很簡單就不截圖了摆出。重點來看下其apk文件:將打包出來的apk解壓后結構如下:
首先我們把classes.dex和classes2.dex反編譯出來看看(反編譯軟件推薦使用dex2jar朗徊,相關使用方式可以參考:user guide):
-
classes.dex:
-
classes2.dex
可以看到,兩個dex文件完全沒有包含任何工程代碼偎漫,看上去全部都是無關代碼爷恳。其實這些代碼都是instant-run.jar中的代碼,也就是說InstantRun工程在進行apk打包的時候?qū)ntant-run.jar包打入到了apk中骑丸。但問題是,我們的代碼(也就是上文中MainActivity
)去哪兒了妒貌?
其實用戶代碼都被寫入到apk文件中的instant-run.zip中去了通危,將instant-run.zip解壓后可以看到:
可以看到在這個路徑下還有很多dex文件,而我們的代碼就被放在slice_9-classes.dex中灌曙,至于instant-run.zip中的打包/分包邏輯菊碟,為啥用戶代碼會被打入到 slice_9-classes.dex
中我還不是太清楚,知道的同學可以給我留言:
可以看到在刺,在用戶代碼的每一個函數(shù)中都被插入了這樣一段代碼:
IncrementalChange localIncrementalChange = $change;
if (localIncrementalChange != null)
{
localIncrementalChange.access$dispatch("onClick.(Landroid/view/View;)V", new Object[] { this, paramView });
return;
}
上述代碼通過判斷$change
變量來執(zhí)行不同的邏輯逆害。這也是InstantRun
中hot swap
的實現(xiàn)原理,通過插樁的方式在每一個函數(shù)中植入$change
變量及其相關邏輯蚣驼,當相關代碼被修改時魄幕,利用反射的方式將$change
重置,從而執(zhí)行修改后的邏輯已達到熱修復的目的颖杏。
另外我們再來看下AndroidManifest.xml文件:
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.alibaba.sdk.instandemo" platformBuildVersionCode="25" platformBuildVersionName="7.1.1">
<meta-data android:name="android.support.VERSION" android:value="25.3.1"/>
<application android:allowBackup="true" android:debuggable="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:name="com.android.tools.fd.runtime.BootstrapApplication" android:supportsRtl="true" android:theme="@style/AppTheme">
<activity android:name="com.alibaba.sdk.instandemo.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
可以看到纯陨,工程中的Application被篡改成了com.android.tools.fd.runtime.BootstrapApplication
,這個類輸入intents-run.jar包,不難猜測留储,Application的初始化過程也被instant-run所代理了翼抠。
看到這里,一個instant-run功能的大致結構基本就清晰了:
InstantRun
工程實際上是一個宿主工程获讳,用戶代碼以資源的形式放入到apk中InstantRun
工程通過com.android.tools.fd.runtime.BootstrapApplication
代理app的初始化過程阴颖,猜測在初始化的過程中,com.android.tools.fd.runtime.BootstrapApplication
會去加載用戶代碼-
InstantRun
在編譯代碼時會通過插樁的方式給每一個函數(shù)植入一段代碼,從而在需要時hook?
2 工程架構
2.1 編譯
通過第一節(jié)我們知道丐膝,InstantRun
會在每個函數(shù)中植入一段代碼量愧,達到插樁的效果钾菊。InstantRun
通過Gradle Transform API,在代碼完成之后,被轉(zhuǎn)換成dex之前將相應邏輯插入侠畔。InstantRun
使用ASM
完成插樁结缚。
2.2 運行時
看完了編譯階段,接下來看下運行時階段相關原理软棺。由于com.android.tools.fd.runtime.BootstrapApplication
代理了整個應用的初始化工作红竭,從而成為了整個應用的入口员凝。我們就從com.android.tools.fd.runtime.BootstrapApplication
開始入手拟杉。
2.2.1attachBaseContext
首先來看下attachBaseContext
:
protected void attachBaseContext(Context context) {
if (!AppInfo.usingApkSplits) {
String apkFile = context.getApplicationInfo().sourceDir;
long apkModified = apkFile != null ? new File(apkFile).lastModified() : 0L;
createResources(apkModified);
setupClassLoaders(context, context.getCacheDir().getPath(), apkModified);
}
createRealApplication();
super.attachBaseContext(context);
if (this.realApplication != null) {
try {
Method attachBaseContext = ContextWrapper.class.getDeclaredMethod("attachBaseContext", new Class[] { Context.class });
attachBaseContext.setAccessible(true);
attachBaseContext.invoke(this.realApplication, new Object[] { context });
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
}
我們依次需要關注的方法有:
createResources → setupClassLoaders → createRealApplication → 調(diào)用realApplication的attachBaseContext
2.2.1.1 createResources
private void createResources(long apkModified) {
FileManager.checkInbox();
File file = FileManager.getExternalResourceFile();
this.externalResourcePath = (file != null ? file.getPath() : null);
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Resource override is " + this.externalResourcePath);
}
if (file != null) {
try {
long resourceModified = file.lastModified();
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Resource patch last modified: " + resourceModified);
Log.v("InstantRun", "APK last modified: " + apkModified
+ " "
+ (apkModified > resourceModified ? ">" : "<")
+ " resource patch");
}
if ((apkModified == 0L) || (resourceModified <= apkModified)) {
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Ignoring resource file, older than APK");
}
this.externalResourcePath = null;
}
} catch (Throwable t) {
Log.e("InstantRun", "Failed to check patch timestamps", t);
}
}
}
該方法主要是判斷資源resource.ap_是否改變鹅龄,然后保存resource.ap_的路徑到externalResourcePath中慎王。
2.2.1.2 setupClassLoaders
private static void setupClassLoaders(Context context, String codeCacheDir, long apkModified) {
List dexList = FileManager.getDexList(context, apkModified);
Class server = Server.class;
Class patcher = MonkeyPatcher.class;
if (!dexList.isEmpty()) {
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Bootstrapping class loader with dex list " + join('\n', dexList));
}
ClassLoader classLoader = BootstrapApplication.class.getClassLoader();
String nativeLibraryPath;
try {
nativeLibraryPath = (String) classLoader.getClass().getMethod("getLdLibraryPath", new Class[0]).invoke(classLoader, new Object[0]);
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Native library path: " + nativeLibraryPath);
}
} catch (Throwable t) {
Log.e("InstantRun", "Failed to determine native library path " + t.getMessage());
nativeLibraryPath = FileManager.getNativeLibraryFolder().getPath();
}
IncrementalClassLoader.inject(classLoader, nativeLibraryPath, codeCacheDir, dexList);
}
}
該方法將用戶代碼dexList注入到一個自定義ClassLoader實例中接谨,并將該classloader設置為默認class loader:BootstrapApplication.class.getClassLoader()
的父loader被廓。IncrementalClassLoader
源碼如下:
public class IncrementalClassLoader extends ClassLoader {
public static final boolean DEBUG_CLASS_LOADING = false;
private final DelegateClassLoader delegateClassLoader;
public IncrementalClassLoader(ClassLoader original, String nativeLibraryPath, String codeCacheDir, List dexes) {
super(original.getParent());
this.delegateClassLoader = createDelegateClassLoader(nativeLibraryPath, codeCacheDir, dexes, original);
}
public Class findClass(String className) throws ClassNotFoundException {
try {
return this.delegateClassLoader.findClass(className);
} catch (ClassNotFoundException e) {
throw e;
}
}
private static class DelegateClassLoader extends BaseDexClassLoader {
private DelegateClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) {
super(dexPath, optimizedDirectory, libraryPath, parent);
}
public Class findClass(String name) throws ClassNotFoundException {
try {
return super.findClass(name);
} catch (ClassNotFoundException e) {
throw e;
}
}
}
private static DelegateClassLoader createDelegateClassLoader(String nativeLibraryPath, String codeCacheDir, List dexes,
ClassLoader original) {
String pathBuilder = createDexPath(dexes);
return new DelegateClassLoader(pathBuilder, new File(codeCacheDir), nativeLibraryPath, original);
}
private static String createDexPath(List dexes) {
StringBuilder pathBuilder = new StringBuilder();
boolean first = true;
for (String dex : dexes) {
if (first) {
first = false;
} else {
pathBuilder.append(File.pathSeparator);
}
pathBuilder.append(dex);
}
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Incremental dex path is " + BootstrapApplication.join('\n', dexes));
}
return pathBuilder.toString();
}
private static void setParent(ClassLoader classLoader, ClassLoader newParent) {
try {
Field parent = ClassLoader.class.getDeclaredField("parent");
parent.setAccessible(true);
parent.set(classLoader, newParent);
} catch (IllegalArgumentException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
public static ClassLoader inject(ClassLoader classLoader,
String nativeLibraryPath, String codeCacheDir, List dexes) {
IncrementalClassLoader incrementalClassLoader = new IncrementalClassLoader(classLoader, nativeLibraryPath, codeCacheDir, dexes);
setParent(classLoader, incrementalClassLoader);
return incrementalClassLoader;
}
}
上述代碼總過做了兩件事:
- 將一個DelegateClassLoader設置為系統(tǒng)ClassLoader的父loader
- 將用戶代碼dex文件路徑設置為該classloader加載路徑
由于ClassLoader的加載采用雙親委托模式胧洒,所以當需要加載用戶代碼時肌厨,系統(tǒng)classloader會首先找到BootstrapApplication.class.getClassLoader()
,而BootstrapApplication.class.getClassLoader()
又會委托其父loader也即我們創(chuàng)建的DelegateClassLoader
實例赌朋,該實例會負責完成用戶代碼的加載凰狞。
2.2.1.3 createRealApplication
private void createRealApplication() {
if (AppInfo.applicationClass != null) {
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "About to create real application of class name = " + AppInfo.applicationClass);
}
try {
Class realClass = (Class) Class.forName(AppInfo.applicationClass);
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Created delegate app class successfully : "
+ realClass + " with class loader "
+ realClass.getClassLoader());
}
Constructor constructor = realClass.getConstructor(new Class[0]);
this.realApplication = ((Application) constructor.newInstance(new Object[0]));
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Created real app instance successfully :" + this.realApplication);
}
} catch (Exception e) {
throw new IllegalStateException(e);
}
} else {
this.realApplication = new Application();
}
}
createRealApplication
的目的是創(chuàng)建真實的Application實例。真實的Application保存在AppInfo
中沛慢,如果用戶自定義了Application赡若,則直接創(chuàng)建該Application實例;否則則創(chuàng)建系統(tǒng)默認的Application實例团甲。
2.2.2 onCreate
接下來我們再來看下com.android.tools.fd.runtime.BootstrapApplication
的onCreate
方法:
public void onCreate() {
if (!AppInfo.usingApkSplits) {
MonkeyPatcher.monkeyPatchApplication(this, this, this.realApplication, this.externalResourcePath);
MonkeyPatcher.monkeyPatchExistingResources(this, this.externalResourcePath, null);
} else {
MonkeyPatcher.monkeyPatchApplication(this, this, this.realApplication, null);
}
super.onCreate();
if (AppInfo.applicationId != null) {
try {
boolean foundPackage = false;
int pid = Process.myPid();
ActivityManager manager = (ActivityManager) getSystemService("activity");
List processes = manager.getRunningAppProcesses();
boolean startServer = false;
if ((processes != null) && (processes.size() > 1)) {
for (ActivityManager.RunningAppProcessInfo processInfo : processes) {
if (AppInfo.applicationId.equals(processInfo.processName)) {
foundPackage = true;
if (processInfo.pid == pid) {
startServer = true;
break;
}
}
}
if ((!startServer) && (!foundPackage)) {
startServer = true;
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Multiprocess but didn't find process with package: starting server anyway");
}
}
} else {
startServer = true;
}
if (startServer) {
Server.create(AppInfo.applicationId, this);
}
} catch (Throwable t) {
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Failed during multi process check", t);
}
Server.create(AppInfo.applicationId, this);
}
}
if (this.realApplication != null) {
this.realApplication.onCreate();
}
}
在onCreate()中我們需要注意以下方法:
monkeyPatchApplication → monkeyPatchExistingResources → Server啟動 → 調(diào)用realApplication的onCreate方法逾冬。
2.2.2.1 monkeyPatchApplication
public static void monkeyPatchApplication(Context context, Application bootstrap, Application realApplication, String externalResourceFile) {
try {
Class activityThread = Class.forName("android.app.ActivityThread");
Object currentActivityThread = getActivityThread(context, activityThread);
Field mInitialApplication = activityThread.getDeclaredField("mInitialApplication");
mInitialApplication.setAccessible(true);
Application initialApplication = (Application) mInitialApplication.get(currentActivityThread);
if ((realApplication != null) && (initialApplication == bootstrap)) {
mInitialApplication.set(currentActivityThread, realApplication);
}
if (realApplication != null) {
Field mAllApplications = activityThread.getDeclaredField("mAllApplications");
mAllApplications.setAccessible(true);
List allApplications = (List) mAllApplications.get(currentActivityThread);
for (int i = 0; i < allApplications.size(); i++) {
if (allApplications.get(i) == bootstrap) {
allApplications.set(i, realApplication);
}
}
}
Class loadedApkClass;
try {
loadedApkClass = Class.forName("android.app.LoadedApk");
} catch (ClassNotFoundException e) {
loadedApkClass = Class.forName("android.app.ActivityThread$PackageInfo");
}
Field mApplication = loadedApkClass.getDeclaredField("mApplication");
mApplication.setAccessible(true);
Field mResDir = loadedApkClass.getDeclaredField("mResDir");
mResDir.setAccessible(true);
Field mLoadedApk = null;
try {
mLoadedApk = Application.class.getDeclaredField("mLoadedApk");
} catch (NoSuchFieldException e) {
}
for (String fieldName : new String[] { "mPackages", "mResourcePackages" }) {
Field field = activityThread.getDeclaredField(fieldName);
field.setAccessible(true);
Object value = field.get(currentActivityThread);
for (Map.Entry> entry : ((Map>) value).entrySet()) {
Object loadedApk = ((WeakReference) entry.getValue()).get();
if (loadedApk != null) {
if (mApplication.get(loadedApk) == bootstrap) {
if (realApplication != null) {
mApplication.set(loadedApk, realApplication);
}
if (externalResourceFile != null) {
mResDir.set(loadedApk, externalResourceFile);
}
if ((realApplication != null) && (mLoadedApk != null)) {
mLoadedApk.set(realApplication, loadedApk);
}
}
}
}
}
} catch (Throwable e) {
throw new IllegalStateException(e);
}
}
該方法將當前所有app的application替換為realApplication:
- 替換ActivityThread的mInitialApplication為realApplication
- 替換mAllApplications 中所有的Application為realApplication
- 替換ActivityThread的mPackages,mResourcePackages中的mLoaderApk中的application為realApplication
2.2.2.2 monkeyPatchExistingResources
public static void monkeyPatchExistingResources(Context context, String externalResourceFile, Collection activities) {
if (externalResourceFile == null) {
return;
}
try {
AssetManager newAssetManager = (AssetManager) AssetManager.class.getConstructor(new Class[0]).newInstance(new Object[0]);
Method mAddAssetPath = AssetManager.class.getDeclaredMethod(
"addAssetPath", new Class[] { String.class });
mAddAssetPath.setAccessible(true);
if (((Integer) mAddAssetPath.invoke(newAssetManager, new Object[] { externalResourceFile })).intValue() == 0) {
throw new IllegalStateException(
"Could not create new AssetManager");
}
Method mEnsureStringBlocks = AssetManager.class.getDeclaredMethod("ensureStringBlocks", new Class[0]);
mEnsureStringBlocks.setAccessible(true);
mEnsureStringBlocks.invoke(newAssetManager, new Object[0]);
if (activities != null) {
for (Activity activity : activities) {
Resources resources = activity.getResources();
try {
Field mAssets = Resources.class.getDeclaredField("mAssets");
mAssets.setAccessible(true);
mAssets.set(resources, newAssetManager);
} catch (Throwable ignore) {
Field mResourcesImpl = Resources.class.getDeclaredField("mResourcesImpl");
mResourcesImpl.setAccessible(true);
Object resourceImpl = mResourcesImpl.get(resources);
Field implAssets = resourceImpl.getClass().getDeclaredField("mAssets");
implAssets.setAccessible(true);
implAssets.set(resourceImpl, newAssetManager);
}
Resources.Theme theme = activity.getTheme();
try {
try {
Field ma = Resources.Theme.class.getDeclaredField("mAssets");
ma.setAccessible(true);
ma.set(theme, newAssetManager);
} catch (NoSuchFieldException ignore) {
Field themeField = Resources.Theme.class.getDeclaredField("mThemeImpl");
themeField.setAccessible(true);
Object impl = themeField.get(theme);
Field ma = impl.getClass().getDeclaredField("mAssets");
ma.setAccessible(true);
ma.set(impl, newAssetManager);
}
Field mt = ContextThemeWrapper.class.getDeclaredField("mTheme");
mt.setAccessible(true);
mt.set(activity, null);
Method mtm = ContextThemeWrapper.class.getDeclaredMethod("initializeTheme", new Class[0]);
mtm.setAccessible(true);
mtm.invoke(activity, new Object[0]);
Method mCreateTheme = AssetManager.class.getDeclaredMethod("createTheme", new Class[0]);
mCreateTheme.setAccessible(true);
Object internalTheme = mCreateTheme.invoke(newAssetManager, new Object[0]);
Field mTheme = Resources.Theme.class.getDeclaredField("mTheme");
mTheme.setAccessible(true);
mTheme.set(theme, internalTheme);
} catch (Throwable e) {
Log.e("InstantRun", "Failed to update existing theme for activity " + activity, e);
}
pruneResourceCaches(resources);
}
}
Collection> references;
if (Build.VERSION.SDK_INT >= 19) {
Class resourcesManagerClass = Class.forName("android.app.ResourcesManager");
Method mGetInstance = resourcesManagerClass.getDeclaredMethod("getInstance", new Class[0]);
mGetInstance.setAccessible(true);
Object resourcesManager = mGetInstance.invoke(null, new Object[0]);
try {
Field fMActiveResources = resourcesManagerClass.getDeclaredField("mActiveResources");
fMActiveResources.setAccessible(true);
<ArrayMap> arrayMap = (ArrayMap) fMActiveResources.get(resourcesManager);
references = arrayMap.values();
} catch (NoSuchFieldException ignore) {
Field mResourceReferences = resourcesManagerClass.getDeclaredField("mResourceReferences");
mResourceReferences.setAccessible(true);
references = (Collection) mResourceReferences.get(resourcesManager);
}
} else {
Class activityThread = Class.forName("android.app.ActivityThread");
Field fMActiveResources = activityThread.getDeclaredField("mActiveResources");
fMActiveResources.setAccessible(true);
Object thread = getActivityThread(context, activityThread);
<HashMap> map = (HashMap) fMActiveResources.get(thread);
references = map.values();
}
for (WeakReference wr : references) {
Resources resources = (Resources) wr.get();
if (resources != null) {
try {
Field mAssets = Resources.class.getDeclaredField("mAssets");
mAssets.setAccessible(true);
mAssets.set(resources, newAssetManager);
} catch (Throwable ignore) {
Field mResourcesImpl = Resources.class.getDeclaredField("mResourcesImpl");
mResourcesImpl.setAccessible(true);
Object resourceImpl = mResourcesImpl.get(resources);
Field implAssets = resourceImpl.getClass().getDeclaredField("mAssets");
implAssets.setAccessible(true);
implAssets.set(resourceImpl, newAssetManager);
}
resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrics());
}
}
} catch (Throwable e) {
throw new IllegalStateException(e);
}
}
該方法的作用是替換所有當前app的mAssets為newAssetManager。
monkeyPatchExistingResources的流程如下:
- 如果resource.ap_文件有改變躺苦,那么新建一個AssetManager對象newAssetManager身腻,然后用newAssetManager對象替換所有當前Resource、Resource.Theme的mAssets成員變量匹厘。
- 如果當前的已經(jīng)有Activity啟動了嘀趟,還需要替換所有Activity中mAssets成員變量
判斷Server是否已經(jīng)啟動,如果沒有啟動愈诚,則啟動Server去件。然后調(diào)用realApplication的onCreate方法代理realApplication的生命周期。
至此InstantRun的初始化工作就算完成了扰路,接下來就是在監(jiān)聽到代碼變化后熱更新了尤溜。總結一下汗唱,InstantRun在初始化階段主要做了以下幾部分工作:
- 代碼編譯階段對每一個用戶代碼中的方法進行插樁宫莱,這是hot swap的基礎
- 創(chuàng)建宿主apk,用戶代碼全部寫到instant-run.zip中
- 創(chuàng)建宿主Application(BootstrapApplication)哩罪,并在宿主Application初始化時:
- 通過注入ClassLoader的方式授霸,加載位于instant-run.zip中的用戶代碼
- 利用反射的方式注入真正的Application
當server啟動后巡验,會持續(xù)監(jiān)聽是否有代碼更新,如果有便加載到本地后進行熱更新碘耳。具體的更新邏輯显设,請看下一篇博客。