Возобновить NSUrlSession на iOS10
Скоро выйдет iOS 10, поэтому стоит протестировать приложения на совместимость с ней. Во время такого теста мы обнаружили, что наше приложение не может возобновить фоновые загрузки на iOS10. Код, который хорошо работал в предыдущих версиях, не работает в новой, как на эмуляторе, так и на устройстве.
Вместо того чтобы свести наш код к минимальному рабочему тестовому примеру, я искал в Интернете учебники NSUrlSession и протестировал их. Поведение то же самое: возобновление работает на предыдущих версиях iOS, но перерыв на 10-е.
Действия по воспроизведению:
- Скачать форму проекта NSUrlSession учебник https://www.raywenderlich.com/110458/nsurlsession-tutorial-getting-started
- Прямая ссылка: http://www.raywenderlich.com/wp-content/uploads/2016/01/HalfTunes-Final.zip
- Постройте его и запустите под iOS 10. Найдите что-нибудь, например, "swift". Начните загрузку, затем нажмите "Пауза", а затем "Возобновить".
Ожидаемые результаты:
Загрузка возобновлена. Вы можете проверить, как это работает с версиями до iOS10.
Фактические результаты:
Загрузка не удалась. В консоли XCode вы можете увидеть:
2016-09-02 16:11:24.913 HalfTunes[35205:2279228] *** -[NSKeyedUnarchiver initForReadingWithData:]: data is NULL
2016-09-02 16:11:24.913 HalfTunes[35205:2279228] *** -[NSKeyedUnarchiver initForReadingWithData:]: data is NULL
2016-09-02 16:11:24.913 HalfTunes[35205:2279228] Invalid resume data for background download. Background downloads must use http or https and must download to an accessible file.
Больше сценариев:
Если вы активируете автономный режим во время загрузки файла, вы получите
Url session completed with error: Error Domain=NSURLErrorDomain Code=-1002 "unsupported URL" UserInfo={NSLocalizedDescription=unsupported URL} {
NSLocalizedDescription = "unsupported URL";
}
когда сеть закрыта и загрузка никогда не восстанавливается, когда сеть снова работает. Другие случаи использования с паузой, такие как перезагрузка, также не работают.
Дополнительное расследование:
Я попытался проверить, верны ли возвращенные значения resumeData, используя код, предложенный в
но целевой файл на месте. Хотя формат resumeData изменился, и теперь имя файла сохраняется в NSURLSessionResumeInfoTempFileName, и вам необходимо добавить к нему NSTevenDirectory().
Кроме того, я заполнил отчет об ошибках в Apple, но они еще не ответили.
Вопрос (о жизни, вселенной и обо всем):
Возобновление NSUrlSession не работает во всех других приложениях? Это можно исправить на стороне приложения?
3 ответа
Эта проблема возникла из currentRequest и originalRequest NSKeyArchived, закодированного с необычным корнем "NSKeyedArchiveRootObjectKey" вместо константы NSKeyedArchiveRootObjectKey, которая буквально является "корнем", и некоторых других ошибок в процессе кодирования запроса NSURL(Mutable).
Я обнаружил это в бета-версии 1 и подал ошибку (№ 27144153 на случай, если вы захотите дублировать). Даже я отправил электронное письмо "Эскимосу Куинну" (eskimo1 в apple dot com), который является сотрудником службы поддержки NSURLSession, чтобы подтвердить, что они получили его, и он сказал, что получил и знает о проблеме.
ОБНОВЛЕНИЕ: я наконец понял, как решить эту проблему. Передайте данные функции correctResumeData(), и она вернет пригодные данные возобновления
ОБНОВЛЕНИЕ 2: Вы можете использовать NSURLSession.correctedDownloadTaskWithResumeData() / URLSession.correctedDownloadTask(withResumeData:) для получения задачи с правильными переменными originalRequest и currentRequest
ОБНОВЛЕНИЕ 3: Куинн говорит, что эта проблема решена в iOS 10.2, вы можете продолжать использовать этот код для совместимости с iOS 10.0 и 10.1, и он будет работать с новой версией без каких-либо проблем.
(Для кода Swift 3, прокрутите ниже, для Objective C см. Пост листьев, но я его не проверял)
Swift 2.3:
func correctRequestData(data: NSData?) -> NSData? {
guard let data = data else {
return nil
}
// return the same data if it's correct
if NSKeyedUnarchiver.unarchiveObjectWithData(data) != nil {
return data
}
guard let archive = (try? NSPropertyListSerialization.propertyListWithData(data, options: [.MutableContainersAndLeaves], format: nil)) as? NSMutableDictionary else {
return nil
}
// Rectify weird __nsurlrequest_proto_props objects to $number pattern
var k = 0
while archive["$objects"]?[1].objectForKey("$\(k)") != nil {
k += 1
}
var i = 0
while archive["$objects"]?[1].objectForKey("__nsurlrequest_proto_prop_obj_\(i)") != nil {
let arr = archive["$objects"] as? NSMutableArray
if let dic = arr?[1] as? NSMutableDictionary, let obj = dic["__nsurlrequest_proto_prop_obj_\(i)"] {
dic.setObject(obj, forKey: "$\(i + k)")
dic.removeObjectForKey("__nsurlrequest_proto_prop_obj_\(i)")
arr?[1] = dic
archive["$objects"] = arr
}
i += 1
}
if archive["$objects"]?[1].objectForKey("__nsurlrequest_proto_props") != nil {
let arr = archive["$objects"] as? NSMutableArray
if let dic = arr?[1] as? NSMutableDictionary, let obj = dic["__nsurlrequest_proto_props"] {
dic.setObject(obj, forKey: "$\(i + k)")
dic.removeObjectForKey("__nsurlrequest_proto_props")
arr?[1] = dic
archive["$objects"] = arr
}
}
// Rectify weird "NSKeyedArchiveRootObjectKey" top key to NSKeyedArchiveRootObjectKey = "root"
if archive["$top"]?.objectForKey("NSKeyedArchiveRootObjectKey") != nil {
archive["$top"]?.setObject(archive["$top"]?["NSKeyedArchiveRootObjectKey"], forKey: NSKeyedArchiveRootObjectKey)
archive["$top"]?.removeObjectForKey("NSKeyedArchiveRootObjectKey")
}
// Reencode archived object
let result = try? NSPropertyListSerialization.dataWithPropertyList(archive, format: NSPropertyListFormat.BinaryFormat_v1_0, options: NSPropertyListWriteOptions())
return result
}
func getResumeDictionary(data: NSData) -> NSMutableDictionary? {
var iresumeDictionary: NSMutableDictionary? = nil
// In beta versions, resumeData is NSKeyedArchive encoded instead of plist
if #available(iOS 10.0, OSX 10.12, *) {
var root : AnyObject? = nil
let keyedUnarchiver = NSKeyedUnarchiver(forReadingWithData: data)
do {
root = try keyedUnarchiver.decodeTopLevelObjectForKey("NSKeyedArchiveRootObjectKey") ?? nil
if root == nil {
root = try keyedUnarchiver.decodeTopLevelObjectForKey(NSKeyedArchiveRootObjectKey)
}
} catch {}
keyedUnarchiver.finishDecoding()
iresumeDictionary = root as? NSMutableDictionary
}
if iresumeDictionary == nil {
do {
iresumeDictionary = try NSPropertyListSerialization.propertyListWithData(data, options: [.MutableContainersAndLeaves], format: nil) as? NSMutableDictionary;
} catch {}
}
return iresumeDictionary
}
func correctResumeData(data: NSData?) -> NSData? {
let kResumeCurrentRequest = "NSURLSessionResumeCurrentRequest"
let kResumeOriginalRequest = "NSURLSessionResumeOriginalRequest"
guard let data = data, let resumeDictionary = getResumeDictionary(data) else {
return nil
}
resumeDictionary[kResumeCurrentRequest] = correctRequestData(resumeDictionary[kResumeCurrentRequest] as? NSData)
resumeDictionary[kResumeOriginalRequest] = correctRequestData(resumeDictionary[kResumeOriginalRequest] as? NSData)
let result = try? NSPropertyListSerialization.dataWithPropertyList(resumeDictionary, format: NSPropertyListFormat.XMLFormat_v1_0, options: NSPropertyListWriteOptions())
return result
}
extension NSURLSession {
func correctedDownloadTaskWithResumeData(resumeData: NSData) -> NSURLSessionDownloadTask {
let kResumeCurrentRequest = "NSURLSessionResumeCurrentRequest"
let kResumeOriginalRequest = "NSURLSessionResumeOriginalRequest"
let cData = correctResumeData(resumeData) ?? resumeData
let task = self.downloadTaskWithResumeData(cData)
// a compensation for inability to set task requests in CFNetwork.
// While you still get -[NSKeyedUnarchiver initForReadingWithData:]: data is NULL error,
// this section will set them to real objects
if let resumeDic = getResumeDictionary(cData) {
if task.originalRequest == nil, let originalReqData = resumeDic[kResumeOriginalRequest] as? NSData, let originalRequest = NSKeyedUnarchiver.unarchiveObjectWithData(originalReqData) as? NSURLRequest {
task.setValue(originalRequest, forKey: "originalRequest")
}
if task.currentRequest == nil, let currentReqData = resumeDic[kResumeCurrentRequest] as? NSData, let currentRequest = NSKeyedUnarchiver.unarchiveObjectWithData(currentReqData) as? NSURLRequest {
task.setValue(currentRequest, forKey: "currentRequest")
}
}
return task
}
}
Свифт 3:
func correct(requestData data: Data?) -> Data? {
guard let data = data else {
return nil
}
if NSKeyedUnarchiver.unarchiveObject(with: data) != nil {
return data
}
guard let archive = (try? PropertyListSerialization.propertyList(from: data, options: [.mutableContainersAndLeaves], format: nil)) as? NSMutableDictionary else {
return nil
}
// Rectify weird __nsurlrequest_proto_props objects to $number pattern
var k = 0
while ((archive["$objects"] as? NSArray)?[1] as? NSDictionary)?.object(forKey: "$\(k)") != nil {
k += 1
}
var i = 0
while ((archive["$objects"] as? NSArray)?[1] as? NSDictionary)?.object(forKey: "__nsurlrequest_proto_prop_obj_\(i)") != nil {
let arr = archive["$objects"] as? NSMutableArray
if let dic = arr?[1] as? NSMutableDictionary, let obj = dic["__nsurlrequest_proto_prop_obj_\(i)"] {
dic.setObject(obj, forKey: "$\(i + k)" as NSString)
dic.removeObject(forKey: "__nsurlrequest_proto_prop_obj_\(i)")
arr?[1] = dic
archive["$objects"] = arr
}
i += 1
}
if ((archive["$objects"] as? NSArray)?[1] as? NSDictionary)?.object(forKey: "__nsurlrequest_proto_props") != nil {
let arr = archive["$objects"] as? NSMutableArray
if let dic = arr?[1] as? NSMutableDictionary, let obj = dic["__nsurlrequest_proto_props"] {
dic.setObject(obj, forKey: "$\(i + k)" as NSString)
dic.removeObject(forKey: "__nsurlrequest_proto_props")
arr?[1] = dic
archive["$objects"] = arr
}
}
/* I think we have no reason to keep this section in effect
for item in (archive["$objects"] as? NSMutableArray) ?? [] {
if let cls = item as? NSMutableDictionary, cls["$classname"] as? NSString == "NSURLRequest" {
cls["$classname"] = NSString(string: "NSMutableURLRequest")
(cls["$classes"] as? NSMutableArray)?.insert(NSString(string: "NSMutableURLRequest"), at: 0)
}
}*/
// Rectify weird "NSKeyedArchiveRootObjectKey" top key to NSKeyedArchiveRootObjectKey = "root"
if let obj = (archive["$top"] as? NSMutableDictionary)?.object(forKey: "NSKeyedArchiveRootObjectKey") as AnyObject? {
(archive["$top"] as? NSMutableDictionary)?.setObject(obj, forKey: NSKeyedArchiveRootObjectKey as NSString)
(archive["$top"] as? NSMutableDictionary)?.removeObject(forKey: "NSKeyedArchiveRootObjectKey")
}
// Reencode archived object
let result = try? PropertyListSerialization.data(fromPropertyList: archive, format: PropertyListSerialization.PropertyListFormat.binary, options: PropertyListSerialization.WriteOptions())
return result
}
func getResumeDictionary(_ data: Data) -> NSMutableDictionary? {
// In beta versions, resumeData is NSKeyedArchive encoded instead of plist
var iresumeDictionary: NSMutableDictionary? = nil
if #available(iOS 10.0, OSX 10.12, *) {
var root : AnyObject? = nil
let keyedUnarchiver = NSKeyedUnarchiver(forReadingWith: data)
do {
root = try keyedUnarchiver.decodeTopLevelObject(forKey: "NSKeyedArchiveRootObjectKey") ?? nil
if root == nil {
root = try keyedUnarchiver.decodeTopLevelObject(forKey: NSKeyedArchiveRootObjectKey)
}
} catch {}
keyedUnarchiver.finishDecoding()
iresumeDictionary = root as? NSMutableDictionary
}
if iresumeDictionary == nil {
do {
iresumeDictionary = try PropertyListSerialization.propertyList(from: data, options: PropertyListSerialization.ReadOptions(), format: nil) as? NSMutableDictionary;
} catch {}
}
return iresumeDictionary
}
func correctResumeData(_ data: Data?) -> Data? {
let kResumeCurrentRequest = "NSURLSessionResumeCurrentRequest"
let kResumeOriginalRequest = "NSURLSessionResumeOriginalRequest"
guard let data = data, let resumeDictionary = getResumeDictionary(data) else {
return nil
}
resumeDictionary[kResumeCurrentRequest] = correct(requestData: resumeDictionary[kResumeCurrentRequest] as? Data)
resumeDictionary[kResumeOriginalRequest] = correct(requestData: resumeDictionary[kResumeOriginalRequest] as? Data)
let result = try? PropertyListSerialization.data(fromPropertyList: resumeDictionary, format: PropertyListSerialization.PropertyListFormat.xml, options: PropertyListSerialization.WriteOptions())
return result
}
extension URLSession {
func correctedDownloadTask(withResumeData resumeData: Data) -> URLSessionDownloadTask {
let kResumeCurrentRequest = "NSURLSessionResumeCurrentRequest"
let kResumeOriginalRequest = "NSURLSessionResumeOriginalRequest"
let cData = correctResumeData(resumeData) ?? resumeData
let task = self.downloadTask(withResumeData: cData)
// a compensation for inability to set task requests in CFNetwork.
// While you still get -[NSKeyedUnarchiver initForReadingWithData:]: data is NULL error,
// this section will set them to real objects
if let resumeDic = getResumeDictionary(cData) {
if task.originalRequest == nil, let originalReqData = resumeDic[kResumeOriginalRequest] as? Data, let originalRequest = NSKeyedUnarchiver.unarchiveObject(with: originalReqData) as? NSURLRequest {
task.setValue(originalRequest, forKey: "originalRequest")
}
if task.currentRequest == nil, let currentReqData = resumeDic[kResumeCurrentRequest] as? Data, let currentRequest = NSKeyedUnarchiver.unarchiveObject(with: currentReqData) as? NSURLRequest {
task.setValue(currentRequest, forKey: "currentRequest")
}
}
return task
}
}
Что касается части вопроса о unsupported URL
ошибка и потеря возобновления данных при сбое в сети или другом сбое. Я зарегистрировал TSI в Apple и получил последний ответ от Quinn:
Во-первых, поведение, которое вы видите, определенно является ошибкой в NSURLSession. Мы надеемся исправить эту проблему в будущем обновлении программного обеспечения. Эта работа отслеживается. У меня нет никакой информации о том, когда исправление будет доставлено обычным пользователям iOS.
Что касается обходных путей, я вчера подробно остановился на этом вопросе, и теперь я полностью понимаю неудачу. IMO, есть разумный способ обойти это, но мне нужно проверить свои идеи после разработки NSURLSession, прежде чем я смогу поделиться ими. Я надеюсь услышать от них ответ на следующий день или два. Подождите, пожалуйста.
Я буду публиковать обновления по мере их появления, но я уверен, что это дает людям некоторую надежду, что по крайней мере проблема рассматривается Apple.
(Массивные подпорки к обходному пути Mousavian для приостановки / возобновления поведения)
ОБНОВИТЬ:
Из Куинн,
В самом деле. С тех пор, как мы в последний раз говорили (и я прошу прощения, что я так долго возвращался к вам здесь; недавно я был похоронен в инцидентах), я углубился в это от имени некоторых других разработчиков и обнаружил, что: A. Эта проблема проявляется в двух контекстах, характеризующихся ошибками NSURLErrorCannotWriteToFile и NSURLErrorUnsupportedURL. Б. Мы можем обойти первое, но не второе. Я приложил обновление к своему документу, который заполняет детали. К сожалению, мы не смогли найти обходной путь для второго симптома. Единственный путь вперед - для iOS Engineering исправить эту ошибку. Мы надеемся, что это произойдет в обновлении программного обеспечения iOS 10, но у меня нет конкретных деталей, которыми можно поделиться (кроме того, что это исправление выглядит так, как будто оно пропустило шину 10.1)-:
Так что, к сожалению, unsupported URL
Проблема не имеет решения, и мы должны ждать исправления ошибки.
NSURLErrorCannotWriteToFile
проблема решена кодом Mousavian выше.
ДРУГОЕ ОБНОВЛЕНИЕ:
Куинн подтвердил последние 10,2 бета-попытки решить эти проблемы.
Это взглянул на 10.2?
Да. Решение этой проблемы было включено в первую бета-версию 10.2. Ряд разработчиков, с которыми я работал, сообщили, что этот патч застрял, но я все же рекомендую вам попробовать его на последней бета-версии (в настоящее время iOS 10.2 beta 2, 14C5069c). Дайте мне знать, если вы попали в какие-нибудь препятствия.
Вот код Objective - C для ответа Мусавиана.
Отлично работает в iOS 9.3.5(устройство) и iOS 10.1 (симулятор).
Сначала исправьте данные Resume Мусавианом
- (NSData *)correctRequestData:(NSData *)data
{
if (!data) {
return nil;
}
if ([NSKeyedUnarchiver unarchiveObjectWithData:data]) {
return data;
}
NSMutableDictionary *archive = [NSPropertyListSerialization propertyListWithData:data options:NSPropertyListMutableContainersAndLeaves format:nil error:nil];
if (!archive) {
return nil;
}
int k = 0;
while ([[archive[@"$objects"] objectAtIndex:1] objectForKey:[NSString stringWithFormat:@"$%d", k]]) {
k += 1;
}
int i = 0;
while ([[archive[@"$objects"] objectAtIndex:1] objectForKey:[NSString stringWithFormat:@"__nsurlrequest_proto_prop_obj_%d", i]]) {
NSMutableArray *arr = archive[@"$objects"];
NSMutableDictionary *dic = [arr objectAtIndex:1];
id obj;
if (dic) {
obj = [dic objectForKey:[NSString stringWithFormat:@"__nsurlrequest_proto_prop_obj_%d", i]];
if (obj) {
[dic setObject:obj forKey:[NSString stringWithFormat:@"$%d",i + k]];
[dic removeObjectForKey:[NSString stringWithFormat:@"__nsurlrequest_proto_prop_obj_%d", i]];
arr[1] = dic;
archive[@"$objects"] = arr;
}
}
i += 1;
}
if ([[archive[@"$objects"] objectAtIndex:1] objectForKey:@"__nsurlrequest_proto_props"]) {
NSMutableArray *arr = archive[@"$objects"];
NSMutableDictionary *dic = [arr objectAtIndex:1];
if (dic) {
id obj;
obj = [dic objectForKey:@"__nsurlrequest_proto_props"];
if (obj) {
[dic setObject:obj forKey:[NSString stringWithFormat:@"$%d",i + k]];
[dic removeObjectForKey:@"__nsurlrequest_proto_props"];
arr[1] = dic;
archive[@"$objects"] = arr;
}
}
}
id obj = [archive[@"$top"] objectForKey:@"NSKeyedArchiveRootObjectKey"];
if (obj) {
[archive[@"$top"] setObject:obj forKey:NSKeyedArchiveRootObjectKey];
[archive[@"$top"] removeObjectForKey:@"NSKeyedArchiveRootObjectKey"];
}
NSData *result = [NSPropertyListSerialization dataWithPropertyList:archive format:NSPropertyListBinaryFormat_v1_0 options:0 error:nil];
return result;
}
- (NSMutableDictionary *)getResumDictionary:(NSData *)data
{
NSMutableDictionary *iresumeDictionary;
if ([[NSProcessInfo processInfo] operatingSystemVersion].majorVersion >= 10) {
NSMutableDictionary *root;
NSKeyedUnarchiver *keyedUnarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
NSError *error = nil;
root = [keyedUnarchiver decodeTopLevelObjectForKey:@"NSKeyedArchiveRootObjectKey" error:&error];
if (!root) {
root = [keyedUnarchiver decodeTopLevelObjectForKey:NSKeyedArchiveRootObjectKey error:&error];
}
[keyedUnarchiver finishDecoding];
iresumeDictionary = root;
}
if (!iresumeDictionary) {
iresumeDictionary = [NSPropertyListSerialization propertyListWithData:data options:0 format:nil error:nil];
}
return iresumeDictionary;
}
static NSString * kResumeCurrentRequest = @"NSURLSessionResumeCurrentRequest";
static NSString * kResumeOriginalRequest = @"NSURLSessionResumeOriginalRequest";
- (NSData *)correctResumData:(NSData *)data
{
NSMutableDictionary *resumeDictionary = [self getResumDictionary:data];
if (!data || !resumeDictionary) {
return nil;
}
resumeDictionary[kResumeCurrentRequest] = [self correctRequestData:[resumeDictionary objectForKey:kResumeCurrentRequest]];
resumeDictionary[kResumeOriginalRequest] = [self correctRequestData:[resumeDictionary objectForKey:kResumeOriginalRequest]];
NSData *result = [NSPropertyListSerialization dataWithPropertyList:resumeDictionary format:NSPropertyListXMLFormat_v1_0 options:0 error:nil];
return result;
}
Я не создал категорию для NSURLSession, я просто создал в My Singleton. Вот код для создания NSURLSessionDownloadTask:
NSData *cData = [self correctResumData:self.resumeData];
if (!cData) {
cData = self.resumeData;
}
self.downloadTask = [self.session downloadTaskWithResumeData:cData];
if ([self getResumDictionary:cData]) {
NSDictionary *dict = [self getResumDictionary:cData];
if (!self.downloadTask.originalRequest) {
NSData *originalData = dict[kResumeOriginalRequest];
[self.downloadTask setValue:[NSKeyedUnarchiver unarchiveObjectWithData:originalData] forKey:@"originalRequest"];
}
if (!self.downloadTask.currentRequest) {
NSData *currentData = dict[kResumeCurrentRequest];
[self.downloadTask setValue:[NSKeyedUnarchiver unarchiveObjectWithData:currentData] forKey:@"currentRequest"];
}
}