Systrace 基础知识 - MainThread 和 RenderThread 解读

本文是 Systrace 系列文章的第五篇,主要是是介绍 Android App 的 MainThread 和 RenderThread,也就是你们熟悉的主线程和渲染线程。文章会从 Systrace 的角度来看 MainThread 和 RenderThread 的工做流程,以及涉及到的相关知识:卡顿、软件渲染、掉帧计算等html

本系列的目的是经过 Systrace 这个工具,从另一个角度来看待 Android 系统总体的运行,同时也从另一个角度来对 Framework 进行学习。也许你看了不少讲 Framework 的文章,可是老是记不住代码,或者不清楚其运行的流程,也许从 Systrace 这个图形化的角度,你能够理解的更深刻一些java

系列文章目录

  1. Systrace 简介
  2. Systrace 基础知识 - Systrace 预备知识
  3. Systrace 基础知识 - Why 60 fps ?
  4. Systrace 基础知识 - SystemServer 解读
  5. Systrace 基础知识 - SurfaceFlinger 解读
  6. Systrace 基础知识 - Input 解读
  7. Systrace 基础知识 - Vsync 解读
  8. Systrace 基础知识 - MainThread 和 RenderThread 解读
  9. Systrace 基础知识 - Triple Buffer 解读
  10. Systrace 基础知识 - CPU Info 解读
  11. Systrace 实战 - 分析应用冷启动时间问题
  12. Systrace 实战 - 分析应用热启动时间问题
  13. Systrace 实战 - 分析列表卡顿问题
  14. Systrace 实战 - 分析窗口动画卡顿问题
  15. Systrace 实战 - 分析进程乱跑致使的性能问题
  16. Systrace 实战 - 分析 IO 致使的性能问题
  17. Systrace 实战 - 分析 Memory 致使的性能问题
  18. Systrace 实战 - Systrace 与 MethodTrace 结合使用
  19. Systrace 实战 - 分析硬件加速问题

正文

这里以滑动列表为例 ,咱们截取主线程和渲染线程一帧的工做流程(每一帧都会遵循这个流程,不过有的帧须要处理的事情多,有的帧须要处理的事情少) ,重点看 “UI Thread ” 和 RenderThread 这两行android

图片

这张图对应的工做流程以下git

  1. 主线程处于 Sleep 状态,等待 Vsync 信号
  2. Vsync 信号到来,主线程被唤醒,Choreographer 回调 FrameDisplayEventReceiver.onVsync 开始一帧的绘制
  3. 处理 App 这一帧的 Input 事件(若是有的话)
  4. 处理 App 这一帧的 Animation 事件(若是有的话)
  5. 处理 App 这一帧的 Traversal 事件(若是有的话)
  6. 主线程与渲染线程同步渲染数据,同步结束后,主线程结束一帧的绘制,能够继续处理下一个 Message(若是有的话,IdleHandler 若是不为空,这时候也会触发处理),或者进入 Sleep 状态等待下一个 Vsync
  7. 渲染线程首先须要从 BufferQueue 里面取一个 Buffer(dequeueBuffer) , 进行数据处理以后,调用 OpenGL 相关的函数,真正地进行渲染操做,而后将这个渲染好的 Buffer 还给 BufferQueue (queueBuffer) , SurfaceFlinger 在 Vsync-SF 到了以后,将全部准备好的 Buffer 取出进行合成(这个流程在讲 SurfaceFlinger 的时候会提到)

上面这个流程在 Android 基于 Choreographer 的渲染机制详解 这篇文章里面已经介绍的很详细了,包括每一帧的 doFrame 都在作什么、卡顿计算的原理、APM 相关. 没有看过这篇文章的同窗,建议先去扫一眼github

那么这篇文章咱们主要从 Android 基于 Choreographer 的渲染机制详解 这篇文章没有讲到的几个点来入手,帮你更好地理解主线程和渲染线程数据库

  1. 主线程的发展
  2. 主线程的建立
  3. 渲染线程的建立
  4. 主线程和渲染线程的分工
  5. 游戏的主线程与渲染线程
  6. Flutter 的主线程和渲染线程

主线程的建立

Android App 的进程是基于 Linux 的,其管理也是基于 Linux 的进程管理机制,因此其建立也是调用了 fork 函数浏览器

frameworks/base/core/jni/com_android_internal_os_Zygote.cppbash

pid_t pid = fork();
复制代码

