0%

Android-RemoteViews介绍

前言

关于RemoteViews,官方是这么定义的:

A class that describes a view hierarchy that can be displayed in
another process. The hierarchy is inflated from a layout resource
file, and this class provides some basic operations for modifying the
content of the inflated hierarchy.

简单来说就是一个可以提供在其他进程中的展示、修改View的类。从类的定义上看,RemoteViews是直接继承Object而不是View,因此它不能像普通View一样提供setOnClickListener(有替代方案)、setEnable()等操作。在实际开发中,主要用在展示notification和widget。

Demo

发默认系统通知

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void startNotify() {
Uri smsToUri = Uri.parse("smsto:10086");
Intent mIntent = new Intent( android.content.Intent.ACTION_SENDTO, smsToUri );
mIntent.putExtra("sms_body", "The SMS text");
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, mIntent, PendingIntent.FLAG_UPDATE_CURRENT);
Notification notification = null;
notification = new Notification
.Builder(this)
.setTicker("hello")
.setSmallIcon(R.mipmap.ic_launcher)
.setWhen(System.currentTimeMillis())
.setContentIntent(pendingIntent)
.build();
NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
manager.notify(1, notification);
}

发自定义通知

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private void startCustomNotify() {
Uri smsToUri = Uri.parse("smsto:10086");
Intent mIntent = new Intent( android.content.Intent.ACTION_SENDTO, smsToUri ); // 发短信
mIntent.putExtra("sms_body", "The SMS text");
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, mIntent, PendingIntent.FLAG_UPDATE_CURRENT);
Intent callIntent = new Intent( Intent.ACTION_DIAL, Uri.parse("tel:10086") ); //打电话
PendingIntent callPendingIntent = PendingIntent.getActivity(this, 0, callIntent, PendingIntent.FLAG_UPDATE_CURRENT);
RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.widget_layout);
remoteViews.setTextViewText(R.id.tv_now, String.valueOf(System.currentTimeMillis()));
remoteViews.setOnClickPendingIntent(R.id.tv_now, pendingIntent);
remoteViews.setOnClickPendingIntent(R.id.btn_call, callPendingIntent);
Notification notification = new Notification
.Builder(this)
.setTicker("hello")
.setSmallIcon(R.mipmap.ic_launcher)
.setWhen(System.currentTimeMillis())
.setContentIntent(callPendingIntent)
.setCustomContentView(remoteViews)
.build();
NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
manager.notify(1, notification);
}

widget

使用widget则相比notification要麻烦一些,分三步走。

  1. 自定义布局

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <receiver android:name=".CustomAppwidgetProvider">
    <meta-data
    android:name="android.appwidget.provider"
    android:resource="@xml/widget_provider_info">
    </meta-data>
    <intent-filter>
    <action android:name="change_time_action" />
    <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
    </intent-filter>
    </receiver>

    widget_provider_info是用来描述Widget的。

    1
    2
    3
    4
    5
    <?xml version="1.0" encoding="utf-8"?>
    <appwidget-provider
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:initialLayout="@layout/widget_layout"
    android:updatePeriodMillis="60000"/> <!-- 更新时间 -->
  2. 自定义WidgetProvider

    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
    public class CustomAppwidgetProvider extends AppWidgetProvider {
    private static final String CHANGE_TIME_ACTION = "change_time_action";
    @Override
    public void onReceive(Context context, Intent intent) {
    super.onReceive(context, intent);
    if (intent.getAction().equals(CHANGE_TIME_ACTION)) {
    AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
    RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget_layout);
    remoteViews.setTextViewText(R.id.tv_now, new Date().toString());
    PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, new Intent(CHANGE_TIME_ACTION), 0);
    remoteViews.setOnClickPendingIntent(R.id.tv_now, pendingIntent);
    appWidgetManager.updateAppWidget(new ComponentName(context, CustomAppwidgetProvider.class), remoteViews);
    }
    }
    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
    super.onUpdate(context, appWidgetManager, appWidgetIds);
    int len = appWidgetIds.length;
    for (int i=0 ; i<len ; i++) {
    updateWidgetInfo(context, appWidgetManager, appWidgetIds[i]);
    }
    }
    private void updateWidgetInfo(Context context, AppWidgetManager appWidgetManager, int appWidgetId) {
    RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget_layout);
    PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, new Intent(CHANGE_TIME_ACTION), 0);
    remoteViews.setOnClickPendingIntent(R.id.tv_now, pendingIntent);
    appWidgetManager.updateAppWidget(appWidgetId, remoteViews);
    }
    }

    当然AppWidgetProvider不只有onUpdate和onReceive两个回调,还有onDeleted、onEnabled、onDisabled等方法,当一条广播到来时,onReceive会自动根据intent中的Action来决定调用哪个(onDeleted、onEnabled、onDisabled、onUpdate)方法。

  3. 在menifest中注册
    因为AppWidgetPovider本质上是一个receiver,因此需要在menifest中注册。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <receiver android:name=".CustomAppwidgetProvider">
    <meta-data
    android:name="android.appwidget.provider"
    android:resource="@xml/widget_provider_info">
    </meta-data>
    <intent-filter>
    <action android:name="change_time_action" />
    <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
    </intent-filter>
    </receiver>

