Hippy 常用调试方法和常见问题案例

近日,腾讯开源跨端框架 Hippy,一周即吸引3000+star。在腾讯内部,Hippy 已运行3年之久,跨 BG 共有 18 款线上业务正在使用 Hippy,日均 PV 过亿,且已建立一套完整生态。相较于其他跨端框架,Hippy 对前端开发者更友好:紧贴 W3C 标准,遵从网页开发各项规则,使用 JavaScript 为开发语言,同时支持 React 和 Vue 两种前端主流框架。本文为大家介绍了Hippy 常用调试方法和常见问题案例,希望能够帮助开发者快速上手。

调试服务

前端调试在官网已经有专门章节进行描述,就不多说,这里具体说一下调试常见问题、案例和一些基本原理。

Hippy 已经在 hippy-debug-server 中集成了一套基于 Chrome DevTools Protocol 的调试服务器,启动后在终端进入本地调试界面,便可以进入远程调试模式。

目前 iOS 和 Android 都已经支持了真机调试,Android 通过 adb reverse 命令直接实现了本地调试端口的转发,就是指在手机上访问 localhost:38989 的调试端口时,访问的实际是开发机上的 38989 端口,但是 iOS 需要终端和前端的双方面配合修改端口才可以做到真机调试,所以建议先通过 iOS 模拟器进行调试工作。

启动调试服务、进入终端的本地调试环境后,JavaScript 代码将会通过调试服务加载到真机中运行,如果代码没问题应该能正常运行,但有时候会碰到启动就 Crash 的情况,可以参考常见案例最后一条“iOS 版本低于 9 时模拟器报告 SyntaxError”。

同样的,iOS 上有的特性有的能用 Polyfill 解决,但有的不行(例如 Proxy、正则表达式的 Sticky Flag 等就需要 iOS 10 以上才可以使用,而且无法 Polyfill),所以如果要兼容低版本 iOS,要注意不能使用到太新的 JS 特性。

秘技:整合到终端内的前端 jsbundle 包调试

该方案暂时只适用于 iOS

有的 App 调试模式下运行很正常,但是打完包集成进去以后就挂了,这时候我们需要用到整合后的 jsbundle 包调试大法了。

其实非常简单,Hippy 在 iOS 中时通过自带的 JavaScriptCore 运行的,所以可以通过自带的 Safar 进行调试,在 Safari 的设置 -> 高级打开开发者菜单后 ,启动 Hippy 就能看到多出了一个模拟器设备。

Safari 调试菜单位置

然后就可以用 Safari 开始调试了,唯一要注意的时,断点需要在启动后才生效,启动时是断不下来的,启动问题可以在关键点加上日志,日志能够正常输出。如果是其它启动后问题,可以直接打断点,跟 Chrome 调试服务的使用方法基本一致。

整合后包打断点

内存占用情况

前端开发普遍对内存占用缺乏概念,直到终端同学过来说 JS 内存占用太多把 App 搞崩溃了才回过神来。

JavaScript 目前主要以标记清除算法的方案来进行内存回收,它的核心是定期从全局对象中遍历所有对象,并且对不可到达的对象进行标记,并进而清除。

在绝大多数情况下作为前端开发确实不需要关心内存占用,但是 Hippy 中不太一样,Hippy 是前端的开发方式去开发终端 App,有几个类在组件卸载时一定要记得销毁,包含了 React 中负责事件监听的 EventEmitter 实例、Animation/AnimationSet 动画组件,Vue 中的 $app.on() 终端事件监听等等,不释放掉它们,它们就会一直占用着内存,随着界面越来越多,App 最终将会崩溃。

其实调试方法也非常简单,直接在调试器的 Memory 观察内存占用情况,打快照看一下当时各类对象对内存的占用情况,它是 Hippy 在浏览器里运行的容器,可以代表 App 的整体内存占用情况。

内存调试方案

这部分内容 Google 官方也有文档

常见案例

1. 数据已经更新,但是界面内容或者样式不变

这是经常碰到的,最直接的方式是对 React 和 Vue 进行界面绘画的模块 - UIManagerModule 的三个方法 - createNodeupdateNodedeleteNode 打断点,其实不管 MVVM 怎么做,最终都会通过这三个方法把界面通知终端画上去,这其实也带来了无限的扩展性,任何框架只要对接了这三个方法就可以进行 Hippy 绘制,如果掌握了 UIManagerModule 的语法,甚至不需要 React 或者 Vue 也可以直接通过它画界面。

