从新的上下文绑定服务以进行配置更改,还是从应用程序上下文绑定?

11

我正在尝试确定绑定服务是否适用于我的应用程序中的后台工作。要求是各种应用程序组件可以通过它进行不同优先级的Web请求。(因此,服务必须维护某种队列,并能够取消其正在进行的请求以执行更高优先级的其他请求)。我希望该服务对用户相对不会显眼,以便在完成应用程序后他们不会发现它正在运行 - 如果我想做一些在应用程序关闭后仍然持续的更重要的事情,我可以使用startForeground()在过程中推送通知。

解决方案1:从活动中绑定

因此,对于给定的应用程序组件,它应该能够绑定到服务来完成工作。但是,似乎有一个众所周知的问题,即如果活动正在进行绑定,则在配置更改(旋转)期间将丢失绑定,因为该活动将被关闭。

因此,我想我可以使用另一个上下文(new Context())并从其绑定到服务,然后使用非UI片段在配置更改期间维护此上下文,直到我认为我已经完成了相关工作。我只能在配置更改期间或作为从活动进行绑定的永久替代方案来执行此操作。(我应该指出,这是一种标准且推荐的维护实例的方法。跨配置更改)

解决方案2:

我看到的主要选择是,我可以使用应用程序上下文来执行绑定 - 但这可能会持续太久吗?和/或是否可能存在应用程序上下文和服务之间的某些循环关系,从而阻止服务和应用程序上下文被销毁?

问题:

因此,我试图回答自己的问题是:我应该使用第一种方法(具有临时上下文的活动)?还是第二种方法(只需将服务绑定到应用程序上下文)?

我是否正确地认为应用程序上下文可以多次绑定到服务,然后取消相同数量的绑定?(即,每个上下文可以有多个有效绑定)?

在第一种解决方案中使用自己的上下文(new Context())是否可能会导致任何问题?

编辑

找到了更多信息:https://groups.google.com/forum/#!topic/android-developers/Nb58dOQ8Xfw

似乎很难“创建”一个任意的上下文,因此方案1和2的组合似乎是合适的,其中服务连接在配置更改时保持不变,但绑定到应用程序上下文。 我仍然担心可能从应用程序上下文中取消绑定两次的可能性。 自己保留绑定计数似乎是不必要的 - 有人可以确认/否认绑定是每个连接而不是每个上下文吗?


Raph - 当上下文被销毁时,服务不会死掉吗? - Sam
你知道activity中的retainInstance()方法吗?可能这正适合你的需求。 - RaphMclee
你可以将你的service同时设置为startedbound。那么,unbind不会销毁该service(当它自行决定工作已完成且没有活动绑定到它时,service本身将负责调用stopSelf)。 - Tomasz Gawel
@TomaszGawel 我认为在解除绑定时自动销毁的功能是绑定服务中最理想的特性。 - Sam
@TomaszGawel,我在我的回答中发布了一些代码以演示我的意思... - Sam
显示剩余5条评论
4个回答

5

经过一番探索,我认为我已经想出了一个(尚未)未经测试的解决方案。

首先,基于Diane在这里的建议:https://groups.google.com/forum/#!topic/android-developers/Nb58dOQ8Xfw 我应该绑定到应用程序上下文 - 因此我失去上下文的问题已经解决 - 我可以使用非UI片段来跨配置更改维护我的ServiceConnection - 太好了。然后,当我完成后,我可以使用应用程序上下文来返回服务连接并取消绑定。我不应该收到任何泄漏服务连接警告。(我应该指出,这是一种标准和推荐的跨配置更改维护实例的方法)

