深度理解Android InstantRun原理(一)

MolZIR 8年前
   <p>简单介绍一下Instant Run,它是Android Studio2.0以后新增的一个运行机制,能够显著减少你第二次及以后的构建和部署时间。简单通俗的解释就是,当你在Android Studio中改了你的代码,Instant Run可以很快的让你看到你修改的效果。而在没有Instant Run之前,你的一个小小的修改,都肯能需要几十秒甚至更长的等待才能看到修改后的效果。</p>    <h3><strong>传统的代码修改及编译部署流程</strong></h3>    <p style="text-align:center"><img src="https://simg.open-open.com/show/c0648c6c155e5e0701e50c2eed7e5705.png"></p>    <p style="text-align:center"><em>构建整个apk → 部署app → app重启 → 重启Activity</em></p>    <p>而Instant Run则需要更少的时间。</p>    <h3><strong>Instant Run编译和部署流程</strong></h3>    <p style="text-align:center"><img src="https://simg.open-open.com/show/33b0717c025cfc47197e58f2acb43dc7.jpg"></p>    <p style="text-align:center"><em>只构建修改的部分 → 部署修改的dex或资源 → 热部署,温部署,冷部署</em></p>    <p><strong>热部署</strong></p>    <p>Incremental code changes are applied and reflected in the app without needing to relaunch the app or even restart the current Activity. Can be used for most simple changes within method implementations.</p>    <p>方法内的简单修改,无需重启app和Activity</p>    <p><strong>温部署</strong></p>    <p>The Activity needs to be restarted before changes can be seen and used. Typically required for changes to resources.</p>    <p>app无需重启,但是activity需要重启,比如资源的修改。</p>    <p><strong>冷部署</strong></p>    <p>The app is restarted (but still not reinstalled). Required for any structural changes such as to inheritance or method signatures.</p>    <p>app需要重启,比如继承关系的改变或方法的签名变化等。</p>    <p>上述说这么多概念,估计大家对Instant Run应该有了大体的认知了。那么它的实现原理是什么呢?其实,在没有看案例之前,我基本上可以猜测到Instant Run的思路,基于目前比较火的插件化框架,是比较容易理解Instant Run的。但Instant Run毕竟是Google官方的工具,具有很好的借鉴意义。</p>    <h2><strong>Demo案例</strong></h2>    <p>新建一个简单的android studio项目,新建自己的MyApplication,在AndroidManifest文件中设置:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/a9e2fa854d53c976dc311c4183c9c3d9.png"></p>    <p>首先,我们先反编译一下APK的构成:</p>    <p>使用的工具:d2j-dex2jar 和jd-gui</p>    <p><img src="https://simg.open-open.com/show/92f9f0d243bb7e844f53984ddf14649d.png"></p>    <p>里面有2个dex文件和一个instant-run.zip文件。首先分别看一下两个dex文件的源码:</p>    <p>classes.dex的反编译之后的源码:</p>    <p><img src="https://simg.open-open.com/show/7a2c3e85948c8464ecb842407abe79fe.png"></p>    <p>里面只有一个AppInfo,保存了app的基本信息,主要包含了包名和applicationClass。</p>    <p>classes2.dex反编译之后的源码:</p>    <p><img src="https://simg.open-open.com/show/866ec7e9ba811836e9af9296dda6de54.png"></p>    <p>我们赫然发现,两个dex中竟然没有一句我们自己写的代码??那么代码在哪里呢?你可能猜到,app真正的业务dex在instant-run.zip中。解压instant-run.zip之后,如下图所示:</p>    <p><img src="https://simg.open-open.com/show/459b5ced9fc734921bfddc296090f810.jpg"></p>    <p>反编译之后,我们会发现,我们真正的业务代码都在这里。</p>    <p>另外,我们再decode看一下AndroidManifest文件</p>    <p><img src="https://simg.open-open.com/show/571f9bd3ccdf9ab85487fc67cdd72bbc.jpg"></p>    <p>//TODO</p>    <p>我们发现,我们的application也被替换了,替换成了com.android.tools.fd.runtime.BootstrapApplication</p>    <p>看到这里,那么大体的思路,可以猜到:</p>    <p>1.Instant-Run代码作为一个宿主程序,将app作为资源dex加载起来,和插件化一个思路</p>    <p>2.那么InstantRun是怎么把业务代码运行起来的呢?</p>    <h2><strong>InstantRun启动app</strong></h2>    <p>首先BootstrapApplication分析,按照执行顺序,依次分析attachBaseContext和onCreate方法。</p>    <p><strong>1.attachBaseContext方法</strong></p>    <pre>  <code class="language-java">   ...    protected void attachBaseContext(Context context) {        if (!AppInfo.usingApkSplits) {              String apkFile = context.getApplicationInfo().sourceDir;            long apkModified = apkFile != null ? new File(apkFile)                      .lastModified() : 0L;              createResources(apkModified);              setupClassLoaders(context, context.getCacheDir().getPath(),                      apkModified);          }          createRealApplication();        super.attachBaseContext(context);        if (this.realApplication != null) {            try {                  Method attachBaseContext = ContextWrapper.class                          .getDeclaredMethod("attachBaseContext",                                new Class[] { Context.class });                    attachBaseContext.setAccessible(true);                  attachBaseContext.invoke(this.realApplication,                        new Object[] { context });              } catch (Exception e) {                throw new IllegalStateException(e);              }          }      }      ...</code></pre>    <p>我们依次需要关注的方法有:</p>    <p>createResources → setupClassLoaders → createRealApplication → 调用realApplication的attachBaseContext方法</p>    <p><strong>1.1.createResources</strong></p>    <p>首先看createResources方法:</p>    <pre>  <code class="language-java">    private void createResources(long apkModified) {          FileManager.checkInbox();        File file = FileManager.getExternalResourceFile();        this.externalResourcePath = (file != null ? file.getPath() : null);        if (Log.isLoggable("InstantRun", 2)) {              Log.v("InstantRun", "Resource override is "                      + this.externalResourcePath);          }        if (file != null) {            try {                long resourceModified = file.lastModified();                if (Log.isLoggable("InstantRun", 2)) {                      Log.v("InstantRun", "Resource patch last modified: "                              + resourceModified);                      Log.v("InstantRun", "APK last modified: " + apkModified                              + " "                              + (apkModified > resourceModified ? ">" : "<")                              + " resource patch");                  }                if ((apkModified == 0L) || (resourceModified <= apkModified)) {                    if (Log.isLoggable("InstantRun", 2)) {                          Log.v("InstantRun",                                "Ignoring resource file, older than APK");                      }                    this.externalResourcePath = null;                  }              } catch (Throwable t) {                  Log.e("InstantRun", "Failed to check patch timestamps", t);              }          }      }</code></pre>    <p>该方法主要是判断资源resource.ap_是否改变,然后保存resource.ap_的路径到externalResourcePath中</p>    <p><strong>1.2.setupClassLoaders</strong></p>    <pre>  <code class="language-java">   private static void setupClassLoaders(Context context, String codeCacheDir,              long apkModified) {          List<String> dexList = FileManager.getDexList(context, apkModified);        Class<Server> server = Server.class;        Class<MonkeyPatcher> patcher = MonkeyPatcher.class;        if (!dexList.isEmpty()) {            if (Log.isLoggable("InstantRun", 2)) {                Log.v("InstantRun", "Bootstrapping class loader with dex list "                          + join('\n', dexList));              }              ClassLoader classLoader = BootstrapApplication.class                      .getClassLoader();            String nativeLibraryPath;              try {                  nativeLibraryPath = (String) classLoader.getClass()                          .getMethod("getLdLibraryPath", new Class[0])                          .invoke(classLoader, new Object[0]);                if (Log.isLoggable("InstantRun", 2)) {                    Log.v("InstantRun", "Native library path: "                              + nativeLibraryPath);                  }              } catch (Throwable t) {                Log.e("InstantRun", "Failed to determine native library path "                          + t.getMessage());                  nativeLibraryPath = FileManager.getNativeLibraryFolder()                          .getPath();              }              IncrementalClassLoader.inject(classLoader, nativeLibraryPath,                      codeCacheDir, dexList);          }      }</code></pre>    <p>继续看IncrementalClassLoader.inject方法:</p>    <p>IncrementalClassLoader的源码如下:</p>    <pre>  <code class="language-java">   public class IncrementalClassLoader extends ClassLoader {    public static final boolean DEBUG_CLASS_LOADING = false;    private final DelegateClassLoader delegateClassLoader;    public IncrementalClassLoader(ClassLoader original,            String nativeLibraryPath, String codeCacheDir, List<String> dexes) {        super(original.getParent());        this.delegateClassLoader = createDelegateClassLoader(nativeLibraryPath,                  codeCacheDir, dexes, original);      }    public Class<?> findClass(String className) throws ClassNotFoundException {        try {            return this.delegateClassLoader.findClass(className);          } catch (ClassNotFoundException e) {            throw e;          }      }    private static class DelegateClassLoader extends BaseDexClassLoader {        private DelegateClassLoader(String dexPath, File optimizedDirectory,                String libraryPath, ClassLoader parent) {            super(dexPath, optimizedDirectory, libraryPath, parent);          }        public Class<?> findClass(String name) throws ClassNotFoundException {            try {                return super.findClass(name);              } catch (ClassNotFoundException e) {                throw e;              }          }      }    private static DelegateClassLoader createDelegateClassLoader(            String nativeLibraryPath, String codeCacheDir, List<String> dexes,              ClassLoader original) {        String pathBuilder = createDexPath(dexes);        return new DelegateClassLoader(pathBuilder, new File(codeCacheDir),                  nativeLibraryPath, original);      }    private static String createDexPath(List<String> dexes) {          StringBuilder pathBuilder = new StringBuilder();        boolean first = true;        for (String dex : dexes) {            if (first) {                  first = false;              } else {                  pathBuilder.append(File.pathSeparator);              }              pathBuilder.append(dex);          }        if (Log.isLoggable("InstantRun", 2)) {              Log.v("InstantRun", "Incremental dex path is "                      + BootstrapApplication.join('\n', dexes));          }        return pathBuilder.toString();      }    private static void setParent(ClassLoader classLoader, ClassLoader newParent) {        try {              Field parent = ClassLoader.class.getDeclaredField("parent");              parent.setAccessible(true);              parent.set(classLoader, newParent);          } catch (IllegalArgumentException e) {            throw new RuntimeException(e);          } catch (IllegalAccessException e) {            throw new RuntimeException(e);          } catch (NoSuchFieldException e) {            throw new RuntimeException(e);          }      }    public static ClassLoader inject(ClassLoader classLoader,            String nativeLibraryPath, String codeCacheDir, List<String> dexes) {          IncrementalClassLoader incrementalClassLoader = new IncrementalClassLoader(                  classLoader, nativeLibraryPath, codeCacheDir, dexes);            setParent(classLoader, incrementalClassLoader);        return incrementalClassLoader;      }      }</code></pre>    <p>inject方法是用来设置classloader的父子顺序的,使用IncrementalClassLoader来加载dex。由于ClassLoader的双亲委托模式,也就是委托父类加载类,父类中找不到再在本ClassLoader中查找。</p>    <p>调用之后的效果如下图所示:</p>    <p><img src="https://simg.open-open.com/show/cbee234a49f09da0b6b0a2765648b37f.png"></p>    <p style="text-align:center">我们可以在MyApplication中,用代码验证一下</p>    <pre>  <code class="language-java">    @Override      public void onCreate() {          super.onCreate();          try{            Log.d(TAG,"###onCreate in myApplication");            String classLoaderName = getClassLoader().getClass().getName();            Log.d(TAG,"###onCreate in myApplication classLoaderName = "+classLoaderName);            String parentClassLoaderName = getClassLoader().getParent().getClass().getName();            Log.d(TAG,"###onCreate in myApplication parentClassLoaderName = "+parentClassLoaderName);            String pParentClassLoaderName = getClassLoader().getParent().getParent().getClass().getName();            Log.d(TAG,"###onCreate in myApplication pParentClassLoaderName = "+pParentClassLoaderName);          }catch (Exception e){              e.printStackTrace();          }      }</code></pre>    <p>运行结果:</p>    <pre>  <code class="language-java">...06-30 10:43:42.475 27307-27307/mobctrl.net.testinstantrun D/MyApplication: ###onCreate in myApplication classLoaderName = dalvik.system.PathClassLoader06-30 10:43:42.475 27307-27307/mobctrl.net.testinstantrun D/MyApplication: ###onCreate in myApplication parentClassLoaderName = com.android.tools.fd.runtime.IncrementalClassLoader06-30 10:43:42.475 27307-27307/mobctrl.net.testinstantrun D/MyApplication: ###onCreate in myApplication pParentClassLoaderName = java.lang.BootClassLoader</code></pre>    <p>由此,我们已经知道了,当前PathClassLoader委托IncrementalClassLoader加载dex。</p>    <p> </p>    <p>来自:http://mp.weixin.qq.com/s?__biz=MzA4MjA0MTc4NQ==&mid=2651574039&idx=1&sn=23e944c522ca55169279b8317c36752a&scene=1&srcid=0827t6aBnCuSjJCZlasyAsdD#wechat_redirect</p>    <p> </p>