成人伊人青草久久综合网_91在线视频免费观看_毛片免费视频网站_亚洲欧美日韩在线中文字幕

得物H5容器野指針疑難問題排查 & 解決

來源:得物技術
1、背景

得物 iOS 4.9.x 版本 上線后,一些帶有橫向滾動內容的h5頁面,有一個webkit 相關crash增加較快。通過Crash堆棧判斷是UIScrollview執行滾動動畫過程中內存野指針導致的崩潰。

2、前期排查

通過頁面瀏覽日志,發現發生崩潰時所在的頁面都是在h5 web容器內,且都是在頁面的生命周期方法viewDidDisappear方法調用后才發生崩潰,因此推測崩潰是在h5 頁面返回時發生的。


【資料圖】

剛好交易的同事復現了崩潰證實了我們的推測。因此可以基本確定:崩潰的原因是頁面退出后,頁面內存被釋放,但是滾動動畫繼續執行,這時崩潰堆棧中scrollview的delegate沒有置空,系統繼續執行delegate的相關方法,訪問了已經釋放的對象的內存(野指針問題)。

同時發生crash h5 頁面都存在一個特點,就是頁面內存在可以左右橫滑的tab視圖。

操作手勢側滑存在體驗問題,左右橫滑的tab視圖也會跟著滾動(見下面視頻)。關聯bugly用戶行為日志,判斷這個體驗問題是和本文中的crash有相關性的。

3、不完美的解決方案

經過上面的分析,修復思路是在h5頁面手勢側滑返回時,將h5容器頁面內tab的橫滑手勢禁掉(同時需要在 h5 web容器的viewWillAppear方法里將手勢再打開,因為手勢側滑是可以取消在返回頁面)。

具體代碼如下(這樣在操作頁面側滑返回時,頁面的手勢被禁掉,不會再滾動):

