深入理解Android Instant Run运行机制详解手机开发
Instant Run,是android studio2.0新增的一个运行机制,在你编码开发、测试或debug的时候,它都能显著减少你对当前应用的构建和部署的时间。通俗的解释就是,当你在Android Studio中改了你的代码,Instant Run可以很快的让你看到你修改的效果。而在没有Instant Run之前,你的一个小小的修改,都肯能需要几十秒甚至更长的等待才能看到修改后的效果。
传统的代码修改及编译部署流程传统的代码修改及编译流程如下:构建整个apk → 部署app → app重启 → 重启Activity
Instant Run构建项目的流程:构建修改的部分 → 部署修改的dex或资源 → 热部署,温部署,冷部署
热拔插,温拔插,冷拔插热拔插:代码改变被应用、投射到APP上,不需要重启应用,不需要重建当前activity。
场景:适用于多数的简单改变(包括一些方法实现的修改,或者变量值修改)
**温拔插:**activity需要被重启才能看到所需更改。
场景:典型的情况是代码修改涉及到了资源文件,即resources。
**冷拔插:**app需要被重启(但是仍然不需要重新安装)
场景:任何涉及结构性变化的,比如:修改了继承规则、修改了方法签名等。
一个新的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是被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方法
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();
我们首先看一下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 didnt 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方法
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。
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。
SocketServerReplyThreadprivate 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", "Couldnt show toast (no activity) : " + text); break; }
说明:socket开启后,开始读取数据,当读到1时,获取代码变化的ApplicationPatch列表,然后调用handlePatches来处理代码的变化。
handlePatchesprivate 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", "Couldnt apply code changes", e); e.printStackTrace(); updateMode = 3; } catch (Throwable e) { Log.e("InstantRun", "Couldnt 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运行机制主要涉及到热部署、温部署和冷部署,主要是在第一次运行,app运行时期,有代码修改时。
第一次编译1.把Instant-Run.jar和instant-Run-bootstrap.jar打包到主dex中
2.替换AndroidManifest.xml中的application配置
3.使用asm工具,在每个类中添加$change,在每个方法前加逻辑
4.把源代码编译成dex,然后存放到压缩包instant-run.zip中
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运行机制对于向更深层次的研究是很有帮助的,对于我们自己书写框架也是有借鉴意义的。
5866.html
app程序应用开发手机开发无线开发移动端开发相关文章
- android autosize原理,Android屏幕适配头条:autosize的原理
- android attrs获取_关于Android attrs 自定义属性的说明
- 【Flutter】手机应用类型 ( Android | iOS | Native 应用 | Web 应用 | Hybrid 应用 | ReactNative 应用 | Flutter 应用 )
- Android RuntimePermissions运行时权限:批量权限申请详解手机开发
- Android图片墙实现详解手机开发
- Android为何比iOS卡论1G内存的使用详解手机开发
- Android滑动解锁控件详解手机开发
- Android获取当前手机设备信息工具类详解手机开发
- [android] 轮播图-滑动图片标题焦点详解手机开发
- [android] 手机卫士自定义控件的属性详解手机开发
- [android] activity的启动模式详解手机开发
- [android] 隐式意图和显式意图的使用场景详解手机开发
- android利用jdk制作签名详解手机开发
- android读取sd卡图片并进行缩放操作详解手机开发
- Picasso and Android-Universal-Image-Loader缓存框架详解手机开发
- android之View绘制详解手机开发
- 开源项目Universal Image Loader for Android详解手机开发
- android 防止反编译的若干方法详解手机开发
- android MVP框架详解手机开发
- Android ViewDragHelper及移动处理总结详解手机开发
- Android开发艺术探索(一)——Activity的生命周期和启动模式详解手机开发
- Android Studio2.3支持构建缓存,以及更多新特性详解手机开发
- android获取手机cpu并判断是单核还是多核