小工具知识网

当前位置: 首页 > 行业知识 > 基于Perfetto 解读一帧的生产消费流程 Android >= S Qualcomm

行业知识

基于Perfetto 解读一帧的生产消费流程 Android >= S Qualcomm

2026-11-03 11:01:43 来源:互联网转载

广告

首先帮我朋友打个广告 我们一起在运营一个视频号 感兴趣的可以帮忙点击右边这个小铃铛 铃铛

序

1.这个流程里面的东西如果展开其实是有很多的 内容其实还是比较浅显的 sf处就不贴源码了 关一个Vsync就有的解释 当然笔者在流程上先形成一个思维闭环

2.如有小伙伴需要 笔者可提供所有原材料供二次编辑

先吐槽

其实我觉得大部分Android开发者都是聚集在上层 java层 或者说的具体点就是业务层 app层 始终没有脱离业务场景

我对应用开发范围的定义是 不限于hal层 c++代码实现层 只要涉及到业务场景的 都是应用开发

随着工作中遇到的一些00后 水平是真的不错 在这里也提醒那些80后90后 快了奥 小心被挤下来 逆水行舟 不进则退 出来混是要还的

本文阐述的预期

1.view的绘制流程 以及 送显到屏幕一整个过程

2.trace的分析方法

3.因为很多看似一点思路都没有的问题 其实是基础不够牢靠 希望笔者接下来的阐述 前期可以让大家节省多的熟悉成本

一.从一个view的setText开始

1.1view开始setText

Button btnTraceClick = findViewById(R.id.btn_trace_click);    btnTraceClick.setOnClickListener(new View.OnClickListener() {        @Override        public void onClick(View v) {            Trace.beginSection("super.yu click#btn test");            btnTraceClick.setText("帅是内在 但骚不是");            Trace.endSection();        }});

可以看到2处 是加上去的trace setText就从这里开始 是会走下去请求vsync-app 即app主线程有更新ui的请求 但此时没有往下走 因为1处已经有一个requestNextVsync vsync-app的请求 等待sf进程回调上来 Choreographer#onVsync 告诉app可以doFrame 此时才会绘制 4处是线程运行状态

如果长时间的runnable或runnable preempted或running状态 60帧 超过16.6ms 那就可以看做是一个卡顿或掉帧 优化的思路可以是此处cpu有哪个进程运行时间较长 app线程得不到调度 负载较高 找对应模块的人分析 或修改优先级 等 如果是system_server例如binder 锁竞争 耗时 则要通过阅读源码去定位 或 app自身是否存在主线程耗时 出现诸如 下述log 考虑是否mainthread有耗时操作 ui结构过于复杂等等 思路不仅限于此 在Perfetto可以很直观的看出来

I/Choreographer: Skipped 196 frames! The application may be doing too much work on its main thread. 

分别对应2和3处

此时由于已经在1处requestNextVsync vsync-app请求 在2更新ui就不会往下走 所以只会有句scheduleTraversals 所以3处的onVsync回调其实是上一次ui更新请求的 所以ui的请求一直到屏幕显示至少得在第二个vsync信号到来

我们从这里的vsync请求往下赘述也是对应1处

/frameworks/base/core/java/android/view/ViewRootImpl.java#scheduleTraversalsvoid scheduleTraversals() {    if (!mTraversalScheduled) {    mTraversalScheduled = true;    // 发送一个屏障信号 下次loop来 doFrame    mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();    // 编舞者 post发送请求    mChoreographer.postCallback(    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);    ...
/frameworks/base/core/java/android/view/Choreographer.java#postCallbackDelayedInternalprivate void postCallbackDelayedInternal(int callbackType,              Object action, Object token, long delayMillis) {    ...    synchronized (mLock) {    final long now = SystemClock.uptimeMillis();    final long dueTime = now + delayMillis;    mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);    // dueTime 肯定是大于或等于now 所以除了首次一个loop会直接走这里 其他情况会走下述的msg    if (dueTime requestNextVsync();        return NO_ERROR;    }    return mInitError.has_value() ? mInitError.value() : NO_INIT;}// EventThread这是软件模拟硬件vsync 后续会讲到frameworks/native/services/surfaceflinger/Scheduler/EventThread.cppbinder::Status EventThreadConnection::requestNextVsync() {    ATRACE_CALL();    mEventThread->requestNextVsync(this);    return binder::Status::ok();}

