تسک‌های پس‌زمینه در .NET MAUI: راهنمای عملی BGTaskScheduler در iOS و WorkManager در اندروید (۲۰۲۶)

راهنمای عملی اجرای تسک‌های پس‌زمینه در .NET MAUI: پیاده‌سازی BGTaskScheduler روی iOS و WorkManager روی اندروید ۱۴ همراه کد کامل و Debug.

تسک پس‌زمینه MAUI: iOS و اندروید ۲۰۲۶

به‌روزرسانی: ۲۲ ژوئیه ۲۰۲۶

برای اجرای تسک‌های پس‌زمینه در .NET MAUI باید به دو API بومی متفاوت متصل شوید: BGTaskScheduler در iOS ۱۳+ و WorkManager در اندروید. MAUI هیچ انتزاع یکپارچه‌ای برای این کار ندارد؛ بنابراین از طریق #if IOS و #if ANDROID در پوشه Platforms/ کد پلتفرم‌محور می‌نویسید، در Info.plist شناسه‌های تسک را ثبت می‌کنید و در اندروید ۱۴+ نوع Foreground Service را در AndroidManifest.xml اعلام می‌کنید. صادقانه بگویم، اولین باری که این کار را در یک اپ فروشگاهی انجام دادم، دو هفته وقتم را با کرش NSInternalInconsistencyException در iOS و ForegroundServiceStartNotAllowedException در اندروید ۱۴ گرفت. در ادامه یک پیاده‌سازی کامل و قابل کپی می‌بینید که در دستگاه‌های واقعی (هم iPhone و هم Pixel 8 با Doze فعال) اجرا و آزمایش شده است.

  • iOS از BGAppRefreshTask برای رفرش کوتاه (~۳۰ ثانیه) و BGProcessingTask برای کار سنگین‌تر شبانه استفاده می‌کند؛ شناسه‌ها باید در Info.plist ذیل BGTaskSchedulerPermittedIdentifiers ثبت شوند.
  • در اندروید، WorkManager API رسمی و پیشنهادی گوگل است و در برابر Doze، App Standby و ری‌استارت دستگاه مقاوم است.
  • از اندروید ۱۴ (API 34) اعلام foregroundServiceType در مانیفست اجباری است و نبود آن باعث ForegroundServiceStartNotAllowedException در زمان اجرا می‌شود.
  • هیچ تضمینی برای زمان دقیق اجرای تسک پس‌زمینه در iOS وجود ندارد؛ سیستم بر اساس رفتار کاربر، شارژ و شبکه تصمیم می‌گیرد.
  • برای تست BGTaskScheduler باید از دستور _simulateLaunchForTaskWithIdentifier در LLDB استفاده کنید.
  • در MAUI بهتر است لایه سرویس مشترک را با IBackgroundJobScheduler انتزاع کنید و پیاده‌سازی iOS/Android را جداگانه ثبت کنید.

چرا .NET MAUI انتزاع مشترک ندارد؟

در تجربه من از پورت کردن چند پروژه Xamarin.Forms به MAUI، اولین سؤال تیم همیشه همین بود: «چرا Essentials API واحدی برای Background Task ندارد؟» جواب صریح این است: سیاست‌های اجرای پس‌زمینه در iOS و اندروید از پایه با هم فرق دارند و تیم MAUI به‌درستی تصمیم گرفت به‌جای ساختن یک انتزاع نشتی، شما را مستقیم به APIهای پلتفرم متصل کند. اپل کنترل انحصاری بر زمان‌بندی دارد و برنامه شما نمی‌تواند «الان یک تسک اجرا کن» بگوید؛ فقط می‌توانید تسک را ثبت کنید و منتظر باشید BGTaskScheduler فرصت مناسبی پیدا کند.

