Как эффективно записывать большие файлы на диск в фоновом потоке (Swift)

Обновить

Я решил и удалил отвлекающую ошибку. Пожалуйста, прочитайте весь пост и не стесняйтесь оставлять комментарии, если остались какие-либо вопросы.

Фон

Я пытаюсь записать относительно большие файлы (видео) на диск на iOS, используя Swift 2.0, GCD и обработчик завершения. Я хотел бы знать, есть ли более эффективный способ выполнить эту задачу. Задача должна быть выполнена без блокировки основного интерфейса, с использованием логики завершения, а также обеспечения того, чтобы операция происходила как можно быстрее. У меня есть пользовательские объекты со свойством NSData, поэтому я в настоящее время экспериментирую, используя расширение для NSData. В качестве примера альтернативное решение может включать использование NSFilehandle или NSStreams в сочетании с некоторой формой потоково-безопасного поведения, которая приводит к гораздо более быстрой пропускной способности, чем функция writeDoURL NSData, на которой я основываю текущее решение.

Что не так с NSData?

Обратите внимание на следующее обсуждение, взятое из ссылки на класс NSData, (Сохранение данных). Я выполняю записи в мой временный каталог, однако главная причина, по которой у меня возникает проблема, заключается в том, что при работе с большими файлами я вижу заметное отставание в пользовательском интерфейсе. Эта задержка объясняется тем, что NSData не является асинхронным (и Apple Docs отмечает, что атомарные записи могут вызывать проблемы с производительностью «больших» файлов ~> 1 МБ). Поэтому при работе с большими файлами каждый зависит от того, какой внутренний механизм работает в методах NSData.

Я еще покопался и нашел эту информацию от Apple ... "Этот метод идеально подходит для преобразования data: // URL в объекты NSData, а также может использоваться для чтениякороткие файлы синхронно. Если вам нужно прочитать потенциально большие файлы, используйте inputStreamWithURL: чтобы открыть поток, а затем прочитать файл по частям за раз. "(Ссылка на класс NSData, Objective-C, + dataWithContentsOfURL). Эта информация подразумевает, что я мог бы попытаться использовать потоки для записи файла в фоновом потоке, если перемещения writeToURL в фоновом потоке (как предложено @jtbandes) недостаточно.

Класс NSData и его подклассы предоставляют методы для быстрого и простого сохранения их содержимого на диск. Чтобы минимизировать риск потери данных, эти методы предоставляют возможность атомарного сохранения данных. Атомная запись гарантирует, что данные либо полностью сохранены, либо полностью сбои. Атомная запись начинается с записи данных во временный файл. Если эта запись завершается успешно, метод перемещает временный файл в его окончательное местоположение.

Хотя атомарные операции записи минимизируют риск потери данных из-за поврежденных или частично записанных файлов, они могут не подходить при записи во временный каталог, домашний каталог пользователя или другие общедоступные каталоги. Каждый раз, когда вы работаете с общедоступным файлом, вы должны рассматривать этот файл как ненадежный и потенциально опасный ресурс. Злоумышленник может скомпрометировать или повредить эти файлы. Злоумышленник также может заменить файлы жесткими или символическими ссылками, в результате чего ваши операции записи перезапишут или повредят другие системные ресурсы.

Избегайте использования метода writeToURL: atomically: (и связанных с ним методов) при работе в общедоступном каталоге. Вместо этого инициализируйте объект NSFileHandle с помощью существующего дескриптора файла и используйте методы NSFileHandle для безопасной записи файла.

Другие альтернативы

Одинстатья на параллельное программирование в objc.io предоставляет интересные опции на «Advanced: File I / O в фоновом режиме». Некоторые из этих опций также включают использование InputStream. У Apple также есть некоторые старые ссылки начтение и запись файлов асинхронно, Я отправляю этот вопрос в ожидании альтернатив Свифта.

Пример соответствующего ответа

Вот пример соответствующего ответа, который может удовлетворить этот тип вопроса. (Принято для руководства по программированию потока,Запись в выходные потоки)

Использование экземпляра NSOutputStream для записи в выходной поток требует нескольких шагов:

Создайте и инициализируйте экземпляр NSOutputStream с хранилищем для записанных данных. Также установите делегата.Запланируйте объект потока в цикле выполнения и откройте поток.Обработайте события, о которых объект потока сообщает своему делегату.Если объект потока записал данные в память, получите данные, запросив свойство NSStreamDataWrittenToMemoryStreamKey.Когда больше нет данных для записи, избавьтесь от объекта потока.

