Utiliser URLLoader et ByteArray pour charger un millier d’images en 2sec

Il peut être long voir très long de charger beaucoup d’images (gif/jpg/png/swf) dans flash … et si je vous disais qu’il existe une méthode pour en charger un millier en moins de 2sec ?

Tout est parti d’une application flash qui devait charger plusieurs centaines de petites images (0,1 à 40Ko), les utilisateurs de cette application n’aimaient pas les longues attentes devant la barre de progression.
Le but de tout ceci fut donc de trouver une solution pour réduire ce temps de chargement.

Pour commencer, j’ai fait différents tests afin d’avoir un temps de référence.
1000 gif sont placés sur ce site (pas le plus rapide) pour un total de 1489213 octets (1454 Ko) .
Mon 1er test utilisait une classe qui me permet de faire du chargement de masse en spécifiant le nombre de connexion, 1 connexion = 1 image téléchargé à la fois.
voici les résultats :
1 connexion -> 432,76 sec
5 connexion -> 89,79 sec
10 connexion -> 46,86 sec
Dépasser les 10 connexions n’est pas recommandé pour la charge des serveurs.

J’ai ensuite utilisé un script PHP (chargé par URLLoader dans flash) qui va lister toutes les images d’un répertoire et renvoyer en une fois toutes les données binaires de ces images.
résultat -> 1,74 sec :-D

Temps des différentes étapes :
chargement du fichier contenant les données binaires -> 1,55 sec
extraction des données binaires + conversion en DisplayObject -> 0,19 sec

La différence de performance est gigantesque (251x plus rapide) et une seule connexion utilisée pour les 1000 images ce qui est préférable pour la charge du serveur.
Le chargement des données binaires oblige à convertir ensuite les données dans flash mais cette étape n’impacte pas les performances.

note : plus le nombre d’images à charger sera important, plus la taille de ces images sera petite et plus cette manière de charger sera intéressante.


Les étapes du script PHP (test3.php) :

  • Récupérer la liste de toutes les images.
  • renvoyer le nombre d’images.
  • Boucle sur les images :
    • renvoyer la taille de l’image.
    • renvoyer les données brut de l’image.

Les étapes dans Flash (aprés le chargement) (ChargementBinaire_test3.as) :

  • lire le nombre d’images au début du fichier (permet de créer une barre de progression et de connaitre à quel moment la conversion est finie).
  • Boucle sur les données binaires restantes :
    • lire la taille de l’image.
    • lire les données binaires qui correspondent à la taille de l’image :
      • Passer ces données binaires a un Loader.loadBytes pour en faire la conversion en un DisplayObject.
      • attendre l’évènement Event.COMPLETE du Loader indiquant une conversion réussie.
      • attendre l’évènement IOErrorEvent.IO_ERROR du Loader indiquant une erreur de conversion.


Les nombres qui sont renvoyés par le script PHP doivent être transformés en données binaires !
Il faut utiliser la fonction pack du PHP pour transformer une valeur en une donnée binaire.
ex :

print(pack('N', 1500));// compacte la valeur 1500 en un entier signé big endian
print(pack('V', 1500));// compacte la valeur 1500 en un entier signé little endian

Pour la différence en little endian et big endian, je vous conseille cette page : Endianness

En PHP pour afficher le contenu d’un fichier il suffit d’utiliser la fonction readfile.
ex :

readfile("cheminImage.jpg");

Pour la conversion des données binaires brut d’une image en un DisplayObject utilisable dans flash, il faut passer par la classe Loader et sa méthode loadBytes mais on a pas le retour immédiatement, il faut enregistrer un écouteur sur l’évènement Event.COMPLETE et IOErrorEvent.IO_ERROR pour vérifier que la conversion s’est bien déroulée ou qu’une erreur de conversion est déclenchée.
C’est vraiment dommage que flash ne puisse pas faire la conversion en direct sans passer par des évènements, je rajoute ce grief a ma liste …

Documentation utile (Français) :
Loader
URLLoader
ByteArray
pack (PHP)
readfile (PHP)

Un autre super livre (en cours d’écriture) de Thibault Imbert sur les bits dont un chapitre parlant de ByteArray est téléchargeable gratuitement : The first bits (Anglais).
Une vidéo intéressante sur l’utilisation de ByteArray pour parser et afficher un BMP : ByteArray Image Decoding (Anglais).


