欢迎光临
我们一直在努力

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

赞(0) 打赏
未经允许不得转载:九八云安全 » 深入理解Android Instant Run运行机制

评论 抢沙发