Android学习--深入探索RemoteViews
什么是RemoteViews
RemoteViews表示的是一个View结构,它可以在其他进程中显示,由于它在其他进程中显示,为了能够及时更新它的界面,RemoteViews提供了一组基础的操作来跨进程更新它的界面。源码中对于它的解释如下:
/** * A class that describes a view hierarchy that can be displayed in * another process. The hierarchy is inflated from a layout resource * file, and this class provides some basic operations for modifying * the content of the inflated hierarchy. */
RemoteViews
相信很多人跟我一样觉得这可能是一个View或是layout。真的是这样吗?其实不然上面描述中说到它是描述一个View结构,并不是一个View,下面我们来通过源码看看RemoteViews
public class RemoteViews implements Parcelable, Filter { ...... }
从它的继承方式来看,它跟View和Layout并没有什么关系。下面我们来看看RemoteViews
如何使用。
RemoteViews的应用场景
1、应用于通知栏
2、应用于桌面小部件
RemoteViews的使用
前面说了RemoteViews
用于通知栏和桌面小部件,下面我们一个个来看RemoteViews
是怎么使用的。
RemoteViews在通知栏中使用
RemoteViews
在通知栏中的应用还是比较简单的,话不多说我们直接撸代码
Notification notification = new Notification(); notification.icon = R.mipmap.ic_launcher; notification.tickerText = "hello notification"; notification.when = System.currentTimeMillis(); notification.flags = Notification.FLAG_AUTO_CANCEL; Intent intent = new Intent(this, RemoteViewsActivity.class); PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.layout_notification);//RemoveViews所加载的布局文件 remoteViews.setTextViewText(R.id.tv, "RemoteViews应用于通知栏");//设置文本内容 remoteViews.setTextColor(R.id.tv, Color.parseColor("#abcdef"));//设置文本颜色 remoteViews.setImageViewResource(R.id.iv, R.mipmap.ic_launcher);//设置图片 PendingIntent openActivity2Pending = PendingIntent.getActivity (this, 0, new Intent(this, MainActivity.class), PendingIntent.FLAG_UPDATE_CURRENT);//设置RemoveViews点击后启动界面 remoteViews.setOnClickPendingIntent(R.id.tv, openActivity2Pending); notification.contentView = remoteViews; notification.contentIntent = pendingIntent; NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); manager.notify(2, notification);
RemoteViews在桌面小部件中的应用
桌面小部件是通过AppWidgetProVider
来实现的,而AppWidgetProVider
继承自BroadcastReceiver,所以可以说AppWidgetProVider
是个广播。
1、定义小部件的界面
首先,我们需要在xml文件中定义好桌面小部件的界面。
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <ImageView android:id="@+id/iv" android:layout_width="360dp" android:layout_height="360dp" android:layout_gravity="center" /> </LinearLayout>
2、定义小部件的配置信息
在res/xml/下新建一个xml文件,用来描述桌面部件的配置信息。
<?xml version="1.0" encoding="utf-8"?> <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" android:initialLayout="@layout/widget" android:minHeight="360dp" android:minWidth="360dp" android:updatePeriodMillis="864000"/>
3、定义小部件的实现类
这个类需要继承AppWidgetProVider
,我们这里实现一个简单的widget,点击它后,3张图片随机切换显示。
public class ImgAppWidgetProvider extends AppWidgetProvider { public static final String TAG = "ImgAppWidgetProvider"; public static final String CLICK_ACTION = "packagename.action.click"; private static int index; @Override public void onReceive(Context context, Intent intent) { super.onReceive(context, intent); if (intent.getAction().equals(CLICK_ACTION)) { RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget); AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); updateView(context, remoteViews, appWidgetManager); } } @Override public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { super.onUpdate(context, appWidgetManager, appWidgetIds); RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget); updateView(context, remoteViews, appWidgetManager); } // 由于onReceive 和 onUpdate中部分代码相同 则抽成一个公用方法 public void updateView(Context context, RemoteViews remoteViews, AppWidgetManager appWidgetManager) { index = (int) (Math.random() * 3); if (index == 1) { remoteViews.setImageViewResource(R.id.iv, R.mipmap.haimei1); } else if (index == 2) { remoteViews.setImageViewResource(R.id.iv, R.mipmap.haimei2); } else { remoteViews.setImageViewResource(R.id.iv, R.mipmap.haimei3); } Intent clickIntent = new Intent(); clickIntent.setAction(CLICK_ACTION); PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, clickIntent, 0); remoteViews.setOnClickPendingIntent(R.id.iv, pendingIntent); appWidgetManager.updateAppWidget(new ComponentName(context, ImgAppWidgetProvider.class), remoteViews); } }
4、在AndroidManifest.xml中声明小部件
因为桌面小部件的本质是一个广播组件,因此必须要注册。
<receiver android:name=".RemoveViews_5.ImgAppWidgetProvider"> <meta-data android:name="android.appwidget.provider" android:resource="@xml/appwidget_provider_info"> </meta-data> <intent-filter> <action android:name="packagename.action.click" /> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> </intent-filter> </receiver>
代码中有两个action,第一个是小部件的点击事件,第二个是小部件的标识必须存在的,如果不加它就不是一个小部件也不会显示在手机桌面。
小部件的生命周期
onEnable:
当小部件第一次添加到桌面的时候调用,小部件可以添加多次,但是只在第一次添加的时候调用。
onUpdate:
小部件被添加时或小部件每次更新时都会调用这个方法。每个周期小部件都会自动更新一次,不是点击的时候更新,而是到指定配置文件时间的时候才更新。
onDelete:
每删除一次小部件就调用一次。
onDisable:
当最后一个该类型的小部件删除时调用该方法。
onRceive:
这是广播的内置方法,用于分发具体的事件给其他方法,所以该方法一半要调用super.onReceive(context,intent)
。如果自定义了其他action的广播,就可以在调用了父类方法之后进行判断。
PendingIntent介绍
PendingIntent
表示一种处于Pending状态的意图,而pending状态就是表示接下来有一个Intent(即意图)将在某个待定的时刻发生。它和Intent
的区别就在于,Intent
是立刻、马上发生,而PendingInten
是将来某个不确定的时刻发生。
PendingIntent的主要方法
PendingIntent
支持三种待定意图:启动Activity,启动Service和发送广播,分别对应着它的三个接口方法:
getActivity(Context xontext,int requestCode,Intent intent,int flag)
: 获得一个PendingIntent,该待定意图发生时,效果相当于Context.startActivity(intent)
getService(Context xontext,int requestCode,Intent intent,int flag)
: 获得一个PendingIntent,该待定意图发生时,效果相当于Context.startService(intent)
getBroadcast(Context xontext,int requestCode,Intent intent,int flag)
: 获得一个PendingIntent,该待定意图发生时,效果相当于Context.sendBroadcast(intent)
这里有四个参数,第一个和第三个比较好理解,第二个表示的是PendingIntent发送方的请求码,大多数情况为0,第四个参数flag常见的类型有四种。
PendingIntent的flag参数
FLAG_ONE_SHOT
: 当前描述的PendingIntent只能被使用一次,之后被自动cancle,如果后续还有相同的PendingIntent,那么他们的send方法会调用失败。
FLAG_NO_CREATE
: 当前描述的PendingIntent不会主动创建,如果当前PendingIntent之前不存在,那么getActivity,getService,getBroadcast方法会直接返回null。
FLAG_CANCLE_CURRENT
: 当前的PendingIntent如果已经存在,那么它们都会被cancle,然后系统会创建一个新的PendingIntent。对于通知栏来说那些被calcle的消息单机后将无法打开。
FLAG_UPDATE_CURRENT
: 当前的PendingIntent如果已经存在,那么它们都会被更新,即它们的Intent中的Extras会被替换为最新的。
通知栏而言,notify(int, notification)方法中,若id值每次都不同的话,需要考虑到flag参数对应消息接收的情况。
RemoteViews的内部机制
RemoteViews
没有findViewById方法,因此无法访问里面的View元素,而必须通过RemoteViews提供的一系列set方法来完成,这是通过反射调用的。
通知栏和小部件分别由NotificationManager
和AppWidgetManger
管理,而NotificationManager
和AppWidgetManger
通过Binder分别和SystemService进程中的NotificationManagerService
和AppWidgetMangerService
中加载的,而它们运行在SystemService中,这就构成了跨进程通信。
构造方法
public RemoteViews(String packageName, int layoutId)
第一个参数是包名,第二个参数是待加载的布局文件。
支持组件
布局:FrameLayout、LinearLayout、RelativeLayout、GridLayout。
组件:Button、ImageButton、ImageView、ProgressBar、TextView、ListView、GridView、ViewStub等(例如EditText是不允许在RemoveViews中使用的,使用会抛异常)。
工作原理
系统将View操作封装成Action对象,Action同样实现了Parcelable接口,通过Binder传递到SystemServer进程。远程进程通过RemoteViews的apply
方法来进行view的更新操作,RemoteViews的apply
方法内部则会去遍历所有的action对象并调用它们的apply
方法来进行view的更新操作。
这样做的好处是不需要定义大量的Binder接口,其次批量执行RemoteViews中的更新操作提高了程序性能。
工作流程
首先RemoteViews
会通过Binder传递到SystemService
进程,因为RemoteViews
实现了Parcelable接口,因此它可以跨进程传输,系统会根据RemoteViews
的包名等信息拿到该应用的资源;然后通过LayoutInflater
去加载RemoteViews
中的布局文件。接着系统会对View进行一系列界面更新任务,这些任务就是之前我们通过set来提交的。set方法对View的更新并不会立即执行,会记录下来,等到RemoteViews被加载以后才会执行。这样RemoteViews
就可以在SystemService进程中显示了。
这里需要注意一个小知识点就是apply
和reApply
方法的区别,apply
会加载布局并且更新界面,而reApply
只会更新界面。
源码分析
我们下面基于android8.0的源码看看RemoteViews,set方法之后的逻辑是怎么样的,以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<Action>(); } mActions.add(a); // update the memory usage stats a.updateMemoryUsageEstimate(mMemoryUsageCounter); }
这里仅仅把action加入了list。下面我们通过NotificationManager
的notify
方法来看看。
public void notify(String tag, int id, Notification notification) { notifyAsUser(tag, id, notification, new UserHandle(UserHandle.myUserId())); }
进一步查看
public void notifyAsUser(String tag, int id, Notification notification, UserHandle user) { INotificationManager service = getService(); String pkg = mContext.getPackageName(); // Fix the notification as best we can. Notification.addFieldsFromContext(mContext, notification); if (notification.sound != null) { notification.sound = notification.sound.getCanonicalUri(); if (StrictMode.vmFileUriExposureEnabled()) { notification.sound.checkFileUriExposed("Notification.sound"); } } fixLegacySmallIcon(notification, pkg); if (mContext.getApplicationInfo().targetSdkVersion > Build.VERSION_CODES.LOLLIPOP_MR1) { if (notification.getSmallIcon() == null) { throw new IllegalArgumentException("Invalid notification (no valid small icon): " + notification); } } if (localLOGV) Log.v(TAG, pkg + ": notify(" + id + ", " + notification + ")"); final Notification copy = Builder.maybeCloneStrippedForDelivery(notification); try { service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName(), tag, id, copy, user.getIdentifier()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } }
我们注意到这里最终调用了INotificationManager
的enqueueNotificationWithTag
方法,这里INotificationManager
是aidl,通过Binder通信,真正实现它的Java类是NotificationManagerService
,下面继续跟进这个方法:
@Override public void enqueueNotificationWithTag(String pkg, String opPkg, String tag, int id, Notification notification, int userId) throws RemoteException { enqueueNotificationInternal(pkg, opPkg, Binder.getCallingUid(), Binder.getCallingPid(), tag, id, notification, userId); }
进一步查看enqueueNotificationInternal
的源码,这个方法代码有点多,我们只看重要部分。
void enqueueNotificationInternal(final String pkg, final String opPkg, final int callingUid, final int callingPid, final String tag, final int id, final Notification notification, int incomingUserId) { if (DBG) { Slog.v(TAG, "enqueueNotificationInternal: pkg=" + pkg + " id=" + id + " notification=" + notification); } checkCallerIsSystemOrSameApp(pkg); final int userId = ActivityManager.handleIncomingUser(callingPid, callingUid, incomingUserId, true, false, "enqueueNotification", pkg); final UserHandle user = new UserHandle(userId); ...... 省略部分代码 ...... mHandler.post(new EnqueueNotificationRunnable(userId, r)); }
这里我们看到通过Handler来post了一个Runable对象,暂且先不管这个Runable干啥的我们往下看它的run
方法:
protected class EnqueueNotificationRunnable implements Runnable { private final NotificationRecord r; private final int userId; EnqueueNotificationRunnable(int userId, NotificationRecord r) { this.userId = userId; this.r = r; }; @Override public void run() { synchronized (mNotificationLock) { mEnqueuedNotifications.add(r); scheduleTimeoutLocked(r); ...... 省略部分代码 ...... } else { mHandler.post(new PostNotificationRunnable(r.getKey())); } } } }
我们看到这里它又post了一个PostNotificationRunnable
对象,这又是什么鬼,我们接着往下看:
protected class PostNotificationRunnable implements Runnable { private final String key; PostNotificationRunnable(String key) { this.key = key; } @Override public void run() { synchronized (mNotificationLock) { try { ...... 省略部分代码 ...... // ATTENTION: in a future release we will bail out here // so that we do not play sounds, show lights, etc. for invalid // notifications Slog.e(TAG, "WARNING: In a future release this will crash the app: " + n.getPackageName()); } buzzBeepBlinkLocked(r); } finally { int N = mEnqueuedNotifications.size(); for (int i = 0; i < N; i++) { final NotificationRecord enqueued = mEnqueuedNotifications.get(i); if (Objects.equals(key, enqueued.getKey())) { mEnqueuedNotifications.remove(i); break; } } } } } }
我们看到它最终调用了buzzBeepBlinkLocked
方法,我们进一步查看它的源码:
@VisibleForTesting @GuardedBy("mNotificationLock") void buzzBeepBlinkLocked(NotificationRecord record) { ...... // Should this notification make noise, vibe, or use the LED? ...... // If we're not supposed to beep, vibrate, etc. then don't. ...... // Remember if this notification already owns the notification channels. ...... if (disableEffects == null && canInterrupt && mSystemReady && mAudioManager != null) { if (DBG) Slog.v(TAG, "Interrupting!"); Uri soundUri = record.getSound(); hasValidSound = soundUri != null && !Uri.EMPTY.equals(soundUri); long[] vibration = record.getVibration(); // Demote sound to vibration if vibration missing & phone in vibration mode. ...... // If a notification is updated to remove the actively playing sound or vibrate, // cancel that feedback now if (wasBeep && !hasValidSound) { clearSoundLocked(); } if (wasBuzz && !hasValidVibrate) { clearVibrateLocked(); } // light // release the light ...... if (buzz || beep || blink) { MetricsLogger.action(record.getLogMaker() .setCategory(MetricsEvent.NOTIFICATION_ALERT) .setType(MetricsEvent.TYPE_OPEN) .setSubtype((buzz ? 1 : 0) | (beep ? 2 : 0) | (blink ? 4 : 0))); EventLogTags.writeNotificationAlert(key, buzz ? 1 : 0, beep ? 1 : 0, blink ? 1 : 0); } }
这个方法很长,但是职责相对来说比较明确,确认是否需要声音,震动和闪光,如果需要,那么就发出声音,震动和闪光。最后将mBuzzBeepBlinked
post到工作handler,最后会调用到mStatusBar.buzzBeepBlinked()
,mStatusBar是StatusBarManagerInternal
对象,这个对象是在StatusBarManagerService
中初始化,所以最后调用到了StatusBarManagerService中StatusBarManagerInternal
的buzzBeepBlinked()
方法:
public void buzzBeepBlinked() { if (mBar != null) { try { mBar.buzzBeepBlinked(); } catch (RemoteException ex) { } } }
mBar是一个IStatusBar
对象。关于更进一步的分析看这里源码分析Notification的Notify。我们最终发现是调用到了CommandQueue
中,接着sendEmptyMessage给了内部的H类,接着调用了mCallbacks.buzzBeepBlinked()方法,这个mCallbacks就是BaseStatusBar,最终会将notification绘制出来,到这里一个notification就算是完成了。
RemoteViews的意义
RemoteViews最大的意义应该还是在于它可以跨进程更新UI。
1、当一个应用需要更新另一个应用的某个界面,我们可以选择用AIDL来实现,但如果更新比较频繁,效率会有问题,同时AIDL接口就可能变得很复杂。如果采用RemoteViews就没有这个问题,但RemoteViews仅支持一些常用的View,如果界面的View都是RemoteViews所支持的,那么就可以考虑采用RemoteViews。
2、利用RemoteViews加载其他App的布局文件与资源。
感谢
《Android开发艺术探索》

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
Android 滑动定位+吸附悬停效果实现
在前两篇文章中,分别介绍了tablayout+scrollview 和 tablayout+recyclerview 实现的滑动定位的功能,文章链接: Android 实现锚点定位 Android tabLayout+recyclerView实现锚点定位 仔细看的话,这种滑动定位的功能,还可以整体滑动,再加上顶部tablayout 吸附悬停的效果。 实现效果: 布局 这里采用的是两个 tablayout。 一个用于占位,位于原始位置,scrollview内部,随scrollview滚动;另一个则是在滑动过程中,不断滑动,滑动到顶部时吸附在屏幕顶部,用户实际操作的也是这个tablayout。 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_...
- 下一篇
Flutter新锐专家之路:工程研发体系篇
作者:闲鱼技术-正物 写在前面 当前,闲鱼客户端已经实现了基于Flutter的商品详情页的全量重构,线上效果良好。从alpha一路走来,我们遇到了很多问题,或基于原理,或透过社区,或与官方合作,都一个个解决了,是时候梳理和总结下,也希望为其他的开发者们,尤其是已有工程中引入Flutter(混合场景)实现渐进式重构带来启发和帮助。鉴于存在多个问题一个原因或解法的情况,而本系列的重点在于说明各种问题的解决方案与思路,就不一一列出问题。所有调试/热重载相关的Flutter均为Debug模式的Flutter,不再特殊说明。 本系列文章包含三篇:引入篇,运行篇,上线篇。引入篇重点介绍工程研发体系;运行篇介绍混合情景下的栈管理与能力补齐等;上线篇介绍兼容/稳定性保障及方法。 工程研发体系的关键点包括: a.混合工程下的Flutter研发结构 混合工程中一
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- SpringBoot2整合Thymeleaf,官方推荐html解决方案
- SpringBoot2整合Redis,开启缓存,提高访问速度
- CentOS8安装MyCat,轻松搞定数据库的读写分离、垂直分库、水平分库
- CentOS7安装Docker,走上虚拟化容器引擎之路
- CentOS7设置SWAP分区,小内存服务器的救世主
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- MySQL8.0.19开启GTID主从同步CentOS8
- Hadoop3单机部署,实现最简伪集群
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题