布景

影响

西瓜之前存在过一类RenderThread闪退,从仓库上看,悉数都是体系so调用,给人的第一印象像是一个体系bug,无从下手。闪退会集在Android 5~6上,表现为打开直播间当即闪退。该问题在2022年占据Native Crash Top5,2023年更是上升到到Top1。因而有必要投入时刻和精力再重新审视一下这个问题。在历经多周的源码剖析和排查后,逐步清晰了问题根因并修正,终究取得了显着的稳定性收益和事务收益。

接下来,咱们将抽丝剥茧,一步步深入剖析这个前史遗留问题,揭开它背后实在的原因。

根本信息

详细仓库如下:

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

仓库都是体系的so调用,不能清晰详细闪退事务场景,只能看出是RenderThread线程自动abort了。

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

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

问题特征:

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

RenderThread简介

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

相关类图如下:

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

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

RenderThread::threadLoop

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

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

ThreadedRender

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

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

RenderProxy

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

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

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

CanvasContext

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

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

初步猜测

其他 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根本共同。

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

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

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

那6.0上的abort原来是能够直接删掉的吗?假如是这样,咱们是不是能够有样学样,测验hook requireSurface,把abort去掉,再自动调用一下mEglManager.initialize,从而到达这个commit相似的修正作用?

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

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

再测验一下hook requireSurface的上一层调用CanvasContext::createTextureLayer,发现也没有对应的符号,只需RenderProxy::createTextureLayer 的符号。假如采用7.0的修正计划,需求修正的指令十分多。而且,这个MR中还有其他改动,要不要一同hook?这些问题暂时还没搞清楚,花大力气去做的话风险太高。

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

看起来,参考7.0修正计划操作难度大,而且不确定是否有用。咱们先把它作为备选计划,另谋出路!

正面剖析三种或许性

多线程问题?

CanvasContext的createTextureLayer和setSurface相关代码,都被RenderProxy搬运到了RenderThread上履行,而RenderThread又是单例的,所以这儿不存在多线程问题,因而能够直接扫除线程并发问题

setSurface(null)导致?

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

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

能够看出在CanvasContext中,setSurface的调用有:initialize、swapBuffers、makeCurrent、updateSurface、destory、~ConvasContext。对应的java层调用为ThreadedRender中的的initialize、draw、createTextureLayer、updateSurface、destory、finalize办法。扫除一些不会呈现反常的办法:

扫除ThreadedRender.initialize

initialize时java层传过来的surface做了判别保护,能够确保surface不为nullptr,因而能够扫除initialize,代码如下:

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

扫除ThreadedRender.draw

draw对应的问题是swapBuffers失利。产生在swapBuffers失利时,也便是eglSwapBuffers过错了。

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

体系有以下两种办法处置过错:

  1. EGL_BAD_SURFACE:打印失利日志。但翻看多个盯梢日志,均没有发现相似日志,暂时不重视。
  2. 其他EGL过错:直接abort掉,此时变成别的一个闪退,因而能够扫除。

综上,关于swapBuffers失利这种状况或许存在,但未发现相关报错日志,暂时不作过多重视。相关代码如下:

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

扫除ThreadedRender.finalize

ThreadedRender.finalize之后,会在native层经过delete 释放RenderProxy和ConvasContext,在~ConvasContext析构时调用setSurface(nullptr)。因而,假如之后再调用requireSurface,应该会产生SIGSEGV相关过错,不或许呈现surface not set反常。因而,也能够扫除掉。代码如下:

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

剩下状况

扫除了intialize、swapBuffers、finalize之后,还剩下makeCurrent失利、updateSurface(nullptr)、destory都有或许产生问题,上游调用比较多追寻起来仍然比较困难,暂时无法扫除。

初始化前触发了requireSurface?

一般来说,setSurface的初次调用是在initialize中。那么,假如在initialize之前就调用了requireSurface,是不是就会出问题呢?早年面的剖析能够看出,requireSurface的上游是java层的createTextureLayer,而createTextureLayer的调用途只需一个,在TextureView的getHardwareLayer中。

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

getHardwareLayer是View的一个办法,默认回来null。从注释上也能看出,在6.0上只需TextureView用到了这个办法,调用途也只需移除在getBitmap中。在7.0上也是直接把getHardwareLayer从View中移除了,变成TextureView的一个办法。而getBitmap是个public办法,这儿是能够被app调用到的。

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

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

闪退的前提条件:

  1. ThreadedRender.initialize还未调用
  2. ThreadedRender.destroy或许ThreadedRender.updateSurface(null)之后

在Java层触发requireSurface步骤如下:

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

终究定位

验证剖析定论

经过前面的剖析,找到了问题的前提条件,并发现了一条触发requireSurface的办法。那么,就能够完毕纸上谈兵,经过实操来在本地复现这个闪退,来实锤前面的定论。

ThreadedRender.initialize还未调用

因为ThreadedRenderer不是公开api,需求经过反射来创立实例。拿到实例后不调用其intialize办法,直接反射调用createTextureLayer。代码如下:

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

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

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

destroy或updateSurface(null)

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

事务场景定位

前面的复现,只是从技能层面承认了问题产生的几种或许,但还没有与事务场景相关起来。实在的问题是否在前面提到的这几种或许中间?假如在的话,那详细的调用点在哪,又该怎么修正?

测验在实在场景中复现

经过shaddow hook RenderProxy的Initialize、destroy、updateSurface、createTextureLayer等函数,在hook函数中打印一些日志。因为RenderProxy或许存在多个实例,需求在日志里加上RenderProxy实例的地址来便利追寻单个RenderProxy调用时序。

hook函数如下:

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

尽管这个问题在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之前

日志截图如下:

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

调用时序问题承认:

关于地址为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大幅下降

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

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

后续考虑

这个requireSurface问题产生在RenderThread,但形成问题的原因在主线程,因而假如能在RenderThread线程产生native crash时抓到主线程java仓库,就能够定位到事务根因,也就不需求一系列自下而上地代码剖析来寻觅hook点了。

因而,后续有RenderThread线程反常时,应该把主线程仓库上报上来,进步RenderThread问题的排查效率。

参加咱们

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

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

文章引荐

客户端架构设计的进程

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

Baseline Profile 装置时优化在西瓜视频的实践 文章引荐 客户端架构设计的进程