咱们在宣传Shadow的时候说了Shadow具备两大特性,其中一个叫作“全动态插件框架”。这篇文章就讲这个特性。咱们很早以前用过一款基于数百反射私有API实现的插件框架,在前面的文章也提过,在这种插件框架里要不停的兼容新版本的Android系统,OEM系统。尤为是Activity的attach方法,常常须要兼容。还有壳子Activity上也偶尔要补充覆盖方法。但实际上,这些需求都知足不了。由于最先咱们也是像市面上能见到的其余插件框架同样,将插件框架打包在宿主里的。因此,这些修复和更新代码只有在宿主的下一个版本才能生效。改Bug就还好,下一个版本就没问题了,可是新特性就麻烦了,须要保证使用了新特性的插件不会被老版本的宿主启动。编程
咱们先直接回顾动态化的基本原理,再说明Shadow是如何应用这个基本原理的。动态化的基本原理很是简单,是Java的基本知识。可是确实有不少人没能完全理解,也不会灵活运用。安全
Java代码编译的时候是没有连接过程的。连接过程指的是传统的C语言在编译的时候分为两个步骤,一是将源码编译成机器码,对于其中引用了其余文件的符号(好比文件中的全局变量),暂时用符号名代替。而后第二个步骤是连接步骤,在这个步骤中将前一个步骤中暂时使用的符号名真正替换为实际内存地址。C语言的这个编译过程在Java角度来看,就至关于Java源码先编译成了字节码,对于源码中引用的其余类,先暂时在字节码中以名称代替。而后在一个连接过程当中将字节码中的名称替换为其余类的真正实现代码。不过,实际状况是,Java就没有这个连接过程。Java编译的字节码中保存的就是其余类的名称。其余类的实现是在运行时才去查找的。所以这一过程又至关因而C语言的动态连接过程。咱们前面提到的C语言编译过程被称为静态连接。因此,有一些学过C语言的人评论Java说,Java这门语言是彻底动态连接的语言,是一门动态语言。这里说“动态语言”指的是连接过程是动态的。咱们平时若是说“动态语言”还有一种可能性指的是类型是否动态,Java是一门静态类型语言,这两个静态、动态不要搞混了。bash
除了一些优化成Native实现的特殊系统类,Java的类都是在运行时由ClassLoader动态加载的。若是类A引用了类B,在类A的代码执行到要用B时,就会向加载了本身的ClassLoader查找类B的实现。找到了类B的实现,才能new出来B的实例,才能继续执行,或者是才能调用B的静态方法。并且同一个ClassLoader加载的同一个名字的类才是运行时实际上的同一个类。一个类A有public static final int a静态域,想问有没有可能类B和类C中相同的代码System.out.println(A.a);会打印出来不一样的值?答案应该是“有可能的”。由于在精心构造的ClassLoader结构下,类B和类C可能分别由不一样的ClassLoader加载的,那么它们向各自的ClassLoader请求到的类A的实现多是不一样的。就算是类A的实现只有一份,类B和类C加载到的类A也是两个不一样的类。一旦用反射修改其中一个的a静态域,另外一个的a静态域是不会跟着变化的。框架
Java还有两个和动态化相关的特性,一个是接口,另外一个是向上转型。ide
Class<?> implClass = classLoader.loadClass("com.xxx.AImpl");
Object implObject = implClass.newInstance();
A a = (A) implObject;
复制代码
这里假设classLoader动态加载了一些Java类,其中就有一个类叫作com.xxx.AImpl,AImpl继承自A,或者AImpl实现了A接口。注意这里用了强制类型转换,是由于代码层面是将Object类型向下转型成了A。但实际上咱们知道implObject的类型是AImpl,AImpl转换成A是一个向上转型。向上转型老是安全的。因此用这种方法老是能够先定义出接口,精心设计接口,让接口足够通用和稳定。只要接口不变,它的实现老是能够修改的。咱们将接口打包在宿主中,接口就轻易不能更新了。可是它的实现老是能够更新的。函数
全部的插件框架中,Activity的加载都是这样的,new一个DexClassLoader加载插件apk。而后从插件ClassLoader中load指定的插件Activity名字,newInstance以后强转为Activity类型使用。实际上Android系统自身在启动Activity时也是这样作的。因此这就是插件机制能动态更新Activity的基本原理。学习
因此,全部的插件框架在解决的问题都不是如何动态加载类,而是动态加载的Activity没有在AndroidManifest中注册,该如何能正常运行。若是Android系统没有AndroidManifest的限制,那么全部插件框架都没有存在的必要了。由于Java语言自己就支持动态更新实现的能力。优化
Shadow的Manager的功能就是管理插件,包括插件的下载逻辑、入口逻辑,预加载逻辑等。反正就是一切尚未进入到Loader以前的全部事情。ui
因为Manager就是一个普通类,不是Android系统规定要在Manifest中注册才能使用的类,因此Manager的动态化就是通常性的动态加载实现。编码
为了让宿主中的固定代码足够的少,咱们给Manager定义的接口就是一个相似传统Main函数的接口。
void enter(Context context, long formId, Bundle bundle, EnterCallback callback);
复制代码
这就是Manager的惟一方法,宿主中只会调用这个方法。传入当前界面的Context以便打开下一个插件Activity。将全部插件中可能用到的参数经过Bundle传给插件。定义一些fromId,用来让Manager的实现逻辑分辨这一次enter是从哪里来的。实际上在宿主中的每一处enter调用均可以设置不一样的fromId,就至关于让Manager知道调用来自宿主中的哪一行代码了。再传入一个EnterCallback供Manager能够返回一个动态加载的View做为插件的Loading View。
Loader就是负责加载插件Activity,而后实现插件Activity的生命周期等功能的那部分核心逻辑了。不少插件框架就只有Loader这部分功能,或者说只开源了Loader这部分功能。通常来讲,Loader是宿主到插件的桥梁。好比说咱们要在宿主中执行Loader的代码,才能Hack一些系统类,让它们加载插件Activity。或者在宿主中的代理壳子Activity中,也要使用Loader去加载插件Activity完成转调功能。因此一般宿主代码就直接依赖了Loader的代码。这就是为何其余插件框架都须要将插件框架自己的代码打包在宿主中。
稍复杂一点的问题就是代理壳子ContainerActivity须要和PluginActivity经过Loader相互调用。因此Shadow应用前面提到的动态化原理时,作了双向的接口,能够看到代码中的HostActivityDelegate
和HostActivityDelegator
。经过定义出这两个接口,能够避免ContainerActivity和Loader相互加载对方时还须要加载对方所依赖的其余类。定义成接口,就只须要加载这个接口就好了。
经过这个设计,插件框架的绝大部分须要修改或修复的代码就均可以动态发布了。而且也使得在同一个宿主中能够有多个不一样实现的Loader,这样业务就能够针对业务自身的bug修改Loader的代码,不会影响其余业务了。紧急状况下Loader也能够耦合业务逻辑。
Container就是那些注册在宿主AndroidManifest中的代理壳子。因为Activity的建立是系统根据Activity的名字直接经过宿主的PathClassLoader构造的,因此这些Activity必须打包在宿主中才能处于PathClassLoader,才能被系统找到。因此Container是不能放到Loader中,经过动态加载的通常方法加载的。由于前面提到的通常方法都是要new一个新的ClassLoader加载动态实现的。
可是咱们业务的宿主对合入代码的增量要求极其严格,是要求0增量合入的。也就是咱们合入代码的同时还要优化原有代码,使总体0增量。增量既包含安装包体积增量,也包含方法数增量。
因此作了Loader的动态化仍是不够的,由于代理壳子Activity上须要提早Override很是多的方法。同时因为定义了Delegate和Delegator接口,还在Delegator接口上又添加了superOnCreate等方法,致使Activity上每有一个须要Override的方法,就要增长4个方法数,而Activity上大概有350个方法。
Container的实现因为前面Loader的动态化已经变得很是简单了,不管是什么方法,都是转调给Delegate接口,本身不实现任何逻辑。按理说能够认为不会有什么Bug了,至少将方法所有覆盖实现,Container即便不动态化也是能够长期使用的。
Android系统的虚拟机和通常的JVM有一点不太同样,就是能够经过反射修改private final
域。这在通常的JVM上是不能成功的,读过《Java编程思想》的同窗可能还记得专门有这段讲解。而ClassLoader类的parent域,偏偏就是private final
域。ClassLoader的parent指向的是ClassLoader的“双亲”,就是“双亲委派”中的那个“双亲”(如今去学习这个概念的同窗注意这里的“双”是没有意义的,不存在两个“亲”)。宿主的PathClassLoader就是一个有正常“双亲委派”逻辑的ClassLoader,它加载任何类以前都会委托本身的parent先去加载这个类。若是parent可以加载到,本身就不会加载了。所以,咱们能够经过修改ClassLoader的parent,为ClassLoader新增一个parent。将本来的BootClassLoader <- PathClassLoader
结构变为BootClassLoader <- DexClassLoader <- PathClassLoader
,插入的DexClassLoader加载了ContainerActivity就可使得系统在向PathClassLoader查找ContainerActivity时可以正确找到实现。
因此咱们就迫于无奈作了Container的动态化,也在这个动态化中使用了惟一一次反射修改私有变量。这里要认可,Shadow开源的所有代码中确实有这一处反射。跟Shadow宣传的零反射是有点冲突的。这里值得辩驳一点的是,零反射是和传统插件框架解决动态加载Activity等组件时是否使用反射来对比的。Container的动态化,乃至Shadow的dynamic层对于解决其余插件框架相同的问题来讲都不是必要的部分。特别是Container的动态化是可选的。
ClassLoader的parent域不属于非公开API,甚至不是Android的代码,而是JDK的代码。并且这个反射的实现不须要硬编码“parent”这个单词,由于有getParent这个方法可使咱们经过运行时对比肯定parent域。因此,这一处反射实现仍是比较安全的,实际上咱们线上运行了3年了,也历来没见过失败的状况。
Container的动态化虽然能够说是没必要要的,但确实仍是有好处的。有了Container的动态化,咱们就不必一次性实现Container上的全部须要Override的方法了。能够在业务须要时再添加。
关于Container的动态化,能够具体看com.tencent.shadow.dynamic.host.DynamicRuntime
这个类的实现。
另外Runtime虽然包含了Container,可是实际上只有Container须要这样动态化。Runtime中的其余类是由于简化实现的关系放在了一块儿,其余类是能够按传统作法加载的,只须要在PluginClassLoader上方便可。
最后再分享一点关于加载接口实现的经验。咱们在Shadow里专门写了一个ApkClassLoader类,封装了com.tencent.shadow.dynamic.host.ApkClassLoader#getInterface
泛型方法,能够直接得到接口类型实例。能够注意到,这个方法是不支持有参数的构造器的。看咱们代码历史,能够找到有参数版本的实现,可是最终删掉了。由于我发现,传一组参数类型class令牌,并不能在编译期跟实现类的构造器关联起来。就是说实现类的构造器若是参数列表变化了,这边调用getInterface的参数没有修改是不能在编译期发现的。会等到运行时才会抛出找不到那种参数列表的构造器的异常。因此我改为了定义工厂接口的形式,也就是getInterface老是取出来一个工厂接口,而后再经过工厂接口build有参数的对象。这样实现类的构造参数列表变化,就能在编译期检查出来了。
能够查看Shadow代码中的com.tencent.shadow.dynamic.host.LoaderFactory
和com.tencent.shadow.dynamic.host.ManagerFactory
来分析我讲的区别。
Shadow将咱们定义的插件框架的全部部分所有实现了动态化加载,使得插件框架自身的问题能够动态修复,也使得插件框架成为了插件包的一部分,避免了插件须要适配不一样版本插件框架的问题。
这个特性在实践中比无Hack实现更为重要。由于它甚至使得咱们在不跟宿主版本的状况下,不改宿主一行代码,就把数百反射实现的旧框架替换为了无Hack实现的Shadow框架。使得咱们在作这个切换时,能够彻底不考虑旧框架的维护了。