در اندروید برعکس، شما با WorkManager می‌توانید Constraint‌ها را دقیق تعریف کنید (مثلاً «فقط وقتی شبکه Wi-Fi وصل است و باتری بالای ۳۰٪ است»)، اما بین OEMها (سامسونگ، شیائومی، هواوی) رفتار Battery Optimization متفاوت است و باید کاربر را برای Whitelist کردن هدایت کنید. این تفاوت بنیادین یعنی هر انتزاعی که بخواهد این دو مدل را یکی کند، در نهایت مجبور می‌شود پایین‌ترین مخرج مشترک را عرضه کند و شما در Production با مشکل روبرو می‌شوید. راه‌حل درست، یک interface کوچک در لایه Shared و دو پیاده‌سازی مجزا در Platforms/iOS و Platforms/Android است، دقیقاً همان الگویی که در پیاده‌سازی نوتیفیکیشن Push در MAUI استفاده کردیم.

گزینه‌های پس‌زمینه در iOS

اپل چهار مسیر رسمی برای اجرای کد در پس‌زمینه فراهم می‌کند و انتخاب اشتباه یعنی برنامه شما یا اصلاً اجرا نمی‌شود یا در بازبینی App Store رد خواهد شد. مسیر اول BGAppRefreshTask است: بازه‌ای معمولاً ۳۰ ثانیه‌ای که سیستم چند بار در روز به شما می‌دهد تا داده‌های سبک را رفرش کنید (مثلاً آخرین پست‌های فید). مسیر دوم BGProcessingTask است که برای کارهای سنگین‌تر مانند پاک‌سازی دیتابیس یا آموزش مدل CoreML طراحی شده و معمولاً شب‌هنگام و در حالت شارژ اجرا می‌شود. مسیر سوم Silent Push است که سرور شما با هدر content-available: 1 ارسال می‌کند و PerformFetchWithCompletionHandler را در برنامه فراخوانی می‌کند. مسیر چهارم URLSession Background برای دانلود و آپلود بلندمدت است.

برای اکثر سناریوهای MAUI، ترکیب BGAppRefreshTask برای همگام‌سازی دوره‌ای و BGProcessingTask برای نگهداری داده کافی است. طبق مستندات رسمی Background Tasks framework اپل، شما باید هر شناسه تسک را در فایل Info.plist ذیل کلید BGTaskSchedulerPermittedIdentifiers ثبت کنید و در FinishedLaunching فراخوانی BGTaskScheduler.Shared.Register را انجام دهید. اگر ثبت را بعد از این نقطه انجام دهید، ران‌تایم با NSInternalInconsistencyException کرش می‌کند. زمان اجرای واقعی توسط الگوریتم on-device intelligence اپل تعیین می‌شود که رفتار روزانه کاربر، وضعیت باتری، اتصال شبکه و ساعت شب را می‌سنجد.

WorkManager در اندروید و مقاومت در برابر Doze

WorkManager بخشی از Android Jetpack است و از اندروید ۴.۰ (API 14) به بعد کار می‌کند. مزیت اصلی آن این است که به‌صورت خودکار زیر کاپوت از JobScheduler، AlarmManager یا BroadcastReceiver استفاده می‌کند و شما فقط با یک API واحد سر و کار دارید. برخلاف API‌های قدیمی مثل AsyncTask یا IntentService که در اندروید ۱۲+ محدود شده‌اند، WorkManager در برابر Doze Mode، App Standby Buckets و حتی ری‌استارت دستگاه مقاوم است. سه نوع Work اصلی وجود دارد: OneTimeWorkRequest برای اجرای یک‌باره، PeriodicWorkRequest برای اجرای دوره‌ای با حداقل بازه ۱۵ دقیقه، و ExpeditedWorkRequest که در اندروید ۱۲+ برای کارهای زمان‌حساس معرفی شد و ۱۰ دقیقه فرصت اجرای پس‌زمینه می‌گیرد.

یک نکته حیاتی که در مستندات WorkManager رسمی گوگل کمتر برجسته شده این است که Constraint‌ها ابزار قدرتمندی هستند اما رفتار OEMها تفاوت دارد. مثلاً روی MIUI شیائومی، اگر برنامه در Autostart Whitelist نباشد، حتی WorkManager هم اجرا نمی‌شود. برای پروژه‌های تجاری همیشه یک صفحه راهنما در تنظیمات اپ می‌گذارم که کاربر را به Battery Optimization Exemption با intent ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS هدایت می‌کند. این یک الگو است که در معماری Offline-First در MAUI نیز به کار می‌آید، جایی که همگام‌سازی صف Outbox باید در پس‌زمینه انجام شود.

