岁寒之松柏:小程序skyline渲染引擎初尝试
发布于 2 年前 作者 pingdong 1453 次浏览 来自 分享

小程序架构介绍

我们都知道小程序本质上是运行在安卓端,苹果端的混合APP,只是微信提供了一套JSBridge,方便用户对一些原生功能和微信相关的功能的进行调用。而微信为了安全和性能的需要,一改以往网络架构中的单线程架构,改为小程序的双线程架构。分别是AppServie 和 Webview 两个线程,我们在小程序中编写的JS代码就是运行在AppService线程的JSCore引擎(类似V8 引擎,一个Js解释器)中,而我们的Wxml和Wxss则会依赖WebView线程进行渲染。

目前架构存在的问题

这样的架构虽然已经极大了提高了webview的渲染性能,但是依然会存在一些问题比如:

  1. 当页面节点数目过多,很容易发生卡顿
  2. 当我们新建一个页面,就要新建一个Webview进行渲染
  3. 页面之间共享资源,需要使用Native进行通信,就会消耗更多性能
  4. 当AppService(逻辑层)与Webview(视图层)通信也需要依赖Native

所以为了解决这些问题小程序推出Skyline渲染引擎

Skyline引擎介绍

在Skyline环境中,Skyline 会创建了一条渲染线程来负责 Layout, Composite 和 Paint 等渲染任务,并在 AppService 中划出一个独立的上下文,来运行之前 WebView 承担的 JS 逻辑、DOM 树创建等逻辑。说白了就是之前的样式计算是放到渲染线程来处理,现在把和样式相关的逻辑也放到AppService线程中处理,个人猜测这个渲染线程很有可能很有可能就是flutter,这样的架构就极大减少内存的消耗,和线程上通信时间的消耗。原本wxs中的逻辑,也可以移到Appservice线程中运行

使用Skyline引擎的使用步骤

  1. 在app.json 文件添加 "lazyCodeLoading": "requiredComponents" 属性,这是因为Skyline 依赖按需注入的特性。
  {
  "pages": [
    "pages/index/index",
    "pages/logs/logs",
    "pages/test/test"
  ],
  "window": {
    "backgroundTextStyle": "light",
    "navigationBarBackgroundColor": "#fff",
    "navigationBarTitleText": "Weixin",
    "navigationBarTextStyle": "black"
  },
  "sitemapLocation": "sitemap.json",
  // 在 app.json 文件添加
  "lazyCodeLoading": "requiredComponents"
}
  1. 在全局或页面配置中声明为 Skyline 渲染,即 app.json 或 page.json 配上"renderer": "skyline"
  2. Skyline 不支持页面全局滚动,需在页面配置项加上 "disableScroll": true,在需要滚动的区域使用scroll-view 实现
  3. Skyline 不支持原生导航栏,需在页面配置项加上 "navigationStyle": "custom",并自行实现自定义导航栏
 {
  "usingComponents": {},
   // 在 app.json 文件添加或者page页面的json中添加
  "disableScroll": true,
  "navigationStyle": "custom"
  //也可以放在App.json文件中
  "renderer": "skyline"

 }
  1. 组件适配,参考 Skyline 基础组件支持与差异
  2. WXSS 适配,参考 Skyline WXSS 样式支持与差异
  3. 在本地设置中勾选Skyline渲染调试,如果看不到这个选项框,看一下是否在app.json中配置了"renderer": "skyline"

Skyline的 worklet 动画介绍

小程序采用双线程架构,渲染线程(UI 线程)和逻辑线程(JS 线程)分离。JS 线程不会影响 UI 线程的动画表现,如滚动效果。但引入的问题是,UI 线程的事件发生后,需跨线程传递到 JS 线程,进而触发开发者回调,当做交互动画(如拖动元素)时,这种异步性会带来较大的延迟和不稳定,worklet 动画正是为解决这类问题而诞生的,使得小程序可以做到类原生动画般的体验

worklet函数定义

