Shadow解决插件和宿主有同名View的方法解析

5,573 阅读4分钟

问题描述

在“免安装运行App”这个场景中,插件代码通常和宿主是完全不相关的。甚至项目都是独立管理的,插件和宿主是不同团队开发,不同版本发布管理的。在这种情况下,插件和宿主中出现相同名字的类是非常常见的。只要设计好ClassLoader的结构,将插件和宿主的ClassLoader隔离开,就可以避免Java层面的类冲突问题。但是,Android系统中存在一个错误的设计,导致LayoutInflater在inflate的时候使用的View类并不是简单的从ClassLoader中读取的,而是自行做了一层缓存,以View的类名作为Key保存了Class对象。这个缓存存储在一个静态域上,因此在同一个进程中,不能存在两个同名的View都使用LayoutInflater构造。这个问题常见于插件框架对于support包中的RecycleView支持上。不管这个问题的话,当宿主和插件都使用了support包中的RecycleView,就会出现提示信息类似于“RecycleView cannot cast to RecycleView”的Crash异常。

传统的解决方案

方法一,逃避法。我见过一些插件框架,在宿主和插件都用到support包时,就将插件用的support包去掉,让插件直接使用宿主的support包。这样做可以避免前面例子中使用RecycleView等support包中的View的问题。但是既不能避免其他宿主和插件中重名的自定义View的问题,也使得插件使用的support包版本和预期的不一致了。

方法二,清空缓存法。还有一些插件框架,通过反射修改LayoutInflater的缓存,清空缓存达到目的。这样做有两个问题,一是宿主随时可能再次inflate view,再次写入缓存。插件框架可能要频繁清空缓存,甚至每次使用LayoutInflater前都要清空缓存。这样做会使得LayoutInflater失去缓存机制,造成性能下降。二是通过反射修改这个缓存是不安全的,需要兼容多种版本的Android系统,还会涉及访问系统私有API。

Shadow的解决方案

在开发Shadow的时候,我们仔细学习了LayoutInflater的设计。发现通过LayoutInflater自带的设计就可以解决这个问题。

LayoutInflater是可以继承重写的,而且插件中用到的LayoutInflater都是由插件框架中间层提供的Context返回的,因此我们可以让插件中拿到的所有LayoutInflater都是我们自定义的LayoutInflater。

LayoutInflater具有一个注入Factory的设计,如果设置了LayoutInflater的Factory,LayoutInflater在构造View时就会先试图让注入的Factory构造。如果Factory能够构造出来,就会优先使用。如果构造不出来,才会走内置的构造逻辑。而我们前面说的问题就是内置的构造逻辑造成的。因此,我们就写了一个自定义的Factory,然后复制了原本内置构造逻辑的代码。在这段逻辑中,也有缓存机制。但是我们将缓存的Key添加了标记插件apk的“partKey”作为一部分,这样相同名字的View在缓存中就是不同的Key了。因此,我们既保留了LayoutInflater原本的缓存设计,还让它支持了多插件。所以在Shadow中,不管是宿主和插件,还是多插件之间,都可以使用相同名字的View。

这里还有一个小的知识点,就是一个LayoutInflater对象只能set一次Factory。把我们自定义的Factory设置进去了,插件的代码拿到这个LayoutInflater就不能再次set Factory了。但是LayoutInflater的设计又允许clone一个LayoutInflater对象,保留它的的Factory。当新的LayoutInflater被set Factory时,一个内置的逻辑会将两个Factory合并起来使用。因此在Shadow的代码中,可以看到ShadowLayoutInflater中包含了一个InnerInflater,套了两层LayoutInflater,就是这个原因。

这部分相关代码可以查看com.tencent.shadow.core.runtime.ShadowFactory2类和相关类的实现。

PS

最新版本的Android系统已经没有这个Bug了。为了兼容低版本的Android系统,这个问题还是需要解决的。

github.com/Tencent/Sha…

欢迎大家参与到Shadow的开发中来,Shadow的代码有非常多值得优化的地方,让我们开源共建起来!