日韩成人免费在线_国产成人一二_精品国产免费人成电影在线观..._日本一区二区三区久久久久久久久不

當前位置:首頁 > 科技  > 軟件

西瓜視頻RenderThread引起的閃退問題攻堅歷程

來源: 責編: 時間:2024-01-03 17:21:41 214觀看
導讀背景影響西瓜之前存在過一類RenderThread閃退,從堆棧上看,全部都是系統so調用,給人的第一印象像是一個系統bug,無從下手。閃退集中在Android 5~6上,表現為打開直播間立即閃退。該問題在2022年占據Native Crash Top5,2023年

I8628資訊網——每日最新資訊28at.com

背景

影響

西瓜之前存在過一類RenderThread閃退,從堆棧上看,全部都是系統so調用,給人的第一印象像是一個系統bug,無從下手。閃退集中在Android 5~6上,表現為打開直播間立即閃退。該問題在2022年占據Native Crash Top5,2023年更是上升到到Top1。因此有必要投入時間和精力再重新審視一下這個問題。在歷經多周的源碼分析和排查后,逐步明確了問題根因并修復,最終取得了顯著的穩定性收益和業務收益。I8628資訊網——每日最新資訊28at.com

接下來,我們將抽絲剝繭,一步步深入分析這個歷史遺留問題,揭開它背后真正的原因。I8628資訊網——每日最新資訊28at.com

基本信息

具體堆棧如下:I8628資訊網——每日最新資訊28at.com

圖片圖片I8628資訊網——每日最新資訊28at.com

堆棧都是系統的so調用,不能明確具體閃退業務場景,只能看出是RenderThread線程主動abort了。I8628資訊網——每日最新資訊28at.com

根據abort message找到對應的abort代碼,在CanvasContext::requireSurface時閃退了,代碼如下:I8628資訊網——每日最新資訊28at.com

圖片圖片I8628資訊網——每日最新資訊28at.com

問題特征:I8628資訊網——每日最新資訊28at.com

問題集中在Android 5.0~6.0,線程集中在RenderThread,無明顯機型、廠商特征。I8628資訊網——每日最新資訊28at.com

RenderThread簡介

為了便于理解下面的分析過程,先對RenderThread簡單的介紹。順便看一下是怎么調用到CanvasContext::requireSurface的。I8628資訊網——每日最新資訊28at.com

相關類圖如下:I8628資訊網——每日最新資訊28at.com

圖片圖片I8628資訊網——每日最新資訊28at.com

相關源碼:frameworks/base/libs/hwui/renderthreadI8628資訊網——每日最新資訊28at.com

RenderThread::threadLoopI8628資訊網——每日最新資訊28at.com

RenderThread繼承自Thread和Singleton,是一個單例模式的線程,通過RenderThread.getInstance()獲取。和主線程很像,內部是一個通過for實現的無限循環,不斷從TaskQueue里通過nextTask函數獲取RenderTask并執行,RenderTask執行完后會按需調用requestVsync。核心代碼在threadLoop函數中:I8628資訊網——每日最新資訊28at.com

圖片圖片I8628資訊網——每日最新資訊28at.com

ThreadedRenderI8628資訊網——每日最新資訊28at.com

Java層通過ThreadedRender與RenderThread進行通信。當Window啟用硬件加速時,ViewRootImpl會通過HardwareRenderer.create()創建一個ThreadedRender實例。ThreadedRender在創建時,會調用nCreateProxy在native層創建一個RenderProxy。ThreadedRender通過RenderProxy向RenderThread提交任務。I8628資訊網——每日最新資訊28at.com

圖片圖片I8628資訊網——每日最新資訊28at.com

RenderProxyI8628資訊網——每日最新資訊28at.com

RenderProxy在創建時,會同步創建一個CanvasContext,再通過RenderThread.getInstance()拿到RenderThread實例。RenderProxy通過CREATE_BRIDGE定義了許多Bridge函數,再通過SETUP_TASK把這些Bridge函數包裝成RenderTask,再通過postAndWait提交給RenderThread調用。postAndWait之后,當前線程進入等待狀態,當對應的task執行完畢之后喚醒當前線程。以RenderProxy::createTextureLayer為例:I8628資訊網——每日最新資訊28at.com

圖片圖片I8628資訊網——每日最新資訊28at.com

圖片圖片I8628資訊網——每日最新資訊28at.com

CanvasContextI8628資訊網——每日最新資訊28at.com

RenderProxy把任務提交給RenderThread之后,執行的實際上是CanvasContext::createTextureLayer,就是在這里調用了requireSurface。I8628資訊網——每日最新資訊28at.com

