西瓜视频RenderThread引起的闪退问题攻坚历程

2023-12-16

背景

影响

西瓜之前存在过一类RenderThread闪退,从堆栈上看,全部都是系统so调用,给人的第一印象像是一个系统bug,无从下手。闪退集中在Android 5~6上,表现为打开直播间立即闪退。该问题在2022年占据Native Crash Top5,2023年更是上升到到Top1。因此有必要投入时间和精力再重新审视一下这个问题。在历经多周的源码分析和排查后,逐步明确了问题根因并修复,最终取得了显著的稳定性收益和业务收益。

接下来,我们将抽丝剥茧,一步步深入分析这个历史遗留问题,揭开它背后真正的原因。

基本信息

具体堆栈如下:

12a06dd4db3d8419cc772fea9c608541.png

堆栈都是系统的so调用,不能明确具体闪退业务场景,只能看出是RenderThread线程主动abort了。

根据abort message找到对应的abort代码,在CanvasContext::requireSurface时闪退了,代码如下:

ac6433511b6b3180aa910155f9dd0007.png

问题特征:

问题集中在Android 5.0~6.0,线程集中在RenderThread,无明显机型、厂商特征。

RenderThread简介

为了便于理解下面的分析过程,先对RenderThread简单的介绍。顺便看一下是怎么调用到CanvasContext::requireSurface的。

相关类图如下:

87b7ece3aec66a4aa298273ddb9df7df.png

相关源码:frameworks/base/libs/hwui/renderthread

RenderThread::threadLoop

RenderThread继承自Thread和Singleton,是一个单例模式的线程,通过RenderThread.getInstance()获取。和主线程很像,内部是一个通过for实现的无限循环,不断从TaskQueue里通过nextTask函数获取RenderTask并执行,RenderTask执行完后会按需调用requestVsync。核心代码在threadLoop函数中:

6a42fc716192a7714ce7f0a670287ee6.png

ThreadedRender

Java层通过ThreadedRender与RenderThread进行通信。当Window启用硬件加速时,ViewRootImpl会通过HardwareRenderer.create()创建一个ThreadedRender实例。ThreadedRender在创建时,会调用nCreateProxy在native层创建一个RenderProxy。ThreadedRender通过RenderProxy向RenderThread提交任务。

dee555bbfd540fee585c11c7f8fb7646.png

RenderProxy

RenderProxy在创建时,会同步创建一个CanvasContext,再通过RenderThread.getInstance()拿到RenderThread实例。RenderProxy通过CREATE_BRIDGE定义了许多Bridge函数,再通过SETUP_TASK把这些Bridge函数包装成RenderTask,再通过postAndWait提交给RenderThread调用。postAndWait之后,当前线程进入等待状态,当对应的task执行完毕之后唤醒当前线程。以RenderProxy::createTextureLayer为例:

b4256c051d590169b1115fcd06caee56.png d5e9a71a89e2f1635a75fe6502140bf5.png

CanvasContext

RenderProxy把任务提交给RenderThread之后,执行的实际上是CanvasContext::createTextureLayer,就是在这里调用了requireSurface。

9f90f322cef5eb304ad5822656eb7a29.png

初步猜想

其他 App 相似问题修复

其他端App也曾有过' requireSurface() called but no surface set! '相关闪退。原因是:在Activity进行侧滑退出时,侧滑框架需要强制对下层Activity进行绘制生成Bitmap,再用这个Bitmap来实现Activity的切换效果。但由于下层Activity此前已处于不可见状态,可能有业务层主动释放了下层Activity中的TextureView,导致了no surface set的闪退。经过对西瓜的侧滑框架的源码分析,发现不会产生此类问题,因此西瓜的问题应该另有其因。

正面分析西瓜问题

问题的条件是mEglSurface == EGL_NO_SURFACE,看下mEglSurface赋值为EGL_NO_SURFACE的时机。

总共有两处:

第一处:CanvasContext::setSurface

这里总共两处mEglSurface赋值操作。一处直接赋值为EGL_NO_SURFACE,另一处为mEglManager.createSurface的返回值。而mEglManager.createSurface在返回前判断如果是EGL_NO_SURFACE会主动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;
}

