← Zurück zum Blog

Was Ihnen die Background Assets-Dokumente von Apple nicht sagen

2026-05-20

Das Problem

Eine der überraschenderen Herausforderungen beim Aufbau von Narration Room war nicht die KI selbst – sie bestand darin, das Modell auf das Gerät des Benutzers zu übertragen.

Die App verfügt über ein 3,85-GB-Modell: einen 4-Bit-quantisierten 3,3B-Parametergenerator (ca. 2,75 GB) und ein 0,6B-Parameter-Guardian-Modell in BF16 (ca. 1,1 GB). Sie können das nicht in einer App-Binärdatei bündeln. Der offensichtliche Fallback – laden Sie es beim ersten Start mit URLSession herunter – bricht schneller zusammen, als Sie erwarten würden: Downloads brechen ab, wenn der Benutzer die App im Hintergrund startet, es gibt keinen vom System verwalteten Cache, bei der Deinstallation wird nichts bereinigt und der Benutzer verbringt seine erste Sitzung damit, auf einen Spinner zu starren.

Nach 15 Jahren auf Apple-Plattformen ging ich davon aus, dass es hier einen ausgetretenen Weg geben würde. Meistens gibt es das – es heißt Background Assets –, aber die Dokumentation lässt vieles unausgesprochen.

Warum Background Assets (und die Alternativen, die ich abgewogen habe)

Ich habe mir vier Optionen für den Versand eines Multi-GB-Modells angesehen, und es lohnt sich, noch einmal durchzugehen, wie ich über die einzelnen Optionen nachgedacht habe.

URLSession In-App-Download. Das erste, wonach Sie greifen, und das erste, was mich beim Prototyping beeindruckt hat. Downloads werden angehalten und nie wieder sauber fortgesetzt, wenn die App in den Hintergrund läuft, das Betriebssystem kann einen blockierten Download aufgrund von Speicherauslastung wiederherstellen, Sie müssen die Kontingentverwaltung neu erfinden und die Dateien bleiben nach der Deinstallation bestehen. Gut für etwas Kleines. Falsch für 4 GB.

App Clips. Ich habe kurz darüber nachgedacht und dann festgestellt, dass es das falsche Tool ist – bei App Clips geht es darum, einen Teil Ihrer App über einen Link oder ein Tag zu starten, und nicht darum, die Assets der Haupt-App bereitzustellen.

On-Demand Resources. Apples ältere Antwort, und meiner Meinung nach wird sie stillschweigend für Fälle mit großen Akten ersetzt. Auf Background Assets richtet Apple jetzt seine Aufmerksamkeit.

Mein eigener Server. Verlockend, weil ich alles kontrollieren würde. Aber es bedeutet ein Backend, Authentifizierung, eine CDN-Rechnung, Überwachung und Bandbreite, die mit jeder Installation skaliert. Mehr Klempnerarbeiten, als der Wert rechtfertigt.

Background Assets hat mit einer Kombination gewonnen, die ich sonst nirgendwo bekommen konnte: Das System verwaltet den Download-Lebenszyklus (es überlebt App-Kills und wird nach Neustarts fortgesetzt), das CDN von Apple ist kostenlos, die Deinstallation wird nacheinander bereinigt und es gibt keine Infrastruktur pro Benutzer, die ausgeführt werden muss. Die Kosten sind eine steilere Lernkurve und eine invasivere Integration. Für Multi-GB-Modelle auf Apple Geräten im Jahr 2026 glaube ich nicht, dass irgendetwas anderes konkurriert.

Das mentale Modell, das Ihnen die Dokumente nicht geben

Die Architektur ist der Teil, der mich zum Stolpern gebracht hat, und die meisten Erklärer überspringen ihn. Hier ist das Modell, von dem ich wünschte, dass es am ersten Tag jemand für mich gezeichnet hätte.