接下来再来个图

大家要注意的是 我们目前为止setText的渲染其实是1处右边那代码块 此处还是之前的ui更新操作

1处ui线程ui就开始绘制了 值得注意的是 1处如果draw时长超过16.6ms那么大概率就是应用本身阻塞主线程 我们把trace堆栈放大

这一步我理解是遍历 比如animation input啊 有哪些 measure layout丈量等 是把ui结构进行数据化 比如这个view的坐标 color等

这里是引用一篇博客里面的解释 但我的理解就是 为了后续的遍历而去组织数据结构 分门别类Choreographer.doFrame 计算掉帧逻辑Choreographer.doFrame 处理 Choreographer 的第一个 callback : inputChoreographer.doFrame 处理 Choreographer 的第二个 callback : animationChoreographer.doFrame 处理 Choreographer 的第三个 callback : insets animationChoreographer.doFrame 处理 Choreographer 的第四个 callback : traversal

setRefreshRateIfNeed这个应该是手机厂家提供出的接口 不管 traversal 他就是遍历 draw 就是 draw 着重介绍一下postAndWait 这里就会到RenderThread 应用的渲染线程 postAndWait唤醒线程的run 此时我们进入到应用的RenderThread 拓展一下 如果是游戏进程的话 一般是unitymain gfx线程 flutter为什么会比rn要快 因为他直接和sf打交道 不需要再转换一层 想了解的可以看看官方的架构图 流程继续 这里要注意的是 这里的执行顺序是从左往右 单独模块从上到下执行 然后再回到起始点 往右执行

// 代码太多 不一一解释 这里就是把一个frame组织成一个结构体 cpu/gpu可读懂的结构体frameworks/base/libs/hwui/renderthread/DrawFrameTask.cppvoid DrawFrameTask::postAndWait() {    ATRACE_CALL();    AutoMutex _lock(mLock);    mRenderThread->queue().post([this]() { run(); });    mSignal.wait(mLock);}// 从这里我们就可以看到我们熟悉的canvas 当然真正的渲染不是在java进行的// dequeueBufferDuration 这里有个queue buffer的轮转 我们后续分析void DrawFrameTask::run() {    const int64_t vsyncId = mFrameInfo[static_cast(FrameInfoIndex::FrameTimelineVsyncId)];    ATRACE_FORMAT("DrawFrames %" PRId64, vsyncId);   ...    // Grab a copy of everything we need    CanvasContext* context = mContext;    nsecs_t dequeueBufferDuration = 0;    if (CC_LIKELY(canDrawThisFrame)) {        dequeueBufferDuration = context->draw();    } else {       ......}

这里postAndWait后会到2处 也就是自身的渲染线程了 但是2处就是渲染个寂寞 真正渲染的地方是在3处 我们把2处放大一下

我们现在到应用出帧的地方 也就是renderthread 可以看出 DrawFrames 66363537 和上述1处 Choreographer#doFrame 66363537 id是一样的 但是没有进行渲染 是cpu在执行其他线程 没有得到调度 是因为该进程中的一个线程在初始化 有一定的负载

dequeueBuffer - VRI[MainActivity]#0(BLAST Consumer)0 此处MainActivity应该是Producter才对 不应该是Consumer 在这里我们需要引入两个知识点BufferQueue和GPU Fence

// ********** @引用_start 努比亚技术团队**********

