android 绑定服务在屏幕关闭时冻结

android Bound Service gets frozen when screen is off

我创建了一个非常基本的 服务 用于使用 TTS(请参阅下面的完整代码)和 运行 进行字典播放我的所有 3 android 台设备(android 版本 5、7 和 8)都存在同样的问题。

要点: 该应用程序播放词汇条目、定义和示例。在它们之间,应用程序暂停。

症状:

问题主要发生在当我使用 8 秒暂停 并且应用程序处于后台模式(屏幕关闭)时。播放只是被冻结。

调试信息:

我估计在应用程序被冻结后在 Visual Studio 中按下暂停键,以便查看是哪位代码引起的 - 不幸的是,调试器似乎使设备保持唤醒状态,这问题极难揭示.


为了防止我的应用程序被冻结,我在我的服务中获得了 部分唤醒锁(但这仍然无济于事,即使应用程序清单包含 WAKE_LOCK)

private void AcquireWakeLock(MainActivity activity)
{
    var mgr = (PowerManager)activity.ApplicationContext.GetSystemService(Context.PowerService);
    WakeLock = mgr.NewWakeLock(WakeLockFlags.Partial, "myWakeLock");
    WakeLock.Acquire();
}

我的应用程序也有 Play/Pause 按钮,我使用 TaskCompletionSource 让应用程序等待我恢复播放

public async Task PlayPause(bool isChecked, MainActivity mainActivity)
{
    if (isChecked)
    {
        ReleaseWakeLock();
        AppSuspended = new TaskCompletionSource<bool>();
        Tts.Stop();
    }
    else
    {
        AcquireWakeLock(mainActivity);
        AppSuspended.TrySetResult(true);
    }
}

然后,在每个下一个 word/phrase 即将播放之前,我使用以下代码让我的应用程序等待恢复播放

await AppSuspended.Task;

完整代码

[Service(Name = "com.my_app.service.PlaybackService")]
public class PlaybackService : Service, TextToSpeech.IOnInitListener, TextToSpeech.IOnUtteranceCompletedListener
{
    public IBinder Binder { get; private set; }
    private Java.Util.Locale Lang;
    private bool Playing;
    private int EntryIndex;
    private int DefinitionIndex;
    private DictionaryDto Dictionary;
    private EntryDto CurrentEntry;
    private DefinitionDto CurrentDefinition;
    private TaskCompletionSource<bool> AppSuspended;
    protected TextToSpeech Tts;
    private TaskCompletionSource<bool> PlaybackFinished;
    private WakeLock WakeLock;

    public override void OnCreate()
    {
        base.OnCreate();
        Tts = new TextToSpeech(this, this);
        Lang = Tts.DefaultLanguage;
        AppSuspended = new TaskCompletionSource<bool>();
        AppSuspended.TrySetResult(true);
    }

    public override IBinder OnBind(Intent intent)
    {
        Binder = new PlaybackBinder(this);
        return Binder;
    }

    public override bool OnUnbind(Intent intent)
    {
        return base.OnUnbind(intent);
    }

    public override void OnDestroy()
    {
        Binder = null;
        base.OnDestroy();
    }

    void TextToSpeech.IOnUtteranceCompletedListener.OnUtteranceCompleted(string utteranceId)
    {
        if (utteranceId.Equals("PlaybackFinished")) { PlaybackFinished.TrySetResult(true); }
    }

    void TextToSpeech.IOnInitListener.OnInit(OperationResult status)
    {
        // if we get an error, default to the default language
        if (status == OperationResult.Error)
            Tts.SetLanguage(Java.Util.Locale.Default);
        // if the listener is ok, set the lang
        if (status == OperationResult.Success)
        {
            Tts.SetLanguage(Lang);
            Tts.SetOnUtteranceCompletedListener(this);
        }
    }