Sie haben keinen einzigen Prozess. Du hast zwei. Ihre Haupt-App und eine kleine Erweiterung – ein separates Ziel mit eigener Bundle-ID und eigenen Berechtigungen – arbeiten zusammen, um Assets bereitzustellen. Die Erweiterung übernimmt das StoreDownloaderExtension-Protokoll von Apple, und das ist der Teil, der es dem System ermöglicht, Ihren Code aufzurufen, wenn es Zeit zum Herunterladen ist. Ihre App startet niemals direkt einen Download. Das System löst die Erweiterung aus und die Erweiterung entscheidet, ob fortgefahren wird.

Die beiden unterhalten sich über einen gemeinsamen App-Gruppen-Container. Downloads landen dort;Ihre Haupt-App liest von dort. Es hat eine Weile gedauert, bis ich es verinnerlicht habe: Der Lebenszyklus der Erweiterung gehört zum Betriebssystem, nicht zu Ihrer App. Es kann ausgeführt werden, auch wenn Ihre App noch nicht einmal geöffnet ist. Die Downloads laufen über App-Kills, über Neustarts bis hin zu Wi-Fi-Abbrüchen.

Background Assets lifecycle: main app and extension communicating through a shared app-group container; the system triggers the extension to download an asset pack containing the generator and guardian models, which the main app then reads from the shared container

Sobald es Klick machte, wurde alles andere einfacher. Der Wechsel erfolgt von „meine App lädt eine Datei herunter“ zu „das System verwaltet eine Datei und meine App abonniert ihren Status“.

Die Fallstricke, auf die Sie in der Produktion tatsächlich stoßen

1. Die Abhängigkeitsfalle – und warum ein Rudel oft zwei besiegt

Narration Room benötigt zwei Modelle, die zusammenarbeiten: den Generator, der die Erzählung schreibt, und den Wächter, der prüft, was hineingeht und was herauskommt. Mein erster Instinkt war das Aufgeräumte – zwei Asset-Pakete, eines pro Modell, jedes in einer eigenen Version.

Das war falsch, und man musste die Misserfolgsfälle durchdenken, um herauszufinden, warum. Sobald ein Benutzer ein Modell erhält, das andere jedoch nicht oder nicht übereinstimmende Versionen beider, wird der moderierte Pfad unterbrochen. Und mit zwei unabhängigen Downloads ist dieser defekte Zustand erreichbar.

Also packe ich sie zusammen – ein Asset-Paket, beide Modelle, deklariert mit mehreren fileSelectors im Manifest:

{
    "assetPackID": "app-models",
    "downloadPolicy": {
        "onDemand": {}
    },
    "fileSelectors": [
        { "directory": "Models/Generator-3B-Instruct-4bit" },
        { "directory": "Models/Guardian-0.6B-BF16" }
    ],
    "platforms": ["macOS"]
}

Der Kompromiss ist real: Um eines der beiden Modelle zu aktualisieren, müssen die gesamten 3,85 GB erneut heruntergeladen werden. Für mich ist das die richtige Entscheidung – sicherzustellen, dass beide Modelle zum Zeitpunkt der Installation vorhanden und synchron sind, ist wichtiger als unabhängige Update-Streams. Wenn Ihre beiden Modelle sehr unterschiedliche Zeitpläne haben, könnten sich separate Pakete lohnen. Aber dann schulden Sie sich selbst eine echte Geschichte für „Generator vorhanden, Wächter fehlt“, und ich würde diesen Code lieber nicht schreiben.

2. Das AAR-Archivformat (das in den Dokumenten kaum dokumentiert ist)

Asset-Pakete sind .aar Dateien – Apple Asset-Archive. Sie erstellen eines mit xcrun ba-package aus einem JSON-Manifest wie oben, und mehrere Verzeichnisse werden in einem einzigen Archiv gespeichert, das atomar extrahiert.

Die Atomizität ist der stille Sieg. Es gibt keinen halb-extrahierten Zustand, in dem man etwas vom Generator und nichts vom Wächter hat. Es ist alles auf der Festplatte, oder nichts davon.

