深入理解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()


  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.       } 

我们依次需要关注的方法有:

createResources → setupClassLoaders → createRealApplication → 调用realApplication的attachBaseContext方法

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. }  

说明:该方法主要是判断资源resource.ap_是否改变,然后保存resource.ap_的路径到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. }  

说明,该方法是初始化一个ClassLoaders并调用IncrementalClassLoader。

IncrementalClassLoader的源码如下:


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

inject方法是用来设置classloader的父子顺序的,使用IncrementalClassLoader来加载dex。由于ClassLoader的双亲委托模式,也就是委托父类加载类,父类中找不到再在本ClassLoader中查找。

调用的效果图如下:

为了方便我们对委托父类加载机制的理解,我们可以做一个实验,在我们的application做一些Log。


  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. }  

输出结果:


  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 

由此,我们知道,当前PathClassLoader委托IncrementalClassLoader加载dex。

我们继续对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. }  

该方法就是用classes.dex中的AppInfo类的applicationClass常量中保存的app真实的application。由例子的分析我们可以知道applicationClass就是com.xzh.demo.MyApplication。通过反射的方式,创建真是的realApplication。

看完attachBaseContext我们继续看BootstrapApplication();

BootstrapApplication()

我们首先看一下onCreate方法:

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. }  

在onCreate()中我们需要注意以下方法:

monkeyPatchApplication → monkeyPatchExistingResources → Server启动 → 调用realApplication的onCreate方法

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. }  

说明:该方法的作用是替换所有当前app的application为realApplication。

替换的过程如下:

1.替换ActivityThread的mInitialApplication为realApplication

2.替换mAllApplications 中所有的Application为realApplication

3.替换ActivityThread的mPackages,mResourcePackages中的mLoaderApk中的application为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. }  

说明:该方法的作用是替换所有当前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


  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. }  

说明:socket开启后,开始读取数据,当读到1时,获取代码变化的ApplicationPatch列表,然后调用handlePatches来处理代码的变化。

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. }  

说明:本方法主要通过判断Change的内容,来判断采用什么模式(热部署、温部署或冷部署)

  • 如果后缀为“.dex”,冷部署处理handleColdSwapPatch
  • 如果后缀为“classes.dex.3”,热部署处理handleHotSwapPatch
  • 其他情况,温部署,处理资源handleResourcePatch

handleColdSwapPatch冷部署


  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. }  

说明:该方法把dex文件写到私有目录,等待整个app重启,重启之后,使用前面提到的IncrementalClassLoader加载dex即可。

handleHotSwapPatch热部署


  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. }  

说明:该方法将patch的dex文件写入到临时目录,然后使用DexClassLoader去加载dex。然后反射调用AppPatchesLoaderImpl类的load方法。

需要强调的是:AppPatchesLoaderImpl继承自抽象类AbstractPatchesLoaderImpl,并实现了抽象方法:getPatchedClasses。而AbstractPatchesLoaderImpl抽象类代码如下:


  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.                   return false; 
  28.             } 
  29.             return true; 
  30.       } 
  31. }  

Instant Run热部署原理

由上面的代码分析,我们对Instant Run的流程可以分析如下:

1,在第一次构建apk时,在每一个类中注入了一个$change的成员变量,它实现了IncrementalChange接口,并在每一个方法中,插入了一段类似的逻辑。


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

当$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运行机制对于向更深层次的研究是很有帮助的,对于我们自己书写框架也是有借鉴意义的。

本文作者:佚名

来源:51CTO

时间: 2024-10-25 15:43:37

深入理解Android Instant Run运行机制的相关文章

深入理解Android组件间通信机制对面向对象特性的影响详解_Android

组件的特点对于Android的四大组件Activity, Service, ContentProvider和Service,不能有Setter和Getter,也不能给组件添加接口.原因是组件都是给系统框架调用的,开发者只能实现其规定的回调接口,组件的创建与销毁都是由系统框架控制的,开发者不能强行干预,更没有办法获取组件的对象.比如Activity,Service,BroadcastReceiver,你没有办法去创建一个Activity,Service或BroadcastReceiver,然后像使

深入理解Android之接口回调机制_Android

