为了让读者对问题理解更清晰,先介绍下什么是小程序,以及什么是小程序容器:
小程序本质上就是 H5 应用,加上 4 个增强能力:
- 拦截 WebView 的网络请求,重定向到本地已下载好的 JS、CSS 等资源的离线包能力
- 通过内部的 serviceWorker,实现渲染和业务逻辑并发的能力
- 通过 js bridge,那客户端能力复用给 H5 的 JSAPI 能力
- 通过复用客户端的 UI 实现,实现仿原生的 UI 能力
小程序容器就是为支持以上 4 项增强,而部署在客户端的 Native 代码(iOS、Android)。
为了实现以上 4 项增强能力,需要 Native 层与 WebView 层紧密配合。小程序容器架构极简版如下:
本文描述的 Bug,就发生在 JS 核心框架层_render。
这天,收到线上问题反馈,小程序调用my.redirectTo()
之后,再调用my.navigateTo()
无响应,无法跳转下一个页面。
my.redirectTo()
和my.navigateTo()
都是小程序的 JSAPI,用来调用小程序容器提供的页面路由能力。支付宝小程序开发文档:Link[1]
用支付宝官方小程序 Demo 演示问题。开始时,可以通过点击基础视图
按钮进入下一个页面;视频最后,再次点击基础视图
按钮时,业务无法跳转。
收到问题反馈,第一件要做的事就是要求 QA 补充更多信息,缩小排查范围。
惯例排除 iOS 系统版本、小程序版本、机型、网络等因素后,得到以下关键信息:仅在 iOS 出现,可稳定复现,是个存在多个容器版本的深山老 Bug。只要能稳定复现的问题,就都好解决,我隐隐松了一口气;但是,UI 类的问题的排查难度往往是最高的。果然,接下来我经历了一个痛苦的排查过程。
缩小问题范围后,第二件事就是自己上手复现。只要跟着调用栈调试一番,我估计能快速解决问题。配置好测试环境,在真机上复现问题后,我开开心心地连上 Safari Debugger,结果得到了一个让人傻眼的现象:问题无法又复现了。在 Safari 调试期间,一切运行正常,页面 redirect 后 navigate 的表现符合预期。
反复确认后,我得到以下结论:
不连接 Safari Debugger 就好使,连接 Safari Debugger 就不好使。
我承认那一刻我慌了。固然,以上结论意味着我没法用 Safari 调试,意味着问题定位的难度徒增,我即将花费更多时间;但更重要的是,这一结论超出了我的认知范围。恐惧的来源即是未知。对一个精神状态正常的程序员来说,应当有以下基础认识:99.99%的软件行为,都应该且只应该被代码影响。连不连 Debugger,对程序行为应当没有影响。这个玄妙的问题背后,定当有一个或愚蠢或深刻的原因。
然而,计算机这一行另一个更稳固的真理是,二进制的世界一切皆有缘由。任何意外的、让人摸不着头脑的问题,背后都有一个的原因静静地等待挖掘。只是有时,代码调用链路太长以至于追踪范围太大,难以定位;或者,问题的产生是多个 bug 共同作用导致,导致业务表现诡谲,难以分析。每一个这样的问题都是珍贵有价值的,正因为它难以发现,才使得挖掘它的过程充满意义。
此时,需要回答的问题变成了两个:
一,哪段业务代码造成了业务逻辑的分叉;
二,为什么 Safari Debugger 会导致程序异常。因为解决业务问题更要紧,我只能放弃断点调试,通过打 log 进行调试。
小程序源码本身不可能有问题,问题应该出在小程序底层的 JS 运行时框架上;小程序在 WebView 执行的都是被 uglify 的代码,无可读性。为了方便增加 log,我又编译了一份未被 uglify 的小程序核心 JS 框架。但另一个现象让我彻底麻了:只有被 uglify 的代码才能复现问题,可读性更好的非 uglify 版本一切正常。
在我粗浅的认知中,uglify 应当是一个极其谨慎的过程。因为从词法分析、语法分析和语义分析,我相信 uglify 的作者不会蠢到改变代码的 AST,否则它也不可能被大范围使用了。
然而认知归认知,面对 uglify 后的代码出异常的情况,此时我隐隐感觉是更底层的东西不对。可能是 uglify 的实现有 bug,在某种情况下改变了程序的行为。但是,这种程序行为的改变,不应该被 Safari Debugger 影响。也就是,如果是 uglify 的锅,那么产出的错误代码,不论是否连接 Debugger,都应该是不好使的,不应该连上 Safari Debugger 后就又好使了。
对 uglify 的有罪推演走不通,另一个更大胆的猜测是,问题发生在比 uglify 和 Safari Debugger 还要低的那一层,也就是编译器。但我一个小小的 button 仔何德何能可以碰到这样罕见的问题呢?
回到问题。没有 Debugger,也没有可读的源码,为了定位发生问题的代码,我只能基于 uglify 后的 JavaScript 代码增加 log,一点一点地逼近问题。有一说一,对着 uglify 的代码用 log 调试真不是人干的事。UI 逻辑本就复杂不好理解,还要拿着满屏的鬼画符和奇怪的 JS 写法,对比着源码理解调用栈,调试成本及其高。但这都是体力活,靠堆时间就能搞定,这里按下不表。
经过辛苦的排查,最终将问题代码定位到一个函数。也正是该函数的问题,导致小程序中页面堆栈管理的逻辑不符合预期,最终导致了本文开始时介绍的问题。
我将发生问题的代码抽象成以下代码。这一段简单的代码,居然揭示了一个诡异的 WebKit Bug。
(function (){
var myCar = { color: 0 }
var myCarCopy = myCar;
myCar = (myCar.color += 1);
console.log(myCarCopy.color);
})();
试分析以上代码,请问 console 会打印什么?
代码解读
以上代码的关键在于第 4 行。括号内,
myCar.color += 1
是一个 Addition Assignment 调用,myCar.color
原先为0
,现在应该为1
。接着,在等号左边,myCar
原先是一个Obejct
,然后被赋值括号右边的返回值。第 4 行结束后,myCar
的值从{ color: 1 }
变成了1
。此时,myCarCopy
作为指向原先myCar
对象的指针,其color
的值应当是1
。
用 Node 测试,以上代码会打印1
。如下图:
image.png
但在我对小程序 uglify 后的代码的 log 打印测试中,以上代码打印0
。为了缩小问题场景,经过我的反复测试,最终得出最简单的复现场景:用 macOS 的 Safari,打开元素检查器,在 console 的 tab 中,在 Disable Breakpoints 的情况下,以上代码会打印0
。如下图:
image.png
但若Enable Breakpoints
之后,则打印1
。如下图:
image.png
也正因为这个行为,导致了本文中遇到的问题。
Breakpoints
的Enable
与否,会改变程序行为,因此连接 Debugger 后,Breakpoints 默认 Enable,导致跳转失败问题无法复现。我尝试连接 Debugger 并将 Breakpoints 给关闭后,问题也可以复现(function (){
var myCar = { color: 0 }
var myCarCopy = myCar;
myCar.color += 1;
myCar = myCar.color;
console.log(myCarCopy.color);
})();
确定以上结论后,我将问题反馈给了 WebKit,链接:https://bugs.webkit.org/show_bug.cgi?id=246787[2]。目前该问题的状态是进入了 apple 内部的 radar 系统 现在,我的面前有两条路。一是继续研究 WebKit 源码,找到 WebKit 内部问题根因;二是就此停下脚步。抬头浩瀚的 WebKit 工程,再低头看看我手上还没写完的 button,我陷入了两难。此时,我想起在 coursera 的[Build a System](https://www.coursera.org/learn/build-a-computer "Build a System")
这堂课中学到一个观点:
计算机的世界其复杂,人类为了解决这一问题,将计算机分成了很多层。比如站在 Software Engineer 的角度,可以不用理解 Hardware 的一个原因是:我们要相信,在地球的某个角落,已经有另一帮聪明的人帮你把更低一层的问题搞定了。因此,我们只负责调用底层,不用关心其实现。以此,防止自己被淹没在无穷无尽的细节中。
因此,为了防止自己被不熟悉的 WebKit 源码呛死,我选择了后者,暂时放弃对 WebKit 问题的追寻。
幸运的是,在我将本文发到内部后,得到 V8、LLVM 领域的大神 @林作健[3] 的响应。大神快速地定位了该问题,不仅从 WebKit 源码的角度解释了发生问题的原因,还给出了修复方案。
代码引发问题一个重要特点是 myCar 这个变量发生了第二次赋值。第一次是myCar = { color: 0}
。第二次是myCar = (myCar.color += 1);
。这个特点是 JSC 漏处理的情况。
首先遇到的表达式节点是=
。左手边是myCar
,右手边是(myCar.color += 1)
。JSC 对此的处理是在AssignResolveNode::emitBytecode
RegisterID* right = generator.emitNode(local, m_right);
generator.emitProfileType(right, var, divotStart(), divotEnd());
result = generator.move(dst, right);
可见是先生成右手边的表达式。把结果移动到dst
。这里的dst
就是 myCar。事实上local
也是dst
。这个 move 在处理这个表达式是空操作的。因为local
是RegisterID* local = var.local()
。var
来自Variable var = generator.variable(m_ident);``m_ident
就是符号myCar
。总结是这个表达式 emit 了右手边然后把结果赋予左手边的变量myCar
(有点废话)。
当处理右手边的ReadModifyDotNode
就出问题了。
先拿到myCar.color += 1
的左手边:
RefPtr<RegisterID> base = generator.emitNodeForLeftHandSide(m_base, m_rightHasAssignments, m_right->isPure(generator));
拿到的base
是和要求的输出dst
是同一个位置。这就是出错的根源了。
处理了+=
过程:
RegisterID* updatedValue = emitReadModifyAssignment(generator, generator.finalDestination(dst, value.get()), value.get(), m_right, m_operator, OperandTypes(ResultType::unknownType(), m_right->resultDescriptor()));
把+=的结果放在了dst
。因为dst
是和base
同一个位置。所以base
被覆盖位+=
后的结果 1。
后面的回写:
RefPtr<RegisterID> ret = emitPutProperty(generator, base.get(), updatedValue, thisValue);
base
此刻已经是结果,一个整数,目前值是 1。相当于表达式1.color = 1
对于 JSC 几个 ReadModifyNode 而言,需要添加识别base
是否和dst
同一个位置的代码:
if (base.get() == dst) {
RefPtr<RegisterID> tmp = generator.newTemporary();
base = generator.move(tmp.get(), base.get());
}
可以对比下生成正确的字节码和错误字节码
错误版:
[ 17] mov dst:loc6, src:loc8
[ 20] mov dst:loc7, src:loc6
[ 23] get_by_id dst:loc8, base:loc6, property:0
[ 28] add dst:loc6, lhs:loc8, rhs:Int32: 1(const1), profileIndex:0, operandTypes:OperandTypes(126, 3)
[ 34] put_by_id base:loc6, property:0, value:loc6, flags:
正确版:
[ 17] mov dst:loc6, src:loc8
[ 20] mov dst:loc7, src:loc6
[ 23] mov dst:loc8, src:loc6
[ 26] get_by_id dst:loc9, base:loc8, property:0
[ 31] add dst:loc6, lhs:loc9, rhs:Int32: 1(const1), profileIndex:0, operandTypes:OperandTypes(126, 3)
[ 37] put_by_id base:loc8, property:0, value:loc6, flags:
对于业务的同学,应该尽可能避免写出表达式
a = (a.xxx+=x)
。
好消息是知道了问题发生的原因,并且在 @林作健[4] 大神的帮助下定位了 WebKit 的问题,此锅与我无关。但坏消息是,如何 workaround,帮助业务解决问题,成了下一个难题。
首先,该问题发生在小程序容器的核心 JS 框架中,业务无法绕过;其次,该问题属 WebKit 范畴,属于系统底层问题,Native 和 JS 框架层都无法绕过;唯一可行的,就是通过定制 uglify,在 uglify 的 pass 完成后,在小程序框架编译后,人为地增加订正过程。
然而,这种方案也不完美。虽然很有必要,但这一编译打包 pass 的增加,增加了团队的理解成本和维护负担,且不具备扩展性。因为将来此外源码的变更,可能使这一 fix 方案无效;并且,将来其它代码也有可能出同样的 uglify 结果,无法从根本上避免。
我心底认为,这里一定有更好的修复方案。此时,回头看看出问题的这一行代码:
myCar = (myCar.color += 1);
这里有两个解决方案,要么将这一句拆分,要么,就是不要服用 myCar 这个变量,重新使用一个。我的猜测是,uglify 这里为了性能,检查到 myCar 这个变量在函数返回之前没有再使用到,因此做了优化,复用了这一变量名。按照这个思路,我想 uglify 一定有某个参数,可以禁用这个 feature。因此我去 github 找到 uglify 的文档做了一番研究,并没有找到合适的参数;为了寻求帮助,我提了一个 issue 给 uglify 的维护者,以求获得帮助。
链接:https://github.com/mishoo/UglifyJS/issues/5727[5]
此时才发现 uglify 有 --webkit
这个参数,来绕过一个奇怪的 WebKit 问题。加上这个参数,uglify 之后的源码就变成了以下形式:
(function (){
var myCar = { color: 0 }
var myCarCopy = myCar;
var anotherCar = (myCar.color += 1);
console.log(myCarCopy.color);
})();
此时,myCar 这一变量不再被复用。
最终,我们的 fix 方案是,在小程序的核心 JS 框架编译时,对 uglify-js 的调用增加--webkit 这一编译选项。
这个问题的排查,是一个比较少见的排查难度较高、问题根因较深的问题,排查期间的思考记录下来以为日后参考。
人的固定认知,不一定就是对的。比如,我认为 Debugger 不会改变程序行为,这在 Objective-C 和 Swift 的世界或许是对的,因为端上执行的是被编译好的二进制,Debugger 的不会改变原有逻辑;但在 JS 的世界,即使是 AOT,每次刷新也会重新编译,使环境影响代码行为成为可能。
本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/Qif83v-b9MSae-a3D3IQAQ
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。