稳定性性能系列之八——系统性能分析基础:Systrace与Perfetto入门

性能优化的第一步,不是猜测问题在哪里,而是用数据说话。Trace工具就是那个让性能瓶颈无所遁形的"显微镜"。

引言:从3秒到1秒的优化历程

那是去年Q2的一个周一早晨,产品经理神色凝重地找到我:"用户反馈我们的App启动太慢了,平均3秒才能看到首页,竞品只要1秒。老板说如果这个季度优化不到1.5秒以内,项目可能要黄。"

我打开App试了试,确实慢得让人抓狂。但问题在哪里呢?Application初始化?Activity加载?网络请求?还是图片渲染?

凭感觉优化?那只是在盲人摸象。

于是我打开Systrace,抓取了一份启动Trace。当Chrome打开那个HTML文件的瞬间,整个启动流程一览无遗:

复制代码
Application.onCreate(): 450ms
  ├─ SDK初始化: 280ms    ← 元凶!
  ├─ 数据库迁移: 120ms
  └─ 其他初始化: 50ms

Activity.onCreate(): 380ms
  ├─ setContentView(): 80ms
  ├─ 网络请求: 200ms      ← 在主线程!
  └─ RecyclerView首屏渲染: 100ms

问题一目了然:SDK初始化和网络请求占据了启动时间的一半。

经过两周的优化(SDK延迟初始化、网络请求移到后台、预加载优化),启动时间降到了0.9秒。产品经理再也没提过启动慢的问题,项目也顺利上线了。

这次经历让我明白:性能优化不是靠经验和直觉,而是靠数据。而Trace工具,就是获取这些数据的最强武器。

本文将带你全面掌握Systrace和Perfetto两大性能分析工具。读完本文,你将能够:

  1. 理解Systrace和Perfetto的工作原理和区别
  2. 掌握Trace抓取的完整流程和常用配置
  3. 学会分析Trace文件,快速定位性能瓶颈
  4. 使用代码插桩技术精确追踪关键路径
  5. 运用SQL查询进行高级性能分析

一、Trace工具概述

1.1 什么是Trace

Trace(跟踪) 是一种记录系统运行时事件序列的技术。简单来说,就是给系统"装个监控摄像头",记录下指定时间段内发生的所有关键事件。

Trace能记录什么?

类型 内容 用途
函数调用 方法进入/退出时间 分析哪个函数耗时长
线程调度 线程运行/等待/睡眠状态 定位线程阻塞问题
CPU调度 哪个线程在哪个核心上运行 CPU占用分析
渲染事件 VSYNC、SurfaceFlinger绘制 卡顿和掉帧分析
I/O操作 文件读写、网络请求 I/O性能分析
GC事件 垃圾回收的时间和频率 内存抖动分析
Binder调用 跨进程通信 IPC性能分析

Trace能解决什么问题?

启动慢 - 精确定位启动过程中的耗时操作

滑动卡顿 - 找出导致掉帧的函数调用

动画不流畅 - 分析渲染Pipeline的瓶颈

CPU占用高 - 识别CPU密集型操作

功耗高 - 定位频繁唤醒和长时间运行的任务

1.2 Systrace简介

Systrace是Google早期推出的系统级性能分析工具,从Android 4.1开始引入。

核心特点:

  • 基于Linux ftrace机制
  • 轻量级,对系统性能影响小
  • 以HTML文件呈现,使用Chrome Trace Viewer查看
  • 支持Android 4.1到Android 12的所有版本

工作原理:

优势:

  • ✅ 简单易用,一行命令即可抓取
  • ✅ 兼容性好,支持老版本Android
  • ✅ 无需额外安装工具

局限性:

  • ❌ UI体验一般(基于Chrome插件)
  • ❌ 不支持SQL查询
  • ❌ 大文件加载慢
  • ❌ 功能相对基础

1.3 Perfetto简介

Perfetto 是Google从Android 10开始推出的新一代性能分析平台,旨在取代Systrace。

