999精品国内在线观看视频-日韩 中文字幕 在线视频-伊人久久中文字幕在线-少妇熟女激情一区二区三区-久久精品国产亚洲av水果派-91成人在线视频网-91久久久在线免费-国产综合一区二区三区-一区,二区av在线,亚洲天堂中文字幕91,欧美偷拍亚洲变态另类,国产精品久久久久久久hd

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

來源:得物技術(shù)
1、背景

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

2、前期排查

通過頁面瀏覽日志,發(fā)現(xiàn)發(fā)生崩潰時所在的頁面都是在h5 web容器內(nèi),且都是在頁面的生命周期方法viewDidDisappear方法調(diào)用后才發(fā)生崩潰,因此推測崩潰是在h5 頁面返回時發(fā)生的。


【資料圖】

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

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

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

3、不完美的解決方案

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

具體代碼如下(這樣在操作頁面?zhèn)然祷貢r,頁面的手勢被禁掉,不會再滾動):

@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    }

經(jīng)過測試,h5 web容器側(cè)滑時出現(xiàn)的tab頁面左右滾動的體驗問題確實被解決。這樣既可以解決體驗問題,又可以解決側(cè)滑離開頁面導(dǎo)致的崩潰問題,但是這樣并沒有定位crash的根因。修復(fù)代碼上線后,crash量確實下降,但是每天還是有一些crash出現(xiàn),且收到了個別頁面極端操作下偶現(xiàn)卡住的問題反饋。因此需要繼續(xù)排查crash根因,將crash根本解決掉。

繼續(xù)看文章開始的crash堆棧,通過Crash堆棧判斷崩潰原因是UIScrollview執(zhí)行滾動動畫過程中回調(diào)代理方法(見上圖)時訪問被釋放的內(nèi)存。常規(guī)解決思路是在退出頁面后,在頁面生命周期的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是如何實現(xiàn)的,因為我們猜想這個scrollview的delegate除了我們自己設(shè)置的,是否還有其他delegate(比如崩潰堆棧中的WKScrollingNodeScrollViewDelegate)。

通過對Webkit源碼一番研究,發(fā)現(xiàn)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 代理實現(xiàn)

首先看到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

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

- (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的作用是讓W(xué)KWebView 監(jiān)聽scrollview的滾動回調(diào),同時也可以讓開發(fā)者在外部監(jiān)聽WKWebView的scrollview回調(diào)。如何實現(xiàn)的呢?可以查看WKScrollViewDelegateForwarder的實現(xiàn)。

- (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()];}

通過復(fù)寫- (void)forwardInvocation:(NSInvocation *)anInvocation 方法,在消息轉(zhuǎn)發(fā)時實現(xiàn)的。

4.2 猜想 & 驗證

既然WKScrollingNodeScrollViewDelegate 不是WKScrollview的屬性,那說明崩潰堆棧中的scrollview不是WKScrollview,那頁面上還有其他scrollview么。我們看源碼WKScrollingNodeScrollViewDelegate 是在哪里設(shè)置的。

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的源碼,發(fā)現(xiàn)創(chuàng)建WKScrollingNodeScrollViewDelegate的位置只有一處。但是webkit的源碼太過于復(fù)雜,無法通過閱讀源碼的方式知道WKScrollingNodeScrollViewDelegate屬于哪個scrollview。

為此我們只能換一種思路,我們通過xcode調(diào)試的方式查看當(dāng)前webview加載的頁面是否還有其他scrollview。

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

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

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

@interface WKChildScrollView : UIScrollView @end@interface WKChildScrollView : UIScrollView @end

我們只能轉(zhuǎn)換思路在運行時找到WKWebView的類型為WKChildScrollView的子view(通過OC runtime & 視圖樹遍歷的方式),判斷他的delegate是否為WKScrollingNodeScrollViewDelegate 。

我們運行時找到類型為 WKChildScrollView 的子view后,獲取其delegate類型,確實是WKScrollingNodeScrollViewDelegate。至此我們找到了崩潰堆棧中的scrollview。

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

5、蘋果的修復(fù)方案

本著嚴(yán)謹(jǐn)?shù)膽B(tài)度,我們想是什么導(dǎo)致了最開始的崩潰堆棧呢?是我們開發(fā)過程中的功能還是系統(tǒng)bug?如果是系統(tǒng)bug,其他公司也可能遇到,但是互聯(lián)網(wǎng)上搜不到其他公司或開發(fā)者討論崩潰相關(guān)信息。我們繼續(xù)看一下崩潰堆棧的top 函數(shù)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);}

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

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

這個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.

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

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

6、總結(jié)

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

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

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

標(biāo)簽:

推薦

財富更多》

動態(tài)更多》

熱點

