Android RemoteViews源码分析以及扩展使用
2017-10-31 21:51
351 查看
一,写在前面
在前面两篇文章RemoteViews的基本使用(上)之通知栏 ,RemoteViews的基本使用(下)之窗口小部件 中讲述了RemoteViews的两个应用场景,这篇文章主要介绍RemoteViews的内部机制,以及一个小扩展,使用RemoteViews实现跨进程操作界面。本篇文章以窗口小部件为例,来分析RemoteViews如何实现跨进程操作界面。我们都知道在将小部件列表中将窗口小部件拖到桌面,会调用onUpdate方法,在该方法中会调用AppWidgetManager.updateAppWidget(appWidgetIds,remoteViews)来更新窗口小部件,调用RemoteViews方法的一些set..方法,修改窗口小部件的界面。对于这些不是很清楚的哥们,可以查看文章RemoteViews的基本使用(下)之窗口小部件 ,这篇文章对窗口小部件做了简单的介绍,本篇文章主要从源码角度分析RemoteViews,对窗口小部件的生命周期以及使用不再阐述。二,以窗口小部件为例
查看AppWidgetManager$updateAppWidget源码:public void updateAppWidget(int[] appWidgetIds, RemoteViews views) { try { sService.updateAppWidgetIds(appWidgetIds, views, mContext.getUserId()); } catch (RemoteException e) { throw new RuntimeException("system server dead?", e); } } public static AppWidgetManager getInstance(Context context) { synchronized (sManagerCache) { if (sService == null) { IBinder b = ServiceManager.getService(Context.APPWIDGET_SERVICE); sService = IAppWidgetService.Stub.asInterface(b); } WeakReference<AppWidgetManager> ref = sManagerCache.get(context); AppWidgetManager result = null; if (ref != null) { result = ref.get(); } if (result == null) { result = new AppWidgetManager(context); sManagerCache.put(context, new WeakReference<AppWidgetManager>(result)); } return result; } }sService是一个代理对象,updateAppWidgetIds方法的真正调用在服务里,IAppWidgetService是一个AIDL接口,需要找到继承IAppWidgetService.Stub的那个类,这里直接告诉大家该类是AppWidgetService。
查看AppWidgetService$updateAppWidgetIds源码:
public void updateAppWidgetIds(int[] appWidgetIds, RemoteViews views) { if (appWidgetIds == null) { return; } if (appWidgetIds.length == 0) { return; } final int N = appWidgetIds.length; synchronized (mAppWidgetIds) { for (int i=0; i<N; i++) { AppWidgetId id = lookupAppWidgetIdLocked(appWidgetIds[i]); updateAppWidgetInstanceLocked(id, views); } } } //进入updateAppWidgetInstanceLocked方法 void updateAppWidgetInstanceLocked(AppWidgetId id, RemoteViews views) { // allow for stale appWidgetIds and other badness // lookup also checks that the calling process can access the appWidgetId // drop unbound appWidgetIds (shouldn't be possible under normal circumstances) if (id != null && id.provider != null && !id.provider.zombie && !id.host.zombie) { id.views = views; // is anyone listening? if (id.host.callbacks != null) { try { // the lock is held, but this is a oneway call id.host.callbacks.updateAppWidget(id.appWidgetId, views); } catch (RemoteException e) { // It failed; remove the callback. No need to prune because // we know that this host is still referenced by this instance. id.host.callbacks = null; } } } } //callbacks实例化的位置 public int[] startListening(IAppWidgetHost callbacks, String packageName, int hostId, List<RemoteViews> updatedViews) { int callingUid = enforceCallingUid(packageName); synchronized (mAppWidgetIds) { Host host = lookupOrAddHostLocked(callingUid, packageName, hostId); host.callbacks = callbacks; updatedViews.clear(); ArrayList<AppWidgetId> instances = host.instances; int N = instances.size(); int[] updatedIds = new int ; for (int i=0; i<N; i++) { AppWidgetId id = instances.get(i); updatedIds[i] = id.appWidgetId; updatedViews.add(id.views); } return updatedIds; } }最后会调用id.host.callbacks.updateAppWidget(id.appWidgetId, views),需要找到callbacks的实例化位置,上面代码已经给出答案,调用AppWidgetService$startListening方法会实例化callbacks对象。那么,谁调用了AppWidgetService$startListening方法呢。
查看类AppWidgetHost$startListening方法,源码如下:
public void startListening() { int[] updatedIds; ArrayList<RemoteViews> updatedViews = new ArrayList<RemoteViews>(); try { if (mPackageName == null) { mPackageName = mContext.getPackageName(); } updatedIds = sService.startListening(mCallbacks, mPackageName, mHostId, updatedViews); } catch (RemoteException e) { throw new RuntimeException("system server dead?", e); } final int N = updatedIds.length; for (int i=0; i<N; i++) { updateAppWidgetView(updatedIds[i], updatedViews.get(i)); } }sService对象就是AppWidgetService,代理对象sService.startListening(mCallbacks, mPackageName, mHostId, updatedViews)的调用,基于底层Binder机制,调用远程服务的startListening方法,也就是AppWidgetService$startListening。
我们可以查看sService实例化的位置,AppWidgetHost构造函数源码如下:
int mHostId; Callbacks mCallbacks = new Callbacks(); final HashMap<Integer,AppWidgetHostView> mViews = new HashMap<Integer, AppWidgetHostView>(); public AppWidgetHost(Context context, int hostId) { mContext = context; mHostId = hostId; mHandler = new UpdateHandler(context.getMainLooper()); synchronized (sServiceLock) { if (sService == null) { IBinder b = ServiceManager.getService(Context.APPWIDGET_SERVICE); sService = IAppWidgetService.Stub.asInterface(b); } } }sService = IAppWidgetService.Stub.asInterface(b);获取的不就是AppWidgetService的代理对象么。
注意这样一行代码:Callbacks mCallbacks = new Callbacks(),mCallbacks 就是updatedIds = sService.startListening(mCallbacks, mPackageName, mHostId, updatedViews)中的mCallbacks参数。通过Binder机制,调用远程服务方法,即,AppWidgetService$startListening(IAppWidgetHost callbacks, String
packageName, int hostId,List<RemoteViews> updatedViews),前面提到的mCallbacks参数,传递过来就是callbacks。这样,终于找到了mCallbacks实例化的位置,它是AppwidgetHost里面的一个内部类Callbacks。
前面讲到更新窗口小部件,需要调用appWidgetManager.updateAppwidget(ids,remoteviews),最后会调用id.host.callbacks.updateAppWidget(id.appWidgetId, views),现在我们知道了callbacks的实例化位置,可以查看方法updateAppWidget里面到底做了些什么操作。
查看AppWidgetHost$Callbacks内部类源码如下:
class Callbacks extends IAppWidgetHost.Stub { public void updateAppWidget(int appWidgetId, RemoteViews views) { Message msg = mHandler.obtainMessage(HANDLE_UPDATE); msg.arg1 = appWidgetId; msg.obj = views; msg.sendToTarget(); } public void providerChanged(int appWidgetId, AppWidgetProviderInfo info) { Message msg = mHandler.obtainMessage(HANDLE_PROVIDER_CHANGED); msg.arg1 = appWidgetId; msg.obj = info; msg.sendToTarget(); } }代码比较简单,发送了一个消息到消息队列里,接下来是处理消息,源码如下:
class UpdateHandler extends Handler { public UpdateHandler(Looper looper) { super(looper); } public void handleMessage(Message msg) { switch (msg.what) { case HANDLE_UPDATE: { updateAppWidgetView(msg.arg1, (RemoteViews)msg.obj); break; } case HANDLE_PROVIDER_CHANGED: { onProviderChanged(msg.arg1, (AppWidgetProviderInfo)msg.obj); break; } } } } //继续查看方法updateAppWidgetView void updateAppWidgetView(int appWidgetId, RemoteViews views) { AppWidgetHostView v; synchronized (mViews) { v = mViews.get(appWidgetId); } if (v != null) { v.updateAppWidget(views); } }这里的v就是AppWidgetHostView,继续查看AppWidgetHostView$updateAppWidget源码如下:
/** * Process a set of {@link RemoteViews} coming in as an update from the * AppWidget provider. Will animate into these new views as needed */ public void updateAppWidget(RemoteViews remoteViews) { if (LOGD) Log.d(TAG, "updateAppWidget called mOld=" + mOld); boolean recycled = false; View content = null; Exception exception = null; // Capture the old view into a bitmap so we can do the crossfade. if (CROSSFADE) { if (mFadeStartTime < 0) { if (mView != null) { final int width = mView.getWidth(); final int height = mView.getHeight(); try { mOld = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); } catch (OutOfMemoryError e) { // we just won't do the fade mOld = null; } if (mOld != null) { //mView.drawIntoBitmap(mOld); } } } } 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); 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); 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); } if (mView != content) { removeView(mView); mView = content; } if (CROSSFADE) { if (mFadeStartTime < 0) { // if there is already an animation in progress, don't do anything -- // the new view will pop in on top of the old one during the cross fade, // and that looks okay. mFadeStartTime = SystemClock.uptimeMillis(); invalidate(); } } }注意看49行,61行,分别调用remoteViews.reapply(mContext, mView),content = remoteViews.apply(mContext, this),调用了RemoteViews的apply加载或更新界面,调用RemoteViews的reapply方法更新界面,但不能加载。以本篇文章为例,界面指的是在launcher应用上显示的窗口小部件。
在84行,执行了this.addView(content),这个this就是AppWidgetHostView,也就是说窗口小部件的界面添加在AppWidgetHostView里,也可以理解为AppWidgetHostView是窗口小部件的父容器。理解这个很重要,后面扩展使用的原理就来自于此。
三,小结
开发一个窗口小部件,我们需要在应用中创建AppWidgetProvider的子类,并重写一些生命周期的方法。在重写的onUpdate方法中,调用appWidgetManager.updateAppWidget(ids,remoteviews)更新窗口小部件,参数remoteViews一般会调用一些setXXX方法来确定如何更新界面。这里的remoteViews是在开发的应用中的,而窗口小部件的界面更新并不是在本应用中的,它的更新操作是放在SystemServer进程中。所以窗口小部件的实现体现了一个非常重要的需求:跨进程更新界面。从上面的代码分析可知,界面具体更新是交给AppWidgetService处理,它是一个系统服务,开机启动就会执行。系统服务AppWidgetService处理界面的更新,最终会调用remoteViews$apply方法更新界面。这个remoteViews从本应用通过Binder机制,跨进程传递给了AppWidgetService,我们可以猜测它实现了Parcelable接口,事实也确实如此。
因此可以得出这样一个结论:若需要实现操作远程界面,首先需要调用apply方法并返回一个View,然后将该View添加到父容器中。
四,另外
前面代码这样那样跟下来,更新界面,最终是调用了Remoteviews$apply/reapply方法,那么apply是如何更新界面的呢?下面的分析才是本篇文章的重点,上面一系列代码跟进只是引出这个引子,并没有多大实际意义。只需要知道最终调用AppWidgetHostView$updateAppWidget方法,里面调用RemoteViews的方法apply/reapply更新界面,且AppWidgetHostView是更新界面的父容器。五,RemoteViews源码分析
在分析RemoteViews$apply/reapply方法前,先分析RemoteViews的一些setXXX方法,至于为啥子看完就知道啦。5.1,分析RemoteViews$setXXX方法
以RemoteViews$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 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); }从上面代码可知:setTextViewText方法做了这样一件事,用Action将控件的id,text的值封装起来,并将Action对象放入到list集合中。后面肯定要取出集合里的元素进行相应处理,那么是在哪呢,后面会给出答案。
查看内部类RemoteViews$Action源码:
private abstract static class Action implements Parcelable { public abstract void apply(View root, ViewGroup rootParent, OnClickHandler handler) throws ActionException; //...code }可以发现Action是一个抽象类,并实现了Parcelable接口,有一个很重要的抽象方法apply。Action有很多子类,如TextViewSizeAction,ReflectionAction等,setTextViewText方法中的Action是ReflectionAction,setTextViewTextSize方法中Action是TextViewSizeAction。也就是说,在调用remoteViews.setXXX方法时,对控件的操作的数据封装在Action中。每调用一次remoteViews.setXXX方法,就将对应Action对象存入list集合中,然后统一交给AppWidgetService处理。
当然,我们还可以使用AIDL接口实现跨进程通信,这样我们需要定义大量的AIDL接口去替代RemoteViews$setXXX方法。同时,由于Action都存放到list集合中,在处理时只需要获取到list集合,便可以批量处理RemoteViews更新界面的操作,不需要频繁的进行IPC操作,提高了程序的性能。但是,在文章RemoteViews的基本使用(上)之通知栏中有讲到,RemoteViews的缺陷是不能支持所有的View,它只能支持部分的布局控件,部分的View。具体支持哪些,RemoteViews的基本使用(上)之通知栏有详细介绍,这里不再阐述。
前面讲述了在list集合中存放了Action对象,那么在哪里取出集合里的元素,并执行界面更新的处理呢?这就是下面要讲的,继续看下来呗~
5.2,分析RemoteViews$apply/reapply方法
通过上面的分析,可以猜测RemoteViews$apply/reapply方法中取出了list集合里的元素,然后执行更新操作。当然是SystemServer进程中执行的,文章前半部分已经有很详细的分析了。查看RemoteViews$apply源码:
public View apply(Context context, ViewGroup parent) { return apply(context, parent, null); } /** @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; } //继续看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); } } }result = inflater.inflate(rvToApply.getLayoutId(), parent, false)就是加载布局文件,创建RemoteViews对象时会给字段mLayoutId赋值,即rvToApply.getLayoutId()的返回值;parent是AppWidgetHostView。
performApply证实了前面的猜测,遍历list集合,取出Action对象,调用action.apply(...)更新窗口小部件。Action$apply是如何更新界面的呢,继续往下看~
5.3,分析Action$apply方法
前面以RemoteViews$setTextViewText为例,封装更新界面数据的Action是ReflectionAction,以其为例。源码如下:
private 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; } //...code @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); } Class klass = view.getClass(); Method method; try { method = klass.getMethod(this.methodName, getParameterType()); } catch (NoSuchMethodException ex) { throw new ActionException("view: " + klass.getName() + " doesn't have method: " + this.methodName + "(" + param.getName() + ")"); } if (!method.isAnnotationPresent(RemotableViewMethod.class)) { throw new ActionException("view: " + klass.getName() + " can't use method with RemoteViews: " + this.methodName + "(" + param.getName() + ")"); } try { //noinspection ConstantIfStatement if (false) { Log.d(LOG_TAG, "view: " + klass.getName() + " calling method: " + this.methodName + "(" + param.getName() + ") with " + (this.value == null ? "null" : this.value.getClass().getName())); } method.invoke(view, this.value); } catch (Exception ex) { throw new ActionException(ex); } } }第12行,findViewById获取控件引用;第20,23,43行,使用反射技术更新该控件。有兴趣的哥们可以研究下TextViewSizeAction的apply方法,它更新界面并不是用反射,在findViewById获取到控件引用后,调用view.setTextSize(..)更新界面。
上面大量篇幅从源码角度分析,窗口小部件如何更新界面,前面我们提到这里隐藏了一个非常重要的需求:跨进程更新界面。AppWidgetService是系统服务的一种,在SystemServer进程中执行。还是以窗口小部件为例,RemoteViews通过Binder机制传递到AppWidgetService中,并调用remoteViews$apply方法更新界面,并返回一个view,最后执行appWidgetHostView.addView(view)。好了,下面会给出源码分析后的扩展使用。
六,RemoteViews的扩展使用
上面通过Binder机制传递RemoteViews,这里不想这么复杂去处理,尝试使用广播去完成进程间传递RemoteViews的工作。在应用RvSender中发送广播,并将RemoteViews对象放入intent的extra中;在应用RvReceiver中接受广播,取出intent中的数据,并在该应用中完成界面更新。应用RvSender:
public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } public void clickButton(View v) { //发送广播 Intent intent = new Intent(); intent.setAction("com.example.remoteview"); RemoteViews rv = new RemoteViews(getPackageName(), R.layout.rv_layout); rv.setTextViewText(R.id.tv, "hello, from Sender"); intent.putExtra("remoteview", rv); sendBroadcast(intent); } }rv_layout.xml
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center_horizontal"> <ImageView android:id="@+id/iv" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="10dp" android:src="@drawable/ic_launcher"/> <TextView android:id="@+id/tv" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@id/iv" android:layout_marginTop="20dp" android:textSize="24sp" android:textColor="#f00" android:background="@android:color/darker_gray" android:text="remoteview"/> </RelativeLayout>
应用RvReceiver:
public class MainActivity extends Activity { private RelativeLayout rl; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); init(); } public void init() { IntentFilter filter = new IntentFilter(); filter.addAction("com.example.remoteview"); MyReceiver receiver = new MyReceiver(); registerReceiver(receiver, filter); rl = (RelativeLayout) findViewById(R.id.rl); } private class MyReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if ("com.example.remoteview".equals(intent.getAction())) { RemoteViews remoteViews = intent.getParcelableExtra("remoteview"); View view = remoteViews.apply(context, rl); rl.addView(view); } } } }activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity" > <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="24sp" android:text="RvReceiver" /> <RelativeLayout android:id="@+id/rl" android:layout_marginTop="120dp" android:layout_width="match_parent" android:layout_height="wrap_content"></RelativeLayout> </RelativeLayout>需要注意的是,应用RvReceiver中并不需要rv_layout.xml资源文件。在应用RvSender中点击按钮后,应用RvReceiver界面如下:
这篇文章就分享到这里啦,有疑问可以留言,亦可纠错,亦可补充,互相学习...^_^
相关文章推荐
- Android系统Recovery工作原理之使用update.zip升级过程分析(九)---updater-script脚本语法简介以及执行流程
- Android中Parcel的分析以及使用[转]
- Android中Preference的使用以及监听事件分析
- Android中Preference的使用以及监听事件分析
- Android中Preference的使用以及监听事件分析
- Android中Preference的使用以及监听事件分析
- Android中Parcel的分析以及使用
- (转)Android中Preference的使用以及监听事件分析
- Android系统Recovery工作原理之使用update.zip升级过程分析(九)---updater-script脚本语法简介以及执行流程
- Android中View绘制不同状态背景图片原理深入分析以及StateListDrawable使用详解
- Android Preference 使用以及监听事件分析
- Android中Preference的使用以及监听事件分析
- Android中Preference的使用以及监听事件分析
- Android中Preference的使用以及监听事件分析
- Android系统Recovery工作原理之使用update.zip升级过程分析(九)---updater-script脚本语法简介以及执行流程
- Android系统Recovery工作原理之使用update.zip升级过程分析(九)---updater-script脚本语法简介以及执行流程
- Android中Preference的使用以及监听事件分析
- Android系统Recovery工作原理之使用update.zip升级过程分析(九)---updater-script脚本语法简介以及执行流程
- Android中View(视图)绘制不同状态背景图片原理深入分析以及StateListDrawable使用详解
- Android中Preference的使用以及监听事件分析