但从一定程度上来讲,Hippy 画界面的方式其实跟浏览器是不一样的,它是异步的,MVVM 里组件创建完毕,componentDidMount 或者 mounted 后,其实并不意味着界面真的画上去了(但是这个耗时极少,mounted 后基本可以认为真的画上去了),如果要对界面进行操作,需要确定终端确实画上去了才行,这可以通过 onLayout 事件获得;其次可以看到画界面和普通的 Native Module 调用没有本质区别,最终都要通过 JSBridge 进行通讯。-- 这部分正在通过 C++ 方式重写。

通过观察它,我们可以了解到最终通过 React、Vue 解析后的组件是什么样的,可以观察到为什么界面没有更新,或者样式不如预期。

Hippy 的前端框架在开发初期就考虑到了调试的便利性,调试模式下会将前端框架与终端之间的通讯都打印到 Console 里,当觉得自己的业务 App 或者框架显示存在问题时,直接观察它就能很方便获得所有信息。

以 Hippy-Vue 为例:

Hippy-Vue 的终端通讯日志

Hippy-Vue 要关闭该功能只要将入口文件中的 Vue.config.silent 改为 true 即可;Hippy-React 要关闭该功能需要在启动参数里增加一个 silent: true。不过一般不建议关闭,它在打包后会自动停止输出。

2. ScrollView(Vue 的 div + overflow-x/y: scroll)或者 ListView(Vue 的 ul/li)无法滚动

在 Hippy 中只有这两种 View 是可以滚动的,剩下的都不可以滚动,但是要让它们能滚起来也不是那么简单,需要有样式进行配合,简单说就是:

  • ScrollView 以上所有父节点都必须有一个固定的高度,ScrollView 中只能嵌套一个内容子节点,它可以随意变高。
  • ListView 以上所有父节点都必须有一个固定的高度,里面所有的 renderRow 出来的 ListItemView(Vue 中的 li)可以随意变高。

这里的固定高度可以是直接指定高度,也可以是通过 flex 进行界面动态分割的高度,但是一定要是固定的,因为滚动实际是终端去实现的,它需要能够区分可以滚动和不可以滚动的区域,如果容器高度和内容高度一样,那就变成不可以滚动了。

另外 Vue 里的 ul 默认已经加上了 flex: 1 样式会把整个 View 撑满屏幕,一般情况下不用做特别处理,但是 div + overflow-x/y: scroll 依然需要手工指定高度。

当滚动出现异常的时候,可以通过 XCode 调试一下终端代码,它有个 Debug View Hierarchy 功能,可以非常直观地看到界面层级和尺寸,对调试样式问题有很大帮助。

XCode 的界面层级调试

3. ListView(Vue 里的 ul/li)性能很差、卡顿、闪烁

这里需要提到前端三点非常需要注意的地方:

  1. 如果界面发生异常闪烁,首先需要通过第一个小章节里的 UIManagerModule 观察法,看一下那那三个方法是否有异常的执行,例如 updateNode 执行过于频繁,或者 deleteNode/createNode 异常执行,这通常发生在数据有变化导致界面重绘,可以通过调用栈看一下是哪里的数据更新导致界面重绘,并针对性地进行前端优化。
  2. ListView 决定界面是否重绘,有个很关键的参数是 key(React 官文Vue 官文),Hippy-React 也通过 getRowKey() 的方法实现了 key 在 ListView 中的应用。key 其实是数据的唯一标示符,数据不发生改变,key 就不应该发生改变,而 key 一旦发生改变 ListView 就会重绘。目前很多业务在开发时 key 不指定,或者把 index 作为 key,前者会导致 ListView 每次有数据更新都做一次完整的 Array diff,开销非常大,后者会导致删除中间一个节点时将后面所有的节点全部删除再重新插入一次,开销也非常大。处于性能考虑,key 是必须要加的,一般跟数据的主键保持一致即可。
    **但是:**如果 ListView 中的数据需要进行排序,那就不要指定 key 了,目前 Hippy 的 moveNode 功能,已经计划但仍未完成,指定 key 后在重新排序时会因为对应索引的 key 值不同,先删除全部节点内容,再全部重建,可能会造成轻度闪烁。如果此时不指定 key,就只有一个更新节点的请求,两次请求合并为一次,终端层会对数据进行对比并更新节点内容。
  3. 如果到这一步终端渲染依然很慢、帧率低,我们就要提到另外一个参数 type 了,对应到 Hippy-React 里是 getRowType() 方法,它是用来表示组件样式的,样式不变,type 就不变。这里需要先说一下 Hippy ListView 的复用机制,当不指定 type 时,每次有新的 ListItemView 被渲染(HippyReact 里 renderRow() 将返回 ListItemView,Hippy-Vue 里的 li),终端都会重新构建所有终端组件节点,加了 type 之后,会将将之前渲染过的终端组件节点放到缓存池中,下次碰到相同 type 类型的 ListItemView,就不会重新渲染,而是从缓存池中把缓存的节点拿出来做次拷贝并更新数据,再上屏,即使只有一个样式的 ListItemView,通过 type 也能做到性能优化。