日韩熟女在线播放电影-亚洲精品国自产在线-91精品 粉嫩 久久-一本一道久久综合狠狠老_ | 91麻豆蜜桃在线观看-2021年精品国产福利在线-丁香六月五月婷婷综合-尤物精品在线一区 | 人人妻久久人人澡人人爽人人精品-av一区二区三区人妻少妇-久久精品国产亚洲av黄瓜-色婷婷在线观看视频免费观看视频观看 | 欧美另类a v 一区二区-亚洲熟女av综合一区二区-黑人社长与人妻秘书-色综合亚洲一区二区小说性色aⅴ | 欧美美足美脚一区二区-激情丁香婷婷久久-91福利区试看体验区-国产又粗又细又黄视频 | 日本激情床震无遮掩视频-中文字幕人妻一区二区三区8-久久久久一区三区-麻豆人妻换人妻董小宛 | 国产精品久久久久精品香蕉剃毛-五月天丁香花婷婷亚洲-福利一区二区三区高清视频-色呦呦网站精品国产 | 人人妻人人澡人人爽久久av网站-av中文字幕网一区-日韩欧美一区二区三区国产-麻豆91精品91久久 | 亚洲乱色熟女一区二区三区四区-日本japanese极品少妇-国产三级久久久精品-精品久久99在线观看 | 日韩午夜av网站-久久视频老女人-91精品国产免费久久综合-欧美久久老太婆逼逼 | 亚洲图片日韩视频一区二区-久久久99久久这里只有精品-免费成人看大片-久久久久亚洲美女啪啪 | 人妻丰满熟妇av一区二区-free欧美性黑人极品hd-国产av一区二区三区,com-黑人玩弄人妻一区二区三区a | 97久久香蕉国产熟女线看-日韩成人av在线二区-久久主人久久综合久久综合艹尼玛-10国产精品久久久久久 | 999精品视频在线免费观看-熟女人妻久久久一区二区蜜桃-蜜臀久久精品久久久久久久久久-欧美日韩极品一区二区三区 | av熟女一区二区三区蜜桃-亚洲中文字幕精品人妻-日产精品久久久久久久蜜桃-久久久久久国产少妇熟女 | √天堂亚洲av国产av在线-日韩啪啪啪啪啪亚洲-日韩精品亚洲人成在线-亚洲91在线一区二区 | 日韩人妻一区二区三区在线88-国产欧美精品第一页-欧美黑人性猛交xxxxx-日韩欧美片免费观看 | 日本少妇人妻久久久-蜜桃视频在线一区二区三区-亚洲精品色视频在线-亚洲人成伊人成综合网99精品 激情综合色综合久久丁香-精品久久久久女人18-av久久伊人精品中文字幕-久久久久人妻一区二区三区vr2 | 精品一区二区三区人妻视频-一本色道久久婷婷日韩-欧美亚洲另类一区二区三区-日本巨乳中文字幕 | 国产精品国产三级国产在线专50-51精产国品久久一二三a区蜜桃-欧美日韩深喉视频在线-日韩av手机在线不卡 | 激情五月婷婷丁香色-黑人和人妻在线观看-六月丁香六月婷婷综合-国产综合日韩精品视频 | 久久亚洲私人国产精品99-欧美日韩一区二区三区-av开心六月色婷婷-欧美日韩亚洲一卡二卡三卡四卡不卡 | 亚洲欧美综合777-国产成人精品av久久-91大神精品视频在线播放-国产av日韩最新1区2区3区 | 亚洲成人av夜夜骚-精品伦一区二区三区视频-日韩欧美p片内射在线观看视频-九九99久久精品在免费线 | 天天射天天交天天干-久久精品av中文字幕-18禁国产在线一区观看-日韩欧美xxxx大片 | 欧美日韩男女视频在线观看-日韩黄色一级免费电影-国产麻豆精品在线-激情综合激情五月综合 | 亚洲国产欧美在线人成aⅴ-国色天香精品二区三区-久日视频在线观看免费-91精品久久久久精品一区 | 久久99国产综合精品女人av-久久99国产精品久久99果冻传媒-久久久999com-日韩激情自拍偷拍 | 和黑人老外交换娇妻-日韩精品网址你懂的-成人免费av大片茄子-欧美精品麻豆久久 | 久久久久久影视精品-国产亚洲精品91av久久久-久久久久久不卡久久99精品-国产av日韩av欧美爽爽爽蜜柚 | 五月激情婷婷俺也去-av男人的天堂久久-久久人人爽人人爽人人片av东京热-欧美日韩一级成人免费 | 少妇中文字幕伦理-青青在线精品2018国产-欧美日韩另类人妻中字-国产又大又硬又粗又长又黄视频 | 伊人久久少妇人妻-少妇高潮喷水视频在线观看-巨乳熟女 日本视频-美欧日韩一区二区三区视频 | 国内88av福利在线-特黄特黄特刺激免费播放观看-av中文字幕国产在线观看-2025精品国产高清在线 | av成人中文字幕在线观看-久视频中文字幕免费在线精品-国产av系列一区二区-国产一区二区蜜臀大屁股 | 成人精品一区二区男人看-亚洲天堂熟女的天堂-9999国产精品欧美久久久久久-精产久久久国品一二三产区区别 | 久久久久精精精精品-日韩av一卡在线观看-日韩feex精品视频在线观看-国产综合开心激情五月 | 欧美激情区日韩一区二区三区-日韩欧美区一区二区三-久久嗯啊不要视频-欧美黑人巨大黑人猛交中文 欧美色欧美亚洲另类二区不卡-国产免费自拍色视频-日本妻子中文字幕-人妻精品久久久久中文字幕一 | 丁香九月激情婷婷中文字幕-中文字幕在线播放麻豆-亚洲乳大丰满中文字幕少妇av-日韩超级大片中文字幕 | 黑人玩弄人妻一区二区三区视频-成人精品在线观看91-久久精品一区二区三区四区五区-成人午夜国产电影在线观看 | 国产精品系列免费看-精品日韩中文字幕在线-欧美日韩精品国产综合-欧美日韩国产一区二区在线 |