那么这里根据window是否为nullptr又可以分为两种情况:

  1. setSurface(nullptr)之后,mEglSurface 最终赋值为 EGL_NO_SURFACE,之后调用requireSurface发生abort。

  2. setSurface(window),第5行会先设置为EGL_NO_SURFACE,在第10行createSurface返回之前,此时在另外一个线程调用requireSurface也会发生abort。

第二处:初始值

初始值为EGL_NO_SURFACE。只有调用CanvasContext::setSurface时,mEglSurface 才会被赋值,在此之前,调用了requireSurface也会引发闪退。

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

总结下来,有三个时机调用requireSurface会导致闪退:

  1. 多线程并发,mEglSurface短暂为EGL_NO_SURFACE

  2. CanvasContext::setSurface(nullptr)之后,即mEglSurface被销毁

  3. CanvasContext::setSurface之前,即mEglSurface未初始化

深入分析

7.0+系统是如何避免这个问题的?

从多维信息看出,问题在6.0及以下版本发生。那么7.0上系统做了哪些优化,是如何规避我们上面三种可能的情况的?这些优化思路对我们解决问题能否提供帮助?

对比6.0和7.0代码之后,发现谷歌直接把requireSurface这个方法移除了!

逐个翻看6.0~7.0上RenderThread相关的commit,最终找到了这个commit(8afcc769)。这里确实是把requireSurface删除了,并在createTextureLayer中调用了一下 mEglManager.initialize()。而EglManager::initialize里的实现,是执行下EglManager的初始化,这里跟6.0基本一致。

f7771d130dcd9b1a4cbca0a84425585d.png c1b49f99461b799cbbe37aba4ed5d4aa.png 5d646cbe5cf5c010c2a932eb862ac47b.png

那6.0上的abort原来是可以直接删掉的吗?如果是这样,我们是不是可以有样学样,尝试hook requireSurface,把abort去掉,再主动调用一下mEglManager.initialize,从而达到这个commit相似的修复效果?

在云真机上找了个6.0的设备,把libhwui.so pull下来,通过 readelf -sW libhwui.so 查看requireSurface的符号。发现,没有requireSurface的符号。解开so后发现,requireSurface被inline进了createTextureLayer:

7aa30c30708a687feb96b12e7756e5dd.png

再尝试一下hook requireSurface的上一层调用CanvasContext::createTextureLayer,发现也没有对应的符号,只有RenderProxy::createTextureLayer 的符号。如果采用7.0的修复方案,需要修改的指令非常多。而且,这个MR中还有其他改动,要不要一起hook?这些问题暂时还没搞清楚,花大力气去做的话风险太高。

c81a73615a8dd8321ec30e7e1bbd5b41.png

看起来,参考7.0修复方案操作难度大,并且不确定是否有效。我们先把它作为备选方案,另谋出路!

正面分析三种可能性

多线程问题?

CanvasContext的createTextureLayer和setSurface相关代码,都被RenderProxy转移到了RenderThread上执行,而RenderThread又是单例的,所以这里不存在多线程问题, 因此可以直接排除线程并发问题

setSurface(null)导致?

前面分析过,setSurface(nullptr)也会导致EGL_NO_SURFACE。下面是ThreadedRender一个大概的调用时序图,把几个可能产生EGL_NO_SURFACE的setSurface调用做了标记,序号24就是的闪退函数 requireSurface 。时序图:

1f07fdaba6e940ceb83bb564f0dfcf1c.png

可以看出在CanvasContext中,setSurface的调用有:initialize、swapBuffers、makeCurrent、updateSurface、destory、~ConvasContext。对应的java层调用为ThreadedRender中的的initialize、draw、createTextureLayer、updateSurface、destory、finalize方法。排除一些不会出现异常的方法:

排除ThreadedRender.initialize

initialize时java层传过来的surface做了判断保护,可以确保surface不为nullptr,因此可以排除initialize,代码如下:

2ec574b41a9c2076bc05f2e5c68a9b65.png

排除ThreadedRender.draw

