1. 从 Launcher 里那个"甩不掉的搜索框"说起
如果你做过 Android 系统定制或者 Launcher 相关的开发,大概率遇到过这样一个需求:客户或者产品经理指着桌面顶部那个搜索框说,"这个能不能去掉?"或者"这个能不能换成我们自己的?"这个搜索框在 AOSP 体系里有个专门的叫法——QSB,全称 Quick Search Box,也就是 Google Search Box 的缩写。在 Android Q(也就是 Android 10)的非 Go 版本上,QSB 的处理逻辑和 Go 版本有本质区别,这也是很多人在移植或者定制时容易踩坑的地方。
我自己在做一个基于 Android Q 的非 Go 版本 Launcher 定制项目时,就完整地跟这个 QSB 缠斗了一轮。从最开始以为"删掉一个控件就行",到后来发现它牵扯到 Launcher3 的源码结构、编译配置、GMS 依赖、Feature Flag 开关,甚至还有 Android.mk 和 Android.bp 的构建差异,整个过程踩了不少坑。这篇文章就是把这套处理流程完整地梳理出来,包括 QSB 在非 Go 版本上的存在形态、为什么不能简单粗暴地删、正确的处理路径是什么、编译层面要注意什么,以及实测中遇到的各种意外情况。
这篇文章适合几类人看:一是做 Android 系统定制、Launcher 定制的开发者;二是需要在自己的 ROM 里控制 QSB 显示与否的工程师;三是对 AOSP Launcher3 结构感兴趣、想搞清楚 QSB 来龙去脉的技术人。我会尽量把每一步的"为什么"讲清楚,而不是只丢一堆代码让你抄。因为 QSB 这个东西,不同版本、不同配置下的表现差异很大,只抄代码不理解原理,换个版本就废了。
先给一个最核心的结论:在 Android Q 非 Go 版本上,QSB 的处理不能靠删布局文件解决,必须从 Feature Flag 和构建配置两个层面同时下手,而且要注意 GMS 版本和非 GMS 版本的区别。下面我一步步拆开讲。
2. QSB 在 Android Q 非 Go 版本里到底是个什么东西
2.1 QSB 的两种形态:Go 版和非 Go 版的分水岭
要理解 QSB 的处理,首先得搞清楚它在 Go 版本和非 Go 版本上的差异。Android Go 是 Google 面向低内存设备推出的精简版本,它的 Launcher 叫 Launcher3 Go,整个搜索框的实现是极度精简的,很多功能被砍掉了。而非 Go 版本的 Launcher3,QSB 是一个功能完整的组件,它不仅仅是一个"搜索框"的 UI,背后还挂着一整套搜索建议、语音搜索、Google App 联动等逻辑。
在源码层面,这个分水岭主要体现在几个地方。第一是AndroidManifest.xml里的 feature 声明,非 Go 版本会声明更多的 meta-data 和权限。第二是build.gradle或者Android.mk里的编译开关,Go 版本会走一套精简的资源配置。第三是代码里的FeatureFlags,非 Go 版本默认开启的 flag 更多。
我实测下来,最容易混淆的一点是:很多人以为把 Go 版本的配置照搬到非 Go 版本就能去掉 QSB,结果发现编译能过,但运行起来搜索框还在,或者干脆 Launcher 崩溃。原因就是非 Go 版本的 QSB 绑定逻辑更复杂,不是单一开关能控制的。
2.2 QSB 在 Launcher3 源码中的位置
在 Android Q 的 Launcher3 源码里,QSB 相关的核心类主要集中在src/com/android/launcher3/qsb/这个包下面。你会看到几个关键类:QsbContainerView是搜索框的容器视图,QsbWidget是它作为 widget 的封装,还有一个SearchUiManager负责管理搜索的 UI 状态。
布局文件方面,QSB 的布局通常在res/layout/下的search_container_workspace.xml或者类似的文件里定义。但这里有个大坑:你不能直接删这个布局文件,因为它在多个地方被引用,删了会导致编译报错或者运行时 inflate 失败。正确的做法是通过配置控制它的可见性和加载逻辑。
还有一个关键点是LauncherAppState或者Launcher类里对 QSB 的初始化调用。在非 Go 版本上,这个初始化是默认执行的,你需要找到对应的判断条件,通过 flag 把它关掉。
2.3 为什么非 Go 版本的 QSB 处理更麻烦
这里我要重点解释一下"为什么"。Go 版本的 QSB 之所以好处理,是因为它的设计目标就是"可裁剪",Google 在架构上就留了开关。而非 Go 版本的 QSB 是"默认完整"的设计思路,它假设你会用完整的 Google 搜索体验,所以很多逻辑是硬编码进去的。
具体来说,非 Go 版本的 QSB 牵扯到三个层面的耦合:
| 耦合层面 | 具体表现 | 处理难度 |
|---|---|---|
| UI 层 | 布局文件、View 层级、动画逻辑 | 中等 |
| 逻辑层 | SearchUiManager、FeatureFlags、状态管理 | 较高 |
| 构建层 | Android.mk/Android.bp、资源覆盖、GMS 依赖 | 高 |
UI 层的问题相对好解决,逻辑层需要理解 flag 的作用范围,构建层则是最容易出错的,因为 Android Q 正好处在Android.mk向Android.bp迁移的过渡期,两套构建系统并存,配置写错一个地方就可能导致 QSB 处理不生效。
3. 处理 QSB 的正确路径:从 Feature Flag 入手
3.1 找到控制 QSB 的核心 Feature Flag
在 Launcher3 的源码里,有一个FeatureFlags类,里面定义了大量布尔型的开关。跟 QSB 直接相关的,通常是类似QSB_ON_FIRST_SCREEN或者ENABLE_QSB这样的 flag。在 Android Q 非 Go 版本上,这个 flag 的默认值往往是true。
我第一次处理的时候,直接把这个 flag 改成false,然后编译刷机,发现搜索框确实没了,但桌面上方留了一块空白区域,而且点击那块区域还会触发搜索。这说明 flag 只控制了"显示",没有控制"占位"和"事件响应"。
所以正确的做法是:改 flag 只是第一步,还要处理占位和事件。具体来说,你需要找到 QSB 所在的父容器,在 flag 为 false 的时候,把整个容器的可见性设为GONE,而不是INVISIBLE。GONE和INVISIBLE的区别在这里很关键——INVISIBLE只是看不见,但仍然占位并且能响应点击;GONE才是彻底移除。
3.2 Feature Flag 的作用范围和副作用
改 flag 的时候一定要注意它的作用范围。有些 flag 是全局的,改了会影响整个 Launcher 的行为;有些 flag 是局部的,只影响 QSB 本身。我在项目里遇到过一个问题:把某个 flag 关掉之后,不仅 QSB 没了,连带着"全部应用"按钮的动画也变了。排查了半天才发现,那个 flag 同时控制了两个功能的代码路径。
所以我的经验是:改 flag 之前,先在源码里全局搜索这个 flag 被引用的所有位置,看看它到底影响了哪些逻辑。在 Android Studio 里用Find Usages功能,或者在命令行用grep -rn "FLAG_NAME"都能快速定位。
另外,flag 的修改位置也有讲究。有些 flag 定义在FeatureFlags.java里,直接改默认值就行;有些 flag 是通过Settings或者SystemProperties动态读取的,这种情况你改代码默认值没用,得改配置或者运行时注入。
3.3 实测:flag 修改后的验证方法
改完 flag 之后,怎么验证是否生效?我一般用三步验证法:
- 编译验证:先确保编译能过,没有资源引用错误。这一步能过滤掉大部分低级错误。
- 静态验证:用
adb shell dumpsys activity或者adb shell dumpsys window查看 Launcher 的窗口层级,确认 QSB 对应的 View 是否还存在。 - 动态验证:实际点击、滑动、切换页面,确认没有残留的占位和事件响应。
这里有个小技巧:在开发者选项里打开"显示布局边界",能直观地看到 QSB 区域是否还占位。如果那块区域还有边框,说明占位没处理干净。
4. 构建配置层面的处理:Android.mk 与 Android.bp 的坑
4.1 Android Q 的构建系统过渡期问题
Android Q 是一个很特殊的版本,它处在Android.mk和Android.bp两套构建系统并存的阶段。Launcher3 这个模块,在不同厂商的代码里,可能用的是Android.mk,也可能已经迁移到了Android.bp,甚至两者都有。这就导致 QSB 的处理在构建层面变得复杂。
如果你的项目用的是Android.mk,那么 QSB 相关的资源配置、依赖库、编译开关都在这个文件里。如果你用的是Android.bp,配置方式完全不同。最坑的情况是:主模块用Android.bp,但某些资源或者依赖还在Android.mk里,两边配置不一致,导致 QSB 处理不生效。
4.2 Android.mk 中与 QSB 相关的配置项
在Android.mk里,跟 QSB 相关的配置主要有几类。第一类是LOCAL_STATIC_ANDROID_LIBRARIES或者LOCAL_STATIC_JAVA_LIBRARIES,里面可能包含 QSB 依赖的搜索库。第二类是LOCAL_RESOURCE_DIR,决定了资源文件的搜索路径,如果你要覆盖 QSB 的布局,就得在这里加自己的资源目录。第三类是LOCAL_MANIFEST_FILE,控制 manifest 的合并。
我踩过的一个坑是:在Android.mk里加了一个资源覆盖目录,想用自己的布局替换 QSB 的布局,结果编译过了,但运行时用的还是原来的布局。排查后发现是资源目录的优先级问题——LOCAL_RESOURCE_DIR里目录的顺序决定了优先级,后加的目录优先级反而低。调整顺序后才生效。
4.3 Android.bp 迁移时的注意事项
如果你的项目已经把 Launcher3 迁移到了Android.bp,那么配置方式就变成了android_app或者android_library的模块定义。QSB 相关的配置主要体现在srcs、resource_dirs、static_libs这些字段里。
迁移的时候最容易出问题的是resource_dirs的处理。Android.mk里的LOCAL_RESOURCE_DIR可以写多个目录,Android.bp里对应的是resource_dirs数组,但两者的优先级规则不完全一样。我在迁移一个项目时,就因为资源优先级变了,导致 QSB 的布局覆盖失效,最后是通过调整resource_dirs数组的顺序解决的。
还有一个点是manifest的处理。Android.mk用LOCAL_MANIFEST_FILE,Android.bp用manifest字段,但Android.bp对 manifest 合并的控制更严格,有些在Android.mk里能用的技巧在Android.bp里行不通。
4.4 构建配置的验证与排查
构建配置改完之后,怎么确认生效了?我的做法是:
- 用
mma或者m命令单独编译 Launcher3 模块,看编译日志里资源目录和依赖库是否正确加载。 - 编译完成后,用
aapt dump badging查看生成的 APK 的资源配置。 - 刷机后,用
adb shell pm path找到 Launcher 的 APK 路径,pull 出来解包,确认布局文件是不是你覆盖的那个。
这套流程看起来繁琐,但能帮你快速定位问题出在编译阶段还是运行阶段。我见过太多人改完配置不验证,直接刷机,结果问题一堆,排查起来更费时间。
5. 非 Go 版本 QSB 处理中的典型踩坑与排查链路
5.1 坑一:删了布局文件导致 Launcher 崩溃
这是最典型的坑。很多人第一反应是找到 QSB 的布局文件,直接删掉或者注释掉。结果编译能过(因为布局引用是运行时的),但一刷机 Launcher 就崩溃,日志里报InflateException或者Resources.NotFoundException。
根因:QSB 的布局在多个地方被引用,包括QsbContainerView的 inflate 逻辑、SearchUiManager的初始化、甚至某些动画资源。删掉布局文件,这些引用就找不到资源了。
正确做法:不要删布局文件,而是通过 flag 控制它的加载。如果确实要替换布局,就在自己的资源目录里放一个同名布局,通过资源覆盖的方式替换,而不是删除原文件。
5.2 坑二:flag 改了但 QSB 还在
这个坑我也踩过。明明把FeatureFlags里的 QSB 开关改成了false,编译刷机后搜索框还在。排查链路是这样的:
- 先确认改的 flag 是不是真的被引用了。用
grep搜索,发现这个 flag 只在某个条件分支里被读取,而那个分支在当前配置下根本不会走到。 - 再确认有没有其他 flag 或者配置覆盖了这个值。发现
Settings里有一个运行时配置,优先级高于代码默认值。 - 最后确认 GMS 版本的影响。非 GMS 版本和 GMS 版本的 Launcher 可能是两个不同的 APK,改的代码可能只影响其中一个。
解决:找到真正生效的那个 flag 和配置源,从源头改。同时确认你编译的是哪个版本的 Launcher。
5.3 坑三:QSB 没了但留了空白和点击区域
这个前面提过,是GONE和INVISIBLE的区别问题。但实际排查时,情况可能更复杂。我有一次遇到的是:QSB 的 View 确实GONE了,但它的父容器还占着高度,因为父容器的高度是写死的。
排查链路:用Layout Inspector或者uiautomatorviewer查看 View 层级,找到那个占位的父容器,检查它的layout_height和layout_weight。发现是一个FrameLayout写死了高度,改成wrap_content或者GONE后问题解决。
5.4 坑四:Android.mk 和 Android.bp 配置冲突
这个坑最隐蔽。项目里同时存在Android.mk和Android.bp,你以为改的是生效的那个,实际上编译系统用的是另一个。排查方法是看编译日志,确认实际参与编译的是哪个文件。或者用soong_ui的调试功能,查看模块的最终配置。
经验:在 Android Q 上做 Launcher 定制,一定要先搞清楚你的项目用的是哪套构建系统,别想当然。如果不确定,就两个文件都检查一遍,确保配置一致。
5.5 坑五:GMS 依赖导致的 QSB 复活
这个坑跟 GMS 有关。有些项目在去掉 QSB 之后,集成了 GMS 包,结果 GMS 里的 Launcher 或者搜索组件又把 QSB 带回来了。因为 GMS 有自己的 Launcher 配置,会覆盖你的定制。
解决:要么在 GMS 集成时排除相关的 Launcher 组件,要么在 GMS 的配置里也做对应的 QSB 处理。这个需要跟 GMS 集成的同事配合,单靠改 Launcher3 源码解决不了。
6. 一套可复用的 QSB 处理方案与实操步骤
6.1 方案设计思路
综合上面的经验,我总结了一套在 Android Q 非 Go 版本上处理 QSB 的可复用方案。核心思路是:flag 控制 + 资源覆盖 + 构建配置对齐 + 验证闭环。不删任何原始文件,全部通过配置和覆盖来实现,保证可回退、可维护。
这套方案的好处是,不管你后面要恢复 QSB 还是换成自己的搜索框,都只需要改配置,不用动源码结构。而且在不同项目之间移植时,只需要调整构建配置部分,逻辑部分是通用的。
6.2 具体操作步骤
第一步:定位 QSB 的核心 flag。在FeatureFlags.java里找到 QSB 相关的 flag,确认它的默认值和引用位置。用grep -rn全局搜索,列出所有引用点。
第二步:修改 flag 并处理占位。把 flag 改为false,同时找到 QSB 的父容器,在 flag 为 false 时将其可见性设为GONE。注意检查父容器的高度设置,避免留白。
第三步:资源覆盖(可选)。如果需要替换 QSB 的 UI,在自己的资源目录里放同名布局,通过LOCAL_RESOURCE_DIR或resource_dirs覆盖。注意目录顺序和优先级。
第四步:构建配置对齐。确认项目用的是Android.mk还是Android.bp,在对应的文件里配置资源目录和依赖。如果两者都有,确保配置一致。
第五步:编译验证。单独编译 Launcher3 模块,检查编译日志,确认资源加载正确。
第六步:运行时验证。刷机后,用dumpsys、Layout Inspector、布局边界显示等工具,确认 QSB 彻底移除,没有残留占位和事件。
6.3 关键配置示例
以Android.bp为例,资源覆盖的配置大概是这样:
android_app { name: "Launcher3", srcs: ["src/**/*.java"], resource_dirs: [ "res", "res_custom", // 自定义资源目录,优先级更高 ], manifest: "AndroidManifest.xml", static_libs: [ "launcher3-common", // 其他依赖 ], }如果是Android.mk,对应的配置是:
LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res_custom $(LOCAL_PATH)/res LOCAL_MANIFEST_FILE := AndroidManifest.xml LOCAL_STATIC_JAVA_LIBRARIES := launcher3-common注意LOCAL_RESOURCE_DIR里res_custom放在前面,优先级更高。这个顺序很关键,放反了覆盖就不生效。
6.4 实测效果与回退方案
我用这套方案在几个不同配置的 Android Q 项目上测试过,包括纯 AOSP 版本和带 GMS 的版本。纯 AOSP 版本上,QSB 能彻底移除,桌面布局正常,没有残留。带 GMS 的版本上,需要额外处理 GMS 的 Launcher 配置,处理完之后也能达到预期效果。
回退方案很简单:把 flag 改回true,去掉资源覆盖目录,重新编译即可。因为原始文件都没动,回退没有任何副作用。这也是我坚持不删文件的原因——可回退性在项目维护中太重要了。
7. 几个容易被忽略的细节和我的实操心得
7.1 关于 QSB 的动画和过渡效果
QSB 不只是静态的搜索框,它还带一套动画和过渡效果,比如从桌面滑到搜索界面的转场。去掉 QSB 的时候,如果只处理了静态部分,动画部分可能会出问题,表现为点击某个区域触发了一个空动画,或者转场时闪一下。
我的处理方式是:找到SearchUiManager里跟动画相关的回调,在 flag 为 false 时直接 return,不执行动画逻辑。这样既避免了异常,也省了性能开销。
7.2 关于多语言和资源配置
QSB 的布局在不同语言、不同屏幕尺寸下可能有不同的资源变体,比如layout-land、layout-sw600dp等。做资源覆盖的时候,这些变体目录也要一并处理,否则在横屏或者大屏设备上可能还是显示原来的 QSB。
我一般会把所有跟 QSB 相关的布局变体都列出来,逐个覆盖。虽然麻烦,但能保证全场景一致。
7.3 关于编译缓存
Android 的编译缓存有时候会坑人。你改了配置,但编译系统用了缓存,结果刷机后发现没生效。这种情况我遇到过好几次,后来养成了习惯:改完构建配置后,先make clean或者删掉对应的out目录,再重新编译。
虽然clean后全量编译慢,但能避免缓存导致的假象。如果时间紧,至少要把 Launcher3 模块的out目录删掉。
7.4 关于不同厂商的定制差异
不同厂商的 Android Q 代码,Launcher3 的定制程度不一样。有的厂商把 QSB 改得面目全非,有的基本保持 AOSP 原样。所以上面这套方案,在 AOSP 原版上最直接,在厂商定制版上可能需要先摸清他们的改动。
我的建议是:拿到代码后,先 diff 一下厂商版本和 AOSP 版本的 Launcher3,看看 QSB 相关部分改了多少。改动大的话,处理方式可能要调整。
7.5 一个实用的小工具推荐
排查 QSB 问题时,我常用一个组合:adb shell dumpsys activity top看当前 Activity 的 View 层级,配合Layout Inspector看实时布局。这两个工具结合起来,能快速定位 QSB 的 View 在哪里、占多大空间、有没有被隐藏。
另外,uiautomatorviewer在排查点击区域问题时特别好用,能直观看到每个 View 的边界和属性。虽然它在新版本 Android 上有些兼容性问题,但在 Android Q 上还能用。
这套 QSB 处理方案,我在实际项目里反复用过,从最初的踩坑到后来的熟练,最大的体会就是:Android 系统定制这件事,理解原理比记住步骤重要得多。因为版本在变、厂商在改、配置在迁移,只有搞清楚 QSB 为什么这么设计、flag 为什么这么控制、构建系统为什么这么配置,才能在遇到新情况时快速找到解法。希望这篇梳理能帮你少走点弯路。