版權聲明:本文為部落客原創文章,轉載請注明出處http://blog.csdn.net/u013132758。 https://blog.csdn.net/u013132758/article/details/81435591
什麼是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)
Context.startService(intent)
getBroadcast(Context xontext,int requestCode,Intent intent,int flag)
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開發藝術探索》
android 特殊使用者通知用法彙總–Notification源碼分析