刚开始学对于这个机制理解不够深刻,现在重新整理下思路.开发中,接口回调是我们经常用到的. 接口回调的意思即,注册之后并不立马执行,而在某个时机触发执行. 首先解决啥是回调: 举个例子:某天,我打电话向你请教问题,当然是个难题,你一时想不出解决方法,我又不能拿着电话在那里傻等,于是我们约定:等你想出办法后打手机通知我,这样,我就挂掉电话办其它事情去了.过了XX分钟,我的手机响了,你兴高采烈的说问题已经搞定,应该如此这般处理.   C不会自己调用b,C提供b的目的就是让S来调用它,而且C不得不提供.

解析Android应用程序运行机制_Android

在标准的Windows和Linux桌面操作系统中,同时可以在不同的窗口中运行多个应用程序,每次只有一个应用程序是当前焦点状态,但其他的应用程序都是一个平等的位置.用户可以随时切换每个应用程序,在不需要应用程序时,也需要用户来关闭应用程序.     但是Android操作系统的应用程序却不是采用这种方式.     Android中每次只有一个应用程序运行在最前面,除了状态栏的位置,当前应用程序将铺满整个屏幕.用户最常见的应用程序就是应用的主页(Home application),这个程序主要显示背

解析Android应用程序运行机制

在标准的Windows和Linux桌面操作系统中,同时可以在不同的窗口中运行多个应用程序,每次只有一个应用程序是当前焦点状态,但其他的应用程序都是一个平等的位置.用户可以随时切换每个应用程序,在不需要应用程序时,也需要用户来关闭应用程序. 但是Android操作系统的应用程序却不是采用这种方式. Android中每次只有一个应用程序运行在最前面,除了状态栏的位置,当前应用程序将铺满整个屏幕.用户最常见的应用程序就是应用的主页(Home application),这个程序主要显示背景图和应用程序快

一:理解ASP.NET的运行机制(例:通过HttpModule来计算页面执行时间)

一:简要介绍一下asp.net的执行步骤 1.IIS接收到客户请求 2. IIS把请求交给aspnet_isapi.dll处理 3.(如果是第一次运行程序)装载bin目录中的dll 4.(如果是第一次运行程序)读取各级webconfig中的配置 5.(如果是第一次运行程序)编译装载global.asax,初始化HttpApplication实例 6.创建响应请求的HttpContext 7.创建承载响应结果的HttpTextWriter 8.找到合适的HttpHandler(asp.net页面)

四:理解Page类的运行机制(例:基于PageStatePersister的页面状态存取)

 有人说类似gridview datalist这样的控件最好不要用在高并发,IO大的网站中企业应用中为了快速开发到可以用一用因为这是一类"沉重"的组件我们姑且不谈这种看法的正确性(我个人觉得有道理)只谈它为什么笨重: 因为这些控件给页面带来了大量的viewstate<input type="hidden" name="____VIEWSTATE" id="____VIEWSTATE" value=这就是页面状态一个页面里

二:理解ASP.NET的运行机制(例:基于HttpHandler的URL重写)

url重写就是把一些类似article.aspx?id=28的路径重写成 article/28/这样的路径 当用户访问article/28/的时候我们通过asp.net把这个请求重定向到article.aspx?id=28路径有两种方法可以做这件事情 一:基于HttpModule的方案这个方案有有缺点,具体缺点以后再谈我曾写过一篇文章<不用组件的url重写(适用于较大型项目) >就是按这个模式写的 二:基于HttpHandler的方案我们这个例子就是按这个方案做的我们接下来就按这种方式做这个例

七:理解控件的运行机制(例:基于CompositeControl命名空间的控件)

组合控件与WebControl控件的事件和属性相差不大组合控件,顾名思义就是把一些控件组合起来形成一个控件这个控件将包含这些控件称为他的子控件 CompositeControl类实现了INameContainer接口这样使得复合控件下的子控件都根据各自的层级关系生成唯一的客户端标识不至于产生重复的ID 组合控件比较重要的方法是:1.EnsureChildControls此方法判断属性ChildControlsCreated是否为true如果不是将执行下面的事件2.CreateChildContr

六:理解控件的运行机制(例:基于WebControl命名空间的控件)

Control类的Render方法在WebControl类中被被分成三部分1:RenderBeginTag,呈现标签的开始2:RenderContents,呈现标签的内容3:RenderEndTag,呈现标签的结束 一般情况下不重写RenderBeginTag和RenderEndTagRenderBeginTag生成什么样的标签由WebControl.TagKey和WebControl.TagName决定默认的TagKey是Span(假设你想直接输出文本,那么你重写的TagKey是Unknown