appmigrationkit

appmigrationkit

热门

使用 AppMigrationKit 实现应用数据的跨平台迁移(导出或导入)。适用于实现由系统协同调度的 iOS 与 Android 等平台间单次数据迁移、开发 AppMigrationExtension 扩展、使用 ResourcesArchiver 打包待传输资源、在目标设备上导入资源、上报导入进度、处理迁移错误与 App Group 清理、查询 MigrationStatus 状态,以及通过 AppMigrationTester 测试迁移代码等场景。

967Star
49Fork
更新于 2026/7/31
SKILL.md
只读
名称
appmigrationkit
描述

使用 AppMigrationKit 实现应用数据的跨平台迁移(导出或导入)。适用于实现由系统协同调度的 iOS 与 Android 等平台间单次数据迁移、开发 AppMigrationExtension 扩展、使用 ResourcesArchiver 打包待传输资源、在目标设备上导入资源、上报导入进度、处理迁移错误与 App Group 清理、查询 MigrationStatus 状态,以及通过 AppMigrationTester 测试迁移代码等场景。

AppMigrationKit

用于应用资源的单次跨平台数据传输。支持应用在设备初始设置或首次引导(onboarding)过程中,与其它平台(例如 Android)之间互相导出或导入数据。AppMigrationKit API 适用于 iOS 26.0+ / iPadOS 26.0+;数据容器 Entitlement(权限)适用于 iOS 26.1+ / iPadOS 26.1+ / Mac Catalyst 26.1+。基于 Swift 6.3。

Beta 特性提醒: AppMigrationKit 是 iOS 26 的新增框架,在 GM(正式发布)版本前可能会有变动。在依赖具体 API 细节前,请务必核对 Apple 最新官方文档。

AppMigrationKit 采用 App Extension(应用扩展)架构模式,由系统统一协调和调度设备间的数据传输。应用只需提供一个实现导出/导入协议的 Extension,系统会在合适的时间自动调用该 Extension。应用本身无需也不直接管理设备之间的网络连接。

Contents

Architecture Overview

AppMigrationKit 主要由三个层级组成:

  1. App extension(应用扩展) -- 遵循 AppMigrationExtension 协议的具体类型,在迁移过程中由系统调用,负责具体的数据导出与导入逻辑。
  2. 系统调度(System orchestration) -- 操作系统负责管理设备到设备之间的会话、网络传输以及运行调度。Extension 无法自主控制何时被启动执行。
  3. 宿主应用(Containing app) -- 迁移完成后,宿主应用在首次启动时检查 MigrationStatus.importStatus,用以确认迁移是否发生以及是否成功。

核心类型一览:

