如何修复JobScheduler更新Widget时的ANR错误?
解决Widget更新引发的ANR问题:分析你的两种方案
你遇到的问题很典型——在通过广播触发Widget更新时出现ANR,而且本地复现困难,这大概率是因为广播接收器的onReceive方法运行在主线程,一旦执行时间超过系统阈值(通常是10秒左右)就会触发ANR。先结合你的代码和两种方案来逐一分析:
你的现有代码
JobScheduler 中的任务代码
@Override public boolean onStartJob(final JobParameters params) { // 把工作放到新线程 new Thread(() -> { try { // 接收更新数据并存到SharedPreferences ... MyWidgetProvider.updateWidgets(getApplicationContext()); } catch (Exception e) { Log.e(TAG, "onStartJob: " + e.getMessage()); } finally { jobFinished(params, false); } }).start(); return true; }
AppWidgetProvider 代码
protected static void updateWidget(Context context) { try { ComponentName thisWidget = new ComponentName(context, MyWidgetProvider.class); int[] appWidgetIds = AppWidgetManager.getInstance(context.getApplicationContext()).getAppWidgetIds(thisWidget); if (appWidgetIds != null && appWidgetIds.length > 0) { for (int appWidgetId : appWidgetIds) { try { Intent intent = new Intent(context, MyWidgetProvider.class); intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); intent.setAction(UPDATE_WIDGET); context.sendBroadcast(intent); } catch (Exception e) { } } } } catch (Exception e) { Log.e(TAG, "error" + e.getMessage()); } } @Override public void onReceive(Context context, Intent intent) { super.onReceive(context, intent); try { Bundle extras = intent.getExtras(); String action = intent.getAction(); if (extras != null && !TextUtils.isEmpty(action)) { int appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); if (appWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID) { if (action.equals(UPDATE_WIDGET)) { RemoteViews remoteViews = getRemoteView(context); // 从SharedPreferences读取数据并更新remoteViews .... AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); appWidgetManager.partiallyUpdateAppWidget(appWidgetId, remoteViews); } } } } catch (Exception e) { Log.e(TAG, "onReceive() error: " + e.getMessage()); } }
方案1分析:直接传递数据替代SharedPreferences
这个方案非常值得立即实施。当前你在onReceive里读取SharedPreferences,哪怕有内存缓存,也存在潜在的磁盘IO耗时(比如缓存失效时)。直接把JobScheduler线程中获取到的更新数据传递给Widget更新逻辑,能彻底省去这部分IO开销,大幅缩短onReceive的执行时间,从根源上降低ANR的触发概率。
方案2疑问解答:合并广播是否有助于缓解ANR?
先明确核心前提:广播的onReceive方法始终运行在主线程。
你现在的做法是给每个Widget单独发广播,这意味着主线程会被多次唤醒,执行N次onReceive(N是Widget数量),每次都要完成读数据、更新RemoteViews的操作。改成一次广播传递所有Widget ID,在onReceive里循环处理,相当于把N次主线程的小任务合并成一次连续的主线程任务。
这种改动的效果分两种情况:
- 如果单Widget更新的耗时很短,合并后总耗时仍低于ANR阈值:这种情况下,合并广播反而更好——减少了系统广播调度的开销,避免主线程被频繁打断,降低了和其他主线程任务叠加触发ANR的风险。
- 如果单Widget更新耗时已经不低,合并后总耗时超过ANR阈值:这种情况下,合并广播会让主线程被连续阻塞更长时间,反而更容易触发ANR。
所以方案2本身不能直接解决ANR,关键还是要把耗时操作移出主线程。
更彻底的优化建议
其实你完全可以跳过广播这一步,直接在JobScheduler的后台线程完成所有准备工作,然后一次性更新所有Widget:
@Override public boolean onStartJob(final JobParameters params) { new Thread(() -> { try { // 1. 在后台线程获取更新数据 DataModel updatedData = fetchLatestData(); // 2. 在后台线程构建RemoteViews(这里要确保RemoteViews的构建没有主线程依赖) RemoteViews remoteViews = buildUpdatedRemoteViews(getApplicationContext(), updatedData); // 3. 获取所有Widget ID,一次性批量更新 ComponentName widgetComponent = new ComponentName(getApplicationContext(), MyWidgetProvider.class); int[] widgetIds = AppWidgetManager.getInstance(getApplicationContext()).getAppWidgetIds(widgetComponent); if (widgetIds != null && widgetIds.length > 0) { AppWidgetManager.getInstance(getApplicationContext()).updateAppWidget(widgetIds, remoteViews); } } catch (Exception e) { Log.e(TAG, "onStartJob: " + e.getMessage()); } finally { jobFinished(params, false); } }).start(); return true; }
这种方式完全避开了广播的主线程限制,所有耗时操作都在后台线程完成,从根源上杜绝了ANR的可能。
内容的提问来源于stack exchange,提问作者jclova
相关产品推荐
相关产品推荐

