بهینه‌سازی (۱) – اجرای غیرهمزمان حلقه

yaa110

کاربر فعال
یکی از مهمترین مسائلی که توسعه دهندگان Flash و Air با آن سر و کار دارند، مسئله بهینه سازی پروژه به منظور افزایش کارایی اپلیکیشن می باشد. عدم توجه به مسئله بهینه سازی در توسعه یک اپلیکیشن منجر به افت شدید کارایی نرم افزار و بنابراین افزایش حافظه هدررفته سیستم می شود که این امر موجب کنار گذاشته شدن اپلیکیشن توسط کاربران آن خواهد شد. برای مثال دو اپلیکیشن کاملا مشابه را درنظر بگیرید که توسط دو توسعه دهنده گسترش یافته اند به طوری که در یکی کدها بهینه شده اند ولی در دیگری تنها یک سری کد پشت سرهم نوشته شده است. در این حالت هر دو اپلیکیشن یک خروجی مشابه خواهند داشت ولی مطمئنا کاربران اپلیکیشنی را انتخاب خواهند کرد که کارایی بالاتری دارد و بهینه شده است. برای توسعه دهندگان اپلیکیشن، توجه به این نکته الزامی است که کاربران در هنگام پردازش عملیات های سنگین، مشاهده یک Loading متحرک را به هنگ کردن سیستمشان ترجیح خواهند داد، حتی اگر عملیات پردازش در حالت هنگ سریعتر انجام شود.

در باب بهینه سازی کدهای Actionscript و Air مقالات زیادی نوشته و همچنین کنفرانس های بسیاری برگزار شده است که نشاندهنده چالش برانگیز بودن و به عبارتی حیاتی بودن این امر در زمینه توسعه یک اپلیکیشن می باشد. در میان توسعه دهنگان فارسی زبان نیز افراد معدودی همچون دوست خوبم بهروز پولادرگ مطالب مفیدی را در وبلاگشان منتشر کرده اند. اما با توجه به اهمیت این مبحث بر آن شدم تا با تهیه و تدوین مجموعه ای از مقالات در زمینه بهینه سازی کدنویسی Actionscript و Air قدمی هرچند کوچک برای ارتقای عمومی اپلیکیشن های توسعه افته فارسی زبان بردارم. در این درس که اولین بخش از مجموعه بهینه سازی است، به معرفی نحوه اجرای کد در Flash Player و Air Debugger پرداخته خواهد شد و سپس روشی برای حل مشکل هنگ کردن سیستم در حلقه های طولانی حاوی عملیات های سنگین ارائه می شود.


  • اصول ابتدایی اجرای کد در حالت Runtime
اولین قدم در بحث بهینه سازی کدنویسی در Actionscript و Air شناخت نحوه اجرای و پردازش کدها توسط Flash Player و Air Debugger می باشد. عملیات پردازش کدها در هر فریم به صورت متناوب و در یک حلقه صورت می پذیرد. طبق این تعریف، فریم عبارت است از مقطعی از زمان که طول آن توسط مقدار fps (تعداد فریم در یک ثانیه) تعریف می شود. برای مثال درصورتی که مقدار fps برابر با 24 باشد، آنگاه طول هر فریم برابر با یک بیست و چهارم ثانیه (حدود 42 میلی ثانیه) خواهد شد. مقدار fps در نرم افزار Flash به راحتی در پنل Timeline قابل دسترسی و تغییر است. همچنین به کمک کد زیر در کلاسه اصلی پروژه نیز می توان مقدار fps اولیه را تعیین نمود.

[درصورتی که در دیدن کدها مشکل دارید به لینک منبع مراجعه کنید]

