作者:拔根
优酷App接入支付宝小程序框架,扩展了优酷App的能力。但由于内置小程序sdk过程中,优酷App和支付宝App平台运行时环境存在差异,带来了以下几大问题:
- 小程序sdk包体积较大,远远增加了优酷APP的包大小;
- 小程序容器启动后,线程数暴增,叠加优酷主APP场景线程,引发crash率增高;
- 初始化小程序引擎会影响优酷APP启动速度和占用内存。
为解决以上问题,优酷势必要在包大小、线程数、内存等方面有不一样的处理。接下来,本文就将为大家介绍优酷在面临这些差异与问题时的解决方案。
远程化SO
在内置过程中,我们发现支付宝小程序框架大概占了23MB。比较理想的方案是全部远程化,这样不占用优酷App的包大小,但目前优酷短时间内暂不具备全部远程化的条件。通过分析,我们发现,23MB的空间里面so占了7MB,而相对于代码和资源文件,so相对独立。因此我们的方案如下:
- so间存在依赖关系,事先需要收集好,加载的时候按序加载;
- 打包过程中排除这些so,上传到服务器,并记录SO的相关信息。其中包括上传后的远程下载地址、md5值;
- 用户打开APP进入小程序的时候,从服务器下载这些so,存储到指定目录;
- 按照事先收集好的依赖关系,依次加载so。
其中,比较复杂的就是 so 依赖关系分析,我们的处理过程如下:
1、分析依赖关系
objdump -x *.so|grep -i needed|awk '{print $2}'
objdump是Linux下的反汇编目标文件或者可执行文件的命令,它以一种可阅读的格式让你更多地了解二进制文件可能带有的附加信息,类似的还有readelf命令。依赖的 so 有的是操作系统的,有的是其他aar包里面的,其中操作系统的就不用关心了,其他aar包里面的so需要进行记录。
2、收集依赖关系:
aar包名 | SO名称 | 依赖SO名称 | 有效依赖 |
---|---|---|---|
com.AAA:aaa-build | libA1.so | libopenssl.so libandroid.so liblog.so libm.so libstdc++.so libEGL.so libGLESv2.so libOpenSLES.so libz.so libdl.so libc.so | libopenssl.so |
libA2.so | libB1.so liblog.so libandroid.so libstdc++.so libm.so libc.so libdl.so | libB1.so | |
com.BBB:bbb-build | libB1.so | libopenssl.so libc++_shared.so liblog.so libz.so libc.so libm.so libdl.so |
大家可以参考上述表格来对每个so进行依赖信息的收集,最终这些so的关系可以组成一个网状型。如下所示:
3、分层依赖关系:
分层依赖关系的设计原则如下:
- 第一层:不允许依赖任何其他aar包里面的so,只允许依赖操作系统的so或者无依赖的so;
- 第二层:只允许依赖第一层收集的so和操作系统的so;
- 第三层:只允许依赖第一层、第二层收集的so和操作系统的so。
依此类推。
第一层(xx个) | 第二层(xx个) | 第三层(xx个) | 第四层(xx个) | 第五层(xx个) |
---|---|---|---|---|
A1 | B1 | C1 | ||
A2 | B2 | C2 | ||
A3 | B3 | |||
A4 |
4、代码实现
private static String[] LIB_NAMES = {
// 第一层, 不依赖其他so
"A1",
"A2",
"A3",
"A4",
// 第二阶段, 依赖前一阶段加载完毕
"B1",
"B2",
"B3",
// 第三阶段, 依赖前两个阶段加载完毕
"C1",
"C2",
// 第四阶段, 依赖前三个阶段加载完毕
...
// 第五阶段, 依赖前四个阶段加载完毕
...
};
public static void ensureAllSoLoaded() {
try {
for (String lib_name : LIB_NAMES) {
Log.d(TAG, "lib_name:" + lib_name);
System.loadLibrary(lib_name);
}
} catch (Throwable throwable) {
Log.e(TAG, "loadLibrary lib_name exception:");
throwable.printStackTrace();
}
}
5、小结
在集成小程序过程中,优酷通过远程化so,减小了7MB的包体积。相应的,用户首次进入小程序时,需要一定的等待时间,影响了一点用户体验。
注入线程池
在某些手机中,尤其是华为手机,针对APP线程数有上限,超过就会crash。基于此,优酷APP通过统一线程池对线程进行管控的,当线程数达到一定数量时,会通过排队方式执行任务,而不是继续新增线程。
由于业务属性不同,优酷播放器页面启动线程数量很多,使用过程中一旦进入此页面,线程数最高可以达到300+。该页面如果存在小程序入口,一旦启动优酷小程序,线程数会在原来基础上暴增100左右。这样就很容易达到APP线程数上限,导致 java.lang.OutOfMemoryError 错误。
事实上,通过线上监控平台发现,确实有不少此原因导致的Crash。那么,怎么解决这个问题呢?
一个比较好的方案是希望将支付宝小程序框架的线程纳入到优酷的统一线程池来管理。这样,当支付宝小程序框架希望执行任务时,只需要把任务提交给优酷线程池执行,而不用真正的创建线程。优酷线程池根据系统的情况决定是新建线程还是排队执行。
通过和支付宝的同事沟通,他们也认同这种方案。由支付宝小程序框架提供接口,将优酷统一线程池注入给小程序框架。当底层判断已经有优酷统一线程池,就直接使用,不再新建。
实现如下:
public class MyThreadPoolManager implements IThreadPoolManager {
private static final int NCPU = Runtime.getRuntime().availableProcessors();
private ThreadPoolExecutor mThreadPoolExecutor;
@Override
public ThreadPoolExecutor createExecutor(ScheduleType scheduleType) {
if (mThreadPoolExecutor == null) {
mThreadPoolExecutor = YKExecutorService.from("miniappSdk", 2 * NCPU + 1, 2 * NCPU + 1 ,1000, TimeUnit.MILLISECONDS,new SynchronousQueue<Runnable>());
}
return mThreadPoolExecutor;
}
}
// 支付宝小程序框架提供接口,注入优酷线程池。
TinySdk.setThreadPoolManager(new MyThreadPoolManager())
上线前后,从以下数据中可以看到,效果非常显著。几乎各个模块都减少了90%的crash。数据对比效果如下:
懒加载
小程序上线初期,业务量不大,如果一启动优酷APP就立刻初始化小程序框架,影响了启动速度,浪费内存。因此我们的策略是采取懒加载模式。流程如下所示:
1、远程依赖检查
小程序运行是有远程依赖的,下载这些远程依赖需要流量和等待时间。因此我们会在首次打开小程序业务的时候,弹窗告诉用户这些信息,并询问是否同意:如果用户不接受,则不进入小程序APP,关闭弹窗;如果用户接受,则下载远程依赖,并在下载完成后,自动完成后续流程。
2、小程序运行条件检查
最终加载小程序页面的是渲染内核,当运行条件检查发现不具备运行条件,也就是冷启动小程序时,就必须先判断渲染内核是否已经加载完成 (需注意:渲染内核不止小程序一个业务依赖,有可能被其他业务先加载)。
渲染内核初始化完成后,我们继续进行小程序框架的初始化。等到这一步正常结束,小程序运行条件才完全具备。我们才可以正常加载小程序页面。
初始化后,通过变量标识,当小程序退出时,并不会回收全部内存。所以第二次及以后就是热加载小程序了。由于省略了渲染内核的初始化和小程序框架初始化,速度也会快很多。
总结和展望
优酷APP主要为用户提供内容服务,在集成支付宝小程序框架后,扩大了能够提供内容服务的范围,进一步扩大了优酷作为平台的能力。后续优酷仍会继续在以下技术方向努力:
- 将整体小程序框架打包放到云端,完全消除集成小程序框架对优酷app包大小的影响。
- 增加更多通用JSAPI,让业务方使用更加方便。
- 闲时加载取代懒加载,提供更好的业务体验,形成正反馈。
关注我们,每周 3 篇移动技术实践&干货给你思考!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。