Mobile Development 16 min read

iOS WebView Crash Analysis and Solution

The article investigates a rapid increase in WebView crashes on iOS 4.9.x caused by a UIScrollView animation accessing a released object after an H5 page is dismissed, discovers the offending WKChildScrollView delegate (WKScrollingNodeScrollViewDelegate), and resolves the issue by nullifying that delegate in the view’s deallocation, supplementing earlier gesture‑disable workarounds.

DeWu Technology
DeWu Technology
DeWu Technology
iOS WebView Crash Analysis and Solution

1. Background

得物 iOS 4.9.x 版本上线后,一些带有横向滚动内容的h5页面,有一个webkit 相关crash增加较快。通过Crash堆栈判断是UIScrollview执行滚动动画过程中内存野指针导致的崩溃。

2. Preliminary Investigation

通过页面浏览日志,发现发生崩溃时所在的页面都是在h5 web容器内,且都是在页面的生命周期方法viewDidDisappear方法调用后才发生崩溃,因此推测崩溃是在h5 页面返回时发生的。

3. Root Cause Analysis

经过分析,崩溃的原因是页面退出后,页面内存被释放,但是滚动动画继续执行,这时崩溃堆栈中scrollview的delegate没有置空,系统继续执行delegate的相关方法,访问了已经释放的对象的内存(野指针问题)。同时发生crash h5 页面都存在一个特点,就是页面内存在可以左右横滑的tab视图。

4. Initial Fix

操作手势侧滑存在体验问题,左右横滑的tab视图也会跟着滚动。关联bugly用户行为日志,判断这个体验问题是和本文中的crash有相关性的。因此在h5页面手势侧滑返回时,将h5容器页面内tab的横滑手势禁掉(同时需要在 h5 web容器的viewWillAppear方法里将手势再打开,因为手势侧滑是可以取消在返回页面)。

具体代码如下:

let contentView = self.webView.scrollView.subviews.first { view in
if let className = object_getClass(view), NSStringFromClass(className) == "WKContentView" {
return true
}
return false
}
let webTouchEventsGestureRecognizer = contentView?.gestureRecognizers?.first(where: { gesture in
if let className = object_getClass(gesture), NSStringFromClass(className) == "UIWebTouchEventsGestureRecognizer" {
return true
}
return false
})
webTouchEventsGestureRecognizer?.isEnabled = enable

经过测试,h5 web容器侧滑时出现的tab页面左右滚动的体验问题确实被解决。这样既可以解决体验问题,又可以解决侧滑离开页面导致的崩溃问题,但是这样并没有定位crash的根因。修复代码上线后,crash量确实下降,但是每天还是有一些crash出现,且收到了个别页面极端操作下偶现卡住的问题反馈。因此需要继续排查crash根因,将crash根本解决掉。

5. Deeper Investigation

继续看文章开始的crash堆栈,通过Crash堆栈判断崩溃原因是UIScrollview执行滚动动画过程中回调代理方法(见上图)时访问被释放的内存。常规解决思路是在退出页面后,在页面生命周期的dealloc方法中,将UIScrollview的delegate置空即可。WKWebView确实有一个scrollVIew属性,我们在很早的版本就将其delegate属性置空,但是崩溃没有解决。

deinit {
scrollView.delegate = nil
scrollView.dataSource = nil
}

因此崩溃堆栈里的Scrollview代理不是这里的WKWebView的scrollVIew的代理。那崩溃堆栈中的ScrollViewDelegate到底属于哪个UIScrollview呢?幸运的是苹果webkit 是开源的,我们可以将webkit源码下载下来看一下。

6. Finding the Real ScrollView

通过阅读源码后发现不是这样的(代码有删减,感兴趣可自行阅读源码)。6.1 WKScrollView 代理实现

首先看到WKWebView的scrollview的类型其实是WKScrollView(UIScrollview的子类),他除了继承自父类的delegate属性,还有一个internalDelegate属性,那么这个internalDelegate属性是不是我们要找的WKScrollingNodeScrollViewDelegate 呢?

@interface WKScrollView : UIScrollView
@property (nonatomic, assign) WKWebView <UIScrollViewDelegate> *internalDelegate;
@end

通过阅读源码后发现不是这样的(代码有删减,感兴趣可自行阅读源码)。6.2 猜想 & 验证

既然WKScrollingNodeScrollViewDelegate 不是WKScrollview的属性,那页面上还有其他scrollview么。我们看源码WKScrollingNodeScrollViewDelegate 是在哪里设置的。

void ScrollingTreeScrollingNodeDelegateIOS::commitStateAfterChildren(const ScrollingStateScrollingNode& scrollingStateNode) {
//......
if (scrollingStateNode.hasChangedProperty(ScrollingStateNode::Property::ScrollContainerLayer)) {
if (!m_scrollViewDelegate)
m_scrollViewDelegate = adoptNS([[WKScrollingNodeScrollViewDelegate alloc] initWithScrollingTreeNodeDelegate:
this
]);
}
}

搜索webkit的源码,发现创建WKScrollingNodeScrollViewDelegate的位置只有一处。但是webkit的源码太过于复杂,无法通过阅读源码的方式知道WKScrollingNodeScrollViewDelegate属于哪个scrollview。

为此我们只能换一种思路,我们通过xcode调试的方式查看当前webview加载的页面是否还有其他scrollview。

7. Identifying the Real ScrollView

页面上刚好还有一个scrollview:WKChildScrollview

这个WKChildScrollview 是否是崩溃堆栈中的scrollview呢,如果我们能确定他的delegate是WKScrollingNodeScrollViewDelegate,那就说明这个WKChildScrollview 是崩溃堆栈中的scrollview。

为了验证这个猜想,我们首先找到源码,源码并没有太多,看不出其delegate类型。

@interface WKChildScrollView : UIScrollView <WKContentControlled>
@end

我们只能转换思路在运行时找到WKWebView的类型为WKChildScrollView的子view(通过OC runtime & 视图树遍历的方式),判断他的delegate是否为WKScrollingNodeScrollViewDelegate。

我们运行时找到类型为 WKChildScrollView 的子view后,获取其delegate类型,确实是WKScrollingNodeScrollViewDelegate。至此我们找到了崩溃堆栈中的scrollview。

确定了崩溃堆栈中的scrollview的类型,那么修复起来也比较容易了。在页面生命周期的viewDidAppear方法里,获取类型为 WKChildScrollView的子view。然后在dealloc方法里,将其delegate置空即可。

deinit {
if self.childScrollView != nil {
if self.childScrollView?.delegate != nil {
self.childScrollView?.delegate = nil
}
}
}

8. Conclusion

本文中的crash从出现到解决历时近一年,一开始根据线上日志判断是h5 页面返回 & h5 页面滚动导致的问题,禁用手势后虽然几乎解决问题,但是线上还有零星crash上报,因此为了保证h5 离线功能的线上稳定性,需要完美解决问题。

本文的crash 似曾相识,但是经过验证和阅读源码后发现并不是想象的那样,继续通过猜想+阅读源码的方式寻找到了崩溃堆栈中的真正scrollview代理对象,从而在app 侧解决问题。最后发现是苹果webkit的bug。

9. References

iOS 8 动画执行过程中返回 Crash

同层小程序渲染原理分析

字节跳动如何系统性治理 iOS 稳定性问题

WebViewcrash analysisiOS DevelopmentiOS stabilityTechnical SolutionWebKit
DeWu Technology
Written by

DeWu Technology

A platform for sharing and discussing tech knowledge, guiding you toward the cloud of technology.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.