بهینه‌سازی در AS3: پاسخدهی به رویدادها

اصولاً، AS3.0 یکی از زبان‌هایی است که تفاوت زیادی میان برنامه‌نویسان عادی و با تجربه، قائل می‌شود. چگونه؟ با کاهش کارایی!… چرا؟ از پدرش java به ارث برده است!

به دلیل شباهت زیادی که در ساختار این زبان با java وجود دارد، برنامه‌نویسان AS3.0 به مرور زمان، تمایل زیادی به برنامه‌نویسی ساخت یافته و لایه‌بندی شده پیدا می‌کنند. شئ‌گرایی بی حد و مرز باعث می‌شود که مفاهیم مخفی‌سازی (بخوانید Encapsulation) در تمام اجزای برنامه به وضوح دیده شود. تعریف انواع و اقسام Interface ها، کلاس‌های ایستا و پویا، ارث‌بری و اشیاء متنوع، به خودی خود، سبک برنامه‌نویسیان AS3.0 را به طراحی لایه‌های مستقل نزدیک می‌کند.

اما، خطری که همیشه در کمین این نوع از برنامه‌نویسی منطقی وجود دارد، کاهش کارایی در مدیریت حافظه است.

استفاده زیاد از الگوهای طراحی مانند Singleton برای بهینه‌سازی مصرف حافظه (هرچند که هدف اصلی آن نبوده است) و Proxy یا Facade جهت ساده‌سازی روابط بین کلاس‌ها و اشیاء در محیط‌های ManagedCode مانند Flash، گویای این مسئله است که گاهی، پس از طراحی منطقی کلاس‌ها و سازماندهی لایه‌ها، به میزان کمی از بی‌نظمی با هدف افزایش کارایی نیازمندیم.

از این پس، سعی می‌کنم در سلسله پست‌های بهینه‌سازی در کارایی، به طرح مواردی مانند نکات برنامه‌نویسی بهینه و حتی الگوهای طراحی سودمند در این رابطه بپردازم.

flash_logo

در این مطلب به یکی از مهمترین منابع کاهش کارایی که همان EventHandler ها هستند پرداخته می‌شود.

در زبان AS3.0، قدرت شئ‌گرایی java به همراه قدرت پردازش بصری محصولات شرکت Adobe در یک جا جمع می‌شوند.
نتیجه این می‌شود که کلیه ترسیمات و اشکال گرافیکی را به سادگی، در قالب اشیاء تعاملی (Interactive Objects) تعریف نموده و بارها از آنها استفاده می‌کنیم. تعریف ساده انواع کامپاننت‌های گرافیکی داده‌ای مانند List، Grid و Combo همگی از این دست هستند.

اما از آنجا که این اشیاء، بار تعاملی بودن را نیز به دوش می‌کشند، لاجرم به رویدادها نیز باید پاسخ دهند. اینجا است که بهینه‌سازی حرف اول را می‌زند. یک برنامه کاملا اصولی و منطقی نوشته شده، ممکن است فاقد کارایی لازم باشد.

اجازه دهید با چند مثال این موضوع را شرح دهیم.
فرض کنید برنامه‌ای طراحی کرده‌اید که در آن از تعدادی بیش از معمول، اشیاء تعاملی مختلف استفاده شده است. نمی‌توان برای عبارت “تعداد بیش از معمول” عدد مشخصی را ذکر نمود. چرا که کاهش کارایی، دقیقاً به قدرت سخت‌افزاری بستر اجرا کننده flash وابسته است. طبیعتاً این عدد برای یک برنامه Desktop با برنامه Mobile متفاوت است.

در حالت عادی با استفاده از دستور addEventListener به ازای هر شئ تعاملی، یک تابع پاسخگوی مشخص جهت یک رویداد مشخص ثبت می‌گردد. به عنوان مثال:

var sp:Sprite = new Sprite();
sp.addEventListener(Event.RENDER, onRender);
sp.addEventListener(Event.ACTIVATE, onActivate);
sp.addEventListener(MouseEvent.CLICK, onClick);
sp.addEventListener(MouseEvent.DOUBLE_CLICK, onDblClick);
sp.addEventListener(MouseEvent.MOUSE_OVER, onOver);

function onRender(e:Event):void{
//process event
}