BufferQueue要解决的是生产者和消费者的同步问题 应用程序生产画面 SurfaceFlinger消费画面 SurfaceFlinger生产画面而HWC Service消费画面 用来存储这些画面的存储区我们称其为帧缓冲区buffer

在BufferQueue的设计中 一个buffer的状态有以下几种:

FREE:表示该buffer可以给到应用程序 由应用程序来绘画

DEQUEUED:表示该buffer的控制权已经给到应用程序侧,这个状态下应用程序可以在上面绘画

QUEUED: 表示该buffer已经由应用程序绘画完成 buffer的控制权已经回到SurfaceFlinger手上

ACQUIRED:表示该buffer已经交由HWC Service去合成了 这时控制权已给到HWC Service

FREE->DEQUEUED->QUEUED->ACQUIRED->FREE

CPU和GPU的工作完全是异步的 Fence提供了一种方式来处理不同硬件对共享资源的访问控制

// ********** @引用_end 努比亚技术团队**********

其实真正渲染的地方是在3处 我们放大一下 渲染线程中我们只需要重点了解dequeuebuffer和queuebuffer

此时 应用的renderThread从自身的bufferqueue申请一块buffer用来绘制 需要注意的是从R之后为了分担sf的压力 bufferqueue都在各自应用进程里进行 所以dequeuebuffer此处没有binder调用dequeueBuffer 拿一块buffer的地址下标 也就是往结构体填充指令的数组 下图放大

/frameworks/native/libs/gui/BufferQueueProducer.cppstatus_t BufferQueueProducer::dequeueBuffer(int* outSlot, sp* outFence,                                              uint32_t width, uint32_t height, PixelFormat format,                                              uint64_t usage, uint64_t* outBufferAge,                                              FrameEventHistoryDelta* outTimestamps) {      ATRACE_CALL();      { // Autolock scope          std::lock_guard lock(mCore->mMutex);          // trace中 dequeueBuffer - VRI[MainActivity]#0(BLAST Consumer)0 也是此处的拼接          mConsumerName = mCore->mConsumerName;...

VRI[MainActivity]#0(BLAST Consumer)0: 0 这里的solt是0

在dequeuebuffer 右边还有一句 HWC release fence 19 has signaled 这里dequeuebuffer后这个solt地址并不是立即就往上填充数据 是要等待 GPU释放对应的Fence 只是告诉你我要释放了 相当于bt模组和modem 你请求查询数据 然后模组告诉你有数据了 然后你还得调用个get请求去获取这些数据

接下来就是queuebuffer部分 图片部分放大

// 表示hwc release fence 19 buffer 还给了bufferqueue 但gpu还没有绘制完Trace GPU completion fence 19// 将绘制好的buffer返回SurfacefingereglSwapBuffersWithDamageKHRstatus_t BufferQueueProducer::queueBuffer(int slot,          const QueueBufferInput &input, QueueBufferOutput *output) {      ATRACE_CALL();      ATRACE_BUFFER_INDEX(slot);        int64_t requestedPresentTimestamp;      bool isAutoTimestamp;      android_dataspace dataSpace;      Rect crop(Rect::EMPTY_RECT);      int scalingMode;      ...      if (frameAvailableListener != nullptr) {              // 按照需要回调至app层              frameAvailableListener->onFrameAvailable(item);              ...      ...

此时就会到SurfaceFlinger进程 值得注意的是 BufferQueue 可以看BLASTBufferQueue

waiting for presentFence 699 可以看出kernel消费情况

所以 setText 开始到显示 最终是这样的

其实整体内容还是比较浅显的 sf这块还是比较复杂的 设计的场景有点多 后续也只能找一个闭环去用贴源码解释

感谢观看

参考文献


  1. https://blog.csdn.net/rzleilei/article/details/94639329
  2. 作者:努比亚技术团队

    链接:https://www.jianshu.com/p/3c61375cc15b

    来源:简书

    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


上一篇:酷酷服务器怎么样?

下一篇:宝塔控制面板广告挂载秘籍,高效收益,轻松上手!