恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
用Perfetto透视Flutter混合工程启动加载全流程
首页
资讯中心
/
用Perfetto透视Flutter混合工程启动加载全流程
用Perfetto透视Flutter混合工程启动加载全流程
发布时间:2026/10/14 13:28:52
这一篇接着我们系列的加载性能话题前几篇聊过Flutter混合工程的启动流程也聊过Android侧的trace工具这次把两者串起来用perfetto同时观察Flutter和Android两个层面的加载过程。做性能优化最忌讳没有现场就下手猜而perfetto能把App从进程诞生、Activity建立、Flutter engine初始化、Dart首帧到Surface上屏的整个过程原样录下来。这篇文章适合正在被启动白屏、首帧慢、混合页面卡顿问题折腾的同学跟着跑完一遍你能独立抓取一份可分析的trace并且能清楚指出耗时瓶颈到底在Android层还是Flutter层。1. 加载过程的全貌Flutter和Android其实是两条线在跑1.1 一条App启动两条执行路线一个Flutter应用在Android上加载很多人把它理解成“先启动Activity再启动Flutter”但实际情况要复杂得多。从perfetto的视角看这是两条相对独立又必须协作的执行路线。第一条是Android侧的执行流。应用进程被孵化器创建ActivityThread开始跑目标Activity被实例化接着是View树构建、Window attach、Surface创建最后交给SurfaceFlinger合成显示。这一侧牵扯大量系统服务WindowManager负责窗口层级ActivityTaskManager管理任务栈Binder贯穿整个通信过程。这些系统行为全部会在perfetto里留下可追踪的slice。第二条是Flutter侧的执行流。FlutterEngine创建后Dart isolate初始化Widgets binding启动然后执行build、layout、paint流程最后raster线程生成layer tree提交给GPU。这条线主要在Flutter自己的线程上运行与Android主线程并不在一个线程里而是通过vsync信号和Surface做交接。关键就在“交接”这个环节。如果只盯App内部日志你只能看到自己埋点的时间点如果只看Flutter的Timeline你会漏掉Android系统侧在干什么。只有把两条线放在同一根时间轴上对比才能判断白屏到底卡在“Flutter还没有渲染出来”还是“渲染好了但Android没有合成上屏”。1.2 为什么偏偏用perfetto而不是其他方式之前也试过其他工具各自有各自的问题。Android Studio的CPU Profiler用起来方便点一下按钮就能看到调用栈和线程活动但它更偏向交互式分析而且对启动过程这种“从系统侧拉起来”的场景容易漏掉进程创建阶段的事件。应用自己的日志只能证明某个时间点被执行了无法解释线程为什么在那个时间点没有schedule到CPU上。Flutter自带Timeline可以看Dart侧的事件但和Android系统事件割裂两边的时钟对起来很麻烦。perfetto的优势是系统级、无侵入、跨进程。它直接在内核持续追踪层采集数据应用不用加agent也不影响性能录到的是整机所有进程在同一时间轴上的行为。你既能看到目标App的线程调度也能看到SurfaceFlinger、system_server、CPU频率和GPU活动。对启动加载这种多进程协作场景这是最合适的观察手段。1.3 抓取前的环境准备动手之前先把基础环境确认好。需要一台Android 10以上的设备开发者选项里打开USB调试。先跑一下命令确认perfetto可用adb shell perfetto --version正常会返回版本号。如果设备上报错或者提示命令不存在多半是系统镜像太老建议换一台新系统设备。Flutter项目要打一个profile构建包命令是flutter build apk --profile。为什么要profile而不是debug或者releasedebug包性能太差测出来的数据失真release包会把Flutter engine的大量timeline事件关掉很多关键阶段标记看不到。profile模式保留了timeline信息同时性能表现接近release是做启动分析的常规选择。分析阶段需要访问ui.perfetto.dev这是一个浏览器端的trace分析工具直接把pull下来的文件拖进去就能看不需要额外安装桌面软件。2. 完整抓取一次带Flutter标记的trace2.1 命令行抓取最简单的一条命令先给最简单的方案一条adb命令搞定采集。打开终端执行adb shell perfetto -o /data/misc/perfetto-traces/startup_trace.perfetto-trace -t 15s -b 64mb sched freq idle am wm gfx view稍微拆解一下参数。-o指定输出路径写到/data/misc/perfetto-traces这个目录下普通shell用户就有写权限。-t 15s是采集时长启动过程一般10到20秒足够覆盖。-b 64mb是buffer大小buffer太小容易丢尾部事件分析首帧最怕丢数据宁可大一点。后面的sched freq idle am wm gfx view是数据源categories每一个都有具体含义sched线程调度事件看关键线程什么时候被拉到CPU上freqCPU频率变化idleCPU核的空闲状态amActivityManager事件能看到进程启动和Activity创建wmWindowManager事件窗口attach和relayoutgfx和view渲染相关包含Choreographer和doFrame等信息。顺序也很重要。抓取之前先执行的这几步很多人会忽略导致trace里看不到启动第一帧adb logcat -c adb shell am force-stop 你的包名然后启动perfetto采集紧接着用另一个终端执行adb shell am start -W 你的包名/你的启动Activityam start -W会输出一个TotalTime这个东西可以作为辅助参考。但要注意它统计的是Activity启动完成时间并不完全等于Flutter首帧上屏时间两者之间的差值本身就可能是问题。抓完之后从设备上把trace拉回来adb pull /data/misc/perfetto-traces/startup_trace.perfetto-trace ./startup_trace.perfetto-trace拉到本地后拖到ui.perfetto.dev就能打开。注意如果设备上提示perfetto权限不足去开发者选项里把“系统跟踪”打开部分深度定制系统需要额外授权。2.2 需要更细粒度时用配置文件替代命令行命令行虽然方便但事件种类只能选固定的几个而且没法设置更细的参数。遇到需要观察Binder通信或者调整buffer策略的情况建议用文本配置文件。先把配置文件写好buffers { size_kb: 65536 } duration_ms: 15000 data_sources { config { name: linux.ftrace ftrace_config { ftrace_events: sched/sched_switch ftrace_events: sched/sched_wakeup ftrace_events: power/cpu_frequency } } } data_sources { config { name: atrace atrace_config { categories: gfx categories: view categories: am categories: wm app_name: 你的包名 } } }这个配置同时启用了内核ftrace事件和atrace的应用层事件app_name用于只追踪指定应用的atrace标记避免其他App噪音干扰。然后push到设备并执行adb push config.pbtx /data/local/tmp/ adb shell perfetto --txt -c /data/local/tmp/config.pbtx -o /data/misc/perfetto-traces/custom_trace.perfetto-trace--txt表示配置是文本格式有些老版本Android设备只支持二进制protobuf格式遇到报错就查一下设备上perfetto --help的说明。配置文件最大的价值是可以精确控制想要哪些事件buffer策略也更灵活。2.3 在关键位置打点给trace加“路标”默认情况下采集到的trace里能看到系统事件和Flutter engine的部分事件但业务代码具体什么时候跑到哪里还是需要自己埋点。埋点的好处是快速对齐“业务时间”和“系统时间”不用靠猜。Android宿主侧在Activity的onCreate里加import android.os.Trace; Override protected void onCreate(Bundle savedInstanceState) { Trace.beginSection(AppStartup_onCreate); super.onCreate(savedInstanceState); // 业务逻辑 Trace.endSection(); }Flutter侧需要引入Timelineimport package:flutter/foundation.dart; void main() { Timeline.startSync(AppStartupDart); runApp(MyApp()); Timeline.finishSync(); }这些打点会出现在perfetto的atrace事件流里用搜索一查就能定位。我的习惯是每个关键阶段都埋比如FlutterEngineCache_put、Activity_onResume、FirstFrame_Callback名字统一带项目前缀后续批量统计也好用。2.4 拉出trace用Perfetto UI快速定位关键区间trace文件拖进ui.perfetto.dev之后第一件事不是看细节而是把视野缩放到启动时间段。整个trace可能有15秒而启动过程往往只有头几百毫秒到一两秒。几个快捷键是必须记住的。W和S是放大和缩小时间轴A和D左右平移按住鼠标可以框选区域双击某个slice会直接跳转到它所在的线程和上下文。按M键可以在当前位置打一个临时标记做阶段对比非常方便。右上角的搜索框支持正则表达式输入flutter能看到所有Flutter相关事件输入Rasterizer能看到栅格化过程。左侧面板可以勾选显示Thread States、CPU Slices等行分析线程为什么等待时这一步一定要打开。把线程状态行拖出来颜色越深越说明线程在阻塞等待绿色通常代表正在运行浅色代表可运行但没有被调度。3. 从Trace里拆解Flutter加载的关键阶段3.1 先认清几条核心线程围绕着一张加载trace主要关注下面这几条线程。Flutter不同版本的线程命名有差异常见形式是主线程叫mainFlutter engine线程叫1.ui、2.raster、0.platform这样的组合压测机型上可能还会带进程号后缀。先找到包含ui、raster、platform的线程确认它们归属你的App进程。线程名所属层主要职责trace里重点观察mainAndroid AppActivity生命周期、View构建、事件分发ActivityThread启动、doFrame、inflateuiFlutter EngineDart侧动画、布局、绘制指令生成Flutter.BeginFrame/EndFrame、Dart回调rasterFlutter EngineLayerTree栅格化生成GPU指令Rasterizer::Draw、queueBufferplatformFlutter Engine引擎初始化、插件消息处理Shell初始化、Platform通道消息RenderThreadAndroid系统渲染硬件加速渲染、缓冲提交DrawFrame、eglSwapBuffers加载阶段platform线程会先忙一阵做引擎初始化和插件注册ui线程开始跑Dart代码raster线程要到第一帧需要显示时才正式干活Android的main线程则全程参与Activity生命周期。四个线程谁先谁后、谁等谁就是加载耗时的本质。3.2 按阶段标记把加载流程切开看在perfetto里搜关键字flutter能看到engine侧的一串事件。不同版本命名不完全一样但通常能找到这几类flutter::Shell::Shell或者类似名字Shell创建引擎初始化起点Dart isolate初始化相关事件包含Dart VM启动、加载快照或内核文件flutter::Rasterizer::Draw栅格化线程真正绘制layer tree的slice通常比较长与vsync相关的事件UI帧调度开始firstFrameRasterized或带first frame字样的标记Flutter侧首帧栅格化完成。分析思路是沿着时间轴从进程诞生开始找到引擎初始化起点再找到Dart isolate完成点然后找到ui线程第一次BeginFrame到EndFrame的区间最后定位Rasterizer::Draw结束处。这个过程可以切成四段进程和Activity建立、引擎初始化、Dart首帧构建、栅格化上屏。哪一段占比最长就往哪个方向继续深挖。Perfetto的搜索框支持正则表达式可以输入^flutter批量过滤把所有Flutter事件列出来。但不要指望所有版本引擎事件名完全一致看到的是1.ui的线程在某个时间段里有长slice就知道Dart侧在跑重量级逻辑。3.3 Android侧的启动事件和Surface事件怎么看分析的主角虽然是Flutter但Android侧的Activity启动和Surface流程同样重要。在trace里搜索am_proc_start可以定位进程创建时间搜索wm_task_created能看到任务栈创建startActivity相关slice能对应到系统拉起Activity的时间点。Android系统的启动事件和Flutter事件是交替出现的可对比的关键点有两处。第一处是进程创建到FlutterEngine创建之间的间隔这段时间纯粹消耗在系统孵化和Activity创建上第二处是Flutter首帧Ready到SurfaceFlinger实际合成本文之间的间隔这期间可能在等Surface attach或者等待vsync信号。如果日志里已经看到了Flutter首帧完成但屏幕一直白屏十有八九问题在SurfaceFlinger合成或者View attach流程。这时候把视野拉到SurfaceFlinger进程上看它的onTransaction和queueBuffer相关slice能看到合成究竟卡在哪里。3.4 别把两个“首帧”搞混了这是排查时最容易被绕进去的地方。Flutter侧的“首帧”和Android侧的首帧并不是同一个东西。Flutter的firstFrameRasterized表示layer tree已经在raster线程上栅格化完成GPU命令已经生成。Android侧的首帧是指SurfaceFlinger把这块buffer真正合成到屏幕上。两个时间点之间至少隔着一次buffer队列提交和一次vsync同步。极端情况下Flutter首帧完成得很早但surface一直在等待状态用户看到的仍然是白屏——这种情况如果只用Flutter的日志排查永远找不到答案。定位两个时间点就是从firstFrameRasterized事件和SurfaceFlinger的提交事件中间的时间差入手。之后再把RenderThread上的queueBuffer事件一起考虑基本能还原一次完整的上屏链路。4. 典型耗时瓶颈与排查方向4.1 一次典型的双端耗时对比之前接手一次启动优化现象是某App冷启动接近1秒用户感知明显。当时的第一步就是把日志埋点和perfetto trace一起抓了。trace打开之后先看整体区间从am_proc_start到firstFrameRasterized大概700ms出头这比预期要高。继续往下切发现FlutterEngine的Shell创建其实只占了一小段Dart isolate初始化也不是大头真正的长slice出现在main线程上的View构建阶段——一段很厚的inflate相关slice同时ui线程大量时间处于浅色等待状态。也就是说Flutter侧已经准备好执行Dart逻辑了结果因为Android主线程还在做Window相关的活vsync信号迟迟没有送来。处理方式是把一部分不依赖Window的初始化逻辑延后同时把FlutterEngine预热提前。改完再抓一轮tracemain线程的inflate区间明显变薄ui线程开始干活的时间点提前了整体首帧时间压缩了约200ms。这个例子的价值在于只看日志会以为是Flutter引擎慢实际瓶颈在Android主线程的窗口建立流程上。两边的trace数据摆在一起结论才不会被带偏。4.2 底部曲线怎么读频率和空闲状态Perfetto界面下方通常会有CPU Frequency、CPU Idle、CPU Slices曲线。这些曲线看起来不起眼但读懂了能快速判断是计算瓶颈还是调度瓶颈。CPU频率曲线如果在大负载时段一直贴着最高频说明当前是真实计算压力如果频率起不来一直低水平运行就要考虑供电策略、温控降频或者系统负载不高但任务长期得不到调度。CPU Idle曲线则能看出整体系统空闲程度如果设备整体很忙其他进程占着核心你的App线程就只能排队。观察线程调度要配合CPU Slices行绿色表示线程正在某个核上执行浅色表示线程处于Runnable但没抢到CPU深色表示阻塞等待。如果整条时间线上绿色很短、浅色很长说明App线程在等待调度器分配核心这时候硬编码减少业务逻辑意义不大看看是不是启动阶段线程开太多还是在跑重量级后台任务抢占了cpu。4.3 线程状态和GPU瓶颈的交叉判断当Rasterizer::Draw这种slice特别长时先不要急着下结论说GPU不行。要看对应的freq曲线如果栅格化阶段频率拉满说明确实在做大量计算可以考虑减少重叠效果、检查离线渲染路径如果频率不高slice却很长大概率不是算力问题而是shader编译、buffer分配或者等某个同步对象。RenderThread上的queueBuffer阻塞是另一个常见坑。当queueBuffer事件间隔很大往往意味着SurfaceFlinger或BufferQueue没能及时消费提交的buffer这回过头来又会影响raster线程的下一次绘制。遇到这种情况优先检查是不是窗口Surface尺寸与Flutter生成的buffer不一致或者设备启用了某种裁剪合成策略。还有一条经验ui线程slice过长的情况里有很大比例不是算法本身慢而是后续的vsync被错过。ui线程跑太久错过了下一次vsync信号整个帧就得推迟到下一拍视觉上就表现为首帧或滑动卡顿。perfetto里能看到Choreographer相关事件对比BeginFrame和物理vsync之间的时间偏移就能看出是不是这个原因。5. 让分析可复现从Trace里的信息到实际优化动作5.1 用SQL查询批量统计关键slice耗时Perfetto UI自带一个Query分析页签支持用SQL查询slice数据。分析完单条trace后可以用同一套SQL批量统计优化前后的变化非常方便。SELECT name, count(*) AS cnt, round(avg(dur / 1e6), 2) AS avg_ms, round(sum(dur / 1e6), 2) AS total_ms FROM slice WHERE name GLOB *flutter* OR name GLOB *Rasterizer* OR name GLOB *inflate* GROUP BY name ORDER BY total_ms DESC;这段SQL会列出所有与Flutter和栅格化相关的slice的调用次数、平均耗时和总耗时。跑出来的结果比肉眼在时间轴上框选要准确得多尤其在对比两个版本的trace时可以直接给出量化结论。建议每次抓trace都保留一份带版本号和时间戳的命名比如trace_20240101_rel100_before.perfetto-trace和trace_20240110_rel101_after.perfetto-trace。时间久了这些文件就是性能回归的证物。之前在排查一个偶现的卡顿时靠的就是三天前留的baseline trace直接对比出多出来的一个shader编译耗时段否则单看一次trace很难确认什么才算异常。5.2 从trace数据反推代码层面的优化策略trace告诉你“耗时在哪”代码改动要回答“怎么把耗时降下去”。如果耗时段集中在Android主线程的inflate和relayout思路是减少启动阶段Android View层级或者把FlutterEngine预热提前让两条线并行起来。如果耗时段在Dart侧build和layout属于组件树太复杂需要考虑减少首帧页面元素用异步加载替代同步等待。这里踩过一个坑启动阶段为了做预加载开了好几个后台线程去拉配置和图片结果perfetto显示这些线程把CPU核心吃满Flutter的ui线程反而长时间等待调度。优化了一阵业务逻辑都没效果直到把trace的CPU Slices拉出来才发现是自研的后台任务在捣乱。延迟到首帧结束再启动这些任务问题立刻缓解。所以优化启动性能的原则之一是不要在启动阶段额外制造线程竞争预加载听上去很理想但代价经常被忽略。5.3 形成“先录现场再改代码”的固定工作流工具本身不难难的是养成的分析习惯。我自己固定下来的一套流程是每次大版本改造前后在固定的测试机、固定的网络和充电状态下抓一份profile模式的冷启动trace。采集完成后先跑一遍SQL看关键slice统计再人工核对一次Rasterizer::Draw和inflate的拖影情况最后留档保存。三五个版本下来这些trace数据会形成一个性能基线库。某次发布后发现用户反馈变卡把当期的trace和基线库一拉出来变化一目了然。这比靠记忆和日志猜问题靠谱太多。Flutter和Android加载过程的坑很隐蔽但trace不会说谎记录也不会说谎。