Dans l’archive contenant le projet FlashDevelop, vous trouverez :

  • 3 scripts PHP :
    • test1.php
    • test2.php
    • test3.php
  • 3 scripts AS :
    • ChargementBinaire_test1.as (utilise test1.php)
    • ChargementBinaire_test2.as (utilise test2.php)
    • ChargementBinaire_test3.as (utilise test3.php)
  • 4 images :
    • jpg.jpg
    • png.png
    • swf.swf
    • gif.gif

test1.php renvoie simplement les données d’une image :

<?php
readfile("../img/swf.swf");
?>

test2.php renvoie la taille et les données d’une image :

<?php
$stImg = "../img/png.png";
 
// V = entier signé little endian
// N = entier signé big endian
print(pack('V', filesize($stImg)));
readfile($stImg);
?>

test3.php renvoie le nombre d’images + la taille et les données de chaque image :

<?php
// renvoyer l'extension d'un fichier
function getExt($fichier){
	$tmp = explode(".", $fichier);
	if( count($tmp) == 0 ){
		return "";
	}
	return strtolower($tmp[count($tmp) - 1]);
}
 
 
 
 
// Limiter la recherche des fichiers aux extensions définis (séparés par une virgule).
define("LIMITE_EXT", "gif,png,jpg,jpeg,swf");
$limiteExt = explode(",", strtolower(LIMITE_EXT));
 
 
// Le répertoire a lister
$rep = '../img/';
 
$tbImg = array();
 
 
 
if( !is_dir($rep) || !is_readable($rep) ){
	exit("erreur");
}
 
// boucle d'exploration du répertoire
$rsDir = opendir($rep) or exit();
while( false !== ($fichier = readdir($rsDir)) ){
    $cheminFichier = $rep . $fichier;
	if( !is_file($cheminFichier) || !is_readable($cheminFichier) ){
		continue;
	}
	if( LIMITE_EXT && !in_array(getExt($cheminFichier), $limiteExt) ){
		continue;
	}
	array_push($tbImg, $cheminFichier);
}
closedir($rsDir);
 
 
// boucle sur le tableau des images qui va renvoyer la taille et les données
// V = entier signé little endian
// N = entier signé big endian
print(pack('N', count($tbImg)));// nombre d'images
foreach($tbImg as $img){
	print(pack('N', filesize($img)));// taille de l'image
	readfile($img);// données de l'image
}
?>

attention : dans test2.php j’utilise « little endian » et dans test3.php j’utilise « big endian » pour compacter les entiers !
Vous pouvez utiliser l’un ou l’autre à condition de bien penser à l’indiquer au ByteArray qui recevra les données … si vous faites une erreur à ce niveau, vous aurez des résulats qui finiront la plupart du temps par une erreur EOFError.
Il est d’ailleurs grandement préférable de protéger les méthodes de lecture (readInt, readBoolean, ..) de la classe ByteArray par un try/catch afin de gérer l’erreur (chose que je n’ai pas fait pour ces tests).

ChargementBinaire_test1.as :