Fork 出来的进程,咱们这里能够把他看作主线程,可是这个线程尚未和 Android 进行链接,因此没法处理 Android App 的 Message ;因为 Android App 线程运行基于消息机制 ,那么这个 Fork 出来的主线程须要和 Android 的 Message 消息绑定,才能处理 Android App 的各类 Message网络

这里就引入了 ActivityThread ,确切的说,ActivityThread 应该起名叫 ProcessThread 更贴切一些。ActivityThread 链接了 Fork 出来的进程和 App 的 Message ,他们的通力配合组成了咱们熟知的 Android App 主线程。因此说 ActivityThread 其实并非一个 Thread,而是他初始化了 Message 机制所须要的 MessageQueue、Looper、Handler ,并且其 Handler 负责处理大部分 Message 消息,因此咱们习惯上以为 ActivityThread 是主线程,其实他只是主线程的一个逻辑处理单元。app

ActivityThread 的建立

App 进程 fork 出来以后,回到 App 进程,查找 ActivityThread 的 Main函数

com/android/internal/os/ZygoteInit.java

static final Runnable childZygoteInit(
        int targetSdkVersion, String[] argv, ClassLoader classLoader) {
    RuntimeInit.Arguments args = new RuntimeInit.Arguments(argv);
    return RuntimeInit.findStaticMain(args.startClass, args.startArgs, classLoader);
}
复制代码

这里的 startClass 就是 ActivityThread,找到以后调用,逻辑就到了 ActivityThread的main函数

android/app/ActivityThread.java

public static void main(String[] args) {
    //1. 初始化 Looper、MessageQueue
    Looper.prepareMainLooper();
    // 2. 初始化 ActivityThread
    ActivityThread thread = new ActivityThread();
    // 3. 主要是调用 AMS.attachApplicationLocked,同步进程信息,作一些初始化工做
    thread.attach(false, startSeq);
    // 4. 获取主线程的 Handler,这里是 H ,基本上 App 的 Message 都会在这个 Handler 里面进行处理 
    if (sMainThreadHandler == null) {
        sMainThreadHandler = thread.getHandler();
    }
    // 5. 初始化完成,Looper 开始工做
    Looper.loop();
}
复制代码

注释里面都很清楚,这里就不详细说了,main 函数处理完成以后,主线程就算是正式上线开始工做,其 Systrace 流程以下:

图片

ActivityThread 的功能

另外咱们常常说的,Android 四大组件都是运行在主线程上的,其实这里也很好理解,看一下 ActivityThread 的 Handler 的 Message 就知道了

class H extends Handler { //摘抄了部分
    public static final int BIND_APPLICATION        = 110;
    public static final int EXIT_APPLICATION        = 111;
    public static final int RECEIVER                = 113;
    public static final int CREATE_SERVICE          = 114;
    public static final int STOP_SERVICE            = 116;
    public static final int BIND_SERVICE            = 121;
    public static final int UNBIND_SERVICE          = 122;
    public static final int DUMP_SERVICE            = 123;
    public static final int REMOVE_PROVIDER         = 131;
    public static final int DISPATCH_PACKAGE_BROADCAST = 133;
    public static final int DUMP_PROVIDER           = 141;
    public static final int UNSTABLE_PROVIDER_DIED  = 142;
    public static final int INSTALL_PROVIDER        = 145;
    public static final int ON_NEW_ACTIVITY_OPTIONS = 146;
}
复制代码

能够看到,进程建立、Activity 启动、Service 的管理、Receiver 的管理、Provider 的管理这些都会在这里处理,而后进到具体的 handleXXX

图片

渲染线程的建立和发展

主线程讲完了咱们来说渲染线程,渲染线程也就是 RenderThread ,最初的 Android 版本里面是没有渲染线程的,渲染工做都是在主线程完成,使用的也都是 CPU ,调用的是 libSkia 这个库,RenderThread 是在 Android Lollipop 中新加入的组件,负责承担一部分以前主线程的渲染工做,减轻主线程的负担

软件绘制

咱们通常提到的硬件加速,指的就是 GPU 加速,这里能够理解为用 RenderThread 调用 GPU 来进行渲染加速 。 硬件加速在目前的 Android 中是默认开启的, 因此若是咱们什么都不设置,那么咱们的进程默认都会有主线程和渲染线程(有可见的内容)。咱们若是在 App 的 AndroidManifest 里面,在 Application 标签里面加一个

android:hardwareAccelerated="false"
复制代码

