让我们一起来做小程序直播吧!

4,214 阅读11分钟

直播这两年太火了,做直播的公司也越来越多,尤其是今年小程序针对直播组件的优化(性能优化 + 同屏渲染),催生了很多的小程序直播应用。京喜直播的小程序端上线也有几个月了,基于内部分享,当前稍加总结一下,我这里就主要讲讲直播的几个重点场景。

微信扫码,欢迎体验:
京喜直播

本文目录:

  • 直播间消息处理
  • 直播间上下滑
  • 组件间通信
  • Canvas 多合一

直播间消息处理

直播相对于传统媒介,主要特点是互动性更强,因此产生了大量的互动行为,比如点赞,下单,领券等各种消息需要广播,于是就产生了大量的消息内容,那么对于这些消息我们如何处理呢?

第一种方法

直接接受到消息就渲染到页面上去,这种最简单,代码如下:

function receiveMessage(msg){
    this.data.msgList.push(msg);
    this.setData({
        msgList:this.data.msgList
    });
}

这种方式固然简单,但是在消息越来越多的时候,数组越来越大,节点会越来越多,直播间最终会卡爆,对于本来就是希望延长用户观看时长的直播,这样子显然不可取。

第二种方法

既然随着时间推移,消息会越来越多,导致节点越来越多,从而可能页面响应时间过慢,甚至卡死,那么我们就应当减少节点。处理如下:

function receiveMessage(msg){
    if(this.data.msgList.length>30){
        this.data.msgList.shift();
    }
    this.data.msgList.push(msg);
    this.setData({
        msgList:this.data.msgList
    });
}

如上,我们页面当中的消息节点就会被控制在 30 个以内,当然还有一个很重要的,需要采用 “就地更新”的策略。

就地更新策略:内容变化的时候,无需重新创建 DOM 节点,而是共用已有的 DOM 节点,然后更新里面的内容就行了,性能大大的提升了。消息节点不会随着时间推移而变大,满足大部分场景是没有问题的。

第三种方法

如上第二种方法,在超过 30 个节点之后,会每增加一条,再删除一条,并且采用就地更新的策略,看起来也没啥问题了。然而在有些非常火爆的情况下,我们仍然发现了页面比较卡的问题。究其原因是 shift 性能不怎么滴,当消息量过快,执行 shift 的次数就会很频繁,也就是 msgList 修改很频繁,带来的结果就是页面一直在执行 shift 和 push ,渲染到实体 Dom 的次数很频繁,性能就跟随者大幅度下降了。

首先想着减少操作或者换性能更强的实现方式来实现。于是想到了 slice,splice,我们不妨来看看这三个的性能如何。

slice & splice & shift 性能对比

首先,我们使用 benchmark 来测试,测试代码如下:

var Benchmark = require('benchmark');
var suite = new Benchmark.Suite();
const MAX_COUNT = 100000;
suite
  .add("shift#test", function () {
    var list = [****];// 预设 30 个节点
    let i = 1;
    while (i < MAX_COUNT) {
      list.shift();
      list.push({ a: i, b: "b" });
      i++;
    }
  })
  .add("slice#test", function () {
    var list = [****];// 预设 30 个节点
    let i = 1;
    while (i < MAX_COUNT) {
      list = list.slice(1);
      list.push({ a: i, b: "b" });
      i++;
    }
  })
  .add("splice#test", function () {
    var list = [****];// 预设 30 个节点
    let i = 1;
    while (i < MAX_COUNT) {
      list.splice(0, 1);
      list.push({ a: i, b: "b" });
      i++;
    }
  })
  ……
  .on('cycle', function(event) {
    console.log(String(event.target));
  })
  .run({ async: true });

测试数据如下:

  • shift#test x 207 ops/sec ±6.88% (78 runs sampled)
  • slice#test x 114 ops/sec ±1.89% (71 runs sampled)
  • splice#test x 80.90 ops/sec ±0.71% (67 runs sampled)

207 ops/sec : 每秒钟执行测试代码 207 次。

±6.88% (78 runs sampled) : 抽样 78 次结果中,上述 ops/sec 的统计误差幅度为 6.88%%。

从数据上看,这里可以得出的性能排比顺序:

shift 》 slice 》splice

这样看起来我们之前选择的 shift 还是性能最强的,当然我们可以从 ECMAScript 规范当中来发现这三者的差异,确实 shift 会更快。

参照 ecma262 文档 shift:tc39.es/ecma262/#se…
参照 ecma262 文档 slice:tc39.es/ecma262/#se…
参照 ecma262 文档 splice:tc39.es/ecma262/#se…

再比较

