You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Unity中如何暂停、恢复、取消OnDemandResourcesRequest?ODR续传求助

我之前在Unity iOS项目里也碰到过一模一样的ODR续传坑——Unity自带的OnDemandResourcesRequest只提供了基础的下载、取消能力,完全没考虑后台强杀后的续传场景。要解决这个问题,就得借助Objective-C的NSProgress类来实现暂停、恢复和进度追踪,通过Unity与OC的桥接层来打通能力。下面是我验证过的完整解决方案:

问题根源拆解

Unity对苹果ODR的封装(OnDemandResources类)是基于NSBundleResourceRequest,但C#层没有暴露NSProgress的管控API。当应用被后台强杀时,原有的请求实例直接销毁,重启后无法关联到未完成的系统级下载任务,导致进度卡住。而NSProgress本身支持暂停/恢复,且系统会保留未完成的下载任务状态,只要重新关联就能续传。

完整实现步骤

1. 编写Objective-C原生插件

先创建OC类封装ODR的管控逻辑,暴露方法给Unity调用:

ODRManager.h

#import <Foundation/Foundation.h>

@interface ODRManager : NSObject

+ (instancetype)sharedInstance;
- (void)startDownloadWithTags:(NSArray<NSString*>*)tags completion:(void(^)(BOOL success, NSError* error))completion;
- (void)pauseDownload;
- (void)resumeDownload;
- (void)cancelDownload;
- (float)getDownloadProgress;

@end

ODRManager.m

#import "ODRManager.h"
#import <Unity/UnityInterface.h>

@interface ODRManager ()
@property (nonatomic, strong) NSBundleResourceRequest *resourceRequest;
@property (nonatomic, strong) NSProgress *downloadProgress;
@end

@implementation ODRManager

+ (instancetype)sharedInstance {
    static ODRManager *instance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[ODRManager alloc] init];
    });
    return instance;
}

- (void)startDownloadWithTags:(NSArray<NSString*>*)tags completion:(void(^)(BOOL success, NSError* error))completion {
    // 优先恢复未完成的请求
    if (_resourceRequest && _resourceRequest.state != NSBundleResourceRequestStateFinished) {
        [self resumeDownload];
        return;
    }
    
    _resourceRequest = [[NSBundleResourceRequest alloc] initWithTags:tags];
    _downloadProgress = _resourceRequest.progress;
    
    // KVO监听进度变化,回调到Unity
    [_downloadProgress addObserver:self forKeyPath:@"fractionCompleted" options:NSKeyValueObservingOptionNew context:nil];
    
    __weak typeof(self) weakSelf = self;
    [_resourceRequest beginAccessingResourcesWithCompletionHandler:^(NSError * _Nullable error) {
        if (error) {
            completion(NO, error);
            [weakSelf cleanupObserver];
        } else {
            completion(YES, nil);
            [weakSelf cleanupObserver];
        }
    }];
}

- (void)pauseDownload {
    if (_resourceRequest && _resourceRequest.state == NSBundleResourceRequestStateLoading) {
        _downloadProgress.paused = YES;
    }
}

- (void)resumeDownload {
    if (_resourceRequest && _resourceRequest.state == NSBundleResourceRequestStateLoading) {
        _downloadProgress.paused = NO;
    }
}

- (void)cancelDownload {
    if (_resourceRequest) {
        [_resourceRequest cancel];
        [self cleanupObserver];
        _resourceRequest = nil;
        _downloadProgress = nil;
    }
}

- (float)getDownloadProgress {
    return _downloadProgress ? _downloadProgress.fractionCompleted : 0.0f;
}

- (void)cleanupObserver {
    if (_downloadProgress) {
        [_downloadProgress removeObserver:self forKeyPath:@"fractionCompleted"];
    }
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"fractionCompleted"] && object == _downloadProgress) {
        float progress = [change[NSKeyValueChangeNewKey] floatValue];
        // 回调到Unity的ODRController脚本
        UnitySendMessage("ODRController", "OnODRProgressUpdate", [NSString stringWithFormat:@"%.2f", progress].UTF8String);
    }
}

@end

2. Unity C#层调用与管控脚本

