您现在的位置是:首页 > 文章详情

Android学习--深入探索RemoteViews

日期:2018-08-04点击:477
版权声明:本文为博主原创文章,转载请注明出处http://blog.csdn.net/u013132758。 https://blog.csdn.net/u013132758/article/details/81435591

什么是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方法来完成,这是通过反射调用的。

通知栏和小部件分别由NotificationManagerAppWidgetManger管理,而NotificationManagerAppWidgetManger通过Binder分别和SystemService进程中的NotificationManagerServiceAppWidgetMangerService中加载的,而它们运行在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进程中显示了。
这里需要注意一个小知识点就是applyreApply方法的区别,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。下面我们通过NotificationManagernotify方法来看看。

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

我们注意到这里最终调用了INotificationManagerenqueueNotificationWithTag方法,这里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中StatusBarManagerInternalbuzzBeepBlinked()方法:

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开发艺术探索》

源码分析Notification的Notify

android 特殊用户通知用法汇总–Notification源码分析

原文链接:https://yq.aliyun.com/articles/642358
关注公众号

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。

持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。

文章评论

共有0条评论来说两句吧...

文章二维码

扫描即可查看该文章

点击排行

推荐阅读

最新文章