1

深入理解Android Instant Run运行机制

 2 years ago
source link: http://www.androidchina.net/6714.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

深入理解Android Instant Run运行机制 – Android开发中文站

你的位置:Android开发中文站 > Android开发 > 开发进阶 > 深入理解Android Instant Run运行机制

摘要: Instant Run Instant Run,是android studio2.0新增的一个运行机制,在你编码开发、测试或debug的时候,它都能显著减少你对当前应用的构建和部署的时间。通俗的解释就是,当你在Android Studio中改了你的代码,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的时候,应注意以下几点:

  1. 如果应用的minSdkVersion小于21,可能多数的Instant Run功能会挂掉,这里提供一个解决方法,通过product flavor建立一个minSdkVersion大于21的新分支,用来debug。
  2. Instant Run目前只能在主进程里运行,如果应用是多进程的,类似微信,把webView抽出来单独一个进程,那热、温拔插会被降级为冷拔插。
  3. 在Windows下,Windows Defender Real-Time Protection可能会导致Instant Run挂掉,可用通过添加白名单列表解决。
  4. 暂时不支持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()

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);
            }
      }
}

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

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

createResources()

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);
          }
     }
}

说明:该方法主要是判断资源resource.ap_是否改变,然后保存resource.ap_的路径到externalResourcePath中。

setupClassLoaders()

private static void setupClassLoaders(Context context, String codeCacheDir, long apkModified) {
       List dexList = FileManager.getDexList(context, apkModified);
       Class server = Server.class;
       Class 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);
      }
}

说明,该方法是初始化一个ClassLoaders并调用IncrementalClassLoader。
IncrementalClassLoader的源码如下:

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 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 dexes,
ClassLoader original) {
      String pathBuilder = createDexPath(dexes);
      return new DelegateClassLoader(pathBuilder, new File(codeCacheDir), nativeLibraryPath, original);
}
private static String createDexPath(List 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 dexes) {
     IncrementalClassLoader incrementalClassLoader = new IncrementalClassLoader(classLoader, nativeLibraryPath, codeCacheDir, dexes);
     setParent(classLoader, incrementalClassLoader);
     return incrementalClassLoader;
     }
}

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

调用的效果图如下:

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

@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();
     }
}

输出结果:

03-20 10:43:42.475 27307-27307/mobctrl.net.testinstantrun D/MyApplication: ###onCreate in myApplication classLoaderName = dalvik.system.PathClassLoader
03-20 10:43:42.475 27307-27307/mobctrl.net.testinstantrun D/MyApplication: ###onCreate in myApplication parentClassLoaderName = com.android.tools.fd.runtime.IncrementalClassLoader
03-20 10:43:42.475 27307-27307/mobctrl.net.testinstantrun D/MyApplication: ###onCreate in myApplication pParentClassLoaderName = java.lang.BootClassLoader

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

我们继续对attachBaseContext()继续分析:

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

createRealApplication

private void createRealApplication() {
      if (AppInfo.applicationClass != null) {
           if (Log.isLoggable("InstantRun", 2)) {
                Log.v("InstantRun", "About to create real application of class name = " + AppInfo.applicationClass);
           }
           try {
               Class realClass = (Class) Class.forName(AppInfo.applicationClass);
               if (Log.isLoggable("InstantRun", 2)) {
                    Log.v("InstantRun", "Created delegate app class successfully : "
                    + realClass + " with class loader "
                    + realClass.getClassLoader());
               }
               Constructor constructor = realClass.getConstructor(new Class[0]);
               this.realApplication = ((Application) constructor.newInstance(new Object[0]));
               if (Log.isLoggable("InstantRun", 2)) {
                    Log.v("InstantRun", "Created real app instance successfully :" + this.realApplication);
               }
          } catch (Exception e) {
               throw new IllegalStateException(e);
          }
     } else {
          this.realApplication = new Application();
     }
}

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

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

BootstrapApplication()