Darin sind nur die Modelldateien enthalten, die Sie erwarten würden – .safetensors, config.json, tokenizer.json, die üblichen Tokenizer-Begleiter, manchmal eine .jinja-Chat-Vorlage. Der AAR ist nur die Hülle.

3. Die ASC-Upload- und Verarbeitungszeit ist länger als Sie denken

Dieser hat mich überrascht. Nachdem ba-package das Archiv erstellt hat, laden Sie es auf App Store Connect hoch und Apple verarbeitet es dann asynchron, bevor es heruntergeladen werden kann. Sie müssen also Asynchronität einplanen – binden Sie Ihr Veröffentlichungsdatum nicht an eine Bearbeitung am selben Tag.

Ich bin ehrlich: Ich habe nie eine genaue Bearbeitungszeit festgelegt, und das stört mich immer noch ein wenig. Es variiert und Apple veröffentlicht keine Zahl. Was ich Ihnen sagen kann, ist, früh hochzuladen und den Status abzufragen, anstatt ihn zu blockieren – die asc-CLI oder die App Store Connect-API funktionieren beide. Wenn Sie auf dem Weg zu einer Markteinführung sind, ist die Asset-Pack-Warteschlange genau das, was Sie stillschweigend einen Tag kostet.

4. App Review Ablehnungsrisiko – Richtlinie 2.1

Das ist es, was mir wirklich Sorgen bereitet hat. Die Richtlinie 2.1 (App-Vollständigkeit) von Apple verlangt, dass Apps während der Überprüfung die volle Funktionalität zeigen. Stellen Sie sich vor, wie der Prüfer auf „Generieren“ tippt und anstelle einer funktionierenden Funktion die Aufforderung „Zur Verwendung herunterladen“ erhält – das erscheint als unvollständig und kann zur Ablehnung führen.

Die Lösung besteht darin, die Asset-Pakete zur Überprüfung mit Ihrem Build einzureichen und tatsächlich zu bestätigen, dass sie in der Übermittlung angezeigt werden, anstatt nur hochgeladen zu werden. Asset-Pakete verfügen über einen eigenen Überprüfungspfad (bis zu zehn pro Einreichung) und müssen diesen löschen, bevor externe Benutzer sie abrufen können.

Was bei mir funktioniert hat:

  1. Laden Sie die Binärdatei und die Pakete in derselben Übermittlung hoch.
  2. Überprüfen Sie vor dem Absenden, ob beide unter „Zu überprüfende Elemente“ angezeigt werden.
  3. Erwägen Sie bei allem, was ein hohes Risiko darstellt, die Richtlinie des Pakets von onDemand auf prefetch umzudrehen, damit das Modell bereits vorhanden ist, wenn der Prüfer – oder der Benutzer – die Funktion zum ersten Mal öffnet. Sie zahlen im Voraus Bandbreite;Sie bekommen einen Rezensenten, der sieht, dass das Ding tatsächlich funktioniert.

Interne TestFlight-Tester können sofort ungeprüfte Packungen verwenden, was das Dogfooding schmerzlos macht. Externe Tester und App Store Benutzer warten auf die Überprüfung.

5. Die Einwilligung → Herunterladen → Aufwärmen → UX-Flow verwenden

Der erste Kaltstart einer KI-Funktion hat vier unterschiedliche Zustände, und ich habe auf die harte Tour erfahren, dass das Verwischen dieser Zustände die Leute verwirrt:

public enum ModelAssetState: Sendable {
    case notDownloaded
    case downloading(progress: Double)
    case ready(URL)
    case failed(Error)
}

Jeder erhält seine eigene Benutzeroberfläche:

  • Nicht heruntergeladen – eine explizite Einwilligung. Ich lade nicht automatisch herunter. Ich sage dem Benutzer die Größe und was er dafür bekommt.
  • Herunterladen – Fortschritt und eine Abbruchoption, und entscheidend ist, dass der Download so lange bestehen bleibt, bis der Benutzer den Bildschirm verlässt. Die Ansicht besitzt es nicht;Das System tut es.
  • Bereit – Funktion freigeschaltet.
  • Fehlgeschlagen – ein echter Fehler bei einem Wiederholungsversuch.