function onActivate(e:Event):void{
//process event
}

function onClick(e:MouseEvent):void{
//process event
}

function onDblClick(e:MouseEvent):void{
//process event
}

function onOver(e:MouseEvent):void{
//process event
}

خوب، این روش در کنار منظم بودن، کمترین میزان کارایی را نیز به دنبال دارد. به ازای یک شئ، 5 Listener با 5 پاسخگوی مختلف ثبت شده است. اگر تعداد اشیاء از این دست زیاد باشد، چنین روشی بهینه نیست.

راه حل اول، تجمیع توابع پاسخگو به ازای نوع رویداد است. به عنوان مثال می‌توان هر کدام رویدادهای نوع Event و MouseEvent را به یک توابع پاسخگوی واحد ارسال نمود. در این صورت باید در تابع مربوطه به تشخیص نوع دقیق رویداد پرداخته شود. به عنوان مثال:

var sp:Sprite = new Sprite();
sp.addEventListener(Event.RENDER, onEvent);
sp.addEventListener(Event.ACTIVATE, onEvent);
sp.addEventListener(MouseEvent.CLICK, onMouseEvent);
sp.addEventListener(MouseEvent.DOUBLE_CLICK, onMouseEvent);
sp.addEventListener(MouseEvent.MOUSE_OVER, onMouseEvent);

function onEvent (e:Event):void{
  if (e.type == Event.RENDER){
    //process event
  }else if (e.type == Event. ACTIVATE){
    //process event
  }
}

function onMouseEvent (e:MouseEvent):void{
  if (e.type == MouseEvent.CLICK){
    //process event
  }else if (e.type == MouseEvent.DOUBLE_CLICK){
    //process event
  }else if (e.type == MouseEvent.MOUSE_OVER){
    //process event
  }
}

خوب، این روش تا حد زیادی در حجم کد کامپایل شده و همچنین تا حدودی در مصرف حافظه صرفه جویی می‌کند. در رابطه با اضافه شدن if-else ها نیز جای نگرانی وجود ندارد. این همان بی نظمی کوچکی است که برای بهینه سازی برنامه پذیرفته می‌شود. بار اجرایی آن هم در مجموع، بسیار کمتر از حالت قبل خواهد بود.

خوب این راه‌حل تنها برای یک شئ بیان شد، اگر تعداد اشیاء زیاد باشد، باید پس از تشخیص نوع رویداد، شئ ارسال کننده رویداد را نیز بررسی کنیم. به عنوان مثال:

var s:Sprite;
for (var i:uint = 0; i<100; i++) {
   s = new Sprite();
   s.name = "sp" + i.toString();
   initEvents(s);
}

function initEvents(sp:Sprite):void {
   sp.addEventListener(MouseEvent.CLICK, onMouseEvent);
   sp.addEventListener(MouseEvent.DOUBLE_CLICK, onMouseEvent);
   sp.addEventListener(MouseEvent.MOUSE_OVER, onMouseEvent);
}

function onMouseEvent(e:MouseEvent):void {
   if (e.type == MouseEvent.CLICK) {
      trace(e.currentTarget.name);
   }
   else if (e.type == MouseEvent.DOUBLE_CLICK) {
      trace(e.currentTarget.name);
   }
   else if (e.type == MouseEvent.MOUSE_OVER) {
      trace(e.currentTarget.name);
   }
}

معمولاً زمانی که چنین اشیائی را به تعداد زیاد ایجاد می‌کنیم، وجه تمایزی در نوع عملکرد آنها وجود ندارد. تنها اندیس یا نام شئ است که باعث تفاوت در عملکرد می‌شود. به عنوان مثال سلول‌های یک شئ List از این نوع هستند.

همچنین از آنجا که رویدادهای AS3 نیز همگی شئ هستند، از قوانین توارث پیروی می‌کنند. به همین دلیل هر شئ رویداد، دارای یک شئ والد می‌باشد. در نتیجه می‌توان توابع پاسخگوی نوع رویدادهای متفاوت را نیز حذف نموده و یک تابع جهت پاسخگویی به رویداد والد مشترک آنها ایجاد نمود.
در مثال فوق، MouseEvent از نوع Event است. پس می‌توان فقط یک تابع جهت پاسخگویی به Event ایجاد نمود که برای هر دو حالت قابل استفاده می‌باشد.