我们首先看一下onCreate方法:

onCreate()

public void onCreate() {
      if (!AppInfo.usingApkSplits) {
           MonkeyPatcher.monkeyPatchApplication(this, this, this.realApplication, this.externalResourcePath);
           MonkeyPatcher.monkeyPatchExistingResources(this, this.externalResourcePath, null);
      } else {
           MonkeyPatcher.monkeyPatchApplication(this, this, this.realApplication, null);
      }
      super.onCreate();
      if (AppInfo.applicationId != null) {
           try {
                boolean foundPackage = false;
                int pid = Process.myPid();
                ActivityManager manager = (ActivityManager) getSystemService("activity");
                List processes = manager.getRunningAppProcesses();
                boolean startServer = false;
                if ((processes != null) && (processes.size() > 1)) {
                      for (ActivityManager.RunningAppProcessInfo processInfo : processes) {
                           if (AppInfo.applicationId.equals(processInfo.processName)) {
                                 foundPackage = true;
                                 if (processInfo.pid == pid) {
                                       startServer = true;
                                       break;
                                 }
                           }
                      }
                      if ((!startServer) && (!foundPackage)) {
                           startServer = true;
                           if (Log.isLoggable("InstantRun", 2)) {
                                 Log.v("InstantRun", "Multiprocess but didn't find process with package: starting server anyway");
                           }
                      }
                } else {
                      startServer = true;
                }
                if (startServer) {
                      Server.create(AppInfo.applicationId, this);
                }
           } catch (Throwable t) {
                if (Log.isLoggable("InstantRun", 2)) {
                      Log.v("InstantRun", "Failed during multi process check", t);
                }
                Server.create(AppInfo.applicationId, this);
           }
      }
      if (this.realApplication != null) {
            this.realApplication.onCreate();
      }
}

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

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

monkeyPatchApplication

public static void monkeyPatchApplication(Context context, Application bootstrap, Application realApplication, String externalResourceFile) {
      try {
           Class activityThread = Class.forName("android.app.ActivityThread");
           Object currentActivityThread = getActivityThread(context, activityThread);
           Field mInitialApplication = activityThread.getDeclaredField("mInitialApplication");
           mInitialApplication.setAccessible(true);
           Application initialApplication = (Application) mInitialApplication.get(currentActivityThread);
           if ((realApplication != null) && (initialApplication == bootstrap)) {
                 mInitialApplication.set(currentActivityThread, realApplication);
           }
           if (realApplication != null) {
                Field mAllApplications = activityThread.getDeclaredField("mAllApplications");
                mAllApplications.setAccessible(true);
                List allApplications = (List) mAllApplications.get(currentActivityThread);
                for (int i = 0; i < allApplications.size(); i++) {
                     if (allApplications.get(i) == bootstrap) {
                          allApplications.set(i, realApplication);
                     }
                }
            }
            Class loadedApkClass;
            try {
                  loadedApkClass = Class.forName("android.app.LoadedApk");
            } catch (ClassNotFoundException e) {
                  loadedApkClass = Class.forName("android.app.ActivityThread$PackageInfo");
            }
            Field mApplication = loadedApkClass.getDeclaredField("mApplication");
            mApplication.setAccessible(true);
            Field mResDir = loadedApkClass.getDeclaredField("mResDir");
            mResDir.setAccessible(true);
            Field mLoadedApk = null;
            try {
                  mLoadedApk = Application.class.getDeclaredField("mLoadedApk");
            } catch (NoSuchFieldException e) {
            }
            for (String fieldName : new String[] { "mPackages", "mResourcePackages" }) {
                 Field field = activityThread.getDeclaredField(fieldName);
                 field.setAccessible(true);
                 Object value = field.get(currentActivityThread);
                 for (Map.Entry> entry : ((Map>) value).entrySet()) {
                       Object loadedApk = ((WeakReference) entry.getValue()).get();
                       if (loadedApk != null) {
                             if (mApplication.get(loadedApk) == bootstrap) {
                                   if (realApplication != null) {
                                         mApplication.set(loadedApk, realApplication);
                                   }
                                   if (externalResourceFile != null) {
                                         mResDir.set(loadedApk, externalResourceFile);
                                   }
                                   if ((realApplication != null) && (mLoadedApk != null)) {
                                         mLoadedApk.set(realApplication, loadedApk);
                                   }
                             }
                       }
                  }
             }
        } catch (Throwable e) {
             throw new IllegalStateException(e);
        }
}

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