分析

通过上面的例子不难看出,不管是自定义Notification还是Widget,都需要RemoteViews来协助完成。由于进程隔离的存在,要想操作直接其他进程的View,显然是不可能的。RemoteViews提供了操作远程View的方法,其内部是通过Binder跨进程通信实现的。
Notification和Widget是分别通过NotificationManager和AppWidgetManager进行管理的。通过查看源码可知,NotificationManager和AppWidgetManager其实是使用了Binder和SystemServer进程中的NotificationManagerService、AppWidgetManagerService两个服务进行通信。其实不光是NotificationManager和AppWidgetManager,使用了Context.getSystemService(ServiceName)这种方式获取到的系统服务都是使用Binder和系统进程进行通信的。
以AppWidgetManager为例,通信的步骤如下:

  1. AppWidgetManager通过Binder把RemoteVews传送到SystemServer(别忘了,RemoteViews 实现了parcelable接口)
  2. SystemServer会根据RemoteViews携带过来的信息(如intent、layoutID等),使用LayoutInflater把View加载出来,该View对SystemServer来说就是普通的View,但是对于我们的App来说是RemoteViews。
  3. 用户添加widget时,会展示 android:initialLayout=”@layout/widget_layout” 指示的初始布局.
  4. 用户使用RemoteViews的set方法(如setTextViewText)来更新布局。注意,这里不是每次set就马上生效,而是把每次对View的操作抽象成一个Action,保存在一个列表中,在使用AppWidgetManager.updateAppWidget之后才通过Binder一次性提交这些操作。这样做的好处是可以节省大量的Binder IPC操作,从而提高程序性能。

代码分析

以RemoteViews的SetTextViewText为例

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

调用了setCharSequence

1
2
3
public void setCharSequence(int viewId, String methodName, CharSequence value) {
addAction(new ReflectionAction(viewId, methodName, ReflectionAction.CHAR_SEQUENCE, value));
}

和上面分析中一致,把操作抽象成了Action,然后调用AddAction方法保存到Action列表mActions中

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

通过代码我们可以看到,这些操作是通过反射来进行的。因为View的方法众多,而且名字又不一样,使用反射可以优雅地设置各种View的属性(如LinearLayout、FrameLayout、TextView等),非常精妙。上面已经说过,set方法不会立即更新,那么真正的更新操作在哪里呢?我们来看看AppWidgetManager的updateAppWidget方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
private final IAppWidgetService mService;
......
public void updateAppWidget(int[] appWidgetIds, RemoteViews views) {
if (mService == null) {
return;
}
try {
mService.updateAppWidgetIds(mPackageName, appWidgetIds, views);
}
catch (RemoteException e) {
throw new RuntimeException("system server dead?", e);
}
}

调用IAppWidgetService的updateAppWidgetIds方法跨进程通信

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Override 
public void updateAppWidgetIds(java.lang.String callingPackage,int[]appWidgetIds,android.widget.RemoteViews views)throws android.os.RemoteException {
android.os.Parcel _data=android.os.Parcel.obtain();
android.os.Parcel _reply=android.os.Parcel.obtain();
try{
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeString(callingPackage);
_data.writeIntArray(appWidgetIds);
if((views!=null)){
_data.writeInt(1);
views.writeToParcel(_data,0);
}
else{
_data.writeInt(0);
}
mRemote.transact(Stub.TRANSACTION_updateAppWidgetIds,_data,_reply,0);
_reply.readException();
}
finally{
_reply.recycle();
_data.recycle();
}
}

