Pull to refresh

Мониторинг файлов с помощью GCD

Reading time4 min
Views5.7K
Думаю, что большинству разработчиков под iOS известно как легко включить iTunes File Sharing в своем приложении, добавив лишь одну строчку в Info.plist:
UIFileSharingEnabled = YES
Но это даже не полдела. Соль в том, что, по-хорошему, приложение теперь должно остлеживать все изменения с файлами, происходящие в директории Documents и соответственно обновлять свои данные. Как это релизовать в своём коде и расскажет данная статья.
image

Для начала совсем немного теории из Concurrency Programming Guide. В GCD есть такое понятие как dispatch source – фундаментальный тип данных, который ответчает за координацию обработки специфических низкоуровневых событий. Для решения нашей задачи нас более всего интересует такая его разновидность как descriptor sources, оповещающий о различных операциях с файлами или сокетами.

Получается, что нам нужно методом dispatch_source_create создать диспетчер событий, источником которых послужит дескриптор файлов (directory file descriptor), а по самому событию (запись файла) отработает необходимый блок обновления данных приложения.

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

Итак, ближе к телу. Создадим два основных метода startMonitor и stopMonitor, соответственно запускающих и останавлювающи мониторинг нужной нам директории, а также пару-тройку вспомогательных методов проверки изменений в этой директории, которые будут запускаться через handler block.


- (void)startMonitor
{
	// проверяем создана ли уже dispatch_source_t
	if (_src != NULL) return; 
    
	// путь к директории Documents на устройстве
    NSString *docPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    
	// дескриптор файлов, только для нотификации событий (O_EVTONLY)
	_fileDescriptor = open([docPath fileSystemRepresentation], O_EVTONLY);
    
	// работаем в главной thread, так как будем обновлять UI
	dispatch_queue_t queue = dispatch_get_main_queue();
    
	// создаем тот самый dispatch source для мониторинга событий (write)
	_src = dispatch_source_create(DISPATCH_SOURCE_TYPE_VNODE, _fileDescriptor, DISPATCH_VNODE_WRITE,  queue);	
    
	// handler block отрабатывающий при изменениях в директории
	dispatch_source_set_event_handler(_src, ^{
        [self directoryDidChange];
    });
    
	// при отмене закрываем дескриптор
	dispatch_source_set_cancel_handler(_src, ^{
      close(_fileDescriptor);
   });
    
	dispatch_resume(_src);
}

- (void)stopMonitor
{
	if (_src) { // тут все просто, ломать не строить
		dispatch_source_cancel(_src);
		_src = NULL;
	}
}

- (void)directoryDidChange
{
    if(!waitingForDocumentsDirectoryTimeout)  {
      // помним, что синхронизация не мгновенна, поэтому включаем флаг ожидания
        waitingForDocumentsDirectoryTimeout = YES; 

      // получаем массив с файлами в директории
        _lastDocumentsDirectoryReferenceArray=[self documentsDirectoryReferenceArray]; 

      //...и запускаем блок проверки файлов с таймаутом в одну секунду, например
        [self performSelector:@selector(checkForDocumentsDirectoryChanges) withObject:nil afterDelay:1.0 inModes:[NSArray arrayWithObjects:NSRunLoopCommonModes,nil]];
    }
}

-(void)checkForDocumentsDirectoryChanges {
    NSArray *newDocumentsDirectoryReferenceArray=[self documentsDirectoryReferenceArray];

    // банальный алгоритм сравнения двух массивов
    if(![newDocumentsDirectoryReferenceArray isEqualToArray:_lastDocumentsDirectoryReferenceArray]) {

      // рекурсивно продолжаем проверку файлов с таймаутом
        _lastDocumentsDirectoryReferenceArray=newDocumentsDirectoryReferenceArray;
        [self performSelector:@selector(checkForDocumentsDirectoryChanges) withObject:nil afterDelay:1.0 inModes:[NSArray arrayWithObjects:NSRunLoopCommonModes,nil]];

    } else {

        // синхронизация файлов завершена
        waitingForDocumentsDirectoryTimeout=NO;
        _lastDocumentsDirectoryReferenceArray=nil;

       // ...тут уж нужно вставить ваш блок обновления данных в программе, например
      [self scanDocumentsDirectory];
    }
}

-(NSArray *)documentsDirectoryReferenceArray {
    // возвращает массив со списком файлов в директории формата Название-Размер
    NSString *documentsDirectoryPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    NSArray *documentsDirectoryContents = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:documentsDirectoryPath error:NULL];
    
    NSMutableArray *documentsDirectoryReferenceArray=[NSMutableArray arrayWithCapacity:10];
    
    for(NSString *fileName in documentsDirectoryContents){
        
        NSString *filePath=[documentsDirectoryPath stringByAppendingPathComponent:fileName];
        
        NSError *error;
        NSDictionary *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:&error];
        NSInteger fileSize = [[fileAttributes objectForKey:NSFileSize] intValue];
        NSString *fileWithLength=[NSString stringWithFormat:@"%@-%d",fileName,fileSize];
        
        [documentsDirectoryReferenceArray addObject:fileWithLength];
    }
    
    return documentsDirectoryReferenceArray;
}


Ну, а для тех кто пробежался глазами по тектсу и кому лень разбираться, подобный подход уже реализован в Cocoanetics/DTFoundation класс DTFolderMonitor.

Еще, из опыта работы над своим приложением, использующем iTunes File Sharing, хочу напомнить о необходимости запуска метода типа scanDocumentsDirectory, помимо мониторинга, как только приложение становится активным. Его назначение не только проверять, но и обновлять данные о файлах в приложении с их фактическим наличием в директории, так как синхронизация файлов с iTunes может происходить и в момент, когда приложение находится в бэкграунде, либо совсем не запущено.


- (void)applicationWillEnterForeground:(UIApplication *)application
{
    [self scanDocumentsDirectory];
}


Ссылки на первоисточники
Историческая ветка обсуждений по теме на форуме разработчиков Apple (необходим девелоперский акаунт для доступа)
Directory Monitor – олдскульный монитор от Michael Heyeck
Directory Monitoring and GCD он же, но обновленный
Monitoring a Folder with GCD решение от Сocoanetics
Tags:
Hubs:
+12
Comments2

Articles