function helloWorklet() {
  'worklet'; //'worklet'声明该函数为work函数,可以在js线程和UI线程中调用
  console.log('hello worklet');
}
Page({
  onLoad(options) {
    helloWorklet('hello') // print: hello
    wx.worklet.runOnUI(helloWorklet)()
  },
})

在小程序控制台可以看到如下输出

如果看见SkylineGlobal is not defined错误看看是否__开启了Skyline渲染调试__

worklet函数间的相互调用

function slave() {
  'worklet';
  return "I am slave"
}
function master() {
  'worklet'; 
  const value = slave()
  console.log(value);
}

从 UI 线程调回到 JS 线程

const {runOnUI ,runOnJS} =  wx.worklet

function jsFun(message) {
  // 普通函数不需要声明为worklet
  console.log(message)
}
function uiFun() {
  'worklet';
   runOnJS(jsFun)('I am from UI')
}

使用shared共享数据

由worklet函数捕获的静态变量,会在编译期间序列化后生成在UI线程的拷贝环境之中,这就导致我们在JS线程中后续更新了变量,但是在UI线程中时得不到最新的数值的。

const obj = { name: 'skyline'}
function someWorklet() {
  'worklet'
  console.log(obj.name) // 输出的仍旧是 skyline
}
obj.name = 'change name'

wx.worklet.runOnUI(someWorklet)() 

因此shyline使用shared来实现线程之间数据的共享

const { shared, runOnUI } = wx.worklet

const offset = shared(0)
function someWorklet() {
  'worklet'
  console.log(offset.value) // 输出的是新值 1
}
offset.value = 1

runOnUI(someWorklet)()

简单案例–实现探探的卡片功能

注意:编辑器版本:1.06.2303162 基础库版本:2.30.2

先看效果

代码如下 <br>
wxml 代码

<navigation-bar title="探探" />
<view class="page">
  <block wx:for="{{containers}}" wx:key="*this">
    <pan-gesture-handler data-id="container-{{index}}"  onGestureEvent="handlePan">
      <view id="container-{{index}}" class="container" style="z-index: {{zIdnexes[index]}};background-image: url({{partContentList[index]}});">
      </view>
    </pan-gesture-handler>
  </block>
</view>

scss代码

.page{
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
    width: 100vw;
    position: relative;
    .container{
      height: 80vh;
      width: 95vw;
      background-color: burlywood;
      position: absolute;
      border-radius: 16rpx;
      display: flex;
      justify-content: center;
      align-items: center;
      background-size: cover;
      .image{
        display: block;
        height: 1067rpx;
        width: 712rpx;
        margin: 0 0;
      }
    }
}

核心逻辑

import { useAnimation, setAni, Animation, GestureState } from "./method"

