尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

第 44 章:小组件、RemoteViews 与 RemoteCompose

第 44 章:小组件、RemoteViews 与 RemoteCompose Android 小组件是平台历史最悠久、架构最具特色的功能之一。与普通应用 UI 不同,小组件的视图层级运行在非创建它的进程中 —— 通常是启动器。这种跨进程渲染需求决定了 RemoteViews、AppWidgetService 以及全新 RemoteCompose 子系统的整体设计。本章逐层梳理完整调用链路:从提供方侧的 AppWidgetProvider,到负责中转更新的系统服务,再到 RemoteViews 的动作序列化与填充流水线,最后讲解未来可能彻底取代 XML 布局方案的 RemoteCompose 引擎。44.1 AppWidget 框架客户端 AppWidget 框架定义在 frameworks/base/core/java/android/appwidget/ 目录下。该框架包含 9 个 Java 文件(并非全部为公开 API,AppWidgetManagerInternal 属于系统内部接口),共同定义了小组件提供方(提供小组件内容的应用)与小组件宿主(展示小组件的应用)之间的交互契约。44.1.1 核心类该框架围绕五个核心类展开:类名作用代码行数AppWidgetProvider提供方使用的 BroadcastReceiver 便捷封装类220AppWidgetHost宿主侧与 AppWidgetService 建立连接751AppWidgetHostView渲染 RemoteViews 的实际视图容器1241AppWidgetManager系统服务客户端代理(单例)1876AppWidgetProviderInfo描述小组件提供方信息的 Parcelable 元数据类647新增类:类名作用代码行数AppWidgetEvent用于统计小组件交互行为的参与度指标401PendingHostUpdate宿主重连时排队等待的更新类型~100AppWidgetConfigActivityProxy跨用户空间配置 Activity 的代理类~100AppWidgetManagerInternalsystem_server 内部 API 接口层~5044.1.2 AppWidgetProvider —— 提供方入口AppWidgetProvider(220 行)继承自 BroadcastReceiver。它只是一个便捷封装类:它实现的全部功能都可以直接使用原始广播接收器完成。其onReceive()方法会根据收到的 Intent 动作,分发给各个钩子回调方法:// frameworks/base/core/java/android/appwidget/AppWidgetProvider.java public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (AppWidgetManager.ACTION_APPWIDGET_ENABLE_AND_UPDATE.equals(action)) { this.onReceive(context, new Intent(intent) .setAction(AppWidgetManager.ACTION_APPWIDGET_ENABLED)); this.onReceive(context, new Intent(intent) .setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE)); } else if (AppWidgetManager.ACTION_APPWIDGET_UPDATE.equals(action)) { Bundle extras = intent.getExtras(); if (extras != null) { int[] appWidgetIds = extras.getIntArray( AppWidgetManager.EXTRA_APPWIDGET_IDS); if (appWidgetIds != null appWidgetIds.length 0) { this.onUpdate(context, AppWidgetManager.getInstance(context), appWidgetIds); } } } else if (AppWidgetManager.ACTION_APPWIDGET_DELETED.equals(action)) { // ...提取单个ID,调用onDeleted() } else if (AppWidgetManager.ACTION_APPWIDGET_OPTIONS_CHANGED.equals(action)) { // ...提取options Bundle,调用onAppWidgetOptionsChanged() } else if (AppWidgetManager.ACTION_APPWIDGET_ENABLED.equals(action)) { this.onEnabled(context); } else if (AppWidgetManager.ACTION_APPWIDGET_DISABLED.equals(action)) { this.onDisabled(context); } else if (AppWidgetManager.ACTION_APPWIDGET_RESTORED.equals(action)) { // ...调用onRestored(),随后调用onUpdate() } }分发逻辑十分直接,需要注意组合广播动作ACTION_APPWIDGET_ENABLE_AND_UPDATE。这是一项较新的优化(由 DeviceConfig 标记COMBINED_BROADCAST_ENABLED控制),它将启用广播与首次更新广播合并为一条,降低小组件启动时延。子类需要重写的钩子回调:回调方法调用时机onUpdate()周期更新定时器触发,或小组件首次绑定onEnabled()该提供方的第一个小组件实例被挂载至任意宿主onDisabled()该提供方最后一个小组件实例从全部宿主移除onDeleted()某一个具体小组件实例被删除onAppWidgetOptionsChanged()小组件尺寸调整或配置项发生变更onRestored()小组件实例从备份恢复(之后会执行 onUpdate)44.1.3 AppWidgetHost —— 宿主入口AppWidgetHost(751 行)是宿主应用访问小组件系统的句柄。例如 Launcher3 会使用固定宿主 ID 1024 创建 AppWidgetHost 实例。该类包含三项关键架构要素:1.IPC 回调 Stub:// frameworks/base/core/java/android/appwidget/AppWidgetHost.java static class Callbacks extends IAppWidgetHost.Stub { private final WeakReferenceHandler mWeakHandler; public void updateAppWidget(int appWidgetId, RemoteViews views) { if (isLocalBinder() views != null) { views = views.clone(); } Handler handler = mWeakHandler.get(); if (handler == null) return; Message msg = handler.obtainMessage(HANDLE_UPDATE, appWidgetId, 0, views); msg.sendToTarget(); } // ... providerChanged、appWidgetRemoved、viewDataChanged等方法 }注意isLocalBinder()判断:当调用来自同一进程(system_server 内部调用自身),必须克隆 RemoteViews,防止可变状态被共享而引发数据损坏。2.基于 Handler 的消息分发:共有 6 类消息从回调 Stub 经由 Handler 分发:常量数值含义HANDLE_UPDATE1收到新的 RemoteViewsHANDLE_PROVIDER_CHANGED2小组件提供方 APK 发生更新HANDLE_PROVIDERS_CHANGED3可用小组件列表发生变更HANDLE_VIEW_DATA_CHANGED4集合类组件数据失效HANDLE_APP_WIDGET_REMOVED5服务端侧小组件实例已移除HANDLE_VIEW_UPDATE_DEFERRED6延迟更新,用于惰性填充监听器注册表(SparseArray):private final SparseArrayAppWidgetHostListener mListeners = new SparseArray();每一个小组件实例由整型appWidgetId唯一标识。监听器接口AppWidgetHostListener定义:updateAppWidget(RemoteViews views)—— 应用新内容onUpdateProviderInfo(AppWidgetProviderInfo)—— 提供方信息变更onViewDataChanged(int viewId)—— 集合组件数据变更updateAppWidgetDeferred(String, int)—— 惰性求值执行路径collectWidgetEvent()—— 采集交互参与度指标3.服务绑定在首次构造时惰性执行:private static void bindService(Context context) { synchronized (sServiceLock) { if (sServiceInitialized) return; sServiceInitialized = true; // 校验FEATURE_APP_WIDGETS特性 IBinder b = ServiceManager.getService(Context.APPWIDGET_SERVICE); sService = IAppWidgetService.Stub.asInterface(b); } }44.1.4 AppWidgetProviderInfo —— 小组件元数据AppWidgetProviderInfo(647 行)是 Parcelable 类,描述小组件各项能力。数据来源于提供方 Manifest 中appwidget-providerXML 元数据标签。关键字段:字段类型说明providerComponentName广播接收器组件标识minWidth/minHeightint最小像素尺寸minResizeWidth/Heightint可调整的最小尺寸maxResizeWidth/Heightint可调整的最大尺寸targetCellWidth/Heightint网格单元格默认尺寸updatePeriodMillisint请求的更新周期(最小 30 分钟)initialLayoutint初始布局资源 IDinitialKeyguardLayoutint锁屏界面使用布局configureComponentName配置 Activity 组件resizeModeint位掩码:RESIZE_HORIZONTAL、RESIZE_VERTICALwidgetCategoryintHOME_SCREEN、KEYGUARD、SEARCHBOX、NOT_KEYGUARDwidgetFeaturesintRECONFIGURABLE、HIDE_FROM_PICKER、CONFIGURATION_OPTIONALgeneratedPreviewCategoriesint支持自动生成预览的分类44.1.5 AppWidgetEvent —— 交互参与度指标AppWidgetEvent(401 行)为新增组件(受frameworks/base/core/java/android/appwidget/flags.aconfig内engagement_metrics标记控制),用于追踪用户与小组件的交互行为:// frameworks/base/core/java/android/appwidget/AppWidgetEvent.java public final class AppWidgetEvent implements Parcelable { public static final int MAX_NUM_ITEMS = 10; private final int mAppWidgetId; private final Duration mVisibleDuration; private final Instant mStart; private final Instant mEnd; private final Rect mPosition; private final int[] mClickedIds; // 最多10个View ID private final int[] mScrolledIds; // 最多10个View ID }Builder 类用于记录可见时间窗口:public Builder startVisibility() { long now = System.currentTimeMillis(); if (now mStart) mStart = now; mLastVisibilityChangeMillis = SystemClock.uptimeMillis(); return this; } public Builder endVisibility() { long now = System.currentTimeMillis(); if (now mEnd) mEnd = now; mDurationMillis += SystemClock.uptimeMillis() - mLastVisibilityChangeMillis; return this; }事件序列被序列化为PersistableBundle,通过AppWidgetHost.reportAllWidgetEvents()上报至 UsageStatsManager。提供方通过AppWidgetManager.queryAppWidgetEvents()查询自身小组件事件(参见 44.10.3 小节)。44.1.6 小组件生命周期44.1.7 AppWidgetHostView —— 视图容器AppWidgetHostView 位于frameworks/base/core/java/android/appwidget/AppWidgetHostView.java,继承 FrameLayout,实现AppWidgetHostListener接口。它是真正嵌入宿主布局树的 View 组件。主要职责:调用updateAppWidget()时填充或增量应用 RemoteViews处理异常状态:提供方进程崩溃时展示错误布局通过AppWidgetEvent.Builder采集交互指标应用颜色资源,支持动态主题根据OPTION_APPWIDGET_HOST_CATEGORY切换锁屏 / 桌面布局updateAppWidget()方法区分首次apply()与后续兼容布局更新reapply():public void updateAppWidget(RemoteViews remoteViews) { // ...空值校验、异常处理 ... if (mView != null) { mView = remoteViews.reapply(/* ... */); } else { mView = remoteViews.apply(/* ... */); } }44.2 AppWidgetService系统侧 AppWidgetService 负责管理全部用户下所有小组件的注册、绑定、更新以及安全策略。实现代码位于frameworks/base/services/appwidget/java/com/android/server/appwidget/目录。44.2.1 服务架构该服务拆分为两个类:类名职责AppWidgetService.java生命周期包装类,注册为 APPWIDGET_SERVICE 系统服务AppWidgetServiceImpl.java实际 IPC 业务实现(约 7500 行)AppWidgetServiceImpl 继承IAppWidgetService.Stub,同时实现WidgetBackupProvider与OnCrossProfileWidgetProvidersChangeListener。44.2.2 内部数据模型服务维护五大核心数据结构,全部由mLock锁保护:// frameworks/base/services/appwidget/java/.../AppWidgetServiceImpl.java private final ArrayListWidget mWidgets = new ArrayList(); private final ArrayListHost mHosts = new ArrayList(); private final ArrayListProvider mProviders = new ArrayList(); private final ArraySetPairInteger, String mPackagesWithBindWidgetPermission = new ArraySet(); private final SparseBooleanArray mLoadedUserIds = new SparseBooleanArray();数据之间构成多对多关系:44.2.3 小组件分配与绑定当宿主请求新建小组件:allocateAppWidgetId()基于每个用户独立的mNextAppWidgetIds分配单调递增整型 ID;创建 Widget 对象并关联调用方 Host。bindAppWidgetIdIfAllowed()或bindAppWidgetId()将小组件绑定到指定 Provider,触发:安全策略校验(调用方需要BIND_APPWIDGET权限,或位于mPackagesWithBindWidgetPermission包白名单)通过DevicePolicyManagerInternal完成跨用户空间校验调度首次ACTION_APPWIDGET_UPDATE广播44.2.4 更新流水线提供方调用AppWidgetManager.updateAppWidget()完整流程:PendingHostUpdate用于处理宿主执行stopListening()(例如 Activity 退后台)场景。当再次调用startListening(),全部排队更新有序送达:public void startListening() { ListPendingHostUpdate updates; updates = sService.startListening(mCallbacks, mContextOpPackageName, mHostId, idsToUpdate).getList(); for (PendingHostUpdate update : updates) { switch (update.type) { case TYPE_VIEWS_UPDATE: updateAppWidgetView(/*...*/); break; case TYPE_PROVIDER_CHANGED: onProviderChanged(/*...*/); break; case TYPE_VIEW_DATA_CHANGED: viewDataChanged(/*...*/); break; case TYPE_APP_WIDGET_REMOVED: dispatchOnAppWidgetRemoved(/*...*/); break; } } }44.2.5 通过 AlarmManager 实现周期更新服务使用 AlarmManager 调度周期更新,强制最小更新间隔 30 分钟,由MIN_UPDATE_PERIOD常量约束:// AppWidgetServiceImpl.java private static final int MIN_UPDATE_PERIOD = DEBUG ? 0 : 30 * 60 * 1000;44.2.6 位图内存上限服务根据显示屏尺寸计算小组件位图内存最大配额:private void computeMaximumWidgetBitmapMemory() { Display display = mContext.getDisplayNoVerify(); Point size = new Point(); display.getRealSize(size); // 1.5 * 4 bytes/pixel * w * h == 6 * w * h mMaxWidgetBitmapMemory = 6 * size.x * size.y; }该限制在 RemoteViews 序列化阶段强制执行。在 1080×2400 分辨率屏幕,配额约为 15.5MB。44.2.7 状态持久化小组件状态持久化写入 XML 文件,路径/data/system/appwidgets.xml;多用户场景下每个用户独立文件位于/data/system_ce/user/目录:private static final String STATE_FILENAME = "appwidgets.xml"; private static final int CURRENT_VERSION = 1;handleSaveMessage()在持有锁的情况下完成状态序列化,随后释放锁再执行磁盘 IO,减少锁竞争。44.2.8 自动生成预览小组件提供方可调用AppWidgetManager.setWidgetPreview()设置自动生成预览(用于小组件选择器的快照)。预览文件存储路径/data/system_ce/user/appwidget/previews/,接口调用存在频率限制:private static final long DEFAULT_GENERATED_PREVIEW_RESET_INTERVAL_MS = Duration.ofHours(1).toMillis(); private static final int DEFAULT_GENERATED_PREVIEW_MAX_CALLS_PER_INTERVAL = 2; private static final int DEFAULT_GENERATED_PREVIEW_MAX_PROVIDERS = 50;44.2.9 交互指标上报reportWidgetEvents()接收宿主上报的AppWidgetEvent[]数组,封装为USER_INTERACTION事件转发给 UsageStatsManager。ReportWidgetEventsJob定时触发采集任务:private static final long DEFAULT_WIDGET_EVENTS_REPORT_INTERVAL_MS = Duration.ofHours(1).toMillis();44.2.10 安全策略与硬性限制设置硬限制防止滥用:限制项数值单个应用包最大宿主数量20单个宿主最大小组件数量200最小更新周期30 分钟每小时自动生成预览 API 最大调用次数2支持预览的最大提供方数量5044.3 RemoteViewsRemoteViews 是 Android 实现跨进程 UI 的核心机制,定义文件frameworks/base/core/java/android/widget/RemoteViews.java(共 11236 行)。它将视图修改描述序列化为 Parcelable 动作对象,通过 Binder 跨进程传输,在接收进程执行填充(apply)。44.3.1 架构总览44.3.2 支持的 View 组件RemoteViews 对可用 View 类做限制;组件必须添加@RemoteView注解。布局容器(ViewGroup):AdapterViewFlipper、FrameLayout、GridLayout、GridView、LinearLayout、ListView、RelativeLayout、StackView、ViewFlipper叶子组件(控件):AnalogClock、Button、Chronometer、ImageButton、ImageView、ProgressBar、TextClock、TextViewAPI 31 新增: CheckBox、RadioButton、RadioGroup、Switch过滤逻辑在填充阶段生效:// RemoteViews.java private static final LayoutInflater.Filter INFLATER_FILTER = (clazz) - clazz.isAnnotationPresent(RemoteViews.RemoteView.class);44.3.3 Action 动作系统每一次对 RemoteViews 对象的修改都会生成一个 Action 对象,追加至内部ArrayListAction:@UnsupportedAppUsage private ArrayListAction mActions;每一个 Action 子类对应唯一序列化 Tag。已定义 Tag 常量:
返回列表