创建一个单例脚本ODRController,负责调用OC插件、处理前后台切换和续传逻辑:

using UnityEngine;
using System.Runtime.InteropServices;

public class ODRController : MonoBehaviour
{
    // 导入OC插件方法
    [DllImport("__Internal")]
    private static extern void _ODRStartDownload(string[] tags, int tagCount);
    
    [DllImport("__Internal")]
    private static extern void _ODRPauseDownload();
    
    [DllImport("__Internal")]
    private static extern void _ODRResumeDownload();
    
    [DllImport("__Internal")]
    private static extern void _ODRCancelDownload();
    
    [DllImport("__Internal")]
    private static extern float _ODRGetProgress();

    public static ODRController Instance;

    private void Awake()
    {
        Instance = this;
        DontDestroyOnLoad(gameObject);
    }

    /// <summary>
    /// 启动ODR下载(自动续传未完成任务)
    /// </summary>
    public void StartODRDownload(string[] tags)
    {
#if UNITY_IOS && !UNITY_EDITOR
        SaveDownloadTags(tags);
        _ODRStartDownload(tags, tags.Length);
#endif
    }

    public void PauseODR()
    {
#if UNITY_IOS && !UNITY_EDITOR
        _ODRPauseDownload();
#endif
    }

    public void ResumeODR()
    {
#if UNITY_IOS && !UNITY_EDITOR
        _ODRResumeDownload();
#endif
    }

    public void CancelODR()
    {
#if UNITY_IOS && !UNITY_EDITOR
        _ODRCancelDownload();
        PlayerPrefs.DeleteKey("ODR_SavedTags");
#endif
    }

    public float GetODRProgress()
    {
#if UNITY_IOS && !UNITY_EDITOR
        return _ODRGetProgress();
#else
        return 0f;
#endif
    }

    /// <summary>
    /// OC回调的进度更新方法(方法名必须和UnitySendMessage一致)
    /// </summary>
    public void OnODRProgressUpdate(string progressStr)
    {
        if (float.TryParse(progressStr, out float progress))
        {
            Debug.Log($"ODR Download Progress: {progress:P2}");
            // 这里更新UI进度条等逻辑
        }
    }

    /// <summary>
    /// 处理应用前后台切换
    /// </summary>
    private void OnApplicationPause(bool pauseStatus)
    {
        if (pauseStatus)
        {
            // 后台主动暂停,避免被系统强杀导致任务丢失
            PauseODR();
        }
        else
        {
            // 前台恢复下载
            ResumeODR();
        }
    }

    /// <summary>
    /// 应用重启后自动续传
    /// </summary>
    private void Start()
    {
        string[] savedTags = LoadSavedTags();
        if (savedTags != null && savedTags.Length > 0)
        {
            StartODRDownload(savedTags);
        }
    }

    // 保存下载标签到本地,用于重启续传
    private void SaveDownloadTags(string[] tags)
    {
        PlayerPrefs.SetString("ODR_SavedTags", string.Join(",", tags));
        PlayerPrefs.Save();
    }

    private string[] LoadSavedTags()
    {
        return PlayerPrefs.HasKey("ODR_SavedTags") 
            ? PlayerPrefs.GetString("ODR_SavedTags").Split(',') 
            : null;
    }
}

核心管控逻辑说明

  • 暂停/恢复:通过NSProgress的paused属性控制,应用进入后台时主动暂停,回到前台恢复,避免后台被系统终止导致任务状态丢失。
  • 续传实现:重启应用时,读取本地存储的下载标签重新发起请求,OC层会自动关联系统中未完成的下载任务,实现续传。
  • 进度追踪:用KVO监听NSProgress的进度变化,通过UnitySendMessage回调到C#层,也可以主动调用GetODRProgress查询实时进度。

测试注意事项

  • 确保Xcode项目中已正确配置ODR标签和资源分组,TestFlight包开启ODR功能。
  • 测试后台强杀场景时,不要用Xcode终止进程,要在iPhone上手动划掉应用后重新打开。
  • 注意清理KVO监听,避免内存泄漏,OC层在下载完成或取消时已处理。

内容的提问来源于stack exchange,提问作者Jitendra Gochhayat

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.25 06:45:14