draw对应的问题是swapBuffers失败。发生在swapBuffers失败时,也就是eglSwapBuffers错误了。

170a268af064e522c308c656868ef713.png

系统有以下两种方式处置错误:

  1. EGL_BAD_SURFACE:打印失败日志。但翻看多个跟踪日志,均没有发现类似日志,暂时不关注。

  2. 其他EGL错误:直接abort掉,此时变成另外一个闪退,因此可以排除。

综上,对于swapBuffers失败这种情况可能存在,但未发现相关报错日志,暂时不作过多关注。相关代码如下:

759740d50a56aefa615bb592339f214a.png

排除ThreadedRender.finalize

ThreadedRender.finalize之后,会在native层通过delete 释放RenderProxy和ConvasContext,在~ConvasContext析构时调用setSurface(nullptr)。因此,如果之后再调用requireSurface,应该会发生SIGSEGV相关错误,不可能出现surface not set异常。因此,也可以排除掉。代码如下:

50e9ee00f0117ce8b283ef65307eb4f5.png 4ae0c9af04b8af5eb159f7c18896fe1c.png d9ce31a6e277e4ba9afdf5e8ffcd24f5.png

剩余情况

排除了intialize、swapBuffers、finalize之后,还剩下makeCurrent失败、updateSurface(nullptr)、destory都有可能产生问题,上游调用比较多追踪起来依然比较困难,暂时无法排除。

初始化前触发了requireSurface?

一般来说,setSurface的首次调用是在initialize中。那么,如果在initialize之前就调用了requireSurface,是不是就会出问题呢?从前面的分析可以看出,requireSurface的上游是java层的createTextureLayer,而createTextureLayer的调用处只有一个,在TextureView的getHardwareLayer中。

619380dac2a7ec4d346f15d086ba2261.png

getHardwareLayer是View的一个方法,默认返回null。从注释上也能看出,在6.0上只有TextureView用到了这个方法,调用处也只有移除在getBitmap中。在7.0上也是直接把getHardwareLayer从View中移除了,变成TextureView的一个方法。而getBitmap是个public方法,这里是可以被app调用到的。

8f97c4defe0942d5f2bd51a5ee2091c5.png 8556bb2c64c7c54f25c609fde02f7956.png

闪退的前提条件:

  1. ThreadedRender.initialize还未调用

  2. ThreadedRender.destroy或者ThreadedRender.updateSurface(null)之后

在Java层触发requireSurface步骤如下:

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

最终定位

验证分析结论

通过前面的分析,找到了问题的前提条件,并发现了一条触发requireSurface的方式。那么,就可以结束纸上谈兵,通过实操来在本地复现这个闪退,来实锤前面的结论。

ThreadedRender.initialize还未调用

由于ThreadedRenderer不是公开api,需要通过反射来创建实例。拿到实例后不调用其intialize方法,直接反射调用createTextureLayer。代码如下:

5de947166067017105569e9957156adb.png

果然,复现了'requireSurface() called but no surface set!' 这个问题:

8a2380a1eccd3d8fceef0e90542ce02c.png

destroy或updateSurface(null)

通过反射创建ThreadedRender实例,先执行ThreadedRender.intialize,之后调用destroy或updateSurface清空surface,最后调用createTextureLayer,也成功复现了这个闪退。

业务场景定位

前面的复现,只是从技术层面确认了问题发生的几种可能,但还没有与业务场景关联起来。真实的问题是否在前面提到的这几种可能中间?如果在的话,那具体的调用点在哪,又该如何修复?

尝试在真实场景中复现

通过shaddow hook RenderProxy的Initialize、destroy、updateSurface、createTextureLayer等函数,在hook函数中打印一些日志。由于RenderProxy可能存在多个实例,需要在日志里加上RenderProxy实例的地址来方便追踪单个RenderProxy调用时序。

hook函数如下:

4b8260b8023abd9ecc49073c7f583aa4.png

尽管这个问题在android 6上闪退率比较高,但我用6.0测试机跑自动化测试,还是没有复现这个问题。

线上定位

