【问题标题】:LocalBroadcastManager and the Activity lifecycleLocalBroadcastManager 和 Activity 生命周期
【发布时间】:2014-02-26 20:10:32
【问题描述】:

因此,我正在研究将回调接口更改为本地广播以进行一些长期运行的网络操作的可行性。由于 Activity 生命周期为需要修改 UI 的异步请求创建了各种复杂性(断开 Activity 与 onDestroy() 中的回调,不要在 onSaveInstanceState() 之后修改 FragmentTransactions 等),我认为使用本地广播更有意义,我们可以在生命周期事件中注册/注销接收器。

但是,当 Activity 在配置更改期间被销毁并重新创建时,广播接收器将不会被注册(例如在 onPause()/onResume() 之间)。因此,例如,如果我们在onCreate() 中启动异步请求,如果savedInstanceState == null(例如对于第一次启动Activity),如果用户在完成时发送的广播是否可能会丢失?在操作完成之前改变他们的设备方向?(即接收器在 onPause() 上取消注册,然后操作完成,然后在 onResume() 中重新注册接收器)

如果是这样,那么它会增加很多额外的复杂性,我们需要添加对它的支持,而且可能不值得切换。我已经研究过其他东西,例如 Otto EventBus 库,但我不确定它是否有同样的问题需要担心。

【问题讨论】:

  • Android 会在 Activity 重新启动时禁用主线程中的消息队列处理,因此应始终在您在 Activity 生命周期启动回调之一中为其注册后发送广播。
  • 这听起来很有趣,我认为可能会发生类似的事情,但你有这方面的消息来源吗?
  • 见 Dianne Hackborn 的 comment。这也记录在 Activity onRetainNonConfigurationInstance() 方法中。
  • @corsair992 谢谢!这是非常有用的信息。不幸的是,onRetainNonConfigurationInstance() 现在已弃用,取而代之的是 Fragment setRetainInstance(),所以我可能会在该组上发帖,看看是否有对 Fragment 生命周期的等效保证。如果您想发布此信息作为答案,我会接受。 :)
  • 由于Fragment 生命周期由Activity 管理,我认为它在这方面没有任何不同的语义:)

标签: android android-lifecycle localbroadcastmanager


【解决方案1】:

ActivityonRetainNonConfigurationInstance() 方法中所述,当Activity 正在重新启动时,系统会禁用主线程中的消息队列处理。这确保了发布到主线程的事件将始终在Activity 的生命周期中的稳定点传递。

然而,LocalBroadcastManagersendBroadcast() 方法似乎存在设计缺陷,因为它在将广播排队之前 评估来自发布线程的注册 BroadcastReceivers在主线程上交付,而不是在广播交付时在主线程上评估它们。虽然这使它能够报告传递的成功或失败,但它没有提供适当的语义来允许 BroadcastReceivers 暂时从主线程安全地注销,而不会丢失潜在的广播。

对此的解决方案是使用Handler 从主线程发布广播,使用sendBroadcastSync() 方法,以便立即传递广播而不是重新发布。这是一个实现此功能的示例实用程序类:

public class LocalBroadcastUtils extends Handler {
    private final LocalBroadcastManager manager;

    private LocalBroadcastUtils(Context context) {
        super(context.getMainLooper());
        manager = LocalBroadcastManager.getInstance(context);
    }

    @Override
    public void handleMessage(Message msg) {
        manager.sendBroadcastSync((Intent) msg.obj);
    }

    private static LocalBroadcastUtils instance;

    public static void sendBroadcast(Context context, Intent intent) {
        if (Looper.myLooper() == context.getMainLooper()) {
            // If this is called from the main thread, we can retain the
            // "optimization" provided by the LocalBroadcastManager semantics.
            // Or we could just revert to evaluating matching BroadcastReceivers
            // at the time of delivery consistently for all cases.
            LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
        } else {
            synchronized (LocalBroadcastUtils.class) {
                if (instance == null) {
                    instance = new LocalBroadcastUtils(context);
                }
            }
            instance.sendMessage(instance.obtainMessage(0, intent));
        }
    }
}

【讨论】:

  • 对于未来的观众,实际上这不是询问 onPause()/onResume() 用例的解决方案。这会在 onPause() 和 onDestroy() 调用之间以及 onCreate() 和 onResume() 调用之间留下空隙。
  • @biegleux:假设销毁生命周期将按顺序执行,广播传递不应该有任何“间隙”。
  • onRetainNonConfigurationInstance() 中的文档处理另一个用例。它对 onPause() / onResume() 调用一无所知。如果您在 onStop() 中发送广播,而您在 onPause() 中取消注册接收器,您会看到广播不会被传递,尽管生命周期方法由方向更改调用并且接收器在 onResume() 中注册新活动的方法。如果您在新活动的 onCreate() 方法中发送广播,同样适用。
  • @biegleux:如果你在Handler 上在onPause() 回调中发布Message,然后重新创建Activity,你会看到它会在之后被传递 Activity 被重新创建,所有销毁和创建回调都已执行。但是,您是正确的,LocalBroadcastManager 在没有注册BroadcastReceiver 的情况下不会发布任何广播,实际上只会向发布广播时注册的BroadcastReceivers 发送广播。我已经编辑了我的答案以添加它,并提出了解决此问题的方法。
