به عنوان یک برنامهنویس اپهای اندرویدی، یکی از بزرگترین چالشهای پیشرو، مدیریت چرخههای حیات در برنامهها و قسمتهای مختلف آن است. از همین تریبون از طرف تیم توسعهدهنده فریمورک اندروید از این دوست عزیزمون و شما به خاطر طراحی مزخرف و پیچیده این باگ(به فریمورک کذایی اشاره دارد) عذرخواهی میکنم.
شاید به محض دیدن موضوع این نوشته، بپرسید: چرخهحیاتِ چه چیزی؟ و کاملا سوال به جا و درستی پرسیدهاید. تقریبا هر کامپوننتی که در اندروید (جاوا) با آن کار میکنیم، چرخهحیات مربوط به خودش را دارد، روزی به دنیا میآید و روزی هم توسط Garbage Collector از صفحهی روزگار (مموری) حذف میشود.
بعضی از چرخهها مثل چرخهحیات یک String خیلی ساده هستند و بعضی دیگر مثل چرخهحیات فرگمنت بسیار پیچیدهتر(البته هر دو آبجکت هستند).
اما قبل از آنکه وارد بحث چرخهحیات شویم، بیایید ببینیم چرا و چطور این چرخهها بهوجود میآیند. برای آنکه این موضوع را بفهمیم باید کمی با نحوه مدیریت حافظه در اندروید آشنا شویم.
اندروید از Android Runtime (و یا Dalvik virtual Machine) برای مدیریت مموری استفاده میکند و این سیستم هم برپایه سیستم مدیریت حافظه جاوا یا همان Garbage Collector جاوا است. اگر با زبان c کار کرده باشید، میدانید که مدیریت حافظه در این زبان به عهده برنامهنویس است. مثلاً برنامهنویس مقداری فضا برای ذخیره یک رشته در مموری میگیرد و در انتها که کارش با آن رشته تمام شد، باید آن را از حافظه پاک کند و اگر نه، آن رشته در حافظه خواهد ماند و حافظه را بدون دلیل اِشغال میکند. این کار در جاوا به صورت اتوماتیک و توسط سیستمی به نام Garbage Collector انجام میشود. سیستم Garbage Collector وظیه دارد، آبجکتهایی که دیگر نیازی به آنها نیست را از مموری پاک کند و فضای حافظه را برای استفاده مجدد آماده کند. حال وقتی که یک آبجکت میسازیم (در جاوا همه چیز آبجکت است به جز Primitive typeها)، فضایی برای آن در هیپمموری اشغال میشود، سپس از آن استفاده میکنیم و به آن مقادیر و حالتهای مختلف میدهیم و در انتها(زمانی که کار ما با آن آبجکت تمام میشود) توسط GC (همان Garbage Collector) پاک میشود. به این چرخه از زمان ایجاد آبجکت تا زمانی که توسط GC از مموری پاک میشود، چرخهحیات آبجکت میگوییم. چرخهحیات بعضی از آبجکتها مثل رشتهها به همین سادگی است که زمانی بهوجود میآیند و بعد از استفاده از بینمیروند، اما چرخهحیات بعضی دیگر مثل فرگمنت شامل پیچیدگیهای بیشتری است و حالتهای متفاوتی برای آن ایجاد میشود.
همانطور که گفتیم مدیریت حافظه توسط GC انجام میشود. در این بخش میخواهیم ببینیم GC این کار را چطور انجام میدهد.
جاوا مموری را به دو قسمت تقسیم میکند:
این مموری به وسیله Java Runtime Environment برای لود شدن کلاسهای خودِ JRE و ساخت آبجکتهای برنامه استفاده میشود. در واقع هرپروسهای که توسط JVM اجرا میشود، یک هیپمموری مجزا برای آن ساخته میشود و هر آبجکت جدیدی که در آن پروسه ساخته شود، در این مموری قرار میگیرد. سیستم GC جاوا بر روی این مموری اجرا میشود.
نکته کماهمیتتر اما جالب این هست که این مموری هیچ رابطهای با ساختار داده درخت هیپ (Heap Tree) ندارد و این که نام این دو یکسان است، فقط یک تشابه اسمی است.
برای هر تردی که در برنامه ساخته میشود (از جمله ترد اصلی یا Main Thread) یک استکمموری جداگانه ساخته میشود(دوباره به عکس نگاه کنید).
استکمموریها دو چیز را در خود ذخیره میکنند: ۱-primitive typeها(که بالاتر گفتیم آبجکت نیستند) و ۲-رفرنسی از آبجکتهایشان در هیپمموری
در واقع آبجکتها در هیپمموری ساخته میشوند و فقط رفرنسی از آنها در استکمموری نگهداری میشود تا به آن آبجکت دسترسی داشته باشیم.
استکمموری مثل ساختار داده استک عمل میکند، یعنی LIFO یا Last in First out است و هر آیتمی که دیرتر وارد آن شده باشد، زودتر خارج خواهد شد. هر زمان که متد جدیدی صدا زده میشود، یک بلاک جدید به بالای استک اضافه میشود و در دسترس آن متد قرار میگیرد و بعد از آنکه اجرای آن قسمت از کد به پایان برسد، آن بلاکِ بالای استک هم پاپ (pop) میشود.
اگر بخواهیم یک جمعبندی کوچک روی این قسمت داشته باشیم باید بگوییم که هر پروسهای که در یک برنامه جاوا اجرا میشود، یک و تنها یک هیپمموری مختص به خودش دارد که همه آبجکتهای آن پروسه در همان هیپمموری ذخیره میشوند(بین کل پروسه مشترک است) و هر تردی که در پروسه برنامه ایجاد میشود استکمموری مربوط به خودش هم، همراه خودش ساخته میشود، و هر متد که در ترد اجرا میشود هم، بلاک مربوط به خودش را در استکمموری دارد. پس به ازای هر ترد یک استک مموری داریم ولی کلا یک هیپ مموری برای کل برنامه(در اینجا فرض میکنیم هر برنامه یک پروسه است) خواهیم داشت. این دو مموری(هیپ و استک) با هم و به این صورت کار میکنند که همه آبجکتها در هیپ مموری و متغیرهای primitive و رفرنسی از آبجکتهای هر ترد در استک مموری ذخیره میشوند.
این پروسه وظیفه مدیریت مموری در جاوا را برعهده دارد. برخلاف زبان سی که مموری الوکیشن و دیالوکیشن توسط خود برنامهنویس و به صورت دستی باید انجام شود، جاوا سیستمی هوشمند برای مدیریت مموری دارد که برنامهنویس را از این کار معاف میکند.
پروسهی Garbage Collection به تصمیم JVM اجرا میشود و زمانی که اجرا میشود تمام تردها (هر تردی که در پروسه اجرا شده به جز آنهایی که مربوط به GC هستند) تا کامل شدن آن متوقف میشوند. بعد از آنکه به پایان رسید، تردها ادامه پیدا میکنند.
همانطور که صحبت کردیم، GC روی هیپمموری اجرا میشود تا آبجکتهایی که دیگر از آنها استفاده نمیشود را پاک کند و مموری برای استفادههای دیگر آماده شود.
نحوه کارکرد GC در جاوا را با استفاده از تصویر زیر بررسی می کنیم:
این تصویر را به سه قسمت تقسیم میکنیم:
بیایید با یک مثال این سه قسمت را بهتر بررسی کنیم. فرض کنید یک آبجکتی از یک کلاس میسازیم(ریشه)، به محض ساختن این آبجکت، و در کانستراکتور آن کلی آبجکت دیگر (فرزندان ریشه) که در آن کلاس استفاده کردیم، ساخته میشوند. در اینجا آبجکت اولی که ساختیم ریشه بوده و آبجکتهایی که در آن ساخته میشوند، فرزندان آن ریشه هستند. این آبجکتها از طریق ریشه در دسترس هستند(یک رفرنس به آنها وجود دارد) و از آنها استفاده میشود. حالا اگر رفرنسی که به این آبجکتها داریم به هر دلیلی از ریشه جدا شود، یعنی دیگر هیچ رفرنسی به یکی از فرزندان ریشه وجود نداشته باشد، در این حالت آن آبجکت به عنوان garbage تلقی شده و در دفعه بعدی که GC اجرا میشود، از مموری پاک خواهد شد.
دقت کنید که معیار GC از اینکه آبجکتی را به عنوان garbage کاندید کند، تعداد رفرنسها به آن آبجکت خواهد بود. پس اگر به یک آبجکت هیچ رفرنسی وجود نداشته باشد به عنوان garbage کاندید شده و از مموری پاک خواهد شد و برعکس. یعنی اگر به آبجکتی نیاز نداشته باشیم و عمداً یا سهواً به آن آبجکت رفرنسی را نگه داریم، آن آبجکت در مموری خواهد ماند و پاک نخواهد شد که به این پدیده مموری لیک میگوییم.
در واقع اندروید کار سخت که سروکلهزدن با مموری هست را انجام داده و سعی کرده با اطلاعرسانی قسمتهای مختلف این چرخه به ما کمک کند تا مدیریت بهتری روی آبجکتمان داشته باشیم. مثلاً در فرگمت با کالبکهایی مثل: onAttach, onCreate و.... می خواهد وضعیتی که آبجکت فرگمنت ما در آن قرار دارد را به ما اطلاع دهد تا بتوانیم بر اساس آن کارهایی که میخواهیم را درست و بهموقع انجام دهیم. فقط باید این چرخهها را به درستی درک کرده و یادبگیریم، تا بتوانیم درست از آنها استفاده کنیم. اندروید با این کار سعی کرده اختیار را به برنامهنویس بدهد و دست او را باز بگذارد تا بتواند کارها را به راحتی کنترل کند و از اتفاقاتی که میافتد با خبر شود.
از این توضیحات اولیه که بگذریم، به نکتهای ساده اما مهم میرسیم:
هرچه یک چیز را بیشتر بشناسیم، میتوانیم بهتر از آن استفاده کنیم.
چرخهحیات هم یکی از قسمتهایی است که باعث شناخت بیشتر ما از یک کامپوننت میشود، مثلاً با شناخت چرخهحیات اکتیویتی میتوانیم به راحتی تصمیم بگیریم که در چه زمانی و در چه جایی، چه کاری را در اکتیویتی انجام دهیم.
مشکل آنجاست که اغلب برنامهنویسها بدون آنکه آشنایی کاملی، مثلا با چرخهحیات فرگمنت داشته باشند، از روی یکسری نمونهکد آماده شروع به کار کردن با آن میکنند و بعد از آنکه به اولین مشکل میخورند، شروع به بدوبیراه گفتن به اندروید و فرگمنت و... میکنند.
در ادامه این رشته پست میخواهم درباره چرخهحیات معمولترین و پراستفادهترین کامپوننتهای اندروید صحبت کنم. ابتدا از چرخه حیات اکتیویتیها شروع میکنیم، در ادامه به چرخهحیات فرگمنتها میرسیم و در انتها درباره چرخهحیات ویوها صحبت میکنیم.
در پست بعدی درباره چرخهحیات اکتیویتیها در اندروید صحبت کرده و از زوایای مختلف نگاهی عمیق به آن میکنیم.
منتظر نظراتتون گرمتون هستم♥.