核心特点:

  • 现代化的Web UI(https://ui.perfetto.dev/)
  • 强大的SQL查询引擎
  • 支持Protobuf二进制格式,文件更小
  • 更丰富的数据源(内存、电量、网络等)
  • 向后兼容,可以打开Systrace文件

工作原理 :

优势:

  • ✅ 现代化UI,交互体验极佳
  • ✅ SQL查询引擎,支持复杂分析
  • ✅ 更丰富的数据源
  • ✅ 文件更小,加载更快
  • ✅ 持续更新,功能不断增强

局限性:

  • ⚠️ 主要支持Android 10+
  • ⚠️ 学习曲线稍陡(SQL查询)

1.4 工具对比

特性 Systrace Perfetto
推出时间 Android 4.1 (2012) Android 10 (2019)
UI体验 Chrome Trace Viewer 现代Web UI ⭐⭐⭐⭐⭐
功能丰富度 基础 强大 ⭐⭐⭐⭐⭐
数据格式 HTML Protobuf (更小)
查询能力 SQL查询 ⭐⭐⭐⭐⭐
数据源 ftrace ftrace + 内存 + 电量 + 网络
兼容性 Android 4.1 - 12 Android 9+ (最好10+)
文件大小 较大 更小
学习曲线 简单 中等
推荐使用 Android 9及以下 Android 10+ ⭐推荐

选择建议:

  • 如果你的项目主要支持Android 10+,强烈推荐Perfetto
  • 如果需要兼容Android 9及以下,使用Systrace
  • 两者可以共存,根据场景灵活选择

二、Systrace使用指南

2.1 环境准备

前置条件:

  1. ✅ Python 2.7+ 或 Python 3.x
  2. ✅ Android SDK Platform-Tools (adb命令)
  3. ✅ Chrome浏览器

Systrace位置:

bash 复制代码
# Android SDK中的位置
$ANDROID_HOME/platform-tools/systrace/systrace.py

# 验证Python环境
python --version  # 或 python3 --version

# 验证adb连接
adb devices

设备权限设置:

bash 复制代码
# 1. 开启开发者选项
# 设置 → 关于手机 → 连续点击"版本号"7次

# 2. 开启USB调试
# 设置 → 开发者选项 → USB调试

# 3. 授予adb root权限(如需要)
adb root
adb remount

2.2 抓取Systrace

基础用法:

bash 复制代码
# 进入systrace目录
cd $ANDROID_HOME/platform-tools/systrace

# 抓取10秒的trace
python systrace.py -o trace.html -t 10 -b 32768 \
  gfx input view webview wm am sm audio video camera hal \
  app res dalvik rs bionic power pm ss database network

# 参数说明:
# -o trace.html   : 输出文件名
# -t 10           : 抓取时长(秒)
# -b 32768        : buffer大小(KB),建议32MB以上
# -a package_name : 指定应用包名(可选)
# 后面是category列表

推荐配置(通用场景):

bash 复制代码
python systrace.py \
  -o trace.html \
  -t 10 \
  -b 32768 \
  sched freq idle am wm gfx view binder_driver hal \
  dalvik input res

2.3 常用Category说明

Category 说明 典型用途
sched CPU调度事件 必选,分析线程运行状态
freq CPU频率变化 CPU性能分析
idle CPU idle状态 功耗分析
gfx SurfaceFlinger, VSYNC, Texture 渲染分析必选
input Input事件分发 触摸响应分析
view View绘制(measure/layout/draw) 卡顿分析必选
webview WebView性能 H5页面分析
wm Window Manager 窗口管理分析
am Activity Manager生命周期 启动分析必选
sm Sync Manager 同步操作分析
audio Audio播放 音频卡顿分析
video Video解码 视频性能分析
camera Camera操作 相机性能分析
hal HAL层事件 底层硬件调用
app 应用自定义trace 插桩分析必选
res 资源加载 资源加载性能
dalvik GC事件 内存抖动分析必选
rs RenderScript 图像处理分析
bionic Bionic库调用 Native性能分析
power Power HAL 电量分析
pm Package Manager 安装/卸载分析
ss System Server 系统服务分析
database SQLite操作 数据库性能分析
network 网络I/O 网络性能分析
binder_driver Binder驱动 IPC分析必选

场景推荐配置:

bash 复制代码
# 启动性能分析
python systrace.py -o launch.html -t 10 -a com.example.app \
  sched freq idle am wm gfx view binder_driver hal dalvik input res

# 滑动卡顿分析
python systrace.py -o scroll.html -t 10 -a com.example.app \
  sched gfx view input wm dalvik app

# 渲染性能分析
python systrace.py -o render.html -t 10 -a com.example.app \
  sched freq gfx view wm

# 内存抖动分析
python systrace.py -o memory.html -t 10 -a com.example.app \
  sched dalvik app

# 全面分析(文件会很大)
python systrace.py -o full.html -t 10 -a com.example.app \
  sched freq idle am wm gfx view webview binder_driver hal \
  dalvik input res power database network app

2.4 实战演练:抓取应用启动Trace

完整的启动Trace抓取流程:

bash 复制代码
# Step 1: 确定应用包名
adb shell pm list packages | grep example
# 输出: package:com.example.myapp

# Step 2: 清理应用数据(模拟首次启动)
adb shell pm clear com.example.myapp

# Step 3: 启动Systrace抓取(不要立即启动App)
cd $ANDROID_HOME/platform-tools/systrace
python systrace.py -o launch.html -t 10 -a com.example.myapp \
  sched freq idle am wm gfx view binder_driver hal dalvik input res &

# 注意: 最后的 & 让命令在后台运行

# Step 4: 等待2秒,让systrace准备好
sleep 2

# Step 5: 启动应用并记录启动时间
adb shell am start -W com.example.myapp/.MainActivity

# 输出示例:
# Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.example.myapp/.MainActivity }
# Status: ok
# Activity: com.example.myapp/.MainActivity
# ThisTime: 1245      ← Activity启动时间
# TotalTime: 1245     ← 总启动时间
# WaitTime: 1267      ← 等待时间
# Complete

# Step 6: 等待systrace抓取完成(10秒)
# 会自动生成 launch.html

# Step 7: 使用Chrome打开trace文件
google-chrome launch.html
# 或
open -a "Google Chrome" launch.html

Tip: 为了抓取到完整的启动流程,建议trace时长设置为15-20秒,确保从应用冷启动到首屏渲染完成都被记录。

2.5 查看Systrace

1. 打开Trace文件:

  • 使用Chrome浏览器打开trace.html
  • 文件较大时可能需要等待几秒加载

2. 界面布局:

复制代码
┌──────────────────────────────────────────────────┐
│  [?] [W][A][S][D]  搜索:______  时间范围:___     │ 工具栏
├──────────────────────────────────────────────────┤
│  CPU 0                                            │
│  CPU 1                                            │ CPU核心
│  CPU 2                                            │
│  CPU 3                                            │
├──────────────────────────────────────────────────┤
│  com.example.myapp                                │ 应用进程
│    └─ main                  ████████████████      │ 主线程
│    └─ RenderThread          ██████                │ 渲染线程
│    └─ binder:xxx            ███                   │ Binder线程
├──────────────────────────────────────────────────┤
│  system_server                                    │ 系统进程
│    └─ android.fg            █████████             │
│    └─ android.ui            ████                  │
├──────────────────────────────────────────────────┤
│  surfaceflinger                                   │ SurfaceFlinger
│    └─ surfaceflinger        ████████████          │
└──────────────────────────────────────────────────┘

3. 基本操作:

操作 快捷键/方法 说明
放大 W 或 鼠标滚轮上滚 放大时间轴
缩小 S 或 鼠标滚轮下滚 缩小时间轴
左移 A 向左移动时间轴
右移 D 向右移动时间轴
选择区域 鼠标左键拖拽 选中一段时间范围
查看详情 点击色块 查看函数调用详情
搜索 Ctrl+F 或 搜索框 搜索函数名
高亮 点击函数名 高亮所有同名函数
标记 M 添加书签标记

4. 查看函数详情 :

点击一个色块后,右侧会显示详细信息:

复制代码
Title: onCreate
Wall Duration: 450ms        ← 实际耗时
CPU Duration: 420ms         ← CPU占用时间
Self Time: 50ms             ← 自身耗时(不含子调用)
Start: 1234.567ms           ← 开始时间
Args:                       ← 参数信息
  thread_name: main
  process_name: com.example.myapp

5. 分析技巧:

  • 🔍 先缩小查看整体,识别大块耗时
  • 🔍 再放大查看细节,定位具体函数
  • 🔍 关注主线程的空白区域(可能在等待)
  • 🔍 查看CPU调度,识别线程阻塞
  • 🔍 对比VSYNC信号,分析掉帧原因

三、Perfetto使用指南

3.1 三种使用方式

Perfetto提供了三种抓取和分析trace的方式:

方式1: Web UI (⭐推荐)

优点 : 无需安装,功能完整,体验最佳
缺点: 需要网络连接

使用步骤:

复制代码
1. 打开浏览器访问 https://ui.perfetto.dev/
2. 点击"Record new trace"
3. 连接设备 (会提示授权adb)
4. 配置抓取参数
5. 点击"Start Recording"
6. 执行待分析的操作
7. 点击"Stop"并下载trace文件
方式2: 命令行

优点 : 适合自动化脚本,CI集成
缺点: 需要手动配置

使用步骤:

bash 复制代码
# 1. 下载Perfetto工具(如果设备上没有)
adb shell perfetto --version
# 如果没有,从 https://get.perfetto.dev/perfetto 下载

# 2. 推送配置文件到设备
adb push config.pbtx /data/local/tmp/

# 3. 开始抓取
adb shell perfetto \
  -c /data/local/tmp/config.pbtx \
  -o /data/local/tmp/trace.perfetto-trace

# 4. 导出trace文件
adb pull /data/local/tmp/trace.perfetto-trace ./
方式3: Android Studio集成

优点 : 与开发环境集成,操作简单
缺点: 功能相对基础

使用步骤:

复制代码
1. 打开Android Studio
2. View → Tool Windows → Profiler
3. 点击"+"添加Session
4. 选择设备和进程
5. 点击"CPU" → "System Trace"
6. 点击"Record"开始抓取
7. 执行操作后点击"Stop"
8. 查看trace (会自动用Perfetto UI打开)

3.2 Web UI抓取流程详解

让我们详细讲解最推荐的Web UI方式:

Step 1: 打开Perfetto UI

复制代码
浏览器访问: https://ui.perfetto.dev/
建议使用Chrome或Edge浏览器

Step 2: 选择Record new trace

点击左上角的"Record new trace"按钮

Step 3: 连接设备

  • 确保adb已连接: adb devices
  • 在"Target"下拉框中选择你的设备
  • 首次使用会提示安装"Perfetto UI ADB Bridge",点击同意

Step 4: 配置抓取参数

yaml 复制代码
# Recording Settings配置项:

Duration: 10000 ms          # 抓取时长
Buffer size: 32768 KB       # 缓冲区大小

# Probes (数据源)选择:

✅ Scheduling details       # CPU调度(必选)
✅ CPU frequency and idle states  # CPU频率
✅ Syscalls                 # 系统调用
✅ Process tracking         # 进程跟踪
✅ Frame timeline           # 帧时间线(渲染分析必选)
✅ Memory (RSS, PSS, etc)   # 内存统计
✅ Battery drain & power rails  # 电量分析

# Advanced settings:

Atrace categories:          # 选择category
  ✅ gfx
  ✅ input
  ✅ view
  ✅ am
  ✅ wm
  ✅ dalvik

App name: com.example.myapp  # 指定应用包名

Step 5: 开始抓取

  • 点击"Start Recording"按钮
  • 等待初始化(2-3秒)
  • 执行你要分析的操作(启动App、滑动列表等)

Step 6: 停止并下载

  • 点击"Stop and get trace"
  • 等待数据传输(取决于trace大小)
  • Trace会自动在浏览器中打开
  • 可以点击"Download"保存到本地

3.3 命令行抓取

对于自动化场景,命令行抓取更适合。

创建配置文件 config.pbtx:

protobuf 复制代码
# Perfetto配置文件格式 (Protocol Buffer Text)

buffers {
  size_kb: 32768           # 缓冲区32MB
  fill_policy: RING_BUFFER # 环形缓冲区策略
}

# 数据源配置
data_sources {
  config {
    name: "linux.ftrace"
    ftrace_config {
      # ftrace事件
      ftrace_events: "sched/sched_switch"
      ftrace_events: "sched/sched_wakeup"
      ftrace_events: "sched/sched_wakeup_new"
      ftrace_events: "sched/sched_waking"
      ftrace_events: "power/suspend_resume"
      ftrace_events: "power/cpu_frequency"
      ftrace_events: "power/cpu_idle"

      # atrace categories
      atrace_categories: "gfx"
      atrace_categories: "view"
      atrace_categories: "am"
      atrace_categories: "wm"
      atrace_categories: "input"
      atrace_categories: "dalvik"

      # 指定应用
      atrace_apps: "com.example.myapp"
    }
  }
}

# 抓取时长
duration_ms: 10000  # 10秒

抓取脚本 capture_trace.sh:

bash 复制代码
#!/bin/bash

# 配置
APP_PACKAGE="com.example.myapp"
CONFIG_FILE="config.pbtx"
OUTPUT_FILE="trace_$(date +%Y%m%d_%H%M%S).perfetto-trace"

echo "=== Perfetto Trace Capture ==="
echo "App: $APP_PACKAGE"
echo "Output: $OUTPUT_FILE"

# 1. 推送配置文件
echo "[1/4] Pushing config..."
adb push $CONFIG_FILE /data/local/tmp/config.pbtx

# 2. 清理应用数据(可选)
echo "[2/4] Clearing app data..."
adb shell pm clear $APP_PACKAGE

# 3. 开始抓取(后台运行)
echo "[3/4] Starting trace capture..."
adb shell perfetto \
  -c /data/local/tmp/config.pbtx \
  -o /data/local/tmp/trace.perfetto-trace &

PERFETTO_PID=$!

# 等待perfetto启动
sleep 2

# 4. 启动应用
echo "[4/4] Launching app..."
adb shell am start -W $APP_PACKAGE/.MainActivity

# 等待perfetto完成
wait $PERFETTO_PID

# 5. 导出trace
echo "Pulling trace file..."
adb pull /data/local/tmp/trace.perfetto-trace $OUTPUT_FILE

# 清理设备上的文件
adb shell rm /data/local/tmp/trace.perfetto-trace
adb shell rm /data/local/tmp/config.pbtx

echo "✅ Trace saved to: $OUTPUT_FILE"
echo "Open it at: https://ui.perfetto.dev/"

使用脚本:

bash 复制代码
chmod +x capture_trace.sh
./capture_trace.sh

3.4 Perfetto UI详解

整体界面布局 :

核心功能区域:

  1. Timeline (时间轴)

    • 显示整个trace的时间范围
    • 可以拖拽选择时间区间
    • 双击可以跳转到该时间点
  2. Pinned Tracks (置顶轨道)

    • Frame Timeline: 每一帧的渲染情况
      • 绿色: 正常帧 (< 16.6ms)
      • 黄色: 轻微掉帧 (16.6-33ms)
      • 红色: 严重掉帧 (> 33ms)
    • 可以添加其他重要轨道
  3. CPU轨道

    • 显示每个CPU核心的使用情况
    • 可以看到线程在哪个核心上运行
    • 颜色代表不同的进程
  4. 进程和线程轨道

    • 每个进程可以展开查看线程
    • 线程上的色块代表函数调用
    • 点击色块查看函数详情
  5. 详情面板 (右侧或底部)

    • 显示选中slice的详细信息
    • 包含Wall Duration、CPU Time、Stack Trace
    • 可以查看参数和属性

常用操作:

操作 方法
放大/缩小 鼠标滚轮 或 触控板双指缩放
平移 按住Shift+拖拽 或 触控板双指滑动
选择区域 按住左键拖拽
查看详情 点击slice(色块)
搜索 点击顶部搜索图标 或 按/
测量时间 按住Shift点击两个时间点
添加标记 M
回到全景 双击时间轴空白处

Frame Timeline分析:

Frame Timeline是Perfetto最强大的功能之一,用于分析渲染性能。

分析每一帧:

  • 绿色帧: 正常,< 16.6ms
  • 黄色帧: 轻微掉帧,16.6-33ms (掉1帧)
  • 红色帧: 严重掉帧,> 33ms (掉2帧以上)

点击一个帧,可以看到该帧的详细时间分解,快速定位瓶颈在哪个阶段。

3.5 Perfetto SQL查询入门

Perfetto最强大的功能之一就是SQL查询引擎,可以对trace数据进行复杂的分析。

打开SQL查询界面:

  1. 在Perfetto UI中打开trace文件
  2. 点击右上角的"Query (SQL)"按钮
  3. 在SQL编辑器中输入查询语句

常用表结构:

表名 说明 主要字段
slice 所有函数调用 ts, dur, name, depth, track_id
thread 线程信息 utid, tid, name, upid
process 进程信息 upid, pid, name
thread_track 线程轨道映射 id, utid
sched CPU调度信息 ts, dur, cpu, utid
counter 计数器数据 ts, value, track_id

示例查询:

sql 复制代码
-- 查询主线程的所有函数调用
SELECT
  ts / 1e9 AS start_sec,
  dur / 1e6 AS duration_ms,
  name
FROM slice
JOIN thread_track ON slice.track_id = thread_track.id
JOIN thread ON thread_track.utid = thread.utid
WHERE thread.name = 'main'
  AND dur > 10e6  -- 只看耗时超过10ms的
ORDER BY dur DESC
LIMIT 20;

-- 查询GC事件
SELECT
  ts / 1e9 AS time_sec,
  dur / 1e6 AS duration_ms,
  name AS gc_type
FROM slice
WHERE name LIKE 'Heap%' OR name LIKE 'GC%'
ORDER BY ts;

-- 查询CPU使用率
SELECT
  thread.name,
  SUM(sched.dur) / 1e9 AS cpu_time_sec
FROM sched
JOIN thread USING (utid)
WHERE thread.name LIKE '%myapp%'
GROUP BY thread.name
ORDER BY cpu_time_sec DESC;

后续章节我们会详细介绍更多高级查询技巧。


四、Trace分析实战

掌握了工具的使用,接下来就是最核心的部分:如何分析Trace文件,快速定位性能瓶颈

4.1 分析思路框架

不管分析什么性能问题,都可以遵循这个系统化的思路:

4.2 常见性能指标

在分析Trace时,需要关注以下关键指标:

1. Wall Duration (墙上时间)

  • 函数从开始到结束的实际时间
  • 包含CPU执行时间和等待时间
  • 这是用户感知到的时间

2. CPU Time (CPU时间)

  • 函数实际占用CPU的时间
  • 不包含等待、睡眠等时间
  • 如果 CPU Time << Wall Duration,说明大部分时间在等待

3. Self Time (自身时间)

  • 函数自身的执行时间,不包含调用子函数的时间
  • 用于识别真正的瓶颈函数

4. Thread State (线程状态)

  • Running ®: 正在CPU上执行
  • Runnable ®: 就绪状态,等待CPU调度
  • Sleeping (S): 睡眠,等待事件
  • Uninterruptible Sleep (D): 不可中断睡眠,通常是I/O等待
  • Stopped (T): 停止状态

帧渲染指标:

复制代码
一帧的理想时间分配 (60fps, 16.6ms):
┌────────────────────────┐
│ Input:        2-3ms    │  处理触摸事件
│ Animation:    1-2ms    │  执行动画
│ Measure:      2-3ms    │  测量View尺寸
│ Layout:       1-2ms    │  布局View位置
│ Draw:         2-3ms    │  绘制View
│ RenderThread: 4-5ms    │  渲染线程处理
│ GPU:          2-3ms    │  GPU渲染
├────────────────────────┤
│ Total:       14-18ms   │  总耗时
└────────────────────────┘

4.3 案例1: 启动性能分析

场景: 一个社交App启动需要2.8秒,需要优化到1.5秒以内。

Step 1: 抓取启动Trace

bash 复制代码
# 清理数据
adb shell pm clear com.example.social

# 抓取trace
python systrace.py -o launch.html -t 15 -a com.example.social \
  sched freq am wm gfx view dalvik input res app

# 启动应用
adb shell am start -W com.example.social/.MainActivity

Step 2: 打开Trace,定位关键时间点

在Trace中搜索关键事件:

复制代码
搜索 "activityStart"  → 找到启动开始点 (T0)
搜索 "Choreographer#doFrame" → 找到首帧绘制 (T1)

总启动时间: T1 - T0 = 2850ms

Step 3: 分析主线程时间分布

放大主线程,查看各个阶段:

复制代码
Timeline:
T0     Application.onCreate()        450ms
       ├─ SDK初始化                   280ms  ← ⚠️ 瓶颈1
       ├─ 数据库初始化                 120ms
       └─ 其他                         50ms

T+450  Activity.onCreate()           380ms
       ├─ setContentView()            80ms
       ├─ loadUserInfo() (网络请求)   200ms  ← ⚠️ 瓶颈2 (主线程!)
       └─ RecyclerView.init()         100ms

T+830  onResume()                     50ms

T+880  首帧测量布局绘制                 150ms

T+1030 RenderThread渲染               80ms

T+1110 等待网络数据                    1600ms ← ⚠️ 瓶颈3
       (主线程在sleep!)

T+2710 刷新UI                         140ms

T+2850 启动完成

Step 4: 根因分析

瓶颈1: SDK初始化 280ms

  • 点击"SDK初始化"slice,查看堆栈
  • 发现是第三方统计SDK在做网络请求
  • 优化方案: 延迟到后台初始化

瓶颈2: 主线程网络请求 200ms

  • loadUserInfo()在主线程执行同步网络请求
  • 优化方案: 改为异步请求,使用缓存数据先展示

瓶颈3: 等待网络数据 1600ms

  • 主线程在等待服务器返回
  • 优化方案: 使用本地缓存,异步刷新

Step 5: 优化后验证

优化后再次抓取trace:

复制代码
Timeline (优化后):
T0     Application.onCreate()        120ms  ← 减少330ms
       ├─ 关键初始化                   70ms
       └─ 其他                         50ms

T+120  Activity.onCreate()           180ms  ← 减少200ms
       ├─ setContentView()            80ms
       ├─ 从缓存加载用户信息            30ms
       └─ RecyclerView.init()         70ms

T+300  onResume()                     50ms

T+350  首帧测量布局绘制                 150ms

T+500  RenderThread渲染               80ms

T+580  启动完成 (显示缓存数据)
       后台异步更新数据...

总启动时间: 580ms ✅ 达成目标!

优化效果:

  • 启动时间从 2850ms → 580ms
  • 提升了 79.6%
  • 用户体验显著改善

4.4 案例2: 卡顿分析

场景: RecyclerView滑动时经常掉帧,体验很卡。

Step 1: 抓取滑动Trace

bash 复制代码
python systrace.py -o scroll.html -t 10 -a com.example.social \
  sched gfx view input wm dalvik app

抓取时在App中快速滑动列表。

Step 2: 查看Frame Timeline

在Perfetto中打开trace,查看Frames轨道:

复制代码
Frame Timeline:
Frame #100: 16ms  ✅ 绿色
Frame #101: 17ms  ⚠️ 黄色 (轻微掉帧)
Frame #102: 34ms  ❌ 红色 (掉1帧)
Frame #103: 18ms  ⚠️ 黄色
Frame #104: 52ms  ❌❌ 红色 (掉2帧)

Step 3: 分析掉帧的Frame

点击Frame #104 (52ms的那一帧),查看详情:

复制代码
Frame #104 详细分析:
┌──────────────────────────────────────┐
│ Expected: 16.6ms                     │
│ Actual: 52ms (掉3帧!)                │
├──────────────────────────────────────┤
│ Input:             2ms   ██          │
│ Animation:         1ms   █           │
│ Measure:           3ms   ███         │
│ Layout:            2ms   ██          │
│ Draw:              4ms   ████        │
│ RecyclerView.      35ms  ███████████ │ ← ⚠️ 瓶颈!
│   onBindViewHolder                   │
│ RenderThread:      5ms   █████       │
└──────────────────────────────────────┘

Step 4: 深入分析onBindViewHolder

点击"onBindViewHolder"slice,查看堆栈:

java 复制代码
onBindViewHolder (35ms)
  ├─ loadImage() (20ms)           ← ⚠️ 主线程加载图片
  │   ├─ decodeFile() (18ms)      ← 解码耗时
  │   └─ 其他 (2ms)
  ├─ setText() (8ms)
  │   └─ measureText() (7ms)      ← 复杂文本测量
  └─ 其他 (7ms)

Step 5: 根因与优化

问题1: 主线程加载图片 20ms

java 复制代码
// 【优化前】- 主线程同步加载
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    val bitmap = BitmapFactory.decodeFile(imagePath)  // ⚠️ 主线程解码!
    holder.imageView.setImageBitmap(bitmap)
}

// 【优化后】- 使用Glide异步加载
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    Glide.with(context)
        .load(imagePath)
        .placeholder(R.drawable.placeholder)
        .into(holder.imageView)
}

问题2: 复杂文本测量 7ms

java 复制代码
// 【优化前】- 每次都测量
textView.text = SpannableString(text).apply {
    setSpan(ForegroundColorSpan(color), 0, length, 0)
    setSpan(AbsoluteSizeSpan(20), 0, length, 0)
    // 多个span导致复杂测量
}

// 【优化后】- 缓存测量结果
private val spannableCache = LruCache<String, SpannableString>(100)

fun getSpannable(text: String): SpannableString {
    return spannableCache.get(text) ?: createSpannable(text).also {
        spannableCache.put(text, it)
    }
}

优化后验证:

复制代码
Frame Timeline (优化后):
Frame #100: 15ms  ✅ 绿色
Frame #101: 16ms  ✅ 绿色
Frame #102: 14ms  ✅ 绿色
Frame #103: 17ms  ⚠️ 黄色 (偶尔)
Frame #104: 16ms  ✅ 绿色

平均帧时间: 16ms
掉帧率: < 5%  ✅ 达成目标!

4.5 案例3: CPU占用分析

场景: App在后台CPU占用高达30%,导致耗电快。

Step 1: 抓取后台运行Trace

bash 复制代码
# 使用Perfetto抓取,包含CPU和电量数据
# 在ui.perfetto.dev配置:
# - Duration: 30s
# - Probes: Scheduling, CPU frequency, Battery

Step 2: 使用SQL查询分析CPU占用

在Perfetto SQL界面输入:

sql 复制代码
-- 查询各线程的CPU占用时间
SELECT
  thread.name AS thread_name,
  process.name AS process_name,
  SUM(sched.dur) / 1e9 AS cpu_time_seconds,
  COUNT(*) AS schedule_count
FROM sched
JOIN thread USING (utid)
JOIN process USING (upid)
WHERE process.name = 'com.example.social'
GROUP BY thread.utid
ORDER BY cpu_time_seconds DESC
LIMIT 20;

查询结果:

复制代码
thread_name              cpu_time_seconds  schedule_count
─────────────────────────────────────────────────────────
BackgroundWorker         8.2               1245          ← ⚠️ 异常!
main                     2.1               856
RenderThread             0.8               234
binder:12345_1           0.5               123
...

Step 3: 分析BackgroundWorker线程

搜索"BackgroundWorker",放大查看:

复制代码
BackgroundWorker Timeline:
00:00 - 00:02  运行 (处理数据)     2s
00:02 - 00:02  睡眠               0.1s
00:02 - 00:04  运行 (处理数据)     2s
00:04 - 00:04  睡眠               0.1s
00:04 - 00:06  运行 (处理数据)     2s
...
(每隔0.1秒唤醒一次,处理2秒!)