替换的过程如下:

1.替换ActivityThread的mInitialApplication为realApplication

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

3.替换ActivityThread的mPackages,mResourcePackages中的mLoaderApk中的application为realApplication。

monkeyPatchExistingResources

public static void monkeyPatchExistingResources(Context context, String externalResourceFile, Collection activities) {
      if (externalResourceFile == null) {
            return;
      }
      try {
           AssetManager newAssetManager = (AssetManager) AssetManager.class.getConstructor(new Class[0]).newInstance(new Object[0]);
Method mAddAssetPath = AssetManager.class.getDeclaredMethod(
           "addAssetPath", new Class[] { String.class });
           mAddAssetPath.setAccessible(true);
           if (((Integer) mAddAssetPath.invoke(newAssetManager, new Object[] { externalResourceFile })).intValue() == 0) {
throw new IllegalStateException(
                "Could not create new AssetManager");
           }
           Method mEnsureStringBlocks = AssetManager.class.getDeclaredMethod("ensureStringBlocks", new Class[0]);
           mEnsureStringBlocks.setAccessible(true);
           mEnsureStringBlocks.invoke(newAssetManager, new Object[0]);
           if (activities != null) {
                for (Activity activity : activities) {
                      Resources resources = activity.getResources();
                      try {
                            Field mAssets = Resources.class.getDeclaredField("mAssets");
                            mAssets.setAccessible(true);
                            mAssets.set(resources, newAssetManager);
                      } catch (Throwable ignore) {
                            Field mResourcesImpl = Resources.class.getDeclaredField("mResourcesImpl");
                            mResourcesImpl.setAccessible(true);
                            Object resourceImpl = mResourcesImpl.get(resources);
                            Field implAssets = resourceImpl.getClass().getDeclaredField("mAssets");
                            implAssets.setAccessible(true);
                            implAssets.set(resourceImpl, newAssetManager);
                      }
                      Resources.Theme theme = activity.getTheme();
                      try {
                            try {
                                 Field ma = Resources.Theme.class.getDeclaredField("mAssets");
                                 ma.setAccessible(true);
                                 ma.set(theme, newAssetManager);
                            } catch (NoSuchFieldException ignore) {
                                 Field themeField = Resources.Theme.class.getDeclaredField("mThemeImpl");
                                 themeField.setAccessible(true);
                                 Object impl = themeField.get(theme);
                                 Field ma = impl.getClass().getDeclaredField("mAssets");
                                 ma.setAccessible(true);
                                 ma.set(impl, newAssetManager);
                            }
                                 Field mt = ContextThemeWrapper.class.getDeclaredField("mTheme");
                                 mt.setAccessible(true);
                                 mt.set(activity, null);
                                 Method mtm = ContextThemeWrapper.class.getDeclaredMethod("initializeTheme", new Class[0]);
                                 mtm.setAccessible(true);
                                 mtm.invoke(activity, new Object[0]);
                                 Method mCreateTheme = AssetManager.class.getDeclaredMethod("createTheme", new Class[0]);
                                 mCreateTheme.setAccessible(true);
                                 Object internalTheme = mCreateTheme.invoke(newAssetManager, new Object[0]);
                                 Field mTheme = Resources.Theme.class.getDeclaredField("mTheme");
                                 mTheme.setAccessible(true);
                                 mTheme.set(theme, internalTheme);
                         } catch (Throwable e) {
                                 Log.e("InstantRun", "Failed to update existing theme for activity " + activity, e);
                         }
                         pruneResourceCaches(resources);
                  }
           }
           Collection> references;
           if (Build.VERSION.SDK_INT >= 19) {
                 Class resourcesManagerClass = Class.forName("android.app.ResourcesManager");
                 Method mGetInstance = resourcesManagerClass.getDeclaredMethod("getInstance", new Class[0]);
                 mGetInstance.setAccessible(true);
                 Object resourcesManager = mGetInstance.invoke(null, new Object[0]);
                 try {
                      Field fMActiveResources = resourcesManagerClass.getDeclaredField("mActiveResources");
                      fMActiveResources.setAccessible(true);
                      <ArrayMap> arrayMap = (ArrayMap) fMActiveResources.get(resourcesManager);
                      references = arrayMap.values();
                 } catch (NoSuchFieldException ignore) {
                      Field mResourceReferences = resourcesManagerClass.getDeclaredField("mResourceReferences");
                      mResourceReferences.setAccessible(true);
                      references = (Collection) mResourceReferences.get(resourcesManager);
                 }
          } else {
                 Class activityThread = Class.forName("android.app.ActivityThread");
                 Field fMActiveResources = activityThread.getDeclaredField("mActiveResources");
                 fMActiveResources.setAccessible(true);
                 Object thread = getActivityThread(context, activityThread);
                 <HashMap> map = (HashMap) fMActiveResources.get(thread);
                 references = map.values();
          }
          for (WeakReference wr : references) {
                 Resources resources = (Resources) wr.get();
                 if (resources != null) {
                      try {
                            Field mAssets = Resources.class.getDeclaredField("mAssets");
                            mAssets.setAccessible(true);
                            mAssets.set(resources, newAssetManager);
                      } catch (Throwable ignore) {
                            Field mResourcesImpl = Resources.class.getDeclaredField("mResourcesImpl");
                            mResourcesImpl.setAccessible(true);
                            Object resourceImpl = mResourcesImpl.get(resources);
                            Field implAssets = resourceImpl.getClass().getDeclaredField("mAssets");
                            implAssets.setAccessible(true);
                            implAssets.set(resourceImpl, newAssetManager);
                      }
                      resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrics());
               }
        }
   } catch (Throwable e) {
        throw new IllegalStateException(e);
   }
}

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

