Android(安卓)RemoteViews原理
文章目录
- 1 RemoteViews的应用
- 1.1 RemoteViews在通知栏上的应用
- 1.2 RemoteViews在桌面小部件的应用
- 1.3 PendingIntent
- 2 RemoteViews内部机制
- 2.1 RemoteViews内部机制概述
- 2.2 RemoteViews内部机制源码分析
- 3 RemoteViews的意义
RemoteViews
是一种远程View,它和远程Service是一样的,可以跨进程更新界面。RemoteViews在Android中的使用场景有两种:通知栏和桌面小部件。
1 RemoteViews的应用
通知栏主要是通过 NotificationManager
的 notify()
实现更新,除了默认效果还可以另外定义布局。桌面小部件则是通过 AppWidgetProvider
实现,AppWidgetProvider本质上是一个广播。RemoteViews提供了一系列 set()
更新View,但是支持的View类型也是有限的。
1.1 RemoteViews在通知栏上的应用
// 自定义布局,使用RemoteViews更新通知栏界面RemoteViews remoteViews = new RemoteVies(getPackageName(), R.layout.notification);remoteViews.setTextViewText(R.id.msg, "test"); // 更新TextViewremoteViews.setImageViewResource(R.id.icon, R.drawable.icon); // 更新ImageViewPendingIntent openActivityPendingIntent = PendingIntent.getActivity(this, 0, new Intent(this, MyActivity2.class), PendingIntent.FLAG_UPDATE_CURRENT);// 给View添加点击事件remoteViews.setOnClickPendingIntent(R.id.open_activity, openActivityPendingIntent);Intent intent = new Intent(this, MyActivity.class);PendingIntent pendingIntent = PendingIntent.getActivity(this, 0 ,intent, PendingIntent.FLAG_UPDATE_CURRENT);Notification notification = new Notification();notification.icon = R.drawable.ic_launcher;notification.tickerText = "hello world";notification.when = System.currentTimeMillis();notification.flags = Notification.FLAG_AUTO_CANCEL;notification.contentView = remoteViews;notification.contentIntent = pendingIntent;notification.setLatestEventInfo(this, "test", "this is notification", pendingIntent);NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);manager.notify(1, notification);
1.2 RemoteViews在桌面小部件的应用
- 定义小部件界面
<?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"><ImageViewandroid:id="@+id/imageView1"android:layout_width="wrap_content"android:layout_height="wrap_content"android:src="@drawable/icon1" /></LinearLayout>
- 定义小部件配置信息
在 res/xml/
下新建 appwidget_provider_info.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="84dp" // 最小尺寸android:minWidth="84dp"android:updatePeriodMillis="86400000" /> // 自动更新周期,单位ms
- 定义小部件实现类
这个类需要继承 AppWidgetProvider
:
// 在小部件上显示一张图片,点击它后图片旋转一周public class MyAppWidgetProvider extends AppWidgetProvider {public static final String CLICK_ACTION = "com.example.appwidget.ACTION_CLICK";public MyAppWidgetProvider() {super();}@Overridepublic void onReceive(final Context context, Intent intent) {super.onReceive(context, intent);if (CLICK_ACTION.equals(intent.getAction())) {new Thread(new Runnable() {@Overridepublic void run() {Bitmap srcbBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.icon1);AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);for (int i = 0; i < 37; i++) {float degree = (i * 10) % 360;RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget);remoteViews.setImageViewBitmap(R.id.imageView1, rotateBitmap(context, srcbBitmap, degree));Intent intentClick = new Intent();intentClick.setAction(CLICK_ACTION);PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intentClick, 0);remoteViews.setOnClickPendingIntent(R.id.imageView1, pendingIntent);appWidgetManager.updateAppWidget(new ComponentName(context, MyAppWidgetProvider.class), remoteViews);SystemClock.sleep(30);}}}).start();}}// 每次桌面小部件更新时都调用一次该方法@Overridepublic void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {super.onUpdate(context, appWidgetManager, appWidgetIds);final int counter = appWidgetIds.length;for (int i = 0; i < counter; i++) {int appWidgetId = appWidgetIds[i];onWidgetUpdate(context, appWidgetManager, appWidgetId);}}private void onWidgetUpdate(Context context, AppWidgetManager appWidgetManager, int appWidgetId) {RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget);Intent intentClick = new Intent();intentClick.setAction(CLICK_ACTION);PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intentClick, 0);remoteViews.setOnClickPendingIntent(R.id.imageView1, pendingIntent);appWidgetManager.updateAppWidget(appWidgetId, remoteViews);}private Bitmap rotateBitmap(Context context, Bitmap srcbBitmap, float degree) {Matrix matrix = new Matrix();matrix.reset();matrix.setRotate(degree);Bitmap tmpBitmap = Bitmap.createBitmap(srcbBitmap, 0, 0, srcbBitmap.getWidth(), srcbBitmap.getHeight(), matrix, true);return tmpBitmap;}}
- 在清单文件中声明小部件
<receiver android:name=".MyAppWidgetProvider"><meta-dataandroid:name="android.appwidget.provider"android:resource="@xml/appwidget_provider_info" /><intent-filter><action android:name="com.example.appwidget.action.CLICK" />// 系统指定的小部件标志,不添加小部件无法显示在小部件列表里<action android:name="android.appwidget.action.APPWIDGET_UPDATE" /></intent-filter></receiver>
AppWidgetProvider
除了 onUpdate()
,还有其他方法:onEnabled()
、onDisabled()
、onDeleted()
和 onReceive()
。这些方法会自动地被 onReceive()
在合适的时间调用。
-
onEnabled:当该窗口小部件第一次添加到桌面时调用该方法,可添加多次但只在第一次调用
-
onUpdate:小部件被添加时或者每次小部件更新时都会调用一次该方法,小部件的更新时机由
updatePeriodMillis
来指定,每个周期小部件都会自动更新一次 -
onDeleted:每删除一次桌面小部件就调用一次
-
onDisabled:当最后一个该类型的桌面小部件被删除时调用该方法,注意是最后一个
-
onReceive:这是广播的内置方法,用于分发具体的事件给其他方法
1.3 PendingIntent
关于PendingIntent的使用和分析,可以参考之前的写的一篇文章:
PendingIntent的使用和分析
2 RemoteViews内部机制
RemoteViews并不支持所有的View类型,它所支持的所有类型如下:
Layout | View |
---|---|
FrameLayout、LinearLayout、RelativeLayout、GridLayout | AnalogClock、Button、Chronometer、ImageButton、ImageView、ProgressBar、TextView、ViewFlipper、ListView、GridView、StackView、AdapterViewFlipper、ViewStub |
RemoteViews没有提供findViewById所以无法直接访问布局里面的View元素,必须通过RemoteViews提供的一系列 set()
来完成:
方法名 | 作用 |
---|---|
setTextViewText(int viewId, CharSequence text) | 设置TextView的文本 |
setTextViewTextSize(int viewId, float size) | 设置TextView的字体大小 |
setTextColor(int viewId, int color) | 设置TextView的字体颜色 |
setImageViewResource(int viewId, int srcId) | 设置ImageView的图片资源 |
setInt(int viewId, String methodName, int value) | 反射调用View对象的参数类型为int的方法 |
setLong(int viewId, String methodName, long value) | 反射调用View对象的参数类型为long的方法 |
setBoolean(int viewId, String methodName, boolean value) | 反射调用View对象的参数类型为boolean的方法 |
setOnClickPendingIntent(int viewId, PendingIntent pendingIntent) | 为View添加点击事件,事件类型只能为PendingIntent |
2.1 RemoteViews内部机制概述
在这里先列出RemoteViews跨进程更新的图例,接下来会对途中的元素进行分析。涉及到跨进程在Android中肯定需要用到Binder,对于Binder不清楚的可以参考:Binder原理
由于RemoteViews主要用于通知栏和桌面小部件之中,这里就通过过它们来分析RemoteViews的工作过程。
通知栏和桌面小部件分别有 NotificationManager
和 AppWidgetManager
管理,而NotificationManager和AppWidgetManager通过Binder分别和SystemServer进程中的 NotificationManagerService
和 AppWidgetService
进行通信。所以,通知栏和桌面小部件中的布局文件实际上是在NotificationManagerService以及AppWidgetService中被加载的,而它们运行在系统的SystemServer中,所以是跨进程通信的场景。
从理论上来说,系统完全可以通过Binder去支持所有的View和View操作,但是这样做的话代价太大,因为View的方法太多了,另外就是大量的IPC操作会影响效率。
- 将RemoteViews通过Binder传输到SystemServer进程
RemoteViews会通过Binder传递到SystemServer进程,因为RemoteViews实现了Parcelable接口,因此可以跨进程传输,系统会根据RemoteViews中的包名等信息去得到该应用的资源。然后通过 LayoutInflater
加载RemoteViews中的布局文件,在SystemServer中加载后的布局文件是一个普通的View。加载完成后在SystemServer中显示,这就是我们看到的通知栏或桌面小部件。
- 使用Action封装对View的操作
系统并没有通过Binder直接支持View的跨进程访问,而是提供了 Action
的概念,Action代表一个View操作,Action同样实现了Parcelable接口。当我们调用RemoteViews提供的 set()
方法时,会将这些操作封装到Action对象中。
- 执行Action更新RemoteViews
当我们通过 NotificationManager
和 AppWidgetManager
提交我们的更新时(即 NotificationManager.notify()
和 AppWidgetManager.updateAppWidget()
),会将本地进程的一系列Action对象跨进程传输到远程进程,然后在远程进程调用RemoteViews的 apply()
或 reapply()
遍历一系列Action调用它们的 apply()
进行View的更新操作。
上面做大的好处显而易见,首先不需要定义大量的Binder接口,其次通过在远程进程中批量执行RemoteViews的修改操作从而避免了大量的IPC操作,提供程序性能。
2.2 RemoteViews内部机制源码分析
我们从 setText()
来分析源码走向。
public void setCharSequence(int viewId, String methodName, CharSequence value) {// 和上面分析的一样,调用RemoteViews的set()会添加一个ActionaddAction(new ReflectionAction(viewId, methodName, ReflectionAction.CHAR_SEQUENCE, value));}private void addAction(Action a) {...if (mActions == null) {mActions = new ArrayList<>();}mActions.add(a); // 只是将View的操作封装成Action并且保存起来,并没有开始执行a.updateMemoryUsageEstimate(mMemoryUsageCounter);}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;}...@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 {// 对View的操作有一些是通过反射实现,有些不是getMethod(view, this.methodName, param).invoke(view, wrapArg(this,.value));} catch (ActionException e) {throw e;} catch (Exception ex) {throw new ActionException(ex);}}}// RemoteViews.applypublic View apply(Context context, ViewGroup parent, OnClickHandler handler) {RemoteViews rvToApply = getRemoteViewsToApply(context);View result;...// 在RemoteViews.apply的时候加载布局LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);inflater = inflater.cloneInContext(inflationContext);inflater.setFilter(this);// layoutId是new RemoteViews()传递的layoutIdresult = inflater.inflate(rvToApply.getLayoutId(), parent, false);// 更新操作rvToApply.performApply(result, parent, handler);return result;}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); // 实际执行View的操作}}}// 在NotificationManager和AppWidgetManager调用notify()和updateAppWidget()时// 才会调用RemoteViews的apply()和reapply()加载和更新界面// apply()是会加载布局并更新界面,而reapply()只有更新界面private void updateNotificationViews(NotificationData.Entry entry,StatusBarNotification notification, boolean isHeadsUp) {final RemoteViews contentView = notification.getNotification().contentView;final RemoteViews bigContentView = isHeadsUp? notification.getNotification().headsUpContentView: notification.getNotification().bigContentView;final Notification publicVersion = notification.getNotification().publicVersion;final RemoteViews publicContentView = publicVersion != null ? publicversion.contentView : null;contentView.reapply(mContext, entry.expanded, monClickHandler); // 更新界面...}// 在AppWidgetHostView的updateAppWidget()有以下代码说明apply()和reapply()的区别mRemoteContext = getRemoteContext();int layoutId = remoteVies.getLayoutId();// 如果RemoteViews的layoutId和当前相同,调用reapply()只更新界面if (content == null && layoutId == mLayoutId) {try {remoteViews.reapply(mContext, mView, mOnClickHandler);content = mView;recycled = true;} catch (RuntimeException e) {exception = e;}}// 如果没有则调用apply()加载布局并更新界面if (content == null) {try {content = remoteViews.apply(mContext, this, mOnClickHandler);} catch (RuntimeException e) {exception = e;}}
3 RemoteViews的意义
在实际的场景中,我们需要从一个应用更新另一个应用的界面(当然,两个应用也必须是在某些参数下约定好的),我们可以选择AIDL去实现,但是如果对界面的更新比较频繁,这个时候就会有效率问题,同时AIDL接口就有可能会变得很复杂。这个时候采用RemoteView来实现就没有这个问题了,当然RemoteViews也有缺点,那就是它仅支持一些常见的View,对于自定义View它是不支持的。
面对这种问题,是采用AIDL还是RemoteViews要看具体情况,如果界面中的View都是一些简单的且RemoteViews支持的View,那么可以考虑采用RemoteViews,否则就要使用其他方式。
使用RemoteViews在两个应用间更新界面还有一个问题,就是布局加载问题。如果A和B属于不同的应用,那么B中的布局文件的资源id传输到A中以后很有可能是无效的,因为A中的这个布局文件的资源id不可能刚好和B中的资源id一样。我们可以通过资源名称来加载布局文件,两个应用要提前约定好RemoteViews中的布局文件的资源名称,然后再A中根据名称查找到对应的布局文件并加载,接着再调用RemoteViews的 reapply()
进行加载。
// 使用RemoteViews.apply()会出现问题,因为使用的是B传递给A的RemoteView布局id// 两个应用中的布局id是不相同的会导致加载无效View view = remoteViews.apply(this, mRemoteViewsContent);mRemoteViewsContent.addView(view);// 从B应用中拿到了需要加载的布局名称layout_simulated_notification// A应用在本地查找自己layout目录下的这个布局文件进行加载int layoutId = getResources().getIdentifier("layout_simulated_notification", "layout", getPackageName());View view = getLayoutInflater().inflate(layoutId, mRemoteViewsContent, false);remoteViews.reapply(this, view); // 更新UI mRemoteViewsContent.addView(view);
更多相关文章
- 一段漂亮的Fragment hide,show相关的代码
- Android(安卓)约束布局(ConstraintLayout)详解
- sendMessage&sendEmptyMessage异同
- android常用view布局
- Android(安卓)Gradle使用笔记(持续更新)
- Android(安卓)ListView的getview()中position错位 重复调用(posit
- Android(安卓)Studio 2.0代码热更新
- android防新闻循环轮播图效果
- Android(安卓)getWidth和getMeasuredWidth的正解