再来看看服务端AppWidgetService做了什么工作。AppWidgetService主要由AppWidgetServiceImpl.java实现。看看AppWidgetServiceImpl的方法

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
private void updateAppWidgetIds(String callingPackage, int[] appWidgetIds,
RemoteViews views, boolean partially) {
final int userId = UserHandle.getCallingUserId();
if (appWidgetIds == null || appWidgetIds.length == 0) {
return;
}
// Make sure the package runs under the caller uid.
mSecurityPolicy.enforceCallFromPackage(callingPackage);
final int bitmapMemoryUsage = (views != null) ? views.estimateMemoryUsage() : 0;
if (bitmapMemoryUsage > mMaxWidgetBitmapMemory) {
throw new IllegalArgumentException("RemoteViews for widget update exceeds"
+ " maximum bitmap memory usage (used: " + bitmapMemoryUsage
+ ", max: " + mMaxWidgetBitmapMemory + ")");
}
synchronized (mLock) {
ensureGroupStateLoadedLocked(userId);
final int N = appWidgetIds.length;
for (int i = 0; i < N; i++) {
final int appWidgetId = appWidgetIds[i];
// NOTE: The lookup is enforcing security across users by making
// sure the caller can only access widgets it hosts or provides.
Widget widget = lookupWidgetLocked(appWidgetId,
Binder.getCallingUid(), callingPackage);
if (widget != null) {
updateAppWidgetInstanceLocked(widget, views, partially);
}
}
}
}

继续看updateAppWidgetInstanceLocked

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private void updateAppWidgetInstanceLocked(Widget widget, RemoteViews views,
boolean isPartialUpdate) {
if (widget != null && widget.provider != null
&& !widget.provider.zombie && !widget.host.zombie) {
if (isPartialUpdate && widget.views != null) {
// For a partial update, we merge the new RemoteViews with the old.
widget.views.mergeRemoteViews(views);
} else {
// For a full update we replace the RemoteViews completely.
widget.views = views;
}
scheduleNotifyUpdateAppWidgetLocked(widget, views);
}
}

继续看scheduleNotifyUpdateAppWidgetLocked

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private void scheduleNotifyUpdateAppWidgetLocked(Widget widget, RemoteViews updateViews) {
if (widget == null || widget.provider == null || widget.provider.zombie
|| widget.host.callbacks == null || widget.host.zombie) {
return;
}
SomeArgs args = SomeArgs.obtain();
args.arg1 = widget.host;
args.arg2 = widget.host.callbacks;
args.arg3 = updateViews;
args.argi1 = widget.appWidgetId;
mCallbackHandler.obtainMessage(
CallbackHandler.MSG_NOTIFY_UPDATE_APP_WIDGET,
args).sendToTarget();
}

使用Handler发送了一条消息,最后会走到handleNotifyUpdateAppWidget

1
2
3
4
5
6
7
8
9
10
11
private void handleNotifyUpdateAppWidget(Host host, IAppWidgetHost callbacks,
int appWidgetId, RemoteViews views) {
try {
callbacks.updateAppWidget(appWidgetId, views);
} catch (RemoteException re) {
synchronized (mLock) {
Slog.e(TAG, "Widget host dead: " + host.id, re);
host.callbacks = null;
}
}
}

我靠,这个callbacks又是一次Binder通信!!!是SystemServer和AppWidgetHost(通常来说是launcher)的一次通信,此时SystemServer是客户端,AppWidgetHost是服务端。继续看下IAppWidgetHost对应的实现类AppWidgetHost.updateAppWidget方法

1
2
3
4
5
6
7
8
9
void updateAppWidgetView(int appWidgetId, RemoteViews views) {
AppWidgetHostView v;
synchronized (mViews) {
v = mViews.get(appWidgetId);
}
if (v != null) {
v.updateAppWidget(views);
}
}

