西瓜視頻RenderThread引起的閃退問(wèn)題攻堅(jiān)歷程
背景
影響
西瓜之前存在過(guò)一類RenderThread閃退,從堆棧上看,全部都是系統(tǒng)so調(diào)用,給人的第一印象像是一個(gè)系統(tǒng)bug,無(wú)從下手。閃退集中在Android 5~6上,表現(xiàn)為打開(kāi)直播間立即閃退。該問(wèn)題在2022年占據(jù)Native Crash Top5,2023年更是上升到到Top1。因此有必要投入時(shí)間和精力再重新審視一下這個(gè)問(wèn)題。在歷經(jīng)多周的源碼分析和排查后,逐步明確了問(wèn)題根因并修復(fù),最終取得了顯著的穩(wěn)定性收益和業(yè)務(wù)收益。
接下來(lái),我們將抽絲剝繭,一步步深入分析這個(gè)歷史遺留問(wèn)題,揭開(kāi)它背后真正的原因。
基本信息
具體堆棧如下:
圖片
堆棧都是系統(tǒng)的so調(diào)用,不能明確具體閃退業(yè)務(wù)場(chǎng)景,只能看出是RenderThread線程主動(dòng)abort了。
根據(jù)abort message找到對(duì)應(yīng)的abort代碼,在CanvasContext::requireSurface時(shí)閃退了,代碼如下:
圖片
問(wèn)題特征:
問(wèn)題集中在Android 5.0~6.0,線程集中在RenderThread,無(wú)明顯機(jī)型、廠商特征。
RenderThread簡(jiǎn)介
為了便于理解下面的分析過(guò)程,先對(duì)RenderThread簡(jiǎn)單的介紹。順便看一下是怎么調(diào)用到CanvasContext::requireSurface的。
相關(guān)類圖如下:
圖片
相關(guān)源碼:frameworks/base/libs/hwui/renderthread
RenderThread::threadLoop
RenderThread繼承自Thread和Singleton,是一個(gè)單例模式的線程,通過(guò)RenderThread.getInstance()獲取。和主線程很像,內(nèi)部是一個(gè)通過(guò)for實(shí)現(xiàn)的無(wú)限循環(huán),不斷從TaskQueue里通過(guò)nextTask函數(shù)獲取RenderTask并執(zhí)行,RenderTask執(zhí)行完后會(huì)按需調(diào)用requestVsync。核心代碼在threadLoop函數(shù)中:
圖片
ThreadedRender
Java層通過(guò)ThreadedRender與RenderThread進(jìn)行通信。當(dāng)Window啟用硬件加速時(shí),ViewRootImpl會(huì)通過(guò)HardwareRenderer.create()創(chuàng)建一個(gè)ThreadedRender實(shí)例。ThreadedRender在創(chuàng)建時(shí),會(huì)調(diào)用nCreateProxy在native層創(chuàng)建一個(gè)RenderProxy。ThreadedRender通過(guò)RenderProxy向RenderThread提交任務(wù)。
圖片
RenderProxy
RenderProxy在創(chuàng)建時(shí),會(huì)同步創(chuàng)建一個(gè)CanvasContext,再通過(guò)RenderThread.getInstance()拿到RenderThread實(shí)例。RenderProxy通過(guò)CREATE_BRIDGE定義了許多Bridge函數(shù),再通過(guò)SETUP_TASK把這些Bridge函數(shù)包裝成RenderTask,再通過(guò)postAndWait提交給RenderThread調(diào)用。postAndWait之后,當(dāng)前線程進(jìn)入等待狀態(tài),當(dāng)對(duì)應(yīng)的task執(zhí)行完畢之后喚醒當(dāng)前線程。以RenderProxy::createTextureLayer為例:
圖片
圖片
CanvasContext
RenderProxy把任務(wù)提交給RenderThread之后,執(zhí)行的實(shí)際上是CanvasContext::createTextureLayer,就是在這里調(diào)用了requireSurface。
圖片
初步猜想
其他 App 相似問(wèn)題修復(fù)
其他端App也曾有過(guò)'requireSurface() called but no surface set! '相關(guān)閃退。原因是:在Activity進(jìn)行側(cè)滑退出時(shí),側(cè)滑框架需要強(qiáng)制對(duì)下層Activity進(jìn)行繪制生成Bitmap,再用這個(gè)Bitmap來(lái)實(shí)現(xiàn)Activity的切換效果。但由于下層Activity此前已處于不可見(jiàn)狀態(tài),可能有業(yè)務(wù)層主動(dòng)釋放了下層Activity中的TextureView,導(dǎo)致了no surface set的閃退。經(jīng)過(guò)對(duì)西瓜的側(cè)滑框架的源碼分析,發(fā)現(xiàn)不會(huì)產(chǎn)生此類問(wèn)題,因此西瓜的問(wèn)題應(yīng)該另有其因。
正面分析西瓜問(wèn)題
問(wèn)題的條件是mEglSurface == EGL_NO_SURFACE,看下mEglSurface賦值為EGL_NO_SURFACE的時(shí)機(jī)。
總共有兩處:
第一處:CanvasContext::setSurface
這里總共兩處mEglSurface賦值操作。一處直接賦值為EGL_NO_SURFACE,另一處為mEglManager.createSurface的返回值。而mEglManager.createSurface在返回前判斷如果是EGL_NO_SURFACE會(huì)主動(dòng)abort,顯然createSurface的返回值一定不是EGL_NO_SURFACE。
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;
}
那么這里根據(jù)window是否為nullptr又可以分為兩種情況:
- setSurface(nullptr)之后,mEglSurface 最終賦值為 EGL_NO_SURFACE,之后調(diào)用requireSurface發(fā)生abort。
- setSurface(window),第5行會(huì)先設(shè)置為EGL_NO_SURFACE,在第10行createSurface返回之前,此時(shí)在另外一個(gè)線程調(diào)用requireSurface也會(huì)發(fā)生abort。
第二處:初始值
初始值為EGL_NO_SURFACE。只有調(diào)用CanvasContext::setSurface時(shí),mEglSurface 才會(huì)被賦值,在此之前,調(diào)用了requireSurface也會(huì)引發(fā)閃退。
class CanvasContext : public IFrameCallback {
private:
EGLSurface mEglSurface = EGL_NO_SURFACE;
}
總結(jié)下來(lái),有三個(gè)時(shí)機(jī)調(diào)用requireSurface會(huì)導(dǎo)致閃退:
- 多線程并發(fā),mEglSurface短暫為EGL_NO_SURFACE
- CanvasContext::setSurface(nullptr)之后,即mEglSurface被銷毀
- CanvasContext::setSurface之前,即mEglSurface未初始化
深入分析
7.0+系統(tǒng)是如何避免這個(gè)問(wèn)題的?
從多維信息看出,問(wèn)題在6.0及以下版本發(fā)生。那么7.0上系統(tǒng)做了哪些優(yōu)化,是如何規(guī)避我們上面三種可能的情況的?這些優(yōu)化思路對(duì)我們解決問(wèn)題能否提供幫助?
對(duì)比6.0和7.0代碼之后,發(fā)現(xiàn)谷歌直接把requireSurface這個(gè)方法移除了!
逐個(gè)翻看6.0~7.0上RenderThread相關(guān)的commit,最終找到了這個(gè)commit(8afcc769)。這里確實(shí)是把requireSurface刪除了,并在createTextureLayer中調(diào)用了一下 mEglManager.initialize()。而EglManager::initialize里的實(shí)現(xiàn),是執(zhí)行下EglManager的初始化,這里跟6.0基本一致。
圖片
圖片
圖片
那6.0上的abort原來(lái)是可以直接刪掉的嗎?如果是這樣,我們是不是可以有樣學(xué)樣,嘗試hook requireSurface,把a(bǔ)bort去掉,再主動(dòng)調(diào)用一下mEglManager.initialize,從而達(dá)到這個(gè)commit相似的修復(fù)效果?
在云真機(jī)上找了個(gè)6.0的設(shè)備,把libhwui.so pull下來(lái),通過(guò) readelf -sW libhwui.so查看requireSurface的符號(hào)。發(fā)現(xiàn),沒(méi)有requireSurface的符號(hào)。解開(kāi)so后發(fā)現(xiàn),requireSurface被inline進(jìn)了createTextureLayer:
圖片
再嘗試一下hook requireSurface的上一層調(diào)用CanvasContext::createTextureLayer,發(fā)現(xiàn)也沒(méi)有對(duì)應(yīng)的符號(hào),只有RenderProxy::createTextureLayer 的符號(hào)。如果采用7.0的修復(fù)方案,需要修改的指令非常多。而且,這個(gè)MR中還有其他改動(dòng),要不要一起hook?這些問(wèn)題暫時(shí)還沒(méi)搞清楚,花大力氣去做的話風(fēng)險(xiǎn)太高。
圖片
看起來(lái),參考7.0修復(fù)方案操作難度大,并且不確定是否有效。我們先把它作為備選方案,另謀出路!
正面分析三種可能性
多線程問(wèn)題?
CanvasContext的createTextureLayer和setSurface相關(guān)代碼,都被RenderProxy轉(zhuǎn)移到了RenderThread上執(zhí)行,而RenderThread又是單例的,所以這里不存在多線程問(wèn)題,因此可以直接排除線程并發(fā)問(wèn)題。
setSurface(null)導(dǎo)致?
前面分析過(guò),setSurface(nullptr)也會(huì)導(dǎo)致EGL_NO_SURFACE。下面是ThreadedRender一個(gè)大概的調(diào)用時(shí)序圖,把幾個(gè)可能產(chǎn)生EGL_NO_SURFACE的setSurface調(diào)用做了標(biāo)記,序號(hào)24就是的閃退函數(shù)requireSurface。時(shí)序圖:
圖片
可以看出在CanvasContext中,setSurface的調(diào)用有:initialize、swapBuffers、makeCurrent、updateSurface、destory、~ConvasContext。對(duì)應(yīng)的java層調(diào)用為T(mén)hreadedRender中的的initialize、draw、createTextureLayer、updateSurface、destory、finalize方法。排除一些不會(huì)出現(xiàn)異常的方法:
排除ThreadedRender.initialize
initialize時(shí)java層傳過(guò)來(lái)的surface做了判斷保護(hù),可以確保surface不為nullptr,因此可以排除initialize,代碼如下:
圖片
排除ThreadedRender.draw
draw對(duì)應(yīng)的問(wèn)題是swapBuffers失敗。發(fā)生在swapBuffers失敗時(shí),也就是eglSwapBuffers錯(cuò)誤了。
圖片
系統(tǒng)有以下兩種方式處置錯(cuò)誤:
- EGL_BAD_SURFACE:打印失敗日志。但翻看多個(gè)跟蹤日志,均沒(méi)有發(fā)現(xiàn)類似日志,暫時(shí)不關(guān)注。
- 其他EGL錯(cuò)誤:直接abort掉,此時(shí)變成另外一個(gè)閃退,因此可以排除。
綜上,對(duì)于swapBuffers失敗這種情況可能存在,但未發(fā)現(xiàn)相關(guān)報(bào)錯(cuò)日志,暫時(shí)不作過(guò)多關(guān)注。相關(guān)代碼如下:
圖片
排除ThreadedRender.finalize
ThreadedRender.finalize之后,會(huì)在native層通過(guò)delete 釋放RenderProxy和ConvasContext,在~ConvasContext析構(gòu)時(shí)調(diào)用setSurface(nullptr)。因此,如果之后再調(diào)用requireSurface,應(yīng)該會(huì)發(fā)生SIGSEGV相關(guān)錯(cuò)誤,不可能出現(xiàn)surface not set異常。因此,也可以排除掉。代碼如下:
圖片
圖片
圖片
剩余情況
排除了intialize、swapBuffers、finalize之后,還剩下makeCurrent失敗、updateSurface(nullptr)、destory都有可能產(chǎn)生問(wèn)題,上游調(diào)用比較多追蹤起來(lái)依然比較困難,暫時(shí)無(wú)法排除。
初始化前觸發(fā)了requireSurface?
一般來(lái)說(shuō),setSurface的首次調(diào)用是在initialize中。那么,如果在initialize之前就調(diào)用了requireSurface,是不是就會(huì)出問(wèn)題呢?從前面的分析可以看出,requireSurface的上游是java層的createTextureLayer,而createTextureLayer的調(diào)用處只有一個(gè),在TextureView的getHardwareLayer中。
圖片
getHardwareLayer是View的一個(gè)方法,默認(rèn)返回null。從注釋上也能看出,在6.0上只有TextureView用到了這個(gè)方法,調(diào)用處也只有移除在getBitmap中。在7.0上也是直接把getHardwareLayer從View中移除了,變成TextureView的一個(gè)方法。而getBitmap是個(gè)public方法,這里是可以被app調(diào)用到的。
圖片
圖片
閃退的前提條件:
- ThreadedRender.initialize還未調(diào)用
- ThreadedRender.destroy或者ThreadedRender.updateSurface(null)之后
在Java層觸發(fā)requireSurface步驟如下:
- TextureView.getBitmap => ThreadedRender.createTextureLayer => ConvasContext::requireSurface
最終定位
驗(yàn)證分析結(jié)論
通過(guò)前面的分析,找到了問(wèn)題的前提條件,并發(fā)現(xiàn)了一條觸發(fā)requireSurface的方式。那么,就可以結(jié)束紙上談兵,通過(guò)實(shí)操來(lái)在本地復(fù)現(xiàn)這個(gè)閃退,來(lái)實(shí)錘前面的結(jié)論。
ThreadedRender.initialize還未調(diào)用
由于ThreadedRenderer不是公開(kāi)api,需要通過(guò)反射來(lái)創(chuàng)建實(shí)例。拿到實(shí)例后不調(diào)用其intialize方法,直接反射調(diào)用createTextureLayer。代碼如下:
圖片
果然,復(fù)現(xiàn)了'requireSurface() called but no surface set!' 這個(gè)問(wèn)題:
圖片
destroy或updateSurface(null)
通過(guò)反射創(chuàng)建ThreadedRender實(shí)例,先執(zhí)行ThreadedRender.intialize,之后調(diào)用destroy或updateSurface清空surface,最后調(diào)用createTextureLayer,也成功復(fù)現(xiàn)了這個(gè)閃退。
業(yè)務(wù)場(chǎng)景定位
前面的復(fù)現(xiàn),只是從技術(shù)層面確認(rèn)了問(wèn)題發(fā)生的幾種可能,但還沒(méi)有與業(yè)務(wù)場(chǎng)景關(guān)聯(lián)起來(lái)。真實(shí)的問(wèn)題是否在前面提到的這幾種可能中間?如果在的話,那具體的調(diào)用點(diǎn)在哪,又該如何修復(fù)?
嘗試在真實(shí)場(chǎng)景中復(fù)現(xiàn)
通過(guò)shaddow hook RenderProxy的Initialize、destroy、updateSurface、createTextureLayer等函數(shù),在hook函數(shù)中打印一些日志。由于RenderProxy可能存在多個(gè)實(shí)例,需要在日志里加上RenderProxy實(shí)例的地址來(lái)方便追蹤單個(gè)RenderProxy調(diào)用時(shí)序。
hook函數(shù)如下:
圖片
盡管這個(gè)問(wèn)題在android 6上閃退率比較高,但我用6.0測(cè)試機(jī)跑自動(dòng)化測(cè)試,還是沒(méi)有復(fù)現(xiàn)這個(gè)問(wèn)題。
線上定位
問(wèn)題不是必現(xiàn)的,排查進(jìn)程在線下難以為繼。而只有在真實(shí)的業(yè)務(wù)場(chǎng)景中復(fù)現(xiàn),才能明確問(wèn)題根因,找到最佳修復(fù)方案。因此,需要加把這些hook點(diǎn)上線進(jìn)一步排查。上線無(wú)小事,線下可以小步快走,逐漸定位問(wèn)題。但上線步子一定要穩(wěn),不能邁太大。但也不能太小,否則周期會(huì)拉的很長(zhǎng)。所以,既要保證有足夠的信息排查,也要盡可能的降低穩(wěn)定性和性能影響。
明確業(yè)務(wù)堆棧
前面的hook方案只能確認(rèn)真實(shí)業(yè)務(wù)場(chǎng)景是否也有調(diào)用時(shí)序問(wèn)題,并不清楚具體的業(yè)務(wù)調(diào)用堆棧。
從業(yè)務(wù)上層代碼到異常點(diǎn),必然經(jīng)過(guò)了ThreadedRender的Initialize、destroy、update、createTexture等方法,那么通過(guò)java hook把這些方法hook住,并打印堆棧應(yīng)該就定位到業(yè)務(wù)代碼。需要注意的是:
- 穩(wěn)定性問(wèn)題
Java hook 目前主流的方案中還沒(méi)有能達(dá)到線上大規(guī)模使用的水平,只能小流量觀察。
保障方案:系統(tǒng)版本限制在6.0,放量計(jì)劃1%->5%->10%,小流量觀察。
- 性能問(wèn)題
由于這些api調(diào)用頻率可能很高,也都在主線程,直接打印堆棧會(huì)影響性能。真正需要關(guān)注的就是異常前的幾次調(diào)用,且有些case可以通過(guò)以下條件預(yù)判,其余的堆棧都不必要甚至是干擾信息。需要關(guān)注的堆棧如下:
- initialize:不需要堆棧。只要知道有沒(méi)有調(diào)用過(guò),打印一行日志即可。
- destroy:必須全部打印堆棧。發(fā)生在異常前,無(wú)法預(yù)判過(guò)濾。
- updateSurface:surface==null時(shí)打印堆棧。surface不為null不會(huì)導(dǎo)致異常。
- createTextureLayer:未初始化或者surface==null時(shí)打印堆棧。只有這兩種可能會(huì)有異常。
總結(jié):可以通過(guò)surface是否為null、initialize是否調(diào)用過(guò)這兩個(gè)條件減少stackTrace。
在ThreadedRender中,surface都被透?jìng)鹘o了native層,沒(méi)有對(duì)應(yīng)的Java引用,需要手動(dòng)維護(hù)一個(gè)java 層的實(shí)例。初始化狀態(tài)可以通過(guò)反射ThreadedRenderer.mInitialized拿到,不過(guò)既然已經(jīng)hook intialize和destroy了,這里也選擇手動(dòng)維護(hù)一個(gè)初始化狀態(tài),畢竟可以減少一次反射調(diào)用。
public class ThreadedRenderer extends HardwareRenderer {
private boolean mInitialized = false;
@Override
void updateSurface(Surface surface) throws OutOfResourcesException {
updateEnabledState(surface);//透?jìng)鹘o了Native層,Java層沒(méi)有引用
nUpdateSurface(mNativeProxy, surface);
}
}
Java hook偽代碼如下:
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);
}
線上日志
上線后成功采集到了關(guān)鍵的Java調(diào)用堆棧!基本都集中在直播業(yè)務(wù)場(chǎng)景下,初始化前調(diào)用requireSurface。也有一些零星的destroy之后requireSurface的case,由于量級(jí)太小本文不做重點(diǎn)討論。
ThreadedRender.Initialize之前
日志截圖如下:
圖片
調(diào)用時(shí)序問(wèn)題確認(rèn):
對(duì)于地址為0x814a0b0的RenderProxy實(shí)例,沒(méi)有它intialize相關(guān)調(diào)用日志,只有一條createTextureLayer調(diào)用日志??梢悦鞔_,這個(gè)RenderProxy實(shí)例是在initialize之前調(diào)用createTextureLayer導(dǎo)致閃退!
Java 堆棧分析:
Log.d會(huì)對(duì)過(guò)長(zhǎng)的堆棧進(jìn)行截取,F(xiàn)rameLayout.onMeasure之前的都被截取了,不過(guò)對(duì)于排查問(wèn)題,影響不大。
堆棧關(guān)鍵信息整理如下:
- 閃退時(shí)正在執(zhí)行onMeasure
- 在com.bytedance.android.livesdk.chatroom.ui.LivePlayerWidget.loadSharedPlayer中調(diào)用了TextureView的getBitmap方法,再到ThreadedRender.creatTextureLayer,之后就發(fā)生了Native crash。
為什么沒(méi)有intialize?
onMeasure會(huì)早于ThreadedRender.initialize執(zhí)行嗎?
ThreadedRender.initialize和performMeasure相關(guān)的代碼都在performTraversals中,再次回到源碼中去分析。從代碼結(jié)構(gòu)來(lái)看,initialize前后都有measure相關(guān)操作。initialize之前通過(guò)measureHierarchy調(diào)用了performMeasure,initialize之后是直接調(diào)用performMeasure。由于measureHierarchy外部包了許多判斷條件,所以不能直接從代碼行的上下關(guān)系,得出measure早于initialize的結(jié)論,但我們可以保持這個(gè)懷疑進(jìn)一步驗(yàn)證。
這個(gè)方法過(guò)于巨大,移除無(wú)關(guān)代碼后如下:
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;
}
由于堆棧被裁剪了,無(wú)法確認(rèn)異常是從哪個(gè)分支過(guò)來(lái)的。不過(guò)沒(méi)關(guān)系,注意到當(dāng)mFirst=true時(shí),滿足layoutRequested = true,會(huì)先調(diào)用執(zhí)行measureHierarchy,可以在本地模擬mFirst=true這種情況,即可驗(yàn)證。
本地通過(guò)onMeasure復(fù)現(xiàn)
本地寫(xiě)個(gè)demo,在FrameLayout.onMeasure中立即調(diào)用TextureView.getBitmap,并通過(guò)反射查看mFirst的值,找個(gè)6.0的云真機(jī)驗(yàn)證一下。onMeasure會(huì)連續(xù)執(zhí)行多次,只有第一次的mFirst為true,但沒(méi)能復(fù)現(xiàn)問(wèn)題,代碼如下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
boolean first=reflectFirst();//反射獲取mFirst
activity.log("mFirst=" + first);
Bitmap bitmap = mTextureView.getBitmap(mBitmap);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
再來(lái)看下mTextureView.getBitmap的實(shí)現(xiàn),
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;
}
可以看到,要想執(zhí)行到ThreadedRender.createTextureLayer還需要滿足以:isAvailable()為true,手動(dòng)調(diào)用一下TextureView.setSurfaceTexture就可以滿足。
根據(jù)猜想,再次編寫(xiě)代碼終于復(fù)現(xiàn)成功! demo如下:
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時(shí)執(zhí)行g(shù)etBitmap,再次運(yùn)行,不崩了??梢?jiàn)異常的關(guān)鍵條件就是mFirst!
if (!first) { //沒(méi)問(wèn)題
mTextureView.getBitmap(mBitmap);
}
問(wèn)題根因
梳理下整體流程。ViewRootImpl首次performTraversals時(shí)(mFirst=true),onMeasure會(huì)早于ThreadedRenderer.initialize。而業(yè)務(wù)方在onMeasure中又調(diào)用了TextureView.getBitmap,最終在native層會(huì)調(diào)用CanvasContext::requreSurface。由于還沒(méi)有執(zhí)行過(guò)CanvasContext::initialize,當(dāng)前mEglSurface為EGL_NO_SURFACE,于是在Android5~6上觸發(fā)了abort,發(fā)生surface not set的異常。
總結(jié)起來(lái):在android6.0上,ViewRootImpl首次performTraversals時(shí),如過(guò)在onMeasure中調(diào)用了TextureView.getBitmap,就可能會(huì)發(fā)生這個(gè)異常。
線上還存在一些零星的destroy之后requireSurface、swapBuffers失敗后requireSurface的異常,由于排查思路大同小異,這里就不展開(kāi)說(shuō)了。
修復(fù)方案
通過(guò)字節(jié)碼插樁全局替換TextureView.getBitmap方法,當(dāng)ViewRootImpl.mFirst=true時(shí),就返回默認(rèn)值而不執(zhí)行g(shù)etBitmap原有邏輯,這樣就不會(huì)調(diào)用到ThreadedRender.createTextureLayer。
但由于mFirst只能通過(guò)反射獲取,這可能會(huì)影響performTraversals性能,有沒(méi)有性能更好的方案?
通過(guò)代碼分析,發(fā)現(xiàn)performTraversals會(huì)經(jīng)歷layout階段,而layout之后View會(huì)增加一個(gè)PFLAG3_IS_LAID_OUT:
/**
* 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;
}
因此,可以通過(guò)isLaidOut ()獲取到這一個(gè)屬性,達(dá)到和mFirst基本一致的效果。
最終方案:
插裝替換全局TextureView.getBitmap調(diào)用,增加textureView.isLaidOut()判斷。
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;
}
修復(fù)效果
實(shí)驗(yàn)全量后requireSurface 相關(guān)crash明顯下降,觀察兩周業(yè)務(wù)指標(biāo)沒(méi)有明顯劣化,直播場(chǎng)景有正向收益,符合預(yù)期。全量后量級(jí)大幅下降,還剩下一小部分主要是老版本、以及一些少量的destroy、swapBuffer失敗相關(guān)的問(wèn)題。
業(yè)務(wù)收益:看播滲透顯著提升;人均看播天數(shù)顯著提升;
穩(wěn)定性收益:Native Crash大幅下降
圖片
圖片
后續(xù)思考
這個(gè)requireSurface問(wèn)題發(fā)生在RenderThread,但造成問(wèn)題的原因在主線程,因此如果能在RenderThread線程發(fā)生native crash時(shí)抓到主線程java堆棧,就可以定位到業(yè)務(wù)根因,也就不需要一系列自下而上地代碼分析來(lái)尋找hook點(diǎn)了。
因此,后續(xù)有RenderThread線程異常時(shí),應(yīng)該把主線程堆棧上報(bào)上來(lái),提高RenderThread問(wèn)題的排查效率。
加入我們
我們是字節(jié)跳動(dòng)西瓜視頻客戶端團(tuán)隊(duì),專注于西瓜視頻 App 的開(kāi)發(fā)和基礎(chǔ)技術(shù)建設(shè),在客戶端架構(gòu)、性能、穩(wěn)定性、編譯構(gòu)建、研發(fā)工具等方向都有投入。如果你也想一起攻克技術(shù)難題,迎接更大的技術(shù)挑戰(zhàn),歡迎點(diǎn)擊閱讀原文,或者投遞簡(jiǎn)歷到xiaolin.gan@bytedance.com。
最 Nice 的工作氛圍和成長(zhǎng)機(jī)會(huì),福利與機(jī)遇多多,在上海和杭州均有職位,歡迎加入西瓜視頻客戶端團(tuán)隊(duì) !