عملکرد بهتر ازطریق رشته‌بندی

استفاده ماهرانه از رشته‌ها در Android می‌تواند به شما کمک کند عملکرد برنامه‌تان را تقویت کنید. این صفحه چندین جنبه از کار با رشته‌ها را مورد بحث قرار می‌دهد: کار با رشته UI یا اصلی؛ رابطه بین چرخه حیات برنامه و اولویت رشته؛ و روش‌هایی که پلتفرم برای کمک به مدیریت پیچیدگی رشته ارائه می‌دهد. در هریک از این حوزه‌ها، این صفحه مشکلات احتمالی و استراتژی‌های اجتناب از آن‌ها را شرح می‌دهد.

رشته اصلی

وقتی کاربر برنامه‌تان را راه‌اندازی می‌کند، Android فرایند Linux جدیدی به‌همراه رشته اجرایی ایجاد می‌کند. این رشته اصلی، که به‌عنوان رشته میانای کاربر نیز شناخته می‌شود، مسئول هر چیزی است که روی صفحه اتفاق می‌افتد. درک نحوه عملکرد آن می‌تواند به شما کمک کند برنامه‌تان را به‌گونه‌ای طراحی کنید که از رشته اصلی برای بهترین عملکرد ممکن استفاده کند.

داخلی‌ها

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

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

هنگامی که پویانمایی یا به‌روزرسانی صفحه درحال انجام است، سیستم تلاش می‌کند تا هر ۱۶ میلی‌ثانیه یا بیشتر، یک بلوک کاری (که مسئول ترسیم صفحه است) را اجرا کند تا رندرینگ به‌صورت روان با سرعت ۶۰ فریم در ثانیه انجام شود. برای اینکه سیستم به این هدف برسد، سلسله‌مراتب میانای کاربری/نما باید در رشته اصلی به‌روز شود. بااین‌حال، وقتی صف پیام‌رسانی رشته اصلی حاوی تکالیفی است که یا خیلی زیاد هستند یا خیلی طولانی هستند و رشته اصلی نمی‌تواند به‌روزرسانی را به‌اندازه کافی سریع تکمیل کند، برنامه باید این کار را به رشته عامل منتقل کند. اگر رشته اصلی نتواند اجرای بلوک‌های کاری را ظرف ۱۶ میلی‌ثانیه به‌پایان برساند، کاربر ممکن است شاهد گیر کردن، تأخیر، یا عدم پاسخ‌گویی میانای کاربر به ورودی باشد. اگر رشته اصلی برای حدود پنج ثانیه مسدود شود، سیستم کادر گفتگوی برنامه پاسخ نمی‌دهد (ANR) را نمایش می‌دهد و به کاربر اجازه می‌دهد برنامه را مستقیماً ببندد.

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

رشته‌ها و مراجع شیء میانای کاربر

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

مشکلات مربوط به مراجع به دو دسته متمایز تقسیم می‌شوند: مراجع صریح و مراجع ضمنی.

مرجع‌های صریح

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

برای مثال، برنامه‌ای را درنظر بگیرید که ارجاع مستقیمی به شیء میانای کاربر در رشته عامل دارد. شیء موجود در رشته عامل ممکن است حاوی ارجاع به View باشد؛ اما قبل‌از تکمیل کار، View از سلسله‌مراتب نما برداشته می‌شود. وقتی این دو کنش به‌طور هم‌زمان اتفاق می‌افتد، مرجع شیء View را در حافظه نگه می‌دارد و ویژگی‌هایی را برای آن تنظیم می‌کند. بااین‌حال، کاربر هرگز این شیء را نمی‌بیند و برنامه به‌محض اینکه ارجاع به آن ازبین برود، شیء را حذف می‌کند.

در مثالی دیگر، اشیای View حاوی ارجاعاتی به فعالیتی هستند که مالک آن‌ها است. If that activity is destroyed, but there remains a threaded block of work that references it—directly or indirectly—the garbage collector will not collect the activity until that block of work finishes executing.

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

در سناریوهایی مانند این، پیشنهاد می‌کنیم که برنامه‌تان شامل ارجاع‌های صریح به اشیای میانای کاربری در تکالیف کاری رشته‌ای نباشد. اجتناب از چنین ارجاعاتی به شما کمک می‌کند از این نوع نشت‌های حافظه جلوگیری کنید و درعین‌حال از رقابت رشته‌ای نیز دوری کنید.

