- AR小程序持续踩坑指南
为了兼容安卓帧率问题,转而采用此方案,文档被删除,文档被删除,文档被删除,通过报错信息和朋友得知使用方法 初始化:<开发者工具限定1.0.5及其以上> WXWebAssembly.instantiate(obsoluteWasmFilePath,importObject) 微信初始化胶水代码算法文件方式: WebAssembly:可以加载本地文件,微信8.0.2起WebAssembly对象被删除 WebAssembly.compile() // 已废弃,文档也已删除,先compile加载wasm文件,再instantiate实例化,其instantiate与WXWebAssembly.instantiate完全不同,传入的是一个arraybuffer对象 WXWebAssembly:没有compile方法,初始化仅支持加载代码包内的wasm绝对路径文件,如/pages/index/index.wasm; WXWebAssembly.instantiate(obsoluteWasmFilePathath,importObject) // obsoluteWasmFilePathath为非wxfile://和http开头且以.wasm结尾的绝对路径文件 // importObject为wasm算法模型文件中初始化引入的方法 wasm模型文件: 其中包括算法部分和模型部分<人脸检测,生物检测等等等等>,以及渲染素材,比如眉毛,腮红眼影等 可分拆为单独的data模型文件部分:人脸检测模型,渲染素材 鉴于人脸算法模型文件很容易就超出2M了,可做如下拆分: Wasm文件可以缩减为仅包含核心算法库和渲染库部分,模型文件抽离成model.data文件,通过WXWebAssembly.instantiate实例化之后导入的方法远程加载模型 模型文件在tensflowjs内包含,model.json和model.bin文件,json文件加载bin文件,c++也是类似机制,一个完整的单独wasm文件包含了从c或者c++转译来的 微信文件系统分为: 代码包文件:仅支持绝对路径读取,且不允许动态增删改,如:/pages/index/index.wasm; 本地文件:通过网络方式下载的文件或缓存文件,其文件路径以wxfile://或http开头,且不允许硬编码为绝对路径,通过wx.env.USER_DATA_PATH也无法编译成绝对路径 本地文件的文件路径均为以下格式: {{协议名}}://文件路径 其中,协议名在 iOS/Android 客户端为 "wxfile",在开发者工具上为 "http",开发者无需关注这个差异,也不应在代码中去硬编码完整文件路径。 相关阅读:微信小程序文件系统说明文档 以上是使用胶水代码小程序初始化过程中遇到相对棘手问题,分享出来,希望在文档出来前对大家有所帮助,持续更新,欢迎留言
2021-04-14 - 小程序同层渲染原理剖析
众所周知,小程序当中有一类特殊的内置组件——原生组件,这类组件有别于 WebView 渲染的内置组件,他们是交由原生客户端渲染的。原生组件作为 Webview 的补充,为小程序带来了更丰富的特性和更高的性能,但同时由于脱离 Webview 渲染也给开发者带来了不小的困扰。在小程序引入「同层渲染」之前,原生组件的层级总是最高,不受 [代码]z-index[代码] 属性的控制,无法与 [代码]view[代码]、[代码]image[代码] 等内置组件相互覆盖, [代码]cover-view[代码] 和 [代码]cover-image[代码] 组件的出现一定程度上缓解了覆盖的问题,同时为了让原生组件能被嵌套在 [代码]swiper[代码]、[代码]scroll-view[代码] 等容器内,小程序在过去也推出了一些临时的解决方案。但随着小程序生态的发展,开发者对原生组件的使用场景不断扩大,原生组件的这些问题也日趋显现,为了彻底解决原生组件带来的种种限制,我们对小程序原生组件进行了一次重构,引入了「同层渲染」。 相信已经有不少开发者已经在日常的小程序开发中使用了「同层渲染」的原生组件,那么究竟什么是「同层渲染」?它背后的实现原理是怎样的?它是解决原生组件限制的银弹吗?本文将会为你一一解答这些问题。 什么是「同层渲染」? 首先我们先来了解一下小程序原生组件的渲染原理。我们知道,小程序的内容大多是渲染在 WebView 上的,如果把 WebView 看成单独的一层,那么由系统自带的这些原生组件则位于另一个更高的层级。两个层级是完全独立的,因此无法简单地通过使用 [代码]z-index[代码] 控制原生组件和非原生组件之间的相对层级。正如下图所示,非原生组件位于 WebView 层,而原生组件及 [代码]cover-view[代码] 与 [代码]cover-image[代码] 则位于另一个较高的层级: [图片] 那么「同层渲染」顾名思义则是指通过一定的技术手段把原生组件直接渲染到 WebView 层级上,此时「原生组件层」已经不存在,原生组件此时已被直接挂载到 WebView 节点上。你几乎可以像使用非原生组件一样去使用「同层渲染」的原生组件,比如使用 [代码]view[代码]、[代码]image[代码] 覆盖原生组件、使用 [代码]z-index[代码] 指定原生组件的层级、把原生组件放置在 [代码]scroll-view[代码]、[代码]swiper[代码]、[代码]movable-view[代码] 等容器内,通过 [代码]WXSS[代码] 设置原生组件的样式等等。启用「同层渲染」之后的界面层级如下图所示: [图片] 「同层渲染」原理 你一定也想知道「同层渲染」背后究竟采用了什么技术。只有真正理解了「同层渲染」背后的机制,才能更高效地使用好这项能力。实际上,小程序的同层渲染在 iOS 和 Android 平台下的实现不同,因此下面分成两部分来分别介绍两个平台的实现方案。 iOS 端 小程序在 iOS 端使用 WKWebView 进行渲染的,WKWebView 在内部采用的是分层的方式进行渲染,它会将 WebKit 内核生成的 Compositing Layer(合成层)渲染成 iOS 上的一个 WKCompositingView,这是一个客户端原生的 View,不过可惜的是,内核一般会将多个 DOM 节点渲染到一个 Compositing Layer 上,因此合成层与 DOM 节点之间不存在一对一的映射关系。不过我们发现,当把一个 DOM 节点的 CSS 属性设置为 [代码]overflow: scroll[代码] (低版本需同时设置 [代码]-webkit-overflow-scrolling: touch[代码])之后,WKWebView 会为其生成一个 [代码]WKChildScrollView[代码],与 DOM 节点存在映射关系,这是一个原生的 [代码]UIScrollView[代码] 的子类,也就是说 WebView 里的滚动实际上是由真正的原生滚动组件来承载的。WKWebView 这么做是为了可以让 iOS 上的 WebView 滚动有更流畅的体验。虽说 [代码]WKChildScrollView[代码] 也是原生组件,但 WebKit 内核已经处理了它与其他 DOM 节点之间的层级关系,因此你可以直接使用 WXSS 控制层级而不必担心遮挡的问题。 小程序 iOS 端的「同层渲染」也正是基于 [代码]WKChildScrollView[代码] 实现的,原生组件在 attached 之后会直接挂载到预先创建好的 [代码]WKChildScrollView[代码] 容器下,大致的流程如下: 创建一个 DOM 节点并设置其 CSS 属性为 [代码]overflow: scroll[代码] 且 [代码]-webkit-overflow-scrolling: touch[代码]; 通知客户端查找到该 DOM 节点对应的原生 [代码]WKChildScrollView[代码] 组件; 将原生组件挂载到该 [代码]WKChildScrollView[代码] 节点上作为其子 View。 [图片] 通过上述流程,小程序的原生组件就被插入到 [代码]WKChildScrollView[代码] 了,也即是在 [代码]步骤1[代码] 创建的那个 DOM 节点对应的原生 ScrollView 的子节点。此时,修改这个 DOM 节点的样式属性同样也会应用到原生组件上。因此,「同层渲染」的原生组件与普通的内置组件表现并无二致。 Android 端 小程序在 Android 端采用 chromium 作为 WebView 渲染层,与 iOS 不同的是,Android 端的 WebView 是单独进行渲染而不会在客户端生成类似 iOS 那样的 Compositing View (合成层),经渲染后的 WebView 是一个完整的视图,因此需要采用其他的方案来实现「同层渲染」。经过我们的调研发现,chromium 支持 WebPlugin 机制,WebPlugin 是浏览器内核的一个插件机制,主要用来解析和描述embed 标签。Android 端的同层渲染就是基于 [代码]embed[代码] 标签结合 chromium 内核扩展来实现的。 [图片] Android 端「同层渲染」的大致流程如下: WebView 侧创建一个 [代码]embed[代码] DOM 节点并指定组件类型; chromium 内核会创建一个 [代码]WebPlugin[代码] 实例,并生成一个 [代码]RenderLayer[代码]; Android 客户端初始化一个对应的原生组件; Android 客户端将原生组件的画面绘制到步骤2创建的 [代码]RenderLayer[代码] 所绑定的 [代码]SurfaceTexture[代码] 上; 通知 chromium 内核渲染该 [代码]RenderLayer[代码]; chromium 渲染该 [代码]embed[代码] 节点并上屏。 [图片] 这样就实现了把一个原生组件渲染到 WebView 上,这个流程相当于给 WebView 添加了一个外置的插件,如果你有留意 Chrome 浏览器上的 pdf 预览,会发现实际上它也是基于 [代码]<embed />[代码] 标签实现的。 这种方式可以用于 map、video、canvas、camera 等原生组件的渲染,对于 input 和 textarea,采用的方案是直接对 chromium 的组件进行扩展,来支持一些 WebView 本身不具备的能力。 对比 iOS 端的实现,Android 端的「同层渲染」真正将原生组件视图加到了 WebView 的渲染流程中且 embed 节点是真正的 DOM 节点,理论上可以将任意 WXSS 属性作用在该节点上。Android 端相对来说是更加彻底的「同层渲染」,但相应的重构成本也会更高一些。 「同层渲染」 Tips 通过上文我们已经了解了「同层渲染」在 iOS 和 Android 端的实现原理。Android 端的「同层渲染」是基于 chromium 内核开发的扩展,可以看成是 webview 的一项能力,而 iOS 端则需要在使用过程中稍加注意。以下列出了若干注意事项,可以帮助你避免踩坑: Tips 1. 不是所有情况均会启用「同层渲染」 需要注意的是,原生组件的「同层渲染」能力可能会在特定情况下失效,一方面你需要在开发时稍加注意,另一方面同层渲染失败会触发 [代码]bindrendererror[代码] 事件,可在必要时根据该回调做好 UI 的 fallback。根据我们的统计,目前同层失败率很低,也不需要太过于担心。 对 Android 端来说,如果用户的设备没有微信自研的 [代码]chromium[代码] 内核,则会无法切换至「同层渲染」,此时会在组件初始化阶段触发 [代码]bindrendererror[代码]。而 iOS 端的情况会稍复杂一些:如果在基础库创建同层节点时,节点发生了 WXSS 变化从而引起 WebKit 内核重排,此时可能会出现同层失败的现象。解决方法:应尽量避免在原生组件上频繁修改节点的 WXSS 属性,尤其要尽量避免修改节点的 [代码]position[代码] 属性。如需对原生组件进行变换,强烈推荐使用 [代码]transform[代码] 而非修改节点的 [代码]position[代码] 属性。 Tips 2. iOS 「同层渲染」与 WebView 渲染稍有区别 上文我们已经了解了 iOS 端同层渲染的原理,实际上,WebKit 内核并不感知原生组件的存在,因此并非所有的 WXSS 属性都可以在原生组件上生效。一般来说,定位 (position / margin / padding) 、尺寸 (width / height) 、transform (scale / rotate / translate) 以及层级 (z-index) 相关的属性均可生效,在原生组件外部的属性 (如 shadow、border) 一般也会生效。但如需对组件做裁剪则可能会失败,例如:[代码]border-radius[代码] 属性应用在父节点不会产生圆角效果。 Tips 3. 「同层渲染」的事件机制 启用了「同层渲染」之后的原生组件相比于之前的区别是原生组件上的事件也会冒泡,意味着,一个原生组件或原生组件的子节点上的事件也会冒泡到其父节点上并触发父节点的事件监听,通常可以使用 [代码]catch[代码] 来阻止原生组件的事件冒泡。 Tips 4. 只有子节点才会进入全屏 有别于非同层渲染的原生组件,像 [代码]video[代码] 和 [代码]live-player[代码] 这类组件进入全屏时,只有其子节点会被显示。 [图片] 总结 阅读本文之后,相信你已经对小程序原生组件的「同层渲染」有了更深入的理解。同层渲染不仅解决了原生组件的层级问题,同时也让原生组件有了更丰富的展示和交互的能力。下表列出的原生组件都已经支持了「同层渲染」,其他组件( textarea、camera、webgl 及 input)也会在近期逐步上线。现在你就可以试试用「同层渲染」来优化你的小程序了。 支持同层渲染的原生组件 最低版本 video v2.4.0 map v2.7.0 canvas 2d(新接口) v2.9.0 live-player v2.9.1 live-pusher v2.9.1
2019-11-21 - 小程序通过webassembly加载opencv.js
测试在安卓下(Mate30 Pro)可以使用webassembly的相关函数,随即测试了opencv.js,通过对胶水代码的魔改( 1.如对无法使用的new Function,用固定的function加动态获取传入参数仿写 2.对某些函数读取外部文件,将FS改造,提前将文件通过小程序的FileSystemManager读入虚拟目录中 ),经测试,可以使用opencv.js提供的API,面部、眼睛检测速度大约在15帧/秒。 现提供代码给大家增添思路: https://github.com/leo9960/opencv.js-wechat
2020-06-12 - 聊天素材支持小程序打开示例
上代码片段。 链接:https://developers.weixin.qq.com/s/qrcIHnmL7tnL onLoad: function () { var q=wx.getLaunchOptionsSync() var n = wx.getAccountInfoSync(); if(n.miniProgram.envVersion=="release"){ //线上版本中的功能 if(q.scene!=1173){ //校验场景值是否正确。如果不是从微信聊天中的素材打开,则弹窗提示 wx.showModal({ showCancel:false, title:"提示", content:"场景值错误,无法使用本页面的功能" }) return } if(q.scene==1173){ //如果场景值正确,则执行正常的功能(对聊天素材的处理) var qq=q.forwardMaterials[0] console.log(qq.path) //这里的qq.path为聊天素材文件(图片/视频)的本地临时路径 } } if(n.miniProgram.envVersion!="release"){ //填写提审时给审核人员看的功能,建议与线上版本中的功能一致 } }, "supportedMaterials": [ { "materialType": "image/*", "name": "用${nickname}打开", "desc": "可进行压缩图片等操作。", "path": "dktp/dktp" } ]
2021-01-15 - 一次小游戏优化记录
异名最近负责了一个微信小游戏的项目,在版本迭代间隙对游戏的性能调优进行了一次尝试。 性能指标 引擎和小游戏都有一个性能面板,给开发者们暴露了下面几个性能指标: Frame time(ms) 每一帧的时间。《RAIL模型》建议在10毫秒或更短的时间内制作动画中的每一帧。从技术上讲,每帧的最大预算为16毫秒(1000毫秒/每秒60帧≈16毫秒),但是浏览器需要大约6毫秒才能渲染每帧,因此建议每帧10毫秒或者更短。 Framerate(FPS) 帧率,也叫每秒传输帧数(FPS:Frames Per Second),是指画面每秒传输帧数,通俗来讲就是指动画或视频的画面数;每秒钟帧数越多,所显示的动作就会越流畅,举个例子电影的帧频是24,也就是说1s需要播放24张图片,但是实际上在游戏过程中一般人能接受的最低FPS约为30Hz。帧率也非越高越好,因为显卡处理能力=分辨率×刷新率,分辨率不变的情况下,帧频越高,GPU处理的数据量也会激增,引起卡顿。同理,分辨率也不是约高越好。在某些终端的性能面板下也会展示这三个相关的参数:rt-fps:实时帧率;ex-fps:极限帧率;min-fps:最小帧率; Draw call CPU和GPU是并行工作的,它们之间存在一个命令缓冲区。当CPU需要调用图形编程接口的时候就会往命令缓冲区里面增加命令,当GPU完成上一次渲染命令的时候就会继续从命令缓冲区中执行下一条命令,命令缓冲区里面的命令有很多中,而[代码]drawcall[代码]就是其中的一种。CPU在提交[代码]drawcall[代码]的时候需要处理很多东西,比如一些数据、状态、命令等等,有些渲染卡顿问题就是因为GPU渲染速度比[代码]drawcall[代码]的提交速度快,可能上一次渲染完了,CPU还在计算[代码]drawcall[代码],所以[代码]drawcall[代码]的性能瓶颈在于CPU。优化[代码]drawcall[代码]最有效的方法合批渲染,就是把大量小的[代码]drawcall[代码]合并成大的[代码]drawcall[代码],减少[代码]drawcall[代码]的数量。 Tris 和 Verts [代码]Tris[代码]和[代码]Verts[代码]是渲染的三角面数以及顶点数,在webgl中只有三种基本图元,分别是点、线段和三角形,无论多么复杂的模型本质上都是由这三个基本图元绘制而来的,无论形状多么怪异,它们的本质都是由一个个顶点组成,GPU 将这些点用三角图元绘制成一个个的微小平面,再把这些三角行互相连接,就能绘制出各种复杂的物体了; 一般来说模型的顶点和三角形数越低,模型的复杂度就会越低,所以这两个参数在3D模型中比较有参考意义,设计师在输出3D模型的时候一般都会帮忙去合并一下网格。但是在大部分情况下,我们都会认为性能瓶颈在[代码]drawcall[代码]上,比如有两种情形,情形一是有1000个物体,每个物体的顶点数是10,情景二是有10个物体,每个物体的顶点数是1000,哪个情景的性能更好?首先我们要明白GPU的渲染速度是非常快的,渲染10个顶点组成的三角图元和1000个顶点组成的三角图元通常没啥区别,所以这两种情形中产生[代码]drawcall[代码]更少的情形二性能更好。当然如果你在[代码]shader[代码]里面对顶点做了一些特殊的处理,比如复杂的计算啥的,那就得权衡一下这两个指标的大小影响了 实操 降低DrawCall 想要减少drawcall就要从影响渲染状态的因素入手,比如纹理图片、纹理的渲染模式、[代码]Blend[代码]方式等等。但是在大部分项目中其实我们也不会有多大的需求去单独修改引擎的默认渲染参数,如果你动手了,那肯定是会打断合批的 [图片] 因此在绝大部分情况下,在项目中降低[代码]drawcall[代码]收益比最大的其实是就是利用引擎提供的静态合图和动态合图的功能。静态合图就是自动图集了,或者使用第三方的图集工具[代码]TexturePacker[代码],把资源中的散图进行合并,尽量让画面中的节点都使用一张图集,因为同一张图集的纹理状态都是一致的,所以能够达到渲染批次合并对纹理状态的要求;引擎中的动态合图有两种,一种作用对象是图片资源,是引擎默认启用的,如果不希望使用就在资源面板中把[代码]Packable[代码]勾掉,或者把全局的合图开关关掉[代码]cc.dynamicAtlasManager.enabled = false;[代码] ;一种是针对[代码]label[代码]的,可以在[代码]label[代码]的[代码]cache[代码]模式中进行不同的模式切换。下面就介绍一下异名在这个项目中针对[代码]drawcall[代码]做的一些处理和收益: 合理管理节点 画面外的节点可以直接移除,[代码]drawcall[代码]从50降到了23: [图片] 合理设置label的cache模式 把首页上三个[代码]label[代码]的[代码]cache[代码]模式改为[代码]bitmap[代码],首页上的[代码]drawcall[代码]从79降低到50 [图片] 有一些频繁更改的[代码]label[代码],当[代码]cache[代码]模式改为[代码]char[代码]的时候,在我的苹果手机上差别不大,但是在小伙伴的安卓手机上流畅度上升十分明显 合并图集 在合并图集的时候需要根据画面的内容去做划分,尽量把同一个画面用的的图片资源打包成一个图集。以游戏中的一个中后关卡为例(前面关卡的画面节点太少,差异不明显),[代码]drawcall[代码]均值从190降到了90,[代码]drawcall[代码]峰值从220降到了127。 [图片] 通过Performance排查问题 在开发中的时候,异名会使用[代码]Chrome[代码]的[代码]DevTools[代码],如果是在浏览器中排查性能问题需要屏蔽所有的浏览器插件,最好就是打开隐私模式来调试,因为插件在后台运行会造成干扰。但是在上线前异名会选择使用微信开发者工具的[代码]DevTools[代码]来再查看一下性能,因为在浏览器的中跑的项目是调试模式,一来没有做合图,而来它也没有经过小游戏的编译,所以为了减少和最终项目效果的偏差,最终会以微信开发者工具中的指标为最终的参考指标。 和手机终端环境相比,我们的电脑的CPU是很快的,为了尽可能模拟用户的终端硬件情况,我们首先需要对CPU做一下节流,例如我现在选中的[代码]6x slowdown[代码]就会使我们本地CPU的运算速率比正常情况下降6倍。 [图片] 这时候我们重新去生成录制结果,就可以发现面板上已经出现了醒目的红色告警信息了: [图片] Recurring handler 聚焦放大然后把轴线定位到每个小的告警信息处,可以在[代码]Summary[代码]中看到浏览器给出的警告信息,我发现这里面的告警信息都是一样的,都是[代码]Recurring handler[代码],而且有规律地出现,可以通过[代码]Initiator[代码]去查看重复出现的地方以及具体的执行代码: [图片] 虽然我们已经看到了代码执行的具体位置是[代码]requestAnimateFrame[代码],但是这个api调用不是我们的业务逻辑,而是引擎的封装调用,引擎的帧回调应该是用[代码]requestAnimateFrame[代码]实现的,也就是说在[代码]update[代码]的钩子里面可能存在重复调用的逻辑,在这里就需要进一步地去分析了。我们需要根据[代码]Main[代码]面板中火焰图也就是JS调用栈一步步去寻找并落实到具体的业务代码调用细节,如果能够定位到我们逻辑中的调用函数,马上对症下药就能解决了。但是在异名的这个项目中[代码]Recurring handler[代码]给到的信息很难定位,[代码]task[代码]下面调用栈调用的都是引擎自身的渲染方法,当然横向去看调用的先后顺序的话,它在[代码]touchMove[代码]事件后面运行,这是一个排查方向,需要去分析自己的代码运行调用。如果像异名遇到的这种情况,控制台只能定位到一堆引擎的渲染函数,而不能很明确地定位到我们的具体业务逻辑中,异名会建议放一放,因为重复渲染的问题可能会在[代码]long task[代码]拆分的过程中被fix掉。 Long task 可以通俗理解为一段执行时间很长的js逻辑就是长任务,"长任务"占用着主线程,即使我们的页面看上去准备好了,但是也不能响应用户的操作和点击等交互。至于这个执行时间多长才算是“长”呢?《RAIL模型》建议我们每个任务最好都控制在50毫秒内,Chrome在控制台中也给出了醒目的长任务提示: [图片] 大型的脚本是长任务的主要原因,异名这里先举一个在项目中拆分长任务的简单的例子。异名的游戏中有个复活的逻辑,会在碰撞的回调中会处理一下血量和相关的掉血交互,然后当玩家血量耗尽就会唤起一个复活弹窗,这段逻辑就产生了一个[代码]long task[代码],耗时55.74ms,火焰图如下: [图片] 仔细分析一下,我的碰撞的逻辑处理和唤起复活弹窗虽然是顺序调用,但是它们之间算是一个比较清晰的逻辑界线,而且[代码]askResurgence[代码]这个函数是一个唤起UI弹窗的函数,既然产生了性能限制,我们就在这里做一个任务拆分: [代码]// 用setTimeout包裹一下,把它放在下个宏任务执行 setTimeout(() => { this.askResurgence(); }) [代码] [图片] 这时候我们再看拆分后的火焰图,[代码]long task[代码]标记已经消失了,本来一个长任务,被拆分成了3个任务(中间一个是GC),而且三个任务的耗时相加和开始的长任务相比是折半了的。异名通过排查梳理之后,发现一些很明确的UI状态过渡都很容易造成长任务,异名的项目中还有好几处都是这种界线分明混合逻辑,当你遇到性能压力的时候或许可以像我一样做一下处理。 总结一下拆解大型脚本的时候首先需要把大段的js逻辑重新梳理一遍,可以把一些能提前或者延后的状态拆解到我们应用的空闲阶段去初始化或者变更,比如在首页就先把游戏过程中需要的数据加载进来,游戏过程的的逻辑中就不用再去加载这部分数据了,但是这属于比较“宏观的”逻辑变更,但是大部分情况下我们的状态和逻辑变更都是很难提前或者延后的。我们还可以做一些时间颗粒度更小的逻辑拆分,那就是结合js的事件循环机制来处理我们的逻辑,像用[代码]Promise.then[代码]或者[代码]setTimeout[代码]去做一下任务延迟,甚至可以建立一个任务队列去做事件缓存等,这篇《idle-until-urgent》有介绍到一些比较具体的拆解脚本措施。任务拆分是有风险的,无论是在应用的层面去提升或者延后逻辑,还是利用js的微任务或者宏任务去延后状态逻辑,都会有可能导致你的应用状态同步出现问题,所以在实操之后记得好好测试一下整个流程。 长任务还可以通过精简自身的逻辑来优化,像在一些循环中,如果可以做跳出判断自己是否有做;还有在一些地方你写的逻辑是否执行冗余或者无用,比如在异名的项目中这段交互逻辑👇 [代码]// 效果甚微的动画交互,干掉 this.node.setScale(1.2); const frequency = getRandom(15, 40) / 100; this.centerNode.runAction( cc.sequence( cc.scaleBy(frequency, 1.1), cc.scaleTo(frequency, 1) ).repeatForever() ); [代码] 它只是一个呼吸变化,在实际的画面效果中,这种小变化其实是很微弱的,可以认为它是一个可有可无的动画逻辑,那我们在做性能优化的时候就需要果断地把它给删除了,把这些冗余的逻辑干掉之后,[代码]game logic[代码]的数值就已经可以很明确地降低下来了。 其他 目前还存在较明显的性能方面:发热、敌人数量增多时容易卡顿 发热是个比较综合的问题,一般来说CPU导致发热,降低CPU的工作会有效减少发热。卡顿则受到帧频和drawcall的影响比较大,通常有以下这些优化手段 降低帧数:目前已动态设置帧频,游戏过程60帧,非游戏过程30帧 减少帧回调:目前update中还有很大的逻辑优化空间 减少内存使用:这块目前也有很大的优化空间,GC回收,节点池,对象和节点复用、缓存等等,甚至包括一些贴图的引用释放等 drawcall优化:其实还可以借助一些帧调试工具去进一步分析,项目的后期应该还会对[代码]drawcall[代码]优化进行再深入一点的探索,到时候如果有别的收获就再和大家分享 [图片]
2020-07-03 - 如何在小程序中快速实现环形进度条
在小程序开发过程中经常涉及到一些图表类需求,其中环形进度条比较属于比较常见的需求 [图片] [中间的文字部分需要自己实现,因为每个项目不同,本工具只实现进度条] 上图中,一方面我们我们需要实现动态计算弧度的进度条,还需要在进度条上加上渐变效果,如果每次都需要自己手写,那需要很多重复劳动,所以决定为为小程序生态圈贡献一份小小的力量,下面来介绍一下整个工具的实现思路,喜欢的给个star咯 https://github.com/lucaszhu2zgf/mp-progress 环形进度条由灰色底圈+渐变不确定圆弧+双色纽扣组成,首先先把页面结构写好: .canvas{ position: absolute; top: 0; left: 0; width: 400rpx; height: 400rpx; } 因为进度条需要盖在文字上面,所以采用了绝对定位。接下来先把灰色底圈给画上: const context = wx.createContext(); // 打底灰色曲线 context.beginPath(); context.arc(this.convert_length(200), this.convert_length(200), r, 0, 2*Math.PI); context.setLineWidth(12); context.setStrokeStyle('#f0f0f0'); context.stroke(); wx.drawCanvas({ canvasId: 'progress', actions: context.getActions() }); 效果如下: [图片] 接下来就要画绿色的进度条,渐变暂时先不考虑 // 圆弧角度 const deg = ((remain/total).toFixed(2))*2*Math.PI; // 画渐变曲线 context.beginPath(); // 由于外层大小是400,所以圆弧圆心坐标是200,200 context.arc(this.convert_length(200), this.convert_length(200), r, 0, deg); context.setLineWidth(12); context.setStrokeStyle('#56B37F'); context.stroke(); // 辅助函数,用于转换小程序中的rpx convert_length(length) { return Math.round(wx.getSystemInfoSync().windowWidth * length / 750); } [图片] 似乎完成了一大部分,先自测看看不是满圆的情况是啥样子,比如现在剩余车位是120个 [图片] 因为圆弧函数arc默认的起点在3点钟方向,而设计想要的圆弧的起点从12点钟方向开始,现在这样是没法达到预期效果。是不是可以使用css让canvas自己旋转-90deg就好了呢?于是我在上面的canvas样式中新增以下规则: .canvas{ transform: rotate(-90deg); } 但是在真机上并不起作用,于是我把新增的样式放到包裹canvas的外层元素上,发现外层元素已经旋转,可是圆弧还是从3点钟方向开始的,唯一能解释这个现象的是官方说:小程序中的canvas使用的是原生组件,所以这样设置css并不能达到我们想要的效果 [图片] 所以必须要在canvas画图的时候把坐标原点移动到弧形圆心,并且在画布内旋转-90deg [图片] // 更换原点 context.translate(this.convert_length(200), this.convert_length(200)); // arc原点默认为3点钟方向,需要调整到12点 context.rotate(-90 * Math.PI / 180); // 需要注意的是,原点变换之后圆弧arc原点也变成了0,0 真机预览效果达成预期 [图片] 接下来添加环形渐变效果,但是canvas原本提供的渐变类型只有两种: 1、LinearGradient线性渐变 [图片] 2、CircularGradient圆形渐变 [图片] 两种渐变中离设计效果最近的是线性渐变,至于为什么能够形成似乎是随圆形弧度增加而颜色变深的效果也只是控制坐标开始和结束的坐标位置罢了 const grd = context.createLinearGradient(0, 0, 100, 90); grd.addColorStop(0, '#56B37F'); grd.addColorStop(1, '#c0e674'); // 画渐变曲线 context.beginPath(); context.arc(0, 0, r, 0, deg); context.setLineWidth(12); context.setStrokeStyle(grd); context.stroke(); 来看一下真机预览效果: [图片] 非常棒,最后就剩下跟随进度条的纽扣效果了 [图片] 根据三角函数,已知三角形夹角根据公式radian = 2*Math.PI/360*deg,再利用cos和sin函数可以x、y,从而计算出纽扣在各部分半圆的坐标 const mathDeg = ((remain/total).toFixed(2))*360; // 计算弧度 let radian = ''; // 圆圈半径 const r = +this.convert_length(170); // 三角函数cos=y/r,sin=x/r,分别得到小点的x、y坐标 let x = 0; let y = 0; if (mathDeg <= 90) { // 求弧度 radian = 2*Math.PI/360*mathDeg; x = Math.round(Math.cos(radian)*r); y = Math.round(Math.sin(radian)*r); } else if (mathDeg > 90 && mathDeg <= 180) { // 求弧度 radian = 2*Math.PI/360*(180 - mathDeg); x = -Math.round(Math.cos(radian)*r); y = Math.round(Math.sin(radian)*r); } else if (mathDeg > 180 && mathDeg <= 270) { // 求弧度 radian = 2*Math.PI/360*(mathDeg - 180); x = -Math.round(Math.cos(radian)*r); y = -Math.round(Math.sin(radian)*r); } else{ // 求弧度 radian = 2*Math.PI/360*(360 - mathDeg); x = Math.round(Math.cos(radian)*r); y = -Math.round(Math.sin(radian)*r); } [图片] 有了纽扣的圆形坐标,最后一步就是按照设计绘制样式了 // 画纽扣 context.beginPath(); context.arc(x, y, this.convert_length(24), 0, 2 * Math.PI); context.setFillStyle('#ffffff'); context.setShadow(0, 0, this.convert_length(10), 'rgba(86,179,127,0.5)'); context.fill(); // 画绿点 context.beginPath(); context.arc(x, y, this.convert_length(12), 0, 2 * Math.PI); context.setFillStyle('#56B37F'); context.fill(); 来看一下最终效果 [图片] 最后我重新review了整个代码逻辑,并且已经将代码开源到https://github.com/lucaszhu2zgf/mp-progress,欢迎大家使用
2020-05-27