RemoteViews的内部机制
RemoteViews
的作用是在其他进程中显示并更新View界面.
最常用的构造函数就是public RemoteViews(String packageName, int layoutId)
, 注意RemoteViews
目前并不能支持所有的View类型, 目前支持如下(不包括其子类):
Layout
FrameLayout
, LinearLayout
, RelativeLayout
, GridLayout
View
TextView
, ImageView
, ImageButton
, Button
, AnalogClock
, Chronometer
, ProgressBar
, ViewFlipper
, ListView
, GridView
, StackView
, AdapterViewFlipper
, ViewStub
RemoteViews
没有提供findviewById()
方法, 只有一系列的set()
方法.
方法名 | 作用 |
---|---|
setTextViewText() |
设置TextView的文本 |
setTextViewSize() |
设置TextView的字体大小 |
setTextColor() |
设置TextView的字体颜色 |
setImageViewResource() |
设置imageView的图片资源 |
setImageViewBitmap() |
设置imageView的图片 |
setInt() |
反射调用View对象的参数类型为int的方法 |
setLong() |
反射调用View对象的参数类型为long的方法 |
setBoolean() |
反射调用View对象的参数类型为boolean的方法 |
setOnClickPendingIntent() |
为View添加单击事件, 事件类型只能PendingIntent |
RemoteViews的工作流程
通知栏和桌面小部件分别由NotificationManager
和AppWidgetManager
管理, 而这两个管理者都是通过Binder
分别和SystemServer
进程中的NotificationManagerService
以及AppWidgetService
进行通信. 由此可见,通知栏和桌面小部件中的布局文件实际上是在NotificationManagerService
以及AppWidgetService
中被加载的, 而他们运行在系统的SystemServer
中, 这就和我们的进程构成了进程间通信.
最开始RemoteViews
会通过Binder
传递到SystemServer
进程, RemoteViews
实现了Parcelable
接口. 系统根据RemoteViews
中的包名等信息去得到该应用的资源, 然后通过LayoutInflate
去加载RemoteViews
中的布局文件. 在SystemServer
进程中加载后的布局文件是一个普通的View, 只不过相对于我们的进程他是一个RemoteViews
而已. 接着系统会对View执行一系列界面更新任务, 这些任务就是之前的设置的set()
. set方法对View所做的更新不是立即执行, 在RemoteViews
内部会记录所有的更新操作, 具体的执行时机要等到RemoteViews
被加载以后才能执行, 这样RemoteViews
就可以在SystemServer
进程中显示, 这就是我们看到的通知栏或者桌面小部件. 当需要更新RemoteViews
时, 我们需要调用set
方法并通过NotificationManager
和AppWidgetManager
来提交更新任务, 具体的更新操作也是在SystemServer
进程中完成的.
为什么不支持所有的View和其操作? 因为代价太大, View的方法太多, 另外就是大量的IPC操作会影响效率. 为了解决这个问题, 系统并没有通过Binder直接支持View的跨进程访问, 而是提供了一个Action
的概念, Action
代表一个View操作, Action
同样实现了Parcelable
接口. 系统首先将View操作封装到Action
对象并将这些对象跨进程传输到远程进程, 接着在远程进程中执行Action
对象中的具体操作. 在我们的应用中每调用一次set()
, RemoteViews
中就会添加一个对应的Action
对象, 当我们通过NotificationManager
和AppWidgetManager
来提交我们的更新时, 这些Action对象就会传输到远程进程并在远程进程中一次执行. 如图
[图片上传失败...(image-f43de0-1599738952325)]
远程进程通过RemoteViews
的apply方法来进行View的更新操作, RemoteViews
的apply方法内部则会去遍历所有的Action
对象并调用他们的apply
方法, 具体的View更新操作是由Action对象的apply方法来完成的. 上述做法的好处是显而易见的, 首先不需要定义大量的Binder
接口, 其次通过远程进程中批量执行RemoteViews
的修改操作从而避免了大量的IPC操作, 这就提高了程序的性能.
接下来从源码角度分析.
首先最长用到的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));
}
从这里实现看到, 内部并没有对View进程直接的操作, 而是添加一个ReflectionAction()
一个看名字类似反射类型的对象. 接下看addAction()
private void addAction(Action a) {
//省略部分代码...
if (mActions == null) {
mActions = new ArrayList<Action>();
}
mActions.add(a);
// update the memory usage stats
a.updateMemoryUsageEstimate(mMemoryUsageCounter);
}
这里看到, 在RemoteViews
内部有一个mActions
成员, 它是一个ArrayList, 外界每调用一次set()
, RemoteViews
就会为其创建一个Action
对象并加入到这个集合中, 这里仅仅将Action
对象保存了起来, 并未对View进行实际的操作, 这一点在上面的理论分析中已经提到过.
接下来再看ReflectionAction
的实现之前, 先看一下RemoteViews
的apply()
方法以及Action类的实现.
public View apply(Context context, ViewGroup parent, OnClickHandler handler) {
RemoteViews rvToApply = getRemoteViewsToApply(context);
View result;
final Context contextForResources = getContextForResources(context);
Context inflationContext = new ContextWrapper(context) {
@Override
public Resources getResources() {
return contextForResources.getResources();
}
@Override
public Resources.Theme getTheme() {
return contextForResources.getTheme();
}
@Override
public String getPackageName() {
return contextForResources.getPackageName();
}
};
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;
}
这段代码首先通过LayoutInflate
去加载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);
}
}
}
这个实现就是遍历mActions
并执行每个Action
对象的apply()
方法, 这里猜想Action对象的apply方法就是真正操作View的地方.
RemoteViews
在通知栏和桌面小部件中的工作过程和上面描述的过程是一致的. 当调用了RemoteViews
的set方法时, 并不会立刻更新他们的界面, 而必须要通过NotificationManager
的notify
方法以及AppWidgetManager
的updateAppWidget
才能更新他们的界面. 实际上在AppWidgetManager
的updateAppWidget
内部实现中, 他们就是通过RemoteViews
的apply
以及reapply
方法来加载或者更新布局的. apply
和reApply
的区别在于:前者会加载布局并更新界面, 而后者只会更新界面. 通知栏和桌面小部件在初始化界面的时候回调用apply()
方法, 而在后续的更新界面时则会调用reapply()
方法.
了解了apply()
以及reapply()
的作用后, 接着看Action
的子类具体实现, 先看ReflectionAction
的具体实现.
private final class ReflectionAction extends Action {
//省略部分代码 ...
String methodName;
int type;
Object value;
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
表示的是一个反射动作, 通过它对View的操作会以反射的方式来调用, 其中getMethod
就是根据方法名来得到反射所需要的Method对象
. 除了ReflectionAction
, 还有其他的Action
. 例如: TextViewSizeAction
, ViewPaddingAction
, SetOnClickPendingIntent
等. 看一下TextViewSizeAction
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;
}
这个类没有使用反射, 因为setTextSize的方法有两个参数,因此无法复用ReflectionAction
, 因为这个反射调用只能有一个参数.
关于单击事件, RemoteViews
只支持发起PendingIntent
,不支持onClickListener()
这种模式.
setOnClickPendingIntent
,setPendingIntentTemplate
,setOnClickFillIntent
这三个的区别.
setOnClickPendingIntent
: 只支持普通View设置点击事件, 不能给集合(ListView
,StackView
)中的View设置点击事件,如item. 因为开销比较大, 系统禁止了这种方式. 如果要给集合中的item添加点击事件,则必须使用后两种组合使用才可以.
RemoteViews的意义可以模拟一个通知栏效果并实现跨进程的UI更新
参看文章
《Android 开发艺术探索》书集
《Android 开发艺术探索》 05-理解RemoteViews