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的应用

通知栏主要是通过 NotificationManagernotify() 实现更新,除了默认效果还可以另外定义布局。桌面小部件则是通过 AppWidgetProvider 实现,AppWidgetProvider本质上是一个广播。RemoteViews提供了一系列 set() 更新View,但是支持的View类型也是有限的。

1.1 RemoteViews在通知栏上的应用

// 自定义布局,使用RemoteViews更新通知栏界面
RemoteViews remoteViews = new RemoteVies(getPackageName(), R.layout.notification);
remoteViews.setTextViewText(R.id.msg, "test"); // 更新TextView
remoteViews.setImageViewResource(R.id.icon, R.drawable.icon); // 更新ImageView
PendingIntent 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">
	
	<ImageView
		android: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();
	}

	@Override
	public void onReceive(final Context context, Intent intent) {
		super.onReceive(context, intent);
		if (CLICK_ACTION.equals(intent.getAction())) {
			new Thread(new Runnable() {
				@Override
				public 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();
		}
	}

	// 每次桌面小部件更新时都调用一次该方法
	@Override
	public 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-data
		android: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原理

Android RemoteViews原理_第1张图片

由于RemoteViews主要用于通知栏和桌面小部件之中,这里就通过过它们来分析RemoteViews的工作过程。

通知栏和桌面小部件分别有 NotificationManagerAppWidgetManager 管理,而NotificationManager和AppWidgetManager通过Binder分别和SystemServer进程中的 NotificationManagerServiceAppWidgetService 进行通信。所以,通知栏和桌面小部件中的布局文件实际上是在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

当我们通过 NotificationManagerAppWidgetManager 提交我们的更新时(即 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()会添加一个Action
	addAction(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;
	}
	...
	@Override
	public 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.apply
public 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()传递的layoutId
	result = 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);

你可能感兴趣的:(进阶,原理,Android,面试)