合肥网页模板建站,谷歌广告投放,如何自己制作自己的网站,广西网络优化seo一、前言 RemoteViews 顾名思义就是远程 View#xff0c;它表示的是一个 View 结构#xff0c;它可以在其他进程中显示#xff0c;为了能跨进程更新它的界面#xff0c;RemoteViews 提供了一组基础的操作来实现这个效果。RemoteViews 在 Android 中的使用场景有两种#x…一、前言 RemoteViews 顾名思义就是远程 View它表示的是一个 View 结构它可以在其他进程中显示为了能跨进程更新它的界面RemoteViews 提供了一组基础的操作来实现这个效果。RemoteViews 在 Android 中的使用场景有两种通知栏和桌面小部件。 二、RemoteViews 的使用 2.1 通知栏
使用系统默认的样式弹出一个通知的方式代码如下android3.0之后
private void showDefaultNotification() {NotificationCompat.Builder builder new NotificationCompat.Builder(this);// 设置通知的基本信息icon、标题、内容builder.setSmallIcon(R.mipmap.ic_launcher);builder.setContentTitle(My notification);builder.setContentText(Hello World!);builder.setAutoCancel(true);// 设置通知的点击行为这里启动一个 ActivityIntent intent new Intent(this, SecondActivity.class);PendingIntent pendingIntent PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);builder.setContentIntent(pendingIntent);// 发送通知 id 需要在应用内唯一NotificationManager manager (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);manager.notify(id, builder.build());
} 上述代码会弹出一个系统默认样式的通知单击通知后会打开 SecondActivity 同时会清除本身。通知栏除了默认的效果外还支持自定义布局。实现自定义通知我们首先需要提供一个布局文件然后通过 RemoteViews 来加载这个布局文件即可改变通知的样式。样例代码如下
private void showCustomNotification() {RemoteViews remoteView;// 构建 remoteViewremoteView new RemoteViews(getPackageName(), R.layout.layout_notification);remoteView.setTextViewText(R.id.tvMsg, 哈shenhuniurou);remoteView.setImageViewResource(R.id.ivIcon, R.mipmap.ic_launcher_round);NotificationCompat.Builder builder new NotificationCompat.Builder(this);// 设置自定义 RemoteViewsbuilder.setContent(remoteView).setSmallIcon(R.mipmap.ic_launcher);// 设置通知的优先级(悬浮通知)builder.setPriority(NotificationCompat.PRIORITY_MAX);Uri alarmSound RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);// 设置通知的提示音builder.setSound(alarmSound);// 设置通知的点击行为这里启动一个 ActivityIntent intent new Intent(this, SecondActivity.class);PendingIntent pendingIntent PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);builder.setContentIntent(pendingIntent);builder.setAutoCancel(true);Notification notification builder.build();NotificationManager manager (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);manager.notify(1001, notification);
}
效果如下图所示 创建 RemoteViews 对象我们只需要知道当前应用包名和布局文件的资源 id比较简单但是要更新 RemoteViews 就不是那么容易了因为我们无法直接访问布局文件中的 View而必须通过 RemoteViews 提供的特定的方法来更新 View。比如设置 TextView 文本内容需要用 setTextViewText 方法设置 ImageView 图片需要通过 setImageViewResource 方法。也可以给里面的View设置点击事件需要使用 PendingIntent 并通过 setOnClickPendingIntent 方法来实现。之所以更新 RemoteViews 如此复杂直接原因是因为 RemoteViews 没有提供跟 View 类似的 findViewById 这个方法我们无法获取到 RemoteViews 中的子 View。 2.2 桌面小部件 三、PendingIntent 在 Android 中我们常常使用 PendingIntent 来表达一种“留待日后处理”的意思。从这个角度来说PendingIntent 可以被理解为一种特殊的异步处理机制。不过单就命名而言PendingIntent 其实具有一定误导性因为它既不继承于 Intent也不包含 Intent它的核心可以粗略地汇总成四个字——“异步激发”。很明显这种异步激发常常是要跨进程执行的。比如说 A 进程作为发起端它可以从系统“获取”一个 PendingIntent然后 A 进程可以将 PendingIntent 对象通过 binder 机制传递给 B 进程再由 B 进程在未来某个合适时机“回调” PendingIntent 对象的 send() 动作完成激发。
PendingIntent 是 Android 提供的一种用于外部程序调起自身程序的能力生命周期不与主程序相关。外部程序通过 PendingIntent 只能调用起三种组件Activity、Service、Broadcast。使用场景有三个使用 AlarmManager 设定闹钟、在系统状态栏显示 Notification、在桌面显示 Widget。PendingIntent 也只能通过下列的静态方法获取
// 获取 Broadcast 关联的 PendingIntent
PendingIntent.getBroadcast(Context context, int requestCode, Intent intent, int flags)
// 获取 Activity 关联的 PendingIntent
PendingIntent.getActivity(Context context, int requestCode, Intent intent, int flags)
PendingIntent.getActivity(Context context, int requestCode, Intent intent, int flags, Bundle options)
// 获取 Service 关联的 PendingIntent
PendingIntent.getService(Context context, int requestCode, Intent intent, int flags)
上面的 getActivity() 的意思其实是获取一个 PendingIntent 对象而且该对象日后激发时所做的事情是启动一个新 activity。也就是说当它异步激发时会执行类似 Context.startActivity() 那样的动作。相应地getBroadcast() 和 getService() 所获取的 PendingIntent 对象在激发时会分别执行类似 Context.sendBroadcast() 和 Context.startService() 这样的动作。
PendingIntent 是系统对于待处理数据的一个引用称之为token。当主程序被 Killed 时token 还是会继续存在的可以继续供其他进程使用。如果要取消 PendingIntent需要调用 PendingIntent 的 cancel 方法。对于 PendingIntent 容易误解的一点是如果创建了很多 PendingIntent只要 extra 中的数据不同的话以为就是两个不同的 PendingIntent 这种理解是错误的。Extras不参与 Intent 的匹配过程。正确区分不同 PendingIntent 有两种方法
PendingIntent.getXXX() 方法中的 requestCode 不同通过 Intent.filterEquals 测试时不相等
关于 PendingIntent.getXXX() 方法中第四个参数 flags在 PendingIntent 定义了四个比较常用的 FLAG
FLAG_CANCEL_CURRENT
如果新请求的 PendingIntent 发现已经存在时取消已存在的用新的 PendingIntent 替换。
FLAG_NO_CREATE
如果新请求的 PendingIntent 发现已经存在时忽略新请求的继续使用已存在的。日常开发中很少使用。
FLAG_ONE_SHOT
表示 PendingIntent 只能使用一次如果已使用过那么 getXXX(...) 将会返回 NULL 也就是说同类的通知只能使用一次后续的通知单击后将无法打开。
FLAG_UPDATE_CURRENT
如果新请求的 PendingIntent 发现已经存在时, 如果 Intent 有字段改变了就更新已存在的 PendingIntent。 四、RemoteViews 解析 RemoteViews 的作用是在其他进程中显示并更新 View 界面为了更好地理解它的内部机制我们先来看一下它的主要功能。首先看一下它的构造方法这里只介绍一个最常用的构造方法
public RemoteViews(String packageName, int layoutId) {this(getApplicationInfo(packageName, UserHandle.myUserId()), layoutId);
}
它接收两个参数第一个表示当前应用的包名第二个参数表示待加载的布局文件 id。RemoteViews 所支持 View 所有类型如下
Layout
FrameLyout、LinearLayout、RelativeLayout、GridLayout
View
Button、ImageView、ImageButton、ProgressBar、TextView、ListView、GridView、StackView、ViewStub、AdapterViewFlipper、ViewFlipper、AnalogClock、Chronometer
RemoteViews 目前并不能支持所有的 View 类型RemoteViews 不支持它们的子类以及其他 View 类型也无法使用自定义 View。比如我们在 RemoteViews 中使用系统的 EditText那么将会抛出异常。RemoteViews 没有提供 findViewById 方法因此无法直接访问里面的 View 元素而必须通过 RemoteViews 所提供的一系列 set 方法来完成当然这是因为 RemoteViews 是在远程进程中显示所以没办法直接 findViewById。下表列举了部分常用的 set 方法 从这些方法中看出原本可以直接调用的 View 的方法现在要通过 RemoteViews 的一系列 set 方法来完成。而且从方法的声明上来看很像是通过反射来完成的事实上大部分的 set 方法的确是通过反射来完成的。
下面描述一下 RemoteViews 的内部机制。我们知道通知栏和桌面小部件分别由 NotificationManager 和 AppWidgetManager 来管理的而 NotificationManager 和 AppWidgetManager 是通过 Binder 分别和 SystemServer 进程中的 NotificationManagerService 以及 AppWidgetService 进行通信。由此可见通知栏和桌面小部件中的布局文件实际上是在 NotificationManagerService 和 AppWidgetService 中被加载的而他们运行在系统的 SystemServer 中这其实已经和我们自己的 app 进程构成了跨进程通信。
首先 RemoteViews 会通过 Binder 传递到 SystemServer 进程因为 RemoteViews 实现了 Parcelable 接口因此可以跨进程传输系统会根据 RemoteViews 中的包名等信息去获取到该 app 的资源然后通过 LayoutInflater 去加载 RemoteViews 中的布局文件。在 SystemServer 进程中加载后的布局文件是一个普通的 View只不过相对于我们的 app 进程来说它是一个远程 View 也就是 RemoteViews。接着系统会对 View 执行一系列界面更新任务这些任务就是之前我们通过 set 方法提交的set 方法对 View 的更新操作并不是立刻执行的在 RemoteViews 内部会记录所有的更新操作具体的执行时机要等到 RemoteViews 被完全加载以后才能执行这样 RemoteViews 就可以在 SystemServer 进程中显示了这就是我们所看到的通知栏消息或者桌面小部件。当需要更新 RemoteViews 时我们又需要调用一系列 set 方法通过 NotificationManager 和 AppWidgetManager 来提交更新任务具体更新操作也是在 SystemServer 进程中完成的。
理论上讲系统完全可以通过 Binder 去支持所有的 View 和 View 操作但是这样做代价太大因为 View 的方法太多了另外大量的 IPC 操作会影响效率。为了解决这个问题系统并没有通过 Binder 去直接支持 View 的跨进程访问而是提供了一个 Action 的概念Action 代表一个View操作Action 同样实现了 Parcelable 接口。系统首先将 View 操作封装到 Action 对象并将这些对象跨进程传输到远程进程接着在远程进程中执行 Action 对象中的具体操作。在我们的 app 中每调用一次 set 方法RemoteViews 中就会添加一个对应的 Action 对象当我们通过 NotificationManager 和 AppWidgetManager 来提交我们的更新时这些 Action 对象就会传输到远程进程并在远程进程中依次执行。这个过程可以参看下图 远程进程通过 RemoteViews 的 apply 方法来进行 View 的更新操作apply 方法内部是去遍历所有的 Action 对象并调用它们的 apply 方法具体的 View 更新操作是由 Action 对象的 apply 方法来完成。上述做法的好处首先是不需要定义大量的 Binder 接口其次通过在远程进程中批量执行 RemoteViews 的更新操作从而避免了大量的 IPC 操作这就提高了程序的性能。
上面从理论上分析了 RemoteViews 的内部机制接下来我们从源码的角度再来分析 RemoteViews 的工作流程。首先我们从 RemoteViews 的 set 方法入手比如 setTextViewText它内部实现是这样的
public void setTextViewText(int viewId, CharSequence text) {setCharSequence(viewId, setText, text);
}
在上面的代码中viewId 是被操作的 View 的 idsetText 是方法名text 是要给 TextView 设置的文本这里联想一下 TextView 的 setText 方法是不是很一致呢接着再看 setCharSequence 的实现代码如下
public void setCharSequence(int viewId, String methodName, CharSequence value) {addAction(new ReflectionAction(viewId, methodName, ReflectionAction.CHAR_SEQUENCE, value));
}
从 setCharSequence 的实现可以看出它的内部并没有对 View 进程直接的操作而是添加了一个 ReflectionAction 对象从名字来看这应该是一个反射类型的动作。再看 addAction 的实现代码如下
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 ArrayListAction();}mActions.add(a);// update the memory usage statsa.updateMemoryUsageEstimate(mMemoryUsageCounter);
}
上述代码可以看到在 RemoteViews 内部维护了一个名为 mActions 的 ArrrayList外界每调用一次 set 方法RemoteViews 就会为其创建一个 Action 对象并加入到这个 ArrayList 中。注意仅仅是将 Action 对象添加进来保存并没有去执行这些对 View 实际操作的 Action。到这里 setTextViewText 方法的源码已经结束了下面我们要弄清楚这些 Action 的执行。首先看一下 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) {final Context contextForResources getContextForResources(context);Context inflationContext new RemoteViewsContextWrapper(context, contextForResources);LayoutInflater inflater (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);inflater inflater.cloneInContext(inflationContext);inflater.setFilter(this);View v inflater.inflate(rv.getLayoutId(), parent, false);v.setTagInternal(R.id.widget_frame, rv.getLayoutId());return v;
}
从上面代码可以看出首先会通过 LayoutInflater 去加载 RemoteViews 中的布局文件RemoteViews 中的布局文件可以通过 getLayoutId 这个方法获得加载完布局文件后会通过 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 方法。每一次的 set 操作都会对应它里面的一个 Action 对象Action 对象的 apply 方法就是真正操作 View 的地方。
当我们调用 RemoteViews 的 set 方法时并不会立刻更新它们的界面而必须要通过 NotificationManager 的 notify 方法以及 AppWidgetManager 的 updateAppWidget 方法才能更新它们的界面。实际上在 AppWidgetManager 的 updateAppWidget 内部实现中的确是通过 RemoteViews 的 apply 方法和 reApply 方法来加载或更新界面的apply 和 reApply 的区别在于apply 会加载布局并更新界面而 reApply 则只会更新界面通知栏和桌面小插件在初始化时调用 apply 方法而在后续的更新则调用 reApply 方法。
我们再继续看一些 Action 的子类的具体实现首先看一下 RelectionAction 的具体实现代码如下
private final class ReflectionAction extends Action {String methodName;int type;Object value;ReflectionAction(int viewId, String methodName, int type, Object value) {this.viewId viewId;this.methodName methodName;this.type type;this.value value;}Overridepublic 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 对象。使用 ReflectionAction 的 set 方法有setTextViewText、seetBoolean、setLong、setDouble 等。除了 ReflectionAction还有其他 Action比如 TextViewSizeAction、ViewPaddingAction、SetOnClickPendingIntent 等。这里再分析一下 TextViewSizeAction它的代码如下
private class TextViewSizeAction extends Action {public TextViewSizeAction(int viewId, int units, float size) {this.viewId viewId;this.units units;this.size size;}...Overridepublic void apply(View root, ViewGroup rootParent, OnClickHandler handler) {final TextView target root.findViewById(viewId);if (target null) return;target.setTextSize(units, size);}public String getActionName() {return TextViewSizeAction;}int units;float size;public final static int TAG 13;
}
TextViewSizeAction 的实现比较简单它之所以不用反射来实现是因为 setTextSize 这个方法有 2 个参数因此无法复用 ReflectionAction因为 ReflectionAction 的反射调用只有一个参数。
RemoteViews 中的单击事件只支持发起 PendingIntent不支持 onClickListener 这种方法。我们需要注意 setOnClickPendingIntent、setPendingIntentTemplate 和 setOnClickFillInIntent 这几个方法之间的区别和联系。setOnClickPendingIntent 是用于给普通的 View 设置点击事件但是它不能给 ListView 或者 GridView、StackView 中的 item 设置点击事件因为开销比较大系统禁止了这种方式。而如果要给 ListView 和 StackView 中的 item 添加单击事件则必须将 setPendingIntentTemplate 和 setOnClickFillInIntent 组合使用才可以。
实际开发中跨进程通信我们可以选择 AIDL 去实现但是如果对界面的更新比较频繁这个时候就会有效率问题而且 AIDL 接口可能会变得很复杂但如果采用 RemoteViews 来实现就没有这个问题了RemoteViews 的缺点就是它仅支持一些常见的 View而对于自定义 View 是不支持的。