در همه موارد، برنامه شما باید فقط اشیای واسط کاربر را در رشته اصلی به‌روز کند. این بدین معنی است که باید خط‌مشی مذاکره‌ای ایجاد کنید که به چندین رشته اجازه دهد کار را به رشته اصلی برگردانند، که فعالیت یا قطعه بالایی را با کار به‌روزرسانی شیء میانای کاربر واقعی وظیفه می‌دهد.

ارجاع‌های ضمنی

یک نقص رایج در طراحی کد با اشیاء رشته‌ای را می‌توان در قطعه کد زیر مشاهده کرد:

کاتلین

class MainActivity : Activity() {
    // ...
    inner class MyAsyncTask : AsyncTask<Unit, Unit, String>() {
        override fun doInBackground(vararg params: Unit): String {...}
        override fun onPostExecute(result: String) {...}
    }
}

جاوا

public class MainActivity extends Activity {
  // ...
  public class MyAsyncTask extends AsyncTask<Void, Void, String>   {
    @Override protected String doInBackground(Void... params) {...}
    @Override protected void onPostExecute(String result) {...}
  }
}

نقص این قطعه کد این است که کد، شیء رشته‌بندی MyAsyncTask را به‌عنوان کلاس داخلی غیرایستا از برخی فعالیت‌ها (یا کلاس داخلی در Kotlin) اعلام می‌کند. این اعلامیه ارجاعی ضمنی به نمونه Activity دربرگیرنده ایجاد می‌کند. درنتیجه، شیء حاوی ارجاعی به فعالیت است تا زمانی که کار رشته‌ای تکمیل شود، که باعث تأخیر در تخریب فعالیت ارجاع‌شده می‌شود. این تأخیر، به‌نوبه خود، فشار بیشتری بر حافظه وارد می‌کند.

یک راه‌حل مستقیم برای این مشکل این است که نمونه‌های کلاس سربارگذاری‌شده خود را به‌عنوان کلاس‌های ایستا یا در فایل‌های خودشان تعریف کنید، بنابراین ارجاع ضمنی را حذف کنید.

راه حل دیگر این است که همیشه کارهای پس‌زمینه را در فراخوانی چرخه حیات Activity مناسب، مانند onDestroy، لغو و پاکسازی کنید. بااین‌حال، این رویکرد می‌تواند خسته‌کننده و مستعد خطا باشد. به‌عنوان یک قانون کلی، نباید منطق پیچیده و غیرواسط کاربر را مستقیماً در فعالیت‌ها قرار دهید. علاوه‌براین، AsyncTask اکنون منسوخ شده است و استفاده از آن در کد جدید توصیه نمی‌شود. برای جزئیات بیشتر درباره اصول همزمانی که برای شما دردسترس است، رشته‌بندی در Android را ببینید.

رشته‌ها و چرخه‌های حیات فعالیت برنامه

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

رشته‌های ماندگار

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

موردی را درنظر بگیرید که در آن یک فعالیت مجموعه‌ای از بلوک‌های کاری رشته‌ای را ایجاد می‌کند و سپس قبل‌از اینکه رشته عامل بتواند بلوک‌ها را اجرا کند، ازبین می‌رود. برنامه با بلوک‌هایی که درحال انتقال هستند چه باید بکند؟

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

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

مدیریت دستی پاسخ‌های چرخه حیات برای همه اشیاء رشته‌ای می‌تواند بسیار پیچیده شود. اگر آن‌ها را به‌درستی مدیریت نکنید، ممکن است برنامه‌تان با مشکلاتی مثل رقابت حافظه و مشکلات عملکردی مواجه شود. ترکیب کردن ViewModel با LiveData به شما امکان می‌دهد داده‌ها را بار کنید و وقتی تغییر می‌کنند مطلع شوید بدون اینکه نگران چرخه حیات باشید. ViewModel شیء یکی از راه‌حل‌های این مسئله است. ViewModel در تغییرات پیکربندی حفظ می‌شود که روشی آسان برای حفظ داده‌های نمای شما ارائه می‌دهد. برای اطلاعات بیشتر درباره ViewModels، راهنمای ViewModel را ببینید و برای کسب اطلاعات بیشتر درباره LiveData، راهنمای LiveData را ببینید. اگر اطلاعات بیشتری درباره معماری برنامه می‌خواهید، راهنمای معماری برنامه را بخوانید.