咱们就能够关闭硬件加速,系统检测到你这个 App 关闭了硬件加速,就不会初始化 RenderThread ,直接 cpu 调用 libSkia 来进行渲染。其 Systrace 的表现以下

图片

与这篇文章开头的开了硬件加速的那个图对比,能够看到主线程因为要进行渲染工做,因此执行的时间边长了,也更容易出现卡顿,同时帧与帧直接的空闲间隔也变短了,使得其余 Message 的执行时间被压缩

硬件加速绘制

正常状况下,硬件加速是开启的,主线程的 draw 函数并无真正的执行 drawCall ,而是把要 draw 的内容记录到 DIsplayList 里面,同步到 RenderThread 中,一旦同步完成,主线程就能够被释放出来作其余的事情,RenderThread 则继续进行渲染工做

图片

渲染线程初始化

渲染线程初始化在真正须要 draw 内容的时候,通常咱们启动一个 Activity ,在第一个 draw 执行的时候,会去检测渲染线程是否初始化,若是没有则去进行初始化

android/view/ViewRootImpl.java

mAttachInfo.mThreadedRenderer.initializeIfNeeded(
        mWidth, mHeight, mAttachInfo, mSurface, surfaceInsets);
复制代码

后续直接调用 draw

android/graphics/HardwareRenderer.java

mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this);
void draw(View view, AttachInfo attachInfo, DrawCallbacks callbacks) {
    final Choreographer choreographer = attachInfo.mViewRootImpl.mChoreographer;
    choreographer.mFrameInfo.markDrawStart();

    updateRootDisplayList(view, callbacks);

    if (attachInfo.mPendingAnimatingRenderNodes != null) {
        final int count = attachInfo.mPendingAnimatingRenderNodes.size();
        for (int i = 0; i < count; i++) {
            registerAnimatingRenderNode(
                    attachInfo.mPendingAnimatingRenderNodes.get(i));
        }
        attachInfo.mPendingAnimatingRenderNodes.clear();
        attachInfo.mPendingAnimatingRenderNodes = null;
    }

    int syncResult = syncAndDrawFrame(choreographer.mFrameInfo);
    if ((syncResult & SYNC_LOST_SURFACE_REWARD_IF_FOUND) != 0) {
        setEnabled(false);
        attachInfo.mViewRootImpl.mSurface.release();
        attachInfo.mViewRootImpl.invalidate();
    }
    if ((syncResult & SYNC_REDRAW_REQUESTED) != 0) {
        attachInfo.mViewRootImpl.invalidate();
    }
}
复制代码

上面的 draw 只是更新 DIsplayList ,更新结束后,调用 syncAndDrawFrame ,通知渲染线程开始工做,主线程释放。渲染线程的核心实如今 libhwui 库里面,其代码位于 frameworks/base/libs/hwui

frameworks/base/libs/hwui/renderthread/RenderProxy.cpp

int RenderProxy::syncAndDrawFrame() {
    return mDrawFrameTask.drawFrame();
}
复制代码

关于 RenderThread 的工做流程这里就不细说了,后续会有专门的篇幅来说解这个,目前 hwui 这一块的流程也有不少优秀的文章,你们能够对照文章和源码来看,其核心流程在 Systrace 上的表现以下:

图片

主线程和渲染线程的分工

主线程负责处理进程 Message、处理 Input 事件、处理 Animation 逻辑、处理 Measure、Layout、Draw ,更新 DIsplayList ,可是不涉及 SurfaceFlinger 打交道;渲染线程负责渲染渲染相关的工做,一部分工做也是 CPU 来完成的,一部分操做是调用 OpenGL 函数来完成的

当启动硬件加速后,在 Measure、Layout、Draw 的 Draw 这个环节,Android 使用 DisplayList 进行绘制而非直接使用 CPU 绘制每一帧。DisplayList 是一系列绘制操做的记录,抽象为 RenderNode 类,这样间接的进行绘制操做的优势以下

  1. DisplayList 能够按需屡次绘制而无须同业务逻辑交互
  2. 特定的绘制操做(如 translation, scale 等)能够做用于整个 DisplayList 而无须从新分发绘制操做
  3. 当知晓了全部绘制操做后,能够针对其进行优化:例如,全部的文本能够一块儿进行绘制一次
  4. 能够将对 DisplayList 的处理转移至另外一个线程(也就是 RenderThread)
  5. 主线程在 sync 结束后能够处理其余的 Message,而不用等待 RenderThread 结束