问题不是必现的,排查进程在线下难以为继。而只有在真实的业务场景中复现,才能明确问题根因,找到最佳修复方案。因此,需要加把这些hook点上线进一步排查。上线无小事,线下可以小步快走,逐渐定位问题。但上线步子一定要稳,不能迈太大。但也不能太小,否则周期会拉的很长。所以,既要保证有足够的信息排查,也要尽可能的降低稳定性和性能影响。

明确业务堆栈

前面的hook方案只能确认真实业务场景是否也有调用时序问题,并不清楚具体的业务调用堆栈。

从业务上层代码到异常点,必然经过了ThreadedRender的Initialize、destroy、update、createTexture等方法,那么通过java hook把这些方法hook住,并打印堆栈应该就定位到业务代码。需要注意的是:

  1. 稳定性问题

Java hook 目前主流的方案中还没有能达到线上大规模使用的水平,只能小流量观察。

保障方案:系统版本限制在6.0,放量计划1%->5%->10%,小流量观察。

  1. 性能问题

由于这些api调用频率可能很高,也都在主线程,直接打印堆栈会影响性能。真正需要关注的就是异常前的几次调用,且有些case可以通过以下条件预判,其余的堆栈都不必要甚至是干扰信息。需要关注的堆栈如下:

  1. initialize:不需要堆栈。只要知道有没有调用过,打印一行日志即可。

  2. destroy:必须全部打印堆栈。发生在异常前,无法预判过滤。

  3. updateSurface:surface==null时打印堆栈。surface不为null不会导致异常。

  4. createTextureLayer:未初始化或者surface==null时打印堆栈。只有这两种可能会有异常。

总结:可以通过surface是否为null、initialize是否调用过这两个条件减少stackTrace。

在ThreadedRender中,surface都被透传给了native层,没有对应的Java引用,需要手动维护一个java 层的实例。初始化状态可以通过反射ThreadedRenderer.mInitialized拿到,不过既然已经hook intialize和destroy了,这里也选择手动维护一个初始化状态,毕竟可以减少一次反射调用。

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伪代码如下:

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,由于量级太小本文不做重点讨论。

ThreadedRender.Initialize之前

日志截图如下:

68f02ab211eadf3fd5d65c0f6582fd29.png

调用时序问题确认:

对于地址为0x814a0b0的RenderProxy实例,没有它intialize相关调用日志,只有一条createTextureLayer调用日志。可以明确,这个RenderProxy实例是在initialize之前调用createTextureLayer导致闪退!

Java 堆栈分析:

Log.d会对过长的堆栈进行截取,FrameLayout.onMeasure之前的都被截取了,不过对于排查问题,影响不大。

堆栈关键信息整理如下:

  1. 闪退时正在执行onMeasure

  2. 在com.bytedance.android.livesdk.chatroom.ui.LivePlayerWidget.loadSharedPlayer中调用了 TextureView的getBitmap方法 ,再到ThreadedRender.creatTextureLayer,之后就发生了Native crash。

为什么没有intialize?

onMeasure会早于ThreadedRender.initialize执行吗?

ThreadedRender.initialize和performMeasure相关的代码都在performTraversals中,再次回到源码中去分析。从代码结构来看,initialize前后都有measure相关操作。initialize之前通过measureHierarchy调用了performMeasure,initialize之后是直接调用performMeasure。由于measureHierarchy外部包了许多判断条件,所以不能直接从代码行的上下关系,得出measure早于initialize的结论,但我们可以保持这个怀疑进一步验证。

这个方法过于巨大,移除无关代码后如下:

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这种情况,即可验证。

本地通过onMeasure复现

本地写个demo,在FrameLayout.onMeasure中立即调用TextureView.getBitmap,并通过反射查看mFirst的值,找个6.0的云真机验证一下。onMeasure会连续执行多次,只有第一次的mFirst为true,但没能复现问题,代码如下:

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的实现,

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就可以满足。

根据猜想,再次编写代码终于 复现成功! 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时执行getBitmap,再次运行,不崩了。可见异常的关键条件就是mFirst!

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的异常。

总结起来:在android6.0上,ViewRootImpl首次performTraversals时,如过在onMeasure中调用了TextureView.getBitmap,就可能会发生这个异常。

