深入理解 Android Instant Run 執行機制
Instant Run
Instant Run,是Android studio2.0新增的一個執行機制,在你編碼開發、測試或debug的時候,它都能顯著減少你對當前應用的構建和部署的時間。通俗的解釋就是,當你在Android Studio中改了你的程式碼,Instant Run可以很快的讓你看到你修改的效果。而在沒有Instant Run之前,你的一個小小的修改,都肯能需要幾十秒甚至更長的等待才能看到修改後的效果。
傳統的程式碼修改及編譯部署流程
傳統的程式碼修改及編譯流程如下:構建整個apk → 部署app → app重啟 → 重啟Activity
Instant Run編譯和部署流程
Instant Run構建專案的流程:構建修改的部分 → 部署修改的dex或資源 → 熱部署,溫部署,冷部署
熱拔插,溫拔插,冷拔插
熱拔插:程式碼改變被應用、投射到APP上,不需要重啟應用,不需要重建當前activity。
場景:適用於多數的簡單改變(包括一些方法實現的修改,或者變數值修改)
**溫拔插:**activity需要被重啟才能看到所需更改。
場景:典型的情況是程式碼修改涉及到了資原始檔,即resources。
**冷拔插:**app需要被重啟(但是仍然不需要重新安裝)
場景:任何涉及結構性變化的,比如:修改了繼承規則、修改了方法簽名等。
首次執行Instant Run,Gradle執行過程
一個新的App Server類會被注入到App中,與Bytecode instrumentation協同監控程式碼的變化。
同時會有一個新的Application類,它注入了一個自定義類載入器(Class Loader),同時該Application類會啟動我們所需的新注入的App Server。於是,Manifest會被修改來確保我們的應用能使用這個新的Application類。(這裡不必擔心自己繼承定義了Application類,Instant Run新增的這個新Application類會代理我們自定義的Application類)
至此,Instant Run已經可以跑起來了,在我們使用的時候,它會通過決策,合理運用冷溫熱拔插來協助我們大量地縮短構建程式的時間。
在Instant Run執行之前,Android Studio會檢查是否能連線到App Server中。並且確保這個App Server是Android Studio所需要的。這同樣能確保該應用正處在前臺。
熱拔插
Android Studio monitors: 執行著Gradle任務來生成增量.dex檔案(這個dex檔案是對應著開發中的修改類) Android Studio會提取這些.dex檔案傳送到App Server,然後部署到App(Gradle修改class的原理,請戳連結)。
App Server會不斷監聽是否需要重寫類檔案,如果需要,任務會被立馬執行。新的更改便能立即被響應。我們可以通過打斷點的方式來檢視。
溫拔插
溫拔插需要重啟Activity,因為資原始檔是在Activity建立時載入,所以必須重啟Activity來過載資原始檔。
目前來說,任何資原始檔的修改都會導致重新打包再傳送到APP。但是,google的開發團隊正在致力於開發一個增量包,這個增量包只會包裝修改過的資原始檔並能部署到當前APP上。
所以溫拔插實際上只能應對少數的情況,它並不能應付應用在架構、結構上的變化。
注:溫拔插涉及到的資原始檔修改,在manifest上是無效的(這裡的無效是指不會啟動Instant Run),因為,manifest的值是在APK安裝的時候被讀取,所以想要manifest下資源的修改生效,還需要觸發一個完整的應用構建和部署。
冷拔插
應用部署的時候,會把工程拆分成十個部分,每部分都擁有自己的.dex檔案,然後所有的類會根據包名被分配給相應的.dex檔案。當冷拔插開啟時,修改過的類所對應的.dex檔案,會重組生成新的.dex檔案,然後再部署到裝置上。
之所以能這麼做,是依賴於Android的ART模式,它能允許載入多個.dex檔案。ART模式在android4.4(API-19)中加入,但是Dalvik依然是首選,到了android5.0(API-21),ART模式才成為系統預設首選,所以Instant Run只能執行在API-21及其以上版本。
使用Instant Run一些注意點
Instant Run是被Android Studio控制的。所以我們只能通過IDE來啟動它,如果通過裝置來啟動應用,Instant Run會出現異常情況。在使用Instant Run來啟動Android app的時候,應注意以下幾點:
- 如果應用的minSdkVersion小於21,可能多數的Instant Run功能會掛掉,這裡提供一個解決方法,通過product flavor建立一個minSdkVersion大於21的新分支,用來debug。
- Instant Run目前只能在主程式裡執行,如果應用是多程式的,類似微信,把webView抽出來單獨一個程式,那熱、溫拔插會被降級為冷拔插。
- 在Windows下,Windows Defender Real-Time Protection可能會導致Instant Run掛掉,可用通過新增白名單列表解決。
- 暫時不支援Jack compiler,Instrumentation Tests,或者同時部署到多臺裝置。
結合Demo深度理解
為了方便大家的理解,我們新建一個專案,裡面不寫任何的邏輯功能,只對application做一個修改:
首先,我們先反編譯一下APK的構成,使用的工具:d2j-dex2jar 和jd-gui。
我們要看的啟動的資訊就在這個instant-run.zip檔案裡面,解壓instant-run.zip,我們會發現,我們真正的業務程式碼都在這裡。
從instant-run檔案中我們猜想是BootstrapApplication替換了我們的application,Instant-Run程式碼作為一個宿主程式,將app作為資源dex載入起來。
那麼InstantRun是怎麼把業務程式碼執行起來的呢?
Instant Run如何啟動app
按照我們上面對instant-run執行機制的猜想,我們首先看一下appliaction的分析attachBaseContext和onCreate方法。
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 → 呼叫realApplication的attachBaseContext方法
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中。
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); } }
說明,該方法是初始化一個ClassLoaders並呼叫IncrementalClassLoader。
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; } }
inject方法是用來設定classloader的父子順序的,使用IncrementalClassLoader來載入dex。由於ClassLoader的雙親委託模式,也就是委託父類載入類,父類中找不到再在本ClassLoader中查詢。
呼叫的效果圖如下:
為了方便我們對委託父類載入機制的理解,我們可以做一個實驗,在我們的application做一些Log。
@Override public void onCreate() { super.onCreate(); try{ Log.d(TAG,"###onCreate in myApplication"); String classLoaderName = getClassLoader().getClass().getName(); Log.d(TAG,"###onCreate in myApplication classLoaderName = "+classLoaderName); String parentClassLoaderName = getClassLoader().getParent().getClass().getName(); Log.d(TAG,"###onCreate in myApplication parentClassLoaderName = "+parentClassLoaderName); String pParentClassLoaderName = getClassLoader().getParent().getParent().getClass().getName(); Log.d(TAG,"###onCreate in myApplication pParentClassLoaderName = "+pParentClassLoaderName); }catch (Exception e){ e.printStackTrace(); } }
輸出結果:
03-20 10:43:42.475 27307-27307/mobctrl.net.testinstantrun D/MyApplication: ###onCreate in myApplication classLoaderName = dalvik.system.PathClassLoader 03-20 10:43:42.475 27307-27307/mobctrl.net.testinstantrun D/MyApplication: ###onCreate in myApplication parentClassLoaderName = com.android.tools.fd.runtime.IncrementalClassLoader 03-20 10:43:42.475 27307-27307/mobctrl.net.testinstantrun D/MyApplication: ###onCreate in myApplication pParentClassLoaderName = java.lang.BootClassLoader
由此,我們知道,當前PathClassLoader委託IncrementalClassLoader載入dex。
我們繼續對attachBaseContext()繼續分析:
attachBaseContext.invoke(this.realApplication, new Object[] { context });
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(); } }
該方法就是用classes.dex中的AppInfo類的applicationClass常量中儲存的app真實的application。由例子的分析我們可以知道applicationClass就是com.xzh.demo.MyApplication。通過反射的方式,建立真是的realApplication。
看完attachBaseContext我們繼續看BootstrapApplication();
BootstrapApplication()
我們首先看一下onCreate方法:
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啟動 → 呼叫realApplication的onCreate方法
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。
替換的過程如下:
1.替換ActivityThread的mInitialApplication為realApplication
2.替換mAllApplications 中所有的Application為realApplication
3.替換ActivityThread的mPackages,mResourcePackages中的mLoaderApk中的application為realApplication。
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的流程如下:
1.如果resource.ap_檔案有改變,那麼新建一個AssetManager物件newAssetManager,然後用newAssetManager物件替換所有當前Resource、Resource.Theme的mAssets成員變數。
2.如果當前的已經有Activity啟動了,還需要替換所有Activity中mAssets成員變數
判斷Server是否已經啟動,如果沒有啟動,則啟動Server。然後呼叫realApplication的onCreate方法代理realApplication的生命週期。
接下來我們分析下Server負責的熱部署、溫部署和冷部署等問題。
Server熱部署、溫部署和冷部署
首先重點關注一下Server的內部類SocketServerReplyThread。
SocketServerReplyThread
private class SocketServerReplyThread extends Thread { private final LocalSocket mSocket; SocketServerReplyThread(LocalSocket socket) { this.mSocket = socket; } public void run() { try { DataInputStream input = new DataInputStream( this.mSocket.getInputStream()); DataOutputStream output = new DataOutputStream( this.mSocket.getOutputStream()); try { handle(input, output); } finally { try { input.close(); } catch (IOException ignore) { } try { output.close(); } catch (IOException ignore) { } } return; } catch (IOException e) { if (Log.isLoggable("InstantRun", 2)) { Log.v("InstantRun", "Fatal error receiving messages", e); } } } private void handle(DataInputStream input, DataOutputStream output) throws IOException { long magic = input.readLong(); if (magic != 890269988L) { Log.w("InstantRun", "Unrecognized header format " + Long.toHexString(magic)); return; } int version = input.readInt(); output.writeInt(4); if (version != 4) { Log.w("InstantRun", "Mismatched protocol versions; app is using version 4 and tool is using version " + version); } else { int message; for (;;) { message = input.readInt(); switch (message) { case 7: if (Log.isLoggable("InstantRun", 2)) { Log.v("InstantRun", "Received EOF from the IDE"); } return; case 2: boolean active = Restarter .getForegroundActivity(Server.this.mApplication) != null; output.writeBoolean(active); if (Log.isLoggable("InstantRun", 2)) { Log.v("InstantRun", "Received Ping message from the IDE; returned active = " + active); } break; case 3: String path = input.readUTF(); long size = FileManager.getFileSize(path); output.writeLong(size); if (Log.isLoggable("InstantRun", 2)) { Log.v("InstantRun", "Received path-exists(" + path + ") from the " + "IDE; returned size=" + size); } break; case 4: long begin = System.currentTimeMillis(); path = input.readUTF(); byte[] checksum = FileManager.getCheckSum(path); if (checksum != null) { output.writeInt(checksum.length); output.write(checksum); if (Log.isLoggable("InstantRun", 2)) { long end = System.currentTimeMillis(); String hash = new BigInteger(1, checksum) .toString(16); Log.v("InstantRun", "Received checksum(" + path + ") from the " + "IDE: took " + (end - begin) + "ms to compute " + hash); } } else { output.writeInt(0); if (Log.isLoggable("InstantRun", 2)) { Log.v("InstantRun", "Received checksum(" + path + ") from the " + "IDE: returning "); } } break; case 5: if (!authenticate(input)) { return; } Activity activity = Restarter .getForegroundActivity(Server.this.mApplication); if (activity != null) { if (Log.isLoggable("InstantRun", 2)) { Log.v("InstantRun", "Restarting activity per user request"); } Restarter.restartActivityOnUiThread(activity); } break; case 1: if (!authenticate(input)) { return; } List changes = ApplicationPatch .read(input); if (changes != null) { boolean hasResources = Server.hasResources(changes); int updateMode = input.readInt(); updateMode = Server.this.handlePatches(changes, hasResources, updateMode); boolean showToast = input.readBoolean(); output.writeBoolean(true); Server.this.restart(updateMode, hasResources, showToast); } break; case 6: String text = input.readUTF(); Activity foreground = Restarter .getForegroundActivity(Server.this.mApplication); if (foreground != null) { Restarter.showToast(foreground, text); } else if (Log.isLoggable("InstantRun", 2)) { Log.v("InstantRun", "Couldn't show toast (no activity) : " + text); } break; } } } } }
說明:socket開啟後,開始讀取資料,當讀到1時,獲取程式碼變化的ApplicationPatch列表,然後呼叫handlePatches來處理程式碼的變化。
handlePatches
private int handlePatches(List changes, boolean hasResources, int updateMode) { if (hasResources) { FileManager.startUpdate(); } for (ApplicationPatch change : changes) { String path = change.getPath(); if (path.endsWith(".dex")) { handleColdSwapPatch(change); boolean canHotSwap = false; for (ApplicationPatch c : changes) { if (c.getPath().equals("classes.dex.3")) { canHotSwap = true; break; } } if (!canHotSwap) { updateMode = 3; } } else if (path.equals("classes.dex.3")) { updateMode = handleHotSwapPatch(updateMode, change); } else if (isResourcePath(path)) { updateMode = handleResourcePatch(updateMode, change, path); } } if (hasResources) { FileManager.finishUpdate(true); } return updateMode; }
說明:本方法主要通過判斷Change的內容,來判斷採用什麼模式(熱部署、溫部署或冷部署)
- 如果字尾為“.dex”,冷部署處理handleColdSwapPatch
- 如果字尾為“classes.dex.3”,熱部署處理handleHotSwapPatch
- 其他情況,溫部署,處理資源handleResourcePatch
handleColdSwapPatch冷部署
private static void handleColdSwapPatch(ApplicationPatch patch) { if (patch.path.startsWith("slice-")) { File file = FileManager.writeDexShard(patch.getBytes(), patch.path); if (Log.isLoggable("InstantRun", 2)) { Log.v("InstantRun", "Received dex shard " + file); } } }
說明:該方法把dex檔案寫到私有目錄,等待整個app重啟,重啟之後,使用前面提到的IncrementalClassLoader載入dex即可。
handleHotSwapPatch熱部署
private int handleHotSwapPatch(int updateMode, ApplicationPatch patch) { if (Log.isLoggable("InstantRun", 2)) { Log.v("InstantRun", "Received incremental code patch"); } try { String dexFile = FileManager.writeTempDexFile(patch.getBytes()); if (dexFile == null) { Log.e("InstantRun", "No file to write the code to"); return updateMode; } if (Log.isLoggable("InstantRun", 2)) { Log.v("InstantRun", "Reading live code from " + dexFile); } String nativeLibraryPath = FileManager.getNativeLibraryFolder() .getPath(); DexClassLoader dexClassLoader = new DexClassLoader(dexFile, this.mApplication.getCacheDir().getPath(), nativeLibraryPath, getClass().getClassLoader()); Class aClass = Class.forName( "com.android.tools.fd.runtime.AppPatchesLoaderImpl", true, dexClassLoader); try { if (Log.isLoggable("InstantRun", 2)) { Log.v("InstantRun", "Got the patcher class " + aClass); } PatchesLoader loader = (PatchesLoader) aClass.newInstance(); if (Log.isLoggable("InstantRun", 2)) { Log.v("InstantRun", "Got the patcher instance " + loader); } String[] getPatchedClasses = (String[]) aClass .getDeclaredMethod("getPatchedClasses", new Class[0]) .invoke(loader, new Object[0]); if (Log.isLoggable("InstantRun", 2)) { Log.v("InstantRun", "Got the list of classes "); for (String getPatchedClass : getPatchedClasses) { Log.v("InstantRun", "class " + getPatchedClass); } } if (!loader.load()) { updateMode = 3; } } catch (Exception e) { Log.e("InstantRun", "Couldn't apply code changes", e); e.printStackTrace(); updateMode = 3; } } catch (Throwable e) { Log.e("InstantRun", "Couldn't apply code changes", e); updateMode = 3; } return updateMode; }
說明:該方法將patch的dex檔案寫入到臨時目錄,然後使用DexClassLoader去載入dex。然後反射呼叫AppPatchesLoaderImpl類的load方法。
需要強調的是:AppPatchesLoaderImpl繼承自抽象類AbstractPatchesLoaderImpl,並實現了抽象方法:getPatchedClasses。而AbstractPatchesLoaderImpl抽象類程式碼如下:
public abstract class AbstractPatchesLoaderImpl implements PatchesLoader { public abstract String[] getPatchedClasses(); public boolean load() { try { for (String className : getPatchedClasses()) { ClassLoader cl = getClass().getClassLoader(); Class aClass = cl.loadClass(className + "$override"); Object o = aClass.newInstance(); Class originalClass = cl.loadClass(className); Field changeField = originalClass.getDeclaredField("$change"); changeField.setAccessible(true); Object previous = changeField.get(null); if (previous != null) { Field isObsolete = previous.getClass().getDeclaredField( "$obsolete"); if (isObsolete != null) { isObsolete.set(null, Boolean.valueOf(true)); } } changeField.set(null, o); if ((Log.logging != null) && (Log.logging.isLoggable(Level.FINE))) { Log.logging.log(Level.FINE, String.format("patched %s", new Object[] { className })); } } } catch (Exception e) { if (Log.logging != null) { Log.logging.log(Level.SEVERE, String.format( "Exception while patching %s", new Object[] { "foo.bar" }), e); } return false; } return true; } }
Instant Run熱部署原理
由上面的程式碼分析,我們對Instant Run的流程可以分析如下:
1,在第一次構建apk時,在每一個類中注入了一個$change的成員變數,它實現了IncrementalChange介面,並在每一個方法中,插入了一段類似的邏輯。
IncrementalChange localIncrementalChange = $change; if (localIncrementalChange != null) { localIncrementalChange.access$dispatch( "onCreate.(Landroid/os/Bundle;)V", new Object[] { this, ... }); return; }
當$change不為空的時候,執行IncrementalChange方法。
2,當我們修改程式碼中方法的實現之後,點選InstantRun,它會生成對應的patch檔案來記錄你修改的內容。patch檔案中的替換類是在所修改類名的後面追加$override,並實現IncrementalChange介面。
3,生成AppPatchesLoaderImpl類,繼承自AbstractPatchesLoaderImpl,並實現getPatchedClasses方法,來記錄哪些類被修改了。
4,呼叫load方法之後,根據getPatchedClasses返回的修改過的類的列表,去載入對應的override類,然後把原有類的change設定為對應的實現了IncrementalChange介面的$override類。
Instant Run執行機制總結
Instant Run執行機制主要涉及到熱部署、溫部署和冷部署,主要是在第一次執行,app執行時期,有程式碼修改時。
第一次編譯
- 1.把Instant-Run.jar和instant-Run-bootstrap.jar打包到主dex中
- 2.替換AndroidManifest.xml中的application配置
- 3.使用asm工具,在每個類中新增$change,在每個方法前加邏輯
- 4.把原始碼編譯成dex,然後存放到壓縮包instant-run.zip中
app執行時
- 1.獲取更改後資源resource.ap_的路徑
- 2.設定ClassLoader。setupClassLoader:
- 使用IncrementalClassLoader載入apk的程式碼,將原有的BootClassLoader → PathClassLoader改為BootClassLoader → IncrementalClassLoader → PathClassLoader繼承關係。
- 3.createRealApplication:
- 建立apk真實的application
- 4.monkeyPatchApplication
- 反射替換ActivityThread中的各種Application成員變數
- 5.monkeyPatchExistingResource
- 反射替換所有存在的AssetManager物件
- 6.呼叫realApplication的onCreate方法
- 7.啟動Server,Socket接收patch列表
有程式碼修改時
- 1.生成對應的$override類
- 2.生成AppPatchesLoaderImpl類,記錄修改的類列表
- 3.打包成patch,通過socket傳遞給app
- 4.app的server接收到patch之後,分別按照handleColdSwapPatch、handleHotSwapPatch、handleResourcePatch等待對patch進行處理
- 5.restart使patch生效
在Android外掛化、Android熱修復、apk加殼/脫殼中借鑑了Instant Run執行機制,所以理解Instant Run執行機制對於向更深層次的研究是很有幫助的,對於我們自己書寫框架也是有借鑑意義的。
相關文章
- 深入理解js的執行機制JS
- 深入理解 OpenMP 執行緒同步機制執行緒
- Android 深入理解 Notification 機制Android
- 理解Reacg執行機制
- Android Studio中Instant RunAndroid
- 深入理解Android訊息機制Android
- 深入理解JavaScript之徹底弄懂JsEventLoop執行機制JavaScriptJSOOP
- 深入理解 Android 訊息機制原理Android
- 【React深入】setState的執行機制React
- 深入淺出JavaScript執行機制JavaScript
- 「Go框架」深入理解web框架的中介軟體執行機制Go框架Web
- Android 在 Multidex 下使用 Instant RunAndroidIDE
- 從setTimeout理解JS執行機制JS
- 揭祕最新android studio instant run(一)Android
- 深入理解DOM事件機制事件
- 深入理解 Kafka 副本機制Kafka
- ORACLE鎖機制深入理解Oracle
- Android Studio的Instant Run工作原理及用法Android
- 深入理解Android非同步訊息處理機制Android非同步
- Android執行緒篇(四)深入理解Java執行緒池(二)Android執行緒Java
- Android執行緒篇(三)深入理解Java執行緒池(一)Android執行緒Java
- 【深入 PHP】PHP7 底層執行機制PHP
- 冷飯新炒 | 深入Quartz核心執行機制quartz
- 深入分析Java執行緒中斷機制Java執行緒
- [深入理解Java虛擬機器]執行緒Java虛擬機執行緒
- Android Handler機制理解Android
- 理解Android安全機制Android
- 10分鐘理解JS引擎的執行機制JS
- 深入理解React:事件機制原理React事件
- 深入理解 Java 中 SPI 機制Java
- Java異常機制深入理解Java
- 深入理解非同步事件機制非同步事件
- 深入理解 Swift 派發機制Swift
- 深入js基礎:從記憶體機制、解析機制到執行機制(長文預警)JS記憶體
- 深入理解Promise執行原理Promise
- 深入理解執行緒池的執行流程執行緒
- 理解 Android 訊息機制Android
- 深入理解Android中的快取機制(三)磁碟快取Android快取