RenderThread 的具体流程你们能够看这篇文章 : www.cocoachina.com/articles/35…

游戏的主线程与渲染线程

游戏大多使用单独的渲染线程,有单独的 Surface ,直接跟 SurfaceFlinger 进行交互,其主线程的存在感比较低,绝大部分的逻辑都是本身在本身的渲染线程里面实现的。

你们能够看一下王者荣耀对应的 Systrace ,重点看应用进程和 SurfaceFlinger 进程(30fps)

图片

能够看到王者荣耀主线程的主要工做,就是把 Input 事件传给 Unity 的渲染线程,渲染线程收到 Input 事件以后,进行逻辑处理,画面更新等。

图片

Flutter 的主线程和渲染线程

这里提一下 Flutter App 在 Systrace 上的表现,因为 Flutter 的渲染是基于 libSkia 的,因此它也没有 RenderThread ,而是他自建的 RenderEngine , Flutter 比较重要的两个线程是 ui 线程和 gpu 线程,对应到下面提到的  Framework 和 Engine 两层

图片

Flutter 中也会监听 Vsync 信号 ,其 VsyncView 中会以 postFrameCallback 的形式,监听 doFrame 回调,而后调用 nativeOnVsync ,将 Vsync 到来的信息传给 Flutter UI 线程,开始一帧的绘制。

图片

能够看到 Flutter 的思路跟游戏开发的思路差很少,不依赖具体的平台,自建渲染管道,更新快,跨平台优点明显。

Flutter SDK 自带 Skia 库,不用等系统升级就能够用到最新的 Skia 库,并且 Google 团队在 Skia 上作了不少优化,因此官方号称性能能够媲美原生应用

图片

Flutter 的框架分为 Framework 和 Engine 两层,应用是基于 Framework 层开发的,Framework 负责渲染中的 Build,Layout,Paint,生成 Layer 等环节。Engine 层是 C++实现的渲染引擎,负责把 Framework 生成的 Layer 组合,生成纹理,而后经过 Open GL 接口向 GPU 提交渲染数据。

图片

当须要更新 UI 的时候,Framework 通知 Engine,Engine 会等到下个 Vsync 信号到达的时候,会通知 Framework,而后 Framework 会进行 animations, build,layout,compositing,paint,最后生成 layer 提交给 Engine。Engine 会把 layer 进行组合,生成纹理,最后经过 Open Gl 接口提交数据给 GPU,GPU 通过处理后在显示器上面显示。整个流程以下图:

图片

性能

若是主线程须要处理全部任务,则执行耗时较长的操做(例如,网络访问或数据库查询)将会阻塞整个界面线程。一旦被阻塞,线程将没法分派任何事件,包括绘图事件。主线程执行超时一般会带来两个问题

  1. 卡顿:若是主线程+渲染线程每一帧的执行都超过 16.6ms,那么就会出现掉帧。
  2. 卡死:若是界面线程被阻塞超过几秒钟时间(目前大约是 5 秒钟),用户会看到 “应用无响应” (ANR) 对话框

对于用户来讲,这两个状况都是用户不肯意看到的,因此对于 App 开发者来讲,两个问题是发版本以前必需要解决的,ANR 这个因为有详细的调用栈,因此比较好定位;可是间歇性卡顿这个,可能就须要使用工具来进行分析了:Systrace + TraceView,因此理解主线程和渲染线程的关系和他们的工做原理是很是重要的,这也是本系列的一个初衷

另外关于卡顿,能够参考下面三篇文章,你的 App 卡顿不必定是你 App 的问题,也有多是系统的问题,不过无论怎么说,首先要会分析卡顿问题。

0. Android 中的卡顿丢帧缘由概述 - 方法论

1. Android 中的卡顿丢帧缘由概述 - 系统篇

2. Android 中的卡顿丢帧缘由概述 - 应用篇

参考

  1. juejin.im/post/5a9e01…
  2. www.cocoachina.com/articles/35…
  3. juejin.im/post/5b7767…
  4. gityuan.com/2019/06/15/…
  5. developer.android.google.cn/guide/compo…

附件

本文涉及到的附件也上传了,各位下载后解压,使用 Chrome 浏览器打开便可

点此连接下载文章所涉及到的 Systrace 附件

关于我

小厂系统研发工程师 , 更多信息能够点击 关于我 , 很是但愿和你们一块儿交流 , 共同进步 .

一我的能够走的更快 , 一群人能够走的更远

相关文章
相关标签/搜索