news 2026/9/30 8:08:47

Android SystemUI架构解析:从启动链路到模块拆分与定制实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Android SystemUI架构解析:从启动链路到模块拆分与定制实践

做Android系统方向的同学,对SystemUI这个词一定不陌生。状态栏、通知中心、快捷开关、导航栏、锁屏界面、音量条,几乎你每天都会碰到的系统UI交互,背后全是这个进程在干活。但它到底是什么?它是系统服务还是普通应用?它又是怎么被Android Framework拉起来的?网上讲SystemUI的文章不少,但大多只讲某个点,很少能把"从开机启动到架构分层再到定制修改"这条主线串清楚。这篇我打算以Android 12的源码为大背景,把我对SystemUI的理解、启动链路、架构拆分、关键模块和实际定制过程中踩过的坑完整梳理一遍,希望能帮到正在啃Framework源码、准备系统应用面试、或者被厂商ROM定制折磨的Android开发者。

这更像是一个从"现象到源码"的复盘,不是官方文档翻译。文章里所有类和调用链都以AOSP 12为基准,但思路在后续版本依然通用。

1. SystemUI到底是什么:Framework层最容易被误解的"应用"

先纠正一个最普遍的误解:SystemUI不是一个系统服务,它是一个真正意义上的Android应用,有自己的进程、自己的Manifest、自己的Application类、自己的Service,甚至有自己的资源目录和布局文件。但说它是"普通应用"又完全不对——它运行在独立的com.android.systemui进程里,这个进程声明了sharedUserId="android.uid.system",持有system权限,能够访问大量系统级的Binder接口和内部API,比如StatusBarManagerService、NotificationManagerService、WindowManagerService等等。本质上,这是系统借着"应用外壳"在运行的一套核心UI服务。

为什么Google要把系统UI做成一个应用而不是直接塞进SystemServer进程?这个问题我在读源码之前从没想过,想明白之后才觉得这个设计是很有考量的。SystemServer进程承担了AMS、WMS、PMS这些核心服务的生命周期管理,职责已经非常重,如果再把状态栏的布局、动画、交互逻辑全部塞进去,哪怕一次UI卡顿都可能拖累整个系统服务,风险太大。把SystemUI独立成进程,就相当于让UI渲染和系统核心服务之间多了一层隔离,SystemUI崩了,系统核心服务还能稳住,系统会自动把它拉起恢复,代价只是用户看到状态栏短暂消失或重建。

另外,SystemUI作为独立进程,也让"以应用方式更新系统UI"变成了可能。在Android早期版本里,SystemUI确实可以通过应用商店或者系统OTA进行整体更新,后来出于安全考虑收紧了这类操作,但进程边界的设计保留下来了。

从源码结构上,你可以在frameworks/base/packages/SystemUI目录下看到它。这个目录下不是一个小项目,而是一个包含了状态栏、通知面板、快捷设置、导航栏、手势、锁屏、音量、录屏、截屏等十余个子模块的复合工程。Module的名字也很直白:StatusBar、NotificationShade、QuickSettings、NavigationBar、Keyguard、Volume等等。

// SystemUI的Manifest中注册的核心服务,注意这里只是注册,真正的启动入口在后面讲 <service android:name="com.android.systemui.SystemUIService" android:exported="true" android:process="com.android.systemui" > <intent-filter> <action android:name="com.android.systemui.SystemUIService" /> </intent-filter> </service>

注意:虽然Manifest里写了android:exported="true",但SystemUIService的实际内核并不在这里。SystemUIService只是一个非常薄的壳,真正的模块初始化发生在Application阶段。这个"壳Service + 重Application"的结构,是很多系统应用惯用的启动方式。

一句话总结SystemUI的定位:它是连接Android Framework底层服务和用户手指之间的那层"界面翻译官"。用户看到的是状态栏、通知、开关和导航按钮,背后其实是这个进程在不断把NotificationManagerService的通知、StatusBarManagerService的命令、WindowManagerService的窗口状态翻译成用户能看懂、能操作的UI反馈。

2. 从SystemServer到SystemUI:开机三十秒内的启动链路拆解