Es gibt einen fünften Zustand, den die meisten Artikel überspringen, und er hat mich überrascht: das Aufwärmen. Das Laden eines 2,75-GB-Modells in den GPU-Speicher dauert echte Sekunden, selbst wenn die Datei lokal ist, und die Kaltinferenzlatenz ist grob. Ich führe beim ersten Gebrauch einen Aufwärmdurchgang durch und zeige ihn als eigenen Schritt. Wenn Sie es überspringen, geben Benutzer der Funktion die Schuld, dass sie langsam ist, wenn sie eigentlich nur geladen wird.

6. Die URL-Lebensdauerbeschränkung, die niemand erwähnt

Hier ist die Sache, die mich einen halben Tag gekostet hat, und ich ärgere mich immer noch ein wenig, dass sie nicht dokumentiert ist. Wenn Sie das Framework fragen, wo sich ein heruntergeladenes Asset befindet, erhalten Sie eine URL zurück – und diese URL ist nur für den aktuellen Prozess gültig. Cachen Sie es nicht. Schreiben Sie es nicht an UserDefaults. Überlassen Sie es nicht einer Hintergrundaufgabe, die den Prozess überdauert.

Das Muster, das funktioniert: Lösen Sie es bei jedem Start neu auf und geben Sie es dann an denjenigen weiter, der es benötigt.

Diese Einschränkung hat mich zu einem Design geführt, mit dem ich wirklich zufrieden bin – und es ist derselbe Instinkt, auf den ich mich jetzt überall stütze, nämlich Bedenken frühzeitig in die eigenen Pakete einzubeziehen. Der Asset-Resolution-Code und die Inferenz-Engine befinden sich in separaten SPM-Paketen mit einer sauberen Naht dazwischen: Die Asset-Seite gibt einen URL zurück, die Laufzeitseite nimmt einen URL und die App setzt die beiden zusammen.

public protocol ModelAssetManaging: Sendable {
    func ensureAvailable(for descriptor: ModelAssetDescriptor) async throws -> URL
    func updates(for descriptor: ModelAssetDescriptor) -> AsyncStream<ModelAssetState>
}

// Runtime accepts a URL; it does not know how the URL was resolved.
public protocol ModelRuntime: Sendable {
    func warmLoad(modelDirectory: URL) async throws
    func generate(_ request: GenerationRequest) async throws -> AsyncThrowingStream<Token, Error>
}

Das ist die Art der Trennung, die man später zu schätzen weiß. An dem Tag, an dem die URL-Beschränkung greift, bleibt der Fix an genau einer Stelle.

7. Download-Kontinuität, wenn die initiierende Benutzeroberfläche verschwindet

Eine Frage, die ich schon früh hatte: Wenn der Benutzer die Einstellungen öffnet, den Download startet und die Einstellungen schließt – geht es dann weiter?

Das tut es. Der Download gehört der Erweiterung, nicht der Ansicht, die ihn gestartet hat. Das System pausiert und setzt es bei Starts und sogar Neustarts fort. Ein Benutzer kann die App vollständig beenden und am nächsten Tag zu einem abgeschlossenen Download zurückkehren. Ihre Benutzeroberfläche muss lediglich das erneute Anhängen an einen laufenden Download bewältigen, anstatt einen neuen zu starten.

Die Erweiterung selbst ist fast nichts:

import BackgroundAssets
import OSLog

@main
struct DownloaderExtension: StoreDownloaderExtension {
    private static let logger = Logger(
        subsystem: "com.example.app.BackgroundAssetDownloader",
        category: "AssetDownload"
    )

    func shouldDownload(_ assetPack: AssetPack) -> Bool {
        Self.logger.info("Evaluating download for asset pack: \(assetPack.id)")
        return true
    }
}

