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. 【Android(安卓)Dev Guide - 03】 - Content Providers
  2. 《Android(安卓)Dev Guide》系列教程7:Android生命周期之service/
  3. Ubuntu共享WiFi(AP)给Android方法【修正版】
  4. Android(安卓)反编译apk 到java源码的方法
  5. Android完美解决输入框EditText隐藏密码打勾显示密码问题
  6. 关于getting 'android:label' attribute: attribute is not a st
  7. Android(安卓)下拉刷新框架实现
  8. 浅谈Java中Collections.sort对List排序的两种方法
  9. Python list sort方法的具体使用

随机推荐

  1. Android图片圆角 用简单的方法实现
  2. Android(安卓)APP修改全局字体
  3. Android(安卓)使用decodeFile方法加载手
  4. android中隐藏以及显示软键盘代码
  5. android获取指定路径下目录文件
  6. Android(安卓)Handler(七)
  7. Android(安卓)Opencore OpenMAX学习
  8. android获取位置权限,手机状态权限,存储权
  9. Android中四种OnClick事件的写法
  10. android隐藏底部虚拟键Navigation Bar实