Очередь асинхронных операций

Автор: Андрей. Категория: Уроки Flex

Привет. Сегодня речь пойдёт о том, как облегчить создание программ, использующих асинхронные операции. Я имею ввиду загрузку мультимедийного контента, асинхронные запросы к базе данных и т.д.

Цель

  • Понять суть асинхронных операций
  • Получше познакомиться с классами Vector и Dictionary
  • Понять принцип очереди операций
  • Использовать шаблон проектирования Singleton

Техническое задание

Реализовать возможность загрузки и хранения изображений в одном классе. Этот класс должен быть реализован на основе шаблона проектирования Singleton. Реализовать возможность последовательной загрузки нескольких изображений. Реализовать возможность выполнения методов как после загрузки определённого ресурса, так и после всех заданных загрузок.

Решение

Поговорим немного об асинхронных операциях. Flash не поддерживает многопоточность, но зато у него реализована возможность асинхронного выполнения кода. Что это значит? Если нам нужно загрузить какой-либо ресурс, например изображение или текстовый файл, то после запуска метода типа load() программа не останавливается, чтобы дождаться момента, когда ресурс будет полностью загружен. Тоже самое происходит с запросами к серверу, если мы разрабатываем браузерное приложение. После отправления запроса к серверу, программа продолжает работать и выполнять дальнейшие инструкции. О том, что сервер прислал ответ на запрос программа узнаёт после возникновения определённого события. С одной стороны это большой плюс, т.к. для пользователя подобные операции будут протекать незаметно. Но с другой стороны это может вызвать трудности при разработке.

В этом уроке я расскажу как сделать простейший менеджер ресурсов, который будет загружать изображения и хранить их на всём жизненном цикле программы. Для последовательной загрузки будем использовать очередь загрузок. Т.е. загрузка очередного файла происходит после загрузки предыдущего. Все загруженные данные будут храниться в объекте типа Dictionary. Данный класс по структуре напоминает класс HashMap из языка Java, т.е. в качестве ключей словаря могут использоваться не только строки, но и любые другие объекты.

Запускаем Flash Builder и создаём проект Action Script. Назовём его QueueTest. Сразу начнём с создания класса SourceManager. Определим, что нам нужно. Во первых, для реализации шаблона Singleton нужна переменная instance. Во-вторых, нужен диспетчер событий для информирования о завершении очереди загрузок и статичная константа для типа события. Также нужен массив имён загружаемых файлов, хранилище данных, хранилище хэндлеров и несколько ссылок для выполнения очередной загрузки. Вот как выглядит наш класс в первоначальном виде:

package
{
    import flash.display.*;
    import flash.events.*;
    import flash.events.EventDispatcher;
    import flash.utils.Dictionary;

    public class SourceManager
    {
        /** Тип события, возникающего после загрузки всех файлов в очереди */
        public static const LOADING_COMPLETE:String = 'loading_complete';
        
        protected static var _instance:SourceManager;             //тут храним объект данного класса
        
        protected var _dispatcher:EventDispatcher;                //диспетчер для создания события
        protected var _queue:Vector.<String>;                     //вектор имён файлов
        protected var _handlers:Dictionary;                       //словарь функций для вызова после загрузки определённого файла
        protected var _busy:Boolean;                              //флаг выполнения очереди
        protected var _currentFileName:String;                    //имя очередного файла
        protected var _currentLoader:Loader;                      //загрузчик очередного файла
        protected var _dataStore:Dictionary;                      //хранилище данных
        
        public function SourceManager(singleton:Singleton)
        {
            _dispatcher = new EventDispatcher();
            
            _queue = new Vector.;
            _handlers = new Dictionary();
            _busy = false;
            _dataStore = new Dictionary();
        }
        
        public static function get instance():SourceManager
        {
            if (!_instance)
                _instance = new SourceManager(new Singleton());
            
            return _instance;
        }
    }
}

class Singleton
{
    //Реализация шаблона Singleton
}

С помощью внутреннего класса Singleton мы исключаем возможность создания объекта SourceManager в любом месте приложения.

На самом деле в нашем примере можно использовать обычный массив вместо класса Vector, а класс Dictionary заменить на Object. Но задача урока рассмотреть именно данные классы, поэтому я буду использовать их.