کد:
[COLOR=#333333][FONT=Courier 10 Pitch][SWF(frameRate="24")][/FONT][/COLOR]


هر حلقه فریم شامل دو فاز می شود که عبارتند از فاز اجرای رویدادها (events) و فاز پردازش تصویر (render) فریم. فاز اول را می توان متشکل از دو بخش دانست، بخش اول منحصرا رویداد مربوط به enterFrame است که همواره یکبار در ابتدای فریم ارسال (dispatch) می شود (حتی اگر Listener آن تعریف نشده باشد) و بخش دوم شامل تمامی رویدادهایی می شود که توسط کدنویس یک Listener برایشان تعریف شده است و باید در ابتدای فریم ارسال شوند (مانند رویداد مربوط به پاسخ شبکه بعد از بارگزاری اطلاعات و یا رویدادهای وردودی توسط کاربر). درصورتی که کدنویس هیچ رویدادی را (در بخش دوم) تعریف نکرده باشد، زمان پردازش فریم کوتاهتر نخواهد شد و Actionscript Virtual Machine (AVM) به صورت بیکار خواهد ماند تا زمان پردازش تصویر فرا برسد. AVM بیانگر بخشی از حافظه حفاظت شده سیستم است که عملیات پردازش کد (Actionscript 2.0 یا Actionscript 3.0) در Flash Player یا Air Debugger در آن صورت می گیرد. بعبارت دیگر Frame rate هیجگاه بعلت نبود عملیات برای اجرا، سریعتر نخواهد شد. بعد از ارسال تمامی رویدادها، عملیات پردازش تصویر (فاز دوم در حلقه فریم) آغاز می شود. در این فاز ابتدا حالت نمایش یا عدم نمایش یک شی (Visibility) بررسی می شود و سپس عملیات ترسیم اشیایی که مقدار visible آنها true است، انجام می شود. باید به این نکته توجه کرد، زمانی که مقدار Alpha برای یک شی صفر باشد با حالتی که مقدار visible آن شی false است، تفاوت دارد و عملیات ترسیم برای شی با alpha صفر هم انجام خواهد شد. پس هرگز به جای false کردن مشخصه visible یک شی، Alpha آن شی را صفر نکنید.

توجه: در رویدادهایی که شامل مشخصه updateAfterEvent هستند، عملیات پردازش تصویر را می توان مجبور به اجرا قبل از شروع فاز دوم کرد. این امر می تواند منجر به کاهش کارایی پروژه شود، بنابراین از استفاده بی دلیل updateAfterEvent جدا پرهیز نمایید.

در یک نگاه ساده انگارانه می توان فرض کرد که فاز اول و دوم در یک حلقه فریم در زمان های مشابه انجام می شوند (برای مثال در :فپس: برابر با 24 مقدار طول هر فاز حدود 21 میلی ثانیه). اما در واقعیت زمان لازم برای اجرای هر فاز با یکدیگر متفاوت هستند. گاهی زمان لازم برای فاز اول افزایش می یابد که منجر به کاهش سهم فاز دوم (پردازش تصویر) می شود. همچنین در برخی حالات بویژه حالاتی که از filter ها و blend mode ها استفاده می شود، عملیات پردازش تصویر طولانی می شود و سهمی بیشتر از نصف طول فریم برای فاز دوم نیاز خواهد بود. درنتیجه در شرایطی که هر دو فاز طولانی هستند، runtime قادر به نگهداری از :فپس: نخواهد بود و بنابراین طول هر فریم از مقدار مجاز آن بیشتر خواهد شد که منجر به بروز تاخیر در اجرای فریم بعدی می شود. در بدترین حالت، بروز این اتفاق می تواند منجر به فریز شدن اپلیکیشن و هنگ کردن سیستم شود. در شکل زیر تصویر 1، نحوه تقسیم بندی یک فریم به فازهای مختلف دیده می شود.

general_frame_division.png

شکل 1- تقسیم فریم به دو فاز پردازش کد و تصویر. در عمل یک سری زمان های بیکاری بعد از هر پردازش کد وجود دارد.

گاهی اوقات، زمان پردازش فریم آن قدر طولانی می شود که Flash Player یا Air Debugger از کار می افتند و پیام خطای Timeout مشاهده خواهد شد (شکل 2). برای رفع این مشکل لازم است که عملیات های طولانی شکسته شوند (یعنی در چند حلقه فریم) انجام شوند.

timeout_dialog.png
شکل 2- پیام خطای Timeout


  • تقسیم عملیات های طولانی حلقه به چند قسمت کوچکتر (Chunk)
در این قسمت با ذکر یک مثال، روش اجرای غیرهمزمان حلقه آموزش داده می شود. یک حلقه for طولانی (برای مثال شامل 1000 مرحله) را درنظر بگیرید به طوری که هر مرحله شامل یک عملیات پردازش زمانبر (مثل اضافه کردن اشیا به stage) باشد.

[درصورتی که در دیدن کدها مشکل دارید به لینک منبع مراجعه کنید]

کد:
[COLOR=#333333][FONT=Courier 10 Pitch]for (var i:int = 0; i < 1000; i++) {[/FONT][/COLOR]

    trace(i); [COLOR=#333333][FONT=Courier 10 Pitch]}[/FONT][/COLOR]


در این حالت فاز اول زمانی خاتمه می یابد که تمامی مراحل حلقه for اجرا شده باشد. سپس در فاز دوم، عملیات trace (و یا عملیات اضافه کردن اشیا به stage و پردازش تصویر) اجرا می شود. بدیهی که اجرای این حلقه for طولانی در حالت عادی منجر به هنگ کردن موقت اپلیکیشن خواهد شد. برای رفع این مشکل باید حلقه را به صورت غیرهمزمان اجرا کنیم. مثال زیر را در نظر بگیرید:

[درصورتی که در دیدن کدها مشکل دارید به لینک منبع مراجعه کنید]

کد:
[COLOR=#333333][FONT=Courier 10 Pitch]import flash.events.Event;[/FONT][/COLOR]

import flash.text.TextField;var savedIndex = 0;var breakSetp = 20;addEventListener(Event.ENTER_FRAME, onEnterFrame);function onEnterFrame(e:Event) {    var i:int;    var max:int = 1000;    for (i=savedIndex; i < max; i++){        process(i);        if (needToExit(i)){            savedIndex = i;            if (savedIndex == Math.floor((max - 1) / breakSetp) * breakSetp) {                removeEventListener(Event.ENTER_FRAME, onEnterFrame);            } else {                break;            }        }    }    onResetFrameLoop();}function process(_input:int):void {    trace(_input);}function needToExit(_input:int):Boolean {    if (_input - savedIndex == breakSetp) {        return true;    } else {        return false;    }        }function onResetFrameLoop():void {    if (hasEventListener(Event.ENTER_FRAME)) {        trace("New Chunk");    } else {        trace("Finished.");    } [COLOR=#333333][FONT=Courier 10 Pitch]}[/FONT][/COLOR]


متغیر breakSetp: در این مثال نیز یک حلقه طولانی for با 1000 مرحله (متغیر max) وجود دارد. اما کد این حلقه به صورتی نوشته شده است که در یک حلقه فریم به صورت کامل اجرا نشود تا از هنگ کردن اپلیکیشن جلوگیری شود. بدین منظور یک متغیر به نام breakSetp تعریف شده است که مشخص کننده تعداد مراحل در هر حلقه فریم است. برای تعیین دقیق breakSetp روش مشخصی وجود ندارد و می توان آن را به روش سعی و خطا به دست آورد. یعنی باید زمان تقریبی عملیات هایی که در هر مرحله انجام می شود را محاسبه کرد و سپس با یک ضریب اطمینان، مقدار breakSetp را مشخص نمود.

متغیر savedIndex: از این متغیر برای تعیین مقدار شروع کننده حلقه for در فاز اول حلقه frame استفاده می شود. مقدار آن زمانی که خروجی تابع needToExit برابر با true باشد، تعیین می شود. در تابع needToExit مشخص می شود که آیا تعداد مراحل هر حلقه فریم (برابر با متغیر breakSetp) طی شده است یا خیر.

Listener مربوط به enterFrame: همانطور که گفته شد رویداد enterFrame همواره در فاز اول ارسال می شود. بنابراین یک Listener برای آن تعریف شده است تا بعد از ارسال آن و با شروع حلقه فریم جدید، حلقه for ادامه پیدا کند. در این حلقه for یک سری عملیات می تواند انجام شود که توسط تابع process مشخص می شود. سپس وضعیت تکرار حلقه for توسط تابع needToExit چک می شود تا درصورت نیاز و با اجرای دستور break، حلقه for متوقف شود. بعد از توقف حلقه for تابع onResetFrameLoop فراخوانی می شود که بعد از اجرای آن، فاز اول حلقه فریم خاتمه یافته و فاز دوم شروع می شود. بعد از اتمام فاز دوم، مجددا حلقه فریم تکرار می شود و تا وقتی که دستور removeEventListener اجرا نشده است، تابع onEnterFrame با ارسال رویداد enterFrame فراخوانی خواهد شد. دستور removeEventListener زمانی اجرا می شود که مطمئن شویم دیگر نیازی به فراخوانی تابع onEnterFrame به منظور اتمام مراحل حلقه for نداریم. برای تعیین این زمان از شرطی که برای savedIndex نوشته شده است، استفاده کرده ایم.

تابع onResetFrameLoop: به وسیله این تابع از زمان اتمام تمامی مراحل حلقه for و همچنین زمانهای هر break مطلع می شویم. همانطور که گفته شد، بعد از اطمینان از اتمام مراحل حلقه for با دستور removeEventListener، فراخوانی تابع onEnterFrame متوقف می شود، بنابراین در این حالت مقدار خروجی hasEventListener، برابر با false خواهد بود.


  • حل مشکل حلقه های for each in
در روشی که اشاره شد، مشکل هنگ کردن سیستم تنها برای حلقه های for دارای اندیس (index) قابل حل می باشد. اما در حلقه های for each in که اندیس شمارنده (مثلا i) وجود ندارد، مشکل هنگ کردن سیستم کماکان وجود خواهد داشت. راه حل پیشنهادی در این موارد تبدیل این حلقه ها به یک حلقه اندیس دار با استفاده از یک آرایه است. به مثال زیر توجه کنید:

[درصورتی که در دیدن کدها مشکل دارید به لینک منبع مراجعه کنید]

کد:
[COLOR=#333333][FONT=Courier 10 Pitch]var indexedList:Array = new Array();[/FONT][/COLOR]

for each (_value in _object) {    indexedList.push(value);}for (var i:int = 0; i < indexedList.length; i++) {    //... [COLOR=#333333][FONT=Courier 10 Pitch]}[/FONT][/COLOR]


در این مثال ابتدا تمامی مقادیر مورد نیاز در یک آرایه قرار می گیرد. در ادامه می توان مشابه مثال قبل یک عملیات طولانی را در یک حلقه اندیس دار انجام داد. به هرحال در مرحله اول که عملیات تبدیل حلقه انجام می شود نیز با هنگ موقت سیستم مواجه خواهیم بود، اما زمان این هنگ بسیار کوتاهتر از حالتی است که یک عملیات زمانبر سنگین در همان ابتدا و در حلقه for each in انجام شود.


  • جمع بندی بخش اول از مجموعه بهینه سازی
1- به جای false کردن مشخصه visible اشیا، هرگز alpha آنها را صفر نکنید (ممکن نتیجه نهایی مشابه باشد اما عملیات پردازش متفاوت خواهد بود).


2- از updateAfterEvent تنها در مواقع لزوم استفاده کنید.


3- حلقه های for طولانی را به صورت غیر همزمان اجرا کنید و آنها را به قطعات کوچکتر تبدیل کنید.


4- حتی المقدور سعی کنید از حلقه for each in طولانی استفاده نکنید. درصورت نیاز آنها را به حلقه های اندیس دار تبدیل کنید.


5- سعی کنید در صورت امکان از مقادیر کمتر برای fps استفاده کنید. در LCD های معمولی حتما کمتر از 60 fps را انتخاب کنید.

منبع: http://flashcenter.ir/fa/1392/05/01/بهینهسازی-1-اجرای-غیرهمزمان-حلقه/
 
آخرین ویرایش:

جدیدترین ارسال ها

بالا