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详细解释

更多相关文章

  1. 关于getting 'android:label' attribute: attribute is not a st
  2. Android 反编译apk 到java源码的方法
  3. 混淆Android JAR包的方法
  4. Android通知栏的变化
  5. eclipse:打开 eclipse 出现 “android sdk content loader 0%”
  6. Android Service的使用方法 音乐播放器实例
  7. Android定制RadioButton样式三种实现方法

随机推荐

  1. Android 如何让EditText不自动获取焦点
  2. Android SDK ADT下载地址
  3. StudyJams第一课的学习
  4. Android添加一个系统service
  5. Android 远程视频监控程序源码
  6. Android Notebook
  7. Android学习札记12:对Parcelable中describ
  8. Android GPS 开发
  9. android防止EditText自动弹出软键盘
  10. android EditView不换行的原因