Android之RemoteViews篇下————RemoteViews的内部机制
Android之RemoteViews篇下————RemoteViews的内部机制
一.目录
文章目录
- Android之RemoteViews篇下————RemoteViews的内部机制
- 一.目录
- 二.remoteViews概述
- 三.RemoteViews源码
- 四.RemoteViews的简单应用
- 五.参考资料
二.remoteViews概述
上一篇博客中讲了通知栏和桌面小部件的简单使用,它们分别由otificationManager和AppWidgetProvider管理,而NotificationManager和AppWidgetProvider通过Binder分别为SystemService进程中的NotificationManagerService和AppWidgetService,由此可见,通知栏和小部件实际上是这两个中加载出来的,这就和我们的进程构成了跨进程通信的原理。
remoteViews构造方法
public RemoteViews(String packageName, int layoutId),第一个参数是当前应用的包名,第二个参数是待加载的布局文件。
remoteViews原理
系统将view操作封装成Action对象,Action同样实现了Parcelable接口,通过Binder传递到SystemServer进程。远程进程通过RemoteViews的apply方法来进行view的更新操作,RemoteViews的apply方法内部则会去遍历所有的action对象并调用它们的apply方法来进行view的更新操作。
这样做的好处是不需要定义大量的Binder接口,其次批量执行RemoteViews中的更新操作提高了程序性能。\
remoteViews的工作流程
首先RemoteViews会通过Bingder传递到SystemServic进程,因为RemoteViews实现了Parcelable接口,因此他们可以跨进程传输系统会根据RemoteViews的包名信息拿到该应用的资源;然后通过LayoutInflater去加载RemoteViews中的布局文件。接着系统会对View进行一系列界面更新任务,这些任务就是之前我们通过set来提交的。set方法对View的更新并不会立即执行,会记录下来,等到RemoteViews被加载以后才会执行。
apply和reApply的区别
apply会加载布局并更新界面,而reApply则只会更新界面。通知栏和桌面小部件在界面的初始化中会调用apply方法,而在后面的更新界面中会调用reapply方法
三.RemoteViews源码
从setTextViewText中跟进
public void setTextViewText(int viewId, CharSequence text) { setCharSequence(viewId, "setText", text); }
继续跟进
public void setCharSequence(int viewId, String methodName, CharSequence value) { addAction(new ReflectionAction(viewId, methodName, ReflectionAction.CHAR_SEQUENCE, value)); }
没有对view直接操作,但是添加了一个ReflectionAction,继续跟进:
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(); } mActions.add(a); // update the memory usage stats a.updateMemoryUsageEstimate(mMemoryUsageCounter); }
这里仅仅是把每一个action存进了list。这时候换一个切入点查看updateAppWidget方法,或者是notificationManager.notify因为跟新视图都要调用者两个方法
public void updateAppWidget(int appWidgetId, RemoteViews views) { if (mService == null) { return; } updateAppWidget(new int[] { appWidgetId }, views); }
继续跟进
public void updateAppWidget(int[] appWidgetIds, RemoteViews views) { if (mService == null) { return; } try { mService.updateAppWidgetIds(mPackageName, appWidgetIds, views); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } }
这时候无法继续查看,这时候我们思考,RemoteViews不是真正的view啊,所以是否可以去AppWidgetHostView看看,调转到updateAppWidget方法:
public void updateAppWidget(RemoteViews remoteViews) { applyRemoteViews(remoteViews, true); }
继续跟进
protected void applyRemoteViews(RemoteViews remoteViews, boolean useAsyncIfPossible) { 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 (mLastExecutionSignal != null) { mLastExecutionSignal.cancel(); mLastExecutionSignal = null; } 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 { if (mAsyncExecutor != null && useAsyncIfPossible) { inflateAsync(remoteViews); return; } // Prepare a local reference to the remote Context so we're ready to // inflate any requested LayoutParams. mRemoteContext = getRemoteContext(); 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 recycle 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; } applyContent(content, recycled, exception); updateContentDescription(mInfo); }
跳转到RemoteViews的reapply方法:
public void reapply(Context context, View v) { reapply(context, v, null); }
继续跟进
public void reapply(Context context, View v, OnClickHandler handler) { RemoteViews rvToApply = getRemoteViewsToApply(context); // In the case that a view has this RemoteViews applied in one orientation, is persisted // across orientation change, and has the RemoteViews re-applied in the new orientation, // we throw an exception, since the layouts may be completely unrelated. 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); }
继续跟进
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,现在终于看到了,由于action是抽象类,我们可以看看它子类的实现:
@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); } }
四.RemoteViews的简单应用
可以参考桌面小部件的原理,利用RemoteViews来实现两个进程之间View的传递
首先我们两个Activity分别运行在了两个不同的进程,一个是A,一个是B,其中A扮演的是通知栏的角色,而B则可以不停地发送通知栏消息,当然这是模拟的消息。为了模拟通知栏的效果,我们修改A的process属性使其运行在单独的进程中,这样A和B就构成了多进程通信的情形。我们在B中创建Remoteviews对象,然后通知A显示这个RemoteViews对象。如何通知A显示B中的RemoteViews呢?我们可以像系统一样采用
Binder来实现,但是这里为了简单起见就采用了广播。B每发送一次模拟通知,就会发送一个特定的广播,然后A接收到广播后就开始显示B中定义的RemoteViews对象,这个过程和系统的通知栏消息的显示过程几乎一致,或者说这里就是复制了通知栏的显示过程而已。
首先看B的实现,B只要构造RemoteViews对象并将其传输给A即可,这一过程通知栏是采用Binder实现的,但是本例中采用广播来实现,RemoteViews对象通过Intent传输A中,代码如下所示。
public class BActivity extends Activity implements View.OnClickListener { private Button btn_send; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_b); initView(); } private void initView() { btn_send = (Button) findViewById(R.id.btn_send); btn_send.setOnClickListener(this); } @Override public void onClick(View v) { switch (v.getId()) { case R.id.btn_send: RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.layout_item); remoteViews.setTextViewText(R.id.textView1, "Hello"); remoteViews.setImageViewResource(R.id.imageview1, R.mipmap.ic_launcher); PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, new Intent(this, TestActivity.class), PendingIntent.FLAG_UPDATE_CURRENT); remoteViews.setOnClickPendingIntent(R.id.imageview1, pendingIntent); Intent intent = new Intent("send_bro"); sendBroadcast(intent); break; } }}
A的代码比较简单只是接收一个广播就行
public class AActivity extends Activity { private LinearLayout mLinearLayout; private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { RemoteViews remoteViews = intent.getParcelableExtra("send_bro"); if (remoteViews != null) { updateUI(remoteViews); } } }; private void updateUI(RemoteViews remoteViews) { View view = remoteViews.apply(this, mLinearLayout); mLinearLayout.addView(view); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_a); initView(); } private void initView() { mLinearLayout = (LinearLayout) findViewById(R.id.mLinearLayout); IntentFilter intent = new IntentFilter("send_bro"); registerReceiver(mBroadcastReceiver, intent); } @Override protected void onDestroy() { super.onDestroy(); unregisterReceiver(mBroadcastReceiver); }}
上述代码很简单,除了注册和解除广播以外,最主要的逻辑其实就是updateUI,当A收到广播后,会从Intent中取出RemoteViews对象,然后通过apply方法加载布局并且执行更新操作,最后将得到的View添加到A的布局中即可。可以发现这个过程很简单,但是通知栏的底层是如何实现的呢?
木节这个例子是可以在实际中使用的,比如现在有两个应用,一个应用需要能够事更新另一个应用中的某个界面,这个时候我们当然可以选择AIDL去实现,但是如果对界面的更新比较频繁,这个时候就会有效率问题,同时AIDL接口就有可能会变得很复杂。这个时间如果采用RemoteViews来实现就没有这个问题了,当然RemoteViews也有缺点,那就是他只支持一些常见的View,对于自定义View它是不支持的。面对这种问题,到底是采用AIDL还是采用RemoteViews,这个要看具体情况,如果界面中的View都是一些简单的且被RemoteViews支持的View,那么可以考虑采用RemoteViews,否则就不适合用RemoteViews 了。
如果打算采用RemoteViews来实现两个应用之间的界面更新,那么这里还有一个问题,那就是布局文件的加载问题。在上面的代码中,我们直接通过RemoteViews的的apply方法来加载并更新界面,如下所示。’
View view = remoteViews.apply(this, mLinearLayout); mLinearLayout.addView(view);
这种写法在同一个应用的多进程情形下是适用的,但是如果A和B属于不同应用,那么B中的布局文件的资源id传输到A中以后很有可能是无效的,因为A中的这个布局文件的资源id不可能刚好和B中的资源id一样,面对这种情况,我们就要适当的修改Remoteviews的显示过程的代码了。这里给出一种方法,既然资源不相同,那我们就通过资源名称来加载布局文件。首先两个应用要提前约定好RemoteViews中的布局的文件名称,比如“layout simulated notification”,然后在A中根据名称找到并加载,接着再调用Remoteviews 的的reapply方法即可将B中对View所做的一系列更新操作加载到View上了,关于applyHe reapply方法的差别在前面说了,这样历程就OK了
int layoutId = getResources().getIdentifier("layout_simulated_notification","layout",getPackageName()); View view = getLayoutInflater().inflate(layoutId,mLinearLayout,false); remoteViews.reapply(this,view); mLinearLayout.addView(view);
五.参考资料
这篇博客参考了很多其他的博客,这里就当自己的一个笔记好了
《android艺术开发探索》
https://blog.csdn.net/qq_26787115/article/details/54427183
RemoteViews详细解释
更多相关文章
- 关于getting 'android:label' attribute: attribute is not a st
- Android 反编译apk 到java源码的方法
- 混淆Android JAR包的方法
- Android通知栏的变化
- eclipse:打开 eclipse 出现 “android sdk content loader 0%”
- Android Service的使用方法 音乐播放器实例
- Android定制RadioButton样式三种实现方法