private class SocketServerReplyThread extends Thread {
    private final LocalSocket mSocket;

    SocketServerReplyThread(LocalSocket socket) {
        this.mSocket = socket;
    }

    public void run() {
        try {
            DataInputStream input = new DataInputStream(this.mSocket.getInputStream());
            DataOutputStream output = new DataOutputStream(this.mSocket.getOutputStream());
            try {
                handle(input, output);
            } finally {
                try {
                    input.close();
                } catch (IOException ignore) {
                }
                try {
                    output.close();
                } catch (IOException ignore) {
                }
            }
            return;
        } catch (IOException e) {
            if (Log.isLoggable("InstantRun", 2)) {
                Log.v("InstantRun", "Fatal error receiving messages", e);
            }
        }
    }

    private void handle(DataInputStream input, DataOutputStream output) throws IOException {
        long magic = input.readLong();
        if (magic != 890269988L) {
            Log.w("InstantRun", "Unrecognized header format " + Long.toHexString(magic));
            return;
        }
        int version = input.readInt();
        output.writeInt(4);
        if (version != 4) {
            Log.w("InstantRun", "Mismatched protocol versions; app is using version 4 and tool is using version " + version);
        } else {
            int message;
            for (; ; ) {
                message = input.readInt();
                switch (message) {
                    case 7:
                        if (Log.isLoggable("InstantRun", 2)) {
                            Log.v("InstantRun", "Received EOF from the IDE");
                        }
                        return;
                    case 2:
                        boolean active = Restarter.getForegroundActivity(Server.this.mApplication) != null;
                        output.writeBoolean(active);
                        if (Log.isLoggable("InstantRun", 2)) {
                            Log.v("InstantRun", "Received Ping message from the IDE; returned active = " + active);
                        }
                        break;
                    case 3:
                        String path = input.readUTF();
                        long size = FileManager.getFileSize(path);
                        output.writeLong(size);
                        if (Log.isLoggable("InstantRun", 2)) {
                            Log.v("InstantRun", "Received path-exists(" + path + ") from the " + "IDE; returned size=" + size);
                        }
                        break;
                    case 4:
                        long begin = System.currentTimeMillis();
                        path = input.readUTF();
                        byte[] checksum = FileManager.getCheckSum(path);
                        if (checksum != null) {
                            output.writeInt(checksum.length);
                            output.write(checksum);
                            if (Log.isLoggable("InstantRun", 2)) {
                                long end = System.currentTimeMillis();
                                String hash = new BigInteger(1, checksum)
                                        .toString(16);
                                Log.v("InstantRun", "Received checksum(" + path
                                        + ") from the " + "IDE: took "
                                        + (end - begin) + "ms to compute "
                                        + hash);
                            }
                        } else {
                            output.writeInt(0);
                            if (Log.isLoggable("InstantRun", 2)) {
                                Log.v("InstantRun", "Received checksum(" + path
                                        + ") from the "
                                        + "IDE: returning ");
                            }
                        }
                        break;
                    case 5:
                        if (!authenticate(input)) {
                            return;
                        }
                        Activity activity = Restarter
                                .getForegroundActivity(Server.this.mApplication);
                        if (activity != null) {
                            if (Log.isLoggable("InstantRun", 2)) {
                                Log.v("InstantRun",
                                        "Restarting activity per user request");
                            }
                            Restarter.restartActivityOnUiThread(activity);
                        }
                        break;
                    case 1:
                        if (!authenticate(input)) {
                            return;
                        }
                        List changes = ApplicationPatch
                                .read(input);
                        if (changes != null) {
                            boolean hasResources = Server.hasResources(changes);
                            int updateMode = input.readInt();
                            updateMode = Server.this.handlePatches(changes,
                                    hasResources, updateMode);
                            boolean showToast = input.readBoolean();
                            output.writeBoolean(true);
                            Server.this.restart(updateMode, hasResources,
                                    showToast);
                        }
                        break;
                    case 6:
                        String text = input.readUTF();
                        Activity foreground = Restarter
                                .getForegroundActivity(Server.this.mApplication);
                        if (foreground != null) {
                            Restarter.showToast(foreground, text);
                        } else if (Log.isLoggable("InstantRun", 2)) {
                            Log.v("InstantRun",
                                    "Couldn't show toast (no activity) : "
                                            + text);
                        }
                        break;
                }
            }
        }
    }
}

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