线上还存在一些零星的destroy之后requireSurface、swapBuffers失败后requireSurface的异常,由于排查思路大同小异,这里就不展开说了。

修复方案

通过字节码插桩全局替换TextureView.getBitmap方法,当ViewRootImpl.mFirst=true时,就返回默认值而不执行getBitmap原有逻辑,这样就不会调用到ThreadedRender.createTextureLayer。

但由于mFirst只能通过反射获取,这可能会影响performTraversals性能,有没有性能更好的方案?

通过代码分析,发现performTraversals会经历layout阶段,而layout之后View会增加一个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;
}

因此,可以通过isLaidOut ()获取到这一个属性,达到和mFirst基本一致的效果。

最终方案:

插装替换全局TextureView.getBitmap调用,增加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;
}

修复效果

实验全量后requireSurface 相关crash明显下降,观察两周业务指标没有明显劣化,直播场景有正向收益,符合预期。全量后量级大幅下降,还剩下一小部分主要是老版本、以及一些少量的destroy、swapBuffer失败相关的问题。

业务收益:看播渗透显著提升;人均看播天数显著提升;

稳定性收益:Native Crash大幅下降

84e65b80902c4a424af26c17a6aef58b.png ff68340af3f8188ca69e6090cb1d42b6.png

后续思考

这个requireSurface问题发生在RenderThread,但造成问题的原因在主线程,因此如果能在RenderThread线程发生native crash时抓到主线程java堆栈,就可以定位到业务根因,也就不需要一系列自下而上地代码分析来寻找hook点了。

因此,后续有RenderThread线程异常时,应该把主线程堆栈上报上来,提高RenderThread问题的排查效率。

加入我们

我们是字节跳动西瓜视频客户端团队,专注于西瓜视频 App 的开发和基础技术建设,在客户端架构、性能、稳定性、编译构建、研发工具等方向都有投入。如果你也想一起攻克技术难题,迎接更大的技术挑战,欢迎点击阅读原文,或者投递简历到xiaolin.gan@bytedance.com。

最 Nice 的工作氛围和成长机会,福利与机遇多多,在上海和杭州均有职位,欢迎加入西瓜视频客户端团队 !

文章推荐

客户端架构设计的过程

Android D8编译器“bug”导致Crash的问题排查

Baseline Profile 安装时优化在西瓜视频的实践

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