    public async Task Play(string text)
    {
        Dictionary<string, string> myHashRender = new Dictionary<string, string>();
        myHashRender.Add(TextToSpeech.Engine.KeyParamUtteranceId, "PlaybackFinished");
        PlaybackFinished = new TaskCompletionSource<bool>();
        Tts.Speak(text, QueueMode.Flush, myHashRender);
        await PlaybackFinished.Task;
    }

    public async Task PlaySilence(long ms)
    {
        Dictionary<string, string> myHashRender = new Dictionary<string, string>();
        myHashRender.Add(TextToSpeech.Engine.KeyParamUtteranceId, "PlaybackFinished");
        PlaybackFinished = new TaskCompletionSource<bool>();
        Tts.PlaySilence(ms, QueueMode.Flush, myHashRender);
        await PlaybackFinished.Task;
    }

    private async Task PlayDictionary(MainActivity activity)
    {
        EntryIndex = 0;

        for (; EntryIndex < Dictionary.Entries.Count;)
        {
            CurrentEntry = Dictionary.Entries.ElementAt(EntryIndex);

            await AppSuspended.Task;

            if (!Playing) { return; }

            if (!string.IsNullOrEmpty(CurrentEntry.Text))
            {
                await AppSuspended.Task;
                if (!Playing) { return; }
                await Play(CurrentEntry.Text);
            }

            DefinitionIndex = 0;

            for (; DefinitionIndex < CurrentEntry.Definitions.Count();)
            {
                CurrentDefinition = CurrentEntry.Definitions.ElementAt(DefinitionIndex);

                await PlayDefinition();
                await PlayExamples();

                DefinitionIndex++;
            }

            if (Playing)
            {
                DefinitionIndex++;
            }

            EntryIndex++;
        }
    }

    private async Task PlayExamples()
    {
        if (!Playing) { return; }

        foreach (var example in CurrentDefinition.Examples)
        {
            if (!string.IsNullOrEmpty(example))
            {
                await AppSuspended.Task;

                if (!Playing) { return; }

                await Play(example);

                if (Playing)
                {
                    await PlaySilence((long)TimeSpan.FromSeconds(8).TotalMilliseconds);
                }
            }
        }
    }

    private async Task PlayDefinition()
    {
        if (!Playing) { return; }

        if (!string.IsNullOrEmpty(CurrentEntry.Definitions.ElementAt(DefinitionIndex).Text))
        {
            await AppSuspended.Task;

            if (!Playing) { return; }

            await PlayDefinitionText();

            if (Playing)
            {
                await PlaySilence((long)TimeSpan.FromSeconds(7).TotalMilliseconds);
            }
        }
    }

    private async Task PlayDefinitionText()
    {
        await AppSuspended.Task;

        await Play($"{CurrentEntry.Definitions.ElementAt(DefinitionIndex).Text}");
    }

    private void ReleaseWakeLock()
    {
        if (WakeLock != null)
        {
            WakeLock.Release();
        }
    }

    private void AcquireWakeLock(MainActivity activity)
    {
        var mgr = (PowerManager)activity.ApplicationContext.GetSystemService(Context.PowerService);
        WakeLock = mgr.NewWakeLock(WakeLockFlags.Partial, "myWakeLock");
        WakeLock.Acquire();
    }

    public async Task PlayPause(bool isChecked, MainActivity mainActivity)
    {
        if (isChecked)
        {
            ReleaseWakeLock();
            AppSuspended = new TaskCompletionSource<bool>();
            Tts.Stop();
        }
        else
        {
            AcquireWakeLock(mainActivity);
            AppSuspended.TrySetResult(true);
        }
    }

}

附加信息:

我所有的设备都会出现这个问题

我彻底调查了这个问题,并按照建议切换到 Foreground Service,完美解决了我的问题。

使用 LollipopNougatOreo

进行了测试

前台服务方法

将以下方法放入您的 MainActivity class

public void StartForegroundServiceSafely(Intent intent)
{
    if (Android.OS.Build.VERSION.SdkInt >= Android.OS.BuildVersionCodes.O)
    {
        StartForegroundService(intent);
    }
    else
    {
        StartService(intent);
    }
}