اولویت رشته

همان‌طور که در فرایندها و چرخه حیات برنامه شرح داده شده است، اولویتی که رشته‌های برنامه‌تان دریافت می‌کنند تاحدی به این بستگی دارد که برنامه در کجای چرخه حیات برنامه قرار دارد. هنگام ایجاد و مدیریت رشته‌ها در برنامه‌تان، مهم است که اولویت آن‌ها را تنظیم کنید تا رشته‌های مناسب اولویت‌های مناسب را در زمان‌های مناسب دریافت کنند. اگر خیلی بالا تنظیم شود، رشته شما ممکن است رشته میانای کاربر و RenderThread را قطع کند و باعث شود برنامه‌تان قاب‌ها را ازدست بدهد. اگر خیلی کم تنظیم شود، ممکن است تکالیف غیرهمگام (مثل بار کردن تصویر) را کندتر از آنچه باید باشد کنید.

هر بار که رشته‌ای ایجاد می‌کنید، باید setThreadPriority() را فراخوانی کنید. زمان‌بند رشته سیستم اولویت را به رشته‌های با اولویت بالا می‌دهد و این اولویت‌ها را با نیاز به انجام نهایی همه کارها متعادل می‌کند. به‌طور کلی، رشته‌ها در گروه پیش‌زمینه حدود ۹۵٪ از کل زمان اجرا را از دستگاه دریافت می‌کنند، درحالی‌که گروه پس‌زمینه تقریباً ۵٪ را دریافت می‌کند.

سیستم همچنین بااستفاده از کلاس Process، مقدار اولویت هر رشته را به آن اختصاص می‌دهد.

به‌طور پیش‌فرض، سیستم اولویت رشته را روی همان اولویت و عضویت‌های گروهی رشته تولیدکننده تنظیم می‌کند. بااین‌حال، برنامه شما می‌تواند بااستفاده از setThreadPriority() اولویت رشته را به‌طور صریح تنظیم کند.

کلاس Process با ارائه مجموعه‌ای از ثابت‌ها که برنامه‌تان می‌تواند برای تنظیم اولویت‌های رشته استفاده کند، به کاهش پیچیدگی در اختصاص دادن مقادیر اولویت کمک می‌کند. برای مثال، THREAD_PRIORITY_DEFAULT مقدار پیش‌فرض رشته را نشان می‌دهد. برنامه‌تان باید اولویت رشته را روی THREAD_PRIORITY_BACKGROUND تنظیم کند تا رشته‌ها کارهای با فوریت کمتر را اجرا کنند.

برنامه شما می‌تواند از ثابت‌های THREAD_PRIORITY_LESS_FAVORABLE و THREAD_PRIORITY_MORE_FAVORABLE به‌عنوان افزاینده برای تنظیم اولویت‌های نسبی استفاده کند. برای مشاهده فهرست اولویت‌های رشته، به ثابت‌های THREAD_PRIORITY در کلاس Process مراجعه کنید.

برای اطلاعات بیشتر درباره مدیریت رشته‌ها، به اسناد مرجع درباره کلاس‌های Thread و Process مراجعه کنید.

کلاس‌های کمکی برای رشته‌بندی

برای توسعه‌دهندگانی که از Kotlin به‌عنوان زبان اصلی خود استفاده می‌کنند، توصیه می‌کنیم از روال‌های مشترک استفاده کنند. روال‌های همکار مزایای متعددی دارند، ازجمله نوشتن کد ناهمگام بدون فراخوانی‌های برگشتی و همچنین همزمانی ساختاریافته برای تعیین محدوده، لغو، و رسیدگی به خطا.

این چارچوب همچنین همان کلاس‌ها و اصول اولیه Java را برای تسهیل رشته‌بندی ارائه می‌دهد، مانند کلاس‌های Thread، Runnable، و Executors، و همچنین کلاس‌های اضافی مانند HandlerThread. برای اطلاعات بیشتر، لطفاً به رشته‌سازی در Android مراجعه کنید.

کلاس HandlerThread

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