通过查看 ecma 文档,我们可以轻易的发现为啥 shift 要更快,他们的大体实现如下:

  • shift:做一次循环移位

  • splice:需要新创建一个数组,局部循环一次用于保存删除的数据,然后需要做一次循环移位

  • slice:每次操作都需要创建一个新数组,然后将需要保留的数据移到新数组。

由此看来,shift 会更快一点,也是意料之中,只需要一次循环移位即可。

虽然 slice 和 splice 除了一次循环之外,还有额外开销,但是我们可以采取积累一段数据,再统一删除的方式来减少对数组的执行频率。

修改测试代码如下:

.add("slice#test", function () {
    var list = [****];// 预设 30 个节点
    let i = 1;
    while (i < MAX_COUNT) {
      if (i % 20 == 0) {
          list = list.slice(20);
        }
      list.push({ a: i, b: "b" });
      i++;
    }
  })
  .add("splice#test", function () {
    var list = [****];// 预设 30 个节点
    let i = 1;
    while (i < MAX_COUNT) {
        if (i % 20 == 0) {
            list.splice(0, 20);
        }
      list.push({ a: i, b: "b" });
      i++;
    }
  })

于是,我们再用 benchmark 测试一下,结果如下:

shift#test x 210 ops/sec ±7.33% (81 runs sampled) slice#test x 972 ops/sec ±1.69% (86 runs sampled) splice#test x 696 ops/sec ±0.38% (89 runs sampled)

从这个数据我们可以看出:

slice 》 splice 》 shift

于是最佳方法应该是使用 slice 来更新 msgList 数据。

为啥这样子 slice 又会是最快的呢?如果有疑问,可以再试试阅读 ecma 文档,来体会执行的差异。

直播间上下滑

我们先看下效果,长按小程序二维码,在列表中点击一个进入直播间,然后上滑查看。

京喜直播
京喜直播

小程序直播间怎么做上下滑呢?

relative + animation

这个是H5最通常的做法,京喜H5直播间的上下滑就是这么干的,但是很遗憾,在小程序中行不通,因为实在是 太……卡 了。

scroll-view

scroll-view 滑动自如,我们可否将直播间内容按一个一个 item(height:100vh)顺序排列呢,这样用户将页面滑动到哪就播放哪个直播。

我们确实也尝试了这种方式,但是仍然带来了几个问题:

  1. scroll-view 翻页太灵敏了,还带有很强的惯性,效果上难以做到给用户一页一页的翻阅的体验(动态控制里面的 item 效果也不行),
  2. 滑动到不是整数(100vh)的高度的时候,需要校准归位,响应仍然过慢。

swiper

经过一翻比较,仍然需要采用 swiper 来做直播间上下滑。我在 2019 年 2 月份做小程序视频上下滑的时候,就是采用此方法,没想到一年过去了,仍然还是卡卡卡(不过性能还是有大幅的提升哈~)。

首先,我们来看下布局:

<swiper class="scroll-wrap" circular="{{circular}}" vertical="true" duration="300" bindchange="itemChange" bindanimationfinish="animFinish">
    <swiper-item wx:for="{{list}}" wx:key="item" class="scroll-item">
        <image class="item-image"  src="{{item}}" wx:if="{{index>=currentIndex-1&&index<=currentIndex+1}}"/>
    </swiper-item>
    <view class="scroll-content" animation="{{animData}}">
        ……        
    </view>
    </swiper>

这里用一个 swiper 包裹起来,里面按照需要显示的直播列表渲染 swiper-item。非常可喜可贺的是我们在监听用户手势滑动 swiper 的时候,再去实时操作 scroll-content 的 animation ,居然不卡了。当然这里和当前页面只有一个 scroll-content 也有关系。

scroll-content 就是页面的直播所有内容了,节点很庞大,这个 scroll-content 在初始化之后,就一直存在页面中,只是根据滑动到不同的直播间,然后更新里面的内容。

组件间通信

首先,我们来讲下组件通信方式,小程序的组件间通信,通常主要有如下几种方式:

Props & triggerEvent 属性间通信

这种方式是最直接最简单的,相信也是大家使用的最多的方式。

  • 优点:简单快捷,容易理解。
  • 缺点:是通信都需要经过视图层,影响性能。

为啥说这种普遍的方式影响性能呢?

如下摘自小程序文档的一段话:

小程序的视图层目前使用 WebView 作为渲染载体,而逻辑层是由独立的 JavascriptCore 作为运行环境。在架构上,WebView 和 JavascriptCore 都是独立的模块,并不具备数据直接共享的通道。当前,视图层和逻辑层的数据传输,实际上通过两边提供的 evaluateJavascript 所实现。即用户传输的数据,需要将其转换为字符串形式传递,同时把转换后的数据内容拼接成一份 JS 脚本,再通过执行 JS 脚本的形式传递到两边独立环境。