然后您通过 Intent

开始您的服务
public void PlayFromFile(Android.Net.Uri uri)
{
    AcquireWakeLock();

    Intent startIntent = new Intent(this, typeof(PlaybackService));
    startIntent.SetAction(PlaybackConsts.Start);
    startIntent.PutExtra("uri", uri.ToString());

    StartForegroundServiceSafely(startIntent);
}

在您的服务中实施 OnStartCommand 方法

public class PlaybackService : Service, TextToSpeech.IOnInitListener, TextToSpeech.IOnUtteranceCompletedListener

    [return: GeneratedEnum]
    public override StartCommandResult OnStartCommand(Intent intent, [GeneratedEnum] StartCommandFlags flags, int startId)
    {
        if (intent.Action.Equals(PlaybackConsts.Start))
        {
            var notification =
                new Notification.Builder(this)
                .SetContentTitle(Resources.GetString(Resource.String.ApplicationName))
                .SetContentText("HELLO WORLD")
                .SetOngoing(true)
                .Build();

            StartForeground(SERVICE_RUNNING_NOTIFICATION_ID, notification);
        }

        if (intent.Action.Equals(PlaybackConsts.Start))
        {
            var uri = Android.Net.Uri.Parse(intent.GetStringExtra("uri"));
            var content = MiscellaneousHelper.GetTextFromStream(ContentResolver.OpenInputStream(uri));
            Dictionary = DictionaryFactory.Get(content);
            Playing = true;

            Task.Factory.StartNew(async () =>
            {
                await PlayDictionary();
            });
        }
        if (intent.Action.Equals(PlaybackConsts.PlayPause))
        {
            bool isChecked = intent.GetBooleanExtra("isChecked", false);
            PlayPause(isChecked);
        }

        if (intent.Action.Equals(PlaybackConsts.NextEntry))
        {
            NextEntry();
        }

        if (intent.Action.Equals(PlaybackConsts.PrevEntry))
        {
            PrevEntry();
        }

        if (intent.Action.Equals(PlaybackConsts.Stop))
        {
            Task.Factory.StartNew(async () =>
            {
                await Stop();
            });

            StopForeground(true);
            StopSelf();
        }

        return StartCommandResult.Sticky;
    }

从上面的代码中,我们了解了如何在 OnStartCommand 方法中触发服务的功能。

如何从服务广播事件

定义你的BroadcastReceiver

[BroadcastReceiver(Enabled = true, Exported = false)]
public class PlaybackBroadcastReceiver : BroadcastReceiver
{
    public override void OnReceive(Context context, Intent intent)
    {
        var activity = MainActivity.GetInstance(); // if you need your activity here, see further code below

        if (intent.Action == "renderEntry")
        {
            string entryHtml = intent.GetStringExtra("html");

            // omitting code to keep example concise
        }
    }
}

在您的 MainActivity class 中声明接收者字段。

此外,如果您需要 activity 在 BroadcastReceiver class 中,您可以声明 GetInstance 方法(单例方法)。

public class MainActivity : AppCompatActivity
{
    PlaybackBroadcastReceiver receiver;

    protected DrawerLayout drawerLayout;
    protected NavigationView navigationView;

    protected WakeLock WakeLock;

    private static MainActivity instance;

    public static MainActivity GetInstance()
    {
        return instance;
    }

    protected override void OnCreate(Bundle bundle)
    {
        base.OnCreate(bundle);
        receiver = new PlaybackBroadcastReceiver();

        instance = this;
    }

    protected override void OnStart()
    {
        base.OnStart();
        RegisterReceiver(receiver, new IntentFilter("renderEntry"));
    }

要取消注册接收器,请使用以下行:

UnregisterReceiver(receiver);

正在从服务广播事件

在您的服务中您还必须使用 intent

private void SendRenderEntryBroadcast(EntryDto entry)
{
    Intent intent = new Intent("renderEntry");
    intent.PutExtra("html", GetEntryHtml(entry));
    SendBroadcast(intent);
}