我正在参与「·启航计划」

背景

​ 9.16号Apple向大众开放了iOS 16.1的榜首个Beta版别,现已有不少用户更新到了此版别(截止9.18日京东APP 的单日用户已达4w+)。

​ 新版别发布后,咱们溃散监控体系监控到了在16.1版别呈现了大量导航栏相关的溃散,溃散量数现已排到榜首。下面介绍下此溃散问题的处理进程。

溃散信息

​ 查看反常原因如下:

Terminating app due to uncaught exception 'NSGenericException', reason: 'Unable to
activate constraint with anchors <NSLayoutDimension:0x283e9cc40 "_UINavigationBarTitleControl:0x115566ce0.height"> 
and <NSLayoutDimension:0x283eb3500 "UILayoutGuide:0x281205500'TitleViewGuide(0x115542e70)'.height"> because they have no common ancestor. 
Does the constraint or its anchors reference items in different view hierarchies? That's illegal.' 
-[XXViewController viewWillDisappear:] (in XXApp) (XXViewController.m:298)

​ 调用仓库为某个ViewController调用了viewWillDisappear:办法,在该办法中调用了

self.navigationController.navigationBarHidden = NO;

​ 该最终办法走到体系库,体系库调用仓库如下:

-[_UINavigationBarTitleControl updateConstraints] (in UIKitCore) + 1368,
-[UIView(Hierarchy) layoutBelowIfNeeded] (in UIKitCore) + 292
-[UINavigationController _positionNavigationBarHidden:edge:] (in UIKitCore) + 268
......
-[UINavigationController setNavigationBarHidden:animated:] (in UIKitCore) + 96
-[XXViewController viewWillDisappear:] (in JD...) (XXViewController.m:298)

​ 溃散仓库最后一个调用办法为体系导航栏更新束缚布局办法。这儿从仓库暂时看不出任何有用信息(仅仅调用体系导航栏展示办法就触发了Crash),只能看到是导航相关的两个布局目标因为束缚层级不对导致的无法激活特定的束缚条件引起的反常。

​ Apple Forums也能看到相关反应:

developer.apple.com/forums/thre…

问题复现

​ 现在来看溃散都是因为页面切换时将体系导航栏躲藏状态改变导致的Crash,溃散页面为主页或者RN页等页面层级比较靠前的页面。经过和搭档测试发现,Xcode 14-beta版别编译运转iOS 16.1机型并不能复现Crash,可是运用Xcode 13+iOS 16.1模拟器经过不断切页面测试,复现了溃散。复现途径为:发动App进入主页,主页进一个躲藏导航栏的页面,然后再进一个原生导航栏的页面,再回来到主页。

原因定位

​ 因为”self.navigationController.navigationBarHidden = NO”这个办法本身调用的便是体系办法,没有特殊性,报错的当地也是体系库,比较怪异。这儿先经过Xcode布局看下这个_UINavigationBarTitleControl和UILayoutGuide有没有特殊的当地。调试截图如下:
NO_1.png

​ 能够看到出问题的当地是标红的当地。经过查资料得知,“iOS 16体系针对导航栏titleView的内部逻辑发生了改变,新体系会将自定义视图包在一个新类_UINavagationBarTitleControl里,但其内部的视图联系在展示出来之前是不确定的,也便是自定义视图titleView.superView在完全展示前并不一定会存在。所以,自定义视图中不要随意重写 -updateConstraints, 并保证其autolayout条件正确,处理了咱们问题”。

咱们查了下工程中并无 -updateConstraints办法的调用,所以这个处理方案不适用于咱们。

​ 经过LLDB调试,咱们发现UINavagationBarTitleControl和其有束缚的UILayoutGuide目标这儿并无明显的反常,而UINavagationBarTitleControl目标的superView 和 LayoutGuide虚拟布局目标的owingView也正常,都是NavigationBarContentView,并不会呈现视图层级不一致的问题。

猜测一

因为事务需求,咱们的许多页面都采用了自定义导航栏的办法去呈现页面,所以存在较多的场景是A页面运用自定义导航,B页面运用体系导航,那A页面会在ViewWillAppear:办法中将体系导航栏躲藏,等到到viewWillDisAppear:生命周期办法中再将体系导航栏展示。这儿或许是因为在再次展示的时分导航栏还没有被添加到当时ViewController的view中,_UINavagationBarTitleControl.superView.superView还没有被添加到视图中,就引发束缚反常Crash。

