问题现象:在切换语言后短时间,偶现(概率较高)快速拨号小部件更新失败
分析:问题发生时,App内重写的回调函数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是一个自增的数字。
b) 在AppWidgetHost.startListening()中,处理从lastWidgetUpdateRequestId到现有requestId最大值的所有request。生成update,将update添加到updatesMap(Array)中。
c) AppWidgetHost.startListening()
d) viewDataChanged()调用adapter.notifyDataSetChanged() - RemoteViewsService通知应用实现的RemoteViewsAdapter更新数据(App重写回调函数onDatasetChanged,自动回调)
a) 最终回调到应用实现的RemoteViewsFactory.onDataSetChanged(),局部更新小部件 - 应用将新的RemoteViews再次传递给AppWidgetService
- 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
问题:
- provider应该是没有变的,但这里有一个29号更新ID_PROVIDER_CHANGED,不清楚来源是什么。
- 应该不影响小部件更新,不关注
- 切换语言之后,再次更新小部件,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创建的时机。
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、重写一些回调函数,就可以使用该控件。
解决的问题: