Android Systrace基础知识

Systrace概述

概述

Systrace 是 Android4.1 中新增的性能数据采样和分析工具。可以用来收集Android关键子系统的信息,用来分析改进性能。

Systrace 的功能包括跟踪系统的 I/O 操作、内核工作队列、CPU 负载以及 Android 各个子系统的运行状况等。在 Android 平台中,它主要由3部分组成:

  • 内核:利用了Linux Kernel 中的ftrace功能,所以在使用的时候需要开启Kernel中的ftrace相关模块。

  • 数据采集:Systrace中定义了一个ftrace类,用来采集数据,可以从中读取并分析数据。

  • 数据分析工具:Android其中提供了systrace.py用来配置数据的采集方式,可以把默认的 Trace 打开工具切换成 Perfetto UI。比如你想要计算 SurfaceFlinger 线程消耗 CPU 的总量,或者运行在大核中的线程都有哪一些等等,可以与领域专家合作,把他们的经验转成 SQL 指令。如果这个还不满足你的需求, Perfetto 也提供了 Python API,将数据导出成 DataFrame 格式近乎可以实现任意你想要的数据分析效果。

    可以在 Android 性能优化的术、道、器 这篇文章中查看各个工具的介绍和使用,以及他们的优劣比。

Systrace设计思路

系统的一些关键操作(比如 Touch 操作、Power 按钮、滑动操作等)、系统机制(input 分发、View 绘制、进程间通信、进程管理机制等)、软硬件信息(CPU 频率信息、CPU 调度信息、磁盘信息、内存信息等)的关键流程上,插入类似 Log 的信息,称之为 TracePoint(本质是 Ftrace 信息),通过这些 TracePoint 来展示一个核心操作过程的执行时间、某些变量的值等信息。然后 Android 系统把这些散布在各个进程中的 TracePoint 收集起来,写入到一个文件中。导出这个文件后,Systrace 通过解析这些 TracePoint 的信息,得到一段时间内整个系统的运行信息。

实践中的应用情况

Systrace中含有大量的系统信息,适合用来分析Android App和Android系统的性能问题。

  1. 从技术角度来说,Systrace 可覆盖性能涉及到的 响应速度卡顿丢帧ANR 这几个大类。
  2. 从用户角度来说,Systrace 可以分析用户遇到的性能问题,包括但不限于:
    1. 应用启动速度问题,包括冷启动、热启动、温启动
    2. 界面跳转速度慢、跳转动画卡顿
    3. 其他非跳转的点击操作慢(开关、弹窗、长按、选择等)
    4. 亮灭屏速度慢、开关机慢、解锁慢、人脸识别慢等
    5. 列表滑动卡顿
    6. 窗口动画卡顿
    7. 界面加载卡顿
    8. 整机卡顿
    9. App 点击无响应、卡死闪退

Systrace简单使用

  • 手机准备好你要进行抓取的界面
  • 点击开始抓取(命令行的话就是开始执行命令)
  • 手机上开始操作(不要太长时间)
  • 设定好的时间到了之后,会将生成 Trace.html 文件,使用 Chrome 将这个文件打开进行分析

