Tencent shadow动态化插件实现基础

548 阅读7分钟

1. 腾讯的shadow 插件框架

1.1 框架特点

1. 对系统代码零反射
2. 对框架自身动态化
3. Kotlin实现,core.loader,core.transform核心代码完全用Kotlin实现,代码简洁易维护

1.2 技术特点

  • 复用独立安装App的源码,插件App的源码原本就是可以正常安装运行的

  • 零反射无Hack实现插件技术,从理论上就已经确定无需对任何系统做兼容开发,更无任何隐藏API调用,和Google限制非公开SDK接口访问的策略完全不冲突

  • 全动态插件框架,一次性实现完美的插件框架很难,但Shadow将这些实现全部动态化起来,使插件框架的代码成为了插件的一部分。插件的迭代不再受宿主打包了旧版本插件框架所限制

  • 宿主增量极小,得益于全动态实现,真正合入宿主程序的代码量极小

  • Kotlin实现,core.loader,core.transform核心代码完全用Kotlin实现,代码简洁易维护

image.png

#### 1.3 动态化设计原理

Java的类都是在运行时由ClassLoader动态加载的。如果类A引用了类B,在类A的代码执行到要用B时,就会向加载了自己的ClassLoader查找类B的实现。找到了类B的实现,才能new出来B的实例,才能继续执行,或者是才能调用B的静态方法。而且同一个ClassLoader加载的同一个名字的类才是运行时实际上的同一个类。

Class<?> implClass = classLoader.loadClass("com.xxx.AImpl") *;*

Object implObject = implClass.newInstance() *;*

A a = (A) implObject *;*

所有的插件框架中,Activity的加载都是这样的,new一个DexClassLoader加载插件apk。然后从插件ClassLoader中load指定的插件Activity名字,newInstance之后强转为Activity类型使用。实际上Android系统自身在启动Activity时也是这样做的。所以这就是插件机制能动态更新Activity的基本原理。

所以,所有的插件框架在解决的问题都不是如何动态加载类,而是动态加载的Activity没有在AndroidManifest中注册,该如何能正常运行。如果Android系统没有AndroidManifest的限制,那么所有插件框架都没有存在的必要了。因为Java语言本身就支持动态更新实现的能力。

#### 1.3.1 Manager的设计

Shadow的Manager的功能就是管理插件,包括插件的下载逻辑、入口逻辑,预加载逻辑等。反正就是一切还没有进入到Loader之前的所有事情。由于Manager就是一个普通类,不是Android系统规定要在Manifest中注册才能使用的类,所以Manager的动态化就是一般性的动态加载实现。为了让宿主中的固定代码足够的少,我们给Manager定义的接口就是一个类似传统Main函数的接口。

**void** **enter**(Context context, **long** formId, Bundle bundle, EnterCallback callback);

这就是Manager的唯一方法,宿主中只会调用这个方法。传入当前界面的Context以便打开下一个插件Activity。

#### 1.3.2 Loader的动态化

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也可以耦合业务逻辑。

#### 1.3.3 Container的动态化

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个方法。

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的动态化是可选的。

1.4 源码解读

Shadow插件的核心:插件里面的四大组件没有直接实现,只是实现的普通的类。

  1. PluginContainerActivity这个类包含当前插件中中的类的基本实现: 需要重写getClassLoader 和 getResources()
  2. PluginMangerImpl:实现两个getClassLoader 和 getResources()
  3. 研究Inflat方法:final Resources = getContext().getResource(); 只需要将getResouce()进行重定向处理
  4. AssetManager的使用:通过反射一把获取插件 AssetManager assetManager = AssetManager.class.newInstance() ; AssetManager.class.getDeclaredMethod("addAssetPath", String.class).invoke(assetManager, apkPath);
  5. 实现Shodow插件的生命周期,实现动态化的框架:宿主和插件使用周期调用接口传递
  6. 插件和宿主之间的Context的传递:通过生命周期接口将Context传递下去
  7. 插件中去绑定控件:需要在宿主实现的生命周期来里面全部进行重写回调
  8. 插件里面如何实现内部跳转:需要重写PluginContainerActivity里面的startActivity方法实现当前的内容。