上一篇博客中讲了通知栏和桌面小部件的简单使用,它们分别由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方法
从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来实现两个进程之间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详细解释