طراحی لایه سرویس مشترک با IBackgroundJobScheduler

قبل از دست‌زدن به کد پلتفرمی، یک اینترفیس کوچک در پروژه MAUI shared تعریف کنید. این کار باعث می‌شود ViewModel‌ها و سرویس‌های Domain بدون وابستگی مستقیم به iOS/Android بتوانند تسک ثبت کنند.

// File: Services/IBackgroundJobScheduler.cs
namespace MobileTechLead.Background;

public interface IBackgroundJobScheduler
{
    Task RegisterPeriodicSyncAsync(TimeSpan minimumInterval);
    Task ScheduleOneTimeAsync(string jobId, TimeSpan delay);
    Task CancelAsync(string jobId);
}

// File: Services/BackgroundJobIds.cs
public static class BackgroundJobIds
{
    // شناسه‌های یکسان در iOS Info.plist و Android WorkManager
    public const string PeriodicSync = "com.mobiletechlead.sync.periodic";
    public const string NightlyCleanup = "com.mobiletechlead.cleanup.nightly";
}

سپس در MauiProgram.cs با استفاده از دستورات پیش‌پردازنده، پیاده‌سازی مناسب هر پلتفرم را در DI Container ثبت می‌کنید:

// File: MauiProgram.cs (بخش سرویس‌ها)
builder.Services.AddSingleton<ISyncService, SyncService>();

#if IOS
    builder.Services.AddSingleton<IBackgroundJobScheduler,
        Platforms.iOS.Background.IosBackgroundScheduler>();
#elif ANDROID
    builder.Services.AddSingleton<IBackgroundJobScheduler,
        Platforms.Android.Background.AndroidBackgroundScheduler>();
#endif

پیاده‌سازی iOS با BGTaskScheduler

در iOS، فایل Platforms/iOS/Info.plist باید سه چیز داشته باشد: مجوز حالت پس‌زمینه، لیست شناسه‌های تسک، و اطلاعات UIBackgroundModes. کد زیر را داخل تگ <dict> اصلی اضافه کنید:

<!-- File: Platforms/iOS/Info.plist -->
<key>UIBackgroundModes</key>
<array>
    <string>fetch</string>
    <string>processing</string>
</array>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
    <string>com.mobiletechlead.sync.periodic</string>
    <string>com.mobiletechlead.cleanup.nightly</string>
</array>

حالا در Platforms/iOS/AppDelegate.cs در متد FinishedLaunching، تسک‌ها را ثبت می‌کنیم. این ثبت باید قبل از بازگشت متد اتفاق بیفتد وگرنه سیستم iOS خطا می‌دهد:

// File: Platforms/iOS/AppDelegate.cs
using BackgroundTasks;
using Foundation;
using UIKit;

[Register("AppDelegate")]
public class AppDelegate : MauiUIApplicationDelegate
{
    protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();

    public override bool FinishedLaunching(UIApplication app, NSDictionary opts)
    {
        // ثبت هندلر برای هر شناسه؛ بدون این کار در زمان اجرا کرش می‌کند
        BGTaskScheduler.Shared.Register(
            BackgroundJobIds.PeriodicSync,
            dispatchQueue: null,
            task => HandleAppRefresh((BGAppRefreshTask)task));

        BGTaskScheduler.Shared.Register(
            BackgroundJobIds.NightlyCleanup,
            dispatchQueue: null,
            task => HandleProcessing((BGProcessingTask)task));

        return base.FinishedLaunching(app, opts);
    }

    private async void HandleAppRefresh(BGAppRefreshTask task)
    {
        // زمان‌بندی رفرش بعدی — iOS این کار را خودکار انجام نمی‌دهد
        ScheduleNextAppRefresh();

        var cts = new CancellationTokenSource();
        task.ExpirationHandler = () => cts.Cancel();

        try
        {
            var syncService = IPlatformApplication.Current!
                .Services.GetRequiredService<ISyncService>();
            await syncService.PullLatestAsync(cts.Token);
            task.SetTaskCompleted(success: true);
        }
        catch (OperationCanceledException)
        {
            task.SetTaskCompleted(success: false);
        }
    }

