跨平台应用开发进阶 (五十六):应用渲染异常问题分析及解决
一、前言
继前期 iOS 由于移动设备内存不足导致页面白屏问题之后,(详参博文《跨平台应用开发进阶(五十)uni-app ios web-view嵌套H5项目白屏问题分析及解决》)又发现 APP 在iOS
系统运行过程中,会高频出现页面黑屏、黑色区块,白屏问题。
二、问题分析
出现以上问题是由于页面渲染问题导致的,引发的可能原因是页面栈溢出、应用内存泄漏。
经过getCurrentPages()
输出页面栈信息,发现切换底部导航时,页面栈信息已经清空,故不存在页面栈溢出问题。
继续尝试 APP 缓存清理机制,制定清理策略如下:
点击当前 tabitem,滚动或刷新当前页面调用原生方法plus.cache.clear
清理应用缓存。如果是点击不同的 tabitem,一定会触发页面切换。
经过清理缓存策略的实施,问题得到解决。
由此,衍生出调用uni.setStorage(OBJECT)
(宏任务)、uni.setStorageSync(KEY,DATA)
(微任务)等 API 及uni.downloadFile
、uni.saveFile
是存储至设备内存中吗?
三、延伸阅读
3.1 页面跳转
uni.navigateTo(OBJECT)
会保留页面栈,能通过调用navigateBack
返回。但是有页面栈溢出的问题,不能当作通用跳转方法。uni.redirectTo(OBJECT)
不会保留页面栈,不能返回上一页,返回默认pages.json
第一个页面。uni.reLaunch(OBJECT)
销毁页面栈并跳转到指定页面。通常用于退出登录返回首页。uni.switchTab(OBJECT)
切换选项卡用。uni.navigateBack(OBJECT)
配合navigateTo
,返回页面使用,能有效避免页面栈溢出。可通过getCurrentPages()
获取当前的页面栈,决定需要返回几层。
通过项目代码分析发现,路由跳转并不存在页面栈溢出异常情况。底部导航栏切换时,页面栈会做清空处理。同一导航栏下的页面跳转才会在页面栈中。
2 排查思路
根据具体业务场景,逻辑上控制页面跳转和返回,防止页面栈溢出。通过
getCurrentPages()
判断页面栈有多高;查看webview
对象多少,来判断当前跳转方式是否合适。排查代码逻辑,是否有长时间后台请求不返回的问题,可以在
uni.request
添加timeout
来规避这种情况。排查内存溢出,是否有创建定时器(
setInterval
),但是没清空的情况(clearInterval
)。uniapp
本身问题,因为其跨平台性需要js
调用native
,性能肯定会有所损失,如果无法接受还是使用原生语言开发。删除缓存时,如果是清空数据需要使用
uni.clearStorageSync
。
那就需要查看应用运行过程中占用的内存了,是否存在内存泄漏问题。
3.2 $nextTick 原理深度解析
Vue
实现响应式并不是数据发⽣变化之后 DOM
⽴即变化,⽽是按⼀定的策略进⾏ DOM
更新。$nextTick
是在下次 DOM
更新循环结束之后执⾏延迟回调,在修改数据之后使⽤ $nextTick
,则可以在回调中获取更新后的 DOM
,在下次 DOM
更新循环结束之后执行延迟回调。
简单的理解是:当数据更新了,在dom
中渲染后,⾃动执⾏该函数。Vue
在更新data
之后并不会立即更新DOM
上的数据,就是说如果我们修改了data
中的数据,再马上获取DOM
上的值,我们取得的是旧值,我们把获取DOM
上值的操作放进$nextTick
里,就可以得到更新后得数据。
正确的⽤法是:vue 改变 data 中的数据后,使⽤vue.$nextTick()
⽅法包裹 js 对象执行后续代码。
3.2.1 什么时候使用 $nextTick()
Vue
⽣命周期的created()
钩⼦函数进⾏的 DOM 操作⼀定要放在Vue.nextTick()
的回调函数中,原因是在created()
钩⼦函数执⾏的时候,DOM 其实并未进⾏任何渲染,⽽此时进⾏DOM 操作⽆异于徒劳,所以此处⼀定要将 DOM 操作的 js 代码放进Vue.nextTick()
的回调函数中。
当项⽬中改变data
函数的数据,想基于新的 dom 做点什么,对新DOM
⼀系列的 js 操作都需要放进Vue.nextTick()
的回调函数中。
3.2.2 $nextTick() 执行原理
Vue
在更新 DOM
时是异步执行的。只要侦听到数据变化,Vue
将开启一个任务队列,并缓冲在同一时间循环中发生的所有数据变更。如果同一个 watcher
被多次触发,只会被推入到队列中一次。(这种在缓冲时去除重复数据对于避免不必要的计算和 DOM
操作是非常重要的)
然后,在下一个的事件循环“tick
”中,Vue
刷新队列并执行任务队列 (已去重的) 工作。
Vue
在内部对异步队列尝试使用原生的 Promise.then
(微任务)、MutationObserver
和 setImmediate
,如果执行环境不支持,则会采用 setTimeout(fn, 0)
(宏任务)代替。
3.3 JS 运行机制
JS
执行是单线程的,它是基于事件循环的。事件循环大致分为以下几个步骤:
所有同步任务都在主线程上执行,形成一个执行栈;
主线程之外,还存在一个"任务队列"(
task queue
)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。主线程不断重复上面的第三步。
主线程的执行过程就是一个 tick,而所有的异步结果都是通过 “任务队列” 来调度。"任务队列"中存放的是一个个的任务(task)。规范中规定 task 分为两大类,宏任务和微任务。
3.3.1 微任务
也称job
,通常用于在当前正在执行的脚本之后直接发生的事情,比如对一系列的行为做出反应,或者做出一些异步的任务,而不需要新建一个全新的 task。只要执行栈没有其他javascript
在执行,在每个 task 结束时,微任务队列就会在回调后处理。在微任务期间排队的其他微任务将被添加到这个队列的末尾。
常见的微任务 有 MutationObsever
、 Promise.then
、$nextTiock
。
3.3.2 宏任务
宏任务的作用是为了让浏览器能够从内部获取javascript / dom
的内容并确保执行栈能够顺序进行。调度是随处可见的,例如解析 HTML,获得鼠标点击的事件回调等等。
常见的宏任务有 setTimeout
、MessageChannel
、postMessage
、setImmediate
;
vue
进行 DOM 更新,内部也是调用nextTick
来做异步队列控制。而当我们自己调用nextTick
的时候,它就在更新 DOM 的那个 micro task 后追加了我们自己的回调函数,从而确保我们的代码在 DOM 更新后执行。
setTimeout
是宏任务:只是延迟执行,在延迟执行的方法里,DOM
有可能会更新也有可能没有更新。
三、解决措施
注⚠️:
uni-app
App 端内置HTML5+
引擎,让 js 可以直接调用丰富的原生能力。条件编译调用
HTML5+
:小程序及 H5 等平台是没有HTML5+
扩展规范的,因此在uni-app
调用HTML5+
的扩展规范时,需要注意使用条件编译。否则运行到 h5、小程序等平台会出现plus is not defined
错误。
uni-app
逻辑层是运行在一个独立的jscore
里的,它不依赖于本机的webview
,所以一方面它没有浏览器兼容问题,可以在Android4.4
上跑es6
代码,另一方面,它无法运行window、document、navigator、localstorage
等浏览器专用的 js API。jscore
就是一个标准 js 引擎,标准 js 是可以正常运行的,比如 if、for、各种字符串、日期处理等。js 和浏览器的区别要注意区分开来。uni-app
的 App 端没有 App 那种webkit remote debug
,因为uni-app
的 js 不是运行在webview
里,而是独立的jscore
里。由于浏览器 GUI 渲染线程与 JS 引擎线程是互斥的关系,JS 引擎执行时,会将 GUI 线程挂起,直到 JS 引擎执行结束。当页面中有很多长任务时,就会造成页面 UI 阻塞,出现界面卡顿、掉帧等情况。
GUI 渲染线程:负责页面的绘制和渲染,我们所熟知的html、css
资源的解析、dom 树的生成、页面绘制都是该线程负责的。JS 引擎线程:负责 js 的解析以及所有异步同步任务的执行,维护一个执行栈,先逐个处理同步代码,当遇到异步任务时,就会借助事件触发线程。
浏览器的进程、线程信息如下:
其中,前端主要关注的是浏览器内核的渲染进程,其主要负责html、css、js
资源解析渲染,还负责事件循环、异步请求等。
数据存储:uni-app 中的缓存、H5 缓存是存储在内存吗?
原生
plus Storage
模块管理应用本地数据存储区,用于应用数据的保存和读取。应用本地数据与localStorage
、sessionStorage
的区别在于数据有效域不同,前者可在应用内跨域操作,数据存储期是持久化的,并且没有容量限制。通过plus.storage
可获取应用本地数据管理对象。uni-app
应用uni.setStorage(OBJECT)
(宏任务)、uni.setStorageSync(KEY,DATA)
(微任务)进行数据存储时,其本质是使用原生plus.storage
,无大小限制,不是缓存,是将数据存储至sqlite
数据库文件进行持久化。H5 端应用
localStorage
、sessionStorage
进行数据存储,浏览器限制 5M 大小,是缓存概念,可能会被清理。应用
Vuex
实现状态管理。原生
plus.cache
管理应用缓存数据仅包括程序中使用webview
产生的数据,不包括业务逻辑中使用扩展api
(例如uni.setStorage(OBJECT)
(宏任务)、uni.setStorageSync(KEY,DATA)
(微任务)等 API)保存的数据。各个小程序端为其自带的
storage api
,数据存储生命周期跟小程序本身一致,即除用户主动删除或超过一定时间被自动清理,否则数据都一直可用。uni-app
框架组件本身无法解决的问题,可抛弃框架组件,使用原生进行优化。
最终是选择在切换底部页签时做缓存清理动作,注意此处的缓存清理使用plus.cache.clear
实现清除应用的缓存数据。清理应用缓存数据仅包括程序中使用webview
产生的数据,不包括业务逻辑中使用扩展api
保存的数据。代码如下:
四、拓展阅读
版权声明: 本文为 InfoQ 作者【No Silver Bullet】的原创文章。
原文链接:【http://xie.infoq.cn/article/5d198ebd6a5c1c954075e330f】。文章转载请联系作者。
评论