类型 职责说明
AppMigrationExtension App Extension 入口点协议
ResourcesExportingWithOptions 支持自定义选项的资源导出协议(通过 archiver 打包)
ResourcesExporting 简化的资源导出协议(无自定义选项)
ResourcesImporting 目标设备上的资源导入协议
ResourcesArchiver 流式写入文件到导出归档包中
MigrationDataContainer 访问宿主应用的数据目录
MigrationStatus 宿主应用用于检查导入结果状态
MigrationPlatform 标识另一台设备的平台类型(例如 .android
MigrationAppIdentifier 通过 App Store 和 Bundle ID 标识来源应用
AppMigrationTester 仅用于测试的 Actor,可验证导出/导入逻辑

Setup and Entitlements

Entitlement 权限设置

App Extension 需要配置 com.apple.developer.app-migration.data-container-access entitlement。其值为一个仅包含单元素的字符串数组,填入宿主应用的 Bundle Identifier:

<key>com.apple.developer.app-migration.data-container-access</key>
<array>
    <string>com.example.myapp</string>
</array>

填入其它任何值均无效。此 entitlement 授予 Extension 在导出阶段对宿主应用数据容器的读取权限,以及在导入阶段的写入权限。需要注意的是,尽管 AppMigrationKit 的核心 API 适用于 iOS 26.0+ 和 iPadOS 26.0+,但该 entitlement 本身要求 iOS 26.1+、iPadOS 26.1+ 和 Mac Catalyst 26.1+。

Extension Target 配置

在 Xcode 项目中新增一个 App Extension Target。该 Extension 需要实现一个或多个迁移协议(ResourcesExportingWithOptionsResourcesExportingResourcesImporting)。

App Migration Extension

Extension 的入口点需遵守 AppMigrationExtension 协议。迁移进行期间,系统会阻止启动宿主应用及其其它 Extension,以确保数据访问的独占性。

访问数据容器

Extension 通过 appContainer 访问宿主应用的文件系统:

import AppMigrationKit

struct MyMigrationExtension: ResourcesExporting {
    var resourcesSizeEstimate: Int { estimateTotalExportSize() }
    var resourcesVersion: String { "1.0" }
    var resourcesCompressible: Bool { true }

    func exportResources(
        to archiver: sending ResourcesArchiver,
        request: MigrationRequest
    ) async throws {
        let container = appContainer

        // container.bundleIdentifier     -- 应用的 Bundle ID
        // container.containerRootDirectory -- 应用容器根目录
        // container.documentsDirectory    -- Documents/ 目录
        // container.applicationSupportDirectory -- Application Support/ 目录
    }
}

MigrationDataContainer 提供了 containerRootDirectorydocumentsDirectoryapplicationSupportDirectoryURL 属性,直接指向宿主应用的沙盒路径。

Exporting Resources

通过遵守 ResourcesExportingWithOptions 协议(无自定义选项时遵守 ResourcesExporting 协议)将待传输的文件进行打包。系统会传入 ResourcesArchiverMigrationRequestWithOptions 并调用 exportResources(to:request:)

声明导出属性

struct MyMigrationExtension: ResourcesExportingWithOptions {
    typealias OptionsType = MigrationDefaultSupportedOptions

    var resourcesSizeEstimate: Int {
        // 返回预计导出的总字节数
        calculateExportSize()
    }

    var resourcesVersion: String {
        "1.0"
    }

    var resourcesCompressible: Bool {
        true  // 允许系统在传输过程中压缩文件
    }
}
  • resourcesSizeEstimate -- 预估的总字节数(Byte)。系统以此在 UI 上展示进度并进行磁盘剩余空间检查。
  • resourcesVersion -- 导出数据格式的版本标识字符串。目标导入端会收到此版本号,以便按版本处理数据兼容。
  • resourcesCompressible -- 设为 true 时,传输归档器可在传输网络数据时自动压缩文件。

实现导出逻辑

func exportResources(
    to archiver: sending ResourcesArchiver,
    request: MigrationRequestWithOptions<MigrationDefaultSupportedOptions>
) async throws {
    let docsDir = appContainer.documentsDirectory

    // 根据需要检查目标设备平台
    if request.destinationPlatform == .android {
        // 针对特定平台的导出逻辑
    }

    // 逐个追加文件 -- 保持持续的进度更新
    let userDataURL = docsDir.appending(path: "user_data.json")
    try await archiver.appendItem(at: userDataURL)

    // 使用自定义的归档内路径追加文件
    let settingsURL = docsDir.appending(path: "settings.plist")
    try await archiver.appendItem(at: settingsURL, pathInArchive: "preferences/settings.plist")

    // 追加整个目录
    let photosDir = docsDir.appending(path: "photos")
    try await archiver.appendItem(at: photosDir, pathInArchive: "media/photos")
}

归档器使用流式方式增量处理文件。当每个资源准备好后,连续调用 appendItem(at:pathInArchive:)。系统会在判定 Extension 假死/无响应时将其强制终止,因此务必避免在两次追加调用之间产生过长的等待间隔。

取消操作处理

ResourcesArchiver 会通过抛出取消错误(cancellation errors)来自动处理任务取消。切勿捕获这些取消错误 -- 捕获会导致系统强行杀掉 Extension。

迁移平台(Migration Platform)

MigrationRequestWithOptions 暴露了 destinationPlatform 属性,类型为 MigrationPlatform。可据此针对不同目标平台量身定制导出格式:

if request.destinationPlatform == .android {
    // 导出为 Android 端应用期望的数据格式
}

MigrationPlatform 预设了 .android 静态常量。自定义平台可以通过 MigrationPlatform("customPlatform") 进行构造。

Importing Resources

目标设备通过遵守 ResourcesImporting 协议来接收传输过来的文件。系统会在应用安装完成之后、但允许用户启动应用之前调用 importResources(at:request:)

struct MyMigrationExtension: ResourcesImporting {
    func importResources(
        at importedDataURL: URL,
        request: ResourcesImportRequest
    ) async throws {
        let sourceVersion = request.sourceVersion
        let sourceApp = request.sourceAppIdentifier

        // sourceApp.platform        -- 例如 .android
        // sourceApp.bundleIdentifier -- 来源应用的 Bundle ID
        // sourceApp.storeIdentifier  -- 例如 .googlePlay

        // 将导入的文件复制到宿主应用容器中
        let docsDir = appContainer.documentsDirectory

        let userData = importedDataURL.appending(path: "user_data.json")
        if FileManager.default.fileExists(atPath: userData.path()) {
            try FileManager.default.copyItem(
                at: userData,
                to: docsDir.appending(path: "user_data.json")
            )
        }
    }
}

导入期间的错误处理

若导入过程中发生错误,系统将清空宿主应用的数据容器,以防止留有不完整的数据脏状态。然而,App Group(应用组)容器不会被系统自动清空。因此,导入实现逻辑在写入新内容前,应主动清除已有的 App Group 容器数据:

func importResources(
    at importedDataURL: URL,
    request: ResourcesImportRequest
) async throws {
    // 首先清理共享的 App Group 数据
    let groupURL = FileManager.default.containerURL(
        forSecurityApplicationGroupIdentifier: "group.com.example.myapp"
    )
    if let groupURL {
        try? FileManager.default.removeItem(at: groupURL.appending(path: "shared_data"))
    }

    // 然后执行导入
    try await performImport(from: importedDataURL)
}

来源应用标识符(Source App Identifier)

ResourcesImportRequest 提供了 sourceAppIdentifier 属性,其类型为 MigrationAppIdentifier,包含三个核心字段:

  • platform -- 来源设备的平台(例如 .android
  • bundleIdentifier -- 来源应用的 Bundle ID
  • storeIdentifier -- 应用商店标识(例如 .googlePlay

Migration Status

迁移完成后,宿主应用在首次启动时检查迁移结果:

import AppMigrationKit

func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
    if let status = MigrationStatus.importStatus {
        switch status {
        case .success:
            showMigrationSuccessUI()
            MigrationStatus.clearImportStatus()
        case .failure(let error):
            showMigrationFailureUI(error: error)
            MigrationStatus.clearImportStatus()
        }
    }
    return true
}
  • 如果未发生过迁移,MigrationStatus.importStatusnil
  • 处理完迁移结果后,务必调用 clearImportStatus(),避免后续每次打开应用时重复触发提示。
  • 该 Enum 包含两个枚举值:.success.failure(any Error)

Progress Tracking

导入端通过 resourcesImportProgress 暴露一个 Progress 对象。系统利用该对象向用户展示传输与导入进度。在导入过程中累进更新 completedUnitCount

struct MyMigrationExtension: ResourcesImporting {
    private let importProgress = Progress(totalUnitCount: 100)

    var resourcesImportProgress: Progress { importProgress }

    func importResources(
        at importedDataURL: URL,
        request: ResourcesImportRequest
    ) async throws {
        let files = try FileManager.default.contentsOfDirectory(
            at: importedDataURL, includingPropertiesForKeys: nil
        )
        let increment = Int64(100 / max(files.count, 1))
        for file in files {
            try processFile(file)
            importProgress.completedUnitCount += increment
        }
        importProgres

<!-- truncated for translation batch; full body continues in source -->