In-depth understanding of the Android Instant Run operating mechanism

In-depth understanding of the Android Instant Run operating mechanism

Instant Run

Instant Run is a new operating mechanism added to Android Studio 2.0. It can significantly reduce the time it takes to build and deploy your current application when you are coding, developing, testing or debugging. In simple terms, when you change your code in Android Studio, Instant Run can quickly let you see the effect of your changes. Before Instant Run, a small change may take tens of seconds or even longer to see the effect of the change.

Traditional code modification and compilation deployment process

The traditional code modification and compilation process is as follows: Build the entire apk → Deploy the app → Restart the app → Restart the Activity

Instant Run compilation and deployment process

The process of building a project with Instant Run: Build the modified part → Deploy the modified dex or resources → Hot deployment, warm deployment, cold deployment

Hot plug, warm plug, cold plug

Hot plugging: Code changes are applied and projected to the app without restarting the app or rebuilding the current activity.

Scenario: Applicable to most simple changes (including modifications to some method implementations or variable values)

Warm unplug: The activity needs to be restarted to see the desired changes.

Scenario: A typical situation is that code modification involves resource files, namely resources.

Cold Unplug: The app needs to be restarted (but still does not need to be reinstalled)

Scenario: Any structural changes, such as changes to inheritance rules or method signatures.

First run of Instant Run, Gradle execution process

A new App Server class will be injected into the App to monitor code changes in conjunction with Bytecode instrumentation.

At the same time, there will be a new Application class, which has a custom class loader injected into it. This Application class will start the newly injected App Server we need. Therefore, the Manifest will be modified to ensure that our application can use this new Application class. (Here you don’t have to worry about inheriting and defining the Application class yourself. The new Application class added by Instant Run will proxy your custom Application class)

At this point, Instant Run is ready to run. When we use it, it will help us greatly shorten the time to build the program by making decisions and making reasonable use of hot and cold plug-ins.

Before Instant Run runs, Android Studio checks whether it can connect to the App Server. And makes sure that the App Server is what Android Studio needs. This also ensures that the app is in the foreground.

Hot Swap

Android Studio monitors: Running Gradle tasks to generate incremental .dex files (this dex file corresponds to the modified classes in development) Android Studio will extract these .dex files and send them to the App Server, and then deploy them to the App (the principle of Gradle modifying classes, please click the link).

App Server will keep monitoring whether the class file needs to be rewritten. If necessary, the task will be executed immediately. New changes can be responded to immediately. We can check it by setting breakpoints.

Warm plug and unplug

Warm plug and unplug requires restarting the Activity, because the resource files are loaded when the Activity is created, so the Activity must be restarted to reload the resource files.

Currently, any modification of resource files will result in repackaging and sending to the APP. However, Google's development team is working on developing an incremental package that will only package the modified resource files and can be deployed to the current APP.

Therefore, warm plugging can only handle a few situations, and it cannot cope with changes in the application's architecture and structure.

Note: The resource file modifications involved in warm plugging are invalid in the manifest (invalid here means that Instant Run will not be started), because the manifest value is read when the APK is installed. Therefore, if you want the resource modifications under the manifest to take effect, you also need to trigger a complete application build and deployment.

Cold plug

When deploying an application, the project will be split into ten parts, each with its own .dex file, and all classes will be assigned to the corresponding .dex file according to the package name. When cold plugging is turned on, the .dex files corresponding to the modified classes will be reorganized into new .dex files and then deployed to the device.

The reason why this can be done is to rely on Android's ART mode, which allows loading multiple .dex files. ART mode was added in Android 4.4 (API-19), but Dalvik is still the first choice. In Android 5.0 (API-21), ART mode became the system default choice, 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 abnormal behavior. When using Instant Run to start an Android app, you should pay attention to the following points:

If the minSdkVersion of the application is less than 21, most of the Instant Run features may fail. Here is a solution. Use product flavor to create a new branch with a minSdkVersion greater than 21 for debugging.

Instant Run can currently only run in the main process. If the application is multi-process, like WeChat, and the webView is extracted into a separate process, hot and warm plug-ins will be downgraded to cold plug-ins.

Under Windows, Windows Defender Real-Time Protection may cause Instant Run to crash, which can be solved by adding a whitelist.

There is currently no support for the Jack compiler, Instrumentation Tests, or deploying to multiple devices simultaneously.

Combined with Demo in-depth understanding

To make it easier for everyone to understand, we create a new project without writing any logical functions, and only make a modification to the application:

First, let's decompile the structure of APK using the 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 guess that BootstrapApplication replaces our application. The Instant-Run code acts as a host program and loads the app as a resource dex.

So how does InstantRun run the business code?

How to launch an app using Instant Run

According to our above conjecture about the instant-run operation mechanism, let's first look at the analysis of appliaction's attachBaseContext and onCreate methods.

attachBaseContext()

  1. protected void attachBaseContext(Context context) {
  2. if (!AppInfo.usingApkSplits) {
  3. String apkFile = context.getApplicationInfo().sourceDir;
  4. long apkModified = apkFile != null ? new File(apkFile).lastModified() : 0L;
  5. createResources(apkModified);
  6. setupClassLoaders(context, context.getCacheDir().getPath(), apkModified);
  7. }
  8. createRealApplication();
  9. super.attachBaseContext(context);
  10. if (this.realApplication != null ) {
  11. try {
  12. Method attachBaseContext = ContextWrapper.class.getDeclaredMethod( "attachBaseContext" , new Class[] { Context.class });
  13. attachBaseContext.setAccessible( true );
  14. attachBaseContext.invoke(this.realApplication, new Object[] { context });
  15. } catch (Exception e) {
  16. throw new IllegalStateException(e);
  17. }
  18. }
  19. }