西瓜视频RenderThread引起的闪退问题攻坚历程 的相关文章

  • pwn入门:基本栈溢出之ret2libc详解(以32位+64位程序为例)

    目录 写在开头 题目简介 解题思路 前置知识 简要了解 plt表和got表 延迟绑定 例题详解 32位 64位 总结与思考 写在开头 这篇博客早就想写了 但由于近期事情较多 一直懒得动笔 近期被领导派去临时给合作单位当讲师 要给零基础的学员
  • 毕业设计|SSM+VUE的旅游平台系统

    作者简介 Java领域优质创作者 CSDN博客专家 CSDN内容合伙人 掘金特邀作者 阿里云博客专家 51CTO特邀作者 多年架构师设计经验 腾讯课堂常驻讲师 主要内容 Java项目 Python项目 前端项目 人工智能与大数据 简历模板
  • 题解|#走多远# lanqiao 拓扑排序模板

    题解 走多远 lanqiao 拓扑排序模板 include
  • 干货!MES系统选型必须要考虑的9点要素!

    你所在的企业是否为这些问题困扰 纸质化管理混乱 物料供应不及时 设备数据难采集 生产进度难透明 如果是的话 你的企业需要MES系统的帮助 MES是制造执行系统 Manufacturing Execution System 的缩写 它是一种信
  • 波奇学Linux:Linux进程状态,进程优先级

    编写一个程序模拟进程 查看进程状态 修改代码后发现进程状态为由S 变成R R为运行态 S为阻塞态 第一次为S是因为调用了外设 printf调用屏幕外设 实际上应该为R S状态轮换 但是R太快了 所以每次查到了都为S R 为前台运行 运行ba
  • 如何挑选比较便宜的VPS省钱攻略

    在今天的数字时代 许多人需要强大的VPS来托管他们的网站 应用程序或在线业务 然而 VPS的价格可能会相当高昂 对于那些在预算有限的情况下寻找可行解决方案的人来说 这可能会成为一个挑战 在本文中 我们将分享一些有关如何挑选比较便宜的VPS的
  • 聚焦算力网络,Aginode焕新品牌内核

    12月8日 由Aginode 原耐克森通讯系统 主办的 算达今昔 智见未来 Aginode 2023智慧互联峰会 在上海成功举行 作为通讯和数据网络领域的一次技术盛会 本次峰会汇集了来自不同行业领域的专家学者 重点聚焦算力网络的发展趋势 向
  • CNS0创建交货单没有WBS元素

    1 问题 CNS0创建交货单带不出WBS 但是交货单过账之后 又可以读取到WBS 2 原因 2 1 项目挂料 创建项目挂料时 当物料为通用料 则在网络中挂料时 采购类型为网络预留 当物料为专用料 则在网络中挂料时 采购类型为WBS要素预留
  • 题解 | #查找入职员工时间排名倒数第三的员工所有信息#

    转转深圳这波操作是真的服了 转转毁了多少意向 阿里1688暑期实习Java二面 2023 3 15 阿里1688一面 24秋招总结 新生一学期敲70万行代码 秋招结束 前天谈薪 明天准备签了 傻叉研究生 人保财险一面 人保科技 二面 快过年
  • 题解 | #反转链表#很简单,新建节点插在末尾就行

    华为车bu最新情况汇总 华为车bu最新情况汇总 某小厂实习面经 已oc 华子开了 比亚迪vs华为 麻烦大家帮忙比较一下offer 帮选offer 秋招决赛圈 offer选择 太难了呜呜呜帮忙选一下Offer 晒一晒我的offer 38210
  • 亚洲VPS行业深度分析:如何选择最适合你的服务

    在数字化时代 亚洲地区的企业和个人越来越需要可靠的VPS服务 本文将深入分析亚洲VPS行业 以帮助你选择最适合你需求的服务 为什么选择VPS VPS是一种虚拟服务器 具有更高的性能和稳定性 适合网站托管 应用程序开发和数据存储等用途 在亚洲
  • 面试想拿 10K,HR 说你只值 7K,该怎样回答或者反驳?

    傲韦科技二面 拷打项目30分钟 随便聊20分钟1 shardingsphere是什么 怎么用的 分片键如何选择的 电话关联表和邮箱关联表为什么要分开搞 敏感信息加密存储 实 题目解析 题目描述 给出一个矩阵 求出其中的最大值及所在的行号和列
  • 题解|#E. Sending a Sequence Over the Network# cf round 826

    入职半年 成长颇多 秋招逃兵 2020秋招面经大汇总 旷视 回暖分析 战绩结算 on 赛文X 体制内离职 奔赴下一场山海 拒了华为 重回0 offer 体制内离职 奔赴下一场山海 拒了华为 重回0 offer 找实习需要实习经历 应届生谨慎
  • 龙蜥位列用户意愿迁移系统首位,中国操作系统要敢于“亮剑”

    让前路 起后浪 迷茫过 怀疑过 跌倒过 熄不灭 眼中的光 摘自歌曲 行者为光 就像歌中所唱 国内操作系统的演进虽然是夹缝中求生存 却始终前赴后继 矢志不渝 从而逐步实现替代甚至跃升的过程 曾经占据市场主流的CentOS停服 像是一个信号 一
  • CN2 VPS引领行业革命:为什么每个企业都需要它

    在当今数字化时代 企业面临着巨大的在线竞争压力 为了在市场中脱颖而出并确保业务的可持续增长 可靠的网络基础设施是至关重要的 这就是为什么越来越多的企业正在转向CN2 VPS的原因 这一技术正在引领行业革命 为企业提供了更高效 更安全 更可靠
  • 24届友友们还想去腾讯!?哪些岗位还在热招?看这一篇就足够!

    分享虾皮Shopee面经 iOS方向 Shopee ios开发面经 广东工业大学电子信息硕士这么好找工作吗 秋招补录公司直投链接汇总 被鸽了32个sp 太难了 求大家投小米吧 华为给我oc了 问我毁不毁约 2023年最佳雇主榜单评选结果出来
  • 工业互联网会是制造业数字化发展的未来吗?

    新工业革命正逐步形成 你认为工业互联网会是制造业数字化发展的未来吗 从这几年的工业互联网发展政策措施 就能看到答案 早在近三年前 工业互联网创新发展行动计划 2021 2023年 就提出了五方面 11项重点行动和10大重点工程 着力解决工业
  • 在喜马拉雅直播运营mentor超级nice,弹性工作制

    12月校招补录开启 C 大类岗位12 14已更新 华子开奖 好未来测开一二三四面面经 联想 测开一面面经 攒人品 联想 4 13测开 面试 联想测开一面 联想测开二面 联想测开 面试 联想测开一面 联想测开一面 base天津 联想测开一二面
  • 国外免费VPS真的零成本云计算体验?

    在当今数字化时代 云计算已经成为企业和个人的热门选择之一 云计算提供了便捷 灵活和经济高效的计算资源 可以帮助人们处理数据 托管网站 运行应用程序等等 然而 对于一些初学者来说 使用云计算可能会变得有些昂贵 但是 有一种方式可以让您尝试云计
  • 工业级路由器在风力发电场的远程监控技术

    工业级路由器在风力发电场的远程监控技术方面具有重要的应用意义 风力发电场通常由分布在广阔地区的风力发电机组组成 需要进行实时监测 数据采集和远程管理 工业级路由器作为网络通信设备 能够提供稳定可靠的网络连接和多种远程管理功能 对于风力发电场