Я ищу наиболее эффективный алгоритм, который применяется для записи очень больших файлов в iOS с использованием Swift, API или, возможно, даже C / ObjC. Я могу перенести алгоритм в соответствующие Swift-совместимые конструкции.

Нота Бене

Я понимаю информационную ошибку ниже. Это включено для полноты. Этот вопрос задает вопрос, существует ли лучший алгоритм для записи больших файлов на диск с гарантированной последовательностью зависимостей (например, зависимости NSOperation). Если есть, предоставьте достаточно информации (описание / пример для меня, чтобы восстановить соответствующий код, совместимый с Swift 2.0). Пожалуйста, сообщите, если мне не хватает какой-либо информации, которая поможет ответить на вопрос.

Обратите внимание на расширение

Я добавил обработчик завершения в базовый writeToURL, чтобы предотвратить непреднамеренное совместное использование ресурсов. Мои зависимые задачи, которые используют файл, никогда не должны сталкиваться с условиями гонки.

extension NSData {

    func writeToURL(named:String, completion: (result: Bool, url:NSURL?) -> Void)  {

       let filePath = NSTemporaryDirectory() + named
       //var success:Bool = false
       let tmpURL = NSURL( fileURLWithPath:  filePath )
       weak var weakSelf = self


      dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
                //write to URL atomically
                if weakSelf!.writeToURL(tmpURL, atomically: true) {

                        if NSFileManager.defaultManager().fileExistsAtPath( filePath ) {
                            completion(result: true, url:tmpURL)                        
                        } else {
                            completion (result: false, url:tmpURL)
                        }
                    }
            })

        }
    }

Этот метод используется для обработки данных пользовательских объектов из контроллера с использованием:

var items = [AnyObject]()
if let video = myCustomClass.data {

    //video is of type NSData        
    video.writeToURL("shared.mp4", completion: { (result, url) -> Void in
        if result {
            items.append(url!)
            if items.count > 0 {

                let sharedActivityView = UIActivityViewController(activityItems: items, applicationActivities: nil)

                self.presentViewController(sharedActivityView, animated: true) { () -> Void in
                //finished
    }
}
        }
     })
}

Заключение

Документы Apple наОсновная производительность данных дать несколько полезных советов о том, как справиться с нехваткой памяти и управлять BLOB-объектами. Это действительно чертова статья со множеством подсказок о поведении и о том, как смягчить проблему больших файлов в вашем приложении. Теперь, хотя это относится к основным данным, а не к файлам, предупреждение об атомарной записи говорит мне, что я должен с большой осторожностью реализовать методы, которые пишут атомарно.

В случае больших файлов единственным безопасным способом управления записью является добавление обработчика завершения (к методу записи) и отображение представления активности в главном потоке. Делать это с потоком или модифицировать существующий API для добавления логики завершения - решать читателю. Я делал как в прошлом, так и нахожусь в процессе тестирования на лучшую производительность.

До тех пор я меняю решение, чтобы удалить все свойства двоичных данных из Core Data и заменить их строками для хранения URL-адресов активов на диске. Я также использую встроенную функциональность из библиотеки активов и PHAsset для захвата и хранения всех связанных URL-адресов активов. Когда или если мне нужно скопировать какие-либо активы, я буду использовать стандартные методы API (методы экспорта в PHAsset / Asset Library) с обработчиками завершения, чтобы уведомить пользователя о состоянии завершения в главном потоке.

(Действительно полезные фрагменты из статьи Core Data Performance)

Уменьшение накладных расходов памяти

Иногда бывает так, что вы хотите использовать управляемые объекты на временной основе, например, для вычисления среднего значения для определенного атрибута. Это приводит к росту графа вашего объекта и потребления памяти. Вы можете уменьшить накладные расходы памяти путем повторного сбоя отдельных управляемых объектов, которые вам больше не нужны, или вы можете сбросить контекст управляемого объекта, чтобы очистить весь граф объекта. Вы также можете использовать шаблоны, которые применяются к программированию Какао в целом.

Вы можете повторно выполнить отказ отдельного управляемого объекта, используя метод refreshObject: mergeChanges: метод NSManagedObjectContext. Это приводит к очистке его значений свойств в памяти, тем самым уменьшая накладные расходы памяти. (Обратите внимание, что это не то же самое, что установка значений свойств на ноль - значения будут получены по требованию, если возникнет ошибка - см. Faulting и Uniquing.)

Когда вы создаете запрос на выборку, вы можете установить для includePropertyValues ​​значение NO>, чтобы уменьшить накладные расходы памяти, избегая создания объектов для представления значений свойств. Как правило, это следует делать только в том случае, если вы уверены, что либо вам не понадобятся фактические данные свойств, либо у вас уже есть информация в кэше строк, в противном случае вы будете подвергаться нескольким поездкам в постоянное хранилище.

