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