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

目录
相关文章
|
28天前
|
移动开发 监控 安全
mPaaS常见问题之Android集成dexPatch热修复运行时候无法正常进行热更新如何解决
mPaaS(移动平台即服务,Mobile Platform as a Service)是阿里巴巴集团提供的一套移动开发解决方案,它包含了一系列移动开发、测试、监控和运营的工具和服务。以下是mPaaS常见问题的汇总,旨在帮助开发者和企业用户解决在使用mPaaS产品过程中遇到的各种挑战
34 0
|
3月前
|
Linux Android开发
Android 正常运行所需的一系列 Linux 内核接口
Android 正常运行所需的一系列 Linux 内核接口
53 0
|
4月前
|
Java 关系型数据库 数据库
Android App连接真机步骤与APP的开发语言和工程结构讲解以及运行实例(超详细必看)
Android App连接真机步骤与APP的开发语言和工程结构讲解以及运行实例(超详细必看)
36 0
|
7月前
|
Java 数据库 Android开发
性能提示-流畅运行的Android应用
性能提示-流畅运行的Android应用
45 0
|
6月前
|
存储 传感器 定位技术
《移动互联网技术》 第四章 移动应用开发: Android Studio开发环境的使用方法:建立工程,编写源程序,编译链接,安装模拟器,通过模拟器运行和调试程序
《移动互联网技术》 第四章 移动应用开发: Android Studio开发环境的使用方法:建立工程,编写源程序,编译链接,安装模拟器,通过模拟器运行和调试程序
65 0
|
2月前
|
编译器 开发工具 Android开发
|
8月前
|
IDE Java 开发工具
Android Gradle plugin requires Java 11 to run. You are currently using Java 1.8的解决方案
Android Gradle plugin requires Java 11 to run. You are currently using Java 1.8的解决方案
|
4月前
|
网络协议 Android开发 虚拟化
Android Studio无法运行程序调试程序出现Unable to connect to ADB.Check the Event Log for possible issues.Verify th
Android Studio无法运行程序调试程序出现Unable to connect to ADB.Check the Event Log for possible issues.Verify th
59 0
Android Studio无法运行程序调试程序出现Unable to connect to ADB.Check the Event Log for possible issues.Verify th
|
6月前
|
Android开发
Android推送运行报错的问题
Android推送运行报错的问题
42 2
|
8月前
|
XML Java Android开发
#4,Android Studio Android程序结构 工程目录介绍 文件作用 运行配置文件AndroidManifest.xml
#4,Android Studio Android程序结构 工程目录介绍 文件作用 运行配置文件AndroidManifest.xml