Android studio 2.0 Stable 版本中集成了 Install run 即时编译技术,官方描述能够大幅加速编译速度。看了下 Install run 的实现方式,其中有一个总体框架的基础,即Android替换Application和动态加载机制。java
Instant run 的大概实现原理能够看下这篇 Instant Run 浅析 ,咱们须要知道 Instant run 使用的 gradle plugin2.0.0 ,源码在 这里 ,文中大概讲了下 Instant run 的实现原理,可是并无深刻细节,特别是替换Application和动态加载机制。android
关于动态加载,实际上 Instant run 提供了两种动态加载的机制:bootstrap
在编译时 Instant run 用到了 Transform API 修改字节码文件。其中 AndroidManifest.xml 文件也被修改,以下:数组
/app/build/intermediates/bundles/production/instant-run/AndroidManifest.xml ,其中的 Application 标签 <application name="com.aa.bb.MyApplication" android:name="com.android.tools.fd.runtime.BootstrapApplication" ... />
多了一个 com.android.tools.fd.runtime.BootstrapApplication ,在刚刚提到的gradle plugin 中的 instant-run-server 目录下找到该文件。缓存
实际上 BootstrapApplication 是咱们app的实际入口,咱们本身的 Application即 MyApplication 采用反射机制调用。app
咱们知道 Application 是 ContextWrapper 的子类框架
// android.app.Application public class Application extends ContextWrapper { // ... public application() { super(null); } // ... } // android.content.ContextWrapper public class ContextWrapper extends Context { Context mBase; // ... public ContextWrapper(Context base) { mBase = base; } protected void attachBaseContext(Context base) { if (mBase != null) { throw new IllegalStateException("Base context already set"); } mBase = base; } // ... @Override public AssetManager getAssets() { return mBase.getAssets(); } @Override public Resources getResources() { return mBase.getResources(); } // ... }
ContextWrapper一方面继承了Context,一方面又包含(composite)了一个Context对象(称为mBase),对Context的实现为转发给mBase对象处理。上面的代码表示,在attachBaseContext 方式调用以前Application是没有用的,由于mBase是空的。因此咱们看下 BootstrapApplication 的 attachBaseContext 方法ide
protected void attachBaseContext(Context context) {性能
if (!AppInfo.usingApkSplits) { createResources(apkModified); //新建一个ClassLoader并设置为原ClassLoader的parent setupClassLoaders(context, context.getCacheDir().getPath(), apkModified); } //经过Manifest中咱们的实际Application即MyApplication名反射生成对象 createRealApplication(); //调用attachBaseContext完成初始化 super.attachBaseContext(context); if (realApplication != null) { //反射调用实际Application的attachBaseContext方法 try { Method attachBaseContext = ContextWrapper.class.getDeclaredMethod("attachBaseContext", Context.class); attachBaseContext.setAccessible(true); attachBaseContext.invoke(realApplication, context); } catch (Exception e) { throw new IllegalStateException(e); } } }
初始化ClassLoadergradle
//BootstrapApplication.setupClassLoaders private static void setupClassLoaders(Context context, String codeCacheDir, long apkModified) { // /data/data/package_name/files/instant-run/dex/目录下的dex列表 List<String> dexList = FileManager.getDexList(context, apkModified); ClassLoader classLoader = BootstrapApplication.class.getClassLoader(); String nativeLibraryPath = (String) classLoader.getClass().getMethod("getLdLibraryPath") .invoke(classLoader); IncrementalClassLoader.inject( classLoader, nativeLibraryPath, codeCacheDir, dexList); } } //IncrementalClassLoader.inject public static ClassLoader inject( ClassLoader classLoader, String nativeLibraryPath, String codeCacheDir, List<String> dexes) { //新建一个自定义ClassLoader,dexPath为参数中的dexList IncrementalClassLoader incrementalClassLoader = new IncrementalClassLoader(classLoader, nativeLibraryPath, codeCacheDir, dexes); //设置为原ClassLoader的parent setParent(classLoader, incrementalClassLoader); return incrementalClassLoader; }
新建一个自定义的 ClassLoader 名为IncrementalClassLoader,该 ClassLoader 很简单,就是 BaseDexClassLoader 的一个子类,而且将 IncrementalClassLoader 设置为原ClassLoader的parent,熟悉JVM加载机制的同窗应该都知道,因为ClassLoader采用双亲委托模式,即委托父类加载类,父类找不到再本身去找。这样 IncrementalClassLoader 就变成了整个App的全部类的加载的ClassLoader,而且dexPath是 /data/data/package_name/files/instant-run/dex 目录下的dex列表,这意味着什么呢?
//``BaseDexClassLoader``的``findClass`` protected Class<?> findClass(String name) throws ClassNotFoundException { List<Throwable> suppressedExceptions = new ArrayList<Throwable>(); Class c = pathList.findClass(name, suppressedExceptions); if (c == null) { ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList); for (Throwable t : suppressedExceptions) { cnfe.addSuppressed(t); } throw cnfe; } return c; }
能够看到,查找Class的任务经过pathList完成;这个pathList是一个DexPathList类的对象,它的findClass方法以下:
public Class findClass(String name, List<Throwable> suppressed) { for (Element element : dexElements) { DexFile dex = element.dexFile; if (dex != null) { Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed); if (clazz != null) { return clazz; } } } if (dexElementsSuppressedExceptions != null) { suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions)); } return null; }
这个DexPathList内部有一个叫作dexElements的数组,而后findClass的时候会遍历这个数组来查找Class。看到了吗,这个dexElements就是从dexPath来的,也就说是 IncrementalClassLoader 用来加载dexPath(/data/data/package_name/files/instant-run/dex/)下面的dex文件。感兴趣的同窗能够看下,咱们app中的全部第三方库和本身项目中的代码,都被打包成若干个slice dex分片,该目录下有几十个dex文件。每当修改代码用 Instant run 完成编译,该目录下的dex文件就会有一个或者几个的更新时间发生改变。
正常状况下,apk被安装以后,APK文件的代码以及资源会被系统存放在固定的目录(好比/data/app/package_name/base-1.apk )系统在进行类加载的时候,会自动去这一个或者几个特定的路径来寻找这个类。而使用 Install run 则彻底无论以前的加载路径,全部的分片dex文件和资源都在dexPath下,用 IncrementalClassLoader 去加载。也就是加载不存在APK固定路径以外的类,即动态加载。
可是仅仅有ClassLoader是不够的。由于每一个被修改的类都被改了名字,类名在原名后面添加 $override ,目录在 app/build/intermediates/transforms/instantRun/debug/folders/4000 。AndroidManifest中并无注册这些被改了名字的Activity。> 所以正常状况下系统没法加载咱们插件中的类;所以也没有办法建立Activity的对象。
解决这个问题有两个思路,要么全盘接管这个类加载的过程;要么告知系统咱们使用的插件存在于哪里,让系统帮忙加载;这两种方式或多或少都须要干预这个类加载的过程。
ref: Android 插件加载机制
先来看下系统如何完成类的加载过程。
Activity 的建立过程
java.lang.ClassLoader cl = r.packageInfo.getClassLoader(); activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent); StrictMode.incrementExpectedActivityCount(activity.getClass()); r.intent.setExtrasClassLoader(cl);
经过 ClassLoader 和类名加载,反射调用生成 Activity 对象,其中的 ClassLoader 从 LoadedApk 的一个对象 r.packageInfo 中得到的。 LoadedApk 对象是APK文件在内存中的表示。 Apk文件的相关信息,诸如Apk文件的代码和资源,甚至代码里面的 Activity , Service 等组件的信息咱们均可以经过此对象获取。
r.packageInfo的来源:
private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo, ClassLoader baseLoader, boolean securityViolation, boolean includeCode, boolean registerPackage) { // 获取userid信息 final boolean differentUser = (UserHandle.myUserId() != UserHandle.getUserId(aInfo.uid)); synchronized (mResourcesManager) { // 尝试获取缓存信息 WeakReference<LoadedApk> ref; if (differentUser) { // Caching not supported across users ref = null; } else if (includeCode) { ref = mPackages.get(aInfo.packageName); } else { ref = mResourcePackages.get(aInfo.packageName); } LoadedApk packageInfo = ref != null ? ref.get() : null; if (packageInfo == null || (packageInfo.mResources != null && !packageInfo.mResources.getAssets().isUpToDate())) { // 缓存没有命中,直接new packageInfo = new LoadedApk(this, aInfo, compatInfo, baseLoader, securityViolation, includeCode && (aInfo.flags&ApplicationInfo.FLAG_HAS_CODE) != 0, registerPackage); return packageInfo; } }
重要的是这个缓存 mPackage , LoadedApk 对象 packageInfo 就是从这个缓存中取的,因此咱们只要在 mPackage 修改里面的 ClassLoader 控制类的加载就能完成动态加载。
在Android 插件加载机制中,做者已经提出两种动态加载的解决方案:
『激进方案』中咱们自定义了插件的ClassLoader,而且绕开了Framework的检测;利用ActivityThread对于LoadedApk的缓存机制,咱们把携带这个自定义的ClassLoader的插件信息添加进mPackages中,进而完成了类的加载过程。
『保守方案』中咱们深刻探究了系统使用ClassLoader findClass的过程,发现应用程序使用的非系统类都是经过同一个PathClassLoader加载的;而这个类的最终父类BaseDexClassLoader经过DexPathList完成类的查找过程;咱们hack了这个查找过程,从而完成了插件类的加载。
激进方案因为是一个插件一个 Classloader 也叫多 ClassLoader 方案,表明做 DroidPlugin ;保守方案也叫作单 ClassLoader 方案,表明做,Small、众多热更新框架如 nuwa 等。
继续看 BootstrapApplication的 onCreate 方法:
public void onCreate() { MonkeyPatcher.monkeyPatchApplication( BootstrapApplication.this, BootstrapApplication.this, realApplication, externalResourcePath); MonkeyPatcher.monkeyPatchExistingResources(BootstrapApplication.this, externalResourcePath, null); super.onCreate(); ... //手机客户端app和Android Studio创建Socket通讯,AS是客户端发消息,app //是服务端接收消息做出相应操做。Instant run的通讯方式。不在本文范围内 Server.create(AppInfo.applicationId, BootstrapApplication.this); if (realApplication != null) { //还记得这个realApplication吗,咱们app中实际的Application realApplication.onCreate(); } }
上面代码,手机客户端app和Android Studio创建Socket通讯,AS是客户端发消息,app是服务端接收消息做出相应操做,这是Instant run的通讯方式,不在本文范围内。而后反射调用实际 Application 的 onCreate 方法。
那么前面的两个 MonkeyPatcher 的方法是干吗的呢
先看 MonkeyPatcher.monkeyPatchApplication
public static void monkeyPatchApplication(@Nullable Context context, @Nullable Application bootstrap, @Nullable Application realApplication, @Nullable String externalResourceFile) { try { // Find the ActivityThread instance for the current thread Class<?> activityThread = Class.forName("android.app.ActivityThread"); Object currentActivityThread = getActivityThread(context, activityThread); // Find the mInitialApplication field of the ActivityThread to the real application Field mInitialApplication = activityThread.getDeclaredField("mInitialApplication"); mInitialApplication.setAccessible(true); Application initialApplication = (Application) mInitialApplication.get(currentActivityThread); if (realApplication != null && initialApplication == bootstrap) { //**2.替换掉ActivityThread.mInitialApplication** mInitialApplication.set(currentActivityThread, realApplication); } // Replace all instance of the stub application in ActivityThread#mAllApplications with the // real one if (realApplication != null) { Field mAllApplications = activityThread.getDeclaredField("mAllApplications"); mAllApplications.setAccessible(true); List<Application> allApplications = (List<Application>) mAllApplications .get(currentActivityThread); for (int i = 0; i < allApplications.size(); i++) { if (allApplications.get(i) == bootstrap) { //**1.替换掉ActivityThread.mAllApplications** allApplications.set(i, realApplication); } } } // Figure out how loaded APKs are stored. // API version 8 has PackageInfo, 10 has LoadedApk. 9, I don't know. 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); // 10 doesn't have this field, 14 does. Fortunately, there are not many Honeycomb devices // floating around. Field mLoadedApk = null; try { mLoadedApk = Application.class.getDeclaredField("mLoadedApk"); } catch (NoSuchFieldException e) { // According to testing, it's okay to ignore this. } // Enumerate all LoadedApk (or PackageInfo) fields in ActivityThread#mPackages and // ActivityThread#mResourcePackages and do two things: // - Replace the Application instance in its mApplication field with the real one // - Replace mResDir to point to the external resource file instead of the .apk. This is // used as the asset path for new Resources objects. // - Set Application#mLoadedApk to the found LoadedApk instance for (String fieldName : new String[]{"mPackages", "mResourcePackages"}) { Field field = activityThread.getDeclaredField(fieldName); field.setAccessible(true); Object value = field.get(currentActivityThread); for (Map.Entry<String, WeakReference<?>> entry : ((Map<String, WeakReference<?>>) value).entrySet()) { Object loadedApk = entry.getValue().get(); if (loadedApk == null) { continue; } if (mApplication.get(loadedApk) == bootstrap) { if (realApplication != null) { //**3.替换掉mApplication** mApplication.set(loadedApk, realApplication); } if (externalResourceFile != null) { //替换掉资源目录 mResDir.set(loadedApk, externalResourceFile); } if (realApplication != null && mLoadedApk != null) { //**4.替换掉mLoadedApk** mLoadedApk.set(realApplication, loadedApk); } } } } } catch (Throwable e) { throw new IllegalStateException(e); } }
这里作了三件事情:
1.替换Application对象
BootstrapApplication 的做用就是加载 realApplication 也就是 MyApplication,因此咱们就要把全部Framework层的 BootstrapApplication 对象替换为 MyApplication 对象。包括:
baseContext.mPackageInfo.mApplication 代码3处
baseContext.mPackageInfo.mActivityThread.mInitialApplication 代码2处
baseContext.mPackageInfo.mActivityThread.mAllApplications 代码1处
2.替换资源相关对象mResDir,前面咱们已经说过,正常状况下寻找资源都是在 /data/app/package_name/base-1.apk 目录下,而 Instant run 将资源也抽出来放在 /data/data/package_name/files/instant-run/ ,加载目录也更改成后者
3.替换 mLoadedApk 对象
还记得前面的讲的 LoadedApk 吗,这里面有加载类的 ClassLoader ,因为 BootstrapApplication 在 attachBaseContext 方法中就将其已经替换为了 IncrementalClassLoader ,因此代码4处反射将 BootstrapApplication 的 mLoadedApk 赋值给了 MyApplication ,那么接下来MyApplication的全部类的加载都将由 IncrementalClassLoader 来负责。
MonkeyPatcher.monkeyPatchExistingResources 更新资源补丁,不在本文范围内就不讲了。
这些工做作完以后调用 MyApplication 的 onCreate 方法 BootstrapApplication就将控制权交给了 MyApplication ,这样在整个运行环境中, MyApplication 就是正牌 Application 了,完成 Application 的替换。
总结一下,刚才咱们说了已经有两个动态加载的方案,激进方案和保守方案,而 Instant run 的重启更新机制更像后者–保守方案即单 ClassLoader 方案,首先,该种方案只有一个 ClassLoader ,只不过是经过替换 Application 达到的替换 mLoadedApk进而替换 ClassLoader 的目的,并无涉及到缓存 mPackage 而后dexList也是它本身维护的。
Instant run哪里用到的热更新机制呢?还记得刚才咱们提到的Socket通讯吗,其中S端也就是手机客户端,接收到热更新的消息会执行下面的方法:
private int handleHotSwapPatch(int updateMode, @NonNull ApplicationPatch patch) { try { String dexFile = FileManager.writeTempDexFile(patch.getBytes()); String nativeLibraryPath = FileManager.getNativeLibraryFolder().getPath(); //新建一个ClassLoader,dexFile是刚更新的插件 DexClassLoader dexClassLoader = new DexClassLoader(dexFile, mApplication.getCacheDir().getPath(), nativeLibraryPath, getClass().getClassLoader()); // we should transform this process with an interface/impl Class<?> aClass = Class.forName( "com.android.tools.fd.runtime.AppPatchesLoaderImpl", true, dexClassLoader); try { PatchesLoader loader = (PatchesLoader) aClass.newInstance(); String[] getPatchedClasses = (String[]) aClass .getDeclaredMethod("getPatchedClasses").invoke(loader); //loader是PatchesLoader的一个实例,调用load方法加载插件 if (!loader.load()) { updateMode = UPDATE_MODE_COLD_SWAP; } } catch (Exception e) { updateMode = UPDATE_MODE_COLD_SWAP; } } catch (Throwable e) { updateMode = UPDATE_MODE_COLD_SWAP; } return updateMode; }
能够看到根据单个dexFile新建了一个 ClassLoader ,而后调用 loader.load() 方法, loader 是 PatchesLoader 接口的实例, PatchesLoader 接口的一个实现类 AppPatchesLoaderImpl ,该类中记录了哪些修改的类。看一下 load 方法
@Override public boolean load() { try { //遍历已记录的全部修改的类 for (String className : getPatchedClasses()) { ClassLoader cl = getClass().getClassLoader(); //咱们刚才说的修改的类名后面都有$override Class<?> aClass = cl.loadClass(className + "$override"); Object o = aClass.newInstance(); //1.**反射修改原类中的$change字段为修改后的值** Class<?> originalClass = cl.loadClass(className); Field changeField = originalClass.getDeclaredField("$change"); // force the field accessibility as the class might not be "visible" // from this package. changeField.setAccessible(true); // If there was a previous change set, mark it as obsolete: Object previous = changeField.get(null); if (previous != null) { Field isObsolete = previous.getClass().getDeclaredField("$obsolete"); if (isObsolete != null) { isObsolete.set(null, true); } } changeField.set(null, o); } } catch (Exception e) { return false; } return true; }
Instant run 的热更新原理能够概述为:
1.第一次运行,应用 transform API 修改字节码。
输出目录在 app/build/intermediates/transforms/instantRun/debug/folders/1/,给全部的类添加 $change 字段, $change 为 IncrementalChange 类型, IncrementalChange 是个接口。若是 $change 不为空,去调用 $change 的 access$dispatch方法,参数为方法签名字符串和方法参数数组,不然调用原逻辑。
load方法中会去加载所有补丁类,并赋值给对应原类的 $change 。
这也验证了咱们说它是多 ClassLoader 方案。
2.全部修改的类有 gradle plugin 自动生成,类名在原名后面添加$override,复制修改后类的大部分方法,实现IncrementalChange 接口的access$dispatch方法,该方法会根据传递过来的方法签名,调用本类的同名方法。
那么也就是说只要把原类的 $change 字段设置为该类,那就会调用该类的 access$dispatch 方法,就会使用修改后的方法了。上面代码1处就经过反射修改了原类中的 $change 为修改后补丁类中的值。 AppPatchesLoaderImpl 记录了全部被修改的类,也会被打进补丁dex。
总结一下,能够看到 Instant run 热更新是多 ClassLoader 加载方案,每一个插件dex都有一个 ClassLoader ,若是插件须要升级,直接从新建立一个自定的 ClassLoader 加载新的插件。可是目前来看, Instant run 修改java代码大部分状况下都是重启更新机制,可能热更新机制还有bug。资源更新是热更新,重启对应Activity就能够。
Instant run 看下来真的有好多东西,其中就以替换 Application 和动态加载尤其重要,关于动态加载,彻底能够根据 Instant run 的实现方式完成一个热修复和重启修复相结合的更新框架,用于线上bug的修复和功能更新,而且能够支持资源文件的更新,是无侵入性的更新框架,最重要的一点,这是官方支持的。可是,性能确定会有所影响,实际开发中使用 Instant run 编译其实还有不少的问题,并且app初始化时使用的不少反射,这也直接致使app的启动速度下降好多。
从Instant run看Application替换和动态加载机制