Вы можете использовать метод сброса NSManagedObjectContext, чтобы удалить все управляемые объекты, связанные с контекстом, и «начать заново», как если бы вы только что создали его. Обратите внимание, что любой управляемый объект, связанный с этим контекстом, будет признан недействительным, и поэтому вам нужно будет отказаться от любых ссылок и повторно извлечь любые объекты, связанные с этим контекстом, в которых вы все еще заинтересованы. Если вы выполняете итерацию по большому количеству объектов, вам может потребоваться использовать блоки локального пула автоматического выпуска, чтобы обеспечить временное освобождение временных объектов.

Если вы не намерены использовать функциональные возможности отмены Core Data, вы можете уменьшить требования к ресурсам вашего приложения, установив диспетчер отмены контекста на ноль. Это может быть особенно полезно для фоновых рабочих потоков, а также для крупных операций импорта или пакетной обработки.

Наконец, Базовые данные по умолчанию не сохраняют строгие ссылки на управляемые объекты (если они не имеют несохраненных изменений). Если у вас много объектов в памяти, вы должны определить ссылки-владельцы. Управляемые объекты поддерживают сильные ссылки друг на друга посредством отношений, которые могут легко создавать сильные ссылочные циклы. Вы можете прерывать циклы путем повторного сбоя объектов (опять же, используя refreshObject: mergeChanges: метод NSManagedObjectContext).

Большие объекты данных (BLOB)

Если ваше приложение использует большие большие двоичные объекты («большие двоичные объекты», такие как данные изображения и звука), вам необходимо позаботиться о том, чтобы минимизировать накладные расходы. Точное определение «маленький», «скромный» и «большой» является гибким и зависит от использования приложения. Эмпирическое правило гласит, что объекты размером порядка килобайт имеют «скромный» размер, а объекты размером порядка мегабайт - «большой». Некоторые разработчики достигли хорошей производительности с 10 МБ BLOB в базе данных. С другой стороны, если приложение имеет миллионы строк в таблице, даже 128 байтов могут быть CLOB «скромного» размера (крупный символьный объект), который необходимо нормализовать в отдельную таблицу.

В общем, если вам нужно хранить большие двоичные объекты в постоянном хранилище, вам следует использовать хранилище SQLite. Для хранения XML и двоичных файлов требуется, чтобы весь граф объектов находился в памяти, а записи в хранилище были атомарными (см. «Функции постоянного хранилища»), что означает, что они не могут эффективно работать с большими объектами данных. SQLite может масштабироваться для работы с очень большими базами данных. При правильном использовании SQLite обеспечивает хорошую производительность для баз данных объемом до 100 ГБ, а одна строка может содержать до 1 ГБ (хотя, конечно, чтение 1 ГБ данных в память является дорогостоящей операцией, независимо от того, насколько эффективен репозиторий).

BLOB часто представляет атрибут объекта - например, фотография может быть атрибутом объекта Employee. Для BLOB небольшого или небольшого размера (и CLOB) необходимо создать отдельную сущность для данных и создать отношение «один к одному» вместо атрибута. Например, вы можете создать сущности «Сотрудник» и «Фотография» с взаимно-однозначным отношением между ними, где отношение «Сотрудник» к «Фотография» заменяет атрибут «Фотография сотрудника». Этот шаблон максимизирует преимущества повреждения объекта (см. Faulting и Uniquing). Любая данная фотография извлекается, только если она действительно необходима (если пересекаются отношения).

Однако лучше, если вы можете хранить большие двоичные объекты как ресурсы в файловой системе и поддерживать ссылки (например, URL-адреса или пути) на эти ресурсы. Затем вы можете загрузить BLOB по мере необходимости.

Замечания:

Я переместил приведенную ниже логику в обработчик завершения (см. Код выше) и больше не вижу ошибок. Как упоминалось ранее, этот вопрос касается того, существует ли более эффективный способ обработки больших файлов в iOS с помощью Swift.

При попытке обработки результирующего массива элементов для передачи в UIActvityViewController, используя следующую логику:

if items.count> 0 {
let sharedActivityView = UIActivityViewController (activityItems: items, applicationActivities: nil) self.presentViewController (sharedActivityView, animated: true) {() -> Void in // Закончено}}

Я вижу следующую ошибку: Ошибка связи: {count = 1, contents = "XPCErrorDescription" => {length = 22, contents = "Соединение прервано"}}> (обратите внимание, я ищу лучший дизайн, а не ответьте на это сообщение об ошибке)

Ответы на вопрос(1)

Ваш ответ на вопрос