ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Android桌面小控件从哪来?追踪Manifest中的Widget Provider声明

Android桌面小控件从哪来?追踪Manifest中的Widget Provider声明 很多人第一次看到桌面小控件widget列表时心里都冒过同一句话“这个 xxx 小控件到底是哪句代码让它出现的”我每次被人拿这个问题问住对方都会带着一张截图有人以为是自己代码里某个new WidgetView()写出来的有人怀疑系统桌面悄悄缓存了旧数据还有人紧张地问是不是被谁“注入”了。其实在 Android 系统里小控件不是动态画到桌面上的而是从 Manifst 里“登记”出来的。想定位“是哪句代码”得先搞懂系统扫描小部件列表的完整链路。这篇就沿着这条链路拆开讲顺便也把“列表里多出来的不认识的 xxx 小控件怎么揪出来”这件事说清楚。1. 桌面“小控件窗口”到底在扫描什么1.1 小部件列表不是你的 App 自己画出来的我们平时长按桌面空白处弹出的那个“小控件 / 小组件 / Widgets”窗口在 Android 系统里其实是一个标准的系统界面。Launcher 桌面应用通过系统服务去查询所有已经安装的 App看看谁在 Manifest 里注册了“小控件 Provider”。这个查询发生在你的 App 进程之外也就是说哪怕你的 App 从来没被打开过、进程已经死了一万年只要这个 App 安装包里带着小控件声明Launcher 就有能力把它列出来。很多开发者第一次得知这个结论时都会愣一下。你以为是自己的代码“画”了一个控件上去实际上系统只是在读一份注册名单。名单上有什么小部件列表就显示什么。这不是new View()能参与的流程而是PackageManagerAppWidgetService在背后做了一层筛选。所以回到标题这个问题桌面的小控件窗口会出现某个 xxx 小控件最直接的答案就是——你的应用或你引用的某个库在系统注册名单里留了一条 AppWidget Provider 记录。1.2 它和悬浮窗、桌面快捷方式的本质区别很多人会把“小控件”跟“悬浮窗”搞混。悬浮窗是用WindowManager.addView()加出来的需要申请SYSTEM_ALERT_WINDOW权限视图还跑在你的应用进程里关掉权限就没有了。小控件完全是另一套机制。它本质上是一个RemoteViews对象由系统交给 Launcher 进程去渲染。你的 App 进程可以完全不参与后续的显示过程Launcher 拿到 RemoteViews 之后自己 inflate、自己展示。这就好比电视台和电视的关系。你的 App 是电视台Launcher 是你家里的电视机。电视台把“节目信号”打包好发出去电视机负责把画面播出来。电视台不可能把一台电视硬塞进你家同样你的 App 也不可能用自己的代码把一个小控件“怼”到桌面上。理解了这个前提你再去翻代码就会轻松很多。凡是找“小控件窗口为什么会有 xxx”核心都落在“有没有注册”和“注册信息写的是什么”上而不是去某个 Activity 里翻布局。2. 登记小控件的核心代码藏在 Manifest 的 receiver 里2.1 让系统认可你是一个 Widget Provider 的“户口本”Android 里的桌面小控件不是靠一个类继承就算数的系统只认AndroidManifest.xml里的声明。完整的声明一般是这样的receiver android:name.XxxWidgetProvider android:exportedtrue android:labelstring/widget_name intent-filter action android:nameandroid.appwidget.action.APPWIDGET_UPDATE / /intent-filter meta-data android:nameandroid.appwidget.provider android:resourcexml/xxx_widget_info / /receiver这段代码里最关键的是两个部分intent-filter里的android.appwidget.action.APPWIDGET_UPDATE它告诉系统“我有一个广播接收器能接收小控件更新事件”。meta-data里的android.appwidget.provider它把系统引导到res/xml/xxx_widget_info.xml让系统读到这个小控件的详细配置。这两个条件缺一不可。如果没有 intent-filter桌面可能扫不到这个入口如果没有 meta-data系统不知道你到底想做成什么样的小控件自然也不会把它当成一个合法的 provider。我在实际项目里见过有人把 receiver 改成普通 class忘了继承AppWidgetProvider结果小部件列表里照样能看到但拖到桌面后永远显示不出内容。原因是系统检查到类不匹配时会有兼容老版本的降级处理但你的onUpdate永远不会被回调界面自然一片空白。2.2 appwidget-provider XML 决定了你看到的预览和外貌注册完 receiver 之后系统会去读res/xml/xxx_widget_info.xml里面通常长这样appwidget-provider xmlns:androidhttp://schemas.android.com/apk/res/android android:minWidth250dp android:minHeight110dp android:targetCellWidth4 android:targetCellHeight2 android:updatePeriodMillis1800000 android:initialLayoutlayout/widget_xxx android:previewLayoutlayout/widget_xxx_preview android:resizeModehorizontal|vertical android:widgetCategoryhome_screen android:descriptionstring/widget_desc /你看到的“小控件卡片有多宽、多高、名字叫什么、预览图长什么样”大部分都和这个文件里的属性直接相关。我把几个容易影响观感的属性放在下面这张表里属性影响android:label小部件列表里显示的名称也就是你常说的“xxx 小控件”这几个字android:previewLayout/android:previewImage小部件列表里的预览卡片样式android:initialLayout放到桌面后初始渲染的布局也可能被系统当成预览兜底android:widgetCategory只写home_screen才会出现在桌面小部件列表写keyguard是锁屏场景android:updatePeriodMillis自动刷新周期短于 30 分钟会被系统按 30 分钟处理android:resizeMode用户能不能在桌面上拖动改变尺寸很多人会忽视一个问题如果你没写previewLayout系统就会拿initialLayout去生成一个静态预览这在绝大多数情况下会非常难看。你看到的某个 xxx 小控件“缩略图”和桌面上的实际效果不一样多半就是这里没配好而不是代码写错了。2.3 “xxx”到底是列表里的名字还是桌面上的文字标题里这个“xxx”容易产生歧义。如果指的是“小部件列表里那个叫 xxx 的小控件”那你要找的是receiver android:label或 appwidget-provider 里的android:label。如果指的是“把某个小控件放到桌面后窗口里显示了一段 xxx 文字或数字”那问题就要换个方向查了。这种内容通常不是在布局 XML 里硬编码的而是在AppWidgetProvider#onUpdate里用 RemoteViews 动态赋值写上去的。举个例子一个倒计时小控件用户看到剩余“10 天”。布局 XML 里只会有空的 TextView真正把“10 天”塞进去的代码是这样class CountdownWidgetProvider : AppWidgetProvider() { override fun onUpdate( context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray ) { val views RemoteViews(context.packageName, R.layout.widget_countdown) val remainDays TimeUtil.remainDays() views.setTextViewText(R.id.tv_remain, 还剩 $remainDays 天) appWidgetManager.updateAppWidget(appWidgetIds, views) } }所以你在桌面上看到的“xxx 小控件”如果是一串文字、一张图、甚至一个按钮的文字那“始作俑者”往往是 Provider 类里的某句setTextViewText()、setImageViewResource()或setOnClickPendingIntent()。这一下就把排查范围缩小了列表里能否出现看 Manifest列表里展示的名称和预览图看 label 和 preview 配置放到桌面后显示的内容看onUpdate里的 RemoteViews 赋值。3. 最常见真凶不是你的代码是你引用的 SDK 里带了个小控件3.1 Manifest Merger 是怎么悄悄给你的项目“加料”的有一次我帮同事排查一个诡异问题他自己写的小工具 App 完全没有做任何桌面小控件功能但用户反馈长按桌面能看到一个叫“XX快捷入口”的小控件而且拖到桌面上还能用。我第一反应就是查依赖。这种“看似凭空出现的小控件”八成都不是你自己写的而是某个三方 SDK 通过 Manifest Merger 合进来的。Android 项目在构建时会把所有 library 模块里的 AndroidManifest 和你自己写的 app 模块的 Manifest 合并成一个最终文件。三方 SDK 如果自带 AppWidgetProvider 声明哪怕你压根不知道它也会被并进最终 APK。定位方法很简单。Android Studio 里打开app/build/intermediates/merged_manifests/目录不同 AGP 版本路径略有差异找到最终合并后的 AndroidManifest.xml直接搜android.appwidget.action.APPWIDGET_UPDATE。所有出现的位置就是所有注册小控件的入口。很多 SDK 设计者喜欢往自己的库里头塞一个小控件目的是在桌面上增加品牌曝光。如果你的产品不需要它就会变成你眼里那个“不知道哪来的 xxx 小控件”。3.2 用 tools:node 把不想要的 SDK 小控件直接摘掉找到是哪个三方库之后解决办法也很粗暴在你自己 app 的 AndroidManifest.xml 里写一个同名 receiver然后用tools:noderemove把它干掉。receiver android:namecom.some.sdk.QuickWidgetProvider tools:noderemove /记得在 Manifest 根节点加上xmlns:toolshttp://schemas.android.com/tools这样构建时Manifest Merger 就会把 SDK 仓库里那个 receiver 节点移除最终 APK 里就不会再带这个小控件声明桌面列表里自然也看不到它。如果不想整个移除只想改名字或改预览图也可以用tools:nodereplace自己重写android:label、android:exported等属性。这种方式适合那种“功能可以留但展示名字必须归我管”的场景。这里面最容易踩的坑是你改了代码、重新安装了 App但是桌面小部件列表里那个 xxx 小控件还在。这不是你删得不干净而是 Launcher 有缓存。通常重启一次桌面进程或者重启手机就能刷新彻底一点可以在系统设置里清除 Launcher 应用的数据但那样会连桌面图标布局一起清掉动手前先备份。4. 从“出现在列表”到“成功挂到桌面”系统背后跑过的五步理解了声明和展示的关系之后你再回头看“是哪句代码”这种问题会发现真正的代码路径其实分成好几段。4.1 Launcher 查询 Provider 列表桌面长按弹出来小部件列表Launcher 会调用系统服务拿到当前设备上所有可用的小控件 provider。你可以把这一步理解成“系统翻户口本”户口本上有没有你取决于第 2 节里那段 Manifest 声明。4.2 用户选择之后申请绑定当你从列表里选中一个 xxx 小控件拖到桌面上的时候Launcher 会向AppWidgetService申请一个 appWidgetId并把它和某个 provider 绑定。普通应用想直接自动让桌面出现小控件可以调用if (Build.VERSION.SDK_INT Build.VERSION_CODES.O) { AppWidgetManager.getInstance(context).requestPinAppWidget( ComponentName(context, XxxWidgetProvider::class.java), null, null ) }但requestPinAppWidget()也只会弹出一个系统确认窗口让用户亲自确认是否添加。系统不允许普通应用静默往桌面塞控件这是刻在 Android 安全模型里的规则。所以如果你问“能不能用一句代码让桌面小控件窗口出现一个 xxx”答案是声明可以强制静默不行。4.3 系统回调 Provider 的 onUpdate绑定成功之后系统会触发AppWidgetProvider的onUpdate()。你的 App 进程可能这时候才第一次被拉起来你在这个回调里准备 RemoteViews并用AppWidgetManager.updateAppWidget()推给系统。这一步是“给桌面送内容”的关键。很多刚上手的人以为写了onUpdate就万事大吉忘了调用updateAppWidget()结果小控件在桌面上一直是空的或者一直显示布局 XML 里的初始值。4.4 AppWidgetHost 创建视图真正在桌面上渲染小控件的是 Launcher 自己实现的AppWidgetHost和AppWidgetHostView。如果你写过 Launcher 或者桌面定制应用你会看到类似这样的代码AppWidgetHost host new AppWidgetHost(context, hostId); AppWidgetHostView hostView host.createView(context, appWidgetId, appWidgetInfo);createView()内部会把 RemoteViews 加载成 Android View再放进桌面的 ViewGroup。这一步属于“宿主端代码”它不在你的 App 工程里而是系统桌面自带的能力。4.5 后续更新App 进程可以不在RemoteViews 仍然存在这也是很反直觉的一点你的 App 被“一键清理”杀掉之后桌面上的小控件通常还能继续显示。因为小控件的实际视图已经从 RemoteViews 变成 Launcher 进程里的 View 了你的 App 进程只是遥控器电视遥控器没电了电视当前画面并不会瞬间消失。除非系统触发下一次 widget 更新、或者桌面重启Launcher 才会重新向你的 Provider 请求一份新的 RemoteViews。如果那个时候你的 App 进程已经不在Provider 才会被拉起来响应。这也是很多用户发现“杀完后台之后小控件不刷新了”的深层原因。5. 无论你看到的 xxx 是什么按这份清单定位代码我把这些年排查桌面小控件问题的思路整理成了一套流程你照着走一遍基本都能找到源头。5.1 先判断你看到的现象属于哪一类看到的现象排查方向小部件列表里多了一个不认识的小控件Manifest 里的 receiver 声明尤其是三方 SDK 合入的列表里显示的控件名称不对receiver 的android:label或 appwidget-provider 的android:label列表里的缩略图和实际桌面效果不一致previewLayout/previewImage没配置或配置了但和initialLayout不匹配拖到桌面后小控件是一片空白Provider 类是否正确继承 AppWidgetProvideronUpdate是否调用updateAppWidget()拖到桌面后某个文字/数字/图片不对onUpdate里的setTextViewText、setImageViewResource、数据源计算逻辑明明删了相关代码列表里还有Launcher 缓存系统重启桌面或清 Launcher 数据5.2 按下面五步翻代码第一步在 Android Studio 里全局搜索android.appwidget.action.APPWIDGET_UPDATE把当前项目里所有注册过小控件的 receiver 全部列出来。这个 action 相当于小控件的身份标识但凡有地方出现大概率就是那个“入口”。第二步点进每个 receiver看android:name指向哪个类再看meta-data里的android:resource指向哪个 XML。这两个字段会把“入口”和“配置”串起来。第三步打开那个 XML逐行检查label、previewLayout、previewImage、initialLayout、widgetCategory。如果标题里问的 xxx 是指“为什么叫这个名字”答案基本就在这一步可能藏在string资源里。第四步找到 receiver 指向的 Provider 类看onUpdate方法里给 RemoteViews 做了什么赋值。如果桌面上的小控件内容有明显错误比如显示的时间不对、文字是硬编码死的问题多半在这里。第五步如果项目里怎么搜都搜不到关键词换英文widget、appwidget搜整包路径和资源文件。还是找不到就去 build 目录看合并后的最终 Manifest并把范围扩大到所有第三方依赖。这套流程做完还找不到的情况很少。真到那一步再用adb shell dumpsys appwidget看看系统当前记录的 provider 列表确认系统端到底加载了哪些小控件。输出里能看到 provider 组件名、host 信息、当前绑定的 appWidgetId对于排查“为什么重启后还在”这类问题很有用。6. 如果你想自己“造”一个 xxx 小控件最小可用工程长这样知道怎么定位还要知道怎么快速写一个。我直接给一套最小可用的示例你把它跑起来桌面小部件列表里立刻就能看到属于你的小控件。6.1 第一步写一个 receiver 声明在AndroidManifest.xml的application里面加这段receiver android:name.DemoWidgetProvider android:exportedtrue android:labelstring/demo_widget_name intent-filter action android:nameandroid.appwidget.action.APPWIDGET_UPDATE / /intent-filter meta-data android:nameandroid.appwidget.provider android:resourcexml/demo_widget_info / /receiver这里我特意把android:exported写成true。新版 Android 对带 intent-filter 的组件要求显式声明 exported而且小控件是需要被系统跨进程启动的保持 true 最稳。6.2 第二步创建 provider 配置文件res/xml/demo_widget_info.xml?xml version1.0 encodingutf-8? appwidget-provider xmlns:androidhttp://schemas.android.com/apk/res/android android:minWidth250dp android:minHeight110dp android:targetCellWidth4 android:targetCellHeight2 android:updatePeriodMillis1800000 android:initialLayoutlayout/widget_demo android:previewLayoutlayout/widget_demo android:resizeModehorizontal|vertical android:widgetCategoryhome_screen /注意targetCellWidth和targetCellHeight只对 Android 12 及以上的桌面格子生效旧版本桌面看的是minWidth和minHeight。两个都写上兼容性最好。6.3 第三步写 Provider 类class DemoWidgetProvider : AppWidgetProvider() { override fun onUpdate( context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray ) { for (appWidgetId in appWidgetIds) { updateWidget(context, appWidgetManager, appWidgetId) } } private fun updateWidget( context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int ) { val views RemoteViews(context.packageName, R.layout.widget_demo) views.setTextViewText(R.id.tv_demo, 来自代码的 xxx 小控件) val intent Intent(context, MainActivity::class.java) val pi PendingIntent.getActivity( context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) views.setOnClickPendingIntent(R.id.widget_root, pi) appWidgetManager.updateAppWidget(appWidgetId, views) } }RemoteViews支持的可点击事件大多用 PendingIntent 实现setOnClickPendingIntent是使用频率最高的一种。记得加FLAG_IMMUTABLEAndroid 12 之后对可变 PendingIntent 限制很严格不加在低版本上可能直接崩溃。6.4 第四步注意 RemoteViews 布局限制桌面小控件用的布局不是普通 Activity 里那种任意 View 都能用的布局。RemoteViews只能支持有限的 View 类型比如FrameLayout、LinearLayout、RelativeLayout、GridLayout、TextView、ImageView、Button、Chronometer等。你如果往里面塞一个自定义 View运行时大概率直接抛异常。所以别想着在小控件里塞一个高仿原生控件设计上能用系统基础 View 拼出来就够用了。等熟悉了基础用法再去研究RemoteViewsService支持的集合型小控件那种可以在桌面展示 ListView、GridView。6.5 顺手验证打包结果写完代码构建 APK用下面的命令检查最终 APK 里的 Manifestaapt2 dump xmltree app-debug.apk --file AndroidManifest.xml然后在输出里搜appwidget能看到你声明的 receiver 和 meta-data说明这个 provider 已经进了包。安装之后长按桌面找到小部件列表就能看到你自己写的那个 xxx 小控件了。个人经验是写桌面小控件最容易翻车的不是功能逻辑而是“以为改了代码就能立刻看到”。小控件更新机制里有太多系统缓存和 Broadcast 派发时机最有效的验证方式就是删掉桌面上的旧小控件重新拖一个上来或者直接重启一次桌面进程。搞清楚“声明决定存在、onUpdate 决定内容、Launcher 缓存决定什么时候刷新”这三件事桌面小控件里那些奇奇怪怪的 xxx基本都绕不过你的手心。
返回列表