The methods we need to focus on in turn are:

createResources → setupClassLoaders → createRealApplication → call the attachBaseContext method of realApplication

createResources()

  1. private void createResources(long apkModified) {
  2. FileManager.checkInbox();
  3. File file = FileManager.getExternalResourceFile();
  4. this.externalResourcePath = (file != null ? file.getPath() : null );
  5. if (Log.isLoggable( "InstantRun" , 2)) {
  6. Log.v( "InstantRun" , "Resource override is " + this.externalResourcePath);
  7. }
  8. if (file != null ) {
  9. try {
  10. long resourceModified = file.lastModified();
  11. if (Log.isLoggable( "InstantRun" , 2)) {
  12. Log.v( "InstantRun" , "Resource patch last modified: " + resourceModified);
  13. Log.v( "InstantRun" , "APK last modified: " + apkModified
  14. + " "  
  15. + (apkModified > resourceModified ? ">" : "<" )
  16. + "resource patch" );
  17. }
  18. if ((apkModified == 0L) || (resourceModified <= apkModified)) {
  19. if (Log.isLoggable( "InstantRun" , 2)) {
  20. Log.v( "InstantRun" , "Ignoring resource file, older than APK" );
  21. }
  22. this.externalResourcePath = null ;
  23. }
  24. } catch (Throwable t) {
  25. Log.e( "InstantRun" , "Failed to check patch timestamps" , t);
  26. }
  27. }
  28. }

Description: This method mainly determines whether the resource resource.ap_ has changed, and then saves the path of resource.ap_ to externalResourcePath.

setupClassLoaders()

  1. private static void setupClassLoaders(Context context, String codeCacheDir, long apkModified) {
  2. List dexList = FileManager.getDexList(context, apkModified);
  3. Class server = Server.class;
  4. Class patcher = MonkeyPatcher.class;
  5. if (!dexList.isEmpty()) {
  6. if (Log.isLoggable( "InstantRun" , 2)) {
  7. Log.v( "InstantRun" , "Bootstrapping class loader with dex list " + join ( '\n' , dexList));
  8. }
  9. ClassLoader classLoader = BootstrapApplication.class.getClassLoader();
  10. String nativeLibraryPath;
  11. try {
  12. nativeLibraryPath = (String) classLoader.getClass().getMethod( "getLdLibraryPath" , new Class[0]).invoke(classLoader, new Object[0]);
  13. if (Log.isLoggable( "InstantRun" , 2)) {
  14. Log.v( "InstantRun" , "Native library path: " + nativeLibraryPath);
  15. }
  16. } catch (Throwable t) {
  17. Log.e( "InstantRun" , "Failed to determine native library path " + t.getMessage());
  18. nativeLibraryPath = FileManager.getNativeLibraryFolder().getPath();
  19. }
  20. IncrementalClassLoader.inject(classLoader, nativeLibraryPath, codeCacheDir, dexList);
  21. }
  22. }

Description: This method initializes a ClassLoaders and calls IncrementalClassLoader.

The source code of IncrementalClassLoader is as follows:

  1. public class IncrementalClassLoader extends ClassLoader {
  2. public   static final boolean DEBUG_CLASS_LOADING = false ;
  3. private final DelegateClassLoader delegateClassLoader;
  4. public IncrementalClassLoader(ClassLoader original, String nativeLibraryPath, String codeCacheDir, List dexes) {
  5. super(original.getParent());
  6. this.delegateClassLoader = createDelegateClassLoader(nativeLibraryPath, codeCacheDir, dexes, original);
  7. }
  8.  
  9. public Class findClass(String className) throws ClassNotFoundException {
  10. try {
  11. return this.delegateClassLoader.findClass(className);
  12. } catch (ClassNotFoundException e) {
  13. throw e;
  14. }
  15. }
  16. private static class DelegateClassLoader extends BaseDexClassLoader {
  17. private DelegateClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) {
  18. super(dexPath, optimizedDirectory, libraryPath, parent);
  19. }
  20.  
  21. public Class findClass(String name ) throws ClassNotFoundException {
  22. try {
  23. return super.findClass( name );
  24. } catch (ClassNotFoundException e) {
  25. throw e;
  26. }
  27. }
  28. }
  29.  
  30. private static DelegateClassLoader createDelegateClassLoader(String nativeLibraryPath, String codeCacheDir, List dexes,
  31. ClassLoader original) {
  32. String pathBuilder = createDexPath(dexes);
  33. return new DelegateClassLoader(pathBuilder, new File(codeCacheDir), nativeLibraryPath, original);
  34. }
  35. private static String createDexPath(List dexes) {
  36. StringBuilder pathBuilder = new StringBuilder();
  37. boolean first = true ;
  38. for (String dex : dexes) {
  39. if ( first ) {
  40. first = false ;
  41. } else {
  42. pathBuilder.append(File.pathSeparator);
  43. }
  44. pathBuilder.append(dex);
  45. }
  46. if (Log.isLoggable( "InstantRun" , 2)) {
  47. Log.v( "InstantRun" , "Incremental dex path is " + BootstrapApplication. join ( '\n' , dexes));
  48. }
  49. return pathBuilder.toString();
  50. }
  51. private static void setParent(ClassLoader classLoader, ClassLoader newParent) {
  52. try {
  53. Field parent = ClassLoader.class.getDeclaredField( "parent" );
  54. parent.setAccessible( true );
  55. parent.set (classLoader, newParent);
  56. } catch (IllegalArgumentException e) {
  57. throw new RuntimeException(e);
  58. } catch (IllegalAccessException e) {
  59. throw new RuntimeException(e);
  60. } catch (NoSuchFieldException e) {
  61. throw new RuntimeException(e);
  62. }
  63. }
  64. public   static ClassLoader inject(ClassLoader classLoader,
  65. String nativeLibraryPath, String codeCacheDir, List dexes) {
  66. IncrementalClassLoader incrementalClassLoader = new IncrementalClassLoader(classLoader, nativeLibraryPath, codeCacheDir, dexes);
  67. setParent(classLoader, incrementalClassLoader);
  68. return incrementalClassLoader;
  69. }
  70. }