圖片圖片I8628資訊網——每日最新資訊28at.com

初步猜想

其他 App 相似問題修復I8628資訊網——每日最新資訊28at.com

其他端App也曾有過'requireSurface() called but no surface set! '相關閃退。原因是:在Activity進行側滑退出時,側滑框架需要強制對下層Activity進行繪制生成Bitmap,再用這個Bitmap來實現Activity的切換效果。但由于下層Activity此前已處于不可見狀態,可能有業務層主動釋放了下層Activity中的TextureView,導致了no surface set的閃退。經過對西瓜的側滑框架的源碼分析,發現不會產生此類問題,因此西瓜的問題應該另有其因。I8628資訊網——每日最新資訊28at.com

正面分析西瓜問題I8628資訊網——每日最新資訊28at.com

問題的條件是mEglSurface == EGL_NO_SURFACE,看下mEglSurface賦值為EGL_NO_SURFACE的時機。I8628資訊網——每日最新資訊28at.com

總共有兩處:I8628資訊網——每日最新資訊28at.com

第一處:CanvasContext::setSurfaceI8628資訊網——每日最新資訊28at.com

這里總共兩處mEglSurface賦值操作。一處直接賦值為EGL_NO_SURFACE,另一處為mEglManager.createSurface的返回值。而mEglManager.createSurface在返回前判斷如果是EGL_NO_SURFACE會主動abort,顯然createSurface的返回值一定不是EGL_NO_SURFACE。I8628資訊網——每日最新資訊28at.com