随机推荐

  • 开源、免费、可私有部署的在线多人协同办公系统推荐?

    开源 免费 可私有部署的在线多人协同办公系统推荐 在考虑选择在线多人协同办公系统时 免费且开源的方案看似经济实惠 但实际上可能存在一些潜在 隐性成本 比如在系统的维护 安全性 定制化需求以及技术支持方面可能会有额外的支出 企业在选择系统时应
  • 张正友相机标定法原理与实现

    张正友相机标定法是张正友教授1998年提出的单平面棋盘格的相机标定方法 传统标定法的标定板是需要三维的 需要非常精确 这很难制作 而张正友教授提出的方法介于传统标定法和自标定法之间 但克服了传统标定法需要的高精度标定物的缺点 而仅需使用一个
  • Java_Mybatis_缓存

    缓存 1 概述 Mybatis 缓存 MyBatis 内置了一个强大的事务性查询缓存机制 它可以非常方便地配置和定制 2 会话缓存 一级缓存 sqlSession 级别的 也就是说 使用同一个 sqlSession 查询同一 sql 时 直
  • 字节电商双11 大促容量保障是如何做的?

    前言 Rhino 简介 Rhino是字节自研全链路容量评估产品 致力于构建完整的全链路容量评估解决方案 覆盖 容量预估 gt 资源准备 gt 数据准备 gt 容量验证 gt 监控 gt 分析 gt 决策 gt 处理反馈 围绕容量在稳定性 成
  • 《VirtualLab Fusion物理光学实验教程》好书分享

    目录 第一章 物理光学概念介绍 6 1 1 几何光学和光线追迹 6 1 2 物理光学和光场追迹 6 1 3 电场 磁场以及坡印廷矢量 8 1 4 振幅 相位及实部和虚部 9 1 5 振幅 相位与偏振 10 1 6菲涅尔公式 11 1 7 全
  • SqlServer_更改数据类型

    SQL server修改数据类型 使用以下命令可以进行修改 Alter table monthly import demand alter column ImportWeight DECIMAL 18 4
  • 3、Linux_系统用户管理

    1 Linux 用户管理 1 1概述 Linux系统是一个多用户多任务的操作系统 任何一个要使用系统资源的用户 都必须首先向系统管理员申请一个账号 然后以这个账号的身份进入系统 root 用户是系统默认创建的管理员账号 1 2添加用户 语法
  • Java_EasyExcel_导入_导出Java-js

    easyExcel导入 从easyexcel官网中拷贝过来 使用到的 这是使用监听器的方法 EasyExcel read file getInputStream BaseStoreDataExcelVo class new ReadList
  • 利用迈克尔逊干涉仪和傅里叶变换光谱法测量相干性

    摘要 在干涉仪中 条纹的对比度可能取决于光源的相干特性 例如 在具有一定带宽的光源的迈克尔逊干涉仪中 干涉条纹对比度随光程差的不同而变化 通过测量可动镜不同位置的干涉图对比度 可以得到光源的相干长度 典型的傅里叶变换光谱通常基于这种类型的光
  • 千呼万唤始出来!《“外研社·国才杯”竞赛一本通》焕新上线!

    写在前面 作为连续五年入选 全国高校大学生竞赛榜 的比赛 外研社 国才杯 理解当代中国 全国大学生外语能力大赛 含金量可想而知 对于想要保研的计算机er来说 能够在这样 规模大 影响力高 的比赛中 取得一定的成绩 必定会拉开自己与他人的差距
  • Java_Mybatis_动态SQL

    一 动态SQL 1 概述 动态SQL 是 MyBatis 的强大特性之一 解决拼接动态SQL时候的难题 提高开发效率 分类 if choose when otherwise trim where set foreach 2 if 做 whe
  • Java_原生api分页

    Java分页 当 mybatis plus 分页插件无法使用时 就可以使用 Java 中的 api 进行分页 本次使用的是通过stream流的方法进行分页 List
  • 参数扫描文件的用法

    摘要 通过控制和改变所选参数有助于检查给定光学系统的性能 VirtualLab Fusion提供了完全灵活且计算效率高 通过并行化 的参数运行 使用户可以指定不同的参数变化方式 作为示例 它可以用于所研究的任何系统参数的公差分析 分析结果可
  • Linux_vi/vim编辑器

    3 VI 与 VIM 3 1概述 vi编辑器 是Linux和Unix上最基本的文本编辑器 工作在字符模式下 由于不需要图形界面 vi是效率很高的文本编辑器 vim是 vi的增强版 比vi更容易使用 vi的命令几乎全部都可以在vim上使用 3
  • 好书推荐《VirtualLab Fusion入门与进阶实用教程(第二版)》

    目 录 第一章 VirtualLab Fusion理论基础 1 1 1 几何光学和光线追迹 1 1 2 物理光学和光场追迹 1 1 2 1 统一场追迹 3 1 2 2 第二代场追迹 6 第二章 VirtualLab Fusion安装与更新
  • Java_mybatis-结果集映射-ResultType&ResultMap

    Mybatis返回值接收 可以使用两种方式进行参数的接收 resultType resultMap 这两种分别都是需要在Mapper xml文件中去设置的 当结果是一个简单的对象或者 list 或者 map 对象中没有嵌套对象 或者集合时
  • vue_table导出excel

    vue直接导出对应table const exportClick gt const wb XLSX utils table to book document querySelector newTables 关联dom节点 get binar
  • vue_域名部署无法访问后端

    前言 目前部署的比较另类 因为服务器为 windows 目前还不是很会 nginx 所以现在就只能在服务器上安装 nodejs 然后直接使用 npm run dev 命令行的方式运行项目 遇到的坑 使用ip访问前端的时候 就可以访问 但是通
  • 题解 | #密码合格# 把条件三“包含公共元素”去掉就好理解

    题解 E Easy problem E Easy problem刚刚随便交了一发过了 来讲下自己的做法 一眼没构出来 但是考虑到1e18的数有18位数 显然rand足够多次 大概率是可以得到 题解 考试分数 四 select job a d
  • 西瓜视频RenderThread引起的闪退问题攻坚历程

    背景 影响 西瓜之前存在过一类RenderThread闪退 从堆栈上看 全部都是系统so调用 给人的第一印象像是一个系统bug 无从下手 闪退集中在Android 5 6上 表现为打开直播间立即闪退 该问题在2022年占据Native Cr