Теперь реализуем очередь загрузки файлов. При вызове метода загрузки мы не создаём загрузчика, а всего лишь добавляем имя файла в вектор имён, из которого последовательно будем эти имена доставать. Для загрузки изображений используется класс Loader. Это необходимо учесть, т.к. загрузку текстовых файлов, файлов XML необходимо реализовывать с помощью класса URLLoader. Ниже я приведу полный листинг метода загрузки и поясню некоторые моменты в комментариях:

    public static function loadSource(fileName:String, handler:Function = null):void
    {
        //через статичный метод класса вызываем метод экземпляра
        _instance.loadFile(fileName, handler); 
    }
    
    protected function loadFile(fileName:String, handler:Function):void
    {
        //добавляем имя файла в очередь
        _queue.push(fileName);
        
        //если указан метод для выполнения после загрузки данного файла, запоминаем его
        if (handler != null)
            _handlers[fileName] = handler; 
        
        //если ничего не загружается, то запускаем очередь
        if (!_busy)
        {
            loadNextFile();
            _busy = true;
        }
    }
    
    protected function loadNextFile():void
    {
        //запоминаем текущее имя файла и создаём текущий загрузчик
        _currentFileName = _queue.shift();
        _currentLoader = new Loader();
        
        //вешаем слушателей загрузчику
        addHandlers();
        
        //загружаем
        _currentLoader.load(new URLRequest(_currentFileName));
    }
    
    protected function addHandlers():void
    {
        _currentLoader.contentLoaderInfo.addEventListener(Event.COMPLETE, completeHandler);
        _currentLoader.contentLoaderInfo.addEventListener(IOErrorEvent.IO_ERROR, ioErrorHandler);        
    }
    
    protected function completeHandler(event:Event):void
    {
        //удаляем слушателей загрузчика
        removeHandlers();
        
        //сохраняем загруженные данные
        _dataStore[_currentFileName] = _currentLoader.content;
        
        //если у текущего файла был хэндлер, то выполняем его
        var handler:Function = _handlers[_currentFileName] as Function; 
        if (handler != null)
        {
            handler(_currentLoader.content);
 
            //...и удаляем после выполнения
            _handlers[_currentFileName] = null;
            delete _handlers[_currentFileName];
        }
        
        //проверяем очередь загрузок
        checkQueue();
    }
    
    protected function ioErrorHandler(event:IOErrorEvent):void
    {
        removeHandlers();
        
        trace(event.text);
        
        checkQueue();
    }
    
    protected function removeHandlers():void
    {
        _currentLoader.removeEventListener(Event.COMPLETE, completeHandler);
        _currentLoader.removeEventListener(IOErrorEvent.IO_ERROR, ioErrorHandler);        
    }
    
    protected function checkQueue():void
    {
        //если в очереди есть файлы, то загружаем очередной
        if (_queue.length > 0)
            loadNextFile();
        //либо сбрасываем флаг и создаём событие
        else
        {
            _busy = false;
            _dispatcher.dispatchEvent(new Event(LOADING_COMPLETE));
        }
    }

Теперь чтобы получить загруженный файл по имени, нужно обратиться к хранилищу данных по ключу, в качестве которого это имя используется. Главный класс проекта будет выглядеть следующим образом

    public static function getSource(fileName:String):DisplayObject
    {
        //через статичный метод класса вызываем метод экземпляра
        return _instance.getFile(fileName);
    }
    
    protected function getFile(fileName:String):DisplayObject
    {
        //получаем данные из хранилища
        var data:DisplayObject = _dataStore[fileName];
        
        //если данные существуют, то возвращаем
        if (data)
            return data;
        
        //если нет, то возвращаем null
        trace('Файл "' + fileName + '" не загружен');
        return null;
    }

Теперь осталось протестировать наш класс. Для этого в папке с проектом создадим папку assets и положим туда 4 изображения. Их мы будем загружать. Для 2-го изображения создадим хэндлер, который выполниться сразу после загрузки.