Step 4: 查看源码定位问题

查看堆栈,找到对应代码:

kotlin 复制代码
// 【问题代码】
class BackgroundWorker : Thread() {
    override fun run() {
        while (true) {
            processData()  // 耗时操作
            Thread.sleep(100)  // 只睡眠0.1秒!
        }
    }

    private fun processData() {
        // 处理大量数据,耗时2秒
        val data = loadDataFromDB()
        analyzeData(data)
        saveResult(data)
    }
}

问题根因:

  • 后台线程每0.1秒唤醒一次
  • 每次执行耗时2秒的数据处理
  • 实际上这个任务根本不需要这么频繁

Step 5: 优化方案

kotlin 复制代码
// 【优化后】
class BackgroundWorker : Thread() {
    override fun run() {
        while (true) {
            processData()
            Thread.sleep(300_000)  // 改为5分钟执行一次
        }
    }
}

// 【更好的方案】使用WorkManager
val workRequest = PeriodicWorkRequestBuilder<DataProcessor>(
    15, TimeUnit.MINUTES  // 每15分钟执行一次
).build()

WorkManager.getInstance(context).enqueue(workRequest)

优化效果:

复制代码
优化前:
- 后台CPU占用: 30%
- 30秒内CPU时间: 8.2秒
- 电量消耗: 高