【解决方案2】:

要克服这个问题,您需要一个组件,即使在配置更改时重新创建活动时也能保持活动状态。您可以使用 Application 单例或 retained Fragment

如果您使用OttoEventBus,那么您可以创建一个事件总线实例作为应用程序的一个字段,它将与设备配置更改(如方向更改)保持分离。您的活动需要在onStart() 中注册事件侦听器,它将接收最新事件。

如果您使用保留的 Fragment,则 Fragment 将保持活动状态,直到活动未完成。配置更改也不会释放保留片段的实例。使保留的 Fragment 不可见也是一种很好的做法(从 onCreateView() 方法返回 null)。在您的活动的onStart() 中,您始终可以从该片段中获取最新状态。

您可以将 LocalBroadcastManager 与其中一种方法一起使用,但这并不能真正解决问题。它就像任何其他事件总线一样,但 API 丑陋且不方便;)

【讨论】:

    【解决方案3】:

    我发现android loaders 在这种情况下非常有用。

    在我的情况下,我需要接收来自另一个应用程序的广播并在我的应用程序中管理片段转换。

    所以我喜欢下面。

    /**
     * LoaderManager callbacks
     */
    private LoaderManager.LoaderCallbacks<Intent> mLoaderCallbacks =
            new LoaderManager.LoaderCallbacks<Intent>() {
    
                @Override
                public Loader<Intent> onCreateLoader(int id, Bundle args) {
                    Logger.v(SUB_TAG + " onCreateLoader");
                    return new MyLoader(MyActivity.this);
                }
    
                @Override
                public void onLoadFinished(Loader<Intent> loader, Intent intent) {
                    Logger.i(SUB_TAG + " onLoadFinished");
                    // Display our data
                    if (intent.getAction().equals(INTENT_CHANGE_SCREEN)) {
                        if (false == isFinishing()) {
                            // handle fragment transaction 
                            handleChangeScreen(intent.getExtras());
    
                        }
                    } else if (intent.getAction().equals(INTENT_CLOSE_SCREEN)) {
                        finishActivity();
                    }
                }
    
                @Override
                public void onLoaderReset(Loader<Intent> loader) {
                    Logger.i(SUB_TAG + " onLoaderReset");
                }
    };
    
     /**
     * Listening to change screen commands. We use Loader here because
     * it works well with activity life cycle.
     * eg, like when the activity is paused and we receive command, it
     * will be delivered to activity only after activity comes back.  
     * LoaderManager handles this.
     */
    private static class MyLoader extends Loader<Intent> {
    
        private Intent mIntent;
        BroadcastReceiver mCommadListner;
        public MyLoader(Context context) {
            super(context);
            Logger.i(SUB_TAG + " MyLoader");
        }
    
        private void registerMyListner() {
            if (mCommadListner !=  null) {
                return;
            }
            Logger.i(SUB_TAG + " registerMyListner");
            mCommadListner = new BroadcastReceiver() {
                @Override
                public void onReceive(Context context, Intent intent) {
                    String action = intent.getAction();
                    if (action == null || action.isEmpty()) {
                        Logger.i(SUB_TAG + " intent action null/empty returning: ");
                        return;
                    }
                    Logger.i(SUB_TAG + " intent action: " + action);
                    mIntent = intent;
                    deliverResult(mIntent);
                }
            };
            IntentFilter filter = new IntentFilter();
            filter.addAction(INTENT_CHANGE_SCREEN);
            getContext().registerReceiver(mCommadListner, filter);
        }
    
        @Override
        protected void onStartLoading() {
            Logger.i(SUB_TAG + " onStartLoading");
            if (mIntent != null) {
                deliverResult(mIntent);
            }
            registerMyListner();
        }
    
        @Override
        protected void onReset() {
            Logger.i(SUB_TAG + "Loader onReset");
            if (mCommadListner != null) {
                getContext().unregisterReceiver(mCommadListner);
                mCommadListner = null;
            }
        }
    }
    
    Activity#onCreate or Fragment@onActivityCreated()
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // Listening to change screen commands from broadcast listner. We use Loader here because
        // it works well with activity life cycle.
        // eg, like when the activity is paused and we receive intent from broadcast, it will delivered
        // to activity only after activity comes back. LoaderManager handles this.
        getLoaderManager().initLoader(0, null, mLoaderCallbacks);
    }
    

    【讨论】:

      【解决方案4】:

      如果您的活动将被暂停或重新创建,则正常广播将丢失。您可以使用sticky broadcast,但它不适用于LocalBroadcastManager,并且您必须记住通过调用Context.removeStickyBroadcast() 手动删除粘性广播。系统会保留粘性广播(即使您的活动已暂停),直到您决定将其删除。

      EventBus 提供 postSticky() 方法,其工作原理类似于粘性广播。

      【讨论】:

      • 正如我在回答中提到的,您的陈述不准确。
      猜你喜欢
      • 2018-07-18
      • 2012-09-21
      • 2019-01-05
      • 1970-01-01
      • 2013-04-06
      • 2015-05-13
      • 2013-07-10
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多