Android 开发艺术探索笔记 第五章 理解RemoteViews

RemoteViews的应用

RemoteViews在实际开发中,主要用在通知栏和桌面小部件的开发过程中。通知栏通过NotificationManager的notify实现的,桌面小部件则是通过AppWidgetProvider来实现的。

  1. RemoteViews在通知栏上的应用
    RemoteViews使用的时候提供当前应用的包名和布局文件的资源id即可创建一个RemoteViews对象,更新则通过一系列set方法进行更新,点击事件则要使用PendingIntent并通过setOnClickPendingIntent方法来实现。
  2. RemoteViews在桌面小部件上的应用
    AppWidgetProvider是Android中提供的实现桌面小部件的类,其本质是一个广播,即BroadcastReceiver。桌面小部件的定义,主要是通过如下几个方面:
    1. 定义小部件界面
    2. 定义小部件配置信息
    3. 定义小部件的实现类
    4. 在AndroidManifest.xml中声明小部件

实现一个小部件的代码可以在书中源码`https://github.com/singwhatiwanna/android-art-res/tree/master/Chapter_5 找到,这里就不贴出来了,不过声明小部件的时候:

<receiver android:name=".MyAppWidgetProvider">
            <meta-data
                android:name="android.appwidget.provider"
                android:resource="@xml/appwidget_provider_info">
            </meta-data>

            <intent-filter>
                <action android:name="com.ryg.chapter_5.action.CLICK"/>
                <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
            </intent-filter>
        </receiver>

上面代码代码中有两个Action,第一个用于识别小部件的单击行为,第二个作为小部件的表示必须存在。

AppWidgetProvider除了最常用的onUpdate方法,还有其它几个方法:onEnable、onDisabled、onDeleted以及onReceive。当广播来到以后,AppWidgetProvider会根据广播的Action通过onReceiver方法来自动分发广播,调用时机如下:
- onEnable:当该窗口小部件第一次添加到桌面时调用该方法,可添加多次但只在第一次调用。
- onUpda:小部件被添加时或者每次小部件更新时都会调用一次该方法,小部件的更新时机由updatePeriodMillis来指定,每个周期小部件都会自动更新一次。
- onDeleted:没删除一次桌面小部件就调用一次
- onDisab:当最后一个该类型的桌面小部件被删除时调用该方法
- onReceiver:广播的内置方法,用于分发具体的事件给其它方法
3. PendingIntent概述

PendingIntent表示一种处于pending状态的意图,接下来有一个Intent将在某个待定的时刻发生。PendingIntent和Intent的区别就是,PendingIntent实在将来的某个不确定的时刻发生,而Intent是立刻发生。

给RemoteViews设置单击事件,必须使用PendingIntent,PendingIntent通过send和cancel方法来取消特定的待定Intent。
PendingIntent支持三种特定的意图:启动Activity、启动Service和发送广播。
对应着三个接口方法:
1. getActivity(Context context, int requestCode,
Intent intent, @Flags int flags)
2. getService(Context context, int requestCode,
Intent intent, @Flags int flags)
3. getBroadcast(Context context, int requestCode,
Intent intent, @Flags int flags)
三个方法的参数意义都是相同的,requestCode表示PendingIntent发送方的请求码,多数情况下设为0即可,requestCode会影响到flags的效果。
flags常见的类型有四种,我们首先要明白PendingIntent的匹配规则:

如果两个PendingIntent内部的Intent相同并且requestCode也相同,那么这两个PendingIntent就是相同的。
Intent的匹配规则:如果两个Intent的ComponentName和intent-filter都相同,那么这两个Intent就是相同的,Extras不参与Intent的匹配过程。

四种flag如下:

FLAG_ONE_SHOT
当前描述的PendingIntent只能被使用一次,然后就会被自动cancel,如果后续还有相同的PendingIntent,send方法就会调用失败。对于通知消息栏来说,采用这个标记位,同类的通知只能使用一次。

FLAG_NO_CREATE
当前描述的PendingIntent不会主动创建,如果当前PendingIntent之前不存在,那么getActivity、getService和getBroadcast方法会直接返回null,即获取PendingIntent失败。无法单独使用。

FLAG_CANCEL_CURRENT
当前描述的PendingIntent如果已经存在,那么它们都会被cancel,然后系统会创建一个新的PendingIntent。对于通知栏消息,那些被cancel的消息单击后将无法打开。

FLAG_UPDATE_CURRENT
当前描述的PendingIntent如果已经存在,那么它们都会被更新,即它们的Intent中的Extras会被替换成最新的。

manager.notify(1, notification),如果notify的第一个参数id是常量,那么多次调用notify只能弹出一个通知,每次id不同,多次调用会弹出多个通知。

如果notify的方法是常量,那么不管PendingIntent是否匹配,后面的通知会直接替换掉前面的通知。

如果每次不同,那么当PendingIntent不匹配时,这种情况下采用何种标记位,这些通知之间不会相互干扰。如果PendingIntent处于匹配状态,这个时候分情况讨论:
1. 采用FLAG_ONE_SHOT标记位,后续通知中的PendingIntent会和第一条通知保持完全一致,包括其中的Extras,反击任何一条通知,剩下的通知无法再打开,所有通知清除后,会再次重复这个过程。
2. 采用FLAG_CANCEL_CURRENT标记位,那么最新的标记位可以打开,之前弹出的所有通知均无法打开。
3. 采用FLAG_UPDATE_CURRENT标记位,那么之前弹出通知中的PendingIntent会被更新,最终它们和最新的一条通知保持完全一致,包括其中的Extras,这些通知都是可以打开的。

RemoteViews的内部机制

构造方法public RemoteViews(String packageName, int layoutId) 接受两个参数,第一个表示当前应用的包名,第二个参数表示待加载的布局文件。RemoteViews并不支持所有的view类型,也不支持自定义view。
RemoteViews也没有提供findViewById方法,必须通过RemoteViews提供的一些列set方法来完成,这是因为RemoteViews在远程进程中显示,没有办法直接findViewById。大部分set方法都是通过反射来完成的。

通知栏桌面小部件分别由NotificationManager和AppWidgetManager管理,而NotificationManager和AppWidgetManager通过Binder分别和SystemServer进程中的NotificationManagerService和AppWidgetManagerService进行通信。通知栏和桌面小部件中的布局文件实际上是在NotificationManagerService以及AppWidgetService中被加载的,而它们运行在系统的SystemServer中,就和我们的进程构成了跨进程通信的场景。

首先RemoteViews会通过Binder传递到SystemServer进程,因为RemoteViews实现了Parcelable接口,可以跨进程传输。
然后会通过LayoutInflater去加载RemoteViews中的布局文件。

系统没有通过Binder去直接支持View的跨进程访问,而是提供了一个Action的概念,Action代表一个View操作,Action同样实现了Parcelable接口。系统将View操作封装到Action对象并将这些对象传输到远程进程,接着在远程进程中执行Action对象中的具体操作。远程进程通过RemoteViews的apply方法来进行view的更新操作,RemoteViews的apply方法内部则会去遍历所有的Action 对象并调用它们的apply方法,具体的View更新操作是由Action对象的apply方法来完成的。

分析一下RemoteViews的内部源码:

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的对象,下面看一下addAction的实现:

private void addAction(Action a) {
        ...
        if (mActions == null) {
            mActions = new ArrayList<Action>();
        }
        mActions.add(a);

        // update the memory usage stats
        a.updateMemoryUsageEstimate(mMemoryUsageCounter);
    }

RemoteViews内部有一个mAction成员,它是一个ArrayList,将Action对象保存起来,并未对View进行实际的操作。我们看一下RemoteViews的apply方法:

public View apply(Context context, ViewGroup parent, OnClickHandler handler) {
        RemoteViews rvToApply = getRemoteViewsToApply(context);

        View result = inflateView(context, rvToApply, parent);
        loadTransitionOverride(context, handler);

        rvToApply.performApply(result, parent, handler);

        return result;
    }

private View inflateView(Context context, RemoteViews rv, ViewGroup parent) {
        ...
        LayoutInflater inflater = (LayoutInflater)
                context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

        // Clone inflater so we load resources from correct context and
        // we don't add a filter to the static version returned by getSystemService.
        inflater = inflater.cloneInContext(inflationContext);
        inflater.setFilter(this);
        return inflater.inflate(rv.getLayoutId(), parent, false);
    }

上面代码可以看出,首先通过LayoutInflate去加载RemoteViews中的布局文件,加载完布局文件后会通过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);
            }
        }
    }

遍历mActions这个列表并执行每个Action对象的Apply方法,Action对象的apply方法就是真正操作View的地方。

调用RemoteViews的set方法时,并不会立刻更新它们的界面,必须通过NotificationManager的notify方法以及AppWidgetManager的updateAPPWidget才能更新它们的界面。AppWidgetManager的updateAPPWidget的内部实现中,它们的确是通过RemoteViews的apply以及reapply方法来加载或者更新界面的,apply和reApply的区别在于:apply会加载布局并更新界面,而reApply则只会更新界面。通知栏和桌面小部件在初始化界面时会调用apply方法,而在后续的更新界面时则会调用reApply方法。

我们看一下AppWidgetHostView的updateAPPWidget方法:

 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;
                }
            }

桌面小部件在更新界面时也是通过RemoteViews的reapply方法来实现的。

 private final 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;
        }

       ...
         @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);
            }
        }
    }

ReflectionAction表示的是一个反射动作,通过它对View的操作会以反射的方式来调用,其中getMethod就是根据方法名来得到反射所需的Method对象。

RemoteViews中只支持发起PendingIntent,不支持onClickListener那种模式。
setOnClickPendingIntent用于给普通的view设置单击事件,但是不能给集合中的View设置单击事件。如果要给ListView和StackView中添加单击事件,则必须将setPendingIntentTemplate和setOnClickFillInIntent组合使用才可以。

RemoteViews的意义

比如我们有两个应用,一个应用需要去更新另外一个应用中的某个界面,可以选择AIDL去实现,但是如果对象跟新比较频繁,就会有效率问题,这个时候就可以采用RemoteViews来实现,但是它仅支持一些常见的View。究竟采用哪种方案,要看具体情况,界面中的View都是一些简单的且被RemoteViews支持的view,那么就可以考虑RemoteViews,否则就不适合。

RemoteViews的布局文件加载问题,如果两个应用布局文件的资源ID不一样,我们就要通过资源名称来加载布局文件,两个应用提前约定好RemoteViews中的布局文件的资源名称,接着在调用RemoteViews的reapply方法即可更新view。

书中跨进程通信的程序在跨进程通信Demo可以找到。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值