优化后:
- 后台CPU占用: < 1%
- 30秒内CPU时间: 0.1秒
- 电量消耗: 正常

五、代码插桩:自定义Trace事件

有时候系统默认的trace信息不够详细,我们需要在自己的代码中添加trace标记,这就是代码插桩(Instrumentation)。

5.1 Java/Kotlin代码插桩

使用android.os.Trace API添加trace标记:

基础用法:

kotlin 复制代码
import android.os.Trace

class MyActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        Trace.beginSection("MyActivity.onCreate")
        super.onCreate(savedInstanceState)

        Trace.beginSection("InitData")
        initData()
        Trace.endSection()

        Trace.beginSection("SetupUI")
        setupUI()
        Trace.endSection()

        Trace.endSection() // MyActivity.onCreate
    }

    private fun initData() {
        Trace.beginSection("LoadConfig")
        loadConfig()
        Trace.endSection()

        Trace.beginSection("ConnectDB")
        connectDatabase()
        Trace.endSection()
    }
}

注意事项:

  • ⚠️ beginSection()endSection()必须成对出现
  • ⚠️ 必须在同一个线程中调用
  • ⚠️ Section名称最长127个字符
  • ⚠️ 嵌套深度有限制(通常不超过20层)

使用扩展函数简化:

kotlin 复制代码
// 定义扩展函数
inline fun <T> trace(sectionName: String, block: () -> T): T {
    Trace.beginSection(sectionName)
    try {
        return block()
    } finally {
        Trace.endSection()
    }
}

// 使用
override fun onCreate(savedInstanceState: Bundle?) {
    trace("MyActivity.onCreate") {
        super.onCreate(savedInstanceState)

        trace("InitData") {
            initData()
        }

        trace("SetupUI") {
            setupUI()
        }
    }
}

5.2 Native代码插桩

对于C/C++代码,使用<cutils/trace.h>头文件:

cpp 复制代码
#include <cutils/trace.h>

class MyNativeClass {
public:
    void expensiveOperation() {
        ATRACE_CALL();  // 自动跟踪整个函数

        ATRACE_BEGIN("LoadData");
        loadData();
        ATRACE_END();

        ATRACE_BEGIN("ProcessData");
        processData();
        ATRACE_END();

        ATRACE_BEGIN("SaveResult");
        saveResult();
        ATRACE_END();
    }

private:
    void loadData() {
        ATRACE_CALL();  // 跟踪loadData函数
        // ... 加载数据
    }

    void processData() {
        ATRACE_CALL();
        // ... 处理数据
    }
};

Android.mk配置:

makefile 复制代码
LOCAL_SHARED_LIBRARIES := libcutils

CMake配置:

cmake 复制代码
target_link_libraries(mynative cutils)

5.3 异步事件跟踪

对于异步操作(如网络请求、图片加载),使用异步trace:

kotlin 复制代码
class ImageLoader {
    private var cookie = 0

    fun loadImageAsync(url: String, callback: (Bitmap) -> Unit) {
        val currentCookie = cookie++
        Trace.beginAsyncSection("LoadImage-$url", currentCookie)

        thread {
            try {
                val bitmap = downloadAndDecode(url)

                runOnUiThread {
                    Trace.endAsyncSection("LoadImage-$url", currentCookie)
                    callback(bitmap)
                }
            } catch (e: Exception) {
                Trace.endAsyncSection("LoadImage-$url", currentCookie)
                Log.e(TAG, "Failed to load image", e)
            }
        }
    }
}

在Trace中的展示:

复制代码
Timeline:
T0    beginAsyncSection("LoadImage", 123)
      |
      |  (主线程继续执行其他操作)
      |
T+200 endAsyncSection("LoadImage", 123)

在Trace中会显示一条横跨200ms的异步slice

5.4 Counter跟踪

跟踪数值变化(如内存使用、队列长度):

kotlin 复制代码
class TaskQueue {
    private val queue = ConcurrentLinkedQueue<Task>()

    fun enqueue(task: Task) {
        queue.add(task)
        Trace.setCounter("TaskQueue.size", queue.size.toLong())
    }

    fun dequeue(): Task? {
        val task = queue.poll()
        Trace.setCounter("TaskQueue.size", queue.size.toLong())
        return task
    }
}

在Trace中会显示一条折线图,实时展示队列大小的变化。

5.5 最佳实践

1. 合理命名:

kotlin 复制代码
// ❌ 不好的命名
Trace.beginSection("func1")

// ✅ 好的命名
Trace.beginSection("UserRepository.loadProfile")

2. 控制粒度:

kotlin 复制代码
// ❌ 太细粒度 (每个小函数都trace)
fun processData(data: List<Item>) {
    Trace.beginSection("processData")
    for (item in data) {
        Trace.beginSection("processItem")  // 不必要
        process(item)
        Trace.endSection()
    }
    Trace.endSection()
}

// ✅ 合适的粒度
fun processData(data: List<Item>) {
    Trace.beginSection("processData")
    for (item in data) {
        process(item)  // 不需要每个item都trace
    }
    Trace.endSection()
}

3. Release版本优化:

