目录
1. 现象
前段时间在开发某个应用的Widget时,由于数据集是一个集合,所以remoteViews采用了AdapterViewFlipper这么一个有轮播功能的View去展示列表数据。当数据集有变更时,原则上只需要调用AppWidgetManager.notifyAppWidgetViewDataChanged这个方法就可以走到我们本地重写的RemoteViewsFactory.onDataSetChanged方法,在这里面开始重新拉取数据并更新数据集,并会自动刷新桌面上的Widget。
但是实际上大概率会出现调用了notifyAppWidgetViewDataChanged方法但不会走到onDataSetChanged方法,导致数据一直无法更新,Widget内容也就没有更新。
2. 原因
经过一行一行代码排查,发现是在调用notifyAppWidgetViewDataChanged方法的前后由于业务需要同时也使用了WorkManager.enqueueUniqueWork来插入了一个延时任务,如果把WorkManager相关的代码注掉就没有问题了。
这时候我就好奇了,为毛WorkManager的插入任务操作会影响到小部件的刷新???
我在网上找了半天,中文互联网上我是没有搜到任何相关的信息,没办法只能求助万能的Google了,最后终于找到了有一篇文章说了类似的现象。
WorkManager, App Widgets, and the Cost of Side Effects (commonsware.com)
经过他的提醒,我顺着WorkManager源码和Widget源码一路查找,发现确实大致如这篇文章所说,那我就先说简单的结论:
WorkManager的插入任务会触发Intent.ACTION_PACKAGE_CHANGED广播,而AppWidgetServiceImpl类会监听这个广播,并采触发AppWidgetProvider的onUpdate方法。
从代码上看,
WorkManager插入任务时,会enable一个自定义的广播接收器组件:
public class EnqueueRunnable implements Runnable {
@Override
public void run() {
...
PackageManagerHelper.setComponentEnabled(context, RescheduleReceiver.class, true);
...
}
public class PackageManagerHelper {
public static void setComponentEnabled(@NonNull Context context,@NonNull Class<?> klazz,boolean enabled) {
...
packageManager.setComponentEnabledSetting(componentName,
enabled
? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
: PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP);
...
}
}
而在PackageManagerService的实现方法中,会发送一个Intent.ACTION_PACKAGE_CHANGED广播
private void setEnabledSettings(List<ComponentEnabledSetting> settings, int userId,String callingPackage) {
...
try {
final Computer newSnapshot = snapshotComputer();
for (int i = 0; i < sendNowBroadcasts.size(); i++) {
final String packageName = sendNowBroadcasts.keyAt(i);
final ArrayList<String> components = sendNowBroadcasts.valueAt(i);
final int packageUid = UserHandle.getUid(
userId, pkgSettings.get(packageName).getAppId());
sendPackageChangedBroadcast(newSnapshot, packageName, false /* dontKillApp */,
components, packageUid, null /* reason */);
}
...
}
而AppWidgetServiceImpl恰好监听了这个广播:
private void onPackageBroadcastReceived(Intent intent, int userId) {
switch (action) {
...
default: {
...
changed = Intent.ACTION_PACKAGE_CHANGED.equals(action);
...
}
}
...
synchronized (mLock) {
...
if (added || changed) {
...
for (String pkgName : pkgList) {
componentsModified |= updateProvidersForPackageLocked(pkgName, userId, null);
...
}
}
...
}
}
private boolean updateProvidersForPackageLocked(String packageName, int userId,
Set<ProviderId> removedProviders) {
...
if (packageName.equals(ai.packageName)) {
...
AppWidgetProviderInfo info =
createPartialProviderInfo(providerId, ri, provider);
if (info != null) {
...
final int M = provider.widgets.size();
if (M > 0) {
int[] appWidgetIds = getWidgetIds(provider.widgets);
...
for (int j = 0; j < M; j++) {
Widget widget = provider.widgets.get(j);
widget.views = null;
scheduleNotifyProviderChangedLocked(widget);
}
// Now that we've told the host, push out an update.
sendUpdateIntentLocked(provider, appWidgetIds);
}
}
}
}
private void scheduleNotifyProviderChangedLocked(Widget widget) {
long requestId = UPDATE_COUNTER.incrementAndGet();
if (widget != null) {
// When the provider changes, reset everything else.
widget.updateSequenceNos.clear();
widget.updateSequenceNos.append(ID_PROVIDER_CHANGED, requestId);
}
...
}
在收到广播后触发这个应用所有Widget的onUpdate方法重新添加新的RemoteViews,并清除Widget里缓存的RemoteViews对象以及更新请求,而这个清除也就会导致清除掉我们调用notifyAppWidgetViewDataChanged方法添加的数据更新请求,所以回到桌面后,只会触发onUpdate,不会触发onDataSetChanged,所以我们的数据一直得不到更新。
尤其是文章中提到的,如果直接在AppWidgetProvider的onUpdate方法里面使用WorkManager,可能会陷入循环,造成不好的结果。
3. 方案
目前Google官方并没有给出解决方案,我们只能弃用WorkManager,还是直接使用AlarmManager或者JobSchedule来实现定时器效果,毕竟WorkManager本质上也是对这两种框架的包装而已,那我们干脆还是直接使用这两个即可。