    private async void HandleProcessing(BGProcessingTask task)
    {
        var cts = new CancellationTokenSource();
        task.ExpirationHandler = () => cts.Cancel();

        try
        {
            var cleanup = IPlatformApplication.Current!
                .Services.GetRequiredService<ICleanupService>();
            await cleanup.RunAsync(cts.Token);
            task.SetTaskCompleted(true);
        }
        catch
        {
            task.SetTaskCompleted(false);
        }
    }

    public static void ScheduleNextAppRefresh()
    {
        var request = new BGAppRefreshTaskRequest(BackgroundJobIds.PeriodicSync)
        {
            // زودترین زمان اجرا — سیستم می‌تواند دیرتر اجرا کند
            EarliestBeginDate = NSDate.FromTimeIntervalSinceNow(15 * 60)
        };
        BGTaskScheduler.Shared.Submit(request, out var error);
        if (error is not null)
            System.Diagnostics.Debug.WriteLine($"BGTask submit error: {error}");
    }
}

و در نهایت پیاده‌سازی IBackgroundJobScheduler برای iOS:

// File: Platforms/iOS/Background/IosBackgroundScheduler.cs
using BackgroundTasks;
using Foundation;

public sealed class IosBackgroundScheduler : IBackgroundJobScheduler
{
    public Task RegisterPeriodicSyncAsync(TimeSpan minimumInterval)
    {
        var request = new BGAppRefreshTaskRequest(BackgroundJobIds.PeriodicSync)
        {
            EarliestBeginDate = NSDate.FromTimeIntervalSinceNow(minimumInterval.TotalSeconds)
        };
        BGTaskScheduler.Shared.Submit(request, out _);
        return Task.CompletedTask;
    }

    public Task ScheduleOneTimeAsync(string jobId, TimeSpan delay)
    {
        var request = new BGProcessingTaskRequest(jobId)
        {
            EarliestBeginDate = NSDate.FromTimeIntervalSinceNow(delay.TotalSeconds),
            RequiresNetworkConnectivity = true,
            RequiresExternalPower = false
        };
        BGTaskScheduler.Shared.Submit(request, out _);
        return Task.CompletedTask;
    }

    public Task CancelAsync(string jobId)
    {
        BGTaskScheduler.Shared.Cancel(jobId);
        return Task.CompletedTask;
    }
}

پیاده‌سازی اندروید با WorkManager

در اندروید، قبل از هر چیز پکیج Xamarin.AndroidX.Work.Runtime را نصب کنید. سپس یک Worker با ارث‌بری از Worker بنویسید. من ترجیح می‌دهم Worker با Hilt-style DI کار کند، اما در MAUI ساده‌ترین راه استفاده از IPlatformApplication.Current.Services است:

// File: Platforms/Android/Background/SyncWorker.cs
using Android.Content;
using AndroidX.Work;
using Microsoft.Maui;

public class SyncWorker : Worker
{
    public SyncWorker(Context context, WorkerParameters parameters)
        : base(context, parameters) { }

    public override Result DoWork()
    {
        try
        {
            var syncService = IPlatformApplication.Current!
                .Services.GetRequiredService<ISyncService>();

            // Worker همیشه روی thread غیر UI اجرا می‌شود؛ بنابراین
            // GetAwaiter().GetResult() اینجا امن است
            syncService.PullLatestAsync(default).GetAwaiter().GetResult();

            return Result.InvokeSuccess();
        }
        catch (System.Exception ex)
        {
            Android.Util.Log.Error("SyncWorker", ex.ToString());
            // در صورت خطای موقت با backoff تدریجی retry می‌کند
            return Result.InvokeRetry();
        }
    }
}

و حالا پیاده‌سازی AndroidBackgroundScheduler که Constraint‌ها را تنظیم می‌کند و کار را در صف WorkManager قرار می‌دهد:

// File: Platforms/Android/Background/AndroidBackgroundScheduler.cs
using Android.Content;
using AndroidX.Work;
using Java.Util.Concurrent;

public sealed class AndroidBackgroundScheduler : IBackgroundJobScheduler
{
    private WorkManager Manager =>
        WorkManager.GetInstance(Android.App.Application.Context);

