- RemoteViews的作用是在其他进程中显示并更新View界面,为了更好地理解它的内部
机制,我们先来看一下它的主要功能。
首先我们先看一下它的构造方法
public RemoteViews(String packageName, int layoutId) {
this(getApplicationInfo(packageName, UserHandle.myUserId()), layoutId);
}
构造方法中第一个参数需要传入报名,第二个方法需要传入一个layoutId,RemoteViews目前不支持所有的View,它支持的view如下:
Layout
FrameLayout、LinearLayout、RelativeLayout、GridLayout
View
AnalogClock、Button、Chronometer、ImageButton、ImageView、ProgressBar、TextView、ViewFlipper、ListView、GridView、StackView、AdapterViewFlipper、ViewStub
-RemoteViews不支持他们的子类,同时也不支持自定义View,如果我们在布局中使用EditText会报如下错误:
2019-05-07 23:40:05.012 4642-4642/com.demo.myapplication E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.demo.myapplication, PID: 4642
android.app.RemoteServiceException: Bad notification posted from package com.demo.myapplication: Couldn't expand RemoteViews for: StatusBarNotification(pkg=com.demo.myapplication user=UserHandle{0} id=1 tag=null key=0|com.demo.myapplication|1|null|10093: Notification(pri=0 contentView=com.demo.myapplication/0x7f09001e vibrate=null sound=null defaults=0x0 flags=0x10 color=0x00000000 vis=PRIVATE))
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1650)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:154)
at android.app.ActivityThread.main(ActivityThread.java:6119)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776)
- RemoteViews没有findViewById方法,因此无法直接访问里面的元素而是必须通过一系列的set方法来完成,下面是在源码中截图
这只是其中一部分的方法,原本可以通过view的方法来实现的功能,现在需要调用一系列的set方法来实现,事实上大多数set方法都是通过反射机制来实现的。 - 下面描述一下RemoteViews的内部机制,由于RemoteViews主要用于通知栏和桌面小部件之中,这里就通过它们来分析RemoteViews的工作过程。我们知道,通知栏和桌面小部件分别由NotificationManager和AppWidgetManager管理,而NotificationManager和AppWidgetManager通过Binder分别和SystemServer进程中的NotificationManagerService以及
AppWidgetService进行通信。由此可见,通知栏和桌面小部件中的布局文件实际上是在NotificationManagerService以及AppWidgetService中被加载的,而它们运行在系统的SystemServer中,这就和我们的进程构成了跨进程通信的场景。 - 首先RemoteViews会通过Binder传递到SystemServer进程,这是因为RemoteViews实现了Parcelable接口,因此它可以跨进程传输,系统会根据RemoteViews中的包名等信息去得到该应用的资源。然后会通过LayoutInflater去加载RemoteViews中的布局文件。在SystemServer进程中加载后的布局文件是一个普通的View,只不过相对于我们的进程它是一个RemoteViews而已。接着系统会对View执行一系列界面更新任务,这些任务就是之前我们通过set方法来提交的。set方法对View所做的更新并不是立刻执行的,在RemoteViews内部会记录所有的更新操作,具体的执行时机要等到RemoteViews被加载以后才能执行,这样RemoteViews就可以在SystemServer进程中显示了,这就是我们所看到的通知栏消息或者桌面小部件。当需要更新RemoteViews时,我们需要调用一系列set方法并通过NotificationManager和AppWidgetManager来提交更新任务,具体的更新操作也是在SystemServer进程中完成的。
- 从理论上来说,系统完全可以通过Binder去支持所有的View和View操作,但是这样做的话代价太大,因为View的方法太多了,另外就是大量的IPC操作会影响效率。为了解决这个问题,系统并没有通过Binder去直接支持View的跨进程访问,而是提供了一个Action的概念,Action代表一个View操作,Action同样实现了Parcelable接口。系统首先将View操作封装到Action对象并将这些对象跨进程传输到远程进程,接着在远程进程中执行Action对象中的具体操作。在我们的应用中每调用一次set方法,RemoteViews中就会添加一个对应的Action对象,当我们通过NotificationManager和AppWidgetManager来提交我们
的更新时,这些Action对象就会传输到远程进程并在远程进程中依次执行,这个过程可以参看图5-3。远程进程通过RemoteViews的apply方法来进行View的更新操作,RemoteViews的apply方法内部则会去遍历所有的Action对象并调用它们的apply方法,具体的View更新操作是由Action对象的apply方法来完成的。上述做法的好处是显而易见的,首先不需要定义大量的Binder接口,其次通过在远程进程中批量执行RemoteViews的修改操作从而避免了大量的IPC操作,这就提高了程序的性能,由此可见,Android系统在这方面的设计的确很精妙。
- 上面从理论上分析了RemoteViews的内部机制,接下来我们从源码的角度再来分析RemoteViews的工作流程。它的构造方法就不用多说了,这里我们首先看一下它提供的一系列set方法,比如setTextViewText方法,其源码如下所示。
public void setTextViewText(int viewId, CharSequence text) {
setCharSequence(viewId, "setText", text);
}
- 接下来看setCharSequence方法
public void setCharSequence(int viewId, String methodName, CharSequence value) {
addAction(new ReflectionAction(viewId, methodName, ReflectionAction.CHAR_SEQUENCE, value));
}
addAction中添加了一个ReflectionAction对象,传入了方法名,viewId等参数,接下来看一下addAction方法
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成员,它是一个ArrayList,外界每调用一次set方法,RemoteViews就会为其创建一个Action对象并加入到这个ArrayList中。需要注意的是,这里仅仅是将Action对象保存起来了,并未对View进行实际的操作。
- 接下来我们要看一下RemoteViews的apply方法
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;
}
- 从上面代码可以看出,首先会通过LayoutInflater去加载RemoteViews中的布局文件,
RemoteViews中的布局文件可以通过getLayoutId这个方法获得,加载完布局文件后会通过
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);
}
}
}
- performApply的实现就比较好理解了,它的作用就是遍历mActions这个列表并执行每个Action对象的apply方法。还记得mAction吗?每一次的set操作都会对应着它里面的一个Action对象,因此我们可以断定,Action对象的apply方法就是真正操作View的地方,实际上的确如此
- RemoteViews在通知栏和桌面小部件中的工作过程和上面描述的过程是一致的,当我们调用RemoteViews的set方法时,并不会立刻更新它们的界面,而必须要通过NotificationManager的notify方法以及AppWidgetManager的updateAppWidget才能更新它们的界面。实际上在AppWidgetManager的updateAppWidget的内部实现中,它们的确是通过RemoteViews的apply以及reapply方法来加载或者更新界面的,apply和reApply的区别在于:apply会加载布局并更新界面,而reApply则只会更新界面。通知栏和桌面小插件在初始化界面时会调用apply方法,而在后续的更新界面时则会调用reapply方法。
- 了解了apply以及reapply的作用以后,我们再继续看一些Action的子类的具体实现,首
先看一下ReflectionAction的具体实现,它的源码如下所示
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);
}
}
- 通过上面的代码可以看出,view的设置是通过反射的方式来调用的。