深入理解Android Instant Run运行机制

    xiaoxiao2021-07-22  254

    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()

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

    本文作者:佚名 来源:51CTO 相关资源:七夕情人节表白HTML源码(两款)

    最新回复(0)