در صورتی که تعداد اشیاء کم بوده ولی دفعات ساخت و حذف آنها زیاد باشد، بهتر است به جای مقایسه نام شئ، از مرجع شئ استفاده شود. دسترسی به خاصیت name از صفات شئ به منظور مقایسه، زمان بیشتری را نسبت به مقایسه شئ و مرجع آن طلب می‌کند. به عنوان مثال:

if(e.currentTarget.name == "nameOfObject"){
    //process event			
}

if (e.currentTarget == sp1){
    //process event			
}

زمان اجرای تابع دوم، حداقل 60 درصد کمتر از تابع اول است. به دلیل مرجع بودن اشیاء در AS3، مقایسه دو شئ مانند مقایسه دو عدد اشاره‌گر می‌باشد.

در مثال‌هایی که تا کنون ذکر شد بهینه‌سایز در سمت تابع پاسخگو انجام شد. اما بخش مهمی از این ماجرا به سمت زنجیره گوش فرادهندگان رویداد یا Listener Chain بستگی دارد.

به دلیل ساختاز سلسله مراتبی اشیاء، همیشه یک شئ والد وجود دارد. به همین دلیل در بیشتر موارد می‌توان به جای تک تک فرزندان، یک رویداد برای والد آنها ثبت نمود. البته این روش به نوع رویداد بستگی دارد. در رویدادهای بصری، اجرای یک رویداد به فرزندان آن هم منتقل می‌گردد. به عنوان مثال:

var main:Sprite = new Sprite();
var child:Sprite;

for (var i:uint = 0; i<100; i++) {
   child = new Sprite();
   child.name = "child" + i.toString();
   main.addChild(child);
}

main.addEventListener(MouseEvent.CLICK, onMouseEvent);
main.addEventListener(MouseEvent.DOUBLE_CLICK, onMouseEvent);
main.addEventListener(MouseEvent.MOUSE_OVER, onMouseEvent);

function onMouseEvent(e:MouseEvent):void {
   trace(e.type, e.target);
}

در این حالت صفت target، به شئ فرزند اشاره می‌کند که می‌توان بر اساس نام یا مرجع آن عملکرد مناسب را اتخاذ نمود.

این پایان کار نیست. روش‌های فوق هم در برخی از حالات خاص، کارایی مناسبی ندارند. به عنوان مثال اگر تعداد و حجم اشیاء هر دو زیاد باشد (مثلا سلول‌های گرافیکی و تصویری در List یا Grid یا اشیاء ComponentGrid) اطلاع رسانی سلسله مراتبی (از والد به فرزند) در رابطه با رویدادها زمان زیادی طول خواهد کشید.

طبیعتاً غلبه بر مشکلات خاص، راه‌حل‌های خاص نیاز دارد. در این حالت می‌توان بر نکات ایجاد تمایز بین اشیاء متمرکز شد. به عنوان مثال در یک شئ لیست، ابعاد سلول‌ها یکسان و مختصات آنها متفاوت است. در نتیجه برای پاسخگویی به رویدادی مانند Click، با بررسی مختصات نقطه کلیک شده در صفحه و مختصات شئ لیست و میزان scroll آن، پی برد که مختصات کلیک شده در کدام سلول قرار گرفته است.

به عنوان مثال:

var list: List = new List();
const cellHeight:uint = 100;

list.mouseEnabled = list.mouseChildren = false;

stage.addEventListener(MouseEvent.CLICK, onMouseEvent);

function onMouseEvent(e:MouseEvent):void {
   var c:cell;
   var i:uint = Math.ceil((e.stageY - list.y + list.scrollY) / cellHeight);

   c = list.getChildByName("cell" + i.toString());
   //do event on c
}

در این روش به دلیل عدم ثبت رویداد برای اشیاء لیست و سلول‌های آن کارایی تا حد زیادی افزایش می‌یابد.

در بخش آینده به بررسی callBack، promise و کتابخانه ارزشمند AS3Signals برای بهینه‌سازی رویدادهای فلش می‌پردازم.

About محمد شمس

برنامه‌نویس، طراح انیمیشن و علاقمند به هوش مصنوعی

پاسخ دهید