Page<{ pos: Animation }, any>({

  /**
   * 页面的初始数据
   */
  data: {
    containers: [
      "burlywood",
      "blue",
      "cyan",
      "black"
    ],
    zIdnexes:[],
    current:0,
    partContentList:[]
  },

  /**
   * 生命周期函数--监听页面加载
   */
  onLoad() {
    this.initNode()
    // 当前node的下标
    this.active = wx.worklet.shared(0)
    // 当前contentList的下标
    this.current = wx.worklet.shared(0)
    this.zIndex = 100000

  },
  initNode() {
    // 用与保存shared值
    this.Nodes = {}
    // 图片文件
    this.contentList = [
      "https://i.hexuexiao.cn/up/ca/63/4a/a32912fc26b8445797c8095ab74a63ca.jpg",
      "https://th.bing.com/th/id/OIP.kSrrRGx6nqOgWzbaEvVD9AHaNK?pid=ImgDet&rs=1",
      "https://img.zmtc.com/2019/0806/20190806061552744.jpg",
      "https://img.zmtc.com/2019/0806/20190806061000600.jpg",
      "https://img.ratoo.net/uploads/allimg/190523/7-1Z5231J058.jpg",
      "https://th.bing.com/th/id/R.47de9dfcc25d579d84850d4575d24a6a?rik=%2fGkmrewzIEY4Iw&riu=http%3a%2f%2fimg3.redocn.com%2ftupian%2f20150930%2fqizhimeinvlisheyingtu_5034226.jpg&ehk=rG9Ks2QRzj81mZl38gVGmWVAgCHVLWppoDezpfwdxjo%3d&risl=&pid=ImgRaw&r=0",
      "https://th.bing.com/th/id/R.95f8e6f6bd5b660ae3ad4f3e0d712276?rik=ELKcha%2bE5ryuiw&riu=http%3a%2f%2f222.186.12.239%3a10010%2fwlp_180123%2f003.jpg&ehk=mVN7AzIRR%2fmVPJYWrWOFbEiher3QWtwSdH%2f%2fe4lE7n8%3d&risl=&pid=ImgRaw&r=0"
    ]
    this.data.containers.forEach((_: string, index: number) => {
      if (index == 0) {
        this.Nodes[`#container-${index}`] = useAnimation(`#container-${index}`, { x: 0, y: 0 }, this)
        this.setData({
          [`zIdnexes[${index}]`]:100000-index,
          [`partContentList[${index}]`]:this.contentList[index]
        })
      } else {
        console.log("10123")
        this.Nodes[`#container-${index}`] = useAnimation(`#container-${index}`, { x: 0, y: 20, scale: 0.95 }, this)
        this.setData({
          [`zIdnexes[${index}]`]:100000-index,
          [`partContentList[${index}]`]:this.contentList[index]
        })
      }

    });
  },
  handlePan(evt: any) {
    "worklet";
    console.log(evt)
    const now = this.Nodes[`#container-${this.active.value}`] as Animation
    const next = this.Nodes[`#container-${(this.active.value+1)%4}`] as Animation
    if (evt.state == GestureState.ACTIVE) {
      // 滑动激活状态
      // 设置当前的滑动块
      now.x.value += evt.deltaX
      now.y.value += evt.deltaY
      now.rotate.value = now.x.value * 10 / 360
      // 设置下一个滑动块
      let rate = Math.abs(now.x.value) / 150
      rate = rate > 1 ? 1 : rate
      next.y.value = (20 - rate * 20) < 0 ? 0 : (20 - rate * 20)
      next.scale.value = 0.95 + rate * 0.05
    }
    if (evt.state == GestureState.END) {
      // 滑动结束
      if (Math.abs(now.x.value) < 150) {
        // 判断是否超过界限值
        setAni(now.x, 0)
        setAni(now.y, 0)
        setAni(now.rotate, 0)
      } else if (now.x.value < 0) {
        // 判断判断左划还是右划
        setAni(now.x, -2000)
        setAni(now.y, -2000)
        setAni(now.rotate, 0)
        // 通知js线程进行数据的更新
        wx.worklet.runOnJS(this.toNext.bind(this))()
      } else if (now.x.value > 0) {
        setAni(now.x, 2000)
        setAni(now.y, -2000)
        setAni(now.rotate, 0)
        wx.worklet.runOnJS(this.toNext.bind(this))()
      }
    }
  },
  // 将当前序号的跳转到下一个
  toNext(){
    const current = this.current.value+1
    this.active.value = current%4
    this.current.value = current
    this.setData({
      current
    })
    if(current-2>=0){
      wx.worklet.runOnUI(this.toReset)((current-2)%4) 
      this.setData({
        [`zIdnexes[${(current-2)%4}]`]:99998-current,
        [`partContentList[${(current-2)%4}]`]:this.contentList[current+2]
      })
    }

  },
  // 将动画归位
  toReset(index:number){
    "worklet";
    const reset = this.Nodes[`#container-${index}`] as Animation
    setAni(reset.x, 0,0)
    setAni(reset.y, 20,0)
    setAni(reset.rotate, 0,0)
    setAni(reset.scale, 0.95,0)
  }
})

参考

skyline worklet 动画

2 回复

麻烦问一下setAni方法的作用是什么呢 新手没太看懂

这个 demo 看起来不错 👍

回到顶部