عملکرد بهتر از طریق threading

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

موضوع اصلی

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

داخلی

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

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

در حالی که یک انیمیشن یا به‌روزرسانی صفحه نمایش در حال رخ دادن است، سیستم سعی می‌کند یک بلوک کار (که وظیفه ترسیم صفحه را بر عهده دارد) هر 16 میلی‌ثانیه یا بیشتر اجرا کند تا به آرامی با سرعت 60 فریم در ثانیه رندر شود. برای اینکه سیستم به این هدف برسد، سلسله مراتب UI/View باید در رشته اصلی به روز شود. با این حال، زمانی که صف پیام‌رسانی رشته اصلی حاوی وظایفی است که خیلی زیاد یا خیلی طولانی هستند تا رشته اصلی نتواند به‌روزرسانی سریع را تکمیل کند، برنامه باید این کار را به یک رشته کاری منتقل کند. اگر رشته اصلی نتواند اجرای بلوک‌های کاری را در عرض 16 میلی‌ثانیه به پایان برساند، کاربر ممکن است مشکل، تاخیر یا عدم پاسخگویی رابط کاربری به ورودی را مشاهده کند. اگر رشته اصلی تقریباً پنج ثانیه مسدود شود، سیستم گفتگوی برنامه پاسخ نمی دهد (ANR) را نمایش می دهد و به کاربر اجازه می دهد برنامه را مستقیماً ببندد.

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

موضوعات و ارجاعات شی UI

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

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

ارجاعات صریح

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

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

در مثالی دیگر، View اشیاء حاوی ارجاعاتی به فعالیتی است که مالک آنهاست. اگر آن فعالیت از بین برود، اما یک بلوک رشته‌ای از کار باقی بماند که به آن ارجاع می‌دهد - مستقیم یا غیرمستقیم - جمع‌آورکننده زباله فعالیت را جمع‌آوری نمی‌کند تا زمانی که اجرای آن بلوک کار به پایان برسد.

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

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

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

ارجاعات ضمنی

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

کاتلین

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) {...}
  }
}

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

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

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

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

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

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

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

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

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

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

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

اولویت موضوع

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

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

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

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

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

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

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

کلاس های کمکی برای نخ زنی

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

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

کلاس HandlerThread

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

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

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

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

کلاس ThreadPoolExecutor

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

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

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

چند تا موضوع باید ایجاد کنید؟

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

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

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

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