shouldDownload(_:) ist der Ort, an dem sich das Laufzeit-Gating befinden würde – freie Festplatte, auf Wi-Fi, ein Zustimmungsflag in gemeinsamen Standardeinstellungen. Ich gebe true zurück, da die Konversation zur Einwilligung bereits in der App stattgefunden hat, bevor der Benutzer irgendetwas davon auslösen konnte.

8. Seien Sie vorsichtig, was „auf dem Gerät“ verspricht

Ich musste mich hier beim Marketing verfangen. Es ist verlockend zu schreiben: „Kein Internet erforderlich.“Ich würde mich dagegen wehren. Der Benutzer benötigt das Netzwerk mindestens einmal, um das Modell herunterzufahren – und je nachdem, wie Ihre App ihre schwierigeren Anforderungen weiterleitet, gibt es möglicherweise Pfade, die auch später erreichbar sind. Ich sage also, was tatsächlich stimmt: Das Modell wird einmal heruntergeladen und verrichtet dann seine Arbeit auf Ihrem Mac. Ich behaupte nicht „vollständig offline“, denn für die meisten echten Apps ist das ein Satz, dem man nicht völlig folgen kann.

Eine solche Nichtübereinstimmung – „offline“ auf der Verpackung, ein Download mit mehreren GB beim ersten Start – ist genau das, was zu einer Ein-Stern-Bewertung führt. Eine präzise Kopie ist die Version, die Bestand hat.

9. Vorveröffentlichung: Asset-Pakete an TestFlight anhängen

Asset-Pakete leben getrennt von Ihrer Binärdatei in App Store Connect – sie werden eigenständig hochgeladen, eigenständig verarbeitet und haben ihren eigenen Überprüfungsstatus.

Interne TestFlight-Tester können ein Paket verwenden, sobald die Verarbeitung abgeschlossen ist – es ist kein App Review erforderlich – was interne Builds zum schnellsten Weg macht, den tatsächlichen Download-Fluss auf einem Gerät zu testen. Externe Tester und App Store Benutzer warten auf die Überprüfung. Maximal zehn Packungen pro Einreichung, was für ein oder zwei Modelle ausreichend ist.

10. Ihr IPA wächst nicht – der residente RAM hingegen schon

Ich habe schon früh immer wieder zwei Zahlen miteinander verwechselt, und es lohnt sich, sie zu trennen.„Meine Modelle haben 4 GB, also ist meine App 4 GB“ – stimmt nicht. Mit Background Assets bleibt der IPA klein;Benutzer zahlen die Festplattenkosten nur, wenn sie sich dafür entscheiden, und das Betriebssystem speichert sie außerhalb Ihrer Sandbox.

Was wächst, ist der residente RAM zur Inferenzzeit. Der Generator (4-Bit, ~2,6 GB resident) plus der Guardian (BF16 im 8K-Kontext, nördlich von 1,1 GB) erreichen einen Spitzenwert von etwa 4,9 GB. Gut auf einem 32-GB-Mac. Knapp 16 GB. Außer Reichweite auf einem 8 GB iPad. Ich gebe in der App Store-Beschreibung sowohl die Festplatten- als auch die RAM-Mindestwerte an – die Festplattennummer ist offensichtlich, die RAM-Nummer jedoch nicht, und es ist mir lieber, dass ein Benutzer sie weiß, als sie durch einen Absturz aufgrund von Speichermangel zu entdecken.

Lokale Entwicklung mit dem Mock-Server

Wenn ich von all dem nur eine praktische Sache mitteilen könnte, dann diese.

Ohne Mock führt jede Iteration einen Roundtrip von App Store Connect durch – Minuten bis Stunden pro Zyklus. Das ist nicht umsetzbar. Apple liefert xcrun ba-serve, und fast kein Tutorial erwähnt es:

xcrun ba-serve --asset-path Models/