@objc dynamic func webViewCanScroll(enable:Bool) {        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    }@objc dynamic func webViewCanScroll(enable:Bool) {        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根本解決掉。

繼續看文章開始的crash堆棧,通過Crash堆棧判斷崩潰原因是UIScrollview執行滾動動畫過程中回調代理方法(見上圖)時訪問被釋放的內存。常規解決思路是在退出頁面后,在頁面生命周期的dealloc方法中,將UIScrollview的delegate置空即可。WKWebView確實有一個scrollVIew屬性,我們在很早的版本就將其delegate屬性置空,但是崩潰沒有解決。

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

因此崩潰堆棧里的Scrollview代理不是這里的WKWebView的scrollVIew的代理。那崩潰堆棧中的scrollView代理到底屬于哪個UIScrollview呢?幸運的是蘋果webkit 是開源的,我們可以將webkit源碼下載下來看一下。

4、尋找崩潰堆棧中的ScrollViewDelegate

崩潰堆棧中的ScrollViewDelegate是WKScrollingNodeScrollViewDelegate。首先看看WKWebView的scrollview的 delegate是如何實現的,因為我們猜想這個scrollview的delegate除了我們自己設置的,是否還有其他delegate(比如崩潰堆棧中的WKScrollingNodeScrollViewDelegate)。

通過對Webkit源碼一番研究,發現scrollview的初始化方法:

- (void)_setupScrollAndContentViews{    CGRect bounds = self.bounds;    _scrollView = adoptNS([[WKScrollView alloc] initWithFrame:bounds]);    [_scrollView setInternalDelegate:self];    [_scrollView setBouncesZoom:YES];}- (void)_setupScrollAndContentViews{    CGRect bounds = self.bounds;    _scrollView = adoptNS([[WKScrollView alloc] initWithFrame:bounds]);    [_scrollView setInternalDelegate:self];    [_scrollView setBouncesZoom:YES];}

WKWebView的scrollVIew 是WKScrollView 類型。

4.1 WKScrollView 代理實現

首先看到WKWebView的scrollview的類型其實是WKScrollView(UIScrollview的子類),他除了繼承自父類的delegate屬性,還有一個internalDelegate屬性,那么這個internalDelegate屬性是不是我們要找的WKScrollingNodeScrollViewDelegate 呢?

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

通過閱讀源碼后發現不是這樣的(代碼有刪減,感興趣可自行閱讀源碼)。

- (void)setInternalDelegate:(WKWebView  *)internalDelegate{    if (internalDelegate == _internalDelegate)        return;    _internalDelegate = internalDelegate;    [self _updateDelegate];}- (void)setDelegate:(id )delegate{    if (_externalDelegate.get().get() == delegate)        return;    _externalDelegate = delegate;    [self _updateDelegate];}- (id )delegate{    return _externalDelegate.getAutoreleased();}- (void)_updateDelegate{//......    if (!externalDelegate)    else if (!_internalDelegate)    else {        _delegateForwarder = adoptNS([[WKScrollViewDelegateForwarder alloc] initWithInternalDelegate:_internalDelegate externalDelegate:externalDelegate.get()]);        [super setDelegate:_delegateForwarder.get()];    }}- (void)setInternalDelegate:(WKWebView  *)internalDelegate{    if (internalDelegate == _internalDelegate)        return;    _internalDelegate = internalDelegate;    [self _updateDelegate];}- (void)setDelegate:(id )delegate{    if (_externalDelegate.get().get() == delegate)        return;    _externalDelegate = delegate;    [self _updateDelegate];}- (id )delegate{    return _externalDelegate.getAutoreleased();}- (void)_updateDelegate{//......    if (!externalDelegate)    else if (!_internalDelegate)    else {        _delegateForwarder = adoptNS([[WKScrollViewDelegateForwarder alloc] initWithInternalDelegate:_internalDelegate externalDelegate:externalDelegate.get()]);        [super setDelegate:_delegateForwarder.get()];    }}

這個internalDelegate的作用是讓WKWebView 監聽scrollview的滾動回調,同時也可以讓開發者在外部監聽WKWebView的scrollview回調。如何實現的呢?可以查看WKScrollViewDelegateForwarder的實現。

- (void)forwardInvocation:(NSInvocation *)anInvocation{    //...    if (internalDelegateWillRespond)        [anInvocation invokeWithTarget:_internalDelegate];    if (externalDelegateWillRespond)        [anInvocation invokeWithTarget:externalDelegate.get()];}- (void)forwardInvocation:(NSInvocation *)anInvocation{    //...    if (internalDelegateWillRespond)        [anInvocation invokeWithTarget:_internalDelegate];    if (externalDelegateWillRespond)        [anInvocation invokeWithTarget:externalDelegate.get()];}

通過復寫- (void)forwardInvocation:(NSInvocation *)anInvocation 方法,在消息轉發時實現的。

4.2 猜想 & 驗證

既然WKScrollingNodeScrollViewDelegate 不是WKScrollview的屬性,那說明崩潰堆棧中的scrollview不是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]);        } }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。

頁面上剛好還有一個scrollview:WKChildScrollview

這個WKChildScrollview 是否是崩潰堆棧中的scrollview呢,如果我們能確定他的delegate是WKScrollingNodeScrollViewDelegate,那就說明這個WKChildScrollview 是崩潰堆棧中的scrollview。

為了驗證這個猜想,我們首先找到源碼,源碼并沒有太多,看不出其delegate類型。

@interface WKChildScrollView : UIScrollView @end@interface WKChildScrollView : UIScrollView @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             }        }}deinit {        if self.childScrollView != nil {            if self.childScrollView?.delegate != nil {                 self.childScrollView?.delegate = nil             }        }}
4.3 小程序同層渲染

想完了解決方案,那么WKChildScrollView 是做啥用的呢?

