Introduction
Many games download in-game assets using OTA(over the air) . This is mainly because they have lots of assets and it is not possible to package all of them in the client. This also allows us to push and update the assets to the client without a client release to the play store.
While downloading the assets, the player might not be able to play their normal game session which is typically blocked by a screen which shows the progress of the download. This can annoy users and lead to a loss in user retention as they can switch to other apps during the download process.
Whenever the app is in the background the Main/UI thread of the app is paused. In Android, we can create worker threads to download the assets which can run even when the app is moved to the background. After the player returns to the app, the game should have downloaded the assets in the background and the player can continue playing.
On iOS, we have URLSession APIs to do any network-related tasks. It provides a downloadTask to download files and which can later be saved to the FileSystem. But once the App goes to the background the job can run a maximum of 30 sec, after which it times out, and the download fails. Also, all threads created by the App process would be paused after the app goes to the background.
We can configure the URLSession object to download files in the background, but the download start time is not guaranteed. And the subsequent downloads will even wait for more than the earlier one. It's ideal for a single, large, time-insensitive download task.
With iOS 16 iOS has introduced the Background Assets Framework which can allow us to download multiple files in the background without the above limitations of URL Session. This article details the steps needed to implement the background download on iOS.
The Background Assets Framework also has some limitations which will be explained later, but it is comparatively much better than URLSession. We developed the Background Download Library for our games which is a wrapper of the Background Assets Framework. In the following sections, we will provide a detailed explanation of the Background Download Library.
Background Download Architecture
iOS Background Assets Framework
Background Assets Framework was introduced in iOS 16. To use the framework, we need to add an Extension(a new target) along with our App target in XCode. Each app target runs as a separate process on the device.
The App target contains our game logic and the scheduling logic. It runs as a User process on the device. The Background Assets Framework provides the shared class BADownloadManager that is used to schedule the download, monitor the progress, and modify the ongoing downloads.
The Extension target runs as a separate System process managed by the OS. The Framework provides the BADownloaderExtension, which should be implemented in the Extension target. The extension is triggered by the OS during App Installation, App Updates, and Periodic Events and it gets callbacks to the implemented BADownloaderExtension class.
The app and extension should contain the same App Group ID in their entitlements so that they can share the same App Group folder. Files that are scheduled for download will be downloaded to the App Group folder, which is accessible by both the app and the extension.
As both the App and Extension run as separate processes, the callbacks and the scheduling logic might run in both the App and Extension. To ensure mutual exclusion we should use the shared class’ withExclusiveControl.
As the asset scheduling logic is the same for both the App and Extension, we developed the Background Download Library and it is imported by both App and Extension.
Overview and Architecture Diagram
Let’s see how the App, Extension, Background Download Library, iOS Background Assets Framework and App Group folder are structured in the device.
The Background Assets framework is a shared Framework between the App and Extension and is managed by the OS.
Let's take a closer look inside the library for a more detailed exploration. Some of the classes/components are not shown to simplify the diagram.
The library can detect whether it is an App environment or an Extension environment and can take appropriate flow.
App-related changes
The iOS App / Native Interface (for Unity Projects) can act as a Client to the library and can schedule and receive the status of the downloads. Internally, the BackgroundDownloadManager uses the Background Assets Framework to schedule downloads and receive callbacks, which are then sent back to the client.
Extension-related changes
In the Extension, there won’t be any clients. Instead, we have a main class BackgroundDownloadManagerExtension that extends BADownloaderExtension and is triggered by OS. It receives iOS events i.e., Install, Update, and Periodic events. It will also schedule downloads to the Framework and will receive callbacks.
App Group Folder
Initially, files are downloaded to the App Group folder, and they are moved to the App Data folder by the App.
App Group Folder is also used to share objects and communicate between the App and the extension which will be explained in coming sections.
Multi Download Request
A collection of download requests is referred to as a Multi Download Request. These requests can be categorized based on the entities that schedule them. The library accepts requests from the App (or client), Extension. Therefore, we support two types of Multi Download Requests.
Client Requests
These are scheduled by the App itself and the files associated with these requests will be downloaded and saved to the App Data folder.
Extension Requests
These are scheduled by the Extension, during app installation or update events. The files associated with these requests will be downloaded to the App Group folder.
Each Multi Download Request has a unique ID as client_ABC, extension_PQR, etc. The Download Request also has a unique ID as client_ABC-request_1, client_ABC-request_2, etc.
Sharing Requests between App and Extension
We store requests in the App Group folder. The App and Extension will sync them whenever required. Each MultiDowload request folder has constants, mutable, requests batch, and request mutable files.
We separated mutables and constants so that the constants file will be synced only once and mutables can be synced/updated whenever required. The constant Batch file contains 500 requests so that all can be read once. We download requests batch-wise and proceed to the next batch. The status of individual requests is stored in their respective mutable file.
All file operations happen in withExclusiveControl to ensure mutual exclusion between App and Extension.
Scheduling Algorithm
BackgroundDownloadManager contains the queue of MultiDownload requests. We process each MultiDownload request sequentially.
While processing each MultiDownload request, we sync the current batch of requests(500 requests per batch) into the memory. From the current batch, we schedule 20 concurrent downloads each time.
After successful downloads, we schedule the next requests in the current batch maintaining the maximum concurrent downloads to 20, and continue until all requests are finished in the current batch. After that, we sync the next batch of 500 requests and will process them, and so on.
We process requests batch-wise because we have memory restrictions in the extension process.
After scheduling requests from the current batch we try to sync the next batch and keep it in the cache. We sync the next batch because sometimes due to memory limitations, the extension process might get killed at the sync step. But due to scheduled requests, it will be respawned. So maximum we keep 2 batches in the memory( i.e., the current and the next batch).
After scheduling we receive callbacks of success and failures of the requests. In the App process, we can move the downloaded files to the App Data folder. But In the Extension process, we keep the downloaded files in the App Group folder itself and update the status to downloadedToTemp, Later the App can move them to the appropriate App Data folders.
Issues / Challenges
The below section outlines the issues we faced and our approach inorder to resolve them.
Lock is not getting released - https://developer.apple.com/forums/thread/726053, https://developer.apple.com/forums/thread/733803
Whenever we are not able to get the lock, we kill the extension process forcibly and wait until the app process is also killed by the player. After both App and Extension are killed, from the next launch we can acquire the lock.
Memory Limit in Extension - https://developer.apple.com/forums/thread/726053.
In the extension process, we have a 6MB memory limit, so we load requests only batch-wise into the memory and we clear it up after processing them.
Notifications - https://developer.apple.com/forums/thread/725834
The extension runs on a tight sandbox and we can’t trigger notifications from the extension to show the download status. So we are not showing the download status in the notifications.
The time limit of 10-15 min for Extension
The scheduling logic has a time limit in the extension. So it should be quick and after it exceeds the limit, the next scheduling in the extension happens only if OS triggers it.
Scheduled downloads getting missed.
Sometimes the requests that are scheduled are not getting downloaded and are not present in current downloads either. In this case we make the request fail and will retry them again.
Sometimes the download stops and resumes by itself.
We don’t have any workaround for this. The download is managed by OS and pausing and resuming of it depends on the OS logic.