前言 关于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则相比notification要麻烦一些,分三步走。
自定义布局
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" />
自定义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)方法。
在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为例,通信的步骤如下:
AppWidgetManager通过Binder把RemoteVews传送到SystemServer(别忘了,RemoteViews 实现了parcelable接口)
SystemServer会根据RemoteViews携带过来的信息(如intent、layoutID等),使用LayoutInflater把View加载出来,该View对SystemServer来说就是普通的View,但是对于我们的App来说是RemoteViews。
用户添加widget时,会展示 android:initialLayout=”@layout/widget_layout” 指示的初始布局.
用户使用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); 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 ; } 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]; 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 ) { widget.views.mergeRemoteViews(views); } else { 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) { return ; } content = getDefaultView(); mLayoutId = -1 ; mViewMode = VIEW_MODE_DEFAULT; } else { mRemoteContext = getRemoteContext(remoteViews); int layoutId = remoteViews.getLayoutId(); 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; } } 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) { 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 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 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 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)中,个人感觉也没啥问题。
参考