The inject method is used to set the parent-child order of classloader, and use IncrementalClassLoader to load dex. Due to the parent delegation mode of ClassLoader, that is, entrusting the parent class to load the class, if it is not found in the parent class, it will be searched in the current ClassLoader.

The effect diagram of the call is as follows:

To facilitate our understanding of the mechanism of delegated parent class loading, we can do an experiment and do some logs in our application.

  1. @Override
  2. public void onCreate() {
  3. super.onCreate();
  4. try{
  5. Log.d(TAG, "###onCreate in myApplication" );
  6. String classLoaderName = getClassLoader().getClass().getName();
  7. Log.d(TAG, "###onCreate in myApplication classLoaderName = " +classLoaderName);
  8. String parentClassLoaderName = getClassLoader().getParent().getClass().getName();
  9. Log.d(TAG, "###onCreate in myApplication parentClassLoaderName = " +parentClassLoaderName);
  10. String pParentClassLoaderName = getClassLoader().getParent().getParent().getClass().getName();
  11. Log.d(TAG, "###onCreate in myApplication pParentClassLoaderName = " +pParentClassLoaderName);
  12. }catch (Exception e){
  13. e.printStackTrace();
  14. }
  15. }

Output:

  1. 03-20 10:43:42.475 27307-27307/mobctrl.net.testinstantrun D/MyApplication: ###onCreate in myApplication classLoaderName = dalvik.system.PathClassLoader
  2. 03-20 10:43:42.475 27307-27307/mobctrl.net.testinstantrun D/MyApplication: ###onCreate in myApplication parentClassLoaderName = com.android.tools.fd.runtime.IncrementalClassLoader
  3. 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 the current PathClassLoader delegates IncrementalClassLoader to load dex.

We continue to analyze attachBaseContext():

  1. attachBaseContext.invoke(this.realApplication, new Object[] { context });

createRealApplication

  1. private void createRealApplication() {
  2. if (AppInfo.applicationClass != null ) {
  3. if (Log.isLoggable( "InstantRun" , 2)) {
  4. Log.v( "InstantRun" , "About to create real application of class name = " + AppInfo.applicationClass);
  5. }
  6. try {
  7. Class realClass = (Class) Class.forName(AppInfo.applicationClass);
  8. if (Log.isLoggable( "InstantRun" , 2)) {
  9. Log.v( "InstantRun" , "Created delegate app class successfully : "  
  10. + realClass + " with class loader "  
  11. + realClass.getClassLoader());
  12. }
  13. Constructor constructor = realClass.getConstructor(new Class[0]);
  14. this.realApplication = ((Application) constructor.newInstance(new Object[0]));
  15. if (Log.isLoggable( "InstantRun" , 2)) {
  16. Log.v( "InstantRun" , "Created real app instance successfully :" + this.realApplication);
  17. }
  18. } catch (Exception e) {
  19. throw new IllegalStateException(e);
  20. }
  21. } else {
  22. this.realApplication = new Application();
  23. }
  24. }

This method uses the real application of the app stored in the applicationClass constant of the AppInfo class in classes.dex. From the analysis of the example, we can know that applicationClass is com.xzh.demo.MyApplication. Through reflection, the real application is created.

After reading attachBaseContext, let's continue to look at BootstrapApplication();

BootstrapApplication()

Let's first look at the onCreate method:

