1. 应用场景
- 通知栏
- 桌面小控件
2. RemoteViews 原理
2.1 RemoteViews 不是一个 View
public class RemoteViews implements Parcelable, Filter {
}
从类的定义能够看出 RemoteViews 实际上并不是一个 View,它只是一个实现了 Parcelable
接口的类,为什么要实现 Parcelable 接口呢,这是因为我们在跨进程更新布局的时候需要传递 RemoteViews,而跨进程通信中传递的数据除了基本数据类型外,对于自定义的数据类型一定要实现 Parcelable 接口才能在进程间实现数据的传递。
2.2 RemoteViews 不支持自定义的布局和控件
RemoteViews 支持的布局和控件是有限的,支持的布局有:
FrameLayout
GridLayout
GridView
LinearLayout
ListView
RelativeLayout
StackView
ViewFlipper
AdapterViewFlipper
支持的控件有:
Button
ImageButton
TextView
TextClock
ImageView
ProgressBar
Chronometer
AnalogClock
-
注意
在 RemoteViews 的 XML 布局中使用了 ImageView
,按道理来说 RemoteViews 是支持的。实际应用中,在 AppCompatActivity
布局中使用的 ImageView 等会在加载布局时被转换成对应的兼容类即 AppCompatImageView
,兼容类都是 ImageView 的子类,有如下错误提示:
Caused by: android.widget.RemoteViews ActionException:android.widget.RemoteViews$ActionException:
view:android.support.v7.widget.AppCompatImageView can't use method with RemoteViews: setImageResource(int)
错误提示表明在 RemoteViews 中 AppCompatImageView 不能使用 setImageResource() 方法,这种情况下我们将 AppCompatActivity 改为 Activity
,不使用 AppCompatActivity 就可以避免将布局中的 ImageView 转换为 AppCompatImageView,或者在 build.gradle 中设置 appcompat 为 compile ‘com.android.support:appcompat-v7:23.0.1’
或更低。
原因:IamgeView 的 setImageResource() 方法有注解 @android.view.RemotableViewMethod(asyncImpl="setImageResourceAsync")
这表明它是支持在 RemoteViews 中使用 setImageResources() 的,但 AppCompatImageView 虽然是 ImageView 的子类, 重写了 setImageResources() 方法,但是 23.0.1
以后的 AppCompatImageView 的 setImageResources() 方法并没有以上的注解,所以在 RemoteViews 中使用 AppCompatImageView 的 setImageResources() 方法时会报错。
那么在 RemoteViews 中使用 AppCompatTextView 的 setText() 方法会报错吗?不会,因为AppCompatTextView 是 TextView 的子类,但它并没有重写 setText() 方法,所以使用 AppCompatTextView 的 setText() 方法实际上是去调用了 TextView 的 setText() 方法,而 TextView 的 setText() 方法有注解 @android.view.RemotableViewMethod
,表明是 RemoteViews 支持的方法所以并不会报错。
2.3 RemoteViews 的作用
实现跨进程更新布局,为什么是跨进程更新布局呢?以使用通知栏为例,我们在应用中定义通知,通知是显示在通知栏中。我们的应用和通知栏是位于不同进程中的,每一个应用有一个单独的进程而通知栏则是在系统的进程之中。
2.4 RemoteViews 如何更新布局
同样以使用通知栏为例,我们使用 NotificationManager
的 notify()
去发起通知(即在通知栏种更新布局),SystemServer 的 NotificationManagerService
接收通知并调用 RemoteViews 的 apply()
方法更新布局。采用 Binder
实现了 NotificationManager 和 NotificationManagerService 的连接。
Binder 的作用就是连接各种系统的 Manager 和相应 Service 的桥梁
2.5 RemoteViews 不会立即更新布局
我们在进程 A 中把布局的更新信息通过 Binder 传递给进程 B,布局的更新信息不会在进程 A 中立即完成,首先会将布局和布局的更新信息保存在 RemoteViews 的 Action
中,Action 同样实现了 Parcelable
,等进程 B 接收到这个 RemoteViews 通过反射资源 id 拿到布局中对应的控件后才完成 RemoteViews 中保存的更新信息。
3. 源码分析 RemoteViews 的工作流程
RemoteViews 提供了一系列的 setXXX() 方法实现对布局中相关控件的更新。以更新 TextView 的内容为例来了解 RemoteViews 具体是如何实现布局更新的。
3.1 本地进程调用 setXXX()
RemoteViews 中要为 TextView 设置文字需在本地进程
调用 setTextViewText(int viewId, CharSequence text) 方法,传入要设置的 TextView 的 id 和要设置的文字 text。我们来看看 RemoteViews 中具体是如何实现给 TextView 设置文字的。
-
setTextViewText()
public void setTextViewText(int viewId, CharSequence text) {
setCharSequence(viewId, "setText", text);
}
setTextViewText 方法里调用了 setCharSequence
-
setCharSequence()
public void setCharSequence(int viewId, String methodName, CharSequence value) {
addAction(new ReflectionAction(viewId, methodName, ReflectionAction.CHAR_SEQUENCE, value));
}
setCharSequence 方法内部调用 addAction 方法,并传入了一个 ReflectionAction 对象,ReflectionAction 根据 viewId 通过反射拿到对应的 View,具体的过程我们后面再分析,先来看 addAction 方法。
-
addAction()
private void addAction(Action a) {
...
if (mActions == null) {
mActions = new ArrayList<>();
}
mActions.add(a);
}
mActions 是一个数组,存储 Action 对象,每调用一次 setXXX() 方法都会向 mActions 中添加一个新的 Action 对象,这里的 mActions 只是保存作用,并未对 View 实现设置操作。
3.2 远程进程调用 apply()
在远程进程
中调用 RemoteViews 的 apply() 方法更新布局。
-
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;
}
首先通过 getRemoteViewsToApply 方法获取 RemoteViews 对象,接着调用 inflateView(),返回了根据 RemoteViews 解析出来的 View,进入 inflateView() 看看具体实现:
private View inflateView(Context context, RemoteViews rv, ViewGroup parent) {
if (mApplyThemeResId != 0) {
inflationContext = new ContextThemeWrapper(inflationContext, mApplyThemeResId);
}
LayoutInflater inflater = (LayoutInflater)
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater = inflater.cloneInContext(inflationContext);
inflater.setFilter(this);
View v = inflater.inflate(rv.getLayoutId(), parent, false);
v.setTagInternal(R.id.widget_frame, rv.getLayoutId());
return v;
}
inflateView() 返回的是我们在 RmoteViews 中设置的布局文件,首先会通过系统拿到 LayoutInflater,LayoutInflater 通过 getLayoutId() 得到的布局文件的 id 返回对应的布局文件。最后通过调用 performApply() 执行更新操作。
performApply() 中去遍历我们的 mActions,即每一个需要更新 View 的 Action,然后调用 Action 的 apply(),实现真正的更新。可见在 RemoteView 中实现更新的地方不是 performApply() 而是在内部类 Action 的 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);
}
apply() 和 reApply() 的区别:apply()
会加载布局并更新,而 reApply()
不会加载布局只对布局进行更新。RemoteViews 首次更新布局时调用 apply(),因为要加载布局,后续再更新时直接调用 reApply()。
上面步骤中我们了解到每调用一次 setXXX()
方法, RemoteViews 都通过 mActions
保存布局需要的更新信息 Action;apply()
方法完成布局的加载并调用了 performApply(),performApplay()
方法中没有真正实现布局的更新而是遍历 mActions,调用了 action.applay()
,这里的 Action 是一个内部接口类, ReflectionAction
实现了该接口,接下来进入到 ReflectionAction 的 apply()
中看看如何对布局进行更新。
-
ReflectionAction#apply()
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, false /* async */).invoke(view, this.value);
} catch (Throwable ex) {
throw new ActionException(ex);
}
}
...
}
首先根据 viewId
拿到对应的 View 视图,这里的 viewId 是在调用 setXXX() 创建一个新的 ReflectionAction 时传进来的,接着调用 getMethod()
,根据当前的 View 和 methodName
方法名 反射
拿到对应的方法并调用 invoke()
设置相关的更新信息,这里的 methodName 也是在调用 setXXX() 创建一个新的 ReflectionAction 时传进来的。
3.3 添加点击事件
-
setOnClickPendingIntent()
用于给普通的 View 添加点击事件,但不能给集合(ListView 和 StackView) 中的 item 添加点击事件。
-
setPendingIntentTemplate()
-
setOnClickFillInIntent()
给集合(ListView 和 StackView) 中的 item 添加点击事件,需要 setPendingIntentTemplate() 和 setOnClickFillInIntent() 组合使用。
网友评论