5.2 RemoteViews的內(nèi)部機制
RemoteViews的作用是在其他進程中顯示并更新View界面籍救,為了更好地理解它的內(nèi)部機制劳殖,我們先來看一下它的主要功能。首先看一下它的構(gòu)造方法诞仓,這里只介紹一個最常用的構(gòu)造方法:public RemoteViews(String packageName, int layoutId)幕与,它接受兩個參數(shù),第一個表示當(dāng)前應(yīng)用的包名价淌,第二個參數(shù)表示待加載的布局文件申眼,這個很好理解。RemoteViews目前并不能支持所有的View類型蝉衣,它所支持的所有類型如下:
Layout
FrameLayout括尸、LinarLayout、RelativeLayout病毡、GridLayout濒翻。
View
AnalogClock、Button啦膜、Chronometer有送、ImageButton、ImageView僧家、ProgressBar雀摘、TextView、ViewFlipper啸臀、ListView届宠、GridView、StackView乘粒、AdapterViewFlipper、ViewStub伤塌。
上面所描述的是RemoteViews所支持的所有的View類型灯萍,RemoteViews不支持它們的子類以及其他View類型,也就是說RemoteViews中不能使用除了上述列表意外的View每聪,也無法只用自定義View旦棉。比如如果我們在通知欄的RemoteViews中使用系統(tǒng)的EditText齿风,那么通知欄消息將無法彈出并且會拋出如下異常。
E/StatusBar(765): couldn't inflate view for notification com.chenstyle.chapter_5/0x2
E/StatusBar(765): android.view.InflateException: Binary XML file line #25: Error inflating class android.widget.EditText
E/StatusBar(765): Caused by: android.view.InflatedException: Binary XML file line #25: Class not allowed to be inflated android.widget.EditText
E/StatusBar(765): at android.view.LayoutInflater.failNotAllowed(LayoutInflater.java:695)
E/StatusBar(765): at android.view.LayoutInflater.createView(LayoutInflater.java:628)
E/StatusBar(765): ...21 more
上面的異常信息很明確绑洛,android.widget.EditText不允許在RemoteViews中使用救斑。
RemoteViews沒有提供findViewById方法,因此無法直接訪問里面的View元素真屯,而必須通過RemoteViews所提供的一系列set方法來完成脸候,當(dāng)然這是因為RemoteViews在遠程進程中顯示,所以沒辦法直接findViewById绑蔫。表5-2列舉了部分常用的set方法运沦,更多方法請查看相關(guān)資料。
表5-2 RemoteViews的部分set方法
方法名 | 作用 |
---|---|
setTextViewText(int viewId, CharSquence text) | 設(shè)置TextView的文本 |
setTextViewTextSize(int viewId, int units, float size) | 設(shè)置TextView的字體大小 |
setTextColor(int viewId, int color) | 設(shè)置TextView的字體顏色 |
setImageViewResource(int viewId, int srcId) | 設(shè)置ImageView的圖片資源 |
setImageViewResource | 設(shè)置ImageView的圖片 |
setInt(int viewId, String methodName, int value) | 反射調(diào)用View對象的參數(shù)類型為int的方法 |
setLong(int viewId, String methodName, long value) | 反射調(diào)用View對象的參數(shù)類型為long的方法 |
setBoolean(int viewId, String methodName, boolean value) | 反射調(diào)用View的對象的參數(shù)為boolean的方法 |
setOnClickPendingIntent(int viewId, PendingIntent pendingIntent) | 為View添加單擊事件配深,事件類型只能為PendingIntent |
從表5-2中可以看出携添,原本可以直接調(diào)用的View的方法,現(xiàn)在卻必須要通過RemoteViews的一系列set方法才能完成篓叶,而且從方法的聲明上來看烈掠,很像是通過反射來完成的,事實上大部分set方法的確是通過反射來完成的缸托。
下面描述一下RemoteViews的內(nèi)部機制左敌,由于RemoteViews主要用于通知欄和桌面小部件之中,這里就通過它們來分析RemoteViews的工作過程嗦董。我們知道母谎,通知欄和桌面小部件分別由NotificationManager和AppWidgetManager管理,而NotificationManager和AppWidgetManager通過Binder分別和SystemServer進程中的NotificationManagerService以及AppWidgetService進行通信京革。由此可見奇唤,通知欄和桌面小部件中的布局文件實際上是在NotificationManagerService以及AppWidgetService中被加載的,而他們運行在系統(tǒng)的SystemServer中匹摇,這就和我們的進程構(gòu)成了跨進程的通信場景咬扇。
首先RemoteViews會通過Binder傳遞到SystemServer進程,這是因為RemoteViews實現(xiàn)了Parcelable接口廊勃,因此它可以跨進程傳輸懈贺,系統(tǒng)會根據(jù)RemoteViews中的包名等信息去得到該應(yīng)用的資源。然后會通過LayoutInflater去加載RemoteViews中的布局文件坡垫。在SystemServer進程中加載后的布局文件是一個普通的View梭灿,只不過相對于我們的進程它是一個RemoteViews而已。接著系統(tǒng)會對View執(zhí)行一系列界面更新任務(wù)冰悠,這些任務(wù)就是之前我們通過set方法來提交的堡妒。set方法對View所做的更新并不是立刻執(zhí)行的,在RemoteViews內(nèi)部會記錄所有的更新操作溉卓,具體的執(zhí)行時機要等到RemoteViews被加載以后才能執(zhí)行皮迟,這樣RemoteViews就可以在SystemServer進程中顯示了搬泥,這就是我們所看到的同時蘭消息或者桌面小部件。當(dāng)需要更新RemoteViews時伏尼,我們需要調(diào)用一系列set方法并通過NotificationManager和AppWidgetManager來提交更新任務(wù)忿檩,具體的更新操作也是在SystemServer進程中完成的。
從理論上來說爆阶,系統(tǒng)完全可以通過Binder去支持所有的View和View操作燥透,但是這樣做的話代價太大,因為View的方法太多了扰她,另外就是大量的IPC操作會影響效率兽掰。為了解決這個問題,系統(tǒng)并沒有通過Binder去直接支持View的跨進程訪問徒役,而是提供了一個Action的概念孽尽,Action代表一個View操作,Action同樣實現(xiàn)了Parcelable接口忧勿。系統(tǒng)首先將View操作封裝到Action對象并將這些對象跨進程傳輸?shù)竭h程進程杉女,接著再遠程進程中執(zhí)行Action對象中的具體操作。在我們的應(yīng)用中每調(diào)用一次set方法鸳吸,RemoteViews中就會添加一個對應(yīng)的Action對象熏挎,當(dāng)我們通過NotificationManager和AppWidgetManager來提交我們的更新時,這些Action對象就會傳輸?shù)竭h程進程并在遠程進程中依次執(zhí)行晌砾,這個過程可以參看圖5-3坎拐。遠程進程通過RemoteViews的apply方法來進行View的更新操作,RemoteViews的apply方法內(nèi)部則會去遍歷所有的Action對象并調(diào)它們的apply方法养匈,具體的View更新操作是由Action對象的apply方法來完成的哼勇。上述做法的好處是顯而易見的,首先不需要定義大量的Binder接口呕乎,其次通過在遠程進程中批量執(zhí)行RemoteViews的修改操作從而避免了大量的IPC操作积担,這就提高了程序的性能,由此可見猬仁,Android系統(tǒng)在這方面的設(shè)計的確很精妙帝璧。
上面從理論上分析了RemoteViews的內(nèi)部機制,接下來我們從源碼的角度再來分析RemoteViews的工作流程湿刽。它的構(gòu)造方法就不用多說了的烁,這么我們首先看一下它提供的一系列set方法,比如setTextViewText方法诈闺,其源碼如下所示撮躁。
public void setTextViewText(int viewId, CharSequence text) {
setCharSequence(viewId, "setText", text);
}
在上面的代碼中,viewId是被操作的View的id买雾,“setText"是方法名把曼,text是要給TextView設(shè)置的文本,這里可以聯(lián)想一下TextView的setText方法漓穿,是不是很一致呢嗤军?接著再看setCharSequence的實現(xiàn),如下所示晃危。
public void setCharSequence(int viewId, String methodName, CharSequence value) {
addAction(new ReflectionAction(viewId, methodName, ReflectionAction.CHAR_SEQUENCE, value));
}
從setCharSequence的實現(xiàn)可以看出叙赚,它的內(nèi)部并沒有對View進程直接的操作,而是添加了一個ReflectionAction對象僚饭,從名字來看震叮,這應(yīng)該是一個反射類型的動作。再看addAction的實現(xiàn)鳍鸵,如下所示苇瓣。
private void addAction(Action a) {
...
if (mAction == null) {
mAction = new ArrayList<Action>();
}
mAction.add(a);
// update the memory usage stats
a.updateMemoryUsageEstimate(mMemoryUsageCounter);
}
從上述代碼可以知道,RemoteViews內(nèi)部有一個mActions成員偿乖,它是一個ArrayList击罪,外界每調(diào)用一次set方法,RemoteViews就會為其創(chuàng)建一個Action對象并加入到這個ArrayList中贪薪。需要注意的是媳禁,這里僅僅是將Action對象保存起來了,并未對View進行實際的操作画切,這一點在上面的理論分析中已經(jīng)提到過了竣稽。到這里setTextViewText這個方法的源碼已經(jīng)分析完了,但是我們好像還是什么都不知道的感覺霍弹,沒關(guān)系毫别,接著我們需要看一下這個ReflectionAction的實現(xiàn)就知道了。在看它的實現(xiàn)之前庞萍,我們需要先看一下RemoteViews的apply方法以及Action類的實現(xiàn)拧烦,首先看一下RemoteViews的apply方法,如下所示钝计。
public View apply(Context context, ViewGroup parent, onClickHandler handler) {
RemoteViews rvToApply getRemoteViewsToApply(context);
View result;
...
LayoutInflater inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
// Clone inflater so we load resources from correct context and
// we don't add a filter to the static version returned by getSystemService.
inflater = inflater.cloneInContext(inflationContext);
inflater.setFilter(this);
result = inflater.inflate(rvToApply.getLayoutId(), parent, false);
rvToApply.performApply(result, parent, handler);
return result;
}
從上面的代碼可以看出恋博,首先會通過LayoutInflater去加載RemoteViews中的布局文件,RemoteViews中的布局文件可以通過getLayoutId這個方法獲得私恬,加載完布局文件后會通過performApply去執(zhí)行一些更新操作债沮,代碼如下所示。
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);
}
}
}
performApply的實現(xiàn)就比較好理解了本鸣,它的作用就是遍歷mActions這個列表并執(zhí)行每個Action對象的apply方法疫衩。還記得mAction嗎?每一次的set操作都會對應(yīng)著它里面的一個Action對象荣德,因此我們可以斷定闷煤,Action對象的apply方法就是真正操作View的地方童芹,實際上的確如此。
RemoteViews在通知欄和桌面小部件中的工作過程和上面描述的過程是一致的鲤拿,當(dāng)我們調(diào)用RemoteViews的set方法時假褪,并不會立刻更新它們的界面,而必須要通過NotificationManager的notify方法以及AppWidgetManager的updateAppWidget的內(nèi)部實現(xiàn)中近顷,它們的確是通過RemoteViews的apply以及reapply方法來加載或者更新界面的生音,apply和reApply的區(qū)別在于:apply會加載布局并更新界面,而reApply則只會更新界面窒升。通知欄和桌面小插件在初始化界面時會調(diào)用apply方法缀遍,而在后續(xù)的更新界面時則會調(diào)用reapply方法。這里先看一下BaseStatusBar的updateNotificationViews方法中饱须,如下所示域醇。
private void updateNotificationViews(NotificationData.Entry entry, StatusBarNotification notification, boolean isHandsUp) {
final RemoteViews contentViews = notification.getNotification().contentView;
final RemoteViews bigContentView = isHandsUp ? notification.getNotification().headsUpContentView : notification.getNotification().bigCOntentView;
final Notification publicVersion = notification.getNotification().publicVersion;
final RemoteViews publicContentView = publicVersion != null ? publicVersion.contentView : null;
// Reapply the RemoteViews
contentView.reapply(mContext, entry.expanded, mOnClickHandler);
...
}
很顯然,上述代碼表示當(dāng)通知欄界面需要更新時冤寿,它會通過RemoteViews的reapply方法來更新界面歹苦。
接著再看一下AppWidgetHostView的updateAppWidget方法,在它的內(nèi)部有如下一段代碼:
mRemoteContext = getRemoteContext();
int layoutId = remoteViews.getLayoutId();
// If our stale view has been prepared to match active, and the new
// layout matches, try recycling it
if (content == null && layoutId == mLayoutId) {
try {
remoteViews.reapply(mContext, mView, mOnClickHandler);
content = mView;
recycled = true;
if (LOGD) Log.d(TAG, "was able to recycled existing layout");
} catch (RuntimeException e) {
exception = e;
}
}
// Try normal RemoteView inflation
if (content == null) {
try {
content = remoteViews.apply(mContext, this, mOnClickHandler);
if (LOGD) Log.d(TAG, "had to inflate new layout");
} catch (RuntimeException e) {
exception = e;
}
}
從上述代碼可以發(fā)現(xiàn)督怜,桌面小部件在更新界面時也是通過RemoteViews的reapply方法來實現(xiàn)的殴瘦。
了解了apply以及reapply的作用以后,我們再繼續(xù)看一些Action的子類的具體實現(xiàn)号杠,首先看一下RefectionAction的具體實現(xiàn)蚪腋,它的源碼如下所示。
private final class ReflectionAction extends Action {
ReflectionAction(int viewId, String methodName, int type, Object value) {
this.valueId = 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 {
getMethod(view, this.methodName, param).invoke(view, wrapArg(this.value));
} catch (ActionException e) {
throw e;
} catch (Exception ex) {
throw new ActionException(ex);
}
}
}
通過上述代碼可以發(fā)現(xiàn)姨蟋,RefectionAction表示的是一個反射動作屉凯,通過它對View的操作會以反射的方式來調(diào)用,其中g(shù)etMethod就是根據(jù)方法名來得到反射所需要的Method對象眼溶。使用Reflection的set方法有:setTextViewText悠砚、setBoolean、setLong堂飞、setDouble等灌旧。除了ReflectionAction,還有其他Action绰筛,比如TextViewSizeAction枢泰、ViewPaddingAction、setOnClickPendingIntent等铝噩。這里再分析一下TextViewSizeAction衡蚂,它的實現(xiàn)如下所示。
private class TextViewSizeAction extends Action {
public TextViewSizeAction(int viewId, int units, float size) {
this.viewId = viewId;
this.units = units;
this.size = size;
}
...
@Override
public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
final TextView target = (TextView) root.findViewById(viewId);
if (target == null) return;
target.setTextSize(units, size);
}
public String getActionName() {
return "TextViewSizeAction";
}
int units;
float size;
public final static int TAG = 13;
}
TextViewSizeAction的實現(xiàn)比較簡單,它之所以不用反射來實現(xiàn)毛甲,是因為setTextSize這個方法有2個參數(shù)年叮,因此無法復(fù)用ReflectionAction,因為ReflectionAction的反射調(diào)用只有一個參數(shù)丽啡。其他Action這里就不一一進行分析了谋右,讀者可以查看RemoteViews的源代碼。
關(guān)于單擊事件补箍,RemoteViews中只支持發(fā)起PendingIntent,不支持onClickListener那種模式啸蜜。另外坑雅,我們需要注意setOnClickPendingIntent、setPendingIntentTemplate以及setOnClickFillInIntent它們之間的區(qū)別和聯(lián)系衬横。首先setOnClickPendingIntent用于給普通View設(shè)置單擊事件裹粤,但是不能給集合(ListView和StackView)中的View設(shè)置單擊事件,因為開銷比較大蜂林,所以系統(tǒng)禁止了這種方式遥诉;其次,如果要給ListView和StackView中的item添加單擊事件噪叙,則必須將setPendingIntentTemplate和setOnClickFillInIntent組合使用才可以矮锈。