onCreate()

  1. public void onCreate() {
  2. if (!AppInfo.usingApkSplits) {
  3. MonkeyPatcher.monkeyPatchApplication(this, this, this.realApplication, this.externalResourcePath);
  4. MonkeyPatcher.monkeyPatchExistingResources(this, this.externalResourcePath, null );
  5. } else {
  6. MonkeyPatcher.monkeyPatchApplication(this, this, this.realApplication, null );
  7. }
  8. super.onCreate();
  9. if (AppInfo.applicationId != null ) {
  10. try {
  11. boolean foundPackage = false ;
  12. int pid = Process.myPid();
  13. ActivityManager manager = (ActivityManager) getSystemService( "activity" );
  14. List processes = manager.getRunningAppProcesses();
  15. boolean startServer = false ;
  16. if ((processes != null ) && (processes. size () > 1)) {
  17. for (ActivityManager.RunningAppProcessInfo processInfo : processes) {
  18. if (AppInfo.applicationId.equals(processInfo.processName)) {
  19. foundPackage = true ;
  20. if (processInfo.pid == pid) {
  21. startServer = true ;
  22. break;
  23. }
  24. }
  25. }
  26. if ((!startServer) && (!foundPackage)) {
  27. startServer = true ;
  28. if (Log.isLoggable( "InstantRun" , 2)) {
  29. Log.v( "InstantRun" , "Multiprocess but didn't find process with package: starting server anyway" );
  30. }
  31. }
  32. } else {
  33. startServer = true ;
  34. }
  35. if (startServer) {
  36. Server.create (AppInfo.applicationId, this);
  37. }
  38. } catch (Throwable t) {
  39. if (Log.isLoggable( "InstantRun" , 2)) {
  40. Log.v( "InstantRun" , "Failed during multi process check" , t);
  41. }
  42. Server.create (AppInfo.applicationId, this);
  43. }
  44. }
  45. if (this.realApplication != null ) {
  46. this.realApplication.onCreate();
  47. }
  48. }

In onCreate() we need to pay attention to the following methods:

monkeyPatchApplication → monkeyPatchExistingResources → Server startup → call realApplication's onCreate method

monkeyPatchApplication

  1. public   static void monkeyPatchApplication(Context context, Application bootstrap, Application realApplication, String externalResourceFile) {
  2. try {
  3. Class activityThread = Class.forName( "android.app.ActivityThread" );
  4. Object currentActivityThread = getActivityThread(context, activityThread);
  5. Field mInitialApplication = activityThread.getDeclaredField( "mInitialApplication" );
  6. mInitialApplication.setAccessible( true );
  7. Application initialApplication = (Application) mInitialApplication.get(currentActivityThread);
  8. if ((realApplication != null ) && (initialApplication == bootstrap)) {
  9. mInitialApplication.set (currentActivityThread, realApplication);
  10. }
  11. if (realApplication != null ) {
  12. Field mAllApplications = activityThread.getDeclaredField( "mAllApplications" );
  13. mAllApplications.setAccessible( true );
  14. List allApplications = (List) mAllApplications.get(currentActivityThread);
  15. for ( int i = 0; i < allApplications. size (); i++) {
  16. if (allApplications.get(i) == bootstrap) {
  17. allApplications.set ( i, realApplication);
  18. }
  19. }
  20. }
  21. Class loadedApkClass;
  22. try {
  23. loadedApkClass = Class.forName( "android.app.LoadedApk" );
  24. } catch (ClassNotFoundException e) {
  25. loadedApkClass = Class.forName( "android.app.ActivityThread$PackageInfo" );
  26. }
  27. Field mApplication = loadedApkClass.getDeclaredField( "mApplication" );
  28. mApplication.setAccessible( true );
  29. Field mResDir = loadedApkClass.getDeclaredField( "mResDir" );
  30. mResDir.setAccessible( true );
  31. Field mLoadedApk = null ;
  32. try {
  33. mLoadedApk = Application.class.getDeclaredField( "mLoadedApk" );
  34. } catch (NoSuchFieldException e) {
  35. }
  36. for (String fieldName : new String[] { "mPackages" , "mResourcePackages" }) {
  37. Field field = activityThread.getDeclaredField(fieldName);
  38. field.setAccessible( true );
  39. Object value = field.get(currentActivityThread);
  40. for (Map.Entry> entry : ((Map>) value).entrySet()) {
  41. Object loadedApk = ((WeakReference) entry.getValue()).get();
  42. if (loadedApk != null ) {
  43. if (mApplication.get(loadedApk) == bootstrap) {
  44. if (realApplication != null ) {
  45. mApplication.set (loadedApk, realApplication);
  46. }
  47. if (externalResourceFile != null ) {
  48. mResDir.set (loadedApk, externalResourceFile);
  49. }
  50. if ((realApplication != null ) && (mLoadedApk != null )) {
  51. mLoadedApk.set (realApplication, loadedApk);
  52. }
  53. }
  54. }
  55. }
  56. }
  57. } catch (Throwable e) {
  58. throw new IllegalStateException(e);
  59. }
  60. }

Description: This method replaces all current app's application with realApplication.

The replacement process is as follows:

1. Replace ActivityThread's mInitialApplication with realApplication

2. Replace all Application in mAllApplications with realApplication

3. Replace the application in mLoaderApk in mPackages and mResourcePackages of ActivityThread with realApplication.