经过上面三步,能解决 90% 的 ListView 性能问题。

Hippy-Vue 官方范例中也对这三个参数加了注释。

4. iOS 上 ListView 不渲染,但 Android 没问题

首先需要检查 numberOfRows 参数是否真的是 ListView 中 ListItemView 的数量,这个除了在业务代码中打断点查看数据数量是否和 numberOfRows 一致以外,也可以通过第一个 UIManagerModule 的调试方法查出来。

这个问题牵扯到 iOS 上一个 ListView 的上屏性能优化,iOS 上并不是发一个 ListItemView 就上屏一个的,而是需要先改变 ListView 的 numberOfRows 再去创建节点,当节点数量与 numberOfRows 一致时再上屏。

目前碰到的所有不渲染的问题都是因为这个原因造成的。

另外在 Hippy-Vue 中,对于静态的 li(就是终端的 ListItemView),可以不需要手工指定 numberOfRows,Hippy-Vue 会在 DOM 层计算子节点数量。但是对于动态获取的数据,也必须要加上该参数,因为 Hippy-Vue 位于 Vue 的渲染层,跟业务还隔了一个 Vue,无法知道业务到底有多少数据准备要渲染。

5. iPhone 中红屏报告 ModuleNotRegist

这里需要提到 Hippy App 的启动方式:当终端 JS 引擎加载完 JavaScript 后,会从 GLOBAL.appRegister 对象里去寻找终端指定的 moduleName,而 __GLOBAL__.appRegister 是在 Hippy 启动时通过 HippyRegister.regist() 方法注册上的,在 Hippy-React 入口文件 或者 Hippy-Vue 入口文件 定义的 appName 最终都会执行到 regist() 方法上进行 __GLOBAL__.appRegister 的注册,首先,我们要检查终端的 moduleName 是否和 appName 一致。

如果一致依然出错的话,很大几率是之前 JS 执行失败,也不排除 SDK 更新后存在 bug,也有可能其它问题,导致 __GLOBAL__.appRegister 未注册成功,但我们有个办法可以在该错误抛出时二次确认一下终端所寻找到 moduleName 是否和前端定义的 appName 一致,可以在那一行打上日志,然后使用上文的 Release 包调试方案检查终端过来查的到底是什么 appName。

6. iOS 版本低于 9 时模拟器报告 SyntaxError

这是因为 Hippy 自带的 Webpack 默认调试模式配置文件,最低仅开启了 iOS 9 的输出,因为输出到 iOS 8 会多出很多 polyfill,语法上也会转换,导致体积大很多。

Hippy 本身最低支持的 iOS 8,我们建议在高版本的 iOS 上进行调试,然后打包后在低版本 iOS 走一遍测试流程,没什么问题即可。

如果非要在低版本的 iOS 上进行调试,修改一下 webpack 配置文件 iOS 将 preset-env 中的 ios 版本改成更低即可,但目前经过测试 core-js 对 iOS 8 那样对低版本可能存在问题,这就需要你自己手工调整了。

版权所有丨转载请注明出处:https://kxq.io/archives/hippy常用调试方法和常见问题案例