从Instant run谈Android替换Application和动态加载机制
背景
Android studio 2.0 Stable
版本中集成了Install run
即时编译技术,官方描述可以大幅加速编译速度,我们团队在第一时间更新并使用,总体用下来感觉,恩…也就那样吧,还不如不用的快。所以就去看了下Install run
的实现方式,其中有一个整体框架的基础,也就是今天的文章的主题,Android替换Application和动态加载机制。
Instant run
Instant run
的大概实现原理可以看下这篇Instant Run 浅析,我们需要知道Instant run
使用的gradle plugin2.0.0
,源码在这里,文中大概讲了下Instant run
的实现原理,但是并没有深入细节,特别是替换Application和动态加载机制。
关于动态加载,实际上Instant run
提供了两种动态加载的机制:
1.修改java代码需要重启应用加载补丁dex,而在Application初始化时替换了Application,新建了一个自定义的ClassLoader去加载所有的dex文件。我们称为重启更新机制
2.修改代码不需要重启,新建一个ClassLoader
去加载修改部分。我们称为热更新机制
Application入口
在编译时Instant run
用到了Transform API
修改字节码文件。其中AndroidManifest.xml
文件也被修改,如下:
/app/build/intermediates/bundles/production/instant-run/AndroidManifest.xml
,其中的Application
标签
1 2 3 4
| <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
采用反射机制调用。
我们知道Application
是ContextWrapper
的子类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| public class Application extends ContextWrapper { public application() { super(null); } } 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
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| protected void attachBaseContext(Context context) { if (!AppInfo.usingApkSplits) { createResources(apkModified); setupClassLoaders(context, context.getCacheDir().getPath(), apkModified); } createRealApplication(); super.attachBaseContext(context); if (realApplication != null) { try { Method attachBaseContext = ContextWrapper.class.getDeclaredMethod("attachBaseContext", Context.class); attachBaseContext.setAccessible(true); attachBaseContext.invoke(realApplication, context); } catch (Exception e) { throw new IllegalStateException(e); } } }
|
初始化ClassLoader
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| private static void setupClassLoaders(Context context, String codeCacheDir, long apkModified) { 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); } } public static ClassLoader inject( ClassLoader classLoader, String nativeLibraryPath, String codeCacheDir, List<String> dexes) { IncrementalClassLoader incrementalClassLoader = new IncrementalClassLoader(classLoader, nativeLibraryPath, codeCacheDir, dexes); setParent(classLoader, incrementalClassLoader); return incrementalClassLoader; }
|
动态加载
新建一个自定义的ClassLoader
名为IncrementalClassLoader,该ClassLoader
很简单,就是BaseDexClassLoader
的一个子类,并且将IncrementalClassLoader
设置为原ClassLoader的parent,熟悉JVM加载机制的同学应该都知道,由于ClassLoader采用双亲委托模式,即委托父类加载类,父类找不到再自己去找。这样IncrementalClassLoader
就变成了整个App的所有类的加载的ClassLoader,并且dexPath是/data/data/package_name/files/instant-run/dex
目录下的dex列表,这意味着什么呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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的对象。
解决这个问题有两个思路,要么全盘接管这个类加载的过程;要么告知系统我们使用的插件存在于哪里,让系统帮忙加载;这两种方式或多或少都需要干预这个类加载的过程。
引用自 – Android 插件化原理解析——插件加载机制
动态加载的两种方案
先来看下系统如何完成类的加载过程。
Activity
的创建过程
1 2 3 4
| 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的来源:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo, ClassLoader baseLoader, boolean securityViolation, boolean includeCode, boolean registerPackage) { final boolean differentUser = (UserHandle.myUserId() != UserHandle.getUserId(aInfo.uid)); synchronized (mResourcesManager) { WeakReference<LoadedApk> ref; if (differentUser) { 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())) { 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等。
Instant run的重启更新机制
绕了一大圈,终于能接着往下看了。接上面,我们继续看BootstrapApplication
的onCreate
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public void onCreate() { MonkeyPatcher.monkeyPatchApplication( BootstrapApplication.this, BootstrapApplication.this, realApplication, externalResourcePath); MonkeyPatcher.monkeyPatchExistingResources(BootstrapApplication.this, externalResourcePath, null); super.onCreate(); ... Server.create(AppInfo.applicationId, BootstrapApplication.this); if (realApplication != null) { realApplication.onCreate(); } }
|
上面代码,手机客户端app和Android Studio建立Socket通信,AS是客户端发消息,app是服务端接收消息作出相应操作,这是Instant run的通信方式,不在本文范围内。然后反射调用实际Application
的onCreate
方法。
那么前面的两个MonkeyPatcher
的方法是干嘛的呢
先看MonkeyPatcher.monkeyPatchApplication
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
| public static void monkeyPatchApplication(@Nullable Context context, @Nullable Application bootstrap, @Nullable Application realApplication, @Nullable 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<Application> allApplications = (List<Application>) 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<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) { 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); } }
|
这里做了三件事情:
1.替换Application对象
BootstrapApplication
的作用就是加载realApplication
也就是MyApplication
,所以我们就要把所有Framework层的BootstrapApplication
对象替换为MyApplication
对象。包括:
1 2 3
| 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 热更新机制
Instant run哪里用到的热更新机制呢?还记得刚才我们提到的Socket通信吗,其中S端也就是手机客户端,接收到热更新的消息会执行下面的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| private int handleHotSwapPatch(int updateMode, @NonNull ApplicationPatch patch) { try { String dexFile = FileManager.writeTempDexFile(patch.getBytes()); String nativeLibraryPath = FileManager.getNativeLibraryFolder().getPath(); DexClassLoader dexClassLoader = new DexClassLoader(dexFile, mApplication.getCacheDir().getPath(), nativeLibraryPath, getClass().getClassLoader()); 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); 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
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| @Override 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, 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的启动速度降低好多。
另外一点关于Application的替换是基于bazel(一种构建工具,类似于burk)中的StubApplication
参考
本文链接: http://w4lle.com/2016/05/02/从Instant run谈Android替换Application和动态加载机制/
版权声明:本文为 w4lle 原创文章,可以随意转载,但必须在明确位置注明出处!
本文链接: http://w4lle.com/2016/05/02/从Instant run谈Android替换Application和动态加载机制/