从这段话,我们总结出来就是:

  1. 逻辑层(js)和视图层(wxml)之间的交互性能不是那么好,不要太频繁更新。
  2. 逻辑层(js)和视图层(wxml)的交互还需要通过 evaluateJavascript 实现,需要转换 + 反转编译,不要更新太多内容。

因此,我们如果需要开发高性能的小程序,那么就不得不需要考虑一下了。

emit & on 订阅发布模式通信

这个也是广为使用的一种简单方式,订阅发布者模式,我们在H5中也大量的使用到。

  • 优点:容易理解,容易使用,且通信不限组件关系
  • 缺点:是无状态的,可能有 BUG

为啥会可能有 BUG 呢?

主要还是订阅发布者模式,通常来说是无状态的,不受制于页面实例,比如京喜小程序,直接使用 getAPP().events 就可以使用了。

订阅发布模式
订阅发布模式

如上图的访问路径,那么如果 B 页面的代码发布一个消息通知,那么当前缓存的两个页面 B 都会接受到这个消息,做出响应执行代码,这样子,两个 B 页面的数据就相互污染了。

引起如上消息串了的根本原因是 getAPP().events 是属于小程序级别的消息通知,然后该消息通道又是无状态的,固然可能会被污染。

当然,要解决这个问题,也是可以的,我们可以从如下两个方面来解决:

  • 将 events 绑定到页面级实例,那么只会和当前页面实例绑定。
  • 给 events 加上作用域,比如注入当前页面的实例,只会是当前页面实例上的监听才会接受到消息。

selectComponent 通信

小程序提供了 selectComponent 方法用来获取子组件的实例,于是我们在父子之间的传递就非常容易了。

  • 优点:性能好,直接执行方法
  • 缺点:只能父子组件,不能父亲孙组件
selectComponent
selectComponent

如上图所以,A 组件和 B 组件通信都是经过 父pages 来中转的,仅仅是逻辑层的代码调用,所以性能非常优秀,不经过视图层,通信没有啥成本。

在执行 this.selectComponent 的时候,确实也是需要消耗时间的,但是我们只需要把获取到的实例缓存起来即可。

当前京喜直播间主要还是使用 selectComponent 来实现组件间的通信。

定义 corssComponent 方法来实现跨组件通信,然后直接调用即可。

export function crossComponent ({ id, fun, params }) {
    if (!id || !fun) return;
    const key = id + '_com';
    if (!this[key]) {
        this[key] = this.selectComponent('#' + id);
    }
    if (this[key] && typeof this[key][fun] == 'function') {
        return this[key][fun](params);
    }
    // 兜底,拿不到实例的时候,再重复拿实例,保存起来
    const self = this;
    (function _t (c) {
        if (c >= 10) return;
        const com = self.selectComponent('#' + id);
        if (com) {
            self[key] = com;
            com && typeof com[fun] == 'function' && com[fun](params);
            return;
        }
        setTimeout(() => {
            _t(++c);
        }, 200)
    })(0);
}
this.crossComponent({id: 'b组件',fun:'add',params:{count:1}});

刚才我们提到了 selectComponent 是不支持子孙组件直接通信,那么我们如果需要子孙组件通信怎么办呢?

其实也不难想到,我们只需要提供保存子孙组件的 crossComponent 的方法即可,我们可以逐级往 子,孙 查找实例,比如 id:"B__b2" 来代表 B 组件的子组件 b2,找到 B 组件的实例,再次 selectComponent 找到 b2,然后将所有的实例保存起来,这样子就可以完全的跨组件通信了。

Canvas 多合一

Canvas 这个东西太好用了,H5如此,小程序也是一样。

  • 深爱的Canvas:功能非常强大,使用非常广泛
  • 讨厌的 Canvas:太太太……耗性能了

在小程序 Canvas3d 时代,性能更是瓶颈。当然目前小程序推荐的 Canvas2d 在性能方面改善很多,但是如果页面中有多个地方需要使用到 Canvas 呢?

很不幸,直播间小程序有两个功能需要使用到 Canvas:分享图片的绘制 和 点赞。

初期我们直接页面初始化两个 canvas ,然后发现在低内存手机上就很卡了,甚至直接退出。

解决这个问题,其实可以在页面中只创建一个 Canvas 节点,初始化一次 Canvas 来解决。

欢迎关注我的微信公众号,一起做靠谱前端!

follow-me