Systrace 工具在 Android-SDK 目录下的 platform-tools 里面(最新版本的 platform-tools 里面已经移除了 systrace 工具,需要下载老版本的 platform-tools ,33 之前的版本

常见的命令

-o : 指示输出文件的路径和名字
-t : 抓取时间(最新版本可以不用指定, 按 Enter 即可结束)
-b : 指定 buffer 大小 (一般情况下,默认的 Buffer 是够用的,如果你要抓很长的 Trae , 那么建议调大 Buffer )
-a : 指定 app 包名 (如果要 Debug 自定义的 Trace 点, 记得要加这个)

但是一般使用工具的时候不会考虑那么多,直接在对应的项目之前打勾,只有使用命令行的时候才会去输入。一般会将需要操作的命令配置成Alias,比如:

alias st-start='python /path-to-sdk/platform-tools/systrace/systrace.py'  
    
alias st-start-gfx-trace = ‘st-start -t 8                               
 am,binder_driver,camera,dalvik,freq,gfx,hal,idle,input,memory,memreclaim,res,sched,sync,view,webview,wm,workq,binder’

这样在使用的时候,可以直接敲 st-start 即可。

使用 adb shell atrace –list_categories 来查看你的手机支持的 tag

分析 Systrace

线程状态查看

绿色:运行中(Running)

​ 只有Running状态的线程才能在CPU中运行。同一时刻可以有多个线程处于Runnable状态,这些Runnable状态的线程的task_struct结构放入对应的CPU可执行队列中(同一时刻一个线程最多出现在一个CPU可执行队列中)。调度器 的任务就是从各个CPU的可执行队列中分别选择一个线程在该CPU上运行。

通过查看Running状态的线程,根据其运行的时间和竞品的运行时间比较,分析快或者慢的原因。 频率不够、跑在了小核上、在Running和Runnable状态间频繁切换、在Running和Sleep状态之间频繁切换、是否跑在了不该跑的核上等。

蓝色:可运行(Runnable)

​ 线程可以运行,等待CPU调度。通过查看Runnable状态的持续时间可以判断CPU调度的速率。分析其调度快慢的原因,后台有太多任务、频率太低、被限制等。

白色:休眠中(Sleep)

​ 处于Sleep状态的线程是没有工作,可能是线程被阻塞。一般是在等事件驱动。

橘色 : 不可中断的睡眠态 (Uninterruptible Sleep - IO Block)

​ 线程在I / O上被阻塞或等待磁盘操作完成,一般底线都会标识出此时的 callsite :wait_on_page_locked_killable。这个一般是标示 io 操作慢,如果有大量的橘色不可中断的睡眠态出现,那么一般是由于进入了低内存状态。

紫色 : 不可中断的睡眠态(Uninterruptible Sleep)

​ 线程在另一个内核操作(通常是内存管理)上被阻塞。一般是陷入了内核态,有些情况下是正常的,有些情况下是不正常的,需要按照具体的情况去分析。

线程唤醒信息分析

Systrace 会标识出一个非常有用的信息,可以帮助我们进行线程调用等待相关的分析。一个线程的唤醒信息是非常好重要的,如果一个线程出现一个比较长的Sleep的状态,然后被唤醒,然后我们就可以查看到唤醒者的信息。

比如下面这个场景,这一帧 doFrame 执行了 152ms,有明显的异常,但是大部分时间是在 sleep

image-20211210185851589

这时候放大来看,可以看到是一段一段被唤醒的,这时候点击图中的 runnable ,下面的信息区就会出现唤醒信息,可以顺着看这个线程到底在做什么

image-20211213145728467

20424 线程是 RenderHeartbeat,这就牵扯到了 App 自身的代码逻辑,需要 App 自己去分析 RenderHeartbeat 到底做了什么事情

image-20211210190921614

Systrace 可以标示出这个的一个原因是,一个任务在进入 Running 状态之前,会先进入 Runnable 状态进行等待,而 Systrace 会把这个状态也标示在 Systrace 上(非常短,需要放大进行看)

img

拉到最上面查看对应的 cpu 上的 taks 信息,会标识这个 task 在被唤醒之前的状态:
img

顺便贴一下 Linux 常见的进程状态

  1. D 无法中断的休眠状态(通常 IO 的进程);
  2. R 正在可运行队列中等待被调度的;
  3. S 处于休眠状态;
  4. T 停止或被追踪;
  5. W 进入内存交换 (从内核2.6开始无效);
  6. X 死掉的进程 (基本很少見);
  7. Z 僵尸进程;
  8. < 优先级高的进程
  9. N 优先级较低的进程
  10. L 有些页被锁进内存
  11. s 进程的领导者(在它之下有子进程)
  12. l 多进程的(使用 CLONE_THREAD, 类似 NPTL pthreads)
  13. + 位于后台的进程组

信息区数据解析

线程状态信息解析

img

函数 Slice 信息解析

img

Counter Sample 信息解析

img

Async Slice 信息解析

img

CPU Slice 信息解析

img

User Expectation 信息解析

位于整个 Systrace 最上面的部分,标识了 Rendering Response 和 Input Response
img

快捷键

W : 放大 Systrace , 放大可以更好地看清局部细节
S : 缩小 Systrace, 缩小以查看整体
A : 左移
D : 右移
M : 高亮选中当前鼠标点击的段(这个比较常用,可以快速标识出这个方法的左右边界和执行时间,方便上下查看)

数字键1 : 切换到 Selection 模式 , 这个模式下鼠标可以点击某一个段查看其详细信息, 一般打开 Systrace 默认就是这个模式 , 也是最常用的一个模式 , 配合 M 和 ASDW 可以做基本的操作
数字键2 : 切换到 Pan 模式 , 这个模式下长按鼠标可以左右拖动, 有时候会用到
数字键3 : 切换到 Zoom 模式 , 这个模式下长按鼠标可以放大和缩小, 有时候会用到
数字键4 : 切换到 Timing 模式 , 这个模式下主要是用来衡量时间的,比如选择一个起点, 选择一个终点, 查看起点和终点这中间的操作所花费的时间.

SystemServer

窗口动画

​ 由于窗口是由SystemServer来管,窗口动画也是由SystemServer统一处理,其中涉及到两个比较重要的线程是Android.Anim 和 Android.Anim.if 这两个线程。

​ 当我们点击一个应用图标的时候,由于App还在启动,Launcher(安卓系统桌面启动器)首先启动一个StartingWindow,等App的第一帧绘制好了之后,再切换到App的窗口动画。

-w1236

ActivityManagerService

​ AMS 和 WMS 算是 SystemServer 中最繁忙的两个 Service 了,与 AMS 相关的 Trace( Android4.1 中新增的性能数据采样和分析工具) 一般会用 TRACE_TAG_ACTIVITY_MANAGER 这个 TAG,在 Systrace 中的名字是 ActivityManager。

​ 在进程和四大组件的各种场景一般都会有对应的 Trace 点来记录,比如大家熟悉的 ActivityStart、ActivityResume、activityStop 等,这些 Trace 点有一些在应用进程,有一些在 SystemServer 进程。

WindowManagerService

​ 与 WMS 相关的 Trace 一般会用 TRACE_TAG_WINDOW_MANAGER 这个 TAG,在 Systrace 中 WindowManagerService 在 SystemServer 中多在对应的 Binder 中出现,比如下面应用启动的时候,relayoutWindow 的 Trace 输出。

​ 在 Window 的各种场景一般都会有对应的 Trace 点来记录,比如大家熟悉的 relayoutWIndow、performLayout、prepareToDisplay 等

Input(Systrace角度)

Input主要是Android系统中的输入事件跟踪,像一些屏幕点击事件、键盘输入、用户输入等。

Input 是 SystemServer 线程里面非常重要的一部分,主要是由 InputReader 和 InputDispatcher 这两个 Native 线程组成。

  • InputReader 负责从EventHub中读取输入事件,‌然后将这些事件传递给InputDispatcher进行分发。‌
  • InputDispatcher 在获取到InputReader提供的事件后,‌对这些事件进行包装并分发给对应的应用程序连接。‌

简单理解就是屏幕会每隔几毫秒扫描一次,如果扫描到有点击事件,InputReader会读取这个点击事件,然后将读取到的信息传递给InputDispatcher进行事件派发,InputDispatcher就会将这个触摸事件发送给对应的注册了Input事件的App,App拿到事件之后会对画面进行渲染。

滑动桌面

滑动桌面包括 Input_Down + 若干个 Input_Move + Input_Up 。

下面是在这个过程中会涉及到的事件:

  1. InputReader 负责从 EventHub 里面把 Input 事件读取出来,然后交给 InputDispatcher 进行事件分发。
  2. InputDispatcher 在拿到 InputReader 获取的事件之后,对事件进行包装和分发 (也就是发给对应的App)。
  3. OutboundQueue 里面放的是即将要被派发给对应 AppConnection 的事件。
  4. WaitQueue 里面记录的是已经派发给 AppConnection 但是 App 还在处理没有返回处理成功的事件。
  5. PendingInputEventQueue 里面记录的是 App 需要处理的 Input 事件,这里可以看到已经到了应用进程。
  6. deliverInputEvent 标识 App UI Thread 被 Input 事件唤醒。
  7. InputResponse 标识 Input 事件区域,这里可以看到一个 Input_Down 事件 + 若干个 Input_Move 事件 + 一个 Input_Up 事件的处理阶段都被算到了这里。
  8. App 响应 Input 事件 : 这里是滑动然后松手,也就是我们熟悉的桌面滑动的操作,桌面随着手指的滑动更新画面,松手后触发 Fling 继续滑动,从 Systrace 就可以看到整个事件的流程。

Input基本流程

  1. InputReader 读取 Input 事件
  2. InputReader 将读取的 Input 事件放到 InboundQueue 中
  3. InputDispatcher 从 InboundQueue 中取出 Input 事件派发到各个 App(连接) 的 OutBoundQueue
  4. 同时将事件记录到各个 App(连接) 的 WaitQueue
  5. App 接收到 Input 事件,同时记录到 PendingInputEventQueue ,然后对事件进行分发处理
  6. App 处理完成后,回调 InputManagerService 将负责监听的 WaitQueue 中对应的 Input 移除

InputReader

InputReader是一个Native线程,实在SystemSever进程里面的,其功能就是从EventHub读取事件,然后进行加工,将加工好的事件发送到InputDispatcher。

InputReader Loop流程如下:

  1. getEvents:通过EventHub监听目录,将读取的事件放入mEventBuffer(数组),再将事件input_event转换为RawEvent。
  2. processEventsLocked: 对事件进行加工, 转换 RawEvent -> NotifyKeyArgs(NotifyArgs)
  3. QueuedListener->flush:将事件发送到 InputDispatcher 线程, 转换 NotifyKeyArgs -> KeyEntry(EventEntry)

代码逻辑:

void InputReader::loopOnce() {
    int32_t oldGeneration;
    int32_t timeoutMillis;
    bool inputDevicesChanged = false;
    std::vector<InputDeviceInfo> inputDevices;
    { // acquire lock
    ......
    //获取输入事件、设备增删事件,count 为事件数量
    size_t count = mEventHub ->getEvents(timeoutMillis, mEventBuffer, EVENT_BUFFER_SIZE);
    {
    ......
        if (count) {//处理事件
            processEventsLocked(mEventBuffer, count);
        }

    }
    ......
    mQueuedListener->flush();//将事件传到 InputDispatcher,这里getListener 得到的就是 InputDispatcher
}

InputDispatcher

当InputReader最后调用mQueuedListener->flush()之后,将Input事件加入到了InputDispatcher中的mInboundQueue,然后唤醒了InputDispatcher。

InputDispatcher的核心逻辑主要如下:

InputDispatcher 的核心逻辑如下:

  1. dispatchOnceInnerLocked(): 从 InputDispatcher 的 mInboundQueue 队列,取出事件 EventEntry。另外该方法开始执行的时间点 (currentTime) 便是后续事件 dispatchEntry 的分发时间 (deliveryTime)
  2. dispatchKeyLocked():满足一定条件时会添加命令 doInterceptKeyBeforeDispatchingLockedInterruptible;
  3. enqueueDispatchEntryLocked():生成事件 DispatchEntry 并加入 connection 的 outbound 队列
  4. startDispatchCycleLocked():从 outboundQueue 中取出事件 DispatchEntry, 重新放入 connection 的 waitQueue 队列;
  5. InputChannel.sendMessage 通过 socket 方式将消息发送给远程进程;
  6. runCommandsLockedInterruptible():通过循环遍历的方式,依次处理 mCommandQueue 队列中的所有命令。而 mCommandQueue 队列中的命令是通过 postCommandLocked() 方式向该队列添加的。

代码逻辑:

void InputDispatcher::dispatchOnceInnerLocked(nsecs_t* nextWakeupTime) {
    // Ready to start a new event.
    // If we don't already have a pending event, go grab one.
    if (! mPendingEvent) {
        if (mInboundQueue.isEmpty()) {
        } else {
            // Inbound queue has at least one entry.
            mPendingEvent = mInboundQueue.dequeueAtHead();
            traceInboundQueueLengthLocked();
        }

        // Poke user activity for this event.
        if (mPendingEvent->policyFlags & POLICY_FLAG_PASS_TO_USER) {
            pokeUserActivityLocked(mPendingEvent);
        }

        // Get ready to dispatch the event.
        resetANRTimeoutsLocked();
    }
    case EventEntry::TYPE_MOTION: {
        done = dispatchMotionLocked(currentTime, typedEntry,
                &dropReason, nextWakeupTime);
        break;
    }

    if (done) {
        if (dropReason != DROP_REASON_NOT_DROPPED) {
            dropInboundEventLocked(mPendingEvent, dropReason);
        }
        mLastDropReason = dropReason;
        releasePendingEventLocked();
        *nextWakeupTime = LONG_LONG_MIN;  // force next poll to wake up immediately
    }
}

InboundQueue

InputDispatcher 执行 notifyKey 的时候,会将 Input 事件封装后放到 InboundQueue 中,后续 InputDispatcher 循环处理 Input 事件的时候,就是从 InboundQueue 取出事件然后做处理

OutboundQueue

Outbound 意思是出站,这里的 OutboundQueue 指的是要被 App 拿去处理的事件队列,每一个 App(Connection) 都对应有一个 OutboundQueue ,从 InboundQueue 那一节的图来看,事件会先进入 InboundQueue ,然后被 InputDIspatcher 派发到各个 App 的 OutboundQueue。

WaitQueue

当 InputDispatcher 将 Input 事件分发出去之后,将 DispatchEntry 从 outboundQueue 中取出来放到 WaitQueue 中,当 publish 出去的事件被处理完成(finished),InputManagerService 就会从应用中得到一个回复,此时就会取出 WaitQueue 中的事件,从 Systrace 中看就是对应 App 的 WaitQueue 减少。当主线程卡顿,Input事件没有及时被消耗,WaitQueue里面等待结束标志的Input事件就会变长。

Input 刷新与 Vsync

Input的刷新率取决于触摸屏的采样率,目前比较常见的是120Hz和180Hz对应就是 8ms 采样一次或者 6.25ms 采样一次,但是如果是60Hz的屏幕,两个 Vsync 的间隔在 16.6ms,如果采样率是6.25ms这期间如果有两个或三个Input事件,那么系统会抛弃一俩个只会拿最新的那个Input。

所以在平时设置采样率的时候:

  1. 在屏幕刷新率和系统 FPS 都是 60 的时候,盲目提高触摸屏的采样率,是没有太大的效果的,反而有可能出现上面图中那样,有的 Vsync 周期中有两个 Input 事件,而有的 Vsync 周期中有三个 Input 事件,这样造成事件不均匀,可能会使 UI 产生抖动
  2. 在屏幕刷新率和系统 FPS 都是 60 的时候,使用 120Hz 采样率的触摸屏就可以了
  3. 如果在屏幕刷新率和系统 FPS 都是 90 的时候 ,那么 120Hz 采样率的触摸屏显然不够用了,这时候应该采用 180Hz 采样率的屏幕

Input 调试信息

Dumpsys Input 主要是 Debug 用,我们也可以来看一下其中的一些关键信息,到时候遇到了问题也可以从这里面找 , 其命令如下:

adb shell dumpsys input

其中的输出比较多,我们终点截取 Device 信息、InputReader、InputDispatcher 三段来看就可以了

Device信息: 主要是目前连接上的 Device 信息。

Input Reader 状态: InputReader 这里就是当前 Input 事件的一些展示。

InputDispatcher 状态: InputDispatch 这里的重要信息主要包括

  1. FocusedApplication :当前获取焦点的应用
  2. FocusedWindow : 当前获取焦点的窗口
  3. TouchStatesByDisplay
  4. Windows :所有的 Window
  5. MonitoringChannels :Window 对应的 Channel
  6. Connections :所有的连接
  7. AppSwitch: not pending
  8. Configuration

Binder

Binder是Android中实现进程间通信(‌IPC)‌的一种机制。‌它是基于进程间通信(IPC)机制中的共享内存和消息传递机制实现的

​ SystemServer 由于提供大量的基础服务,所以进程间的通信非常繁忙,且大部分通信都是通过 Binder ,所以 Binder 在 SystemServer 中的作用非常关键,很多时候当后台有大量的 App 存在的时候,SystemServer 就会由于 Binder 通信和锁竞争,导致系统或者 App 卡顿。

HandlerThread

BackgroundThread

com/android/internal/os/BackgroundThread.java

private BackgroundThread() {    super("android.bg", android.os.Process.THREAD_PRIORITY_BACKGROUND);}

Systrace 中的 BackgroundThread
-w1082

BackgroundThread 在系统中使用比较多,许多对性能没有要求的任务,一般都会放到 BackgroundThread 中去执行。

ServiceThread

ServiceThread 继承自 HandlerThread。下面介绍的几个工作线程都是继承自 ServiceThread ,分别实现不同的功能,根据线程功能不同,其线程优先级也不同:UIThread、IoThread、DisplayThread、AnimationThread、FgThread、SurfaceAnimationThread。

每个 Thread 都有自己的 Looper 、Thread 和 MessageQueue,互相不会影响。Android 系统根据功能,会使用不同的 Thread 来完成。

​ SurfaceAnimationThread 的名字叫 android.anim.lf , 与 android.anim 有区别,这个 Thread 主要是执行窗口动画,用于分担 android.anim 线程的一部分动画工作,减少由于锁导致的窗口动画卡顿问题。

SurfaceFlinger

SurfaceFlinger负责在界面上合成各种应用程序和系统界面的图像,主要任务是将多个应用程序的图形输出合并到一起。

​ SurfaceFlinger 最主要的功能:SurfaceFlinger 接受来自多个来源的数据缓冲区,对它们进行合成,然后发送到显示设备。

​ 大多数应用在屏幕上一次显示三个层:屏幕顶部的状态栏、底部或侧面的导航栏以及应用界面。状态栏和导航栏由系统进程渲染,而应用层由应用渲染,两者之间不进行协调。

​ 设备显示的时候是按照一定速率刷新的,一般会将刷新的数据提交到缓冲区,可以安全更新内容时,系统便会接受来自显示设备的VSYNC信号。SurfaceFlinger不会在每次提交缓冲区时执行操作,而是接收到VSYNC信号后,SurfaceFlinger 会遍历它的层列表,以寻找新的缓冲区。如果找到新的缓冲区,它会获取该缓冲区;否则,它会继续使用以前获取的缓冲区。SurfaceFlinger 必须始终显示内容,因此它会保留一个缓冲区。如果在某个层上没有提交缓冲区,则该层会被忽略。

img

App部分

App 部分主要负责生产 SurfaceFlinger 合成所需要的 Surface。

img

App 与 SurfaceFlinger 的交互主要集中在三点

  1. Vsync 信号的接收和处理

    Vsync-App 信号到达,就是指的是 SurfaceFlinger 的 Vsync-App 信号。应用收到这个信号后,开始一帧的渲染准备

  2. RenderThread 的 dequeueBuffer

    就是从队列中拿出一个 Buffer,这个队列就是 SurfaceFlinger 中的 BufferQueue。

  3. RenderThread 的 queueBuffer

    App处理(写入具体的drawcall)完Buffer之后将Buffer放回BufferQueue,

BufferQueue

每个有显示界面的进程对应一个 BufferQueue,使用方创建并拥有 BufferQueue 数据结构,并且可存在于与其生产方不同的进程中。

img

  1. dequeue(生产者发起) : 当生产者需要缓冲区时,它会通过调用 dequeueBuffer() 从 BufferQueue 请求一个可用的缓冲区,并指定缓冲区的宽度、高度、像素格式和使用标记。
  2. queue(生产者发起):生产者填充缓冲区并通过调用 queueBuffer() 将缓冲区返回到队列。
  3. acquire(消费者发起) :消费者通过 acquireBuffer() 获取该缓冲区并使用该缓冲区的内容
  4. release(消费者发起) :当消费者操作完成后,它会通过调用 releaseBuffer() 将该缓冲区返回到队列

SurfaceFlinger

SurfaceFlinger 的主要工作就是合成:

当 VSYNC 信号到达时,SurfaceFlinger 会遍历它的层列表,以寻找新的缓冲区。如果找到新的缓冲区,它会获取该缓冲区;否则,它会继续使用以前获取的缓冲区。SurfaceFlinger 必须始终显示内容,因此它会保留一个缓冲区。如果在某个层上没有提交缓冲区,则该层会被忽略。SurfaceFlinger 在收集可见层的所有缓冲区之后,便会询问 Hardware Composer 应如何进行合成。

HWComposer

HWComposer(硬件合成器)是一个重要的组件,它是与SurfaceFlinger协作,用来优化和加速屏幕内容的显示过程。HWComposer主要的作用是利用硬件加速来处理屏幕渲染,减轻CPU的负担,并提高能效和渲染性能。

Vsync信号

Vsync 信号可以由硬件产生,也可以用软件模拟,不过现在基本上都是硬件产生,硬件负责产生Vsync的是HWC,HWC可以将生成的Vsync事件通过回调将事件发送到SurfaceFlinge,DispSync 将 Vsync 生成由 Choreographer 和 SurfaceFlinger 使用的 VSYNC_APP 和 VSYNC_SF 信号。

​ 当Vsync信号来的时候,系统通过对Vsync信号周期的调整,来控制每一帧绘制操作的时机,系统配合屏幕刷新率,Vsync信号唤醒Choreographer来做App的绘制操作。

​ 渲染层(App)与Vsync打交道的是Choreographer,而合成层与Vsync打交道的是SurfaceFlinger,SurfaceFlinger会在Vsync到来的时候,将所有已经准备好的Surface进行合成。

Android图形数据流向

从App绘制到屏幕显示,主要有这几个阶段:

  1. App在收到Vsync-App的时候,在主线程执行measure、layout、draw(构建 DisplayList , 里面包含 OpenGL 渲染需要的命令及数据) 。这里对应的 Systrace 中的主线程 doFrame 操作。
  2. CPU将数据上传给GPU。这里对应的Systrace中的渲染线程的 flush drawing commands 操作。
  3. 通知GPU进行渲染,真机一般不会阻塞GPU渲染结束,CPU通知其结束后就返回继续执行其他任务,使用 Fence 机制辅助 GPU CPU 进行同步操作
  4. swapBuffers,并通知SurfaceFlinger图层合成。这里对应的Systrace 中的渲染线程的 eglSwapBuffersWithDamageKHR 操作。
  5. SurfaceFlinger开始合成图层,SurfaceFlinger 在合成的时候,会将一些合成工作委托给 Hardware Composer,从而降低来自 OpenGL 和 GPU 的负载,只有 Hardware Composer 无法处理的图层,或者指定用 OpenGL 处理的图层,其他的 图层偶会使用 Hardware Composer 进行合成。
  6. 最终合成好的数据放到屏幕对应的 Frame Buffer 中,固定刷新的时候就可以看到了。

Systrace中的图像数据流

  1. 第一个 Vsync 信号到来, SurfaceFlinger 和 App 同时收到 Vsync 信号
  2. SurfaceFlinger 收到 Vsync-sf 信号,开始进行 App 上一帧的 Buffer 的合成
  3. App 收到 Vsycn-app 信号,开始进行这一帧的 Buffer 的渲染(对应上面的第一、二、三、四阶段)
  4. 第二个 Vsync 信号到来 ,SurfaceFlinger 和 App 同时收到 Vsync 信号,SurfaceFlinger 获取 App 在第二步里面渲染的 Buffer,开始合成(对应上面的第五阶段),App 收到 Vsycn-app 信号,开始新一帧的 Buffer 的渲染(对应上面的第一、二、三、四阶段)

Vsync Offset

Vsync Offset指的是在Vsync_App到Vsync_sf之间有一个Offset值,这个Offset厂商可以配置,如果Offset不为0,就意味着App和SurfaceFlinger主进程不是同时收到Vsync信号,中间间隔Offset。 目前大部分厂商还是没有配置Offset,所以App和SurfaceFlinger是同时收到Vsync信号的。可以通过 Dumpsys SurfaceFlinger 来查看对应的值。

Offset为0: 当Offset为0时,App和SurfaceFlinger时同时接收到Vsync信号的,当同时接收到信号之后,App开始进行第N帧渲染,渲染结束后会将Buffer Swap到BufferQueue中。要等到下一个Vsync信号到来的时候,这一帧才会被SrufaceFlinger拿去合成,这个时间大概是16.6ms(一个周期)。

Offset不为0: 因为同时接收Vsync信号的话,SrufaceFlinger要等第二轮才会合成上一轮App渲染的帧,这时候就引入了Offset机制,这种情况下,App先接收到Vsync信号,进行一帧的渲染操作,过了Offset时间之后,SurfaceFlinger才收到Vsync信号开始合成。这时候如果App的Buffer已经完成了,SurfaceFlinger会直接合成这一帧,用户也会早一点看到。

Offset优缺点: Offset 的一个比较难以确定的点就在于 Offset 的时间该如何设置,如果设置时间过短,可能App收到Vsync信号后还没有渲染完成,SurfaceFlinger就收到Vsync信号就开始合成,但此时App的BufferQueue中没有数据,SurfaceFlinger就得等下一次Vsync信号之后再去合成,此时等待时间是,Vsync周期+Offset,而不是我们期望的样子。如果Offset设置时间过长,也就不起作用了。

HW_Vsync

  1. Choreographer 初始化
  2. 初始化 FrameHandler ,绑定 Looper
  3. 初始化 FrameDisplayEventReceiver ,与 SurfaceFlinger 建立通信用于接收和请求 Vsync
  4. 初始化 CallBackQueues
  5. SurfaceFlinger 的 appEventThread 唤醒发送 Vsync ,Choreographer 回调 FrameDisplayEventReceiver.onVsync , 进入 Choreographer 的主处理函数 doFrame
  6. Choreographer.doFrame 计算掉帧逻辑
  7. Choreographer.doFrame 处理 Choreographer 的第一个 callback : input
  8. Choreographer.doFrame 处理 Choreographer 的第二个 callback : animation
  9. Choreographer.doFrame 处理 Choreographer 的第三个 callback : insets animation
  10. Choreographer.doFrame 处理 Choreographer 的第四个 callback : traversal
  11. traversal-draw 中 UIThread 与 RenderThread 同步数据
  12. Choreographer.doFrame 处理 Choreographer 的第五个 callback : commit ?
  13. RenderThread 处理绘制命令,将处理好的绘制命令发给 GPU 处理
  14. 调用 swapBuffer 提交给 SurfaceFlinger 进行合成(此时 Buffer 并没有真正完成,需要等 CPU 完成后 SurfaceFlinger 才能真正使用,新版本的 Systrace 中有 gpu 的 fence 来标识这个时间)

第一步初始化完成后,后续就会在步骤 2-9 之间循环

对上述步骤进行部分重点介绍:

FrameDisplayEventReceiver类: Vsync的注册、申请、接收都是通过这个类,这个类继承DisplayEventReceiver ,主要有三个方法:onVsync-Vsync信号回调,run-执行doFrame,scheduleVsync-请求Vsync信号。

简单来说,FrameDisplayEventReceiver 的初始化过程中,通过 BitTube(本质是一个 socket pair),来传递和请求 Vsync 事件,当 SurfaceFlinger 收到 Vsync 事件之后,通过 appEventThread 将这个事件通过 BitTube 传给 DisplayEventDispatcher ,DisplayEventDispatcher 通过 BitTube 的接收端监听到 Vsync 事件之后,回调 Choreographer.FrameDisplayEventReceiver.onVsync ,触发开始一帧的绘制。

MainThread和RenderThread

主线程的创建

AndroidApp上的进程是基于Linux的,所以在创建进程的时候是调用了fork函数,Fork出来的进程,我们暂且称为主进程,由于这个主进程没有与Android进行链接,无法处理Android App 中的Message,由于Android App是基于消息机制,所有这个主进程需要与Android的Message进行绑定才能处理其中的各种Message。此时就引入了ActivityThread,ActivityThread是负责链接Fork进程和Android的Message,这三者配合组成了Android App的主进程。所以ActivityThread并不是一个Thread,他是初始化了Message机制所需要的MessageQueue、Looper、Handler,Handler负责处理大部分的Message消息,所以我们习惯上觉得ActivityThread是主线程,实际上他只是主线程的一个逻辑处理单元。

渲染线程的创建和发展

渲染线程也就是RenderThread,最初的Android版本里面是没有渲染线程的,渲染工作都是在主线程里面完成的,使用的也都是CPU,RenderThread实在Android Lollipop中新加入的组件,负责承担之前主线程的一部分渲染工作,减轻主线程的负担。

软件绘制和硬件加速绘制

​ 硬件加速一般就是GPU加速,目前在Android中是默认开启的,默认都会有主线程和渲染线程,如果我们在App的标签里面加一个android:hardwareAccelerated="false",就可以关闭硬件加速,系统就不会初始化RenderThread,直接CPU调用libSkia来进行渲染。只不过执行的时间变长了,更容易出现卡顿。

​ 在硬件加速中,主线程的draw函数并没有真正执行drawCall,而是把要draw的内容记录到DisplayList里面,同步到RenderThread中,同步完成之后,主线程就可以被释放做其他事情,RenderThread进行之后的渲染工作。

渲染线程初始化

渲染线程初始化在需要draw内容的时候,一般会启动一个Activity,在第一个draw执行的时候,会检测渲染线程是否初始化,如果没有则去初始化,后续直接调用draw,这只是更新DisplayList,更新结束后会调用syncAndDrawFrame ,通知渲染线程开始工作,释放主线程。渲染线程的核心实现在 libhwui 库里面。

主线程和渲染线程的分工

​ 主线程负责处理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 结束

游戏的主线程与渲染线程

游戏大多使用单独的渲染线程,有单独的 Surface ,直接跟 SurfaceFlinger 进行交互,其主线程的存在感比较低,绝大部分的逻辑都是自己在自己的渲染线程里面实现的。可以看到王者荣耀主线程的主要工作,就是把 Input 事件传给 Unity 的渲染线程,渲染线程收到 Input 事件之后,进行逻辑处理,画面更新等。

Flutter 的主线程和渲染线程

由于 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 提交渲染数据。

性能

如果主线程需要处理所有任务,则执行耗时较长的操作(例如,网络访问或数据库查询)将会阻塞整个界面线程。一旦被阻塞,线程将无法分派任何事件,包括绘图事件。主线程执行超时通常会带来两个问题

  1. 卡顿:如果主线程 + 渲染线程每一帧的执行都超过 16.6ms(60fps 的情况下),那么就可能会出现掉帧。
  2. 卡死:如果界面线程被阻塞超过几秒钟时间(根据组件不同 , 这里的阈值也不同),用户会看到'应用无响应'对话框(部分厂商屏蔽了这个弹框,会直接 Crash 到桌面)

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

Binder和锁竞争

Binder概述

Android中大部分的进程间通信都是用Binder。它是Android操作系统中一个高效的、基于C++实现的轻量级RPC(远程过程调用)机制。

Binder通信机制在内核层有一个Binder驱动,以及在用户层有相应的库来支持。当一个进程(客户端)想要与另一个进程(服务端)通信时,客户端会通过Binder驱动发送请求,Binder驱动再将请求转发给服务端。这个过程是通过创建Binder对象来实现的,服务端会注册这个Binder对象,而客户端则可以通过这个对象发起对服务端的调用。

在Systrace中的Binder和锁,很多卡顿问题和相应速度问题,是因为跨进程Binder通信的时候,锁竞争导致binder通信时间变长,影响了调用端。最常见的就是应用渲染线程 dequeueBuffer 的时候 SurfaceFlinger 主线程阻塞导致 dequeueBuffer 耗时,从而导致应用渲染出现卡顿; 或者 SystemServer 中的 AMS 或者 WMS 持锁方法等待太多, 导致应用调用的时候等待时间比较长导致主线程卡顿。

锁的信息

ActivityTaskManagerService 的 getFocusedStackInfo 方法在执行过程中被阻塞,原因是因为执行同步方法块的时候,没有拿到同步对象的锁的拥有权;需要等待拥有同步对象的锁拥有权的另外一个方法 ActivityTaskManagerService.activityPaused 执行完成后,才能拿到同步对象的锁的拥有权,然后继续执行。

等锁执行

在Systrace中,Binder信息显示Waiters=2,意味着还有两个操作在等锁释放,也就是说一共有三个操作在等Binder释放锁,Binder的执行情况是此时正在执行 activityPaused,中间也有一些其他的 Binder 操作,最终 activityPaused 执行完成后,释放锁。

img

上图中可以看到 mGlobalLock 这个对象锁的争夺情况

  1. Binder_1605_B 首先开始执行 activityPaused,这个方法中是要获取 mGlobalLock 对象锁的,由于此时 mGlobalLock 没有竞争,所以 activityPaused 获取对象锁之后开始执行
  2. android.display 线程开始执行 checkVisibility 方法,这个方法也是要获取 mGlobalLock 对象锁的,但是此时 Binder_1605_B 的 activityPaused 持有 mGlobalLock 对象锁 ,所以这里 android.display 的 checkVisibility 开始等待,进入 sleep 状态
  3. android.anim 线程开始执行 relayoutWindow 方法,这个方法也是要获取 mGlobalLock 对象锁的,但是此时 Binder_1605_B 的 activityPaused 持有 mGlobalLock 对象锁 ,所以这里 android.display 的 checkVisibility 开始等待,进入 sleep 状态
  4. android.bg 线程开始执行 getFocusedStackInfo 方法,这个方法也是要获取 mGlobalLock 对象锁的,但是此时 Binder_1605_B 的 activityPaused 持有 mGlobalLock 对象锁 ,所以这里 android.display 的 checkVisibility 开始等待,进入 sleep 状态

经过上面四步,就形成了 Binder_1605_B 线程在运行,其他三个争夺 mGlobalLock 对象锁失败的线程分别进入 sleep 状态,等待 Binder_1605_B 执行结束后释放 mGlobalLock 对象锁

img

上图可以看到 mGlobalLock 锁的释放和后续的流程

  1. Binder_1605_B 线程的 activityPaused 执行结束,mGlobalLock 对象锁释放
  2. 第一个进入等待的 android.display 线程开始执行 checkVisibility 方法 ,这里从 android.display 线程的唤醒信息可以看到,是被 Binder_1605_B(4667) 唤醒的
  3. android.display 线程的 checkVisibility 执行结束,mGlobalLock 对象锁释放
  4. 第二个进入等待的 android.anim 线程开始执行 relayoutWindow 方法 ,这里从 android.anim 线程的唤醒信息可以看到,是被 android.display(1683) 唤醒的
  5. android.anim 线程的 relayoutWindow 执行结束,mGlobalLock 对象锁释放
  6. 第三个进入等待的 android.bg 线程开始执行 getFocusedStackInfo 方法 ,这里从 android.bg 线程的唤醒信息可以看到,是被 android.anim(1684) 唤醒的

经过上面 6 步,这一轮由于 mGlobalLock 对象锁引起的等锁现象结束。这里只是一个简单的例子,在实际情况下,SystemServer 中的 BInder 等锁情况会非常严重,经常 waiter 会到达 7 - 10 个,非常恐怖。

Triple Buffer(三重缓冲)

怎么定义掉帧?

一般主线程超过 16.6 ms 就会掉帧,但是其实不是所有的掉帧都是这种情况。

App端判断掉帧

在Systrace 中,从理论上来说,主线程绘制时间超过16.6ms,这个应用就应该是掉帧了,但其实不一定,因为有BufferQueue和TripleBuffer的存在,此时BufferQueue中可能又上一帧或上上一帧准备好的Buffer,可以直接被SurfaceFlinger拿去合成。所以从Systrace的App端是没有办法判断应用是否掉帧,需要从Systrace的SurfaceFlinger端去看。

SurfaceFlinger端判断掉帧

SurfaceFlinger 端可以看到 SurfaceFlinger 主线程和合成情况和应用对应的 BufferQueue 中 Buffer 的情况。如上图,就是一个掉帧的例子。App 没有及时渲染完成,且此时 BufferQueue 中也没有前几帧的 Buffer,所以这一帧 SurfaceFlinger 没有合成对应 App 的 Layer,在用户看来这里就掉了一帧。

而在第一张图中我们说从 App 端无法看出是否掉帧,那张图对应的 SurfaceFlinger 的 Trace 如下, 可以看到由于有 Triple Buffer 的存在, SF 这里有之前 App 的 Buffer,所以尽管 App 测一帧超过了 16.6 ms, 但是 SF 这里依然有可用来合成的 Buffer, 所以没有掉帧。

SurfaceFlinger

BufferQueue和TripleBuffer

BufferQueue

img

生产者Producer和BufferQueue缓冲区,和Consumer消费者,三者之间的关系是:

  1. Producer需要Buffer时,通过调用dequeueBuffer并指定Buffer的宽度,高度,像素格式和使用标志,然后从BufferQueue中请求释放Buffer。
  2. Producer填充Buffer,然后调用queueBuffer将缓冲区放回队列。
  3. Consumer使用acquireBuffer方法获取Buffer并消费Buffer的内容。
  4. 使用完成后,Consumer会调用release将Buffer返回队列之中。

在Android App的渲染界面中,App就是Producer,SrufaceFlinger是一个Consumer。所以这个流程就相当于:

  1. 当App需要Buffer时,调用unqueueBuffer,然后指定宽度,高度,像素格式和使用标志,从BufferQueue中请求释放Buffer;
  2. App可以使用CPU进行渲染,也可以调用GPU进行渲染,渲染完成后可以调用queueBuffer将缓冲区Buffer返回到App对应的BufferQueue(如果是GPU渲染的话,这里会有一个GPU的渲染过程)。
  3. SurfaceFlinger在收到Vsync信号之后,开始准备合成。使用acquireBuffer获取App对应的Buffer进行合成操作。
  4. 合成结束后,SurfaceFlinger将通过调用releaseBuffer将Buffer返回到App对应的BufferQueue中。

Single Buffer

单 Buffer 的情况下,因为只有一个 Buffer 可用,那么这个 Buffer 既要用来做合成显示,又要被应用拿去做渲染。理想情况下是可以完成任务的(有 Vsync-Offset 存在的情况下):

  1. App收到Vsync信号,获取Buffer开始渲染。
  2. 间隔Vsync-Offset时间后,SurfaceFlinger收到Vsync信号,开始合成。
  3. 屏幕刷新,我们看到合成后的画面。

上面只是理想情况,如果在在这个过程中App渲染或SurfaceFlinger合成在屏幕刷新之前没有完成,Buffer就是不完整的,用户看来就有一种撕裂的感觉。

Double Buffer

DoubleBuffer相当于BufferQueue中有两个Buffer可供轮转,消费者在消费Buffer的同时,生产者可以拿到另一个Buffer进行生产操作。

但是DoubleBuffer也会存在性能上的问题,比如当App显示错过了SurfaceFlinger的合成时机,就是屏幕要进行显示的时候上一个Buffer还没有合成,就会出现掉帧情况。

Triple Buffer

在TripleBuffer中,加入了一个BackBuffer,这样的话就有三个Buffer进行轮转,当第一个Buffer被使用的时候,App中还有两个空闲的Buffer拿去生产,当一个Buffer生产过程中GPU超时,CPU仍然可以拿到BackBuffer进行生产(一个被拿去消费,GPU使用一个Buffer,CPU使用一个Buffer)。

TripleBufferPipline_NoJank

TripleBuffer的作用

  1. 缓解掉帧: 在DoubleBuffer和TripleBuffer的对比下,可以明显发现,在主线程连续超时的情况下,三个Buffer可以明显有效缓解掉帧出现的次数。(从两次掉帧->一次掉帧)

  2. 减少主线程和渲染线程等待时间: DoubleBuffer的时候,App主线程大部分时间需要等待SurfaceFlinger消费释放Buffer之后,才能获取Buffer进行生产,这个时候就有个问题,如果SurfaceFlinger和App同时收到Vsync信号,如果主线程等待SurfaceFlinger消费释放,势必会让App主线程执行时间延后,而 三个 Buffer 轮转的情况下,则基本不会有这种情况的发生,渲染线程一般在 dequeueBuffer 的时候,都可以顺利拿到可用的 Buffer。

  3. 降低GPU和SrufaceFlinger瓶颈: 双 Buffer 的时候,App 生产的 Buffer 必须要及时拿去让 GPU 进行渲染,然后 SurfaceFlinger 才能进行合成,一旦 GPU 超时,就很容易出现 SurfaceFlinger 无法及时合成而导致掉帧

    在三个 Buffer 轮转的时候,App 生产的 Buffer 可以及早进入 BufferQueue,让 GPU 去进行渲染(因为不需要等待,就算这里积累了 2 个 Buffer,下下一帧才去合成,这里也会提早进行,而不是在真正使用之前去匆忙让 GPU 去渲染),另外 SurfaceFlinger 本身的负载如果比较大,三个 Buffer 轮转也会有效降低 dequeueBuffer 的等待时间

CPU Info

CPU 区域图例

Systrace中CPU Info一般经常用到的信息包括:

  1. CPU频率变化情况。
  2. 任务执行情况。
  3. 大小核的调度情况。
  4. CPU Boost调度情况。

在Systrace中Kernel CPU Info这里一般是看任务调度信息,查看是否是频率或者调度导致当前任务出现性能问题,例如:

  1. 某个场景任务下hi行比较慢,可以查看这个任务是不是被调度到了小核或者执行这个任务的CPU的频率是不是不够。
  2. 特殊任务能不能放大核里面跑,或者限制CPU最低频率。

核心架构

目前的手机CPU按照核心数和架构分为三类:非大小核架构,大小核架构,大中小核架构

CPU0-CPU3:小核心;CPU4-CPU6:中核心;CPU7:大核心。

非大小核架构

很早的机器的CPU只有双核或者四核的时候,一般只有一种核心架构,也就是说这四个或者两个核都是同构的,相同的频率,相同的功耗,一起开启或者关闭;现在大部分机器已经不适用非大小核架构了。

大小核架构

现在的CPU一般都是8核,0-3为小核,4-7为中大核。

小核一般来说主频低,功耗也低,使用的一般是armA5X系列,比如高通晓龙845,小核心是由四个A55(最高主频1.8GHz)组成。

大核心一般来说最高主频比较高,功耗相对来说也会比较高,使用的一般是armA7X系列,比如高通晓龙845,大核心是由四个A75(最高主频2.8GHz)组成。

大小核还有一些变种,有的是4+2或者6+2,但是原理还是一样的,大核是用来支持高负载的场景,小核用来日常使用。

大中小核架构

部分 CPU 比较另辟蹊径,选择了大中小核的架构,比如高通骁龙 855 8 核 (1 个 A76 的大核+3 个 A76 的中核 + 4 个 A55 的小核)和之前的的 MTK X30 10 核 (2 个 A73 的大核 + 4 个 A53 的中核 + 4 个 A35 的小核)以及麒麟 980 8 核 (2 个 A76 的大核 + 2 个 A76 的中核 + 4 个 A55 的小核)。

相较于大小核结构,大中小核中可以说是有一个或两个超大核,这种超大核的主频一般比较高,功耗也会高很多,用来处理一些比较繁重的任务。

绑核

绑核就是把某个任务绑定在某个或者某些核心上,用来满足这个任务的性能需求

  1. 任务本身负载比较高,需要在大核上才能满足需求。
  2. 任务本身不想被频繁切换,需要绑定在一个核心上面。
  3. 任务本身不重要,对时间要求不高,可以绑定或限制在小核上。

上面是三种例子,目前Android中绑核操作一般是由系统来实现的,常用有三种方法

配置CPUset

使用CPUset子系统可以限制某一类任务跑在特定的CPU或者CPU组里面。比如下面,Android中会默认划分一下CPU组,厂商可以根据针对不同的CPU架构进行定制,默认划分的有:

  1. system-backgound:一些优先级低的会划分到这里,在小核里面运行。
  2. foreground:前台进程。
  3. top-app:目前正在前台和用户进行交互的进程。
  4. background:后台进程。
  5. foreground/boost前台boost进程,通常是用来联动的,现在已经没有再用了。之前是应用启动的时候会将foreground里面的进程迁移到这个进程组里面来。

需要注意每个任务跑在哪个组里面,是动态的,并不是一成不变的,有权限的进程就可以改

部分进程也可以在启动的时候就配置好跑到哪个进程里面,大部分 App 进程是根据状态动态去变化的。

配置 affinity

使用affinity也可以设置任务跑在哪个核心上,其系统调用的taskset,taskset用来查看和设定CPU优先级,可以查看或者配置进程和CPU的绑定关系,让某进程在指定的CPU核上运行,也就是绑核。

taskset用法

显示进程运行的CPU:taskset -p pid

这个命令返回是十六进制的,转换成二进制后,每一位对应一个逻辑CPU,低位是0,以此类推,如果某个位置上是1,表示该进程绑定了该CPU。

绑核设定

taskset -pc 3 pid  //表示将进程pid绑定到第三个核上
taskset -c 3 command //表示执行command命令,并将command启动的进程绑定到第三个核上

Android中也可以使用这个系统调用,将任务绑定到对应的核上运行。某些较老的内核不支持CPUset就会用taskset进行设置。

调度算法

在Linux的调度算法中修改调度逻辑,也可以让指定的task跑在指定的核上面,部分厂家的核调度优化就是使用这种方式。

锁频

调度器判断任务执行显示在小核中,如果发现频率不够,就会去往上提升,这样一套下来会浪费很多时间,在这种情况下,系统一般会直接将硬件资源拉到最高去执行,比如CPU、GPU、IO、BUS等。除此之外像一些其他场景也会限制其资源使用,比如发热严重,就会限制CPU的最高频率,达到降温目的;有时候基于功耗的考虑,会限制一些资源的某些场景的使用。

Android系统一般在下面几个场景直接进行锁频

应用启动、应用安装、转屏、窗口动画、List Fling、Game

CPU状态

每个核都有对应的C-State,CPU启动的状态在Systrace中查看的话是高度不同。之前的CPU支持热插拔,不用的时候可以直接关闭,目前CPU都不支持热插拔,而是使用C-State。

Systrace中的详细信息

Systrace可以用图形化界面的形式打开,也可以文本的形式打开,可以看到一些详细信息。

posted @ 2025-04-16 10:32  屈家全  阅读(198)  评论(0)    收藏  举报