Back to blog
    GGUF 模型分发:应用商店打包 vs 安装后下载
    GGUFdeploymentApp Storemodel deliverymobile AIsegment:mobile-builder

    GGUF 模型分发:应用商店打包 vs 安装后下载

    两种将 GGUF 模型送达用户设备的方式。与应用打包以获得简单性,或安装后下载以获得灵活性。两种方案的架构、大小限制和最佳实践。

    EErtas Team·

    您的模型已经微调并导出为 GGUF。现在需要将它送达用户的设备。有两种基本方式:与应用二进制文件打包,或安装后下载。

    每种方式在应用大小、用户体验、更新灵活性和平台限制方面都有不同的取舍。

    选项 1:与应用打包

    将 GGUF 文件包含在应用安装包中。用户下载应用时同时下载模型。

    iOS 打包

    直接包含: 将 GGUF 文件添加到 Xcode 项目中。它随 IPA 一起分发。通过 Bundle.main 访问:

    let modelPath = Bundle.main.path(forResource: "model", ofType: "gguf")!

    On Demand Resources(ODR): 将模型标记为按需资源。iOS 在首次需要时下载,而非安装时。初始应用下载保持较小。

    let request = NSBundleResourceRequest(tags: ["ai-model"])
    request.beginAccessingResources { error in
        guard error == nil else { return }
        let modelPath = Bundle.main.path(forResource: "model", ofType: "gguf")!
        loadModel(at: modelPath)
    }

    ODR 文件由 iOS 管理,在存储空间紧张时可能被清除。您的应用必须处理重新下载。

    Android 打包

    APK assets: 对于 150MB 以内的模型,将 GGUF 放在 assets/ 中。首次启动时复制到内部存储供 llama.cpp 访问。

    Play Asset Delivery: 对于更大的模型,使用 Google Play 的资产分发系统:

    // 安装时分发(随应用下载)
    // build.gradle.kts
    assetPacks += ":model_pack"

    Play Asset Delivery 支持三种模式:

    • Install-time: 随应用下载。最简单但增加初始下载大小。
    • Fast-follow: 安装后立即在后台下载。
    • On-demand: 应用请求时才下载。

    大小限制

    平台限制说明
    iOS IPA4GB包含所有资源
    iOS OTA 下载200MB蜂窝网络下载限制(用户可覆盖)
    Android APK150MB不使用 Play Asset Delivery 的情况
    Android AAB150MB 基础 + 2GB 资产使用 Play Asset Delivery
    Play Asset Delivery 包每包 512MB允许多个包

    1B GGUF Q4 模型(约 600MB)在 iOS 的 4GB 限制内,但超过了 200MB 蜂窝 OTA 阈值。在 Android 上需要 Play Asset Delivery。

    3B GGUF Q4 模型(约 1.7GB)在两个平台的上限内,但会是一个较大的下载。

    打包的优缺点

    优点:

    • 首次启动时模型立即可用(无需等待下载)
    • 不需要 CDN 基础设施
    • 首次使用不需要网络连接
    • 更简单的架构(无需下载/验证/恢复逻辑)

    缺点:

    • 显著增加应用下载大小
    • 模型更新需要通过商店进行完整的应用更新
    • 每次模型更改都需要应用商店审核
    • 用户可能不愿下载 600MB-1.7GB 的应用
    • 在 iOS 上,200MB 蜂窝限制意味着用户可能需要 WiFi 来下载

    选项 2:安装后下载

    应用安装时不包含模型。首次启动时(或用户访问 AI 功能时),应用从您的 CDN 下载模型。

    下载流程

    [应用已安装] -> [用户打开 AI 功能] -> [未找到模型]
      -> [显示下载提示:"下载 AI 模型(1.7GB)?"]
      -> [用户点击下载] -> [进度条]
      -> [下载完成] -> [验证哈希] -> [模型就绪]
    

    iOS 实现

    class ModelDownloader: ObservableObject {
        @Published var progress: Double = 0
        @Published var isDownloading = false
        @Published var isReady = false
    
        private let modelURL = URL(string: "https://cdn.example.com/model.gguf")!
        private var modelPath: URL {
            FileManager.default
                .urls(for: .documentDirectory, in: .userDomainMask)[0]
                .appendingPathComponent("model.gguf")
        }
    
        func checkModelAvailable() -> Bool {
            FileManager.default.fileExists(atPath: modelPath.path)
        }
    
        func downloadModel() async throws {
            isDownloading = true
    
            let (tempURL, response) = try await URLSession.shared.download(
                from: modelURL,
                delegate: ProgressDelegate { progress in
                    Task { @MainActor in self.progress = progress }
                }
            )
    
            try FileManager.default.moveItem(at: tempURL, to: modelPath)
    
            // 验证完整性
            guard verifyHash(modelPath, expected: expectedSHA256) else {
                try FileManager.default.removeItem(at: modelPath)
                throw ModelError.corruptedDownload
            }
    
            isDownloading = false
            isReady = true
        }
    }

    Android 实现

    class ModelDownloader(private val context: Context) {
        private val modelFile = File(context.filesDir, "model.gguf")
    
        fun isModelAvailable(): Boolean = modelFile.exists()
    
        suspend fun downloadModel(
            onProgress: (Float) -> Unit
        ) = withContext(Dispatchers.IO) {
            val client = OkHttpClient()
            val request = Request.Builder().url(MODEL_CDN_URL).build()
            val response = client.newCall(request).execute()
    
            val body = response.body ?: throw IOException("Empty response")
            val totalBytes = body.contentLength()
            var downloadedBytes = 0L
    
            modelFile.outputStream().use { output ->
                body.byteStream().use { input ->
                    val buffer = ByteArray(8192)
                    var read: Int
                    while (input.read(buffer).also { read = it } != -1) {
                        output.write(buffer, 0, read)
                        downloadedBytes += read
                        onProgress(downloadedBytes.toFloat() / totalBytes)
                    }
                }
            }
    
            // 验证完整性
            val hash = modelFile.sha256()
            if (hash != EXPECTED_SHA256) {
                modelFile.delete()
                throw IOException("Corrupted download")
            }
        }
    }

    CDN 设置

    将 GGUF 文件托管在 CDN 上以实现快速、可靠的分发:

    • AWS CloudFront + S3: 标准方案。约 $0.085/GB 传输费用。
    • Cloudflare R2: 无出站费用。仅约 $0.015/GB 存储费用。
    • Firebase Hosting: 适合小项目。10GB 免费,之后 $0.15/GB。

    以每月 10,000 次下载 1.7GB 模型为例的成本:

    • CloudFront:约 $1,445/月
    • Cloudflare R2:约 $0.26/月(仅存储费,无出站费)
    • Firebase:约 $2,550/月

    Cloudflare R2 的零出站费定价使其在模型分发方面成本大幅降低。

    断点续传支持

    大文件下载会被中断。支持断点续传:

    // iOS:恢复中断的下载
    let resumeData = try? Data(contentsOf: resumeDataURL)
    if let resumeData = resumeData {
        downloadTask = session.downloadTask(withResumeData: resumeData)
    } else {
        downloadTask = session.downloadTask(with: modelURL)
    }

    安装后下载的优缺点

    优点:

    • 初始应用下载小(安装快,不因商店大小而犹豫)
    • 模型更新无需应用商店审核(将新模型推送到 CDN)
    • 可提供多种模型大小(1B 给所有人,3B 作为升级选项)
    • 用户只在使用 AI 功能时才下载

    缺点:

    • 首次使用有延迟(下载需要 1-5 分钟)
    • 首次使用需要网络
    • CDN 基础设施和成本
    • 更复杂的代码(下载、验证、断点续传、存储管理)

    建议方案

    场景方案
    1B 模型,AI 是应用核心打包(600MB 可接受)
    3B 模型,AI 是应用核心Fast-follow / On-demand 分发
    AI 是可选功能安装后下载
    模型频繁更新(每月)安装后下载
    模型稳定(每季度更新)打包或 fast-follow
    目标市场网络慢打包

    对于大多数应用:安装后下载并提供清晰的下载提示。 这样可以保持初始应用下载小,让您独立更新模型,并且只为实际使用 AI 功能的用户下载。

    完整性验证

    始终在下载后验证模型文件。损坏的 GGUF 文件会在推理过程中导致崩溃:

    func verifyHash(_ fileURL: URL, expected: String) -> Bool {
        guard let data = try? Data(contentsOf: fileURL) else { return false }
        let hash = SHA256.hash(data: data)
        let hashString = hash.compactMap { String(format: "%02x", $0) }.joined()
        return hashString == expected
    }

    存储管理

    GGUF 模型文件很大。尊重用户的存储空间:

    • 下载前显示模型大小
    • 允许删除并重新下载模型
    • 在 iOS 上,将模型排除在 iCloud 备份之外(可以重新下载)
    • 优雅处理低存储空间场景

    模型本身才是关键。通过 Ertas 等平台生成的高质量微调 GGUF,无论通过打包还是下载分发,都能提供在本地即时运行、零使用成本的领域特定 AI。

    Ship AI that runs on your users' devices.

    Early bird pricing starts at $14.50/mo — locked in for life. Plans for builders and agencies.

    Keep reading