​ 有了这个疑问,就去验证下。在基类导航中重写 setNavigationBarHidden:(BOOL )hiden animated: (BOOL )animated 办法:

-(void)setNavigationBarHidden:(BOOL)hidden animated:(BOOL)animated{
     if (!self.navigationBar.superview) return;
     [super setNavigationBarHidden:hidden animated:animated];
}

​ 经验证,App的确是不溃散了,可是导航栏标题区域变空白了。这种办法虽然修正了溃散,可是导航栏titleView布局也没有被更新到视图中,引出了新的问题。

继续研究了下_UINavagationBarTitleControl这个类的相关布局,结合溃散的布局目标

<NSLayoutDimension:0x283e9cc40 "UINavigationBarTitleControl:0x115566ce0.height">
and <NSLayoutDimension:0x283eb3500 "UILayoutGuide:0x281205500'TitleViewGuide(0x115542e70)'.height">

咱们发现这个束缚在*_UINavagationBarTitleControl->*sosConstraint束缚成员变量中。如图:

NO_2.png

​ 那么咱们能不能直接把束缚给去掉,来达到避免溃散的意图呢? 经过以下办法的确能够经过runtime将sosConstraint成员给设置成空,也处理了问题。详细逻辑:

判断UINavagationBarTitleControl->titleLayoutGuide不为空,且sosConstraint束缚成员变量不为空,就经过Ivar指针将其设置空目标,代码如下图:

NO_3.png

​ 经过简单打包验证,的确能够处理问题,可是这个做法不妥:此办法破坏了原有的代码逻辑,危险极高。

​ 到此,这个问题暂时陷入了僵局。

​ 经过进行页面切换回来的一个屡次验证,发现这个途径下App不会Crash:

主页(自定义导航栏)–》页面2(自定义导航栏)–》页面3(体系导航栏+运用体系titleView)–》页面4(体系导航栏+运用自定义titleView),依次回来页面,不会触发Crash。

​ 假如到页面3就依次回来主页,就会Crash; 假如进入了页面4再回来,就Crash。经过LLDB发现页面4导航栏没有UINavagationBarTitleControl目标生成,也便是说页面4因为运用自定义titleView,用不到UINavagationBarTitleControl,也不会溃散,过错的布局被更新掉了,自然也不会溃散了。

猜测二

​ 那会不会是因为体系导航栏的_UINavagationBarTitleControl更新不及时,譬如说有个延时更新的机制,导致前次的束缚相关的目标现已被提前开释了,导致下次更新的时分找不到目标,就引发了Crash?

​ 验证: 复现场景发现,反常情况下,_sosConstraint目标的确现已被标记为 _unsafe_unretained Class,如图:

NO_4.png

​ 看来这个猜想有一定牢靠度,那么怎么去修正呢? 能不能能够在每次设置导航栏躲藏/显现状态前提前更新前次未完成的布局呢? 体系的确提供给了咱们更新导航栏布局的办法,能够经过触发[navigationbar layoutSubViews]办法来做到:

-(void)setNavigationBarHidden:(BOOL)hidden animated:(BOOL)animated{
     if (@available(iOS 16.1, *)) {
         [self.navigationBar setNeedsLayout];
         [self.navigationBar layoutIfNeeded];
     }
     [super setNavigationBarHidden:hidden animated:animated];
}

验证

经过验证,上述代码逻辑的确能够修正问题。在Apple 开发论坛上苹果的工程师也清晰了当时测试版(16.1 -20B5050f)体系有这个bug存在,后续版别会予以修正(详见developer.apple.com/forums/thre…)。

NO_5.png

总结

​ 经过调试发现关于iOS 16.1(20B5050f)体系,针对导航栏在默认体系titleView的场景,新增的TitleControl不会及时去更新布局束缚,导致其layout束缚成员变量开释后才更新布局,等下次更新这个束缚目标成了unsafe_unretained 目标,造成了Crash。对应的处理方案为:在每次更新导航栏状态前,先主动调用一下更新布局办法,避免更新不及时触发体系Crash。

​ 经验证,Apple刚刚发布的iOS 16.1第二个版别(20B5050f),修正了此问题。

作者简介:京东客户端专家曹金果
邮箱:caojinguo@jd.com