【艺术探索笔记】第 5 章 理解 RemoteViews

第 5 章 理解 RemoteViews

  • 一个 View 结构,可以在其他进程显示,提供跨进程更新界面的方法

  • 使用场景:通知栏、桌面小部件


5.1 RemoteViews 的应用

  • 通知栏通过 NotificationManagernotify 方法实现,可自定义布局

  • 桌面小部件通过 AppWidgetProvider 来实现。它本质是个广播

  • 他俩在开发中都用到 RemoteViews ,二者都无法直接去更新 ui

  • RemoteViews 提供一系列 set 方法用来更新界面,这些方法是 View 全部方法的子集。

  • RemoteViews 支持的 View 类型是有限的

5.1.1 RemoteViews 在通知栏上的应用

自定义通知栏弹出通知样式的时候,就用到 RemoteViews 了:

【艺术探索笔记】第 5 章 理解 RemoteViews

5.1.2 RemoteViews 在桌面小部件上的应用

开发桌面小部件的步骤:

  1. 定义小部件布局界面xml

  2. 定义小部件配置信息

    在 res/xml 目录下新建 xml 配置文件,如下

    【艺术探索笔记】第 5 章 理解 RemoteViews

    • initialLayout:指定初始化布局

    • updatePeriodMillis:小工具自动更新周期,最小是 30 分钟

  3. 定义小部件实现类(继承自 AppWidgetProvider

    里边对于桌面小部件的更新都是通过 RemoteViews 来完成的

  4. 在 AndroidManifest.xml 中声明小部件

    【艺术探索笔记】第 5 章 理解 RemoteViews

AppWidgetProvider 中的方法介绍:

  • onEnable: 第一次添加到桌面时调用

  • onUpdate: 小部件被添加时或每次更新时调用。更新时机是由 updatePeriodMillis 指定的

  • onDeleted: 每删除一次就调用一次

  • onDisabled: 最后一个该类型的小部件被删除时调用

  • onReceive: 广播的内置方法,用于分发具体的事件给其他方法。(这里可以处理自己定义的点击事件)

5.1.3 PendingIntent 概述

  • 一种处于 Pending (待定、等待、即将发生) 状态的意图

  • 它和 Intent 的区别是:PendingIntent 是在将来的某个不确定的时刻发生,而 Intent 是立刻发生。

  • 典型使用场景:给 RemoteViews 添加点击事件

  • PendingIntent 通过 send 和 cancel 方法来发送和取消特定的待定 Intent

  • 它支持三种待定意图:

    【艺术探索笔记】第 5 章 理解 RemoteViews

    • 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 方法

    【艺术探索笔记】第 5 章 理解 RemoteViews

    大部分 set 方法是通过反射来完成的

  • RemoteViews 工作过程

    通知栏和桌面小部件分别由 NotificationManager 和 AppWidgetManager 管理,而 NotificationManager 和 AppWidgetManager 通过 Binder 分别和 SystemServer 进程中的 NotificationManagerService 以及 AppWidgetService 进行通信。

    通知栏和桌面小部件中的布局文件实际上是在 NotificationManagerService 以及 AppWidgetService 中被加载的,而它们运行在系统的 SystemServer 中,构成了跨进程通信

  • RemoteViews 内部机制

    1. RemoteViews 会通过 Binder 传递到 SystemServer 进程

      RemoteViews 实现了 Parcelable 接口,因此可以跨进程传输

    2. 在 SystemServer 进程中,通过 LayoutInflater 加载 RemoteViews 中的布局文件

      加载后的布局文件是个普通的 View,只不过相对于我们的进程它是一个 RemoteViews 而已

    3. 系统会对 View 执行一系列界面更新任务

      就是之前用 set 方法提交的。set 方法对 View 所做的更新不是立即执行的,在 RemoteViews 内部会记录所有的更新操作,具体执行时机要等到 RemoteViews 被加载以后才能执行,这样 RemoteViews 就能在 SystemServer 进程中显示了。

      当需要更新 RemoteViews 时,我们需要调用一系列 set 方法并通过 NotificationManager 和 AppWidgetManager 来提交更新任务,具体的更新操作也是在 SystemServer 进程中完成的
      【艺术探索笔记】第 5 章 理解 RemoteViews

  • 为什么系统不通过 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 区别:

    【艺术探索笔记】第 5 章 理解 RemoteViews

    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 中后,对应的资源就不一样了

    【艺术探索笔记】第 5 章 理解 RemoteViews

    这里 appA 扮演的是本地进程;appB 扮演的是 远程进程

    解决方法是:

    • 共同约定布局文件的名称,然后在 appB 中先根据名称拿到该布局在 appB 中的资源 id getResources().getIdentifier("layout_name","layout",getPackageName())

    • 然后加载出来正确的布局getLayoutInflater().inflate(layoutId,parent,false)

    • 调用 RemoteViews#reapply (为什么用 reapply?再看看本章内容)