    public Task RegisterPeriodicSyncAsync(TimeSpan minimumInterval)
    {
        var constraints = new Constraints.Builder()
            .SetRequiredNetworkType(NetworkType.Connected)
            .SetRequiresBatteryNotLow(true)
            .Build();

        // حداقل بازه اجرای PeriodicWork در اندروید ۱۵ دقیقه است
        var minutes = (long)System.Math.Max(15, minimumInterval.TotalMinutes);

        var request = new PeriodicWorkRequest.Builder(
                Java.Lang.Class.FromType(typeof(SyncWorker)),
                minutes, TimeUnit.Minutes!)
            .SetConstraints(constraints)
            .SetBackoffCriteria(BackoffPolicy.Exponential,
                WorkRequest.MinBackoffMillis, TimeUnit.Milliseconds!)
            .Build();

        Manager.EnqueueUniquePeriodicWork(
            BackgroundJobIds.PeriodicSync,
            ExistingPeriodicWorkPolicy.Keep,
            request);

        return Task.CompletedTask;
    }

    public Task ScheduleOneTimeAsync(string jobId, TimeSpan delay)
    {
        var request = new OneTimeWorkRequest.Builder(
                Java.Lang.Class.FromType(typeof(SyncWorker)))
            .SetInitialDelay((long)delay.TotalSeconds, TimeUnit.Seconds!)
            .Build();

        Manager.EnqueueUniqueWork(
            jobId, ExistingWorkPolicy.Replace, request);
        return Task.CompletedTask;
    }

    public Task CancelAsync(string jobId)
    {
        Manager.CancelUniqueWork(jobId);
        return Task.CompletedTask;
    }
}

Foreground Service و الزامات اندروید ۱۴

اگر تسک شما بیش از ۱۰ دقیقه طول می‌کشد یا کاربر باید بداند چیزی در حال اجراست (مثلاً آپلود فایل بزرگ یا Sync اولیه بعد از لاگین)، باید از Foreground Service استفاده کنید. از اندروید ۱۴ (API 34)، گوگل قوانین سخت‌گیرانه‌ای اعمال کرده و هر Foreground Service باید نوع مشخصی داشته باشد. در صورت عدم رعایت، رانتایم ForegroundServiceStartNotAllowedException پرتاب می‌کند و اپ شما در Play Store هم رد می‌شود.

در فایل Platforms/Android/AndroidManifest.xml باید مجوزها و نوع سرویس را اعلام کنید:

<!-- File: Platforms/Android/AndroidManifest.xml -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<application ...>
    <service
        android:name=".Background.SyncForegroundService"
        android:exported="false"
        android:foregroundServiceType="dataSync" />
</application>

انواع مجاز شامل dataSync، mediaPlayback، location، connectedDevice، camera، microphone و health هستند. طبق راهنمای رسمی Android 14 Foreground Service Types، انتخاب نوع اشتباه می‌تواند باعث حذف اپ در بازبینی Play Store شود.

تست، Debug و شبیه‌سازی

یکی از دشوارترین بخش‌های کار با تسک پس‌زمینه، تست کردن آن است. iOS به شما اجازه نمی‌دهد به‌سادگی بگویید «الان اجرا کن»، اما یک ترفند LLDB وجود دارد. برنامه را روی سیمولاتور یا دستگاه واقعی اجرا کنید، آن را Suspend کنید و در Debug Console این دستور را وارد کنید:

e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.mobiletechlead.sync.periodic"]

برای اندروید، ابزار adb کارتان را راه می‌اندازد. برای Trigger فوری یک PeriodicWork:

adb shell cmd jobscheduler run -f com.mobiletechlead.app 999
# یا برای وارد کردن اپ به Doze:
adb shell dumpsys deviceidle force-idle

حتماً روی چند دستگاه واقعی متفاوت (Pixel، سامسونگ، شیائومی) تست کنید. سامسونگ و شیائومی رفتار Battery Optimization پرخاش‌گرانه‌تری دارند و ممکن است WorkManager شما پس از چند ساعت اصلاً اجرا نشود. برای استراتژی جامع تست‌نویسی در MAUI، توصیه می‌کنم لایه Domain را کاملاً از پلتفرم جدا کنید تا Worker خود قابل Unit Test باشد.