package
{
    import flash.display.*;
    import flash.events.Event;
    
    public class QueueTest extends Sprite
    {
        public function QueueTest()
        {
            //стандартно для проекта Action Script
            if (stage)
                init();
            else
                addEventListener(Event.ADDED_TO_STAGE, init);
        }
        
        protected function init(event:Event = null):void
        {
            //изменяем свойства приложения
            removeEventListener(Event.ADDED_TO_STAGE, init);
            stage.scaleMode = StageScaleMode.NO_SCALE;
            stage.align = StageAlign.TOP_LEFT;
            
            //сразу вешаем слушатель на загрузку всех файлов
            SourceManager.instance.dispatcher.addEventListener(SourceManager.LOADING_COMPLETE, allFilesLoaded);

            //...и загружаем
            SourceManager.loadSource('../assets/pict01.jpg');
            SourceManager.loadSource('../assets/pict02.jpg', file2Loaded); //указываем хэндлер
            SourceManager.loadSource('../assets/pict03.jpg');
            SourceManager.loadSource('../assets/pict04.jpg');
        }
        
        protected function file2Loaded(data:DisplayObject):void
        {
            trace('Файл 2 загружен');
            stage.addChild(data);
        }
        
        protected function allFilesLoaded(event:Event):void
        {
            SourceManager.instance.dispatcher.removeEventListener(SourceManager.LOADING_COMPLETE, allFilesLoaded);
            trace('Все файлы загружены')
            
            testSource();
        }
        
        protected function testSource():void
        {
            var pict:DisplayObject;
            
            //показываем загруженный файл
            pict = SourceManager.getSource('../assets/pict04.jpg');
            if (pict)
            {
                pict.x = 200;
                stage.addChild(pict);
            }
            
            //пытаемся получить не загруженный файл
            pict = SourceManager.getSource('../assets/pict05.jpg');
            if (pict)
            {
                pict.x = 400;
                stage.addChild(pict);
            }
        }
    }
}

Готово. Запускаем проект и видим 2 изображения и 3 сообщения в отладчике. С ними, я думаю, вы разберётесь сами.

Данный проект — всего лишь пример использования очереди. Эту технику можно использовать и при получении данных с сервера, либо при создании диалогов в играх и .т.д. Фантазия подскажет. Удачных проектов…

Результат

Архив с готовым проектом

Тэги: , ,

Ссылка для вашего сайта.



Комментарии (2)

  • Роман

    |

    Интересная статья. У меня только возник вопрос по реализации Singleton.

    Когда то вычитал вот такой способ:

    public function SourceManager(caller:Function = null)
    {
        if (caller != SourceManager.instance)
        {
            throw new Error ("SourceManager is a singleton class, use instance() instead.");
        }
        if (SourceManager.instance != null)
        {
            throw new Error( "Only one SourceManager instance should be instantiated" );
        }
        
        /** Все остальное */
    }
    
    public static function get instance():SourceManager
    {
        if (!_instance)
        {
            _instance = new SourceManager(arguments.calee);
        }
        return _instance;
    }
    

    Вот у меня и возник вопрос, а не будет ли вышеизложенный вариант более корректной реализацией для данного шаблона? Если нет, то почему?

    Ответить

    • Андрей

      |

      Здравствуйте, Роман. Хороший вопрос. На самом деле я взял за основу пример реализации Singleton из книги «Action Script 3.0 Шаблоны проектирования». Но, действительно, сейчас обнаружил небольшую недоработку. О ней чуть ниже.
      Что касается Вашего вопроса. Лично мне вариант с внутренним классом нравится больше, т.к. при использовании конструктора класса SourceManager() возникнет ошибка на этапе компиляции. Конструктор требует параметр singleton:Singleton. А это внутренний класс, экземпляр которого можно создать только в классе SourceManager В Вашем случае ошибка возникнет только на этапе выполнения. Это может создать дополнительные неудобства. Лучше увидеть ошибку сразу, чем когда приложение откомпилировано и загружено на сервер.
      Теперь о доработке моего примера. Очевидно, ошибку компиляции можно обойти, передав в качестве параметра null. Для того, чтобы нельзя было таким образом создать экземпляр класса SourceManager, нужно добавить проверку на null в конструктор. К сожалению, эта ошибка возникнет только на этапе выполнения.

      Ответить

Добавить комментарий

Дополнительно