monkeyPatchExistingResources

  1. public   static void monkeyPatchExistingResources(Context context, String externalResourceFile, Collection activities) {
  2. if (externalResourceFile == null ) {
  3. return ;
  4. }
  5. try {
  6. AssetManager newAssetManager = (AssetManager) AssetManager.class.getConstructor(new Class[0]).newInstance(new Object[0]);
  7. Method mAddAssetPath = AssetManager.class.getDeclaredMethod(
  8. "addAssetPath" , new Class[] { String.class });
  9. mAddAssetPath.setAccessible( true );
  10. if ((( Integer ) mAddAssetPath.invoke(newAssetManager, new Object[] { externalResourceFile })).intValue() == 0) {
  11. throw new IllegalStateException(
  12. "Could not create new AssetManager" );
  13. }
  14. Method mEnsureStringBlocks = AssetManager.class.getDeclaredMethod( "ensureStringBlocks" , new Class[0]);
  15. mEnsureStringBlocks.setAccessible( true );
  16. mEnsureStringBlocks.invoke(newAssetManager, new Object[0]);
  17. if (activities != null ) {
  18. for (Activity activity : activities) {
  19. Resources resources = activity.getResources();
  20. try {
  21. Field mAssets = Resources.class.getDeclaredField( "mAssets" );
  22. mAssets.setAccessible( true );
  23. mAssets.set (resources, newAssetManager);
  24. } catch (Throwable ignore ) {
  25. Field mResourcesImpl = Resources.class.getDeclaredField( "mResourcesImpl" );
  26. mResourcesImpl.setAccessible( true );
  27. Object resourceImpl = mResourcesImpl.get(resources);
  28. Field implAssets = resourceImpl.getClass().getDeclaredField( "mAssets" );
  29. implAssets.setAccessible( true );
  30. implAssets.set (resourceImpl, newAssetManager);
  31. }
  32. Resources.Theme theme = activity.getTheme();
  33. try {
  34. try {
  35. Field ma = Resources.Theme.class.getDeclaredField( "mAssets" );
  36. ma.setAccessible( true );
  37. ma.set (theme, newAssetManager);
  38. } catch (NoSuchFieldException ignore ) {
  39. Field themeField = Resources.Theme.class.getDeclaredField( "mThemeImpl" );
  40. themeField.setAccessible( true );
  41. Object impl = themeField.get(theme);
  42. Field ma = impl.getClass().getDeclaredField( "mAssets" );
  43. ma.setAccessible( true );
  44. ma.set (impl, newAssetManager);
  45. }
  46. Field mt = ContextThemeWrapper.class.getDeclaredField( "mTheme" );
  47. mt.setAccessible( true );
  48. mt.set (activity, null );
  49. Method mtm = ContextThemeWrapper.class.getDeclaredMethod( "initializeTheme" , new Class[0]);
  50. mtm.setAccessible( true );
  51. mtm.invoke(activity, new Object[0]);
  52. Method mCreateTheme = AssetManager.class.getDeclaredMethod( "createTheme" , new Class[0]);
  53. mCreateTheme.setAccessible( true );
  54. Object internalTheme = mCreateTheme.invoke(newAssetManager, new Object[0]);
  55. Field mTheme = Resources.Theme.class.getDeclaredField( "mTheme" );
  56. mTheme.setAccessible( true );
  57. mTheme.set (theme, internalTheme);
  58. } catch (Throwable e) {
  59. Log.e( "InstantRun" , "Failed to update existing theme for activity " + activity, e);
  60. }
  61. pruneResourceCaches(resources);
  62. }
  63. }
  64. Collection> references ;
  65. if (Build.VERSION.SDK_INT >= 19) {
  66. Class resourcesManagerClass = Class.forName( "android.app.ResourcesManager" );
  67. Method mGetInstance = resourcesManagerClass.getDeclaredMethod( "getInstance" , new Class[0]);
  68. mGetInstance.setAccessible( true );
  69. Object resourcesManager = mGetInstance.invoke( null , new Object[0]);
  70. try {
  71. Field fMActiveResources = resourcesManagerClass.getDeclaredField( "mActiveResources" );
  72. fMActiveResources.setAccessible( true );
  73. <ArrayMap> arrayMap = (ArrayMap) fMActiveResources.get(resourcesManager);
  74. references = arrayMap. values ​​();
  75. } catch (NoSuchFieldException ignore ) {
  76. Field mResourceReferences = resourcesManagerClass.getDeclaredField( "mResourceReferences" );
  77. mResourceReferences.setAccessible( true );
  78. references = (Collection) mResourceReferences.get(resourcesManager);
  79. }
  80. } else {
  81. Class activityThread = Class.forName( "android.app.ActivityThread" );
  82. Field fMActiveResources = activityThread.getDeclaredField( "mActiveResources" );
  83. fMActiveResources.setAccessible( true );
  84. Object thread = getActivityThread(context, activityThread);
  85. <HashMap> map = (HashMap) fMActiveResources.get(thread);
  86. references = map.values () ;
  87. }
  88. for (WeakReference wr : references ) {
  89. Resources resources = (Resources) wr.get();
  90. if (resources != null ) {
  91. try {
  92. Field mAssets = Resources.class.getDeclaredField( "mAssets" );
  93. mAssets.setAccessible( true );
  94. mAssets.set (resources, newAssetManager);
  95. } catch (Throwable ignore ) {
  96. Field mResourcesImpl = Resources.class.getDeclaredField( "mResourcesImpl" );
  97. mResourcesImpl.setAccessible( true );
  98. Object resourceImpl = mResourcesImpl.get(resources);
  99. Field implAssets = resourceImpl.getClass().getDeclaredField( "mAssets" );
  100. implAssets.setAccessible( true );
  101. implAssets.set (resourceImpl, newAssetManager);
  102. }
  103. resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrics());
  104. }
  105. }
  106. } catch (Throwable e) {
  107. throw new IllegalStateException(e);
  108. }
  109. }