handlePatches

private int handlePatches(List changes,
                          boolean hasResources, int updateMode) {
    if (hasResources) {
        FileManager.startUpdate();
    }
    for (ApplicationPatch change : changes) {
        String path = change.getPath();
        if (path.endsWith(".dex")) {
            handleColdSwapPatch(change);
            boolean canHotSwap = false;
            for (ApplicationPatch c : changes) {
                if (c.getPath().equals("classes.dex.3")) {
                    canHotSwap = true;
                    break;
                }
            }
            if (!canHotSwap) {
                updateMode = 3;
            }
        } else if (path.equals("classes.dex.3")) {
            updateMode = handleHotSwapPatch(updateMode, change);
        } else if (isResourcePath(path)) {
            updateMode = handleResourcePatch(updateMode, change, path);
        }
    }
    if (hasResources) {
        FileManager.finishUpdate(true);
    }
    return updateMode;
}

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

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

handleColdSwapPatch冷部署

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

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

handleHotSwapPatch热部署

private int handleHotSwapPatch(int updateMode, ApplicationPatch patch) {
    if (Log.isLoggable("InstantRun", 2)) {
        Log.v("InstantRun", "Received incremental code patch");
    }
    try {
        String dexFile = FileManager.writeTempDexFile(patch.getBytes());
        if (dexFile == null) {
            Log.e("InstantRun", "No file to write the code to");
            return updateMode;
        }
        if (Log.isLoggable("InstantRun", 2)) {
            Log.v("InstantRun", "Reading live code from " + dexFile);
        }
        String nativeLibraryPath = FileManager.getNativeLibraryFolder()
                .getPath();
        DexClassLoader dexClassLoader = new DexClassLoader(dexFile,
                this.mApplication.getCacheDir().getPath(),
                nativeLibraryPath, getClass().getClassLoader());
        Class aClass = Class.forName(
                "com.android.tools.fd.runtime.AppPatchesLoaderImpl", true,
                dexClassLoader);
        try {
            if (Log.isLoggable("InstantRun", 2)) {
                Log.v("InstantRun", "Got the patcher class " + aClass);
            }
            PatchesLoader loader = (PatchesLoader) aClass.newInstance();
            if (Log.isLoggable("InstantRun", 2)) {
                Log.v("InstantRun", "Got the patcher instance " + loader);
            }
            String[] getPatchedClasses = (String[]) aClass
                    .getDeclaredMethod("getPatchedClasses", new Class[0])
                    .invoke(loader, new Object[0]);
            if (Log.isLoggable("InstantRun", 2)) {
                Log.v("InstantRun", "Got the list of classes ");
                for (String getPatchedClass : getPatchedClasses) {
                    Log.v("InstantRun", "class " + getPatchedClass);
                }
            }
            if (!loader.load()) {
                updateMode = 3;
            }
        } catch (Exception e) {
            Log.e("InstantRun", "Couldn't apply code changes", e);
            e.printStackTrace();
            updateMode = 3;
        }
    } catch (Throwable e) {
        Log.e("InstantRun", "Couldn't apply code changes", e);
        updateMode = 3;
    }
    return updateMode;
}