void CanvasContext::setSurface(ANativeWindow* window) {    if (mEglSurface != EGL_NO_SURFACE) {        mEglSurface = EGL_NO_SURFACE;    }    if (window) {//不可能返回EGL_NO_SURFACE        mEglSurface = mEglManager.createSurface(window);    }}EGLSurface EglManager::createSurface(EGLNativeWindowType window) {    EGLSurface surface = eglCreateWindowSurface(mEglDisplay, mEglConfig, window, nullptr);    LOG_ALWAYS_FATAL_IF(surface == EGL_NO_SURFACE,"Failed to create EGLSurface for window %p, eglErr = %s",(void*) window, egl_error_str());    return surface;}

那么這里根據window是否為nullptr又可以分為兩種情況:I8628資訊網——每日最新資訊28at.com

  1. setSurface(nullptr)之后,mEglSurface 最終賦值為 EGL_NO_SURFACE,之后調用requireSurface發生abort。
  2. setSurface(window),第5行會先設置為EGL_NO_SURFACE,在第10行createSurface返回之前,此時在另外一個線程調用requireSurface也會發生abort。

第二處:初始值I8628資訊網——每日最新資訊28at.com

初始值為EGL_NO_SURFACE。只有調用CanvasContext::setSurface時,mEglSurface 才會被賦值,在此之前,調用了requireSurface也會引發閃退。I8628資訊網——每日最新資訊28at.com

class CanvasContext : public IFrameCallback {    private:    EGLSurface mEglSurface = EGL_NO_SURFACE;}

總結下來,有三個時機調用requireSurface會導致閃退:I8628資訊網——每日最新資訊28at.com

  1. 多線程并發,mEglSurface短暫為EGL_NO_SURFACE
  2. CanvasContext::setSurface(nullptr)之后,即mEglSurface被銷毀
  3. CanvasContext::setSurface之前,即mEglSurface未初始化

深入分析

7.0+系統是如何避免這個問題的?

從多維信息看出,問題在6.0及以下版本發生。那么7.0上系統做了哪些優化,是如何規避我們上面三種可能的情況的?這些優化思路對我們解決問題能否提供幫助?I8628資訊網——每日最新資訊28at.com

對比6.0和7.0代碼之后,發現谷歌直接把requireSurface這個方法移除了!I8628資訊網——每日最新資訊28at.com

逐個翻看6.0~7.0上RenderThread相關的commit,最終找到了這個commit(8afcc769)。這里確實是把requireSurface刪除了,并在createTextureLayer中調用了一下 mEglManager.initialize()。而EglManager::initialize里的實現,是執行下EglManager的初始化,這里跟6.0基本一致。I8628資訊網——每日最新資訊28at.com

圖片圖片I8628資訊網——每日最新資訊28at.com

圖片圖片I8628資訊網——每日最新資訊28at.com

圖片圖片I8628資訊網——每日最新資訊28at.com

那6.0上的abort原來是可以直接刪掉的嗎?如果是這樣,我們是不是可以有樣學樣,嘗試hook requireSurface,把abort去掉,再主動調用一下mEglManager.initialize,從而達到這個commit相似的修復效果?I8628資訊網——每日最新資訊28at.com

在云真機上找了個6.0的設備,把libhwui.so pull下來,通過 readelf -sW libhwui.so查看requireSurface的符號。發現,沒有requireSurface的符號。解開so后發現,requireSurface被inline進了createTextureLayer:I8628資訊網——每日最新資訊28at.com

圖片圖片I8628資訊網——每日最新資訊28at.com

再嘗試一下hook requireSurface的上一層調用CanvasContext::createTextureLayer,發現也沒有對應的符號,只有RenderProxy::createTextureLayer 的符號。如果采用7.0的修復方案,需要修改的指令非常多。而且,這個MR中還有其他改動,要不要一起hook?這些問題暫時還沒搞清楚,花大力氣去做的話風險太高。I8628資訊網——每日最新資訊28at.com

圖片圖片I8628資訊網——每日最新資訊28at.com

看起來,參考7.0修復方案操作難度大,并且不確定是否有效。我們先把它作為備選方案,另謀出路!I8628資訊網——每日最新資訊28at.com

正面分析三種可能性

多線程問題?

CanvasContext的createTextureLayer和setSurface相關代碼,都被RenderProxy轉移到了RenderThread上執行,而RenderThread又是單例的,所以這里不存在多線程問題,因此可以直接排除線程并發問題I8628資訊網——每日最新資訊28at.com

setSurface(null)導致?

前面分析過,setSurface(nullptr)也會導致EGL_NO_SURFACE。下面是ThreadedRender一個大概的調用時序圖,把幾個可能產生EGL_NO_SURFACE的setSurface調用做了標記,序號24就是的閃退函數requireSurface。時序圖:I8628資訊網——每日最新資訊28at.com

圖片圖片I8628資訊網——每日最新資訊28at.com

可以看出在CanvasContext中,setSurface的調用有:initialize、swapBuffers、makeCurrent、updateSurface、destory、~ConvasContext。對應的java層調用為ThreadedRender中的的initialize、draw、createTextureLayer、updateSurface、destory、finalize方法。排除一些不會出現異常的方法:I8628資訊網——每日最新資訊28at.com

排除ThreadedRender.initializeI8628資訊網——每日最新資訊28at.com

initialize時java層傳過來的surface做了判斷保護,可以確保surface不為nullptr,因此可以排除initialize,代碼如下:I8628資訊網——每日最新資訊28at.com

圖片圖片I8628資訊網——每日最新資訊28at.com

排除ThreadedRender.drawI8628資訊網——每日最新資訊28at.com

draw對應的問題是swapBuffers失敗。發生在swapBuffers失敗時,也就是eglSwapBuffers錯誤了。I8628資訊網——每日最新資訊28at.com

圖片圖片I8628資訊網——每日最新資訊28at.com

系統有以下兩種方式處置錯誤:I8628資訊網——每日最新資訊28at.com

  1. EGL_BAD_SURFACE:打印失敗日志。但翻看多個跟蹤日志,均沒有發現類似日志,暫時不關注。
  2. 其他EGL錯誤:直接abort掉,此時變成另外一個閃退,因此可以排除。

綜上,對于swapBuffers失敗這種情況可能存在,但未發現相關報錯日志,暫時不作過多關注。相關代碼如下:I8628資訊網——每日最新資訊28at.com

圖片圖片I8628資訊網——每日最新資訊28at.com

排除ThreadedRender.finalizeI8628資訊網——每日最新資訊28at.com

ThreadedRender.finalize之后,會在native層通過delete 釋放RenderProxy和ConvasContext,在~ConvasContext析構時調用setSurface(nullptr)。因此,如果之后再調用requireSurface,應該會發生SIGSEGV相關錯誤,不可能出現surface not set異常。因此,也可以排除掉。代碼如下:I8628資訊網——每日最新資訊28at.com

圖片圖片I8628資訊網——每日最新資訊28at.com

圖片圖片I8628資訊網——每日最新資訊28at.com

圖片圖片I8628資訊網——每日最新資訊28at.com

剩余情況I8628資訊網——每日最新資訊28at.com

排除了intialize、swapBuffers、finalize之后,還剩下makeCurrent失敗、updateSurface(nullptr)、destory都有可能產生問題,上游調用比較多追蹤起來依然比較困難,暫時無法排除。I8628資訊網——每日最新資訊28at.com

初始化前觸發了requireSurface?

一般來說,setSurface的首次調用是在initialize中。那么,如果在initialize之前就調用了requireSurface,是不是就會出問題呢?從前面的分析可以看出,requireSurface的上游是java層的createTextureLayer,而createTextureLayer的調用處只有一個,在TextureView的getHardwareLayer中。I8628資訊網——每日最新資訊28at.com

圖片圖片I8628資訊網——每日最新資訊28at.com

getHardwareLayer是View的一個方法,默認返回null。從注釋上也能看出,在6.0上只有TextureView用到了這個方法,調用處也只有移除在getBitmap中。在7.0上也是直接把getHardwareLayer從View中移除了,變成TextureView的一個方法。而getBitmap是個public方法,這里是可以被app調用到的。I8628資訊網——每日最新資訊28at.com

圖片圖片I8628資訊網——每日最新資訊28at.com

圖片圖片I8628資訊網——每日最新資訊28at.com

閃退的前提條件:I8628資訊網——每日最新資訊28at.com

  1. ThreadedRender.initialize還未調用
  2. ThreadedRender.destroy或者ThreadedRender.updateSurface(null)之后

在Java層觸發requireSurface步驟如下:I8628資訊網——每日最新資訊28at.com

  • TextureView.getBitmap => ThreadedRender.createTextureLayer => ConvasContext::requireSurface

最終定位

驗證分析結論

通過前面的分析,找到了問題的前提條件,并發現了一條觸發requireSurface的方式。那么,就可以結束紙上談兵,通過實操來在本地復現這個閃退,來實錘前面的結論。I8628資訊網——每日最新資訊28at.com

ThreadedRender.initialize還未調用I8628資訊網——每日最新資訊28at.com

由于ThreadedRenderer不是公開api,需要通過反射來創建實例。拿到實例后不調用其intialize方法,直接反射調用createTextureLayer。代碼如下:I8628資訊網——每日最新資訊28at.com

圖片圖片I8628資訊網——每日最新資訊28at.com

果然,復現了'requireSurface() called but no surface set!' 這個問題:I8628資訊網——每日最新資訊28at.com

圖片圖片I8628資訊網——每日最新資訊28at.com

destroy或updateSurface(null)I8628資訊網——每日最新資訊28at.com

通過反射創建ThreadedRender實例,先執行ThreadedRender.intialize,之后調用destroy或updateSurface清空surface,最后調用createTextureLayer,也成功復現了這個閃退。I8628資訊網——每日最新資訊28at.com

業務場景定位

前面的復現,只是從技術層面確認了問題發生的幾種可能,但還沒有與業務場景關聯起來。真實的問題是否在前面提到的這幾種可能中間?如果在的話,那具體的調用點在哪,又該如何修復?I8628資訊網——每日最新資訊28at.com

嘗試在真實場景中復現I8628資訊網——每日最新資訊28at.com

通過shaddow hook RenderProxy的Initialize、destroy、updateSurface、createTextureLayer等函數,在hook函數中打印一些日志。由于RenderProxy可能存在多個實例,需要在日志里加上RenderProxy實例的地址來方便追蹤單個RenderProxy調用時序。I8628資訊網——每日最新資訊28at.com

hook函數如下:I8628資訊網——每日最新資訊28at.com

圖片圖片I8628資訊網——每日最新資訊28at.com

盡管這個問題在android 6上閃退率比較高,但我用6.0測試機跑自動化測試,還是沒有復現這個問題。I8628資訊網——每日最新資訊28at.com

線上定位

問題不是必現的,排查進程在線下難以為繼。而只有在真實的業務場景中復現,才能明確問題根因,找到最佳修復方案。因此,需要加把這些hook點上線進一步排查。上線無小事,線下可以小步快走,逐漸定位問題。但上線步子一定要穩,不能邁太大。但也不能太小,否則周期會拉的很長。所以,既要保證有足夠的信息排查,也要盡可能的降低穩定性和性能影響。I8628資訊網——每日最新資訊28at.com

明確業務堆棧I8628資訊網——每日最新資訊28at.com

前面的hook方案只能確認真實業務場景是否也有調用時序問題,并不清楚具體的業務調用堆棧。I8628資訊網——每日最新資訊28at.com

從業務上層代碼到異常點,必然經過了ThreadedRender的Initialize、destroy、update、createTexture等方法,那么通過java hook把這些方法hook住,并打印堆棧應該就定位到業務代碼。需要注意的是:I8628資訊網——每日最新資訊28at.com

  1. 穩定性問題

Java hook 目前主流的方案中還沒有能達到線上大規模使用的水平,只能小流量觀察。I8628資訊網——每日最新資訊28at.com

保障方案:系統版本限制在6.0,放量計劃1%->5%->10%,小流量觀察。I8628資訊網——每日最新資訊28at.com

  1. 性能問題

由于這些api調用頻率可能很高,也都在主線程,直接打印堆棧會影響性能。真正需要關注的就是異常前的幾次調用,且有些case可以通過以下條件預判,其余的堆棧都不必要甚至是干擾信息。需要關注的堆棧如下:I8628資訊網——每日最新資訊28at.com

  1. initialize:不需要堆棧。只要知道有沒有調用過,打印一行日志即可。
  2. destroy:必須全部打印堆棧。發生在異常前,無法預判過濾。
  3. updateSurface:surface==null時打印堆棧。surface不為null不會導致異常。
  4. createTextureLayer:未初始化或者surface==null時打印堆棧。只有這兩種可能會有異常。

總結:可以通過surface是否為null、initialize是否調用過這兩個條件減少stackTrace。I8628資訊網——每日最新資訊28at.com

在ThreadedRender中,surface都被透傳給了native層,沒有對應的Java引用,需要手動維護一個java 層的實例。初始化狀態可以通過反射ThreadedRenderer.mInitialized拿到,不過既然已經hook intialize和destroy了,這里也選擇手動維護一個初始化狀態,畢竟可以減少一次反射調用。I8628資訊網——每日最新資訊28at.com

public class ThreadedRenderer extends HardwareRenderer {    private boolean mInitialized = false;    @Override    void updateSurface(Surface surface) throws OutOfResourcesException {        updateEnabledState(surface);//透傳給了Native層,Java層沒有引用        nUpdateSurface(mNativeProxy, surface);    }}

Java hook偽代碼如下:I8628資訊網——每日最新資訊28at.com

public static class ExtraInfo {    private boolean isSurfaceNull = true;    private boolean mInitialized = false;}static Map<Object, ExtraInfo> infoMap = new ConcurrentHashMap<>();private static ExtraInfo extraInfo(Object instance) {    ExtraInfo threadedRendererInfo = infoMap.get(instance);    if (threadedRendererInfo == null) {        threadedRendererInfo = new ExtraInfo();        infoMap.put(instance, threadedRendererInfo);    }    return threadedRendererInfo;}public static boolean initializedHook(Object instance,Surface surface) {    extraInfo(instance).mInitialized = true;    return (Boolean) Hubble.callOrigin(initializeHookEntry, instance, surface);}public static void destroyHook(Object instance) {    infoMap.remove(instance);    Log.d("REPAIR", "destroy", new Throwable());    Hubble.callOrigin(destroyHookEntry, instance);}public static void updateSurfaceHook(Object instance, Surface surface) {    extraInfo(instance).isSurfaceNull = surface == null;    if (surface == null) {        Log.d("REPAIR", "updateSurface null ", new Throwable());    }    Hubble.callOrigin(destroyHookEntry, instance);}public static void createTextureLayerHook(Object instance) {    ExtraInfo extraInfo = extraInfo(instance);    if (extraInfo.mInitialized || extraInfo.isSurfaceNull) {        Log.d("REPAIR", "createTextureLayer null ", new Throwable());    }    return Hubble.callOrigin(createTextureHookEntry, instance);}

線上日志

上線后成功采集到了關鍵的Java調用堆棧!基本都集中在直播業務場景下,初始化前調用requireSurface。也有一些零星的destroy之后requireSurface的case,由于量級太小本文不做重點討論。I8628資訊網——每日最新資訊28at.com

ThreadedRender.Initialize之前I8628資訊網——每日最新資訊28at.com

日志截圖如下:I8628資訊網——每日最新資訊28at.com

圖片圖片I8628資訊網——每日最新資訊28at.com

調用時序問題確認:I8628資訊網——每日最新資訊28at.com

對于地址為0x814a0b0的RenderProxy實例,沒有它intialize相關調用日志,只有一條createTextureLayer調用日志。可以明確,這個RenderProxy實例是在initialize之前調用createTextureLayer導致閃退!I8628資訊網——每日最新資訊28at.com

Java 堆棧分析:I8628資訊網——每日最新資訊28at.com

Log.d會對過長的堆棧進行截取,FrameLayout.onMeasure之前的都被截取了,不過對于排查問題,影響不大。I8628資訊網——每日最新資訊28at.com

堆棧關鍵信息整理如下:I8628資訊網——每日最新資訊28at.com

  1. 閃退時正在執行onMeasure
  2. 在com.bytedance.android.livesdk.chatroom.ui.LivePlayerWidget.loadSharedPlayer中調用了TextureView的getBitmap方法,再到ThreadedRender.creatTextureLayer,之后就發生了Native crash。

為什么沒有intialize?

onMeasure會早于ThreadedRender.initialize執行嗎?I8628資訊網——每日最新資訊28at.com

ThreadedRender.initialize和performMeasure相關的代碼都在performTraversals中,再次回到源碼中去分析。從代碼結構來看,initialize前后都有measure相關操作。initialize之前通過measureHierarchy調用了performMeasure,initialize之后是直接調用performMeasure。由于measureHierarchy外部包了許多判斷條件,所以不能直接從代碼行的上下關系,得出measure早于initialize的結論,但我們可以保持這個懷疑進一步驗證。I8628資訊網——每日最新資訊28at.com

這個方法過于巨大,移除無關代碼后如下:I8628資訊網——每日最新資訊28at.com

private void performTraversals() {    if (mFirst) {        mLayoutRequested = true;    }    boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);    if (layoutRequested) {        windowSizeMayChange |= measureHierarchy(host, lp, res, desiredWindowWidth, desiredWindowHeight);    }    if (mApplyInsetsRequested) {        if (mLayoutRequested) {            windowSizeMayChange |= measureHierarchy(host, lp, mView.getContext().getResources(), desiredWindowWidth, desiredWindowHeight);        }    }    if (mFirst || windowShouldResize || insetsChanged || viewVisibilityChanged || params != null) {        if (!hadSurface) {            if (mSurface.isValid()) {                if (mAttachInfo.mHardwareRenderer != null) {                    hwInitialized = mAttachInfo.mHardwareRenderer.initialize(mSurface);                }            }        }        if (!mStopped || mReportNextDraw) {            if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight() || contentInsetsChanged) {                performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);            }        }    }}private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,                                 final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {    boolean goodMeasure = false;    if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {        if (baseSize != 0 && desiredWindowWidth > baseSize) {            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);            if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {                goodMeasure = true;            } else {                performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);                if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {                    goodMeasure = true;                }            }        }    }    if (!goodMeasure) {        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);    }    return windowSizeMayChange;}

