Shadow为什么要求插件和宿主包名一致

8,637 阅读3分钟

原因

我们过去也用过基于各种反射实现的插件框架,用了3年左右时间,也维护了3年左右时间。在过去维护的经验中,我们就发现了插件使用单独包名(ApplicationId,下同)带来的问题。

ApplicationId一般是在build.gradle中设置的,在编译时这个字符串会被记录在2个位置。第1是记录在应用的AndroidManifest.xml中,第2是记录在应用的resources.arsc文件中。

记录在AndroidManifest.xml中的包名主要用来构造应用的Context对象。在我们开发中一般通过context.getPackageName()方法获得到当前应用设置的ApplicationId。但是重要的是,系统也会通过context获取包名来识别context来自于哪个安装的应用。我们知道系统不允许安装多个相同ApplicationId的应用,这也是因为系统就是根据这个包名来区分安装的应用的。通过这个ApplicationId,系统也可以反向查找到应用安装在系统中的apk文件路径。

问题在于系统不是非常简单的和我们一样只会调用context.getPackageName()方法获得应用的ApplicationId。还会调用一些私有API获取,例如getOpPackageName()方法。所以我们过去以Hack方式实现的插件框架中,只能不停的兼容各种OEM系统、各种Android版本,在Override了这些获取PackageName的方法后,通过抛一个Throwable,查询当前调用来自哪里。如果来自于系统,我们就会返回给系统宿主的包名。否则系统就会拿到一个没有安装的包名,抛出SecurityException,造成Crash。

在设计Shadow时,我们坚持一个原则来避免使用私有API,就是通过一层中间件将插件代码变成宿主代码的一部分。实际上这个过程是可以通过手工将中间件和插件代码都写在宿主中达到相同效果的。所以在这个设计中,插件代码实际上就是宿主代码的一部分。既然是一部分,ApplicationId怎么会不一样呢?所以要求插件和宿主的ApplicationId保持一致,就永远不会将插件代码没有安装这件事暴露给系统。

记录在resources.arsc的ApplicationId其实算一个小问题了。Resources对象上有一些API是接收包名作为参数的,如果这个包名在独立安装和插件环境下动态获取的不一样,那么有可能造成这些API失效,找不到想要的资源。

应对措施

Shadow代码中并没有什么对包名的特殊处理逻辑,只有一处检查包名是否一致的逻辑(com.tencent.shadow.core.loader.blocs.ParsePluginApkBloc#parse)。去掉这段逻辑大部分情况下插件也是可以运行的。只有在一些OEM手机的特殊场景会出问题,比如一些国产手机系统的WebView或者输入栏中长按弹出菜单就有可能会Crash。所以去掉了这个限制后,就需要不停地去兼容各种OEM系统。这也不是不可行,因为Shadow有全动态的设计,插件框架的兼容代码也可以动态更新。

但是我们认为更合理的方法还是保持插件和宿主包名一致。只需要有一套完善的自动构建CI/CD,针对不同渠道自动修改ApplicationId,编译出ApplicationId不同的插件包去分发也不是很难的事情。关键是,这件事看起来麻烦,实际上长期来看不需要人工干预。而不停地兼容OEM系统,则需要长期投入人力人工分析解决。

所以,建议大家保持插件和宿主包名一致。