1. 问题记录
短信召回需求
当用户收到短信后,点击链接启动本地版 APP,外部调起 RN 招聘页面。
QA 反馈问题
在 vivo、华为等机器上,大概率出现 RN 页面崩溃,如下截图:
排查过程
(1) debug bundle 调试
安装对应的本地版非发布包,打开 RN 调试页面,与 FE 进行联调,尝试复现。发现打开本地版 App 后未复现。
(2) 怀疑 server 返回脏数据
错误信息非常有限,即通过 jsbridge 交互后,native thread 渲染 ui 异常:
ViewManager for tag 365 could not be found
Attempt to invoke interface method 'int java.lang.CharSequence.length()' on a null object reference
查看 React Native 源码 (0.57.8):
NativeViewHierarchyManager.java:
public final synchronized ViewManager resolveViewManager(int tag) {
ViewManager viewManager = (ViewManager)this.mTagsToViewManagers.get(tag);
if (viewManager == null) {
throw new IllegalViewOperationException("ViewManager for tag " + tag + " could not be found");
} else {
return viewManager;
}
}
public synchronized void createView(ThemedReactContext themedContext, int tag, String className, @Nullable ReactStylesDiffMap initialProps) {
UiThreadUtil.assertOnUiThread();
SystraceMessage.beginSection(0L, "NativeViewHierarchyManager_createView").arg("tag", tag).arg("className", className).flush();
try {
ViewManager viewManager = this.mViewManagers.get(className);
View view = viewManager.createView(themedContext, this.mJSResponderHandler);
this.mTagsToViews.put(tag, view);
// 反射创建 view 后,放入 mTagsToViewManagers
this.mTagsToViewManagers.put(tag, viewManager);
view.setId(tag);
if (initialProps != null) {
viewManager.updateProperties(view, initialProps);
}
} finally {
Systrace.endSection(0L);
}
}
native thread 收到 js 发送的消息后(经 jsBridge),创建对应的组件,创建组件过程中涉及属性、布局的构造,中间出现异常则会导致 view 构建失败。
ReactBaseTextShadowNode.java:
private static void buildSpannedFromShadowNode(ReactBaseTextShadowNode textShadowNode, SpannableStringBuilder sb, List<ReactBaseTextShadowNode.SetSpanOperation> ops) {
// 空指针
int start = sb.length();
int end = 0;
for(int length = textShadowNode.getChildCount(); end < length; ++end) {
// ...
}
//...
}
该异常也是由于组件创建失败,RTCText 设置文本时报空指针。由于 jsBridge 的特性:
异步。消息队列是异步的,无法保证处理事件。
序列化。通过 JSON 格式来传递消息,每次都要经历序列化和反序列化,开销很大。
批处理。对 native 调用进行排队,批量处理。
所以 view 创建失败后,将输出一系列异常信息。首先怀疑是外部调起情况下 server 下发了脏数据,抓取返回数据:
{
"code": 0,
"data": {
"list": [{
"infoType": 5,
"infoId": "38075662398371",
"userId": null,
"uid": "55418862830611",
"jobTitle": "店长/导购",
"minSalary": 4000,
"maxSalary": 8000,
"salaryUnit": 4,
"welfareTagList": [],
"salaryNegotiable": null,
"localName": "市南区",
"jumpUrl": "wbutown://jump/town/RN?params=%7B%22bundleid%22%3A%22282%22%2C%22params%22%3A%7B%22infoid%22%3A%2238075662398371%22%2C%22hideBar%22%3A1%2C%22tz_page%22%3A%22job%22%2C%22tjFrom%22%3A%22lm_list_jz__tz1298993323622129664__6__jz__adtypes__1__null__%7BCID%7D__eyJyIjp7ImluZm9pZCI6IjM4MDc1NjYyMzk4MzcxIiwic2xvdCI6ImxtX2xpc3RfanoiLCJ0eXBlIjoiMTUxIiwic2lkIjoidHoxMjk4OTkzMzIzNjIyMTI5NjY0In0sInQiOjEsInYiOjEsInciOnsibG1fc2xvdCI6InR6X2xpc3QifX0%253D%22%2C%22logParams%22%3A%5B%220%22%2C%22%22%2C%22%22%2C%22%22%2C%22%22%2C%2238075662398371%22%5D%2C%22from%22%3A%225%22%2C%22chargeUrl%22%3A%22%22%2C%22needlogin%22%3Afalse%7D%7D",
"isSelected": false,
"isAccurate": null,
"isVip": false,
"address": null,
"shopName": "",
"positionId": "811017",
"shopId": null,
"phone": null,
"localId": "370202000000",
"source": 21,
"adClickUrl": null,
"content": "岗位职责:1、接待顾客的咨询,了解顾客的需求并达成销售;2、负责做好货品销售记录、盘点、账目核对等工作,按规定完成各项销售统计工作;3、完成商品的来货验收、上架陈列摆放、补货、退货、防损等日常营业工作;4、做好所负责区域的卫生清洁工作;5、完成上级领导交办的其他任务。任职资格:1、高中以上学历;2、有相关工作经验者优先;3、具有较强的沟通能力及服务意识,吃苦耐劳;4、年龄18-35岁,身体健康。工作时间:两班倒",
"cateName": "店长/卖场经理",
"adInfoType": "2",
"clickReportUrl": "https://jumpluna.58.com/click?target=pZwY0Zn-nYD-nbm-nbu_uyIfmvQGmv_8PH98mvqVFHFApMRV0aN1wMw60hI-IaN1wZKpIdbkyhO_0LGQwY-vNbFVuYVo0dIrnLPCNAuRwHIB0Y67pvYQIdIViD-C0Y68Ih3QH-uEEgIB0h-uHLFKwhF1XNRuN7ILp7KgidIEnHKnUNRDu7KcwR7zpW-vU-0OnyOJUR78wNwsUWDOpdTQHMuERvnQ0b9OnRTQwDVER1-b0hGNidKgrywzp-wsUb68nvOJmYV8iAnQRD6DIh3QRj7zpbOnNAGWygFJnDV8wHIkHWIQuLIrNAP7NMI3RN7bX7RKsH7FRDwwRD6DidwcmYVVnyYQN70knvdvwH7VRYVaNACOuAOow77EphPn0hCOpv3QmW7EiAn1UW7WpvOJRAVNi7wsRD6DivYQrgGEpDOuNj7BR-KcryV8X--uNj77HLPOPdI8RvFyUN6dRdKcryw8pAPv0M-WH7wcw7-8R1T10hGNngFcUWP8nH-nUhGWuZFcmYV8iDRlNjDOnvOJUbq8nH-oUhGBygFcnZu8RDwsNj7NI-wcwZG8pYwsRA-uiY-pwL6VHRcQ0Z-5sHKnUbVFpR-lIN6LEdKvrHnknidnINQiXb-CNW7dXbqy0A9vX7KcPbFdXNqB0Z-AP-NV0RqkXRFa0ZbLu-6OHbVkyMIunAGjuMPvNRu1XH0Q017YpjKnidmduYOyUh6rygKJiNPzyhO5iguFujKPIiYkIWT3Rg-ZEv0QHWPVp7c3INDVEhdOHL6REiYVUy9VP-RnsidAHRFuRRFQnRRnNMGVIbOQiRGZXAdPNW7kXNtVnDQ8idwcmd78pHPwND6DrAOlnLGEiAPsU-wDpdw7wDVmH7-sUb6NpdKJmYV8i7woU-INHAOowj78Eyd5NZuLpAOlyguzEyO50Y67nyYQU-uziDOoUh7uIhOcIRIzXgRBIg-8NyYQUbVNi7wsRDRDpdw7wAVNXNqbRN7oidKgHbqViAFoUhGrNgRcwgGERYE10bN-nWu8UvGdUgT-nYEQFHcvug6YFHPDnywLnN6MIv7myMuYXbIQUgRpN7R2yhRsyyGxIDwJuyOuyWu6ih7ouY-sNDRvmHm3ihDvuLT1ENVlpbDQnAwQNMw6HLcYmRKzEy7vUMR8I17VuduRIA7EEhRZRbw-uvRYpHPQ0A-gPLKDuYDQnAwOwjKgEH7mmNqBUyqorAd6Ph7vmHu7Ih7ENgufHYNLi1RYnD_zIjKbXNEkR10drDILnWw8RY_kR1mdn70LUN-JRMTkRY7GpdqRngwgigEzu-RDURqQwjwsijFGpM0VuAG5nWwQrDV3i19zEYq5nN_QRh7kHvRDUhuRiR6bPW7suhRsi-qQnb7b0y7vuMI8r7FcrjIx0N-2w1RaIhuLrRK6ig6-wg7VyywR0AVxNjbz0R73X-qQw7-bRN-RsRIdXAwgwYw7PM6luD9zEvCLnW6xRMw2HL7DpDVwI7RbENwsuW9zyDq-wZ6hRN-RgYtVUvEzrjwx0NCYuY9zNdRQnWPbraYvgL7VUhwRIDnQRYV3phRswDtLUHub0g6Lmg78NA7OIHI8NNV3i19zNdREUbR6XgNYwHusnywQUbR6w-NvngFwPvqoRNG60-7KU-usrAO5RN66P-RVwHuzIbVoRN9KTEDznHD8nHNQsWn8nWNzTHTKm1m1PW03mvE1mWKBPj9dnk7UrHczP7YKy1DznBk9nHc1gE7dsHF-PA9Lpj61rgIMIgFvugPMshdJp7tdrAF-UhwGmh78gvQGuyFGmyqOuE7YX-qBIgPGUhR10k7YXWDzrH9OrHn1nWnvnWcQnWbvPWEKnW9KPHNYnH93PWc3n1TvnHDKnHEzP193njnOn191rj0knWNOn9D1rjTLPHmvnWnOrjnLnEDvrjNKPW9dTHD8nTDQTgwlgvQG0LEKmgKGgvQfudq-XZwxmyFxEiQ6uZ6xmh-bgv7BgYD_IZGxphqBgLF6UhVxnBQ_UvIxpyOhUdq-XZwxmyFxEiQluDQ-uvqxmyFxEiQkIZFxnHTkPjFxmMRbuvRYgv6JXMIxug6kxjD_0Zwzg1DknjEzgvFduAI-I7qCpMGLgvR30ZkQsZKY0-tQnjTYn-qBIywMugwxpAGlIdq-XZKtniQkIZFxnHTkPjFxmMRbuvRYgv6JXMIxug6kxjD_0Zwzg1DknjEzgvFduAI-I7qCpMGLgvR30ZkQTEDVTyOdUAkKTyOdUAkKUMR_UTDvrjNKnE7Vph6xPH6BuyObpyF6U-q_pyRBpy7fXyNKnTDkTHDKuh7_0vNKTNORHDkKnHTknWTLnkDQTHDzn9DQn19kn9DdnHEQsW9zP1mKnHTK",
"adTag": null,
"infoCode": "122",
"tcAdType": null,
"productLine": "12",
"adid": "1427880393838702592",
"cateIds": "2489",
"slotId": null,
"tjfrom": "lm_list_jz__tz1298993323622129664__6__jz__adtypes__1__null__{CID}__eyJyIjp7ImluZm9pZCI6IjM4MDc1NjYyMzk4MzcxIiwic2xvdCI6ImxtX2xpc3RfanoiLCJ0eXBlIjoiMTUxIiwic2lkIjoidHoxMjk4OTkzMzIzNjIyMTI5NjY0In0sInQiOjEsInYiOjEsInciOnsibG1fc2xvdCI6InR6X2xpc3QifX0%3D",
"strategyId": "1",
"tzAbTest": null,
"infoTitle": null,
"isTop": null,
"showUrl": "https://luna.58.com/track?pn=1&slot=tz_list&v=1&action=1001&pos=1&sid=tz1298993323622129664&infoid=38075662398371&adtype=28&referer=&time=1630385742616&localpath=122%2C123&catepath=9224&utm_source=&spm=NULL_SPM",
"innerPositionId": "900002",
"deliverStatus": 2,
"userPic": "http://pic1.58cdn.com.cn/m1/bigimage/n_v214f25bcd1a6d4a398b98364d759fac9c.jpg",
"userNickName": "予人玫瑰",
"sourceType": 0,
"jumpParam": {
"jobThirdExtend": "{\"spm\":\"u-2e4h7h8s9wgurvesg.mjh_58bendiban_liebiaoye\",\"infoid\":\"38075662398371\",\"pos\":\"1\",\"imei\":\"c63678cd3b0b4853\",\"slot\":\"tz_list\",\"platform\":2,\"sid\":\"tz1298993323622129664\"}"
},
"url": "https://tzad.58.com/tzad/info/1427880393838702592?code=370202000000&mid=38075662398371&p=12&positionId=900002&outPositionId=811017&sid=1298993323621699584&page=tzlist&source=tzapp",
"visitorUrl": "https://luna.58.com/track?media=u-2e4h7h8s9wgurvesg.mjh_58bendiban_liebiaoye&action=1002&localpath=122%2C123&catepath=9224&adtype=28&pn=1&utm_source=tz_business&spm=NULL_SPM&url=http%3A%2F%2Flm-as%2F%3F%26spm%3DNULL_SPM%26utm_source%3Dtz_business&referer=&openid=&time=1630385742616&platform=2&d=%7B%22page_type%22%3A%224%22%2C%22element_id%22%3A%220%22%2C%22element_param%22%3A%7B%22sid%22%3A%22tz1298993323622129664%22%2C%22pos%22%3A%221%22%2C%22slot%22%3A%22tz_list%22%2C%22infoid%22%3A%2238075662398371%22%7D%7D&info_param=%7B%22111566%22%3A%228%22%2C%22111567%22%3A%22%22%7D"
},
// ...
],
"param": {
"tgIndex": 0,
"tzPageIndex": 1,
"showno": 35,
"tcHRIndex": 0,
"tcbFlag": "true",
"pageNo": "1",
"tcADXIndex": 0,
"cateCitySupplementShowno": 0,
"tzbizlcIndex": 0,
"citySupplementShowno": 0,
"tzbizlcHotIndex": 0
}
},
"msg": "success"
}
可以看到有大量的 value 为 null 的数据,经过测试,将 null 数据做容错处理,上述异常仍是时复现时正常。
(3) 怀疑外部调起 RN 框架初始化时机
还有一种情况,外部调起 RN 未初始化完成,立即调起载体页。经过测试打开载体页时 RN 框架早已初始化好。
(4) AOP 捕获 React Native 异常
由于使用的是 React Native 0.57.8 版本,该版本在 Android 上存在大量适配问题。尝试 AOP 捕获上述异常,结果页面可以正常打开,但会随机出现各种属性设置异常:
错误日志 (属性错误随机出现,如 text size, allowFontScaling 等):
LOCATION ViewManagersPropertyCache.java line 100 in com.facebook.react.uimanager.ViewManagersPropertyCache$PropSetter.updateShadowNodeProp()
EXCEPTION java.lang.IllegalArgumentException
MESSAGE method com.facebook.react.views.text.ReactTextShadowNode.setAllowFontScaling argument 1 has type boolean, got java.lang.Integer
而此时 QA 同学提供了一条重要线索,外部调起后需要返回 2 次才能返回主页面。
(5) RN 载体页打开两次
简言之,就是 React Native 0.57.8 在 jsBridge 传输中,传输的 ReadableMap (包装了参数) 存在线程同步问题。
再次查看日志,发现 RN 载体页打开了两次,而 WubaRN SDK 有预创建 RN 环境问题,首次启动先创建一个 WubaRN 对象,再次打开一个载体页会复用上一次的 WubaRN 对象。而当两个载体页同时存在时(特别是加载的还是同一个 bundle),jsBridge 就会出现随机的线程安全问题。
解决方案
# | 方案 |
---|---|
1 | 排查外部调起打开 2 次 RN 载体页的原因 |
2 | 对同时打开多次同一 bundle id 做处理,只保留栈顶的 |
3 | 修复 0.57.8 DynamicFromMap 线程安全问题,但目前 WubaRN 引入的 AspectJ 切片修复难度大,且修改后测试范围较大 |
同时北斗平台之前也上报了大量的此类型错误,属于疑难问题,通过这次 bug 终于找到元凶:
2. 跨端框架问题的排错成本
跨端框架可以减少开发投入和维护成本,同时大都具备热更新的能力(RN、Flutter、小程序、布局动态化等)。但实际过程中,对于跨端框架的维护成本却不小。总结下,主要由于以下几点原因:
- 跨端框架通常具备一定的门槛壁垒(具体不是难度,更侧重于方向),业务开发同学遇到相关问题一般难以定位。
- 涉及的方向较多,一般跨端框架会涉及到客户端、前端、服务端,中间做了大量的桥接转换,排查问题经常会涉及到这几个面。
- 日志少且通常是经过转换的日志,难以直接定位问题。
减少排错成本的方案:
# | 方案 | 难度 |
---|---|---|
1 | 业务同学轮换接触跨端框架 | 实施难度大 |
2 | 加大文档投入,记录常见问题和疑难问题 | 实施难度适中 |
3 | 在框架内部对错误日志进行转换,尽可能收集较多的信息 | 实施难度适中 |
4 | 跨端框架负责同学需要深入框架原理及具备前端开发能力,能够快速定位问题 | 实施难度适中 |
其他方案欢迎头脑风暴进行讨论。