RN-UI随机异常引出的跨端框架问题排错成本

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. 日志少且通常是经过转换的日志,难以直接定位问题。

减少排错成本的方案:

#方案难度
1业务同学轮换接触跨端框架实施难度大
2加大文档投入,记录常见问题和疑难问题实施难度适中
3在框架内部对错误日志进行转换,尽可能收集较多的信息实施难度适中
4跨端框架负责同学需要深入框架原理及具备前端开发能力,能够快速定位问题实施难度适中

其他方案欢迎头脑风暴进行讨论。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值