Android 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原理

在這裏插入圖片描述

由於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);
發佈了199 篇原創文章 · 獲贊 7 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章