package {
	import flash.display.Loader;
	import flash.display.Sprite;
	import flash.errors.IllegalOperationError;
	import flash.events.Event;
	import flash.events.IOErrorEvent;
	import flash.events.SecurityErrorEvent;
	import flash.net.URLLoader;
	import flash.net.URLLoaderDataFormat;
	import flash.net.URLRequest;
	import flash.utils.ByteArray;
 
 
	[SWF(width = "800", height = "600", backgroundColor = "#0", frameRate = "60")]
 
	/**
	 * @author Lorenzo
	 * 
	 * chargement des données binaires d'une image avec un URLLoader et
	 * conversion en un DisplayObject.
	 * Ajout du DisplayObject sur la scene.
	 */
	public class ChargementBinaire_test1 extends Sprite {
 
 
		private var _php:String = "http://localhost/_progFlash/_flashdevelop/ChargementImagesParURLLoader/bin/php/test1.php?x="+Math.random();
		private var _loadImg:URLLoader;
 
 
		public function ChargementBinaire_test1():void {
			if (stage) init();
			else addEventListener(Event.ADDED_TO_STAGE, init);
		}
 
		private function init(e:Event = null):void {
			removeEventListener(Event.ADDED_TO_STAGE, init);
			// entry point
 
			stage.scaleMode = "noScale";
			stage.align = "TL";
 
			_loadImg = new URLLoader();
			_loadImg.dataFormat = URLLoaderDataFormat.BINARY;
			_loadImg.addEventListener(Event.COMPLETE, evtLoadImgComplete);
			_loadImg.addEventListener(IOErrorEvent.IO_ERROR, evtLoadImgError);
			_loadImg.addEventListener(SecurityErrorEvent.SECURITY_ERROR, evtLoadImgError);
			_loadImg.load(new URLRequest(_php));
		}
 
 
 
		// -----------------------------------------------------------------------------------
		// EVENEMENTS
		// -----------------------------------------------------------------------------------
 
		// ---------------------- URLLoader ----------------------
		private function evtLoadImgError(ev:Event):void {
			trace("evtLoadImgError");
			// erreur de chargement du fichier contenant les données binaires
		}
 
		private function evtLoadImgComplete(ev:Event):void {
			trace("evtLoadImgComplete");
			// chargement du fichier contenant les données binaires réussi
 
			var baJpg:ByteArray = _loadImg.data;
			trace("baJpg.length = ", baJpg.length);
 
			// -------------------------------------
			var conversion:Loader = new Loader();
			conversion.contentLoaderInfo.addEventListener(Event.COMPLETE, evtConversionComplete);
			conversion.contentLoaderInfo.addEventListener(IOErrorEvent.IO_ERROR, evtConversionIOError);
 
			try{
				conversion.loadBytes(baJpg);
			}catch (er:ArgumentError) {
				trace("ArgumentError : "+er.message);
			}catch (er:IllegalOperationError) {
				trace("IllegalOperationError : "+er.message);
			}catch (er:SecurityError) {
				trace("SecurityError : "+er.message);
			}
		}
 
		// ---------------------- LOADER ----------------------
		private function evtConversionIOError(ev:IOErrorEvent):void {
			// déclenché si les données binaires ne peuvent pas être recomposé
			// dans un des types supportés par flash (gif/png/jpg/swf)
			trace("evtConversionIOError : " + ev.text);
		}
 
		private function evtConversionComplete(ev:Event):void {
			// conversion des données binaires en un DisplayObject réussi
			this.addChild(ev.currentTarget.loader.content);
		}
 
	}
 
}

ChargementBinaire_test2.as :

package {
	import flash.display.Loader;
	import flash.display.Sprite;
	import flash.errors.IllegalOperationError;
	import flash.events.Event;
	import flash.events.IOErrorEvent;
	import flash.events.SecurityErrorEvent;
	import flash.net.URLLoader;
	import flash.net.URLLoaderDataFormat;
	import flash.net.URLRequest;
	import flash.utils.ByteArray;
	import flash.utils.Endian;
 
 
	[SWF(width = "800", height = "600", backgroundColor = "#454338", frameRate = "60")]
 
	/**
	 * chargement de données binaires avec un URLLoader :
	 * - poids de l'image en binaire
	 * - données binaires de l'image
	 * 
	 * conversion en un DisplayObject.
	 * Ajout du DisplayObject sur la scene.
	 * 
	 * @author Lorenzo
	 */
	public class ChargementBinaire_test2 extends Sprite {
 
		private var _php:String = "http://localhost/_progFlash/_flashdevelop/ChargementImagesParURLLoader/bin/php/test2.php?x="+Math.random();
		private var _loadImg:URLLoader;
 
		public function ChargementBinaire_test2():void {
			if (stage) init();
			else addEventListener(Event.ADDED_TO_STAGE, init);
		}
 
		private function init(e:Event = null):void {
			removeEventListener(Event.ADDED_TO_STAGE, init);
			// entry point
 
			stage.scaleMode = "noScale";
			stage.align = "TL";
 
			_loadImg = new URLLoader();
			_loadImg.dataFormat = URLLoaderDataFormat.BINARY;
			_loadImg.addEventListener(Event.COMPLETE, evtLoadImgComplete);
			_loadImg.addEventListener(IOErrorEvent.IO_ERROR, evtLoadImgError);
			_loadImg.addEventListener(SecurityErrorEvent.SECURITY_ERROR, evtLoadImgError);
			_loadImg.load(new URLRequest(_php));
		}
 
 
 
		// -----------------------------------------------------------------------------------
		// EVENEMENTS
		// -----------------------------------------------------------------------------------
 
		// ---------------------- URLLoader ----------------------
		private function evtLoadImgError(ev:Event):void {
			trace("evtLoadImgError");
			// erreur de chargement du fichier contenant les données binaires
		}
 
		private function evtLoadImgComplete(ev:Event):void {
			trace("evtLoadImgComplete");
			// chargement du fichier contenant les données binaires réussi
 
			var ba:ByteArray = _loadImg.data;
			// définition de la position du bit de poids fort
			// http://fr.wikipedia.org/wiki/Endianness
			ba.endian = Endian.LITTLE_ENDIAN;
 
			// lecture du poids de l'image
			var jpgLength:uint = ba.readUnsignedInt();
 
			// ecriture des données binaires restantes dans un ByteArray
			var baJpg:ByteArray = new ByteArray();
			baJpg.writeBytes(ba, ba.position, jpgLength);
			trace("baJpg.length = ", baJpg.length);
 
			// -------------------------------------
			var conversion:Loader = new Loader();
			conversion.contentLoaderInfo.addEventListener(Event.COMPLETE, evtConversionComplete);
			conversion.contentLoaderInfo.addEventListener(IOErrorEvent.IO_ERROR, evtConversionIOError);
 
			try{
				conversion.loadBytes(baJpg);
			}catch (er:ArgumentError) {
				trace("ArgumentError : "+er.message);
			}catch (er:IllegalOperationError) {
				trace("IllegalOperationError : "+er.message);
			}catch (er:SecurityError) {
				trace("SecurityError : "+er.message);
			}
		}
 
		// ---------------------- LOADER ----------------------
		private function evtConversionIOError(ev:IOErrorEvent):void {
			trace("evtConversionIOError : " + ev.text);
			// déclenché si les données binaires ne peuvent pas être recomposé
			// dans un des types supportés par flash (gif/png/jpg/swf)
		}
 
		private function evtConversionComplete(ev:Event):void {
			// conversion des données binaires en un DisplayObject réussi
			this.addChild(ev.currentTarget.loader.content);
		}
	}
 
}