这个问题的最终关键在于我不确定是否可以从同一上下文中多次绑定 - 绑定的文档暗示了绑定和上下文生命周期之间存在某种依赖关系,因此我担心我需要做自己的引用计数形式。我查看了源代码,并最终到达了这里:http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.4.2_r1/android/app/LoadedApk.java#LoadedApk.forgetServiceDispatcher%28android.content.Context%2Candroid.content.ServiceConnection%29 关键是这些行:
sd = map.get(c);
    if (sd != null) {
        map.remove(c);
        sd.doForget();
        if (map.size() == 0) {
            mServices.remove(context);
        }

揭示了map被用于我担心的引用计数。

所以需要注意的是:

  • 绑定服务与应用程序上下文一起使用时可以正常工作,我们应该这样做以防止在配置更改期间从一个活动泄漏服务连接到另一个活动
  • 我可以将我的服务连接安全地保留在非UI片段上,并在完成后使用它来解除绑定

我会尝试发布一些经过测试的代码。

更新和经过测试的解决方案:我编写了一些代码进行测试,并在此处发布:https://github.com/samskiter/BoundServiceTest

它似乎运行得很好,非UI片段(数据片段)作为旋转更改期间捕获来自服务结果的良好代理监听器(侦听器的目的是紧密绑定请求以确保其保持响应。显然,任何模型更改都可以通过观察者传播到UI中。)

编辑:我认为我应该明确回答OP中的问题...

  • 我应该使用第一种方法(具有临时上下文的活动)?还是第二种(仅将服务绑定到应用程序上下文)? 第二种

  • 我是否正确地认为应用程序上下文可以多次绑定到服务,然后以相同数量的次数取消绑定? (即,您可以在每个上下文中具有多个有效绑定)? 是的

  • 在第一个解决方案中使用自己的上下文(new Context())是否会导致任何问题? 这甚至不可能

最后总结:

这种模式应该非常强大-我可以优先处理来自应用程序各个来源的网络IO(或其他任务)。 我可以有一个前台活动执行用户要求的一些小io,同时我可以启动一个前台服务来同步所有用户数据。 前台服务和活动都可以绑定到同一个网络服务以完成它们的请求。

所有这些都可以确保服务只在需要的时间内存在-即,它与Android很好地配合。

我很高兴尽快将其应用于应用程序中。

更新:我尝试着在这篇博客文章中写下一些背景工作的相关问题并提供一些上下文信息:http://blog.airsource.co.uk/2014/09/10/android-bound-services/


这样你就可以保持连接。这是使用onretainnonconfigurationinstance的新替代方法,而且并不复杂。 - Sam
这只是另一层复杂性。你使用碎片仅仅是为了维护你将服务绑定到的上下文,这对我来说很奇怪,因为一个服务有自己的上下文,可以在不绑定任何东西的情况下运行。但是请发布你的最终解决方案,因为我很想看看你最终使用了什么。 - Rarw
啊,抱歉,我可能解释得不太清楚。我只打算使用片段来维护连接而不是上下文。像那样维护上下文会导致泄漏。保持连接实际上只是一种保持指向服务的指针以使其保持活动状态的方法。因此,当所有指向服务的“指针”都消失时,它就可以被销毁了。 - Sam
你指的是引用。但是你的片段生命周期仍然与托管它的活动相关联,无论是后台还是其他情况。那么配置更改如何不会导致片段生命周期发生变化并失去与服务的连接呢? - Rarw
它使用一个非UI片段,在配置更改时保留:http://developer.android.com/guide/topics/resources/runtime-changes.html#RetainingAnObject - Sam
显示剩余4条评论

0
我曾经遇到过类似的问题,我在一个Activity中使用了一个Bound Service。在Activity中,我定义了一个ServiceConnection,即mConnection,并在onServiceConnected方法中设置了一个类字段syncService,它是对Service的引用。
private SynchronizerService<Entity> syncService;

(...)

/** Defines callbacks for service binding, passed to bindService() */
private ServiceConnection mConnection = new ServiceConnection() {

    @Override
    public void onServiceConnected(ComponentName className, IBinder service) {
        // We've bound to LocalService, cast the IBinder and get
        // LocalService instance
        Log.d(debugTag, "on Service Connected");
        LocalBinder binder = (LocalBinder) service;
        //HERE
        syncService = binder.getService();
        //HERE
        mBound = true;
        onPostConnect();
    }

    @Override
    public void onServiceDisconnected(ComponentName arg0) {
        Log.d(debugTag, "on Service Disconnected");
        syncService = null;
        mBound = false;
    }
};

使用这种方法,每当方向改变时,我都会在引用syncService变量时遇到NullPointerException,尽管服务正在运行,并且我尝试了几种从未奏效的方法。
我即将实施Sam提出的解决方案,使用保留的片段来保持变量,但首先记得尝试一个简单的事情:将syncService变量设置为静态。 当方向改变时,连接引用仍然保持! 所以现在我有
private static SynchronizerService<Entity> syncService = null;

...

/** Defines callbacks for service binding, passed to bindService() */
private ServiceConnection mConnection = new ServiceConnection() {

    @Override
    public void onServiceConnected(ComponentName className, IBinder service) {
        // We've bound to LocalService, cast the IBinder and get
        // LocalService instance
        Log.d(debugTag, "on Service Connected");
        LocalBinder binder = (LocalBinder) service;
        //HERE
        if(syncService == null) {
            Log.d(debugTag, "Initializing service connection");
            syncService = binder.getService();
        }
        //HERE
        mBound = true;
        onPostConnect();
    }

    @Override
    public void onServiceDisconnected(ComponentName arg0) {
        Log.d(debugTag, "on Service Disconnected");
        syncService = null;
        mBound = false;
    }
};

1
@Sam 我不明白为什么不行。如果它是一个异步任务,我会同意,但如果服务始终在运行,并且其生命周期是良好已知和受控的,为什么不能保留一个私有静态变量,甚至是全局变量,反正,如果想要重用它,为什么不呢?虽然我同意你的解决方案可能更优雅,但肯定比无头片段更省成本。 - miguel_rdp
由于绑定服务并非始终运行。从活动的角度来看,您只能保证它与该活动一样长寿(现在我们有了解决方法,不包括配置更改),因此在销毁时最好清除它。因此,使其对其他任何人可用是没有意义的,因为您可能会死亡,删除连接并杀死服务。如果您意味着模型对象可以使用它-他们只需获取自己的引用并像预期的那样使用绑定活动-其中应用程序组件各自绑定它以保持其为其目的而活着。 - Sam
1
@Sam 我明白你的意思。我想这取决于架构。在我的应用程序中,它不仅仅是一个“状态”...服务应该始终可用,就像HTTP池化客户端或应用程序范围内的图像缓存一样。至于你所说的并发问题,即两个活动更改静态变量,如果两个活动尝试在保留的片段内操作变量,你的解决方案也可能存在相同的问题,不是吗? - miguel_rdp
不用担心,因为保留的片段是每个活动实例(其中实例指的是跨配置更改的活动)的。对我来说,如果您需要始终可用,则似乎不应该使用绑定服务。 - Sam
1
我使用绑定服务,因为它更容易进行通信,可以直接调用方法等。哦,我没有意识到你的保留片段是每个实例的。如果我在使用静态或单例模式时遇到问题,我一定会实现你的解决方案! - miguel_rdp
显示剩余3条评论

0

有一种更简单的处理方法,叫做IntentService,您可以在这里阅读更多相关信息。来自安卓网站的介绍:

"IntentService类提供了一个简单的结构,用于在单个后台线程上运行操作。这使得它能够处理长时间运行的操作,而不影响用户界面的响应性。此外,IntentService不会受到大多数用户界面生命周期事件的影响,因此它可以在通常会关闭AsyncTask的情况下继续运行"

与将服务绑定到您的活动不同,您可以使用启动IntentService的意图,在后台线程上开始长时间运行的操作。

public class RSSPullService extends IntentService {

    @Override
    protected void onHandleIntent(Intent workIntent) {
    // Gets data from the incoming Intent
    String dataString = workIntent.getDataString();
    ...
    // Do work here, based on the contents of dataString
    ...
    }
}

这是从Android文档中摘取的一个例子。您可以发送带有相关数据的意图,然后在服务内处理该数据以执行所需操作。例如,您可以仅向您的意图添加优先级标志,以便您的服务知道哪些请求优先于其他请求。
Intent服务的好处在于它在后台线程上运行,并且不与启动活动的生命周期绑定。这意味着您的配置更改不应影响服务执行。
当您的服务完成时,您可以使用本地广播报告工作状态 - 通过广播接收器将结果直接发送回活动,或者甚至可能通过onNewIntent()发送(尽管让其正常工作有点棘手)。 编辑 - 回答评论中的问题

IntentService是一个相对较小的类,这使得它易于修改。IntentService的原始代码在运行完任务后会调用stopSelf()并停止运行。这个问题可以很容易地解决。通过查看IntentService的源代码(请参见上面的链接),您可以看到它基本上已经使用队列工作,即在onStart()中接收消息,然后按照接收顺序执行它们,如注释所述。覆盖onStart()将允许您实现新的队列结构以满足您的需求。使用那里的示例代码来处理传入的消息并获取Intent,然后创建自己的数据结构来处理优先级。您应该能够像在Service中一样启动/停止您的Web请求。因此,通过覆盖onStart()和onHandleIntent(),您应该能够做到想要的事情。


嗨@Rarw,感谢您的回复,不幸的是我已经看过IntentService了,但它并不符合我的要求,因为我需要能够按优先级取消和排队作业,因此需要一个相当定制的服务实现:“要求各种应用程序组件可以通过它进行具有不同优先级的Web请求。(因此,服务必须维护某种队列,并能够取消正在进行的请求以获取更高优先级的其他请求)。 - Sam
这个问题的具体问题在于IntentService被设计成按顺序运行任务:“工作请求按顺序运行。如果一个操作正在IntentService中运行,并且你发送了另一个请求,该请求将等待第一个操作完成。”当然,如果你认为我理解错了,请纠正我。 - Sam
最后一点说明:我不能立即从IntentService返回,然后稍后发布更新,因为当工作队列为空时,IntentService会停止自己:“客户端通过startService(Intent)调用发送请求;服务根据需要启动,使用工作线程依次处理每个Intent,并在没有任务时停止自身。” - Sam
1
我会回答你上面的问题。 - Rarw
你的activity或fragment应该有一个stopService()方法,就像有一个startService()方法一样。但是,你也可以发送一个带有“stop”标志的意图,让服务自己停止。有很多种方法可以做到这一点。 - Rarw
显示剩余2条评论

0

您可以在清单文件中使用configChanges属性选择您想要处理的配置,并手动进行方向更改,而不是直接将方向更改处理在UI中吗? 在这种情况下,您只需要在onCreate中绑定服务,然后在onDestroy中取消绑定。

或者您也可以尝试类似以下的方法(我没有进行适当的错误检查):

    class MyServiceConnection implements ServiceConnection, Parcelable {
        public static final Parcelable.Creator CREATOR
                = new Parcelable.Creator() {
            public MyServiceConnection createFromParcel(Parcel in) {
                return new MyServiceConnection(in);
            }
public MyServiceConnection[] newArray(int size) { return new MyServiceConnection[size]; } };
@Override public int describeContents() { return 0; }
@Override public void writeToParcel(Parcel dest, int flags) {
}
@Override public void onServiceConnected(ComponentName name, IBinder service) {
}
@Override public void onServiceDisconnected(ComponentName name) {
} } MyServiceConnection myServiceConnection; boolean configChange = false;
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); if (savedInstanceState != null) { myServiceConnection = savedInstanceState.getParcelable("serviceConnection"); } else { myServiceConnection = new MyServiceConnection(); }
}
@Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); if (myServiceConnection != null) { outState.putParcelable("serviceConnection", myServiceConnection); configChange = true; } }
@Override protected void onDestroy() { super.onDestroy(); if (!configChange && myServiceConnection != null) { unbindService(myServiceConnection); } }

不是真的 - 那是最后的手段。请参见Romain Guy在这里的评论:https://dev59.com/bHE85IYBdhLWcg3wwWet 对于我的投票抱歉 - 我不希望其他人通过这种方式解决问题。 - Sam
没问题。我添加了另一个解决方案。 - hoomi
嗯,我看到你正在尝试使服务连接可被包裹,但我不知道它是如何被打包的。你能否再解释一下? - Sam
但是你在writeToParcel方法中没有做任何事情?如果你只有空构造函数,你如何调用这个方法:MyServiceConnection(in) - Sam
1
你说得完全正确。当我运行我的测试时,我没有意识到有一个ServiceConnection泄漏的问题。 - hoomi
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接