【艺术探索笔记】第 5 章 理解 RemoteViews
第 5 章 理解 RemoteViews
一个 View 结构,可以在其他进程显示,提供跨进程更新界面的方法
使用场景:通知栏、桌面小部件
5.1 RemoteViews 的应用
通知栏通过
NotificationManager
的notify
方法实现,可自定义布局桌面小部件通过
AppWidgetProvider
来实现。它本质是个广播他俩在开发中都用到 RemoteViews ,二者都无法直接去更新 ui
RemoteViews 提供一系列 set 方法用来更新界面,这些方法是 View 全部方法的子集。
RemoteViews 支持的 View 类型是有限的
5.1.1 RemoteViews 在通知栏上的应用
自定义通知栏弹出通知样式的时候,就用到 RemoteViews 了:
5.1.2 RemoteViews 在桌面小部件上的应用
开发桌面小部件的步骤:
定义小部件布局界面xml
-
定义小部件配置信息
在 res/xml 目录下新建 xml 配置文件,如下
initialLayout:指定初始化布局
updatePeriodMillis:小工具自动更新周期,最小是 30 分钟
-
定义小部件实现类(继承自
AppWidgetProvider
)里边对于桌面小部件的更新都是通过 RemoteViews 来完成的
-
在 AndroidManifest.xml 中声明小部件
AppWidgetProvider 中的方法介绍:
onEnable: 第一次添加到桌面时调用
onUpdate: 小部件被添加时或每次更新时调用。更新时机是由
updatePeriodMillis
指定的onDeleted: 每删除一次就调用一次
onDisabled: 最后一个该类型的小部件被删除时调用
onReceive: 广播的内置方法,用于分发具体的事件给其他方法。(这里可以处理自己定义的点击事件)
5.1.3 PendingIntent 概述
一种处于 Pending (待定、等待、即将发生) 状态的意图
它和 Intent 的区别是:PendingIntent 是在将来的某个不确定的时刻发生,而 Intent 是立刻发生。
典型使用场景:给 RemoteViews 添加点击事件
PendingIntent 通过 send 和 cancel 方法来发送和取消特定的待定 Intent
-
它支持三种待定意图:
-
requestCode
PendingIntent 发送方请求码,多数情况下是 0 ;requestCode 会影响到 flags 的效果
-
flags
-
FLAG_ONE_SHOT
当前 PendingIntent 只能被使用一次,然后被自动 cancel。如果后续还有
相同的
PendingIntent , 他们的 send 会调用失败。(通知栏消息同类通知只能使用一次,后续的无法点击打开) -
FLAG_NO_CREATE
当前 PendingIntent 不会主动创建,如果之前它不存在,那么它的 getActivity、getService、getBroadcast 会返回 null。此标记为很少见,实际开发中没有使用意义
-
FLAG_CANCEL_CURRENT
当前 PendingIntent 如果已经存在,那么会被 cancel 并且系统会创建一个新的 PendingIntent。(通知栏消息被 cancel 的将无法打开)
-
FLAG_UPDATE_CURRENT
当前 PendingIntent 如果已经存在,他们都会被更新,即 Intent 中的 Extras 会被替换成最新的
-
-
-
相同的 PendingIntent ?
-
内部 Intent 相同
Intent 的 ComponentName 相同
Intent 的 intent-filter 相同
Intent 的 Extra 不参与匹配过程,它相同不相同不影响匹配过程
requestCode 相同
-
-
以通知栏消息为例,分析一下 PendingIntent 各标记位的不同
manager.notify(1,notification)
-
notify 第一个参数 id 是常量
多次调用只会弹出一个通知,之前的会被替换掉
-
notify 第一个参数 id 每次调用都不同
-
PendingIntent 不匹配时
不管用什么标记为,通知都互相不会干扰
-
PendingIntent 匹配时
-
FLAG_ONE_SHOT
后续通知中 PendingIntent 会和第一条通知保持一致,包括 Extra
-
FLAG_CANCEL_CURRENT
只有最新一条通知能打开,之前的都不能打开
-
FLAG_UPDATE_CURRENT
之前弹出的通知的 PendingIntent 会被更新,和最新一条通知的 PendingIntent 保持一致,包括 Extra,都可以打开
-
-
-
5.2 RemoteViews 的内部机制
常用构造方法 : public RemoteViews(String packageName, int layoutId)
packageName:当前包名
-
layoutId:待加载的布局文件
只支持有限的 View 类型
-
Layout
FrameLayout、LinearLayout、RelativeLayout、GridLayout
-
View
AnalogClock、Button、Chronometer、ImageButton、ImageView、ProgressBar、TextView、ViewFlipper、ListView、GridView、StackView、AdapterViewFlipper、ViewStub、
TextClock 也是支持的,详情看[官方文档](https://developer.android.com/reference/android/widget/RemoteViews)
-
-
RemoteViews 的部分 set 方法
大部分 set 方法是通过反射来完成的
-
RemoteViews 工作过程
通知栏和桌面小部件分别由 NotificationManager 和 AppWidgetManager 管理,而 NotificationManager 和 AppWidgetManager 通过 Binder 分别和 SystemServer 进程中的 NotificationManagerService 以及 AppWidgetService 进行通信。
通知栏和桌面小部件中的布局文件实际上是在 NotificationManagerService 以及 AppWidgetService 中被加载的,而它们运行在系统的 SystemServer 中,构成了跨进程通信
-
RemoteViews 内部机制
-
RemoteViews 会通过 Binder 传递到 SystemServer 进程
RemoteViews 实现了 Parcelable 接口,因此可以跨进程传输
-
在 SystemServer 进程中,通过 LayoutInflater 加载 RemoteViews 中的布局文件
加载后的布局文件是个普通的 View,只不过相对于我们的进程它是一个 RemoteViews 而已
-
系统会对 View 执行一系列界面更新任务
就是之前用 set 方法提交的。set 方法对 View 所做的更新不是立即执行的,在 RemoteViews 内部会记录所有的更新操作,具体执行时机要等到 RemoteViews 被加载以后才能执行,这样 RemoteViews 就能在 SystemServer 进程中显示了。
当需要更新 RemoteViews 时,我们需要调用一系列 set 方法并通过 NotificationManager 和 AppWidgetManager 来提交更新任务,具体的更新操作也是在 SystemServer 进程中完成的
-
-
为什么系统不通过 Binder 去支持所有 View 和 View 操作?
代价大,View 方法太多了,需要定义大量的 Binder 接口
大量的 IPC 操作(频繁更新单个 View)会影响效率
-
系统的做法:提供了 Action 的概念,Action 代表一个 View 操作,它也实现了 Parcelable 接口
系统将 View 的操作封装到 Action 对象并跨进程传输到远程进程,然后再远程进程中执行 Action 中的具体操作
RemoteViews 每调用一次 set 方法,就会在它里边添加一个 对应的 Action 对象,当通过 NotificationManager 和 AppWidgetManager 提交更新时,这些 Action 会传输到远程进程并依次执行。
远程进程 -> RemoteViews#apply -> 遍历所有 Action 并调用 Action#apply
实现了批量更新
-
apply 和 reapply 区别:
apply:加载布局并更新界面
reapply:只会更新界面
ReflectionAction 看源码可以看出来,表示的是一个反射动作,通过它对 View 的操作会以反射的方式来调用
还有很多 Action ,可以看源码研究一下
-
点击事件
-
setOnClickPendingIntent
普通 View 设置点击事件
-
setPendingIntentTemplate、setOnClickFillInIntent
组合使用,给 ListView 和 StackView 的 item 添加点击事件
-
5.3 RemoteViews 的意义
-
同一个 app 不同进程间需要更新界面
如果都是 RemoteViews 支持的 View,用起来就很好,如果有自定义 View 或者 RemoteViews 不支持的 View ,就要另想办法(比如 AIDL)
-
不同 app 之间更新界面
这里会有一个问题,appA的资源 id 传到 appB 中后,对应的资源就不一样了
这里 appA 扮演的是本地进程;appB 扮演的是 远程进程
解决方法是:
共同约定布局文件的名称,然后在 appB 中先根据名称拿到该布局在 appB 中的资源 id
getResources().getIdentifier("layout_name","layout",getPackageName())
然后加载出来正确的布局
getLayoutInflater().inflate(layoutId,parent,false)
调用 RemoteViews#reapply (为什么用 reapply?再看看本章内容)