Description: This method replaces all mAssets of the current app with newAssetManager.

The process of monkeyPatchExistingResources is as follows:

1. If the resource.ap_ file has changed, create a new AssetManager object newAssetManager, and then replace all current mAssets member variables of Resource and Resource.Theme with the newAssetManager object.

2. If an Activity is already started, you also need to replace the mAssets member variables in all Activities

Determine whether the Server has been started. If not, start the Server. Then call the onCreate method of realApplication to delegate the life cycle of realApplication.

Next, let's analyze the issues that the Server is responsible for, such as **hot deployment**, **warm deployment**, and **cold deployment**.

Server hot deployment, warm deployment and cold deployment

First, focus on the internal class SocketServerReplyThread of Server.

SocketServerReplyThread

  1. private class SocketServerReplyThread extends Thread {
  2. private final LocalSocket mSocket;
  3.  
  4. SocketServerReplyThread(LocalSocket socket) {
  5. this.mSocket = socket;
  6. }
  7.  
  8. public void run() {
  9. try {
  10. DataInputStream input = new DataInputStream(this.mSocket.getInputStream());
  11. DataOutputStream output = new DataOutputStream(this.mSocket.getOutputStream());
  12. try {
  13. handle(input, output );
  14. finally
  15. try {
  16. input.close ();
  17. } catch (IOException ignore ) {
  18. }
  19. try {
  20. output . close ();
  21. } catch (IOException ignore ) {
  22. }
  23. }
  24. return ;
  25. } catch (IOException e) {
  26. if (Log.isLoggable( "InstantRun" , 2)) {
  27. Log.v( "InstantRun" , "Fatal error receiving messages" , e);
  28. }
  29. }
  30. }
  31.  
  32. private void handle(DataInputStream input, DataOutputStream output ) throws IOException {
  33. long magic = input.readLong();
  34. if (magic != 890269988L) {
  35. Log.w( "InstantRun" , "Unrecognized header format " + Long.toHexString(magic));
  36. return ;
  37. }
  38. int version = input.readInt();
  39. output .writeInt(4);
  40. if (version != 4) {
  41. Log.w( "InstantRun" , "Mismatched protocol versions; app is using version 4 and tool is using version " + version);
  42. } else {
  43. int message;
  44. for (; ; ) {
  45. message = input.readInt();
  46. switch (message) {
  47. case 7:
  48. if (Log.isLoggable( "InstantRun" , 2)) {
  49. Log.v( "InstantRun" , "Received EOF from the IDE" );
  50. }
  51. return ;
  52. case 2:
  53. boolean active = Restarter.getForegroundActivity(Server.this.mApplication) != null ;
  54. output .writeBoolean(active);
  55. if (Log.isLoggable( "InstantRun" , 2)) {
  56. Log.v( "InstantRun" , "Received Ping message from the IDE; returned active = " + active);
  57. }
  58. break;
  59. case 3:
  60. String path = input.readUTF();
  61. long size = FileManager.getFileSize(path);
  62. output .writeLong( size );
  63. if (Log.isLoggable( "InstantRun" , 2)) {
  64. Log.v( "InstantRun" , "Received path-exists(" + path + ") from the " + "IDE; returned size=" + size );
  65. }
  66. break;
  67. case 4:
  68. long begin = System.currentTimeMillis();
  69. path = input.readUTF();
  70. byte[] checksum = FileManager.getCheckSum(path);
  71. if (checksum != null ) {
  72. output .writeInt(checksum.length);
  73. output .write(checksum);
  74. if (Log.isLoggable( "InstantRun" , 2)) {
  75. long end = System.currentTimeMillis();
  76. String hash = new BigInteger(1, checksum)
  77. .toString(16);
  78. Log.v( "InstantRun" , "Received checksum(" + path
  79. + ") from the " + "IDE: took "  
  80. + ( end - begin ) + "ms to compute "  
  81. + hash);
  82. }
  83. } else {
  84. output .writeInt(0);
  85. if (Log.isLoggable( "InstantRun" , 2)) {
  86. Log.v( "InstantRun" , "Received checksum(" + path
  87. + ") from the "  
  88. + "IDE: returning " );
  89. }
  90. }
  91. break;
  92. case 5:
  93. if (!authenticate(input)) {
  94. return ;
  95. }
  96. Activity activity = Restarter
  97. .getForegroundActivity(Server.this.mApplication);
  98. if (activity != null ) {
  99. if (Log.isLoggable( "InstantRun" , 2)) {
  100. Log.v( "InstantRun" ,
  101. "Restarting activity per user request" );
  102. }
  103. Restarter.restartActivityOnUiThread(activity);
  104. }
  105. break;
  106. case 1:
  107. if (!authenticate(input)) {
  108. return ;
  109. }
  110. List changes = ApplicationPatch
  111. .read (input);
  112. if (changes != null ) {
  113. boolean hasResources = Server.hasResources(changes);
  114. int updateMode = input.readInt();
  115. updateMode = Server.this.handlePatches(changes,
  116. hasResources, updateMode);
  117. boolean showToast = input.readBoolean();
  118. output .writeBoolean( true );
  119. Server.this.restart(updateMode, hasResources,
  120. showToast);
  121. }
  122. break;
  123. case 6:
  124. String text = input.readUTF();
  125. Activity foreground = Restarter
  126. .getForegroundActivity(Server.this.mApplication);
  127. if (foreground != null ) {
  128. Restarter.showToast(foreground, text);
  129. } else if (Log.isLoggable( "InstantRun" , 2)) {
  130. Log.v( "InstantRun" ,
  131. "Couldn't show toast (no activity) : "  
  132. + text);
  133. }
  134. break;
  135. }
  136. }
  137. }
  138. }
  139. }