kotlin 复制代码
// 在release版本中禁用trace以减少开销
inline fun traceDebug(sectionName: String, block: () -> Unit) {
    if (BuildConfig.DEBUG) {
        Trace.beginSection(sectionName)
        try {
            block()
        } finally {
            Trace.endSection()
        }
    } else {
        block()
    }
}

4. 关键路径优先 :

优先在以下场景添加trace:

  • ✅ 启动流程的关键步骤
  • ✅ 网络请求的各个阶段
  • ✅ 数据库操作
  • ✅ 图片加载和解码
  • ✅ 复杂计算
  • ✅ RecyclerView的bind操作

六、进阶技巧

6.1 Perfetto高级SQL查询

查询所有GC事件及其耗时:

sql 复制代码
SELECT
  ts / 1e9 AS time_sec,
  dur / 1e6 AS duration_ms,
  name AS gc_type,
  CASE
    WHEN dur < 5e6 THEN 'Minor'
    WHEN dur < 50e6 THEN 'Major'
    ELSE 'Critical'
  END AS severity
FROM slice
WHERE name GLOB 'Heap*' OR name GLOB 'GC*'
ORDER BY dur DESC;

查询主线程耗时最长的20个方法:

sql 复制代码
SELECT
  name,
  dur / 1e6 AS duration_ms,
  ts / 1e9 AS start_time_sec
FROM slice
JOIN thread_track ON slice.track_id = thread_track.id
JOIN thread ON thread_track.utid = thread.utid
WHERE
  thread.name = 'main'
  AND depth = 0  -- 只看顶层调用
  AND dur > 10e6  -- 超过10ms
ORDER BY dur DESC
LIMIT 20;

查询Binder调用统计:

sql 复制代码
SELECT
  thread.name AS caller_thread,
  slice.name AS binder_method,
  COUNT(*) AS call_count,
  SUM(dur) / 1e6 AS total_ms,
  AVG(dur) / 1e6 AS avg_ms,
  MAX(dur) / 1e6 AS max_ms
FROM slice
JOIN thread_track ON slice.track_id = thread_track.id
JOIN thread ON thread_track.utid = thread.utid
WHERE slice.name LIKE 'binder%'
GROUP BY thread.name, slice.name
ORDER BY total_ms DESC;

分析帧耗时分布:

sql 复制代码
SELECT
  CASE
    WHEN dur < 16.6e6 THEN '正常 (<16.6ms)'
    WHEN dur < 33e6 THEN '轻微掉帧 (16.6-33ms)'
    WHEN dur < 50e6 THEN '中度掉帧 (33-50ms)'
    ELSE '严重掉帧 (>50ms)'
  END AS frame_category,
  COUNT(*) AS frame_count,
  ROUND(100.0 * COUNT(*) / (SELECT COUNT(*) FROM actual_frame_timeline_slice), 2) AS percentage
FROM actual_frame_timeline_slice
GROUP BY frame_category
ORDER BY frame_count DESC;

6.2 自动化分析脚本

使用Python和Perfetto Trace Processor进行自动化分析:

python 复制代码
#!/usr/bin/env python3
"""
启动时间自动化分析脚本
"""
from perfetto.trace_processor import TraceProcessor
import sys

def analyze_startup(trace_file):
    tp = TraceProcessor(trace=trace_file)

    # 查询Application.onCreate的耗时
    query = '''
    SELECT
        ts / 1e9 AS start_sec,
        dur / 1e6 AS duration_ms
    FROM slice
    WHERE name = 'Application.onCreate'
    '''

    result = tp.query(query)
    if len(result) > 0:
        app_create_time = result.duration_ms[0]
        print(f"Application.onCreate: {app_create_time:.2f}ms")
    else:
        print("未找到Application.onCreate事件")
        return

    # 查询Activity.onCreate的耗时
    query = '''
    SELECT
        ts / 1e9 AS start_sec,
        dur / 1e6 AS duration_ms
    FROM slice
    WHERE name LIKE '%Activity.onCreate%'
    '''

    result = tp.query(query)
    if len(result) > 0:
        activity_create_time = result.duration_ms[0]
        print(f"Activity.onCreate: {activity_create_time:.2f}ms")
    else:
        print("未找到Activity.onCreate事件")
        return

    # 查询首帧渲染时间
    query = '''
    SELECT MIN(ts) / 1e9 AS first_frame_sec
    FROM actual_frame_timeline_slice
    '''

    result = tp.query(query)
    first_frame_time = result.first_frame_sec[0]

    # 计算总启动时间
    query = '''
    SELECT MIN(ts) / 1e9 AS app_start_sec
    FROM slice
    WHERE name = 'Application.onCreate'
    '''

    result = tp.query(query)
    app_start_time = result.app_start_sec[0]

    total_startup = (first_frame_time - app_start_time) * 1000
    print(f"总启动时间: {total_startup:.2f}ms")

    # 性能评级
    if total_startup < 1000:
        print("✅ 性能评级: 优秀")
    elif total_startup < 2000:
        print("⚠️ 性能评级: 良好")
    elif total_startup < 3000:
        print("⚠️ 性能评级: 一般")
    else:
        print("❌ 性能评级: 需要优化")

if __name__ == '__main__':
    if len(sys.argv) < 2:
        print("用法: python analyze_startup.py trace.perfetto-trace")
        sys.exit(1)

    analyze_startup(sys.argv[1])

使用方法:

bash 复制代码
python analyze_startup.py launch.perfetto-trace

# 输出:
# Application.onCreate: 450.23ms
# Activity.onCreate: 380.45ms
# 总启动时间: 1245.67ms
# ⚠️ 性能评级: 良好

七、常见问题与解决

7.1 Trace抓取失败

问题: 执行systrace/perfetto命令后无输出或报错

可能原因与解决:

  1. 设备未授权
bash 复制代码
# 检查设备连接
adb devices
# 如果显示 "unauthorized",重新授权
adb kill-server
adb start-server
  1. 权限不足
bash 复制代码
# 需要root权限
adb root
adb remount
  1. buffer大小不足
bash 复制代码
# 增大buffer
python systrace.py -b 65536 ...  # 64MB
  1. category不支持
bash 复制代码
# 查看设备支持的category
adb shell atrace --list_categories

7.2 Trace文件过大

问题: Trace文件几百MB,Chrome打不开或很卡

解决方案:

  1. 减少抓取时长
bash 复制代码
# 从10秒改为5秒
-t 5
  1. 只选择必要的category