یک چالش رایج در دریافت فریم‌های پیش‌نمایش از شیء Camera را درنظر بگیرید. وقتی برای قاب‌های پیش‌نمایش «دوربین» ثبت‌نام می‌کنید، آن‌ها را در onPreviewFrame() فراخوانی برگشتی دریافت می‌کنید که در رشته رویدادی که از آن فراخوانی شده است فراخوانده می‌شود. اگر این فراخوانی برگشتی در رشته میانای کاربر فراخوانده می‌شد، تکلیف سروکار داشتن با آرایه‌های بزرگ پیکسل با کار پردازش رویداد و بازپردازی تداخل پیدا می‌کرد.

در این مثال، وقتی برنامه‌تان فرمان Camera.open() را به بلوک کاری در رشته گرداننده واگذار می‌کند، onPreviewFrame() فراخوانی برگشتی مرتبط به‌جای رشته میانای کاربر در رشته گرداننده قرار می‌گیرد. بنابراین، اگر می‌خواهید کار طولانی‌مدت روی پیکسل‌ها انجام دهید، این ممکن است راه‌حل بهتری برای شما باشد.

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

کلاس ThreadPoolExecutor

انواع خاصی از کارها وجود دارد که می‌توان آن‌ها را به تکالیف توزیع‌شده و بسیار موازی کاهش داد. برای مثال، یکی از این تکالیف محاسبه فیلتر برای هر بلوک ۸×۸ از یک تصویر ۸ مگاپیکسلی است. با حجم زیاد بسته‌های کاری که این ایجاد می‌کند، HandlerThread کلاس مناسبی برای استفاده نیست.

ThreadPoolExecutor کلاس کمکی برای آسان‌تر کردن این فرایند است. این کلاس ایجاد گروهی از رشته‌ها را مدیریت می‌کند، اولویت‌های آن‌ها را تنظیم می‌کند، و نحوه توزیع کار بین آن رشته‌ها را مدیریت می‌کند. با افزایش یا کاهش حجم کاری، کلاس رشته‌های بیشتری را ایجاد یا حذف می‌کند تا با حجم کاری سازگار شود.

این کلاس همچنین به برنامه‌تان کمک می‌کند تعداد بهینه‌ای از رشته‌ها را ایجاد کند. وقتی برنامه شیء ThreadPoolExecutor را می‌سازد، حداقل و حداکثر تعداد رشته‌ها را تنظیم می‌کند. با افزایش حجم کاری که به ThreadPoolExecutor داده می‌شود، کلاس تعداد رشته‌های حداقل و حداکثر مقداردهی‌شده را درنظر می‌گیرد و مقدار کار معلق را که باید انجام شود درنظر می‌گیرد. براساس این عوامل، ThreadPoolExecutor تصمیم می‌گیرد که چه تعداد رشته باید در هر زمان مشخص فعال باشد.

چند رشته باید ایجاد کنید؟

اگرچه از سطح نرم‌افزار، کد شما توانایی ایجاد صدها رشته را دارد، انجام این کار می‌تواند مشکلات عملکردی ایجاد کند. برنامه‌تان منابع CPU محدودی را با سرویس‌های پس‌زمینه، موتور رندر، موتور صوتی، شبکه‌سازی، و غیره هم‌رسانی می‌کند. CPUها واقعاً فقط توانایی مدیریت تعداد کمی از رشته‌ها را به‌صورت موازی دارند؛ هر چیزی فراتر از آن با مشکل اولویت و زمان‌بندی مواجه می‌شود. بنابراین، مهم است که فقط به تعداد موردنیاز برای حجم کاری‌تان رشته ایجاد کنید.

در عمل، متغیرهای زیادی مسئول این امر هستند، اما انتخاب یک مقدار (مثلاً ۴ برای شروع) و آزمایش آن با Systrace استراتژی محکمی مانند هر استراتژی دیگری است. می‌توانید از روش آزمون و خطا برای پیدا کردن حداقل تعداد رشته‌هایی که می‌توانید بدون مواجه شدن با مشکل استفاده کنید، استفاده کنید.

ملاحظه دیگری که در تصمیم‌گیری درباره تعداد رشته‌ها باید درنظر گرفت این است که رشته‌ها رایگان نیستند: آن‌ها حافظه اشغال می‌کنند. هر رشته حداقل ۶۴ کیلوبایت حافظه مصرف می‌کند. این امر به‌سرعت در بسیاری از برنامه‌های نصب‌شده در دستگاه، به‌ویژه در شرایطی که پشته‌های فراخوانی به‌طور قابل‌توجهی رشد می‌کنند، افزایش می‌یابد.

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