Note: After the socket is opened, it starts reading data. When it reads 1, it obtains the ApplicationPatch list of code changes, and then calls handlePatches to handle the code changes.

handlePatches

  1. private int handlePatches(List changes,
  2. boolean hasResources, int updateMode) {
  3. if (hasResources) {
  4. FileManager.startUpdate();
  5. }
  6. for (ApplicationPatch change : changes) {
  7. String path = change.getPath();
  8. if (path.endsWith( ".dex" )) {
  9. handleColdSwapPatch(change);
  10. boolean canHotSwap = false ;
  11. for (ApplicationPatch c : changes) {
  12. if (c.getPath().equals( "classes.dex.3" )) {
  13. canHotSwap = true ;
  14. break;
  15. }
  16. }
  17. if (!canHotSwap) {
  18. updateMode = 3;
  19. }
  20. } else if (path.equals( "classes.dex.3" )) {
  21. updateMode = handleHotSwapPatch(updateMode, change);
  22. } else if (isResourcePath(path)) {
  23. updateMode = handleResourcePatch(updateMode, change, path);
  24. }
  25. }
  26. if (hasResources) {
  27. FileManager.finishUpdate( true );
  28. }
  29. return updateMode;
  30. }

Note: This method mainly determines the mode (hot deployment, warm deployment or cold deployment) by judging the content of Change.

  • If the suffix is ​​".dex", cold deployment handles handleColdSwapPatch
  • If the suffix is ​​"classes.dex.3", hot deployment handles handleHotSwapPatch
  • In other cases, warm deployment, handle resource handleResourcePatch

handleColdSwapPatchCold deployment

  1. private static void handleColdSwapPatch(ApplicationPatch patch) {
  2. if (patch.path.startsWith( "slice-" )) {
  3. File file = FileManager.writeDexShard(patch.getBytes(), patch.path);
  4. if (Log.isLoggable( "InstantRun" , 2)) {
  5. Log.v( "InstantRun" , "Received dex shard " + file);
  6. }
  7. }
  8. }

Note: This method writes the dex file to a private directory and waits for the entire app to restart. After restarting, use the IncrementalClassLoader mentioned above to load dex.

handleHotSwapPatch hot deployment

  1. private int handleHotSwapPatch( int updateMode, ApplicationPatch patch) {
  2. if (Log.isLoggable( "InstantRun" , 2)) {
  3. Log.v( "InstantRun" , "Received incremental code patch" );
  4. }
  5. try {
  6. String dexFile = FileManager.writeTempDexFile(patch.getBytes());
  7. if (dexFile == null ) {
  8. Log.e( "InstantRun" , "No file to write the code to" );
  9. return updateMode;
  10. }
  11. if (Log.isLoggable( "InstantRun" , 2)) {
  12. Log.v( "InstantRun" , "Reading live code from " + dexFile);
  13. }
  14. String nativeLibraryPath = FileManager.getNativeLibraryFolder()
  15. .getPath();
  16. DexClassLoader dexClassLoader = new DexClassLoader(dexFile,
  17. this.mApplication.getCacheDir().getPath(),
  18. nativeLibraryPath, getClass().getClassLoader());
  19. Class aClass = Class.forName(
  20. "com.android.tools.fd.runtime.AppPatchesLoaderImpl" , true ,
  21. dexClassLoader);
  22. try {
  23. if (Log.isLoggable( "InstantRun" , 2)) {
  24. Log.v( "InstantRun" , "Got the patcher class " + aClass);
  25. }
  26. PatchesLoader loader = (PatchesLoader) aClass.newInstance();
  27. if (Log.isLoggable( "InstantRun" , 2)) {
  28. Log.v( "InstantRun" , "Got the patcher instance " + loader);
  29. }
  30. String[] getPatchedClasses = (String[]) aClass
  31. .getDeclaredMethod( "getPatchedClasses" , new Class[0])
  32. .invoke(loader, new Object[0]);
  33. if (Log.isLoggable( "InstantRun" , 2)) {
  34. Log.v( "InstantRun" , "Got the list of classes " );
  35. for (String getPatchedClass : getPatchedClasses) {
  36. Log.v( "InstantRun" , "class " + getPatchedClass);
  37. }
  38. }
  39. if (!loader. load ()) {
  40. updateMode = 3;
  41. }
  42. } catch (Exception e) {
  43. Log.e( "InstantRun" , "Couldn't apply code changes" , e);
  44. e.printStackTrace();
  45. updateMode = 3;
  46. }
  47. } catch (Throwable e) {
  48. Log.e( "InstantRun" , "Couldn't apply code changes" , e);
  49. updateMode = 3;
  50. }
  51. return updateMode;
  52. }