由于堆棧被裁剪了,無法確認異常是從哪個分支過來的。不過沒關系,注意到當mFirst=true時,滿足layoutRequested = true,會先調用執行measureHierarchy,可以在本地模擬mFirst=true這種情況,即可驗證。I8628資訊網——每日最新資訊28at.com

本地通過onMeasure復現I8628資訊網——每日最新資訊28at.com

本地寫個demo,在FrameLayout.onMeasure中立即調用TextureView.getBitmap,并通過反射查看mFirst的值,找個6.0的云真機驗證一下。onMeasure會連續執行多次,只有第一次的mFirst為true,但沒能復現問題,代碼如下:I8628資訊網——每日最新資訊28at.com

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    boolean first=reflectFirst();//反射獲取mFirst    activity.log("mFirst=" + first);    Bitmap bitmap = mTextureView.getBitmap(mBitmap);    super.onMeasure(widthMeasureSpec, heightMeasureSpec);}

再來看下mTextureView.getBitmap的實現,I8628資訊網——每日最新資訊28at.com

public Bitmap getBitmap(Bitmap bitmap) {        if (bitmap != null && isAvailable()) {            if (mLayer == null && mUpdateSurface) {                getHardwareLayer();            }        }        return bitmap;    }    public boolean isAvailable() {        return mSurface != null;    }        public void setSurfaceTexture(@NonNull SurfaceTexture surfaceTexture) {        mSurface = surfaceTexture;    }

