《Android开发艺术探索读书笔记》
RemoteView是一个可以在其他进程中显示的View结构。
RemoteViews提供了一组基础的操作用于跨进程更新它的界面。
应用场景
通知栏
桌面小部件
通知栏——NotificationManager
使用RemoteViews
实现自定义视图的效果
桌面小部件——AppWidgetProvider
使用RemoteViews
实现桌面小部件的视图
由于二者的View
(通知栏的自定义视图和桌面小部件的视图)都运行在SystemServer进程中,因此无法直接在Activity
(主进程)中更新View
。
RemoteViews
提供了一系列set
方法来控制跨进程更新界面,解决了二者更新View
的困难。
创建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();
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
等方法。
构造方法
/**
* @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
的内部机制
以通知栏与桌面小部件为例说明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的内部机制有了模糊的理解,接着从代码实现上分析它的工作流程。
只以一个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();
}
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 not 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