理解了SystemUI是什么,自然会问它是怎么启动的。很多人会想当然地认为SystemUI是由ActivityManagerService启动的,因为普通应用不都是AMS拉起来的吗?这里恰恰不是。SystemUI的启动入口藏在WindowManagerService里。

看AOSP 12的SystemServer.java,你会发现在startOtherServices()执行到系统就绪阶段时,会调用windowManagerF.startSystemUi()。这个startSystemUi()的名字非常直白,就是专门给SystemUI准备的。

// SystemServer.java 中,系统启动进入systemReady后会调用 try { Slog.i(TAG, "Starting SystemUI"); windowManagerF.startSystemUi(); } catch (Throwable e) { reportWtf("starting System UI", e); }

而WindowManagerService.startSystemUi()内部做的事情,实际上就是通过Context.startServiceAsUser()发出一个显式Intent去拉起SystemUI进程里的SystemUIService。

// WindowManagerService.java public void startSystemUi() { synchronized (mGlobalLock) { if (mSystemUiStarted) return; mSystemUiStarted = true; } mContext.startServiceAsUser( new Intent("com.android.systemui.SystemUIService"), UserHandle.SYSTEM); }

为什么是WMS来通知启动SystemUI,而不是AMS或PMS?我当时的理解是:SystemUI的核心职责之一是"顶在窗口最上层",它的窗口类型是系统级窗口,比如状态栏用的TYPE_STATUS_BAR、导航栏用的TYPE_NAVIGATION_BAR。窗口的添加、层级管理、布局计算全部由WMS负责,只有在WMS已经完成初始化和系统就绪回调之后,SystemUI才能拿到合法的窗口管理能力。所以把SystemUI的启动时机放在WMS的systemReady里,而不是AMS的systemReady里,是很合理的一种依赖控制。

SystemUIService这个Service本身非常轻量。它的onCreate()里会拿到应用级别的SystemUIApplication,然后调用startServicesIfNeeded(),把所有注册好的系统UI模块一个个初始化起来。这里有几个模块是启动时就必须创建的核心服务,比如StatusBar、NotificationShadeWindowController、QuickSettingsController。每个模块在初始化阶段会向WindowManager申请对应的系统窗口、注册Binder回调、建立和SystemServer之间各种ManagerService的连接。

启动顺序上还有个细节值得注意:SystemUIApplication是在进程首次创建Application时执行的。也就是说,startSystemUi()这个Intent发出去之后,Zygote fork出SystemUI进程,走到Application.onCreate(),然后才走SystemUIService.onCreate()。Application阶段初始化全局依赖,Service阶段再启动具体模块,这是一种典型的"先环境后业务"的分层启动结构。

从Android 14开始,SystemUI的启动框架又做了演进,引入了CoreStartable这种更清晰的模块生命周期管理,每个模块实现start()即可。但核心思路没变:SystemServer到WMS到Intent到Service,这条主链路始终是稳定的。

# 可以用这个命令快速验证SystemUI是否活着 adb shell ps -A | grep systemui

如果你看到输出里有u0a_?或者system权限的用户,但进程名是com.android.systemui,说明SystemUI已经跑起来了。然后你可以用dumpsys window windows看系统窗口的层级,能看到StatusBar、NavigationBar这些窗口名,基本就能确定SystemUI的窗口是否已经挂到WMS上了。

3. 架构分层:SystemUI凭什么能同时管理状态栏、通知和导航

SystemUI内部不是一坨堆在StatusBar类里的代码,它采用的是多模块并行、各自持有窗口和Controller的架构。这一点当年我读代码时花了很久才理清,因为它和普通App的"Activity作为模块入口"完全不同,SystemUI没有统一的Activity界面,所有模块都是以"窗口 + View + Controller"三元组的形式并列存在。

在Android 12上,核心模块通常包含以下三层结构:

  • Window层:每个模块通过WindowManager申请一块系统窗口,状态栏是StatusBarWindowView,通知面板是NotificationShadeWindowView,导航栏是NavigationBarWindowView。这些窗口使用系统级WindowType,层级高于普通应用窗口。
  • View层:每个窗口填充对应的根布局,比如status_bar.xml、quick_settings_panel.xml、navigation_bar.xml。View层负责静态布局和直接的用户交互。
  • Controller层:每个模块有一个或多个Controller,负责数据状态、动画、和系统服务的Binder通信。比如StatusBarStateController管理状态栏的展开/收起状态,NotificationPanelViewController管理下拉面板的手势响应。

模块之间怎么通信?SystemUI里有一个非常重要的机制叫CommandQueue。它的作用是承接来自系统服务端的命令,再分发到对应的UI模块。

举个例子:当一个通知被NotificationManagerService取消时,系统并不是直接调SystemUI里的某个View方法,而是通过IStatusBarService这个Binder接口,向SystemUI端的CommandQueue下发命令。CommandQueue内部做了消息队列和Handler封装,把命令排队后投递到SystemUI主线程,最后再调用StatusBar、NotificationShade等模块注册的CommandCallbacks。

// CommandQueue 中典型的命令分发结构,这是SystemUI里很核心的一个类 public final class CommandQueue { private final ArrayList<Callbacks> mCallbacks = new ArrayList<>(); public void addCallback(Callbacks callbacks) { ... } public void removeCallback(Callbacks callbacks) { ... } // 例如状态栏可见性变化 public void setStatusBarVisibility(int displayId, boolean visibility) { synchronized (mLock) { for (Callbacks cb : mCallbacks) { cb.setStatusBarVisibility(displayId, visibility); } } } }

这种设计的好处是:系统端不用关心SystemUI内部的UI实现细节,SystemUI内部也不用直接依赖系统端的具体实现。两边通过一份接口约定互相通信,和服务器与客户端之间通过REST API通信是很像的。

依赖注入在SystemUI里也占了很大比重。Android 12的SystemUI源码里大量使用Dependency类和相关工厂来管理全局单例,比如NotificationShadeWindowController、StatusBarIconController这些跨模块共享的对象,基本都是通过注入方式提供的。你在定制的时候如果要拿某个Controller,最好也走这套注入体系,而不是自己再搞一个全局静态变量,否则很容易出现多个实例互相打架的问题。

模块与模块之间的协作也很有意思。比如通知面板的展开动画,它会同时影响状态栏的时间显示位置、快捷开关的布局偏移、导航栏的透明度。这类跨模块联动不是靠模块之间直接互相调方法,而是通过统一的StatusBarStateController状态机来驱动。状态机里有SHADE、KEYGUARD等状态,动画过程通过监听回调把状态变化广播给所有关心的模块,每个模块自己决定如何响应。

架构上理解了这层"窗口独立 + 命令队列 + 状态机广播 + 依赖注入"的骨架,你再去看SystemUI源码,就不会觉得它是一团乱麻了。它的核心思想其实是:保持模块松耦合,界面呈现和系统通信分离,状态变化统一管理。

4. 核心模块逐个解剖:通知、快捷开关、状态栏和导航栏的协作方式

这一节挑四个最常被改、也最常被问的模块拆一下。这四个模块基本覆盖了SystemUI 90%的日常存在感。

4.1 状态栏(StatusBar)

状态栏是SystemUI里最"显眼"的模块,但它做的事情远不止显示时间、电池和信号。StatusBar类在整个SystemUI里几乎是中枢级别的存在,很多跨模块的交互都会汇聚到它身上。

状态栏有两个关键的子控制器:

  • StatusBarIconController:负责管理状态栏左侧的通知图标和右侧的系统状态图标(wifi、信号、电池等)。它内部有一个IconManager,会监听StatusBarManagerService的图标更新请求,然后动态替换状态栏上的StatusBarIconView。
  • LightBarController:负责状态栏前景色(字体和图标颜色)在深色、浅色模式之间切换。它根据当前状态栏背景亮度、导航栏模式等条件计算是否要切到浅色图标。
// StatusBar 里加载根视图的方式,flutter/dart 读者对照思路看,就是setContentView mStatusBarWindowView = inflateView(R.layout.status_bar); mStatusBarWindowController.addView(mStatusBarWindowView);

状态栏窗口的WindowType是TYPE_STATUS_BAR,层级非常高。这也是为什么你在普通App里设置FLAG_FULLSCREEN隐藏状态栏时,实际上只是系统把状态栏窗口从可见变成不可见,而进程本身始终都在。

4.2 通知面板与通知管理

通知面板是SystemUI里体量最大的模块之一。Android 5.0之后,通知的实现方式是SystemUI通过NotificationListenerService向系统注册监听,所有通知的增删改都会通过onNotificationPosted()、onNotificationRemoved()等回调推送到SystemUI内部。

关键类大概是这几个:

  • NotificationListener:继承自NotificationListenerService,是SystemUI和NotificationManagerService之间的桥梁。
  • NotificationShadeWindowController:管理通知面板窗口的整体显隐和布局。
  • NotificationPanelViewController:管理下拉面板的具体View和手势逻辑,包括折叠、展开、滑动手势的消费。

通知在SystemUI里的展示分两个层次:一条新通知进来,先以HeadsUp浮动通知的形式从状态栏下方弹出,如果用户不处理,它再折叠进通知列表。这个"轻提醒 -> 聚合列表"的层级设计,在Android 12上被保留且强化了,Android 12的颗粒度通知和通知中心改版就是在这个架构上迭代的。

4.3 快捷设置(QuickSettings)

快捷设置面板就是下拉两次或者从状态栏右侧下拉能看到的那一竖排开关。Android 12上对它做了大改成"控件 + 快捷开关"二合一形态,但从源码架构看,还是围绕Tile这个概念展开。

快捷设置的核心入口是QuickSettingsController,内部会向QSTileHost请求当前应该显示哪些Tile。每个Tile对应一个单独的开关或控件,例如WiFi、蓝牙、飞行模式、手电筒等。这些内置Tile在源码里分布在frameworks/base/packages/SystemUI/src/com/android/systemui/qs/tiles/目录下。

QSTileImpl是所有内置Tile的基类,它内部定义了handleClick()、handleUpdateState()、handleLongClick()等生命周期方法。外部应用或者系统组件可以注册自己的自定义Tile,但和内置Tile不同,外部Tile走的是TileService的Binder机制,需要在自己的Manifest里声明服务并绑定android.service.quicksettings.action.QS_TILE权限。

// 内置Tile的基类方法长这样,理解这个抽象后写自定义Tile就不难了 public abstract class QSTileImpl<TState extends State> implements QSTile, Lifecycle, Tunable { public abstract void handleClick(); public abstract void handleUpdateState(TState state, Object arg); public abstract void handleSetListening(boolean listening); }

这块也是厂商定制最集中的区域。加一个快捷开关,调整默认显示的Tile顺序,隐藏某个系统开关,都是通过改config.xml里的数组和新增Tile类完成的,后面第5节我会完整讲一次踩坑过程。

4.4 导航栏与手势

导航栏分两种形态:三键导航和手势导航。Android 12上默认引导用户用手势导航,但三键导航仍保留在市场上大量设备上。

三键导航的核心View是NavigationBarView,它的按钮事件最终会通过NavigationBarController转换成系统层的动作,比如按Home键实际上会向ActivityManagerService发送一个ACTION_HOME的Binder调用。手势导航则复杂得多,需要处理边缘滑动、动画插值、和App窗口系统手势冲突的判定,相关逻辑主要在EdgeBackGestureHandler和NavigationBarEdgePanel里。

导航栏和状态栏之间也有协同。比如弹出输入法时,手势导航模式的导航栏会显示一个输入法切换按钮;进入全屏应用时,导航栏会配合状态栏一起进行自动隐藏和重新显示。这些状态变化都通过CommandQueue下发到两个模块,再各自处理自己的窗口可见性。

5. 厂商定制实录:给SystemUI加自定义快捷开关的完整踩坑过程

前面讲了一堆架构,可能还是有点虚。这节我完整记录一次我在SystemUI里加自定义快捷开关的过程。这个任务本身不复杂,但坑非常多,每一步都踩出了经验,对正在做系统定制或者ROM移植的人来说应该很解渴。

5.1 初始方案:在SystemUI内置Tile列表里注册一个自定义模块

需求是要在快捷设置面板里加一个"一键清理"的开关,点一下触发一个自定义清理逻辑,不需要UI界面。看起来很简单,我最初想的方案有两条路:

  • 路线A:写一个外部App,通过TileService向快捷面板注册Tile。这种不需要动SystemUI源码,但Tile的默认位置、图标资源、权限声明都受限制,而且厂商ROM往往会禁止普通应用自行添加快捷设置Tile。
  • 路线B:直接在SystemUI源码里新增一个内置Tile类,然后在默认Tile配置里把它加进去,让它在系统启动时就和其它系统开关一起出现。

因为是要做到系统级默认显示,我选了路线B。

新增类我放在com.android.systemui.qs.tiles包下,类名就叫CleanMemoryTile,继承QSTileImpl<BooleanState>。

public class CleanMemoryTile extends QSTileImpl<BooleanState> { public CleanMemoryTile(QSHost host) { super(host); } @Override protected void handleClick() { // 处理点击事件,临时放个日志占位 Log.d("CleanMemoryTile", "handleClick"); } @Override protected void handleUpdateState(BooleanState state, Object arg) { state.label = mContext.getString(R.string.quick_settings_clean_memory); state.value = false; // 这里需要设置图标,内部用 IconResource 包装 state.icon = IconResource.INSTANCE; } }

然后在QSFactoryImpl里加一个case分支,当Tile的spec是custom(clean)时,就返回我新建的CleanMemoryTile实例。

// QSFactoryImpl.java 中 createTile 里添加分支 if ("clean".equals(spec)) { return new CleanMemoryTile(mHost); }

默认显示列表也要改,在config.xml里找到quick_settings_tiles_default这个字符串数组,把custom(clean)加进去。Android 12里默认值大概是wifi,bt,dnd,rotation,battery,airplane这一串,最终我加完变成:

<string-array name="quick_settings_tiles_default"> <item>wifi</item> <item>bt</item> <item>dnd</item> <item>rotation</item> <item>custom(clean)</item> <item>battery</item> <item>airplane</item> </string-array>

代码改完,执行make SystemUI -j8编译,编完push到设备上重启。到这里为止一切顺利,编译没报错,开机也没崩溃。

5.2 坑一:快捷面板里看到了Tile,但图标和文案是空白

开机后打开快捷面板,新Tile确实出现了,位置也对了,但图标和文字是空白的,一眼看过去就是一个占位格子。这个问题很典型,它的根因是我在handleUpdateState()里设置的图标资源不完整。

在Android 12的SystemUI里,Tile的图标不是简单给一个Drawable就行,State.icon是Icon类型,需要通过mContext.getDrawable()拿到资源,并且要处理Icon的IconResource封装。更关键的是,如果我没有重写getState()时把state.icon初始化成默认值,第一次刷新状态时就会拿到null,导致空白。

我的排查方式:在handleUpdateState()里加Log,看每次状态刷新时state.icon到底是什么。日志显示icon对象非空,但图标没有绘制出来,后来发现是我传的drawable资源ID对应的是普通App资源,而SystemUI进程读取不到,返回了一个引用有效但内容为空的drawable。改用com.android.internal.R.drawable或SystemUI自身资源里的图标之后,问题解决。

5.3 坑二:点击Tile,日志有输出但UI开关没有任何变化

图标解决后,点Tile发现触发逻辑其实执行了,handleClick()里的Log.d打出来了,但Tile的视觉状态没有从"关闭"变成"开启"。这个问题排查的时间比第一个坑还长,因为代码逻辑看起来完全没问题。

最后确认真实原因:我的handleUpdateState()里把state.value = false写死了。点击事件执行后,状态值应改为true或取反,但我没有在handleClick()里更新state.value并调用refreshState()。所以不管是点击前还是点击后,UI渲染出来的始终是同一个状态。

正确写法应该是这样:

@Override protected void handleClick() { mState.value = !mState.value; refreshState(); }

refreshState()会触发handleUpdateState()重新执行,然后通过State数据绑定去更新TileView。SystemUI里这种"状态驱动UI"的模式和Compose里的State思路非常像,只改了View不换State,UI就一定不会变。

5.4 坑三:连续点击导致SystemUI不明原因重启

状态更新正常之后,我又发现一个隐蔽的崩溃点:快速连击Tile多次,偶尔SystemUI会直接重启,状态栏和快捷面板全部消失,然后自动恢复。

抓崩溃日志用这条命令:

adb logcat -b crash -d | grep SystemUI

堆栈指向我在handleClick()里调用的清理逻辑。原因是我在点击事件里做了一些耗时操作,比如遍历进程列表,这个操作直接跑在了主线程上,加上快速点击产生并发,最终触发ANR进而被系统杀掉重启。

教训很明确:SystemUI里永远不要在handleClick或者状态刷新回调里做耗时操作。我自己写了个线程池,把清理动作丢到后台线程执行,UI这边只负责状态翻转,问题就再没出现。

@Override protected void handleClick() { mState.value = !mState.value; refreshState(); // 把耗时逻辑丢到后台线程 mExecutor.execute(() -> doCleanMemory()); }

这三个坑走完,我最大的感受是:SystemUI虽然是个应用,但它不能按写普通App的思路写。它的数据流是强状态的,改动UI时永远要搞清楚状态是什么、状态怎么刷新的,然后还有一条主线程红线绝对不能踩。现在很多厂商在SystemUI基础上做控制中心、快捷工具,本质上都是在和这套状态机打交道,理解handleClick -> refreshState -> handleUpdateState这个循环,比记住任何具体API都重要。

6. 调试SystemUI的实用工具箱:日志、Dump与崩溃恢复

经常有同事问我,改SystemUI经常把状态栏改没了,怎么快速恢复?SystemUI崩溃后状态栏不显示了怎么办?这里整理一套我平时用得最多的调试命令和方法,按"观察状态 -> 看日志 -> 抓崩溃 -> 快速恢复"的顺序来。

6.1 最快恢复:手动杀掉SystemUI进程

SystemUI进程被系统标记为重要系统进程,它一旦死亡,ActivityManager会很快把它重新拉起来。所以我调试SystemUI改动时最常用的不是重启整机,而是杀进程。

adb shell pkill -f com.android.systemui

执行后观察设备,状态栏和导航栏会闪一下再出现,整个过程通常不超过2秒。如果连续改代码,甚至可以写个循环脚本:画make SystemUI -j8 && adb push ... && adb shell pkill -f com.android.systemui。

注意:如果你只是改了资源文件或者布局文件,pkill之后不一定能加载最新的资源,因为资源在App启动时被加载。稳妥做法是在pkill之前adb push完整APK到/system/priv-app/SystemUIGoogle/或对应目录,然后重新启动进程。

6.2 看SystemUI自己的日志

SystemUI里日志Tag非常多,有StatusBar、NotificationShade、QSTileHost、CommandQueue、SystemUIApplication等。用一条过滤命令把系统服务端和SystemUI端的日志都拉出来看:

adb logcat -c adb logcat | grep -E "SystemUI|StatusBar|QSTile|NotificationShade"

如果怀疑是快捷开关的问题,可以重点看QSTile相关Tag,handleClick、handleUpdateState里的日志都会走这里。如果怀疑是系统命令没有下发到UI,看CommandQueue的日志,上面会打印每一条从系统服务端发过来的命令和对应的回调执行情况。

6.3 抓崩溃现场

SystemUI如果发生Native Crash或者Java异常,系统会重启它。在它重启的瞬间日志会被清掉一些,所以抓崩溃要单独用crash buffer,并且尽快停止刷屏:

adb logcat -b crash -d | tail -100

这段输出里通常会有明确的FATAL EXCEPTION以及完整堆栈,定位到类名和行号后修改代码再验证即可。还有一种隐藏崩溃是ANR,SystemUI如果触发ANR会表现为状态栏长时间无响应,最终同样被系统重启,日志里要找ANR in com.android.systemui。

6.4 用Dump命令查看SystemUI的窗口和状态

SystemUI涉及到窗口管理,所以用dumpsys window能直接看到它的窗口是否挂载正常、层级是多少、可见性如何:

adb shell dumpsys window windows | grep -i "systemui"

能看到Window{... StatusBar}、Window{... NavigationBar}这样的输出,说明SystemUI的窗口已经在WMS注册。

如果我想看Notification相关的数据和SystemUI内部视图的一些状态,可以用:

adb shell dumpsys notification --noredact adb shell dumpsys activity service com.android.systemui/.SystemUIService

第二条命令输出的信息取决于SystemUIService中Dump方法写了什么,在定制ROM时可以用它把模块的配置、Tiles列表、当前状态都打出来,非常实用。

6.5 用Systrace分析启动和掉帧

SystemUI卡顿或者启动慢,用Perfetto来做性能分析最直观。传统systrace.py脚本可以这样指定应用:

python systrace.py --app=com.android.systemui -b 20480 -o systemui_trace.html

抓完打开HTML,重点看SurfaceFlinger、Choreographer和SystemUI进程的线程时间片。状态栏滑动掉帧问题一般能在Frame Timeline上直接看到卡顿点。

还有一个开发者选项的土办法:开启开发者选项里的"显示布局边界",SystemUI的每个系统窗口、每个Tile的layout边界都会画出框,改布局时确认对齐非常方便。

最后再分享一个我在反复改SystemUI过程中的体会:如果能走资源层面的修改,尽量不要动Java/Kotlin代码。比如改状态栏高度、快捷面板背景颜色、Tile图标尺寸,这些用Runtime Resource Overlay(RRO)就能实现,编译一次overlay APK消耗的时间远小于重新编译整个SystemUI。只有真正改变交互逻辑或新增模块时,才需要进SystemUI源码里改Java。这样能让你把宝贵时间花在真正该花的地方,而不是耗在无休止的make SystemUI里。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 8:08:20

MySQL索引原理与B+树:从慢查询到索引优化实践

从根上理解索引&#xff1a;它只是把无序的数据变成有序的查找结构做后端这些年&#xff0c;我见过太多因为索引问题导致的线上事故。最典型的一种&#xff1a;业务跑着跑着&#xff0c;某个接口突然变慢&#xff0c;慢查询日志里发现一条SQL要扫描几百万行&#xff0c;DBA一看…

作者头像 李华
网站建设 2026/9/30 8:08:10

PostgreSQL增删改实战:RETURNING与ON CONFLICT

1. 这一篇到底要解决什么问题这是 PostgreSQL 系列教程的第 8 篇&#xff0c;专门讲插入、更新与删除数据。如果你之前只写过最简单的INSERT INTO ... VALUES&#xff0c;后面的RETURNING、ON CONFLICT、DO UPDATE这些东西肯定能帮你打开新世界的大门。先说句实在话&#xff1a…

作者头像 李华
网站建设 2026/9/30 8:07:45

MySQL子查询性能优化实战:从线上CPU事故到JOIN改写

1. 一次让我对子查询产生警惕的线上故障 1.1 故障背景&#xff1a;一条不算复杂的SQL把数据库CPU打满 先讲一件真事。前两年我负责的一个电商系统出了一次线上事故&#xff0c;用户反馈订单列表打开极慢&#xff0c;后台监控MySQL CPU直接飙到95%以上。我抓出慢查询日志&#…

作者头像 李华
网站建设 2026/9/30 8:07:26

Wireshark RTP丢包率分析:统计口径、排障实践与脚本化

简介&#xff1a;这是一份面向网络运维、音视频技术支持及Wireshark初学者的实操型PDF教程&#xff0c;聚焦如何用Wireshark分析RTP丢包率。资源大小759KB&#xff0c;含1个PDF文档&#xff0c;页面内容围绕四步排查流程展开&#xff1a;先用CtrlF定位rtsp/1.0交互包&#xff0…

作者头像 李华
网站建设 2026/9/30 8:06:10

D2L 工具函数与工具类详解:从超参数管理到 Seq2Seq 训练管线

文档教程人工智能深度学习NLP计算机视觉强化学习 【免费下载链接】d2l-en Interactive deep learning book with multi-framework code, math, and discussions. Adopted at 500 universities from 70 countries including Stanford, MIT, Harvard, and Cambridge. 项目地址&am…

作者头像 李华
网站建设 2026/9/30 8:03:27

大数据面试高频考点:SQL窗口函数、Spark原理与数仓建模全解析

1. 大数据面试到底在考什么——先把这个搞清楚再刷题说实话&#xff0c;我在这个圈子里混了十几年&#xff0c;面过的人少说也有几百个&#xff0c;自己也换过几次工作。我观察到的最普遍现象是&#xff1a;很多候选人刷题的方式完全跑偏了。有的人抱着LeetCode死磕hard题&…

作者头像 李华