Richten Sie Ihren Debug-Build auf den lokalen Server, iterieren Sie gegen ihn und pushen Sie ihn erst an ASC, wenn das Verhalten stimmt. Das hat Background Assets von „sich unterwerfen und beten“ zu etwas gemacht, gegen das ich mich tatsächlich entwickeln konnte.

Produktionscheckliste

Die Dinge, die ich vor dem Absenden überprüfe:

– Asset-Pakete hochgeladen und verarbeitet in App Store Connect (Status überprüfen; nicht einfach einreichen). – Asset-Pakete, die in derselben Einreichung wie die App-Binärdatei enthalten sind oder separat vorab genehmigt wurden. – App-Gruppen-Berechtigungsübereinstimmungen zwischen der Haupt-App und der Erweiterung. – Der Info.plist der Erweiterung deklariert EXExtensionPointIdentifier = com.apple.background-asset-downloader-extension. – Getestet auf einem sauberen Gerät – nicht auf dem Entwicklungscomputer mit zwischengespeicherten Assets. Auf den Kaltstart kommt es an. – Alle vier Download-Status (nicht gestartet, wird heruntergeladen, angehalten/fehlgeschlagen mit erneutem Versuch, bereit) wurden ausgeführt.

  • Der Warmup läuft und unterscheidet sich sichtbar vom Download.
  • Im Marketingtext steht „einmaliger Download“, niemals „kein Internet“.
  • Die Beschreibung von App Store gibt die Mindestfestplatte und den Mindest-RAM an.
  • Interner TestFlight-Aufbau, der mit der Packung rauchgetestet wurde.
  • Externer TestFlight-Build getestet, nachdem das Paket die Prüfung bestanden hat.

Wo die Dokumente von Apple immer noch falsch sind oder fehlen

Ich hätte das nicht geschrieben, wenn die Dokumente vollständig wären. Die Lücken, die mich am meisten gekostet haben:

  • xcrun ba-serve wird kaum erwähnt und der dadurch freigeschaltete Iterationsworkflow ist die nützlichste praktische Tatsache über das Framework.
  • Das Atomaritätsmuster der abhängigen Packung bleibt als Übung übrig. Sie lernen es, indem Sie eine Veröffentlichung brennen. – Die URL-Lebensdauerbeschränkung ist in der API-Oberfläche still und stellt eine stille Absturzquelle dar.
  • Es gibt keine veröffentlichte Schätzung für die ASC-Upload- und Verarbeitungszeit.stundenlang planen.
  • Die Auswirkungen von Richtlinie 2.1 auf noch nicht heruntergeladene Pakete sind nirgendwo dokumentiert, wo ich sie finden konnte – Sie entdecken sie zum Zeitpunkt der Überprüfung.
  • Die Lieferunterschiede zwischen macOS und iOS verdienen eine eigene Beschreibung.

Der Rahmen selbst ist gut. Die Dokumentation braucht Hilfe, und die Community braucht mehr Zuschreibungen von Leuten, die sie tatsächlich ausliefern – das ist der einzige Grund, warum ich dies aufgeschrieben habe.

Was kommt als nächstes?

Dies ist der erste Teil einer Reihe, die ich schreibe, während ich am Apple Silicon an der Bereitstellung von KI arbeite. Ich lerne immer noch, während ich arbeite, und die Arbeitsabläufe verändern sich unter meinen Füßen, also betrachten Sie diese eher als Feldnotizen denn als Evangelium. Als nächstes:

  • Prompte Budgetierung, wenn Ihr Kontextfenster 8K-Tokens umfasst – Überlauf, Chunking und die Kompromisse.
  • Mehrsprachiges TTS auf Apple Silicon – die eigentliche Pipeline-Architektur hinter Narration Room.
  • Feinabstimmung eines Absichtsklassifikators – der Datensatz, der Bewertungsrahmen und was ich anders machen würde.

Wenn das Ihr Ding ist, ist der Newsletter der beste Weg, den Rest zu erfahren.