اشتباهات متداول و رفع خطا

خطای BGTaskSchedulerErrorDomain Code=1

این خطا یعنی شناسه تسک در Info.plist ثبت نشده یا برنامه در حال اجراست. مطمئن شوید BGTaskSchedulerPermittedIdentifiers دقیقاً همان رشته‌ای است که در کد ثبت می‌کنید (حساس به حروف بزرگ و کوچک).

Worker من فقط یک بار اجرا می‌شود و سپس متوقف می‌شود

احتمالاً از Result.Success() استاتیک استفاده کرده‌اید در حالی که در Xamarin/MAUI باید Result.InvokeSuccess() صدا زده شود. همچنین اگر Exception بگیرید و آن را swallow کنید، WorkManager فرض می‌کند کار موفق بوده و retry نمی‌کند.

ForegroundServiceStartNotAllowedException

در اندروید ۱۲+ نمی‌توانید از پس‌زمینه یک Foreground Service شروع کنید مگر با استثناهای محدود (Bluetooth، Alarm، Notification action). راه‌حل استفاده از ExpeditedWorkRequest است.

تسک iOS در سیمولاتور اجرا می‌شود اما در دستگاه واقعی خیر

الگوریتم اپل تا چند روز رفتار کاربر را می‌سنجد. اگر برنامه به‌ندرت باز می‌شود، iOS اولویت اجرای پس‌زمینه را کاهش می‌دهد. هیچ راه‌حل «تضمینی» وجود ندارد؛ اگر نیاز حیاتی به اجرای دقیق دارید، از Silent Push به همراه سرور خودتان استفاده کنید.

سؤالات متداول

آیا .NET MAUI به‌صورت داخلی از تسک پس‌زمینه پشتیبانی می‌کند؟

خیر. MAUI هیچ انتزاع مشترکی ارائه نمی‌دهد و شما باید در پوشه Platforms/iOS از BGTaskScheduler و در Platforms/Android از WorkManager استفاده کنید. الگوی توصیه‌شده تعریف اینترفیس IBackgroundJobScheduler در پروژه Shared و ثبت دو پیاده‌سازی مجزا در DI Container است.

حداقل فاصله زمانی بین اجرای دو PeriodicWork در اندروید چقدر است؟

۱۵ دقیقه. WorkManager هر مقدار کمتر را به ۱۵ دقیقه گرد می‌کند. برای بازه‌های کوتاه‌تر باید از OneTimeWorkRequest با زنجیره‌کردن مصنوعی استفاده کنید که رفتار پیش‌بینی‌ناپذیر ایجاد می‌کند و توصیه نمی‌شود.

چرا تسک پس‌زمینه iOS در دستگاه واقعی اجرا نمی‌شود؟

iOS بر اساس الگوی استفاده کاربر، وضعیت باتری، اتصال شبکه و ساعت روز تصمیم می‌گیرد. اگر کاربر روزی چند بار اپ را باز می‌کند، احتمال اجرا بالا می‌رود. برای تست دستی از دستور _simulateLaunchForTaskWithIdentifier در LLDB استفاده کنید.

آیا در اندروید ۱۴ نیاز به Foreground Service Type است؟

بله. از API 34 اعلام android:foregroundServiceType در مانیفست اجباری شده و انتخاب نوع صحیح (مانند dataSync) الزامی است. عدم رعایت باعث ForegroundServiceStartNotAllowedException در زمان اجرا و رد شدن اپ در Play Store می‌شود.

آیا WorkManager پس از ری‌استارت دستگاه هم کار می‌کند؟

بله. WorkManager به‌صورت پایدار در دیتابیس SQLite داخلی خود ذخیره می‌شود و پس از boot خودکار Job‌های زمان‌بندی‌شده را بازیابی می‌کند. برای این کار نیاز به مجوز خاصی نیست چون کتابخانه به‌طور خودکار BroadcastReceiver مناسب را ثبت می‌کند.

David O'Reilly
درباره نویسنده David O'Reilly

Native iOS/Android specialist turned MAUI advocate. Writes about the gritty platform details most cross-platform tutorials skip.