说明:该方法将patch的dex文件写入到临时目录,然后使用DexClassLoader去加载dex。然后反射调用AppPatchesLoaderImpl类的load方法。
需要强调的是:AppPatchesLoaderImpl继承自抽象类AbstractPatchesLoaderImpl,并实现了抽象方法:getPatchedClasses。而AbstractPatchesLoaderImpl抽象类代码如下:

public abstract class AbstractPatchesLoaderImpl implements PatchesLoader {
      public abstract String[] getPatchedClasses();
      public boolean load() {
           try {
                 for (String className : getPatchedClasses()) {
                       ClassLoader cl = getClass().getClassLoader();
                       Class aClass = cl.loadClass(className + "$override");
                       Object o = aClass.newInstance();
                       Class originalClass = cl.loadClass(className);
                       Field changeField = originalClass.getDeclaredField("$change");
                       changeField.setAccessible(true);
                       Object previous = changeField.get(null);
                       if (previous != null) {
                            Field isObsolete = previous.getClass().getDeclaredField("$obsolete");
                            if (isObsolete != null) {
                                 isObsolete.set(null, Boolean.valueOf(true));
                            }
                       }
                       changeField.set(null, o);
                       if ((Log.logging != null) && (Log.logging.isLoggable(Level.FINE))) {
                            Log.logging.log(Level.FINE, String.format("patched %s", new Object[] { className }));
                       }
                  }
            } catch (Exception e) {
                  if (Log.logging != null) {
                         Log.logging.log(Level.SEVERE, String.format("Exception while patching %s", new Object[] { "foo.bar" }), e);
}
                  return false;
            }
            return true;
      }
}

Instant Run热部署原理

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

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

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

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

转载请注明:Android开发中文站 » 深入理解Android Instant Run运行机制


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK