Widget更新問題

問題現(xiàn)象:在切換語言后短時間,偶現(xiàn)(概率較高)快速撥號小部件更新失敗

分析:問題發(fā)生時,App內重寫的回調函數(shù)onDataSetChanged()沒有被回調。
排查流程,快速撥號增刪條目后App調用AppWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, getListId());

  1. App調用AppWidgetService接口更新小部件
    a) BaseWidgetProvider.updateWidget(),進行更新,該步驟沒問題
    b) appWidgetManager.notifyAppWidgetViewDataChanged()
  2. 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()
  3. RemoteViewsService通知應用實現(xiàn)的RemoteViewsAdapter更新數(shù)據(jù)(App重寫回調函數(shù)onDatasetChanged,自動回調)
    a) 最終回調到應用實現(xiàn)的RemoteViewsFactory.onDataSetChanged(),局部更新小部件
  4. 應用將新的RemoteViews再次傳遞給AppWidgetService
  5. 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

問題:

  1. provider應該是沒有變的,但這里有一個29號更新ID_PROVIDER_CHANGED,不清楚來源是什么。
  • 應該不影響小部件更新,不關注
  1. 切換語言之后,再次更新小部件,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)建的時機。


Android Adapter

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ù),就可以使用該控件。

解決的問題:

1. 切換語言后,應用執(zhí)行了兩次updateWidget,有什么用意,是否屬于bug?
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,163評論 25 708
  • 1.ios高性能編程 (1).內層 最小的內層平均值和峰值(2).耗電量 高效的算法和數(shù)據(jù)結構(3).初始化時...
    歐辰_OSR閱讀 30,262評論 8 265
  • 女人的成長比成功更重要。 我喜歡這樣的女人,穿得起幾千元的大衣,也不嫌棄幾十元的T恤;享受得了高檔的咖啡廳,也咽得...
  • 這篇文章會有很多個版本,今天的是第1版 每周會更新1~7次不等 文章的目的:嘗試著寫出一個邏輯自洽的人生解釋,讓自...
    周書恒閱讀 451評論 0 0
  • 有的人學佛,是為了心里平靜。有的人學佛,是為了生活得更好。有的人學佛就是為了解脫,自利利他。發(fā)心越大,放下的越多。...
    海平大學堂閱讀 279評論 0 1

友情鏈接更多精彩內容