直观地,从RemoteViews名字来看,它类似与一个远程的View,提到远程必然会涉及跨进程的通信。也就是说RemoteViews提供一组基础的操作用于跨进程更新它的界面。RemoteViews在Android中的使用场景有两种,一种是自定义的通知栏,另外一种是桌面小部件。
RemoteViews实际应用
通知栏
在Android应用开发中,通知栏是一个很重要的UI交互。其原理是通过调用NotificationManager的notify方法来实现的。最基本的实现是使用API提供的默认布局,例如
|
|
在Android6.0系统上可以得到扩展后的通知如下,关于更详细的Notification开发不是本篇文章讨论的重点,故不做讨论。
为了满足个性化需求,我们会用到一些复杂布局的通知,这时候就需要我们自定义布局了。对于这类通知最常见的就是下载进度通知,如下图所示:
下面就用代码模拟迅雷下载通知,首先是通知布局
布局中为了美化progressbar缩小了progress的高度和颜色,使得通知中的所有色调都符合产品的统一色调。接下来就是使用RemoteViews来为Notification加载自定义的通知了。
|
|
将remoteViews
赋予notification.contentView
后Notification的视图就将使用RemoteViews来填充了,值得注意的是为remoteViews
中子控件设置属性时,不会通过findViewById
的形式得到子控件对象再去设置属性而是通过RemoteViewsAPI方法来设置相应控件的属性值,以下是RemoteViews API中常用于设置子控件的方法:
- setTextViewText(int viewId, CharSequence text) 为TextView设置文字内容
- setImageViewResource(int viewId, int srcId) 为ImageView设置res
- setOnClickPendingIntent(int viewId, PendingIntent pendingIntent) 为控件添加点击事件
- setProgressBar(int viewId, int max, int progress, boolean indeterminate) 设置ProgressBar
- setViewVisibility(int viewId, int visibility) 视图可见性
RemoteViews没有对所有的控件都提供设置的方法(RemoteViews所支持的View请移步第二部分),所以在开发中尽量避免在通知栏中使用复杂的控件。为什么RemoteViews框架不提供一个findViewById方法来获取每个子View呢?这样也省得调用remoteviews提供的残缺方法了,这里先挖个坑,稍后再填。
桌面小部件
广播android.content.BroadcastReceiver
子类中包含android.appwidget.AppWidgetProvider
类,该类是实现桌面小部件的核心类。该类的核心就是发送广播,接收广播,更新小部件,这时候想一想安卓桌面上的时钟,是不是突然开窍了,通过每一秒都来接收广播达到实时更新的效果。
对于开发桌面小部件一般可以分为以下几个步骤:
1.构造小部件的桌面布局。这里模仿网易云音乐并且模拟实现歌曲进度条和红心的更新操作,最后实现的效果如下
|
|
该文件命名为res/layout/music_widget.xml
2.配置小部件信息。在res/xml/appwidget_provider_info.xml配置如下
从参数名字可以推断出initialLayout
是指初始化小部件;minHeight
和minWidth
设置小部件的最小宽高值;对于updatePeriodMillis
是定义小部件自动更新的周期,单位为毫秒。
3.定义小部件的实现类。 这部分是实现桌面小部件的核心,所有的操作更新逻辑都是从这里AppWidgetProvider
类开始的,首先我们自定义一个类MusicWidgetProvider
继承自AppWidgetProvider
,并且重写onReceive
和onUpdate
方法。onReceive
方法是接收广播的逻辑,本节开头介绍AppWidgetProvider
类时,我们就知道了它继承了BroadcastReceiver
所以自热而然的就具有接收广播的能力;onUpdate
方法是在小部件每次更新时会回调一次,我们可以在该方法中为子控件指定click
监听并触发广播。这样一来就可以在两个方法中形成一个回路从而达到更新自己的效果。具体代码如下:
4.在AndroidManifes.xml中声明小部件。 因为AppWidgetProvider
是一个广播接收者所以要在AndroidManifest
中声明
对于前三个action我们明白是注册代码中定义的广播,最后一个action则是系统规范必须要这么写。
除此之外,AppWidgetProvide
还提供了一些其他重要的覆写方法,具体的调用时机如下表所示:
方法 | 调用时机 |
---|---|
onEnable | 当小部件第一次添加到桌面时被调用 |
onUpdate | 小部件被添加时或者每次更新时亦或是在updatePeriodMillis定义的一个周期后也会调用 |
onDeleted | 每次删除(移除)小部件时会调用 |
onDisabled | 当最后一个该类型的桌面小部件移除时会调用 |
至此,对于RemoteViews的两个实际用途就已经介绍完了,在下节中会探讨RemoteViews的内部机制。
RemoteViews内部机制
通过上节的两个例子可以看出在初始化RemoteViews时会传入两个参数一个是packageName另外一个是layoutId
packageName是当前应用的包名,而layoutId是待加载的布局文件。由于RemoteViews是在其他进程中显示UI和更新UI的。这样势必就要受到一些限制,例如在上节中实践过的,不能通过findViewById等普通View所具有的方法。除此之外,RemoteViews不能支持全部的View子类,它仅支持如下类型:
- ViewGroup
- FrameLayout
- LinearLayout
- RelativeLayout
- GridLayout
- View
- AnalogClock
- Button
- Chronometer
- ImageButton
- ImageView
- ProgressBar
- TextView
- ViewFlipper
- ListView
- GridView
- StackView
- AdapterViewFlipper
- ViewStub
对于上述View的子类型RemoteViews是不支持的,对于自定义的View和android.widget.EditText
也都是不允许在RemoteViews中使用的。
RemoteViews原理简述
从第一部分的两个例子的实践来看RemoteViews的更新是通过setxxx的方法完成的。事实上,这些set方法是通过反射来实现的。
对于自定义通知栏和桌面小部件二者是通过NotificationManager
和AppWidgetManager
来管理的,而它们又是通过Binder
分别和SystemServer
进程中的NotificationManagerService
和AppWidgetService
进行通信的,由此可见RemoteViews
的加载与更新是在SystemServer
中完成的。因此RemoteViews
必须具备跨进程间通信等特征,如实现Parcelable
接口。
这时候也就明白了RemoteViews
构造方法中传入包名的目的就是为了在SystemServer
进程中加载该应用的资源,然后通过LayoutInflater
去加载RemoteViews
中的布局文件。这时候在远程就完成了RemoteViews
的加载过程。
回到本地,当一系列事件后需要更新RemoteViews
的时候。会通过set的反射方法进行更新。从理论上View的所有方法都是可以通过Binder进行传递从而到达SystemServer
进行更新的,但是这样难免带来额外的开销,造成界面的卡顿而得不偿失。这时候我们引入Action对象,它同样实现了Parcelable
接口,可以IPC进行传递。把对View的每一步操作封装到Action中,然后通过NotificationManager
和AppWidgetManager
来提交,这样就通过Binder将Action传递到SystemServer
进程中。到了SystemServer
进程就可以通过RemoteViews
的apply
和reapply
进行更新了。
以上就是RemoteViews
的工作原理,下面通过代码可以细细品味一下。
源码分析
这一节我们分两条线进行切入首先从API调用开始再次通过AppWidgetManager进行更新操作进行代码跟进,这两条线应该有一个汇聚。首先这里我们选取setTextViewText
方法进行切入。
|
|
上述代码没得说,这里我们来具体观察下addAction
和RelfectionAction
。
|
|
RemoteViews
维护着mAction
的全局变量,每次进行更新操作的时候都会将一个Action
添加进去,然后就没有然后了(这里仅仅是保存在了mAction中,我们不知道谁去将它进行实质性的操作)。下面再看看RelfectionAction
|
|
ReflectionAction
中封装了viewId
,methodName
,type
以及value
并且重写了apply
方法。至今我们还不知道这个apply
的作用,没关系,我们看一看RemoteViews
中的performApply
方法做了些什么。
|
|
很明显更新操作是从这里发出的(当然这是我们假设的因为还没有去验证,如果验证的话就得从NotificationManager
和AppWidgetManager
中找调用该方法的代码了,这也正是即将进行跟进分析的第二条线)遍历每一次的更新并调用Action
的apply
方法。这时候再往回看RelfectionAction
的apply
方法,正是通过反射机制来实现View的操作的。
这时候我们的setTextViewText
方法实现原理就了然于胸了,其他方法大致过程亦如此,这里不再赘述。
下面我们来看第二条线,从正面切入,就拿桌面小部件例子来看,每次更新RemoteViews
都是从AppWidgetManager.updateAppWidget
方法开始的。
|
|
上述三种重载方法都是更新RemoteViews
的方法,接下来再看mService.updateAppWidgetProvider
方法,mService
是一个远程接口IAppWidgetService
。代码跟到这就要进行IPC,故我们找到远程服务所对应的updateAppWidgetProvider
方法。该方法位于源码目录:frameworks/base/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
|
|
|
|
代码读到这里,我们发现从本地传进来的RemoteViews
对象被赋予了远程进程中的widget.views
并且调用scheduleNotifyUpdateAppWidgetLocked
方法,接下来再看看该方法
|
|
好吧,又拐弯了!
|
|
沿着这条线继续!
|
|
终于的终于,找到了更新RemoteViews
真正的类和方法callbacks.updateAppWidget(appWidgetId, views)
,看到IAppWidgetHost
又是一个远程接口,我们找到实现它的远程服务AppWidgetHost.Callbacks
|
|
这里将Remoteviews
克隆了一份赋予views
并且通过Handler
转发,我们看一下转发后的具体操作
|
|
这里出现了AppWidgetHostView
类,估计它就是最终的操作类了。
|
|
好了终于明了了,首先通过layoutId
来匹配是否来自同一个RemoteViews
如果匹配上则说明此次是更新操作否则就是第一次调用该方法即第一次来填充RemoteViews
视图,在这两种条件下我们分别对应调用RemoteViews
的reapply
和apply
方法来更新和填充RemoteViews
。现在已经真相大白了,也验证了我们之前的假设在apply
加载在reapply
中进行更新。
通过从API操作和Manager正面更新两条线的代码跟进分析,我们已经熟悉了RemoteViews
的工作原理,相对来说还是比较简单的,但它的思想是值得学习的,小小的RemoteViews
机制也构成了复杂的Android框架的一部分,它运用了java反射机制、IPC本地与远程进程间的通信、并且多次使用Handler进行Message转发操作等一些关键的知识点,这些都是值得每一个开发者熟练掌握并运用。