Widget更新问题

问题现象:在切换语言后短时间,偶现(概率较高)快速拨号小部件更新失败

分析:问题发生时,App内重写的回调函数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是一个自增的数字。
    b) 在AppWidgetHost.startListening()中,处理从lastWidgetUpdateRequestId到现有requestId最大值的所有request。生成update,将update添加到updatesMap(Array)中。
    c) AppWidgetHost.startListening()
    d) viewDataChanged()调用adapter.notifyDataSetChanged()
  3. RemoteViewsService通知应用实现的RemoteViewsAdapter更新数据(App重写回调函数onDatasetChanged,自动回调)
    a) 最终回调到应用实现的RemoteViewsFactory.onDataSetChanged(),局部更新小部件
  4. 应用将新的RemoteViews再次传递给AppWidgetService
  5. AppWidgetService通知Launcher更新小部件

切换语言后,onDataSetChanged()未回调,直接原因是notifyDataSetChanged()没执行,向前追溯到2.d没执行。通过log可知2.a已经执行,那么问题出现在2.b-2.c之间。
Host.startListening()是执行了的,应该没有问题。
打印lastWidgetUpdateRequestId和requestId相关log,一些时序问题可能引起数字关系错乱比如lastWidgetUpdateRequestId ≥ requestId,或者requestId没有被更新,调用路径就会断掉。
加log后发现经过切换语言,随着用户操作,requestId不断在增加,lastWidgetUpdateRequestId却一直没有改变。
widget.updateRequestIds相当于keyedVector,key是viewId,value是requestId,
viewId有三类不同取值:

  • ID_VIEWS_UPDATE = 0
  • ID_PROVIDER_CHANGED = 1
  • 任意viewId,用于更新数据

App直接调用updateAppWidget()则viewId = ID_VIEWS_UPDATE
本问题中应用更新GridView内容,使用AppWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, getListId());
getListId()这个参数就是App内部GridView的viewId。
对于一个widget,viewId是一直不变的,而requestId是一个不断增长的数字。对于同一个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,也就不会触发后续的更新。这里要加log看看之后的流向。
  • adapter = null,defer update,这里说是delay,但之后也没有执行

在AppWidgetHostView中调用
((RemoteAdapterConnectionCallback) adapterView).deferNotifyDataSetChanged();该方法是一个接口函数,在AbsListView的实现中仅有一行:
mDeferNotifyDataSetChanged = true;

该布尔值控制是否在adapter连接到服务时更新,生效位置:

//AbsListView.java
public boolean onRemoteAdapterConnected() {
  if (mDeferNotifyDataSetChanged) {
    mRemoteAdapter.notifyDataSetChanged();
    mDeferNotifyDataSetChanged = false;
...

AbsListView实现了RemoteAdapterConnectionCallback接口,
onRemoteAdapterConnected也是该接口的一个方法。
onServiceConnected
在AbsListView.setRemoteViewsAdapter()中创建了RemoteViewsAdapter实例,this作为callbacks
mRemoteAdapter = new RemoteViewsAdapter(getContext(), intent, this, isAsync);
RemoteViewsAdapter构造函数中创建connection
mServiceConnection = new RemoteViewsAdapterServiceConnection(this);

从log中看,似乎是时序问题,在09:21切换语言后,adapter已经连接到服务,从AbsListView的实现看,此时adapter应该是非空的。
但在09:34更新小部件时,仍然提示adapter = null。是否在09:21后disconnect?需要看一下发生问题前后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,如果没有被使用那就是这个对象被销毁重建了,导致这个值没有使用。
在经过deferNotifyDataSetChanged()之后,GridView销毁,又新建,因此这个布尔值已经不是原来的了。

也可能View本身没有被销毁,但对应的Adapter销毁了,需要查清楚Adapter创建的时机。


Android Adapter

dumpsys appwidget 发现一个现象。
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(),立即通知应用更新数据,就不会有我们遇到的问题。

抓取GridView相关的log,当问题发生后,每次更新小部件都会构造两次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创建的时机?由于不是应用主动创建的,应该是在GridView绑定到widget时创建。
梳理了一下相关类的继承关系:
GridView -> AbsListView -> AdapterView -> ViewGroup -> View
AbsListView实现了多个接口:TextWatcher, ViewTreeObserver.OnGlobalLayoutListener, Filter.FilterListener, ViewTreeObserver.OnTouchModeChangeListener, RemoteViewsAdapter.RemoteAdapterConnectionCallback。
继承关系:RemoteViewsAdapter -> BaseAdapter
BaseAdapter实现了接口:ListAdapter, SpinnerAdapter,ListAdapter继承自Adapter(Adapter是一个接口而非类)
RemoteViews实现了接口:Parcelable, Filter
继承关系:RemoteViewsService -> Service,在RemoteViewsService中定义了接口RemoteViewsFactory(应用需要实现RemoteViewsService,并提供RemoteViewsFactory这个接口以填充remote view如GridView)
RemoteViewsAdapter -> BaseAdapter

App实现了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()的实现仅新建了一个Action,并没有实际执行动作。

setRemoteAdapter()的实现:

public void setRemoteAdapter(int viewId, Intent intent) {
    addAction(new SetRemoteViewsAdapterIntent(viewId, intent));
}

class SetRemoteViewsAdapterIntent继承了另一个内部类Class Action。
在AppWidgetHostView.updateAppWidget()调用AppWidgetHostView.applyRemoteViews()。
尝试调用RemoteViews.reapply()和RemoteViews.apply(),这两个方法实现差不多,都会调用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依次执行一遍。

// 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是否已经存在,如果存在说明已经有了一个RemoteViewsAdapter,直接返回。否则新建一个RemoteViewsAdapter,在RemoteViewsAdapter的构造函数中新建一个RemoteViewsAdapterServiceConnection,
在RemoteViewsAdapterServiceConnection.onServiceConnected()中得到factory
mRemoteViewsFactory = IRemoteViewsFactory.Stub.asInterface(service);

从log中可以看到RemoteViewsService.onBind(Intent intent)根据传入的intent,在sRemoteViewFactories中查找是否已经有该intent对应的factory被创建,如果有则不会在创建新的;如果没有则新建一个。


尝试了一种解法:
如果每次RemoteViewsAdapterServiceConnection.onServiceConnected都强制调用notifyDataSetChanged()是否可以解决问题?
RemoteViewsAdapter有一个布尔型变量private boolean mNotifyDataSetChangedAfterOnServiceConnected = false;
RemoteViewsAdapterServiceConnection.onServiceConnected()会判断该变量,如果为true则调用notifyDataSetChanged()
这里发现一个问题,如果初始化直接置为true,则小部件显示不正常,一直是空白的,说明GridView显示有问题,一直无法传递数据过来。抓log发现RemoteViewsAdapter.RemoteViewsAdapterServiceConnection.onServiceConnected没有调用,这种改动是不可行的,不采用,暂时采用修改Launcher的办法解决。


遇到的问题:

1. Framework代码多且调用关系复杂
  • 尽量多加log,并在log中打出该处的详细信息,有助于理解程序运行到这里是什么状态。调用比较复杂的地方静态分析效率很低,如果跟踪到错误的分支会浪费很多时间。
  • 打印调用栈,梳理调用流程。适用于调用流程较长且不涉及进程间通信的地方。
2. GridView的实现

知识面空白。
GridView用于实现九宫格样式的列表,Android已经提供了接口,应用只需要自行实现一个Adapter、重写一些回调函数,就可以使用该控件。

解决的问题:

1. 切换语言后,应用执行了两次updateWidget,有什么用意,是否属于bug?
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,923评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,154评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,775评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,960评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,976评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,972评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,893评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,709评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,159评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,400评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,552评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,265评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,876评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,528评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,701评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,552评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,451评论 2 352

推荐阅读更多精彩内容