文章目錄
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"); // 更新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的使用和分析,可以參考之前的寫的一篇文章:
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()會添加一個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);