ChargementBinaire_test3.as :

package {
	import flash.display.DisplayObject;
	import flash.display.Loader;
	import flash.display.Sprite;
	import flash.errors.IllegalOperationError;
	import flash.events.Event;
	import flash.events.IOErrorEvent;
	import flash.events.SecurityErrorEvent;
	import flash.net.URLLoader;
	import flash.net.URLLoaderDataFormat;
	import flash.net.URLRequest;
	import flash.utils.ByteArray;
	import flash.utils.Endian;
 
 
	[SWF(width = "800", height = "600", backgroundColor = "#454338", frameRate = "60")]
 
	/**
	 * chargement des données binaires représentant des images (gif/jpg/png/swf) d'un script PHP qui
	 * place les données dans un ordre précis :
	 * -nombre d'images (apparait qu'une seule fois)
	 * -poids de l'image
	 * -données de l'image
	 * 
	 * 
	 * conversion en DisplayObject.
	 * Ajout des DisplayObject sur la scene.
	 * 
	 * @author Lorenzo
	 */
	public class ChargementBinaire_test3 extends Sprite {
 
		private var _totalImg:uint = 0;
		private var _tbImg:Array = new Array();
		private var _posX:uint = 0;
		private var _posY:uint = 0;
		private var _time:int = 0;
		private var _php:String = "http://localhost/_progFlash/_flashdevelop/ChargementImagesParURLLoader/bin/php/test3.php?x="+Math.random();
 
 
 
		public function ChargementBinaire_test3():void {
			if (stage) init();
			else addEventListener(Event.ADDED_TO_STAGE, init);
		}
 
		private function init(e:Event = null):void {
			removeEventListener(Event.ADDED_TO_STAGE, init);
			// entry point
 
			stage.scaleMode = "noScale";
			stage.align = "TL";
 
			var loadGroupImg:URLLoader = new URLLoader();
			loadGroupImg.dataFormat = URLLoaderDataFormat.BINARY;
			loadGroupImg.addEventListener(Event.COMPLETE, evtLoadGroupImgComplete);
			loadGroupImg.addEventListener(IOErrorEvent.IO_ERROR, evtLoadGroupImgError);
			loadGroupImg.addEventListener(SecurityErrorEvent.SECURITY_ERROR, evtLoadGroupImgError);
			loadGroupImg.load(new URLRequest(_php));
 
			stage.addEventListener(Event.RESIZE, evtStageResize);
		}
 
 
		// -----------------------------------------------------------------------------------
		// EVENEMENTS
		// -----------------------------------------------------------------------------------
 
 
		private function evtStageResize(e:Event):void {
			if ( _tbImg.length && _totalImg == _tbImg.length ) {
				while (this.numChildren > 0 ) {
					this.removeChildAt(0);
				}
				_posX = 0;
				_posY = 0;
				for (var i:int = 0; i < _totalImg; i++) {
					var dob:DisplayObject = _tbImg[i] as DisplayObject;
					dob.x = _posX;
					dob.y = _posY;
					this.addChild(dob);
					_posX += dob.width;
					if ( (_posX + dob.width) > stage.stageWidth ) {
						_posX = 0;
						_posY += dob.height;
					}
				}
			}
		}
 
		// ---------------------- URLLoader ----------------------
		private function evtLoadGroupImgError(ev:Event):void {
			trace("evtLoadGroupImgError");
			// erreur de chargement du fichier contenant les données binaires
		}
 
		private function evtLoadImgComplete(ev:Event):void {
			trace("evtLoadImgComplete");
			// chargement du fichier contenant les données binaires réussi
 
			var loadGroupImg:URLLoader = (ev.currentTarget as URLLoader);
			var ba:ByteArray = loadGroupImg.data;
			// définition de la position du bit de poids fort
			// http://fr.wikipedia.org/wiki/Endianness
			ba.endian = Endian.BIG_ENDIAN;
 
			// lecture du nombre total d'images
			_totalImg = ba.readUnsignedInt();
			trace("_totalImg", _totalImg);
 
 
			while (ba.position < ba.length) {
				// lecture du poids de l'image courante
				var jpgLength:uint = ba.readUnsignedInt();
 
				// ecriture des données binaires de l'image courante dans un ByteArray
				var baJpg:ByteArray = new ByteArray();
				baJpg.writeBytes(ba, ba.position, jpgLength);
				ba.position += jpgLength;
 
				// -------------------------------------
				var conversion:Loader = new Loader();
				conversion.contentLoaderInfo.addEventListener(Event.COMPLETE, evtConversionComplete);
				conversion.contentLoaderInfo.addEventListener(IOErrorEvent.IO_ERROR, evtConversionIOError);
 
				try{
					conversion.loadBytes(baJpg);
				}catch (er:ArgumentError) {
					trace("ArgumentError : "+er.message);
				}catch (er:IllegalOperationError) {
					trace("IllegalOperationError : "+er.message);
				}catch (er:SecurityError) {
					trace("SecurityError : "+er.message);
				}
			}
 
			// -------------------------------------
			loadGroupImg.data = null;
			loadGroupImg.removeEventListener(Event.COMPLETE, evtLoadGroupImgComplete);
			loadGroupImg.removeEventListener(IOErrorEvent.IO_ERROR, evtLoadGroupImgError);
			loadGroupImg.removeEventListener(SecurityErrorEvent.SECURITY_ERROR, evtLoadGroupImgError);
			loadGroupImg = null;
		}
 
		// ---------------------- LOADER - conversion ----------------------
		private function evtConversionIOError(ev:IOErrorEvent):void {
			trace("evtConversionIOError : " + ev.text);
			// déclenché si les données binaires ne peuvent pas être recomposé
			// dans un des types supportés par flash (gif/png/jpg/swf)
			var conversion:Loader = ev.currentTarget.loader as Loader;
			conversion.contentLoaderInfo.removeEventListener(Event.COMPLETE, evtConversionComplete);
			conversion.contentLoaderInfo.removeEventListener(IOErrorEvent.IO_ERROR, evtConversionIOError);
			conversion = null;
		}
 
		private function evtConversionComplete(ev:Event):void {
			// conversion des données binaires en un DisplayObject réussi
			var conversion:Loader = ev.currentTarget.loader as Loader;
			var dob:DisplayObject = conversion.content as DisplayObject;
			dob.x = _posX;
			dob.y = _posY;
			this.addChild(dob);
			_tbImg.push(dob);
			_posX += dob.width;
			if ( (_posX + dob.width) > stage.stageWidth ) {
				_posX = 0;
				_posY += dob.height;
			}
			conversion.contentLoaderInfo.removeEventListener(Event.COMPLETE, evtConversionComplete);
			conversion.contentLoaderInfo.removeEventListener(IOErrorEvent.IO_ERROR, evtConversionIOError);
			conversion = null;
		}
 
	}
 
}


Voici le lien pour télécharger l’archive contenant tous les fichiers : ChargementImagesParURLLoader (Projet FlashDevelop)
Pour faire fonctionner le projet vous aurez besoin d’un serveur supportant le PHP.
N’oubliez pas de changer les URLs pointant vers les scripts PHP.
Pour changer le répertoire source (dans test3.php) dont il faut renvoyer toutes les images, il vous suffit de modifier la valeur de $rep (ligne 19).


Laisser un commentaire

*