024.RemoteViews的內部機制
阿新 • • 發佈:2018-11-15
RemoteViews 的作用是在其他的程序中顯示並且更新View介面,為了更好理解它的內部機制,先看下它的主要功能。
首先看它最常用的構造方法:
public RemoteViews( String packageName ,String layoutId )
第一個引數是當前應用的包名,第二個引數表示代價在的佈局檔案。RemoteViews目前並不能支援所有的View型別,它支援的所有型別如下:
Layout
FrameLayout 、 LinearLayout 、RelativeLayout 、GridLayout
View AnalogClock、Button、Chronometer、ImageButton、ImageView、ProgressBar、TextView、ViewFlipper、ListView、GridView、StackView、AdapterViewFlipper、ViewStub
上面描述的是RemoteViews所支援的左右的View型別.不支援其他型別和子類。比如我們如果使用EditText 就會丟擲異常。RemoteViews沒有提供findViewById方法,因此無法直接訪問裡面的View元素(實際上是因為這個RemoteView不是在這個程序當中,自然不能直接訪問這個控制元件物件的地址了),不過我們可以通過RemoteView的set方法來訪問其中的元素,或者是通過反射機制來呼叫它們的部分方法。
下面來分析一下RemoteViews的工作過程。Notification 和 AppWidget 分別由NotificationManager和AppWidgetManager 管理,而NotificationManager和AppWidgetManager通過Binder分別和SystemService程序中的NotificationManagerService和AppWidgetService 進行通訊。所以,RemoteViews中的layout檔案其實是被NotificationManagerService和AppWidgetService載入的,它們執行在SystemServer這個程序當中,所以說我們的程序是在和SystemServer進行跨程序通訊。
首先RemoteViews會通過Binder傳遞到SystemServer程序,這是因為RemoteViews實現了Parecel介面,所以它這個物件可以被跨程序運輸,系統會根據RemoteViews中的包名等資訊獲取到使用者程序的資訊來獲取資源。之後通過LayoutInflater去載入RemoteViews中的佈局檔案,在SystemServer程序載入後的佈局檔案是一個普通的View,只不過對於我們它是一個RemoteViews.之後系統會對View執行一系列介面更新任務,這些任務就是之前我們通過Set方法來提交的。set方法對view所做的更新不是立即生效的,RemoteViews會記錄下這些更新操作,等到RemoteViews被載入了以後才會執行,這樣RemoteViews就可以在SystemServer程序中顯示了,這就是我們所看到的通知欄小戲或者桌面小部件。當需要更新RemoteViews的時候,我們需要呼叫set方法並且通過NotificationManager和AppWidgetManager來提交到遠端程序上,具體的更新操作則是在SystemServer程序中實現的。
RemoteViews的更新是通過Binder實現的,但是不是直接呼叫Binder的介面,RemoteViews引入了Action的概念,我們對View的每呼叫一次set方法都是一個 Action,這個Action也是實現了Parcelable介面,因此,可以傳遞給遠端程序上。當我們呼叫了一系列的set方法以後,RemoteViews會產生一組Action,在麼我們向NotificationManager或者是AppWigetManager提交了更新之後,這個方法就 會被傳遞到遠端程序上。遠端程序再執行這些Action。 下面我們從原始碼來分析RemoteViews的工作機制:
上面我們可以看到,在RemoteViews中 有一個叫mActions的列表在維護Action的資訊,需要注意的是,這裡靜靜是將Action物件儲存了起來了。並未對View進行實際的操作。 接下來我們看RefletctionAction,可以看到,這個表示的是一個反射動作,通過它對View的操作會以反射的方式來呼叫,其中getMethod就是根據方法名來獲取所需要的Method物件。
從上面程式碼可以看出,首先會通過LayoutInflater去載入RemoteViews中的佈局檔案,RemoteViews中的佈局檔案可以通過getlayoutId這個方法獲得,載入完佈局檔案後會通過performApply去執行一些更新操作,程式碼如下:
上面的實現就是遍歷mAction中的Action物件,並且執行它們的apply方法。我們前面看到ReflectionAction的apply就是利用反射機制執行方法,所以,我們可以知道,action#apply其實就是真正執行我們想要的行為的地方。 RemoteViews在通知欄和桌面小部件中的工作過程和上面描述的過程是一樣的,當我們呼叫RemoteViews的set方法的時候,我們不會更新它們的介面,而是要通過NotificationManager和notify方法和AppWidgetManager的updateAppWidget方法才能更新它們的介面。實際上在AppWigetAManager的updateAppWidget的內部視線中,它們是通過RemoteViews的apply和reapply方法來載入和更新介面的。app會載入並且更新介面,而reapply只會更新介面。通知欄和桌面小外掛會在初始化介面的時候呼叫apply方法,而在後續的更新介面時候會呼叫reapply方法。 RemoteViews中只支援發起PendingIntent,不支援OnClickListener那種模式,另外,我們需要注意setOnClickPendingIntent、setPendingIntentTemplate以及setOnClickFillIntent它們之前的區別和聯絡。首先setOnClickPendingIntent用於給普通View設定單擊事件,因為開銷比較大,所以系統進位制了這種方式。其次,如果要給ListView和StackView中的item新增事件,必須要將setPendingIntentTemplate和setOnClickFillIntent組合使用才可以。(使用RemoteViews的setRemoteAdapter 繫結 RemoteViewService) 其他: 前面是使用系統自帶的NotificationManager和AppWidgetManager來使用RemoteViews,那麼除了這兩種情況,我們就不能使用了嗎?肯定不是,我們完全可以自己做NotificationManager和AppWidgetManager一樣的工作。我們可以通過AIDL使用Binder來傳遞RemoteView,也可以通過廣播來傳遞RemoteViews物件。比如我們有2個程序A和B。B可以傳送訊息給A,然後在A中顯示B所需要顯示的控制元件。 我們可以建立一個RemoteViews物件,然後把它放入Intent當中,這樣,在廣播接收器我們就能收到這個RemoteViews了。 不過,我們建立RemoteViews的時候,不能直接使用我們的程序上下文來建立。我們可以檢視AppWigetHostView的getDefaultView方法:
由於不在同一個程序中,往往是兩個APP,因此資源是不能直接找到的,所以,我們想要通過id ,解析出佈局物件,那麼就需要我們先獲取遠端程序的程序上下文,通過Context的createPackageContextAsUser來獲取Context物件。之後再解析成對應的佈局物件。然後就可以使用了。
View AnalogClock、Button、Chronometer、ImageButton、ImageView、ProgressBar、TextView、ViewFlipper、ListView、GridView、StackView、AdapterViewFlipper、ViewStub
首先RemoteViews會通過Binder傳遞到SystemServer程序,這是因為RemoteViews實現了Parecel介面,所以它這個物件可以被跨程序運輸,系統會根據RemoteViews中的包名等資訊獲取到使用者程序的資訊來獲取資源。之後通過LayoutInflater去載入RemoteViews中的佈局檔案,在SystemServer程序載入後的佈局檔案是一個普通的View,只不過對於我們它是一個RemoteViews.之後系統會對View執行一系列介面更新任務,這些任務就是之前我們通過Set方法來提交的。set方法對view所做的更新不是立即生效的,RemoteViews會記錄下這些更新操作,等到RemoteViews被載入了以後才會執行,這樣RemoteViews就可以在SystemServer程序中顯示了,這就是我們所看到的通知欄小戲或者桌面小部件。當需要更新RemoteViews的時候,我們需要呼叫set方法並且通過NotificationManager和AppWidgetManager來提交到遠端程序上,具體的更新操作則是在SystemServer程序中實現的。
RemoteViews的更新是通過Binder實現的,但是不是直接呼叫Binder的介面,RemoteViews引入了Action的概念,我們對View的每呼叫一次set方法都是一個 Action,這個Action也是實現了Parcelable介面,因此,可以傳遞給遠端程序上。當我們呼叫了一系列的set方法以後,RemoteViews會產生一組Action,在麼我們向NotificationManager或者是AppWigetManager提交了更新之後,這個方法就 會被傳遞到遠端程序上。遠端程序再執行這些Action。 下面我們從原始碼來分析RemoteViews的工作機制:
/**
* 相當於呼叫TextView.setText
*
*/
public void setTextViewText(int viewId, CharSequence text) {
setCharSequence(viewId, "setText", text);
}
/**
* 呼叫一個Remoteviews上一個控制元件引數為CharSequence的方法
*/
public void setCharSequence(int viewId, String methodName, CharSequence value) {
addAction(new ReflectionAction(viewId, methodName, ReflectionAction.CHAR_SEQUENCE, value));
}
/**
* 新增一個Action ,它會在遠端程序呼叫apply方法的時候執行
*
* @param a The action to add
*/
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);
}
上面我們可以看到,在RemoteViews中 有一個叫mActions的列表在維護Action的資訊,需要注意的是,這裡靜靜是將Action物件儲存了起來了。並未對View進行實際的操作。 接下來我們看RefletctionAction,可以看到,這個表示的是一個反射動作,通過它對View的操作會以反射的方式來呼叫,其中getMethod就是根據方法名來獲取所需要的Method物件。
@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);
}
}
接下來我們看RemoteViews的apply方法:
public View apply(Context context, ViewGroup parent, OnClickHandler handler) {
RemoteViews rvToApply = getRemoteViewsToApply(context);
View result;
Context c = prepareContext(context);
LayoutInflater inflater = (LayoutInflater)
c.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater = inflater.cloneInContext(c);
//設定過濾器,過濾掉一些不滿足條件的View,
//比如使用者自定義的View是不能被解析的,會報錯
inflater.setFilter(this);
result = inflater.inflate(rvToApply.getLayoutId(), parent, false);
rvToApply.performApply(result, parent, handler);
return result;
}
從上面程式碼可以看出,首先會通過LayoutInflater去載入RemoteViews中的佈局檔案,RemoteViews中的佈局檔案可以通過getlayoutId這個方法獲得,載入完佈局檔案後會通過performApply去執行一些更新操作,程式碼如下:
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);
}
}
}
上面的實現就是遍歷mAction中的Action物件,並且執行它們的apply方法。我們前面看到ReflectionAction的apply就是利用反射機制執行方法,所以,我們可以知道,action#apply其實就是真正執行我們想要的行為的地方。 RemoteViews在通知欄和桌面小部件中的工作過程和上面描述的過程是一樣的,當我們呼叫RemoteViews的set方法的時候,我們不會更新它們的介面,而是要通過NotificationManager和notify方法和AppWidgetManager的updateAppWidget方法才能更新它們的介面。實際上在AppWigetAManager的updateAppWidget的內部視線中,它們是通過RemoteViews的apply和reapply方法來載入和更新介面的。app會載入並且更新介面,而reapply只會更新介面。通知欄和桌面小外掛會在初始化介面的時候呼叫apply方法,而在後續的更新介面時候會呼叫reapply方法。 RemoteViews中只支援發起PendingIntent,不支援OnClickListener那種模式,另外,我們需要注意setOnClickPendingIntent、setPendingIntentTemplate以及setOnClickFillIntent它們之前的區別和聯絡。首先setOnClickPendingIntent用於給普通View設定單擊事件,因為開銷比較大,所以系統進位制了這種方式。其次,如果要給ListView和StackView中的item新增事件,必須要將setPendingIntentTemplate和setOnClickFillIntent組合使用才可以。(使用RemoteViews的setRemoteAdapter 繫結 RemoteViewService) 其他: 前面是使用系統自帶的NotificationManager和AppWidgetManager來使用RemoteViews,那麼除了這兩種情況,我們就不能使用了嗎?肯定不是,我們完全可以自己做NotificationManager和AppWidgetManager一樣的工作。我們可以通過AIDL使用Binder來傳遞RemoteView,也可以通過廣播來傳遞RemoteViews物件。比如我們有2個程序A和B。B可以傳送訊息給A,然後在A中顯示B所需要顯示的控制元件。 我們可以建立一個RemoteViews物件,然後把它放入Intent當中,這樣,在廣播接收器我們就能收到這個RemoteViews了。 不過,我們建立RemoteViews的時候,不能直接使用我們的程序上下文來建立。我們可以檢視AppWigetHostView的getDefaultView方法:
protected View getDefaultView() {
if (LOGD) {
Log.d(TAG, "getDefaultView");
}
View defaultView = null;
Exception exception = null;
try {
if (mInfo != null) {
Context theirContext = mContext.createPackageContextAsUser(
mInfo.provider.getPackageName(), Context.CONTEXT_RESTRICTED, mUser);
mRemoteContext = theirContext;
LayoutInflater inflater = (LayoutInflater)
theirContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater = inflater.cloneInContext(theirContext);
inflater.setFilter(sInflaterFilter);
AppWidgetManager manager = AppWidgetManager.getInstance(mContext);
Bundle options = manager.getAppWidgetOptions(mAppWidgetId);
int layoutId = mInfo.initialLayout;
if (options.containsKey(AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY)) {
int category = options.getInt(AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY);
if (category == AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD) {
int kgLayoutId = mInfo.initialKeyguardLayout;
// If a default keyguard layout is not specified, use the standard
// default layout.
layoutId = kgLayoutId == 0 ? layoutId : kgLayoutId;
}
}
defaultView = inflater.inflate(layoutId, this, false);
} else {
Log.w(TAG, "can't inflate defaultView because mInfo is missing");
}
} catch (PackageManager.NameNotFoundException e) {
exception = e;
} catch (RuntimeException e) {
exception = e;
}
if (exception != null) {
Log.w(TAG, "Error inflating AppWidget " + mInfo + ": " + exception.toString());
}
if (defaultView == null) {
if (LOGD) Log.d(TAG, "getDefaultView couldn't find any view, so inflating error");
defaultView = getErrorView();
}
return defaultView;
}
由於不在同一個程序中,往往是兩個APP,因此資源是不能直接找到的,所以,我們想要通過id ,解析出佈局物件,那麼就需要我們先獲取遠端程序的程序上下文,通過Context的createPackageContextAsUser來獲取Context物件。之後再解析成對應的佈局物件。然後就可以使用了。