bash 复制代码
# 不要用 --all,精确指定
python systrace.py ... sched gfx view am
  1. 使用Perfetto代替Systrace

    Perfetto的protobuf格式文件更小,加载更快

  2. 使用Perfetto的在线查看

    不要下载文件,直接在 ui.perfetto.dev 上分析

7.3 找不到关键事件

问题: 在Trace中搜索不到我的函数调用

可能原因:

  1. 没有启用app category
bash 复制代码
# 确保包含 app category
python systrace.py ... app
  1. 代码没有插桩
kotlin 复制代码
// 需要添加 Trace.beginSection()
Trace.beginSection("MyFunction")
myFunction()
Trace.endSection()
  1. 函数执行时间太短
  • 小于0.1ms的函数可能不会显示
  • 放大时间轴查看
  1. 没有指定应用包名
bash 复制代码
# 指定包名以抓取应用的trace
python systrace.py -a com.example.myapp ...

7.4 Trace看不懂

问题: Trace文件打开了,但不知道怎么分析

建议:

  1. 从宏观到微观

    • 先缩小查看整体
    • 识别大块耗时
    • 再放大查看细节
  2. 关注主线程

    • 主线程的空白区域可能在等待
    • 查看主线程在做什么
  3. 对比VSYNC信号

    • 看渲染是否跟上VSYNC节奏
    • 识别掉帧原因
  4. 搜索关键事件

    • activityStart, onCreate, draw, measure
    • 使用搜索功能快速定位
  5. 查看CPU调度

    • 线程是在运行还是等待
    • CPU是否被其他进程占用

八、工具对比与选择建议

8.1 何时使用Systrace

推荐场景:

  • ✅ Android 9及以下系统
  • ✅ 简单的性能分析(启动、卡顿)
  • ✅ 不需要复杂查询
  • ✅ 团队成员对Chrome Trace Viewer熟悉

优势:

  • 简单易用,一行命令搞定
  • 兼容老版本Android
  • HTML文件可以离线查看

8.2 何时使用Perfetto

推荐场景:

  • Android 10及以上系统 (强烈推荐)
  • ✅ 需要SQL查询进行深度分析
  • ✅ 需要内存、电量等额外数据
  • ✅ 需要自动化分析(CI/CD)
  • ✅ 大型trace文件

优势:

  • 现代化UI,体验极佳
  • SQL查询引擎,分析能力强
  • 文件更小,加载更快
  • 持续更新,功能丰富

8.3 选择建议

场景 推荐工具 理由
新项目 Perfetto 功能强大,体验好
老项目(Android 9-) Systrace 兼容性
启动优化 Perfetto SQL查询方便统计
卡顿分析 Perfetto Frame Timeline强大
快速排查 Systrace 简单快速
深度分析 Perfetto SQL查询 + 多数据源
CI集成 Perfetto Trace Processor API
学习入门 Systrace 简单易懂

总结: 如果你的项目主要支持Android 10+,强烈推荐从现在开始使用Perfetto!


九、总结与展望

9.1 核心要点回顾

通过本文的学习,我们全面掌握了Systrace和Perfetto两大性能分析工具:

工具使用:

  • ✅ 理解了Trace的工作原理和应用场景
  • ✅ 掌握了Systrace和Perfetto的抓取方法
  • ✅ 学会了Perfetto UI的各项功能
  • ✅ 了解了两种工具的对比和选择

分析方法:

  • ✅ 建立了系统化的Trace分析思路
  • ✅ 学会了定位启动慢、卡顿、CPU占用等问题
  • ✅ 掌握了Frame Timeline分析技巧
  • ✅ 理解了关键性能指标的含义

实战技能:

  • ✅ 掌握了Java/Kotlin/Native代码插桩
  • ✅ 学会了Perfetto SQL高级查询
  • ✅ 了解了自动化分析和CI集成
  • ✅ 能够独立完成性能问题的排查

9.2 最佳实践总结

DO (推荐做法):

  • ✅ 优先使用Perfetto (Android 10+)
  • ✅ 在关键路径添加trace标记
  • ✅ 定期进行性能回归测试
  • ✅ 建立性能基线和阈值
  • ✅ 使用SQL查询进行深度分析
  • ✅ 保存重要的trace文件以便对比
  • ✅ 团队内分享trace分析经验

DON'T (避免做法):

  • ❌ 不要凭感觉优化,要用数据说话
  • ❌ 不要只优化局部,要看整体
  • ❌ 不要在release版本保留过多trace
  • ❌ 不要忽略小的性能问题
  • ❌ 不要等用户反馈才开始优化

9.3 进一步学习资源

官方文档:

工具:

社区:

  • Android Performance Patterns (YouTube)
  • Android Developers Blog
  • Stack Overflow (标签: android-performance)

相关文章:


记住:性能优化不是一次性的工作,而是一个持续的过程。定期使用Trace工具检查性能,及时发现和解决问题,才能保证应用始终保持最佳状态。

当你下次遇到性能问题时,不要慌张,打开Perfetto,抓个trace,答案就在那里! 📊✨


作者简介: 多年Android系统开发经验,专注于系统稳定性与性能优化领域。欢迎关注本系列,一起深入Android系统的精彩世界!


🎉 感谢关注,让我们一起深入Android系统的精彩世界!

找到我 : 个人主页

相关推荐
QING6187 小时前
简单说下Kotlin 作用域函数中 apply 和 also 为什么不能空安全调用?
android·kotlin·android jetpack
城东米粉儿8 小时前
着色器 (Shader) 的基本概念和 GLSL 语法 笔记
android
儿歌八万首10 小时前
Jetpack Compose :封装 MVVM 框架
android·kotlin·compose
2501_9159214310 小时前
iOS App 中 SSL Pinning 场景下代理抓包失效的原因
android·网络协议·ios·小程序·uni-app·iphone·ssl
壮哥_icon10 小时前
Android 系统级 USB 存储检测的工程化实现(抗 ROM、抗广播丢失)
android·android-studio·android系统
Junerver10 小时前
积极拥抱AI,ComposeHooks让你更方便地使用AI
android·前端
城东米粉儿10 小时前
ColorMatrix色彩变换 笔记
android
方白羽10 小时前
告别onActivityResult:Android数据回传的三大痛点与终极方案
android·app·客户端
oMcLin10 小时前
如何在 RHEL 8 系统上实现高可用 MySQL 集群,保障电商平台的 24 小时稳定运行
android·mysql·adb