問題現(xiàn)象:在切換語言后短時間,偶現(xiàn)(概率較高)快速撥號小部件更新失敗
分析:問題發(fā)生時,App內重寫的回調函數(shù)onDataSetChanged()沒有被回調。
排查流程,快速撥號增刪條目后App調用AppWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, getListId());
- App調用AppWidgetService接口更新小部件
a) BaseWidgetProvider.updateWidget(),進行更新,該步驟沒問題
b) appWidgetManager.notifyAppWidgetViewDataChanged() - AppWidgetService通知RemoteViewsService更新某個特定的RemoteViews
a) 在AppWidgetServiceImpl. scheduleNotifyAppWidgetViewDataChanged()中
widget.updateRequestIds.put(viewId, requestId);
其中requestId是一個自增的數(shù)字。
b) 在AppWidgetHost.startListening()中,處理從lastWidgetUpdateRequestId到現(xiàn)有requestId最大值的所有request。生成update,將update添加到updatesMap(Array)中。
c) AppWidgetHost.startListening()
d) viewDataChanged()調用adapter.notifyDataSetChanged() - RemoteViewsService通知應用實現(xiàn)的RemoteViewsAdapter更新數(shù)據(jù)(App重寫回調函數(shù)onDatasetChanged,自動回調)
a) 最終回調到應用實現(xiàn)的RemoteViewsFactory.onDataSetChanged(),局部更新小部件 - 應用將新的RemoteViews再次傳遞給AppWidgetService
- AppWidgetService通知Launcher更新小部件
切換語言后,onDataSetChanged()未回調,直接原因是notifyDataSetChanged()沒執(zhí)行,向前追溯到2.d沒執(zhí)行。通過log可知2.a已經(jīng)執(zhí)行,那么問題出現(xiàn)在2.b-2.c之間。
Host.startListening()是執(zhí)行了的,應該沒有問題。
打印lastWidgetUpdateRequestId和requestId相關log,一些時序問題可能引起數(shù)字關系錯亂比如lastWidgetUpdateRequestId ≥ requestId,或者requestId沒有被更新,調用路徑就會斷掉。
加log后發(fā)現(xiàn)經(jīng)過切換語言,隨著用戶操作,requestId不斷在增加,lastWidgetUpdateRequestId卻一直沒有改變。
widget.updateRequestIds相當于keyedVector,key是viewId,value是requestId,
viewId有三類不同取值:
- ID_VIEWS_UPDATE = 0
- ID_PROVIDER_CHANGED = 1
- 任意viewId,用于更新數(shù)據(jù)
App直接調用updateAppWidget()則viewId = ID_VIEWS_UPDATE
本問題中應用更新GridView內容,使用AppWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, getListId());
getListId()這個參數(shù)就是App內部GridView的viewId。
對于一個widget,viewId是一直不變的,而requestId是一個不斷增長的數(shù)字。對于同一個widget的多次更新,新的<viewId, requestId>會把舊的成員替掉。所以widget.updateRequestIds最多只有三個成員,分別對應viewId的三種取值。
打印這里的RequestIds信息,切換語言之前:
updateRequestIds[0] key: 0, value: 21
updateRequestIds[1] key: 2131362287, value: 22
切換語言之后:
updateRequestIds[0] key: 0, value: 36
updateRequestIds[1] key: 1, value: 29
updateRequestIds[2] key: 2131362287, value: 37
問題:
- provider應該是沒有變的,但這里有一個29號更新ID_PROVIDER_CHANGED,不清楚來源是什么。
- 應該不影響小部件更新,不關注
- 切換語言之后,再次更新小部件,AppWidgetHostView.viewDataChanged()并不會將viewId判斷為BaseAdapter,也就不會觸發(fā)后續(xù)的更新。這里要加log看看之后的流向。
- adapter = null,defer update,這里說是delay,但之后也沒有執(zhí)行
在AppWidgetHostView中調用
((RemoteAdapterConnectionCallback) adapterView).deferNotifyDataSetChanged();該方法是一個接口函數(shù),在AbsListView的實現(xiàn)中僅有一行:
mDeferNotifyDataSetChanged = true;
該布爾值控制是否在adapter連接到服務時更新,生效位置:
//AbsListView.java
public boolean onRemoteAdapterConnected() {
if (mDeferNotifyDataSetChanged) {
mRemoteAdapter.notifyDataSetChanged();
mDeferNotifyDataSetChanged = false;
...
AbsListView實現(xiàn)了RemoteAdapterConnectionCallback接口,
onRemoteAdapterConnected也是該接口的一個方法。
onServiceConnected
在AbsListView.setRemoteViewsAdapter()中創(chuàng)建了RemoteViewsAdapter實例,this作為callbacks
mRemoteAdapter = new RemoteViewsAdapter(getContext(), intent, this, isAsync);
RemoteViewsAdapter構造函數(shù)中創(chuàng)建connection
mServiceConnection = new RemoteViewsAdapterServiceConnection(this);
從log中看,似乎是時序問題,在09:21切換語言后,adapter已經(jīng)連接到服務,從AbsListView的實現(xiàn)看,此時adapter應該是非空的。
但在09:34更新小部件時,仍然提示adapter = null。是否在09:21后disconnect?需要看一下發(fā)生問題前后adapter對象是否有變化。
》》》》更新小部件
Line 1757: 01-06 06:08:40.341 2221 2221 D AbsListView: +++setRemoteViewsAdapter()
Line 1797: 01-06 06:08:40.460 2221 2221 D AbsListView: +++setRemoteViewsAdapter()
》》》》切換語言
Line 9915: 01-06 06:09:21.342 2221 2221 D AbsListView: +++setRemoteViewsAdapter()
Line 9916: 01-06 06:09:21.342 2221 2221 D AbsListView: set mDeferNotifyDataSetChanged = false
Line 9934: 01-06 06:09:21.399 2221 2221 D AbsListView: onRemoteAdapterConnected()
》》》》再次更新小部件
Line 12404: 01-06 06:09:34.738 2221 2221 D AbsListView: deferNotifyDataSetChanged
Line 12429: 01-06 06:09:34.802 2221 2221 D AbsListView: +++setRemoteViewsAdapter()
Line 12430: 01-06 06:09:34.802 2221 2221 D AbsListView: set mDeferNotifyDataSetChanged = false
僅當connect時mDeferNotifyDataSetChanged = true才會通知更新,該值初始化為false,僅在deferNotifyDataSetChanged()賦值為true,如果沒有被使用那就是這個對象被銷毀重建了,導致這個值沒有使用。
在經(jīng)過deferNotifyDataSetChanged()之后,GridView銷毀,又新建,因此這個布爾值已經(jīng)不是原來的了。
也可能View本身沒有被銷毀,但對應的Adapter銷毀了,需要查清楚Adapter創(chuàng)建的時機。

dumpsys appwidget 發(fā)現(xiàn)一個現(xiàn)象。
Launcher在前臺:
Widgets:
[0] id=4
host=HostId{user:0, app:10034, hostId:1025, pkg:com.cyanogenmod.trebuchet}
provider=ProviderId{user:0, app:10033, cmp:ComponentInfo{com.android.dialer/com.tplink.contacts.appwidget.QuickDialWidgetProvider}}
host.callbacks=com.android.internal.appwidget.IAppWidgetHost$Stub$Proxy@39d9b94
views=android.widget.RemoteViews@7fa863d
Hosts:
[0] hostId=HostId{user:0, app:10034, hostId:1025, pkg:com.cyanogenmod.trebuchet}
callbacks=com.android.internal.appwidget.IAppWidgetHost$Stub$Proxy@39d9b94
widgets.size=1 zombie=false
Launcher在后臺:
adapter is null, defer update
Widgets:
[0] id=4
host=HostId{user:0, app:10034, hostId:1025, pkg:com.cyanogenmod.trebuchet}
provider=ProviderId{user:0, app:10033, cmp:ComponentInfo{com.android.dialer/com.tplink.contacts.appwidget.QuickDialWidgetProvider}}
host.callbacks=null
views=android.widget.RemoteViews@7f75304
Hosts:
[0] hostId=HostId{user:0, app:10034, hostId:1025, pkg:com.cyanogenmod.trebuchet}
callbacks=null
widgets.size=1 zombie=false
launcher切換到后臺時callbacks = null,應該是stopListening()之后注銷了callbacks。
nova Launcher無問題,原生launcher有該問題。nova Launcher調到后臺也不會stopListening(),widget.host.callbacks != null。因此不會走到deferNotifyDataSetChanged()這個分支。而是調用RemoteViewsServiceImpl.handleNotifyAppWidgetViewDataChanged()
private void handleNotifyAppWidgetViewDataChanged(Host host, IAppWidgetHost callbacks,
int appWidgetId, int viewId, long requestId) {
...
final ServiceConnection connection = new ServiceConnection() {
public void onServiceConnected(ComponentName name, IBinder service) {
IRemoteViewsFactory cb = IRemoteViewsFactory.Stub.asInterface(service);
try {
cb.onDataSetChangedAsync();
}
...
這里直接調用RemoteViewsFactory.onDataSetChanged(),立即通知應用更新數(shù)據(jù),就不會有我們遇到的問題。
抓取GridView相關的log,當問題發(fā)生后,每次更新小部件都會構造兩次GridView,第一次構造完成后,AppWidgetHost識別的callback就是這一個。但緊接著就進行了第二次構造,第一次的結果就被拋棄了。
AbsListView: +++initAbsListView() this is android.widget.GridView{cab617b V.E..V... ......I. 0,0-0,0 #7f0a01ef app:id/layout_contacts_added_list}
AppWidgetHostView: callback is android.widget.GridView{cab617b V.ED.VC.. ......I. 0,0-0,0 #7f0a01ef app:id/layout_contacts_added_list}
AbsListView: +++initAbsListView() this is android.widget.GridView{c427d4f V.E..V... ......I. 0,0-0,0 #7f0a01ef app:id/layout_contacts_added_list}
AbsListView: +++setRemoteViewsAdapter(), this is android.widget.GridView{c427d4f V.ED.VC.. ......I. 0,0-0,0 #7f0a01ef app:id/layout_contacts_added_list}
GridView: setAdapter: android.widget.RemoteViewsAdapter@da63112
如何找到GridView創(chuàng)建的時機?由于不是應用主動創(chuàng)建的,應該是在GridView綁定到widget時創(chuàng)建。
梳理了一下相關類的繼承關系:
GridView -> AbsListView -> AdapterView -> ViewGroup -> View
AbsListView實現(xiàn)了多個接口:TextWatcher, ViewTreeObserver.OnGlobalLayoutListener, Filter.FilterListener, ViewTreeObserver.OnTouchModeChangeListener, RemoteViewsAdapter.RemoteAdapterConnectionCallback。
繼承關系:RemoteViewsAdapter -> BaseAdapter
BaseAdapter實現(xiàn)了接口:ListAdapter, SpinnerAdapter,ListAdapter繼承自Adapter(Adapter是一個接口而非類)
RemoteViews實現(xiàn)了接口:Parcelable, Filter
繼承關系:RemoteViewsService -> Service,在RemoteViewsService中定義了接口RemoteViewsFactory(應用需要實現(xiàn)RemoteViewsService,并提供RemoteViewsFactory這個接口以填充remote view如GridView)
RemoteViewsAdapter -> BaseAdapter
App實現(xiàn)了RemoteViewsService和RemoteViewsFactory,并在updateWidget()中進行如下設置:
final Intent intent = new Intent(context, QuickDialWidgetService.class);
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
remoteViews.setRemoteAdapter(appWidgetId, R.id.layout_contacts_added_list, intent);
setRemoteAdapter()的實現(xiàn)僅新建了一個Action,并沒有實際執(zhí)行動作。
setRemoteAdapter()的實現(xiàn):
public void setRemoteAdapter(int viewId, Intent intent) {
addAction(new SetRemoteViewsAdapterIntent(viewId, intent));
}
class SetRemoteViewsAdapterIntent繼承了另一個內部類Class Action。
在AppWidgetHostView.updateAppWidget()調用AppWidgetHostView.applyRemoteViews()。
嘗試調用RemoteViews.reapply()和RemoteViews.apply(),這兩個方法實現(xiàn)差不多,都會調用RemoteViews.preformApply()
以RemoteViews.apply()作為切入點
public View apply(Context context, ViewGroup parent, OnClickHandler handler) {
RemoteViews rvToApply = getRemoteViewsToApply(context);
...
rvToApply.performApply(result, parent, handler);
...
}
performApply把rvToApply.mActions依次執(zhí)行一遍。
// SetRemoteViewsAdapterIntent.apply()
public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
final View target = root.findViewById(viewId);
if (target instanceof AbsListView) {
AbsListView v = (AbsListView) target;
v.setRemoteViewsAdapter(intent, isAsync);
v.setRemoteViewsOnClickHandler(handler);
AbsListView.setRemoteViewsAdapter()中首先判斷intent是否已經(jīng)存在,如果存在說明已經(jīng)有了一個RemoteViewsAdapter,直接返回。否則新建一個RemoteViewsAdapter,在RemoteViewsAdapter的構造函數(shù)中新建一個RemoteViewsAdapterServiceConnection,
在RemoteViewsAdapterServiceConnection.onServiceConnected()中得到factory
mRemoteViewsFactory = IRemoteViewsFactory.Stub.asInterface(service);
從log中可以看到RemoteViewsService.onBind(Intent intent)根據(jù)傳入的intent,在sRemoteViewFactories中查找是否已經(jīng)有該intent對應的factory被創(chuàng)建,如果有則不會在創(chuàng)建新的;如果沒有則新建一個。
嘗試了一種解法:
如果每次RemoteViewsAdapterServiceConnection.onServiceConnected都強制調用notifyDataSetChanged()是否可以解決問題?
RemoteViewsAdapter有一個布爾型變量private boolean mNotifyDataSetChangedAfterOnServiceConnected = false;
RemoteViewsAdapterServiceConnection.onServiceConnected()會判斷該變量,如果為true則調用notifyDataSetChanged()
這里發(fā)現(xiàn)一個問題,如果初始化直接置為true,則小部件顯示不正常,一直是空白的,說明GridView顯示有問題,一直無法傳遞數(shù)據(jù)過來。抓log發(fā)現(xiàn)RemoteViewsAdapter.RemoteViewsAdapterServiceConnection.onServiceConnected沒有調用,這種改動是不可行的,不采用,暫時采用修改Launcher的辦法解決。
遇到的問題:
1. Framework代碼多且調用關系復雜
- 盡量多加log,并在log中打出該處的詳細信息,有助于理解程序運行到這里是什么狀態(tài)。調用比較復雜的地方靜態(tài)分析效率很低,如果跟蹤到錯誤的分支會浪費很多時間。
- 打印調用棧,梳理調用流程。適用于調用流程較長且不涉及進程間通信的地方。
2. GridView的實現(xiàn)
知識面空白。
GridView用于實現(xiàn)九宮格樣式的列表,Android已經(jīng)提供了接口,應用只需要自行實現(xiàn)一個Adapter、重寫一些回調函數(shù),就可以使用該控件。
解決的問題: