Android RemoteViews

RemoteVies在自定义通知栏布局和桌面Widget的开发中扮演着重要的角色。

RemoteViews 顾名思义,远程 View,RemoteViews 表示的是一个 View 结构,他可以在其他进程中显示,由于他在其他进程中显示,为了能够更新它的界面。RemoteViews 提供了一组基础的操作用于跨进程更新它的界面。自定义通知栏布局和桌面Widget更新界面时无法像 Activity 里面那样去直接更新 View,这是因为他们的界面运行在其他进程中,确切来说是系统的 SystemServer 进程。为了提供更新界面,RemoteViews 提供了一系列的 set 方法。

最常用的构造方法:

1
public RemoteViews(String packageName , int layoutId)

第一个参数是包名,第二个参数是布局文件的资源id;

注意:RemoteView 并不支持所有的 View,只支持部分 View ( 因为更新 UI 是通过多进程的,所以出于性能考虑)

  • 下面列举了一些常用的 set 方法:
方法名 作用
setTextViewText(int viewId, CharSequence text) 设置 TextView 的文本
setTextViewTextSize(int viewId, int units, float size) 设置 TextView 的字体大小
setTextColor(int viewId, int color) 设置 TextView 的字体颜色
setImageViewResource(int viewId, int srcId) 设置 ImageView 的图片资源
setInt(int viewId, String methodName, int value) 反射调用 View 对象的参数类型为 int 的方法
setLong(int viewId, String methodName, long value) 反射调用 View 对象的参数类型为 long 的方法
setBoolean(int viewId, String methodName, boolean value) 反射调用 View 对象的参数类型为 boolean 的方法
setOnClickPendingIntent(int viewId, PendingIntent pendingIntent) 为 View 添加单击事件,事件类型只能为PendingIntent

RemoteView 的内部机制

  • 首先 RemoteViews 会通过 Binder 传递到 SystemSever 进程,这是因为 RemoteViews 实现了 Parcelable 接口,因此可以跨进程传输
  • 系统会根据 RemoteViews 中的包名等信息去得到该应用的资源。
  • 通过 LayoutInflater 去加载 RemoteViews 中的布局文件。
  • 接着系统会对 View 执行一系列界面更新任务,这些任务就是之前我们通过 set 方法来提交的。set 方法对View 的操作不是立刻执行的,在 RemoteViews 内部会记录所有的更新操作,等到 RemoteViews 被加载以后才能执行。

理论上,系统完全可以通过 Binder 去支持所有的 View 和 View 操作,但这样做代价太大,因为 View 的方法太多了,另外就是大量的 IPC 操作会影响效率。为了解决这个问题,系统并没有通过 Binder 去直接支持 View 的跨进程访问,而是提供了一个 Action 的概念,Action 代表一个 View 的操作,Action 同样实现了 Parcelable 接口。系统首先将 View 操作封装到 Action 对象并将这些对象跨进程传输到远程进程,接着远程进程中执行 Action 对象中的具体操作。

例如看 setTextViewText 方法的实现:

1
2
3
public void setTextViewText(int viewId, CharSequence text) {
setCharSequence(viewId, "setText", text);
}

1
2
3
public void setCharSequence(int viewId, String methodName, CharSequence value) {
addAction(new ReflectionAction(viewId, methodName, ReflectionAction.CHAR_SEQUENCE, value));
}
1
2
3
4
5
6
7
8
9
10
11
private void addAction(Action a) {

...

if (mActions == null) {
mActions = new ArrayList<Action>();
}
mActions.add(a);

...
}

可以看到 RemoteViews 内部有个 mActions 成员,用于存储每次 set 的 Action 的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public View apply(Context context, ViewGroup parent, OnClickHandler handler) {
RemoteViews rvToApply = getRemoteViewsToApply(context);

View result;

...

LayoutInflater inflater = (LayoutInflater)
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

// Clone inflater so we load resources from correct context and
// we don't add a filter to the static version returned by getSystemService.
inflater = inflater.cloneInContext(inflationContext);
inflater.setFilter(this);
result = inflater.inflate(rvToApply.getLayoutId(), parent, false);

rvToApply.performApply(result, parent, handler);

return result;
}
1
2
3
4
5
6
7
8
9
10
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);
}
}
}

RemoteViews 的 apply 方法是来进行 View 的更新操作的。可以看到,RemoteViews 的 apply 方法内部会遍历调用所有 Action 对象的 apply 方法,具体的 View 更新操作是由 Action 对象的 apply 方法来完成的。
至于 apply 是怎么被调用的,例如当我们在小部件开发中调用 AppWidgetManager 或者通知栏的NotificationManager 的 notify 方法,他们的确是通过 RemoteViews 的 apply 以及 reapply 方法来加载或者更新界面的。

apply 和 reapply 的区别在于:

  • apply 会加载布局并更新界面。
  • reApply 则只会更新界面。

通知栏和桌面小插件在初始化界面时会调用 apply 方法,而在后续的更新界面时则会调用 reapply 方法。

下面看一些 Action 的子类的具体实现:

  • ReflectionAction
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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));
} catch (ActionException e) {
throw e;
} catch (Exception ex) {
throw new ActionException(ex);
}
}

...

}

ReflectionAction 表示的是一个反射动作。使用 ReflectionAction 的 set 方法有:setTextViewText、setBoolean、setLong、setDouble等等。

除了 ReflectionAction 还有其他的 Action,下面来看看 TextViewSizeAction:

  • TextViewSizeAction
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private class TextViewSizeAction extends Action {
public TextViewSizeAction(int viewId, int units, float size) {
this.viewId = viewId;
this.units = units;
this.size = size;
}

...

@Override
public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
final TextView target = (TextView) root.findViewById(viewId);
if (target == null) return;
target.setTextSize(units, size);
}

public String getActionName() {
return "TextViewSizeAction";
}

int units;
float size;

public final static int TAG = 13;
}

TextViewSizeAction 的实现比较简单,他之所以不用反射来实现,是因为 setTextSize 这个方法有2个参数,因此无法复用 ReflectionAction,因为 ReflectionAction 反射调用只有一个参数。

文章整理自「Android开发艺术探索」