背景: Now直播接入信息流各平台后,线上的进房转化率达不到预期
首先分析一下进房流程:
为提升转化率和进房速度,now结合版经历了一些历史优化方案:
1、专属场景预加载now插件
在固定场景对插件进行预加载,能很好的解决首次进入插件慢的问题,但有以下受限:
场景受限,不是所有场景都可以做预加载的事情, 对场景性能消耗大,如feeds流,不适合做预加载。
2、插件拆分(插件的下载加载耗时与插件大小成正比)
一期我们将插件拆分成了2个插件:音视频插件 业务插件,其中音视频插件< 1M,极大提升了看到音视频的转化率
二期我们继续对业务插件 拆分,拆成了多个插件。
插件拆分核心思想是减少核心功能插件的加载启动耗时,不用下载完整插件更快展示。
拆分插件后,显示核心内容速度有明显提升,但首次仍有一定的时耗,转化率离预期90%仍有一定的差距
6.5s的平均耗时表现在平台的非直播专属场景下转化率仍然不够理想,如feeds场景,视频合集tab场景。而且这部分场景很多用户是属于首次点击。
对线上用户行为统计数据分析:
理论上如果将首次进房速度控制在2s内, 可以减少大部分的进房过程取消。
纯插件模式下: 首次要在2s内完成核心功能包含的3M插件的下载加载、音视频播放、进房是不可能的 结合版也经历了无数次减包,插件减包已经没有了空间。
寻求优化方案
其实房间很多基础能力是长期稳定不变的:
- 音视频播放能力
- 主播信息
- 成员列表
- 公屏消息
- 聊天功能
- 操作栏UI中部分功能
可否将稳定的核心内容放入宿主中? 用户以最快的速度看到一个较完整的房间界面,核心功能秒开。
其他业务再以插件形式动态加载到宿主的页面中 并且让用户对插件下载加载的过程无感知,自然过渡。如下图:
我们确立了方案基本模型:
我们整个插件是基于部门内部自研的插件框架Shadow来做的。 Shadow是什么,可以参见文章: 全新原理的Android插件框架——Shadow
确立方案后,我们面临几个问题:
问题1:房间内其他业务插件怎样融合到宿主容器中?
宿主容器叠加插件思路--各插件的View统一由一个容器View托管
技术方案分析-对插件加载模式的改变:
宿主容器获取插件View技术方案分析:
设计到2点:
1)插件的容器View由谁创建出来?
2)宿主内的模块和插件模块如何通信?
1、原有IPC通道:使用成本较高,消耗性能,不适合频繁交互
2、需要有非常轻量和简易的通信通道:
- 可以让宿主容器 与插件在同个进程;
- 能相互持有接口,降低开发成本;
方案2是比较理想的,但是我们就面临以下问题:
为此我们引入了shadow类加载的白名单机制:
有了这个机制可以很轻松的完成我们的宿主和插件双向持有接口通信,整体方案如下:
问题2:原生Activity无法在宿主与插件中传递 --NoSuchMethodError
插件中大量用到了Activity的类实例以及使用Activity的方法,如下:
按照我们通常做法,我们会定义通过接口向宿主获取:
调用接口报错:java.lang.NoSuchMethodError
类实例中有getActivity方法,为什么找不到?
经过分析后原因如下,和Shadow的机制有关:
Shadow中Activity在编译期会被改写成插件框架的普通类actvity (中间层是shadow中非常经典的一个实现,实现0 hack)
这会导致我们刚定义的接口编译器会被转换:
插件中接口被转换,而宿主没有,导致双方不是同个接口,就报NoSuchMethodErr了
解决思路一:宿主Activity继承shadow的壳子Activity: PluginContainerActivity
这样:壳子Activity不再是一个空壳子,而是装载了一定的内容 再与插件activity中的内容融合
缺点:
- 宿主需依赖打包shadow Loader,
- 200k增量 宿主与shadow强耦合,不符合shadow Loader动态下发原则,破坏了插件框架设计规则
- 插件框架需要对这种场景单独做定制化改动
思路二:插件反向代理宿主Activity
在不打破插件框架设计规则下有没有其他解决办法?
既然只识别插件体系下的Activity,可以给插件一个自己的Activity,再由这个Activity反向代理插件 无法识别的宿主原生Activity
插件中模拟创建了一个Activity,那具体怎样代理宿主的真实Activity?
答案:不再从宿主获取Activity,而是获取一个接口
这样就达到了我们插件使用宿主activity的目标。
修改后我们发现Activity方法中仍会被插件框架修改的类--FragmentManager
难道我们又要加一层代理吗?
思路:最理想的是在宿主中构造一个PluginFragmentManager返回
- 调用方法是在插件,插件运行时shadow loader已启动
- 非插件启动流程,不需破坏插件框架流程
宿主对ShadowLoader是依赖还是反射调用?其实都可以
当我们尝试在宿主中创建PluginFragmentManager时出现以下错误:
整体解决方案如下:
肯定有人会问,宿主中的这部分功能控件想要更改时,只能跟版本修改了吗?
我们针对宿主的这部分本地控件提供了一套托管机制:
有2类控件,一个是已存在宿主中的控件,一个是和宿主控件有联动的控件,比如在一个区域有相互依赖。
我们和产品商定:
规范了一些产品需求常变更区域 这些区域做扩展由插件动态实现,如上图中的绿色区域:主播互动区域,扩展区域。
我们实现了2种托管:静态托管宿主已有的View,动态扩展联动扩展区域
- 动态扩展区域,大部分需求不用跟版本
- 插件动态更新宿主控件能力,动态换肤
问题3:插件托管宿主控件带来的资源问题
插件获取到宿主View后,会有设置资源的操作,对宿主已有View设置会出现ResourceNotFoundException
这时由于插件和宿主的资源是隔离的,我们去iewshe给宿主中的View设置插件的资源是会报错的。
相关源码分析:
资源问题方案选型一:
资源问题方案选型二:
怎样让宿主View使用插件自身的context获取资源?
Context替换思路1:反射修改View的Context
看上去是OK的,但引发下列问题:
由于View是共用的,这样会导致混乱,原则上讲宿主属于第一优先级
资源问题方案选型三:
继续寻找替换Context的方法:
对所有View的源码阅读中:发现直接使用R资源的函数都有用resource获取资源的替代方法,例如:
Context替换思路2:不直接使用R资源,采用替代资源设置方法:
方式1:所有设置资源地方手动改成这种使用模式:
问题:
- 对用户开发习惯改变大
- 后续开发与维护成本高,容易出错;
方式2:采用AOP思想,编译期对字节码处理
安装包问题-怎样保证宿主增量最小?
整个核心内容在原来插件中是有3M的,这个增量对于平台来说是无法接受的!
为了增量降到最低,做了如下工作:
我们将这套方案整理成了深度整合模式的SDK,方便各平台快速接入:
根据宿主能力去适配:
经过深度整合后,宿主进房效果得到极大提升,以下是我们几个版本的对比(双插件,多插件,深度整合版本):