Description: This method writes the patch dex file to a temporary directory, and then uses DexClassLoader to load dex. Then it reflects and 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 code of the AbstractPatchesLoaderImpl abstract class is as follows:

  1. public abstract class AbstractPatchesLoaderImpl implements PatchesLoader {
  2. public abstract String[] getPatchedClasses();
  3. public boolean load () {
  4. try {
  5. for (String className : getPatchedClasses()) {
  6. ClassLoader cl = getClass().getClassLoader();
  7. Class aClass = cl.loadClass(className + "$override" );
  8. Object o = aClass.newInstance();
  9. Class originalClass = cl.loadClass(className);
  10. Field changeField = originalClass.getDeclaredField( "$change" );
  11. changeField.setAccessible( true );
  12. Object previous = changeField.get( null );
  13. if (previous != null ) {
  14. Field isObsolete = previous.getClass().getDeclaredField( "$obsolete" );
  15. if (isObsolete != null ) {
  16. isObsolete.set ( null , Boolean.valueOf( true )) ;
  17. }
  18. }
  19. changeField.set ( null , o);
  20. if ((Log.logging != null ) && (Log.logging.isLoggable( Level .FINE))) {
  21. Log.logging.log( Level .FINE, String.format( "patched %s" , new Object[] { className }));
  22. }
  23. }
  24. } catch (Exception e) {
  25. if (Log.logging != null ) {
  26. Log.logging.log( Level .SEVERE, String.format( "Exception while patching %s" , new Object[] { "foo.bar" }), e);
  27. }
  28. return   false ;
  29. }
  30. return   true ;
  31. }
  32. }

Instant Run Hot Deployment Principle

Based on the above code analysis, we can analyze the process of Instant Run as follows:

1. When building apk for the first time, a member variable of $change is injected into each class. It implements the IncrementalChange interface and inserts a similar piece of logic into each method.

  1. IncrementalChange localIncrementalChange = $change;
  2. if (localIncrementalChange != null ) {
  3. localIncrementalChange.access$dispatch( "onCreate.(Landroid/os/Bundle;)V" , new Object[] { this, ... });
  4. return ;
  5. }

When $change is not empty, execute the IncrementalChange method.

2. After we modify the implementation of the method in the code, click InstantRun, which will generate the corresponding patch file to record the content you modified. The replacement class in the patch file is to append $override to the modified class name and implement the IncrementalChange interface.

3. Generate the AppPatchesLoaderImpl class, inherit from AbstractPatchesLoaderImpl, and implement the getPatchedClasses method to record which classes have been modified.

4. After calling the load method, load the corresponding $override class according to the list of modified classes returned by getPatchedClasses, and then set the $change of the original class to the corresponding $override class that implements the IncrementalChange interface.

Summary of Instant Run Operation Mechanism

The Instant Run operation mechanism mainly involves hot deployment, warm deployment and cold deployment, mainly during the first run, during the app run, when there is code modification.

First time compilation

1. Package Instant-Run.jar and instant-Run-bootstrap.jar into the main dex

2. Replace the application configuration in AndroidManifest.xml

3. Use the asm tool, add $change to each class, and add logic before each method.

4. Compile the source code into dex and store it in the compressed package instant-run.zip

App runtime

1. Get the path to resource resource.ap_ after the change

2. Set ClassLoader. setupClassLoader:

Use IncrementalClassLoader to load the apk code, and change the original BootClassLoader → PathClassLoader to BootClassLoader → IncrementalClassLoader → PathClassLoader inheritance relationship.

3.createRealApplication:

Create apk real application

4.monkeyPatchApplication

Reflection replaces various Application member variables in ActivityThread

5.monkeyPatchExistingResource

Reflection replaces all existing AssetManager objects

6. Call the onCreate method of realApplication

7. Start Server and Socket receives patch list

When there is code modification

1. Generate the corresponding $override class

2. Generate the AppPatchesLoaderImpl class and record the modified class list

3. Package it into patch and pass it to the app through socket

4. After the app server receives the patch, it waits for the patch to be processed according to handleColdSwapPatch, handleHotSwapPatch, handleResourcePatch.

5. Restart makes patch effective

In Android plug-in, Android hot repair, and apk shelling/unshelling, we borrowed the Instant Run running mechanism, so understanding the Instant Run running mechanism is very helpful for deeper research and is also reference for our own writing framework.

<<:  Basic concepts and implementation of genetic algorithms (with Java implementation examples)

>>:  Summarize some suggestions from Effective Java that can help Android development

Recommend

How to identify and acquire high-value super users?

Super users are users who are willing to pay for ...

Short video APP product analysis report!

The structural framework of this article is shown...

Zhihu Promotion and Traffic Draining Strategy

Today I will share with you a low-cost, high-retu...

A brief discussion on the five steps of online operation and promotion

Operation promotion plays a very important role i...

Get the LaunchImage of the app

[[153741]] The management of LaunchImage is actua...

How to sort out the core process of the activity?

I don’t know if you have noticed that after you h...

Maximizing the value of the supply chain LePar is not a simple O2O

In the era of Internet TV, LeTV, which uses both ...

Official first revelation: Does a bracelet tied to a dog count as WeChat steps?

Since WeChat launched the walking rankings, many f...

Apple releases iOS 11.4 beta 2 with more than just ClassKit

Recently, Apple released iOS 11.4 beta 2. Two wee...