您的位置:首页 > 移动开发 > Android开发

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

2017-01-12 21:49 465 查看

RemoteViews的应用

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

RemoteViews在通知栏上的应用

RemoteViews使用的时候提供当前应用的包名和布局文件的资源id即可创建一个RemoteViews对象,更新则通过一系列set方法进行更新,点击事件则要使用PendingIntent并通过setOnClickPendingIntent方法来实现。

RemoteViews在桌面小部件上的应用

AppWidgetProvider是Android中提供的实现桌面小部件的类,其本质是一个广播,即BroadcastReceiver。桌面小部件的定义,主要是通过如下几个方面:

定义小部件界面

定义小部件配置信息

定义小部件的实现类

在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可以找到。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: