RemoveViews的应用和原理
《Android开发艺术探索读书笔记》
概念
RemoteView是一个可以在其他进程中显示的View结构。
RemoteViews提供了一组基础的操作用于跨进程更新它的界面。
应用场景
-
通知栏
-
桌面小部件
RemoteViews的应用
通知栏——NotificationManager
使用RemoteViews
实现自定义视图的效果
桌面小部件——AppWidgetProvider
使用RemoteViews
实现桌面小部件的视图
由于二者的View
(通知栏的自定义视图和桌面小部件的视图)都运行在SystemServer进程中,因此无法直接在Activity
(主进程)中更新View
。
RemoteViews
提供了一系列set
方法来控制跨进程更新界面,解决了二者更新View
的困难。
RemoteViews在通知栏上的应用
创建RemoteViews
来实现自定义布局,然后设置到notification
中,随后就能通过RemoteViews
的set
方法来更新视图。
RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.layout_notification);
remoteViews.setTextViewText(R.id.msg, "custom_notification");// 设置自定义视图的TextView的值
remoteViews.setImageViewResource(R.id.icon, R.drawable.icon);// 设置自定义视图的ImageView的值
PendingIntent j2Main = PendingIntent.getActivity(this, 0, new Intent(this, MainActivity.class), PendingIntent.FLAG_UPDATE_CURRENT);
remoteViews.setOnclickPendingIntent(R.id.button, j2Main);
Notification notification = new NotificationCompat.Builder(this, "custom_view")
.setCustomContentView(remoteViews)//设置自定义视图
...
.build();
RemoteViews在桌面小部件上的应用
AppWidgetProvider
是Android
提供的用于实现桌面小部件的类,本质是一个广播,即BroadcastReceiver
。
桌面小部件的开发步骤
-
定义小部件界面
R.layout.widget
-
定义小部件配置信息
res/xml/appwidget_provider_info.xml
,包括指定小部件使用的初始化布局,设定小部件的最小尺寸 和 小部件的自动更新周期(单位毫秒)。 -
定义小部件的实现类
AppWidgetProvider
-
在
AndroidManifest.xml
中声明小部件(作为receiver)
小部件实现类常用的方法:
-
onEnabled
:当小部件第一次添加到桌面时被调用,可以添加多次,但只在第一次调用。 -
onUpdate
: 小部件被添加时 或 更新时 都会调用。 -
onDeleted
: 每删除一次小部件就调用一次。 -
onDisabled
: 当最后一个该类型的小部件被删除时调用。 -
onReceive
: 广播的内置方法,用于分发具体的事件给其他类。根据接收到的Intent
中的Action
来分别调用onEnabled
,onUpdate
,onDeleted
,onDisabled
等方法。
RemoteViews的内部机制
RemoteViews的主要功能
构造方法
/**
* @param packageName 当前应用的包名
* @param layoutId 待加载的布局文件
*/
public RemoteViews(String packageName, int layoutId);
RemoteViews
能够支持的View类型有限,也不支持自定义View。现在支持的layout
如下:
-
AdapterViewFlipper
-
FrameLayout
-
GridLayout
-
GridView
-
LinearLayout
-
ListView
-
RelativeLayout
-
StackView
-
ViewFlipper
现在支持的widget
如下:
-
AnalogClock
-
Button
-
Chronometer
-
ImageButton
-
ImageView
-
ProgressBar
-
TextClock
-
TextView
这些类的子类不被RemoteViews
支持。
因为RemoteViews
跨进程显示,所以没有提供findViewById
方法,无法直接访问里面的View元素,必须通过RemoteViews
提供的一系列set
方法。大部分set
方法都是通过反射来完成的。
下面先从理论上分析一下RemoteViews
的内部机制
RemoteViews的内部机制概述
以通知栏与桌面小部件为例说明RemomteViews
的工作进程。
通知栏和桌面小部件分别由NotificationManager
和AppWidgetManager
管理,而NotificationManager
和AppWidgetManager
通过Binder
分别与SystemServer
进程中的NotificationManagerService
和AppWidgetService
进行通信。由此可见,通知栏和桌面小部件中的布局文件实际上是在NotificationManagerService
和AppWidgetService
中被加载的,而他们运行在系统的SystemService
中,这就和我们的进程构成了跨进程通信的场景。
-
在主进程中,创建
RemoteViews
,由于它实现了Parcelable
接口,因此可以通过Binder
跨进程传输到SystemServer
进程。 -
在
SystemServer
进程中,系统通过RemoteViews
携带的包名属性获取应用资源,并加载RemoteViews
携带的布局文件,得到一个View
。这样RemoteViews
就在SystemServer
中完成加载了。 -
如果要对
RemoteViews
进行操作,可以在主进程中调用RemoteViews
提供的set
方法,系统将每一个View
操作对应地封装成一个Action
对象,然后通过Binder
传输到SystemServer
进程中,在RemoteViews
中添加一个Action
对象,当NotificationManager
或AppWidgetManager
提交更新时,RemoteViews
就执行apply
方法来更新View
,这会遍历所有暂存的Action
对象并调用他们的apply
方法来执行具体的View
更新操作。
通过简单的理论介绍,现在对RemoteView的内部机制有了模糊的理解,接着从代码实现上分析它的工作流程。
RemoteViews的内部机制代码实现
只以一个set
方法为例即可,比如setTextViewText
方法。
/*
* @param viewId 被操作的View的Id
* @param text 要为TextView设置的文本
*/
public void setTextViewText(int viewId, CharSequence text) {
setCharSequence(viewId, "setText", text);
}
下面看setCharSequence
方法,methodName
是要调用的方法名,在`setTextViewText
方法中就是"setText"。它将set
操作的具体实现封装成一个ReflectionAction
对象,然后传入addAction
方法。
/*
* @param viewId The id of the view on which to call the method.
* @param methodName 要调用的方法名
* @param value The value to pass to the method.
*/
public void setCharSequence(int viewId, String methodName, CharSequence value) {
addAction(new ReflectionAction(viewId, methodName, ReflectionAction.CHAR_SEQUENCE, value));
}
接下来再看addAction
的实现
/*
* @param a The action to add
*/
private void addAction(Action a) {
if (hasLandscapeAndPortraitLayouts()) {
throw new RuntimeException("RemoteViews specifying separate landscape and portrait layouts cannot be modified. Instead, fully configure the landscape and portrait layouts individually before constructing the combined layout.");
}
if (mActions == null) {
mActions = new ArrayList<Action>();
}
mActions.add(a);
// update the memory usage stats
a.updateMemoryUsageEstimate(mMemoryUsageCounter);
}
可以看到,RemoteViews
有一个实例变量mActions
,用于存储Action
对象。当RemoteViews
调用apply
方法时,所有的Action
被触发执行。RemoteViews
的apply
方法如下:
/*
* @param context Default context to use
* @param parent Parent that the resulting view hierarchy will be attached to. This method
* does <strong>not</strong> attach the hierarchy. The caller should do so when appropriate.
* @return The inflated view hierarchy
*/
public View apply(Context context, ViewGroup parent) {
return apply(context, parent, null);
}
/** @hide */
public View apply(Context context, ViewGroup parent, OnClickHandler handler) {
RemoteViews rvToApply = getRemoteViewsToApply(context);
View result = inflateView(context, rvToApply, parent);
loadTransitionOverride(context, handler);
rvToApply.performApply(result, parent, handler);
return result;
}
从代码中可以看出,首先加载布局文件得到View result,然后通过performApply执行一些更新操作。再看performApply的代码:
private void performApply(View v, ViewGroup parent, OnClickHandler handler) {
if (mActions != null) {
handler = handler == null ? DEFAULT_ON_CLICK_HANDLER : handler;
final int count = mActions.size();
for (int i = 0; i < count; i++) {
Action a = mActions.get(i);
a.apply(v, parent, handler);
}
}
}
这里就是遍历mActions列表并执行每一个Action
对象的apply
方法。这里就是真正操作View
的地方。其他set
方法的原理是一样的。
当通知栏和桌面小程序在使用RemoteViews
实现它们的工作过程的时候,区别无非就是RemoteViews
的apply
方法对应到了NotificationManager
的notify
方法,以及AppWidgetManager
的updateAppWidget
方法。这些方法的内部实现也是调用了RemoteViews
的apply
方法和reapply
方法。
apply
方法和reapplay
方法的区别在于:`apply
会加载布局+更新界面,而reapply
只更新界面。通知栏和桌面小部件在初始化的时候会调用apply
方法,再后续更新的时候则调用reapply
方法。
public void reapply(Context context, View v, OnClickHandler handler) {
RemoteViews rvToApply = getRemoteViewsToApply(context);
if (hasLandscapeAndPortraitLayouts()) {
if ((Integer) v.getTag(R.id.widget_frame) != rvToApply.getLayoutId()) {
throw new RuntimeException("Attempting to re-apply RemoteViews to a view that that does not share the same root layout id.");
}
}
rvToApply.performApply(v, (ViewGroup) v.getParent(), handler);
}
接下来看一下封装set
方法的ReflectionAction
类。
private final class ReflectionAction extends Action {
...
ReflectionAction(int viewId, String methodName, int type, Object value) {
this.viewId = viewId;
this.methodName = methodName;
this.type = type;
this.value = value;
}
...
@Override
public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
final View view = root.findViewById(viewId);
if (view == null) return;
Class<?> param = getParameterType();
if (param == null) {
throw new ActionException("bad type: " + this.type);
}
try {
getMethod(view, this.methodName, param).invoke(view, wrapArg(this.value));//采用反射的方法来更新View
} catch (ActionException e) {
throw e;
} catch (Exception ex) {
throw new ActionException(ex);
}
}
...
}
因为更新View
时最终要执行Action
的apply
操作,也就是这里ReflectionAction
的apply
操作。可以看到,这里采用了反射的方法来执行对View
的更新操作。
setTextViewText
, setBoolean
, setLong
, setDouble
等set
方法都使用了ReflectionAction
,还有其他Action
实现类,比如对应setTextViewSize
的TextViewSizeAction
,它没有用反射来实现,代码如下:
private class TextViewSizeAction extends Action {
...
@Override
public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
final TextView target = root.findViewById(viewId);
if (target == null) return;
target.setTextSize(units, size);
}
}
...
}
之所以不使用反射来实现TextViewSizeAction
,是因为setTextSize
这个方法有2个参数,无法复用ReflectionAction
。
参考资料:
1. 《Android开发艺术探索》
2. https://developer.android.google.cn/reference/android/widget/RemoteViews