WKWebView 在內部采用的是分層的方式進行渲染,它會將 WebKit 內核生成的 Compositing Layer(合成層)渲染成 iOS 上的一個 WKCompositingView,這是一個客戶端原生的 View,不過可惜的是,內核一般會將多個 DOM 節點渲染到一個 Compositing Layer 上,因此合成層與 DOM 節點之間不存在一對一的映射關系。當把一個 DOM 節點的 CSS 屬性設置為overflow: scroll(低版本需同時設置-webkit-overflow-scrolling: touch)之后,WKWebView 會為其生成一個WKChildScrollView,與 DOM 節點存在映射關系,這是一個原生的UIScrollView的子類,也就是說 WebView 里的滾動實際上是由真正的原生滾動組件來承載的。WKWebView 這么做是為了可以讓 iOS 上的 WebView 滾動有更流暢的體驗。雖說WKChildScrollView也是原生組件,但 WebKit 內核已經處理了它與其他 DOM 節點之間的層級關系,這一特性可以用來做小程序的同層渲染。(「同層渲染」顧名思義則是指通過一定的技術手段把原生組件直接渲染到 WebView 層級上,此時「原生組件層」已經不存在,原生組件此時已被直接掛載到 WebView 節點上。你幾乎可以像使用非原生組件一樣去使用「同層渲染」的原生組件,比如使用view、image覆蓋原生組件、使用z-index指定原生組件的層級、把原生組件放置在scroll-view、swiper、movable-view等容器內等等)。

5、蘋果的修復方案

本著嚴謹的態度,我們想是什么導致了最開始的崩潰堆棧呢?是我們開發過程中的功能還是系統bug?如果是系統bug,其他公司也可能遇到,但是互聯網上搜不到其他公司或開發者討論崩潰相關信息。我們繼續看一下崩潰堆棧的top 函數RemoteScrollingTree::scrollingTreeNodeDidScroll() 源碼如下:

void RemoteScrollingTree::scrollingTreeNodeDidScroll(ScrollingTreeScrollingNode& node, ScrollingLayerPositionAction scrollingLayerPositionAction){    ASSERT(isMainRunLoop());    ScrollingTree::scrollingTreeNodeDidScroll(node, scrollingLayerPositionAction);    if (!m_scrollingCoordinatorProxy)        return;    std::optional layoutViewportOrigin;    if (is(node))        layoutViewportOrigin = downcast(node).layoutViewport().location();    m_scrollingCoordinatorProxy->scrollingTreeNodeDidScroll(node.scrollingNodeID(), node.currentScrollPosition(), layoutViewportOrigin, scrollingLayerPositionAction);}void RemoteScrollingTree::scrollingTreeNodeDidScroll(ScrollingTreeScrollingNode& node, ScrollingLayerPositionAction scrollingLayerPositionAction){    ASSERT(isMainRunLoop());    ScrollingTree::scrollingTreeNodeDidScroll(node, scrollingLayerPositionAction);    if (!m_scrollingCoordinatorProxy)        return;    std::optional layoutViewportOrigin;    if (is(node))        layoutViewportOrigin = downcast(node).layoutViewport().location();    m_scrollingCoordinatorProxy->scrollingTreeNodeDidScroll(node.scrollingNodeID(), node.currentScrollPosition(), layoutViewportOrigin, scrollingLayerPositionAction);}

崩潰在這個函數里,查看這個函數的commit記錄:

簡單描述一下就是scrollingTreeNodeDidScroll方法中使用的m_scrollingCoordinatorProxy 對象改成weak指針,并進行判空操作。這種改變,正是解決m_scrollingCoordinatorProxy 內存被釋放后還在訪問的方案。

這個commit是2023年2月28號提交的,commit log是:

[UI-side compositing] RemoteScrollingTree needs to hold a weak ref to the RemoteScrollingCoordinatorProxyhttps://bugs.webkit.org/show_bug.cgi?id=252963rdar://105949247Reviewed by Tim Horton.The scrolling thread can extend the lifetime of the RemoteScrollingTree via activity on that thread,so RemoteScrollingTree needs to hold a nullable reference to the RemoteScrollingCoordinatorProxy;use a WeakPtr.[UI-side compositing] RemoteScrollingTree needs to hold a weak ref to the RemoteScrollingCoordinatorProxyhttps://bugs.webkit.org/show_bug.cgi?id=252963rdar://105949247Reviewed by Tim Horton.The scrolling thread can extend the lifetime of the RemoteScrollingTree via activity on that thread,so RemoteScrollingTree needs to hold a nullable reference to the RemoteScrollingCoordinatorProxy;use a WeakPtr.

