Instant Run
Instant Run, a new operating mechanism in Android studio 2.0, can significantly reduce the time you spend building and deploying current applications when you code, develop, test, or debug. The popular explanation is that when you change your code in Android Studio, Instant Run can quickly show you the effect of your changes. Before Instant Run, it would take tens of seconds or even longer for you to see a small change.
Traditional code modification and compilation deployment process
The traditional code modification and compilation process is as follows: build the whole apk deploy app app restart restart Activity
Instant Run Compilation and Deployment Process
Instant Run Construction Project Process: Building Modified Parts Deploying Modified dex or Resources Hot Deployment, Warm Deployment, Cold Deployment
Hot-pull, warm-pull, cold-pull
Hot plug-in: Code changes are applied and projected onto APP without restarting the application or rebuilding the current activity.
Scenario: Suitable for most simple changes (including modifications to some method implementations or variable values)
Plug-in: activity needs to be restarted to see the required changes.
Scenario: Typically, code modification involves resource files, resources.
Cold-Draw Plug: app needs to be restarted (but still does not need to be reinstalled)
Scenario: Anything that involves structural changes, such as modifying inheritance rules, modifying method signatures, etc.
First run Instant Run, Gradle execution process
A new App Server class is injected into App to monitor code changes in collaboration with Bytecode instrumentation.
At the same time, there will be a new Application class, which injects a custom class loader (Class Loader), and the Application class will start the new injected App Server we need. Manifest will then be modified to ensure that our application can use the new Application class. (There's no need to worry about inheriting and defining the Application class. The new Application class added by Instant Run will proxy our custom Application class.)
So far, Instant Run has been able to run, and when we use it, it will help us greatly shorten the time of building programs by making decisions and reasonably using hot and cold plugging.
Before Instant Run runs, Android Studio checks to see if it can connect to App Server. And make sure that the App Server is what Android Studio needs. This also ensures that the application is in the foreground.
Hot Drawing and Plugging
Android Studio monitors: Running the Gradle task to generate incremental. dex files (which correspond to modified classes in development) Android Studio will extract these. dex files and send them to App Server, then deploy them to App (Principle of Gradle Modification class, stamp) link).
App Server constantly monitors whether class files need to be rewritten, and if so, tasks will be executed immediately. New changes can be immediately responded to. We can look at it by interrupting points.
Warm insertion
Hotplug requires restarting Activity, because the resource file is loaded when the activity is created, so Activity must be restarted to reload the resource file.
At present, any modification of the resource file will result in repackaging and sending to APP. However, google's development team is working on an incremental package that only wraps the modified resource files and can be deployed to the current APP.
So plug-and-play can only deal with a few cases, it can not cope with changes in application architecture and structure.
Note: The resource file modification involved in warm plug-in is invalid on manifest (where invalid means that Instant Run will not be started), because the value of manifest is read when APK is installed, so if you want the resource modification under manifest to take effect, you need to trigger a complete application build and deployment.
Cold-drawn insertion
When the application is deployed, the project will be divided into ten parts, each part has its own. dex file, and then all classes will be assigned to the corresponding. dex file according to the package name. When the cold plug-in opens, the. dex file corresponding to the modified class will be reorganized to generate a new. dex file, and then deployed to the device.
The reason for this is that it relies on Android's ART mode, which allows multiple. dex files to be loaded. ART mode is added to Android 4.4 (API-19), but Dalvik is still the preferred mode. By Android 5.0 (API-21), ART mode becomes the default preferred mode of the system, so Instant Run can only run on API-21 and above.
Some Notes on Using Instant Run
Instant Run is controlled by Android Studio. So we can only start it through the IDE. If we start the application through the device, Instant Run will have an exception. When using Instant Run to start Android app, you should pay attention to the following points:
- If the minSdkVersion applied is less than 21, most of the Instant Run functions may be deactivated. This provides a solution to build a new branch of minSdkVersion larger than 21 through product flavor for debug.
- Instant Run can only run in the main process at present. If the application is multi-process, similar to Wechat, extracting webView from a single process, hot and warm plug-in will be reduced to cold plug-in.
- Under Windows, Windows Defender Real-Time Protection may cause Instant Run to hang, which can be solved by adding a white list.
- Jack compiler, Instrumentation Tests, or simultaneous deployment to multiple devices are not supported for the time being.
Deep Understanding with Demo
In order to facilitate your understanding, we have created a new project, which does not write any logical functions, but only makes a modification to the application:
First, let's decompile the composition of APK, using tools: d2j-dex2jar and jd-gui.
The startup information we want to see is in this instant-run.zip file. Unzip instant-run.zip and we will find that our real business code is here.
From the instant-run file, we assume that Bootstrap Application replaces our application. Instant-Run code acts as a host program and loads app as a resource dex.
So how does InstantRun run business code?
How Instant Run Starts app
According to our conjecture about the instant-run mechanism, let's first look at the attachBaseContext and onCreate methods for application analysis.
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); } } }
In turn, we need to pay attention to the following methods:
createResources setupClassLoaders createRealApplication Calling the attachBaseContext method of realApplication
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); } } }
Description: This method is mainly to determine whether the resource.ap_has changed, and then save the path of the resource.ap_to the 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); } }
Explain that this method is to initialize a ClassLoader and call Incremental ClassLoader.
The source code of Incremental ClassLoader is as follows:
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; } }
The inject method is used to set the parent-child order of the classloader, and the Incremental ClassLoader is used to load the dex. Because of the parent delegation mode of ClassLoader, which is to delegate the parent class to load the class, the parent class can not be found in this ClassLoader again.
The effect diagram of the call is as follows:
To facilitate our understanding of the delegated parent loading mechanism, we can do an experiment to do some Log s in our application.
@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(); } }
Output results:
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
From this, we know that PathClassLoader currently delegates Incremental ClassLoader to load dex.
We continue our analysis of 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(); } }
This method uses the real application of app stored in the application Class constant of AppInfo class in classes.dex. From the analysis of examples, we can know that application Class is com.xzh.demo.MyApplication. Create a real application by reflection.
After reading attachBaseContext, we continue to look at Bootstrap Application ();
BootstrapApplication()
Let's first look at the onCreate method:
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(); } }
In onCreate(), we need to pay attention to the following methods:
monkeyPatchApplication monkeyPatchExistingResources Server Start Call the onCreate method of realApplication
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); } }
Description: The purpose of this method is to replace the application of all current apps as real application.
The replacement process is as follows:
1. Replace mInitial Application of ActivityThread with realApplication
2. Replace all applications in mAll Application s with realApplication
3. Replace the mPackages of ActivityThread, and the application in mLoader Apk of mResourcePackages is real Application.
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); } }
Description: The purpose of this method is to replace mAssets of all current app s as new Asset Manager.
The process of monkey Patch Existing Resources is as follows:
1. If the resource.ap_file changes, create a new AssetManager object, newAssetManager, and replace all mAssets member variables of the current Resource and Resource.Theme with the new AssetManager object.
2. If the current Activity is started, all mAssets member variables in Activity need to be replaced.
Determine whether the Server has been started, and if not, start the Server. The onCreate method of realApplication is then called to proxy the lifetime of realApplication.
Next, we analyze the hot deployment, warm deployment and cold deployment of Server.
Server hot deployment, warm deployment and cold deployment
First, focus on Server's internal class SocketServer ReplyThread.
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; } } } } }
Note: When the socket is opened, it starts to read the data. When it reaches 1, it gets the list of Application Patches with code changes, and then calls handlePatches to handle code changes.
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; }
Description: This method mainly judges the content of Change to determine which mode to use (hot deployment, warm deployment or cold deployment).
- If the suffix is ".dex", the cold deployment process handleColdSwapPatch
- If the suffix is "classes.dex.3", hot deployment processing handleHotSwapPatch
- In other cases, warm deployment, processing resources handleResourcePatch
handleColdSwapPatch Cold Deployment
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); } } }
Description: This method writes the dex file to the private directory and waits for the entire app to restart. After restart, it can load the dex using the Incremental Class Loader mentioned above.
HandleHot SwapPatch Hot Deployment
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; }
Description: This method writes the DEX file of patch to the temporary directory, and then uses DexClassLoader to load dex. Then reflection calls the load method of the AppPatchesLoaderImpl class.
It should be emphasized that AppPatchesLoaderImpl inherits from the abstract class AbstractPatchesLoaderImpl and implements the abstract method getPatchedClasses. The AbstractPatchesLoaderImpl abstract class code is as follows:
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 Hot Deployment Principle
From the above code analysis, we can analyze the process of Instant Run as follows:
1. When the apk is first constructed, a member variable of $change is injected into each class, which implements the Incremental Change interface and inserts a similar logic into each method.
IncrementalChange localIncrementalChange = $change; if (localIncrementalChange != null) { localIncrementalChange.access$dispatch( "onCreate.(Landroid/os/Bundle;)V", new Object[] { this, ... }); return; }
When $change is not empty, the IncrementalChange method is executed.
2. When we modify the implementation of the method in the code, click InstantRun, which generates the corresponding patch file to record the content of your modification. The replacement class in the patch file adds $override to the name of the modified class and implements the Incremental Change interface.
3. Generate AppPatchesLoaderImpl class, inherit from AbstractPatchesLoaderImpl, and implement getPatchedClasses method to record which classes have been modified.
4. After calling the load method, according to the list of modified classes returned by getPatchedClasses, the corresponding $override class is loaded, and then the $change of the original class is set to the corresponding $override class that implements the Incremental Change interface.
Summary of Instant Run Operation Mechanism
Instant Run operation mechanism mainly involves hot deployment, warm deployment and cold deployment, mainly in the first run, app run time, when there is code modification.
First compilation
1. Package Instant-Run.jar and instant-Run-bootstrap.jar into the main dex
2. Replace the application configuration in Android Manifest. XML
3. Use the asm tool to add $change to each class and logic before each method
4. Compile the source code into dex and store it in instant-run.zip
app runtime
1. Get the path of the changed resource.ap_
2. Set up ClassLoader. setupClassLoader:
Using Incremental ClassLoader to load the apk code, the original BootClassLoader PathClassLoader is changed to BootClassLoader Incremental ClassLoader PathClassLoader inheritance relationship.
3.createRealApplication:
Create apk real application
4.monkeyPatchApplication
Reflective Replacement of Various Application Member Variables in ActivityThread
5.monkeyPatchExistingResource
Reflection replaces all existing AssetManager objects
6. Call the onCreate method of realApplication
7. Start Server, Socket receives the patch list
When there is code modification
1. Generate the corresponding $override class
2. Generate AppPatchesLoaderImpl class and record modified class list
3. Pack it into patch and pass it to app through socket
4. After the app server receives the patch, it processes the patch according to handleColdSwapPatch, handleHotSwapPatch and handleResourcePatch, respectively.
5.restart makes patch effective
Instant Run is used for reference in Android plug-in, Android hot repair and apk shell/shell. Therefore, understanding Instant Run is helpful for further research and for our own writing framework.