又继续调用AppWidgetHostView.updateAppWidget

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
  public void updateAppWidget(RemoteViews remoteViews) {
......// 省略部分代码
if (remoteViews == null) {
if (mViewMode == VIEW_MODE_DEFAULT) {
// We've already done this -- nothing to do.
return;
}
content = getDefaultView();
mLayoutId = -1;
mViewMode = VIEW_MODE_DEFAULT;
} else {
// Prepare a local reference to the remote Context so we're ready to
// inflate any requested LayoutParams.
mRemoteContext = getRemoteContext(remoteViews);
int layoutId = remoteViews.getLayoutId();
// If our stale view has been prepared to match active, and the new
// layout matches, try recycling it
if (content == null && layoutId == mLayoutId) {
try {
remoteViews.reapply(mContext, mView, mOnClickHandler);
content = mView;
recycled = true;
if (LOGD) Log.d(TAG, "was able to recycled existing layout");
} catch (RuntimeException e) {
exception = e;
}
}
// Try normal RemoteView inflation
if (content == null) {
try {
content = remoteViews.apply(mContext, this, mOnClickHandler);
if (LOGD) Log.d(TAG, "had to inflate new layout");
} catch (RuntimeException e) {
exception = e;
}
}
mLayoutId = layoutId;
mViewMode = VIEW_MODE_CONTENT;
}
if (content == null) {
if (mViewMode == VIEW_MODE_ERROR) {
// We've already done this -- nothing to do.
return ;
}
Log.w(TAG, "updateAppWidget couldn't find any view, using error view", exception);
content = getErrorView();
mViewMode = VIEW_MODE_ERROR;
}
if (!recycled) {
prepareView(content);
addView(content);
}
......//省略部分代码
}

基本逻辑是调用RemoteViews的reapply和apply方法生成新的View,通知栏和widget小控件在初始化时会调用apply方法,而后面的更新操作会调用reapply方法。我们继续来看RemoteViews.apply方法

1
2
3
4
5
6
7
8
9
10
11
12
13
/** @hide */
public View apply(Context context, ViewGroup parent, OnClickHandler handler) {
RemoteViews rvToApply = getRemoteViewsToApply(context);
View result;
Context c = prepareContext(context);
LayoutInflater inflater = (LayoutInflater)
c.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater = inflater.cloneInContext(c);
inflater.setFilter(this);
result = inflater.inflate(rvToApply.getLayoutId(), parent, false);
rvToApply.performApply(result, parent, handler);
return result;
}

到这里终于把View生成了。result就是我们的初始布局。至此,分析算是结束了。
如果再继续往下看的话,流程和上面分析的差不多,继续看performApply方法

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);
}
}
}

又调用了Action的apply方法

1
2
3
4
5
6
7
8
9
10
11
 /**
* Base class for all actions that can be performed on an
* inflated view.
*
* SUBCLASSES MUST BE IMMUTABLE SO CLONE WORKS!!!!!
*/
private abstract static class Action implements Parcelable {
public abstract void apply(View root, ViewGroup rootParent,
OnClickHandler handler) throws ActionException;
......//省略部分代码
}

然而是个抽象方法,找个子类TextViewSizeAction看下实现方法

1
2
3
4
5
6
@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);
}

ViewId和units还有size在构造的时候就会传进来。是通过setTextViewTextSize方法传进来的。

1
2
3
4
5
6
7
8
9
10
/**
* Equivalent to calling {@link TextView#setTextSize(int, float)}
*
* @param viewId The id of the view whose text size should change
* @param units The units of size (e.g. COMPLEX_UNIT_SP)
* @param size The size of the text
*/
public void setTextViewTextSize(int viewId, int units, float size) {
addAction(new TextViewSizeAction(viewId, units, size));
}

因为setTextViewSize比较特殊,有两个参数units和zise,所以重新写了个Action。如果只有一个参数,就可以直接复用ReflectionAction。apply之后,如果有更新操作(mActions不为空)则更新。至此分析结束。

问题1

App中的layout文件是怎么传输的AppWidgetHost(Launcher)中的?
有待研究。

问题2

为何不直接使用App和Launcher进行通信,而需要通过AppWidgetService作为媒介呢?
本人猜测:可能是为了App和Widget之间的解耦,使用AppWidgetService,App不需要关心App的Widget放在哪个进程,反正AppWidgetService会帮我弄好,甚至App的Widget放在另外一个App(非Launcher)中也没有任何问题。不过话说回来,如果直接使用App与AppWidgetHost进行通信,虽然不能把Widget放在App(非Launcher)中,个人感觉也没啥问题。

参考