至此,我們基本確認,這個崩潰堆棧是webkit內部實現的一個bug,蘋果內部開發者最終使用弱引用的方式解決。

同時修復上線后,這個crash的崩潰量也降為0。

6、總結

本文中的crash從出現到解決歷時近一年,一開始根據線上日志判斷是h5 頁面返回 & h5 頁面滾動導致的問題,禁用手勢后雖然幾乎解決問題,但是線上還有零星crash上報,因此為了保證h5 離線功能的線上穩定性,需要完美解決問題。

本文的crash 似曾相識,但是經過驗證和閱讀源碼后發現并不是想象的那樣,繼續通過猜想+閱讀源碼的方式尋找到了崩潰堆棧中的真正scrollview代理對象,從而在app 側解決問題。最后發現是蘋果webkit的bug。

本文中的崩潰問題本質上是野指針問題,那么野指針問題定位有沒有通用的解決方案呢?

標簽:

推薦

財富更多》

動態更多》

熱點

成人伊人青草久久综合网_91在线视频免费观看_毛片免费视频网站_亚洲欧美日韩在线中文字幕

        成人app软件下载大全免费| 成人欧美一区二区三区黑人麻豆 | 国产精品美女一区二区| 亚洲va欧美va天堂v国产综合| 日韩一级完整毛片| 亚洲一区二区四区蜜桃| 粉嫩aⅴ一区二区三区四区五区| 亚洲视频一二三| 日韩视频中午一区| 亚洲成人先锋电影| 99re这里只有精品首页| 五月激情六月综合| 国产片一区二区三区| 蜜桃久久精品一区二区| 亚洲国产精品v| 91麻豆精品久久久久蜜臀| 一区二区三区在线看| 成人午夜激情视频| 亚洲va天堂va国产va久| 日本一区二区电影| 国产伦精品一区二区三区免费| 亚洲蜜臀av乱码久久精品| 精品精品国产高清一毛片一天堂| 天天综合网 天天综合色| 国产三级欧美三级日产三级99| 欧美日韩免费观看一区二区三区 | 一区二区国产盗摄色噜噜| 大尺度一区二区| 色综合一区二区| 日韩理论电影院| 99精品国产视频| 欧美色视频在线观看| 一区二区三区中文字幕在线观看| 久久综合狠狠综合久久综合88| 一区二区在线观看视频| 国产拍揄自揄精品视频麻豆| 国产一区啦啦啦在线观看| 亚洲在线免费播放| 国产精品国产三级国产aⅴ无密码 国产精品国产三级国产aⅴ原创 | 亚洲欧洲日产国产综合网| 国产**成人网毛片九色 | 日韩av电影免费观看高清完整版在线观看| 国产色爱av资源综合区| 日韩一区二区在线看| 人人狠狠综合久久亚洲| 日韩毛片精品高清免费| 久久免费视频一区| 国产精品99久久久久久有的能看| 午夜精彩视频在线观看不卡| 亚洲欧美日韩综合aⅴ视频| 99久久综合精品| 欧美日韩国产成人在线免费| 偷拍与自拍一区| 中文字幕欧美一| 国产日韩欧美精品一区| 成人国产电影网| 777a∨成人精品桃花网| 毛片一区二区三区| 五月综合激情日本mⅴ| 一区二区三区国产精华| 中文字幕乱码久久午夜不卡| xfplay精品久久| 丁香网亚洲国际| 欧美日韩国产免费一区二区| 日日夜夜精品免费视频| 亚洲综合999| 亚洲一区二区三区四区在线观看| 国产精品无圣光一区二区| 久久久99免费| 99re6这里只有精品视频在线观看| 在线播放视频一区| 国产综合色视频| 欧美三级日韩三级国产三级| 日本中文字幕不卡| 亚洲18色成人| 一二三四社区欧美黄| 国产精品护士白丝一区av| 久久精品视频免费观看| 精品av综合导航| 成人精品电影在线观看| 91精品婷婷国产综合久久性色| 韩国精品一区二区| 欧美日韩一区二区三区免费看| 麻豆中文一区二区| 在线欧美一区二区| 久久国内精品自在自线400部| 色爱区综合激月婷婷| 日本不卡视频在线| 欧日韩精品视频| 久久国产精品99久久久久久老狼 | 国产三级久久久| 2020国产精品自拍| 国产日韩高清在线| 中文字幕不卡的av| 亚洲色图视频网| 亚洲毛片av在线| 亚洲午夜电影在线观看| 亚洲一区二区高清| 青草av.久久免费一区| 欧美在线视频日韩| 国产一区日韩二区欧美三区| 91精品久久久久久久91蜜桃| 成人美女视频在线观看18| 26uuu另类欧美| 久久久亚洲高清| 国产精品盗摄一区二区三区| 中文字幕在线不卡| 亚洲成av人综合在线观看| 色天天综合色天天久久| 精品一区二区三区免费观看| 欧美二区乱c少妇| 欧美国产亚洲另类动漫| 欧美影院一区二区三区| 久久99精品久久只有精品| 欧美男人的天堂一二区| 成人免费毛片嘿嘿连载视频| 26uuu精品一区二区三区四区在线 26uuu精品一区二区在线观看 | 欧美经典一区二区| 国产精品初高中害羞小美女文| 一区二区视频免费在线观看| 香蕉影视欧美成人| 韩国成人福利片在线播放| 日韩手机在线导航| 国产欧美精品一区二区三区四区| 亚洲欧美精品午睡沙发| 亚洲激情网站免费观看| 日本不卡不码高清免费观看| 这里只有精品电影| 久久这里只有精品首页| 亚洲欧美区自拍先锋| 色综合久久综合| 国产成人精品免费在线| 国产欧美精品一区| 一区二区三区国产豹纹内裤在线| 蜜臀av一级做a爰片久久| 欧美一级日韩免费不卡| 国产午夜精品久久| 亚洲午夜在线电影| 欧美日韩高清在线播放| 2022国产精品视频| 亚洲综合色丁香婷婷六月图片| 欧美性xxxxx极品少妇| 99国产精品视频免费观看| 亚洲男同性视频| 91高清视频在线| 99久久精品国产一区| 亚洲视频一区二区在线观看| 色婷婷精品久久二区二区蜜臀av| 久久国产精品色| 久久久久久久久岛国免费| 亚洲三级在线观看| 精品影视av免费| 国产亚洲美州欧州综合国| 一区二区三区久久| 国产精品77777| 国产精品高潮呻吟久久| 色哟哟精品一区| 97久久精品人人做人人爽| 亚洲国产一区二区视频| 在线综合+亚洲+欧美中文字幕| 亚洲国产精品高清| 看国产成人h片视频| 久久久久久亚洲综合| 亚洲国产精品麻豆| 成人毛片视频在线观看| 亚洲一区二区三区三| 91精品国产综合久久久蜜臀粉嫩| 中文字幕在线不卡| 国产伦精品一区二区三区视频青涩| 国产精品理论在线观看| 欧美日韩亚洲综合一区二区三区| 久久精品网站免费观看| 奇米亚洲午夜久久精品| 国产欧美日本一区二区三区| 五月婷婷激情综合| 国产午夜精品一区二区三区视频| 日韩av中文字幕一区二区三区 | 肉色丝袜一区二区| 久久久久久亚洲综合影院红桃 | 国产精品污污网站在线观看| 日本高清不卡在线观看| 国产日韩v精品一区二区| 久久国产尿小便嘘嘘| 国产精品午夜春色av| 欧美日韩国产精选| 日韩美女精品在线| 粉嫩一区二区三区在线看| 亚洲线精品一区二区三区八戒| 精品国产1区2区3区| 亚洲va韩国va欧美va| 91国偷自产一区二区使用方法| av电影在线不卡| 视频一区在线视频| 国产日产精品一区| 欧美挠脚心视频网站| 亚洲欧美日韩国产手机在线| 成人精品免费网站| 日韩av电影天堂| 亚洲少妇30p| 精品成人一区二区三区|