可以看到,要想執行到ThreadedRender.createTextureLayer還需要滿足以:isAvailable()為true,手動調用一下TextureView.setSurfaceTexture就可以滿足。I8628資訊網——每日最新資訊28at.com

根據猜想,再次編寫代碼終于復現成功! demo如下:I8628資訊網——每日最新資訊28at.com

mTextureView.setSurfaceTexture(new SurfaceTexture(0));protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    boolean first=reflectFirst();    activity.log("mFirst=" + first);;    mTextureView.getBitmap(mBitmap);    super.onMeasure(widthMeasureSpec, heightMeasureSpec);}

嘗試只在mFisrt=false時執行getBitmap,再次運行,不崩了。可見異常的關鍵條件就是mFirst!I8628資訊網——每日最新資訊28at.com

if (!first) { //沒問題    mTextureView.getBitmap(mBitmap); }

問題根因

梳理下整體流程。ViewRootImpl首次performTraversals時(mFirst=true),onMeasure會早于ThreadedRenderer.initialize。而業務方在onMeasure中又調用了TextureView.getBitmap,最終在native層會調用CanvasContext::requreSurface。由于還沒有執行過CanvasContext::initialize,當前mEglSurface為EGL_NO_SURFACE,于是在Android5~6上觸發了abort,發生surface not set的異常。I8628資訊網——每日最新資訊28at.com

總結起來:在android6.0上,ViewRootImpl首次performTraversals時,如過在onMeasure中調用了TextureView.getBitmap,就可能會發生這個異常。I8628資訊網——每日最新資訊28at.com

線上還存在一些零星的destroy之后requireSurface、swapBuffers失敗后requireSurface的異常,由于排查思路大同小異,這里就不展開說了。I8628資訊網——每日最新資訊28at.com

修復方案

通過字節碼插樁全局替換TextureView.getBitmap方法,當ViewRootImpl.mFirst=true時,就返回默認值而不執行getBitmap原有邏輯,這樣就不會調用到ThreadedRender.createTextureLayer。I8628資訊網——每日最新資訊28at.com

但由于mFirst只能通過反射獲取,這可能會影響performTraversals性能,有沒有性能更好的方案?I8628資訊網——每日最新資訊28at.com

通過代碼分析,發現performTraversals會經歷layout階段,而layout之后View會增加一個PFLAG3_IS_LAID_OUT:I8628資訊網——每日最新資訊28at.com

/*** Flag indicating that the view has been through at least one layout since it* was last attached to a window.*/static final int PFLAG3_IS_LAID_OUT = 0x4;public void layout(int l, int t, int r, int b) {      mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;}public boolean isLaidOut() {    return (mPrivateFlags3 & PFLAG3_IS_LAID_OUT) == PFLAG3_IS_LAID_OUT;}

因此,可以通過isLaidOut ()獲取到這一個屬性,達到和mFirst基本一致的效果。I8628資訊網——每日最新資訊28at.com

最終方案:I8628資訊網——每日最新資訊28at.com

插裝替換全局TextureView.getBitmap調用,增加textureView.isLaidOut()判斷。I8628資訊網——每日最新資訊28at.com

public static boolean isGetBitmapSafe(TextureView textureView) {    return Build.VERSION.SDK_INT > 23 || textureView.isLaidOut() || !AppSettings.inst().mFerretSettings.autoFixRequireSurface.enable();}@ReplaceMethodInvoke(targetClass = TextureView.class, methodName = "getBitmap", includeOverride = true)public static Bitmap getBitmapHook(TextureView textureView) {    return isGetBitmapSafe(textureView) ? textureView.getBitmap() : null;}@ReplaceMethodInvoke(targetClass = TextureView.class, methodName = "getBitmap", includeOverride = true)public static Bitmap getBitmapHook(TextureView textureView, int width, int height) {    return isGetBitmapSafe(textureView) ? textureView.getBitmap(width, height) : null;}@ReplaceMethodInvoke(targetClass = TextureView.class, methodName = "getBitmap", includeOverride = true)public static Bitmap getBitmapHook(TextureView textureView, Bitmap bitmap) {    return isGetBitmapSafe(textureView) ? textureView.getBitmap(bitmap) : bitmap;}

修復效果

實驗全量后requireSurface 相關crash明顯下降,觀察兩周業務指標沒有明顯劣化,直播場景有正向收益,符合預期。全量后量級大幅下降,還剩下一小部分主要是老版本、以及一些少量的destroy、swapBuffer失敗相關的問題。I8628資訊網——每日最新資訊28at.com

業務收益:看播滲透顯著提升;人均看播天數顯著提升;I8628資訊網——每日最新資訊28at.com

穩定性收益:Native Crash大幅下降I8628資訊網——每日最新資訊28at.com

圖片圖片I8628資訊網——每日最新資訊28at.com

圖片圖片I8628資訊網——每日最新資訊28at.com

后續思考

這個requireSurface問題發生在RenderThread,但造成問題的原因在主線程,因此如果能在RenderThread線程發生native crash時抓到主線程java堆棧,就可以定位到業務根因,也就不需要一系列自下而上地代碼分析來尋找hook點了。I8628資訊網——每日最新資訊28at.com

因此,后續有RenderThread線程異常時,應該把主線程堆棧上報上來,提高RenderThread問題的排查效率。I8628資訊網——每日最新資訊28at.com

加入我們

我們是字節跳動西瓜視頻客戶端團隊,專注于西瓜視頻 App 的開發和基礎技術建設,在客戶端架構、性能、穩定性、編譯構建、研發工具等方向都有投入。如果你也想一起攻克技術難題,迎接更大的技術挑戰,歡迎點擊閱讀原文,或者投遞簡歷到xiaolin.gan@bytedance.com。I8628資訊網——每日最新資訊28at.com

最 Nice 的工作氛圍和成長機會,福利與機遇多多,在上海和杭州均有職位,歡迎加入西瓜視頻客戶端團隊 !I8628資訊網——每日最新資訊28at.com

本文鏈接:http://www.www897cc.com/showinfo-26-57272-0.html西瓜視頻RenderThread引起的閃退問題攻堅歷程

聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com

上一篇: 集邦咨詢:強震導致日本多家半導體工廠停產檢修,初步判斷影響可控

下一篇: 字節跳動百萬級Metrics Agent性能優化的探索與實踐

標簽:
  • 熱門焦點
  • 5月iOS設備性能榜:M1 M2依舊是榜單前五

    和上個月一樣,沒有新品發布的iOS設備性能榜的上榜設備并沒有什么更替,僅僅只有跑分變化而產生的排名變動,剛剛開始的蘋果WWDC2023,推出的產品也依舊是新款Mac Pro、新款Mac Stu
  • 5月安卓手機好評榜:魅族20 Pro奪冠

    性能榜和性價比榜之后,我們來看最后的安卓手機好評榜,數據來源安兔兔評測,收集時間2023年5月1日至5月31日,僅限國內市場。第一名:魅族20 Pro好評率:97.50%不得不感慨魅族老品牌還
  • 從 Pulsar Client 的原理到它的監控面板

    背景前段時間業務團隊偶爾會碰到一些 Pulsar 使用的問題,比如消息阻塞不消費了、生產者消息發送緩慢等各種問題。雖然我們有個監控頁面可以根據 topic 維度查看他的發送狀態,
  • 使用AIGC工具提升安全工作效率

    在日常工作中,安全人員可能會涉及各種各樣的安全任務,包括但不限于:開發某些安全工具的插件,滿足自己特定的安全需求;自定義github搜索工具,快速查找所需的安全資料、漏洞poc、exp
  • 慕巖炮轟抖音,百合網今何在?

    來源:價值研究所 作者:Hernanderz&ldquo;難道就因為自己的一個產品牛逼了,從客服到總裁,都不愿意正視自己產品和運營上的問題,選擇逃避了嗎?&rdquo;這一番話,出自百合網聯合創
  • 簽約井川里予、何丹彤,單視頻點贊近千萬,MCN黑馬永恒文希快速崛起!

    來源:視聽觀察永恒文希傳媒作為一家MCN公司,說起它的名字來,可能大家會覺得有點兒陌生,但是說出來下面一串的名字之后,或許大家就會感到震驚,原來這么多網紅,都簽約這家公司了。根
  • 超級標準版旗艦!iQOO 11S全球首發iQOO超算獨顯芯片

    上半年已接近尾聲,截至目前各大品牌旗下的頂級旗艦都已悉數亮相,而下半年即將推出的頂級旗艦已經成為了數碼圈爆料的主流,其中就包括全新的iQOO 11S系
  • iQOO Neo8 Pro即將開售:到手價3099元起 安卓性能最強旗艦

    5月23日,iQOO如期舉行了新品發布會,全新的iQOO Neo8系列也正式與大家見面,包含iQOO Neo8和iQOO Neo8 Pro兩個版本,其中標準版搭載高通驍龍8+,而Pro版更
  • iQOO Neo8 Pro搶先上架:首發天璣9200+ 安卓性能之王

    經過了一段時間的密集爆料,昨日iQOO官方如期對外宣布:將于5月23日推出全新的iQOO Neo8系列新品,官方稱這是一款擁有旗艦級性能調校的作品。隨著發布時
Top 主站蜘蛛池模板: 滦南县| 潞城市| 梧州市| 佛冈县| 革吉县| 彰化市| 白朗县| 若羌县| 伊宁县| 新郑市| 天柱县| 黄浦区| 铜梁县| 施秉县| 聂荣县| 湟中县| 太仓市| 吉水县| 天全县| 普兰店市| 当涂县| 东安县| 武宣县| 黄浦区| 承德市| 新乐市| 马公市| 罗甸县| 德阳市| 襄城县| 繁昌县| 旅游| 梧州市| 霸州市| 道孚县| 青海省| 攀枝花市| 水城县| 三原县| 台安县| 威远县|