مدیریت حافظه بیتمپ برای کاهش خطرات «کمبود حافظه» (OOM) در برنامههای Android با محتوای دیداری غنی بسیار مهم است. درحالیکه توسعهدهندگان میدانند که باید اندازه تصویر را کاهش دهند، دستیابی به پایداری طولانیمدت برنامه از تسلط بر دو ستون کلیدی دیگر حاصل میشود: استفاده مجدد و بازیافت. این جلسه یک کتاب راهنمای کاربردی برای بهینهسازی عملکرد حافظه بااستفاده از ابزارهای مدرن، کتابخانههای متنباز مانند Glide و Coil، و میاناهای برنامهسازی کاربردی سطح سیستم ارائه میدهد.
در این جلسه، نحوه تشخیص رگرسیونهای حافظه در دنیای واقعی بااستفاده از «معیارهای کلیدی Android»، پیادهسازی الگوهای پیشرفته ذخیرهسازی موقت (استفاده مجدد) با Glide و Coil، و آزادسازی پیشگیرانه حافظه (بازیافت) بااستفاده از callback (فراخوانی) onTrimMemory را یاد خواهید گرفت.
BitmapRegionDecoder استفاده کنید.onTrimMemory، وقتی برنامهتان به پسزمینه میرود، حافظه تصویر را آزاد کنید. برای جلوگیری از رشد حافظه پسزمینه، نظرسنجی و پویانماییهای پسزمینه را در onStop متوقف کنید.آوانویسی
Fung Lam
بله، حدود ۱ ساعت تا ناهار مانده است، پس لطفاً با من همراه باشید.
من فانگ لم، مهندس روابط توسعهدهندگان در Android هستم و در لندن مستقرم.
بله، متشکرم.
قبلاز شروع، میتوانم سؤالی بپرسم؟
چند نفر از شما امسال تلفن جدید خریدید؟
بسیار عالی.
بسیار عالی.
راستش را بگو، اگر Pixel نخری سرزنشت نمیکنم.
خب، تلفن جدید گرانتر است، درست است؟
بله، به همین دلیل است که برخی از شما تلفن جدید نخریدید.
چرا گرانتر است؟
آیا میدانید؟
شماره.
حافظه دسترسی تصادفی.
حافظه دسترسی تصادفی، آره، آره.
بله، RAM واقعاً چالشی برای Android و برای ما، برای توسعهدهنده است.
به همین دلیل امروز اینجا هستم تا بگویم چگونه میتوانیم بر چالش حافظه، بر RAM غلبه کنیم.
پیشتر در Google I/O و Google I/O Connect، جلساتی درباره حافظه داشتیم، از نحوه تخصیص حافظه توسط سیستمعامل، نحوه نمایهسازی برنامه، نحوه بهینهسازی کد بااستفاده از R8، و البته، در آن زمان کمی درباره Bitmap صحبت کردم.
میتوانید عکسهای من را در نماگرفت ببینید.
بهینهسازی حافظه یکی از حوزههای بزرگ است که در زمینههای مختلفی مانند زمان اجرای Android، جمعآوری زباله، ابزارهای عملکرد، تخصیص حافظه شیء، کامپایلر، و غیره کاربرد دارد. در این جلسه ۳۰ دقیقهای، اطلاعاتی درباره نحوه بهینهسازی حافظه بیتمپ به شما ارائه خواهم داد.
شاید بپرسید چرا، چرا Bitmap؟
چرا باید روی حافظه بیتمپ تمرکز کنیم؟
چون یک تصویر گویاتر از هزار کلمه است.
درست است، دلیلش را برایتان توضیح میدهم.
در صفحهنمایش تلفن، مثلاً Pixel 7، Pixel 9، یا Pixel 11 Pro، وضوح صفحهنمایش حدود ۲۵۰۰، ۱۲۰۰، ۱۴۰۰ است.
بنابراین، برای هر پیکسل، ۳۲ بیت عمق رنگ داریم.
بنابراین، برای تصویر تمامصفحه، اوه، انگشت کافی ندارم، اما، مثلاً، حدود ۱۵ مگابایت حافظه، ۵۰ مگابایت، مصرف میکند.
بنابراین، اما، مثلاً، یک کلمه در سیستمعامل مانند ۶۴ بیت است، یعنی ۸ بایت است.
یک هزار کلمه فقط هشت، ۸ مگابایت است.
آره، 8K.
بنابراین، قطعاً یک تصویر ارزشش بیشتر از هزار کلمه است.
بله، این مهم است.
یک تصویر ۱۵ مگابایت است.
اگر برنامهتان ۱۰ یا ۱۰۰ تصویر داشته باشد چه؟
خب، عدد بزرگی است.
بنابراین، درواقع Bitmap همیشه، معمولاً بزرگترین مصرفکننده حافظه در برنامه شما است.
وقتی تصویر در حافظه رمزگشایی میشود، همانطور که گفتم، براساس ابعاد و عمق رنگ گسترش مییابد.
این موضوع به اندازه فایل یا اینکه در قالب JPEG یا PNG باشد مربوط نمیشود.
نه، رمزگشایی میشود و از حافظه استفاده میکند.
بنابراین، فقط چند تصویر با وضوح کامل بهراحتی صدها مگابایت از حافظه دستگاهتان را اشغال میکند.
و دوم اینکه وقتی حافظه بسیار کم است، سیستمعامل باید انرژی بیشتری برای مدیریت مصرف حافظه صرف کند.
این کار باعث میشود واسط کاربر بهنظر ناپایدار برسد.
یا ممکن است سیستمعامل برای آزاد کردن حافظه، برنامهتان را متوقف کند.
یا برنامهتان با خطای «کمبود حافظه» یا استثنای «کمبود حافظه» مواجه خواهد شد.
خوشبختانه خبر خوب این است که این چهره معمولاً با تلاش کمی موفقیت بزرگی بهدست میآورد.
بیشتر جهشهای حافظه ناشی از اشتباهات رایج است که میتوان از آنها اجتناب کرد و ما میتوانیم این مشکل را بدون هیچگونه تأثیری بر کیفیت تصویر برطرف کنیم.
اکنون میدانید که حافظه بیتمپ برای برنامه شما مهم است، نه فقط برای روان کردن آن، بلکه برای پایداری و حفظ کاربر.
بنابراین، امروز پنج بخش ساده را برای اجرای روان برنامهتان مرور خواهیم کرد.
ابتدا کیفیت فنی Google Play را بررسی میکنیم، یعنی الزامات کیفیت فنی جدید که شامل محدودیت حافظه آتی و جایی است که میتوانید بهراحتی محدودیت را بررسی کنید.
دوم، مطالعه موردی خودم را درباره ۵۰۰ برنامه Android همرسانی خواهم کرد، اینکه چگونه از آنها در حافظه استفاده میکنند، آیا این کار را بهدرستی انجام میدهند، یا چیزی هست که بتوانیم از آن یاد بگیریم؟
سپس، اصل نحوه بهینهسازی استفاده از حافظه بیتمپ را به شما نشان خواهم داد.
و درنهایت، چند ابزار را که میتواند به شما در بررسی برنامهتان کمک کند همرسانی خواهیم کرد.
Fung Lam
ابتدا، الزامات کیفی جدید Google Play.
هر چند وقت یکبار به «کنسول Google Play» سر میزنید؟
هر ماه؟
هر هفته؟
هرگز؟
اون خندهها یعنی بله، درسته؟
نه؟
شماره.
آیا از بخش «معیارهای کلیدی Android» بازدید کردید؟
بله، بله.
بله، پس شما قطعاً، میدانید قبلاز اینکه من این را بسازم.
بنابراین، در Google Play، الزامات کیفی جدیدی برای آستانه عملکرد وجود دارد.
ما در Android و Play میخواهیم کاربر از برنامهای با کیفیت خوب استفاده کند.
بنابراین، این آستانه هم چیزی است که سعی میکنیم بهترین را برای کاربر نهایی به ارمغان بیاوریم.
بهعبارت ساده، یعنی «برنامه شما، وقتی در پسزمینه یا در سطح پیشزمینه قابلمشاهده اجرا میشود، صدک ۹۰ استفاده از حافظه آن نباید بالاتر از این آستانه باشد.
یا درغیراینصورت، ممکن است رؤیتپذیری برنامهتان در «فروشگاه Play» کاهش یابد یا اتفاقی ازاینقبیل بیفتد.»
و اجرای آن از فوریه ۲۰۲۷ برای برنامهها و بازیها آغاز خواهد شد.
فراتر رفتن از این آستانه مستقیماً بر ارزیابی سلامت برنامهتان تأثیر میگذارد.
بنابراین، حتماً به کنسول Google Play بروید و بررسی کنید، «هی، صدک ۹۰ من امروز چیست؟»
و قبلاز مهلت تعیینشده، بهبود را اعمال کنید.
این پویانمایی است.
«سلام، اگر به کنسول Google Play بروید، میتوانید ببینید، بله، «معیارهای کلیدی Android» آنجا است.
بخشی از حافظه وجود دارد.
و میتوانید صدک ۹۰ را ببینید.
و اگر روی آن کلیک کنید، بله، میتوانید میانگین مصرف حافظه را درطول زمان ببینید.
همچنین سطوح مختلفی براساس RAM، فرم فاکتور، تراکم صفحه، و نسخه سیستمعامل وجود دارد که به شما کمک میکند گلوگاه حافظه را در دستگاههای سطح پایه خاص یا در نسخههای خاص برنامه جداسازی کنید.
اکنون میدانم که ممکن است بگویید: «بسیار خوب، ممنون که به من گفتید برنامهام از آستانه فراتر رفته است.
ولی واقعاً نمیدانم مشکل چیست.
کمک میکنید؟»
بله، به همین دلیل است که من اینجا هستم تا به شما کمک کنم.
سعی میکنم آن را برای شما آسان کنم، مثلاً، اگر درحال تماشای تلویزیون باشید و تلویزیون شما تصویر یا صدا نداشته باشد، چه میکنید؟
شما دفترچه راهنمای تلویزیون را برمیدارید، به چند صفحه آخر میروید، که معمولاً راهنمای عیبیابی است، و میگوید: «سلام، اگر تلویزیون شما صدا ندارد، این دکمه را امتحان کنید، یا آن دکمه را امتحان کنید.»
بله، بهطور مشابه، امروز چند مشکل رایج را شناسایی کردم.
و امیدواریم این راهنمای عیبیابی شما باشد.
معمولاً وقتی دفترچه راهنمای تلویزیون را میخوانیم، باید از صفحه اول شروع کنیم که معمولاً کیتهای ابزار تشخیص خرابی است تا واقعاً به شما کمک کند نحوه کار تلویزیون را درک کنید.
ولی حالا که میدانم اینها کمتر جالب هستند، لطفاً اجازه دهید امروز ابتدا از عیبیابی شروع کنم و بعداً به اینها برگردیم.
و راستش را بخواهید، من واقعاً اینگونه دفترچه راهنمای تلویزیون را میخوانم.
برای کشف رایجترین مشکل حافظه بیتمپ تخصیص بیتمپ در بومسازگان Android، حدود ۵۰۰ برنامه، برنامههای محبوب، را در Android بررسی کردم.
من واقعاً برنامه را اجرا میکنم، برنامه را شروع میکنم، به سیستم وارد میشوم، به صفحههای مختلف میروم، عمیق میشوم، و برمیگردم.
و برنامه را در حافظه دسترسی تصادفی متفاوت و دستگاههای مختلف نیز آزمایش میکنم، مثلاً دستگاه Android Go با ۲ گیگابایت حافظه، برخی دستگاههای سطح پایه با ۴ گیگابایت، برخی دستگاههای سطح متوسط با ۴، ۶، تا ۸ گیگابایت، یا برخی دستگاههای تاشو ممتاز با صفحهنمایش واقعاً بزرگ، کیفیت صفحهنمایش بسیار خوب، و ۱۶ گیگابایت حافظه.
ما میزان استفاده از حافظه را در این دستگاه در وضعیتهای مختلف برنامه اندازهگیری کردیم، رونوشت پشته را ثبت کردیم، و از ابزارهای مختلف، ازجمله Perfetto AI Skill، برای تجزیهوتحلیل تخصیص استفاده کردیم تا الگوهای ضد رایج، بله، اشتباهات رایجی را که در مقیاس بزرگ مرتکب میشویم، شناسایی کنیم.
بنابراین، حافظه در حالتهای مختلف فعالیت به این صورت تغییر میکند.
و با این تحلیل، بیشاز ۱۵ مشکل رایج را شناسایی میکنیم، از تصویر بیشازحد بزرگ گرفته تا افزایش حافظه پنهان پسزمینه.
بنابراین این مانند هسته اصلی است، مطالعه موردی که ما به اشتراک خواهیم گذاشت، امروز کاوش خواهیم کرد.
Fung Lam
بله، کل مطالعه موردی درباره این موضوع است که اکنون همرسانی میکنم و درباره پژوهش ۵۰۰ برنامه است.
چون اینجا هستید، فکر میکنم همه ما Android را دوست داریم، درست است؟
همه ما Android را دوست داریم.
فرض کنیم چنین فردی وجود دارد، یکی از ما هم عاشق Android است، اما او در توسعه Android متخصص نیست.
بنابراین، او یک برنامه Android برای سفارش مجسمه Android ساخت.
اما چند اشتباه وجود دارد.
بیایید صحبت کنیم بیایید ببینیم شاید این اولین اشتباه باشد.
«سلام، در صفحه محصول، چیدمان دو ستونی داریم که مجسمههای مختلف Android را نشان میدهد.
برای هر ستون، اندازه تصویر حدود ۴۰۰ × ۴۰۰ پیکسل است.
اما برنامه درواقع یک تصویر را از سرور بارگیری میکند، صرفنظر از اینکه اندازه صفحهنمایش من چقدر است.
درحال بارگیری تصویر ۳۰۰۰ × ۳۰۰۰ است.
بله، چون توسعهدهنده میگوید: «هی، Android خیلی زیبا و شگفتانگیز است.
میخواهم بهترین را به کاربر نشان دهم.»
ولی درواقع نه، چون مثلاً، هی، با ابعاد نمای تصویر محدود شده است.
بنابراین، حتی اگر تصویر بزرگ را بارگیری کنید، بهترین تصویر به کاربر نشان داده نمیشود.
علاوهبراین، این تصویر بزرگ باعث مصرف حافظه اضافی شده است.
و حتی اگر آن را نمونهکاهی کنیم، فرایند نمونهکاهی نیز از حافظه اضافی استفاده میکند.
چگونه باید به این توسعهدهنده کمک کنیم تا آن را برطرف کند؟
خب، خب، اگر درباره CDN بدانید، CDN درواقع میتواند برخیاز عملیات تصویر را انجام دهد، مثل برش، تغییر اندازه، چرخش، اعمال فیلتر، تبدیل تصویر از مربع به گرد.
با درخواست HTTP، شبکه توزیع محتوا میتواند کاری را انجام دهد.
بنابراین، حتی اگر سمت سرور شما فقط یک تصویر را میزبانی میکند، میتوانید از CDN برای ارسال تصویر تغییراندازه دادهشده به دستگاهتان استفاده کنید.
بنابراین، شماره ۱، بله وقتی تصویر را درخواست میکنید، سعی کنید از CDN استفاده کنید، بنابراین تصویر را بهعنوان اندازه ImageView درخواست خواهید کرد.
اما اگر ممکن نیست، مثلاً گاهی اوقات ممکن است کمی بزرگتر از ImageView باشد، بهخاطر داشته باشید که برای تغییر اندازه آن در کد، نمونهکاهی کنید.
بنابراین، وقتی در حافظه بار میشود، تصویر کوچکتری بار میشود، نه تصویر با وضوح کامل.
در اینجا نمونه کدی برای Coil آورده شده است.
// 1. Ask CDN for the exact UI card dimensions (e.g. 540x675)
val sizedUrl = "$cdnBaseUrl?w=$cardWidthPx&h=$cardHeightPx&fmt=webp"
// 2. Enforce exact downsampling + Hardware Bitmap config
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(sizedUrl)
.size(cardWidthPx, cardHeightPx) // Never use Size.ORIGINAL!
.precision(Precision.EXACT) // Downsample to exact card size
.bitmapConfig(Bitmap.Config.HARDWARE)
.build(),
contentDescription = product.title
)میتوانید اندازه را بهصورت عبارت زرد رنگ ارسال کنید.
بله، میتوانید اندازه را بهعنوان اچتیتیپی در درخواست اچتیتیپی ارسال کنید.
و سپس، اینجا، در کد برجستهشده، میتوانید برجستهسازی را ببینید، بله، بله، از Size.ORIGINAL استفاده نکنید.
بله، از عرض و ارتفاع استفاده کنید تا بهعنوان پارامتر ارسال شود.
از دقت برای نمونهکاهی تصویر به اندازه دقیق استفاده کنید.
و درصورت امکان، از Bitmap.Config.HARDWARE استفاده کنید که حافظه کمتری نیز مصرف میکند زیرا تصویر مستقیماً در VRAM ذخیره میشود.
بهطور مشابه، در این برنامه میگوید، سلام، آنها نسخه جدیدی از کلکسیونیهای Android را منتشر میکنند، بنابراین تصویر طولانیای وجود دارد که کاربر میتواند برای دیدن طراحیهای مختلف مجسمه جدید Android، آن را تند به چپ و راست بکشد.
ولی مشکل دیگری هم اینجا وجود دارد.
برای تصویر طولانی، درواقع بخش زیادی از آن خارج از صفحه است، یعنی درواقع کاربر نمیتواند آن را ببیند، اما تصویر در حافظه بار میشود.
آیا میتوانیم آن را بهتر انجام دهیم؟
بله.
میتوانیم آن را به تصویر کوچکتر و کوچکتری تقسیم کنیم و فقط تصویری را که روی صفحه نشان داده میشود بار کنیم.
آنهایی که خارج از صفحه هستند بار نمیشوند یا بارگیریشان لغو میشود.
بنابراین، حافظه کمتری مصرف خواهد کرد.
بهطور مشابه، اوه، پس در این مورد، حدود ۶۰٪ از حافظه را ذخیره کرد.
البته اگر آن را به مستطیل کوچکتری برش دهید، فضای بیشتری ذخیره خواهد شد.
اما بهطور مشابه، اگر کاربری بخواهد ویژگیای برای کاربر ارائه دهد، مثلاً بتوانید زومپیش کنید و زومپیش کنید تا همه جزئیات مجسمه Android را ببینید، یک اشتباه رایج این است که واقعاً دوباره یک تصویر بسیار بزرگ را بار میکنند تا بتواند دوباره زومپیش و زومپیش شود.
اما نه، مجبور نیستید این کار را بکنید.
روش بهتر این است که وقتی کاربر روی تصویر اصلی با وضوح پایین که قبلاً بار شده است بزرگنمایی میکند و وقتی کاربر نیشگون گرفتن را متوقف میکند، مختصات را محاسبه کنید و سپس وضوح کامل را بار کنید-- فقط آن ناحیه را بار کنید، ناحیه محاسبهشده از تصویر با وضوح بالا.
بنابراین، فقط آن ناحیه قابلمشاهده در حافظه بار میشود.
چگونه این کار را انجام دهم؟
اوه، یک API به نام BitmapRegionDecoder وجود دارد که به شما کمک میکند تا یک منطقه خاص از تصویر را بارگیری کنید.
و البته، برخیاز ابزارها و کتابخانهها نیز میتوانند به شما کمک کنند.
میتوانید آن را در GitHub جستجو کنید.
برخیاز برنامههای کمیک یا برخی نقشهها ازقبل از این استفاده میکنند.
میتوانید از آنها بهعنوان مرجع استفاده کنید.
علاوهبراین، ما انسان هستیم و اشتباه میکنیم.
هوش مصنوعی هم اشتباه میکند.
بنابراین، برای جلوگیری از اشتباه، میتوانیم هنگام بار کردن تصویر، محدودیتهایی نیز اضافه کنیم.
برای مثال، در این مورد، میتوانم بگویم، «هی، بیایید از اندازه پنجره یا ابعاد صفحه بهعنوان حد استفاده کنیم و آن را در تابع min، حداقل عرض درخواست یا عرض مرز قرار دهیم.»
// Query active window bounds instead of hardcoding 1080 / 1920
val bounds = WindowMetricsCalculator.getOrCreate()
.computeCurrentWindowMetrics(activity).bounds
// Clamp target dimensions so Bitmap NEVER exceeds the active window
val safeWidth = minOf(requestedWidthPx, bounds.width())
val safeHeight = minOf(requestedHeightPx, bounds.height())
val bitmap = Bitmap.createBitmap(safeWidth, safeHeight, config)بنابراین، با این تابع، هر زمان که تصویری را بارگذاری میکنم، هرگز از مرز خارج نخواهد شد.
بنابراین، تصویری بزرگتر از وضوح صفحهنمایش بار نمیکند.
یادتان باشد، فقط منطقه قابلمشاهده تصویر را رمزگشایی کنید تا حافظه شما ذخیره شود.
Fung Lam
و دوباره، بگویید، هی، این برنامه، در واقع، از یک توسعهدهنده نیست.
درواقع، این برای همیاران متعدد است و ممکن است آنها از ابزارهای مختلفی استفاده کنند.
برای مثال، بخشی از آن استفاده از «اوه ببخشید» است.
اجازه دهید دوباره شروع کنم.
برای این برنامه هم همینطور.
سلام، من در صفحه محصول هستم، فهرستی از محصولات دارم و این تصویر را دیدم، آن را به سبد خرید اضافه میکنم.
چند دقیقه بعد، به تسویهحساب میروم، هی، همان تصویر دوباره ظاهر میشود.
اما آیا این واقعاً همان تصویر است یا درواقع دو شیء متفاوت در حافظه است؟
بله، یکی از اشتباهات رایجی که پیدا کردیم این است که برنامه تصاویر زیادی را بار میکند.
بااینحال، تصاویر تکراری زیادی هم در حافظه وجود دارد.
اگر از بارکننده تصویر استفاده میکنید، بله، درواقع میتوانید کلیدی را برای انجام ذخیرهسازی موقت ارسال کنید.
بنابراین وقتی از همان تصویر استفاده میشود، میتوانید از شیء Bitmap که قبلاً اختصاص داده شده است استفاده کنید، نه اینکه شیء جدیدی ایجاد کنید.
در Android Studio، میتوانید از «تحلیلگر حافظه» استفاده کنید تا ببینید آیا برنامهتان بیتمپ تکراری دارد یا نه.
من آن را در جلسه Google I/O پوشش دادم، بنابراین میتوانید به YouTube برگردید تا ویدیو را تماشا کنید.
And if I'm not using Android Studio, there are also adb command for you to capture the heap dump and then you could use Perfetto heap graph SQL language to query if my app is having duplicate Bitmap.
این یک نمونه کد در Coil است، نحوه افزودن کلید برای ذخیره موقت Bitmap.
// Strip volatile query tokens (?from=list / ?sig=...) for cache key
val canonicalKey = MemoryCache.Key(productUrl.substringBefore("?"))
// On Checkout Screen: reuse the 200x200 Bitmap already loaded in the List!
AsyncImage(
model = ImageRequest.Builder(context)
.data(productUrl)
.placeholderMemoryCacheKey(canonicalKey) // Instant zero-decode reuse!
.memoryCacheKey(canonicalKey)
.diskCacheKey(productUrl.substringBefore("?"))
.build(),
contentDescription = product.title
)و حالا، بله.
به مثال قبلی برمیگردم.
سلام، برنامه من توسعهدهنده دیگری دارد.
برخیاز آن از Views استفاده میکند، بنابراین از Glide بهعنوان بارکننده تصویر استفاده میکند.
And some of them are using Coil as a because they're using Compose.
So is this I you just mentioned oh, image loader help us to do caching, so is this anything wrong if I have more than one image loader?
قطعاً مشکلی وجود دارد.
بنابراین برای بارکننده تصویر، بهطور پیشفرض، از X٪ اندازه حافظه پشته بهعنوان اندازه حافظه نهان پیشفرض استفاده میکنند.
بنابراین اگر یک بارکننده تصویر داشته باشید، از X٪ حافظه شما استفاده میشود.
اگر دو بارکننده تصویر داشته باشید، حافظه ۲ برابر خواهد شد.
اگر سه بارکننده تصویر داشته باشید، ۳ برابر خواهد شد.
و اگر بارکننده تصویر شما از همان استخر مشترک استفاده نکند، همین اتفاق خواهد افتاد.
مثلاً در صفحه محصول، تصویری را بااستفاده از Glide بار میکنم و سپس در صفحه تسویهحساب، همان تصویر بااستفاده از Coil بار میشود.
بااینحال، دو تصویر در خاطره وجود خواهد داشت.
این خوب نیست.
بله، بنابراین برای رفع آن، البته، در حالت ایدهآل، ما فقط از یک بارگذار تصویر یا یک استخر مشترک برای ذخیره موقت استفاده میکنیم.
اما اگر این کار مثلاً به مهندسی زیادی نیاز داشته باشد، نمیتوانم آن را انجام دهم.
Oh, a easier fix will be, hey, I measure how many percent of the image will be handled by this image loader, how many percent of the image will be handled by another image loader and set weight for different image loader, just like this code.
val totalBudgetBytes = computeAdaptiveCacheBytes(context)
val numberOfLoaders = 2 // e.g., Team A (Glide) + Team B (Coil)
// Approach A (With Stats): Weight by image traffic (70% Team A / 30% Team B)
val loaderABudget = (totalBudgetBytes * 0.70).toInt()
val loaderBBudget = (totalBudgetBytes * 0.30).toInt()
// Approach B (Simple Fix without stats): Divide by number of loaders!
val simpleSplitBudget = totalBudgetBytes / numberOfLoaders
MemoryCache.Builder()
.maxSizeBytes(simpleSplitBudget)
.build()میتوانم loaderABudget را روی ۷۰٪ و loaderBBudget را روی ۳۰٪ تنظیم کنم، این هم روش دیگری است.
اگر واقعاً نمیدانید هر بارگذارنده چقدر تصویر را مدیریت خواهد کرد، بازهم بهتر است فقط تقسیم کنید.
سلام، من دو بارکننده تصویر دارم، تقسیم بر ۲؛ سه بارکننده تصویر، تقسیم بر ۳ بهجای استفاده از مقدار پیشفرض چون دراینصورت از حافظه بیشازحد استفاده میشود.
و فرض کنید، هی، هنوز، در صفحه محصول، ما انتظار داریم کاربر برای دیدن بیشتر محصول پیمایش کند.
بنابراین معمولاً، ما ممکن است بگوییم، بیایید حدود ۵۰ تصویر یا ۵۰ مگابایت تصویر را ذخیره کنیم.
اما اگر برنامه به پسزمینه برود چه اتفاقی میافتد؟
اگر برنامه در پسزمینه باشد، ممکن است کاربر درحال مرور نباشد.
اگر کاربر درحال مرور نیست، آیا همچنان باید این مقدار از تصویر حافظه نهان را در حافظه نگه دارم؟
نه، باید آن را واگذار کنیم.
بله، باید به callback onTrimMemory احترام بگذاریم.
بله، درواقع، درحالحاضر بسیاری از برنامهها callback (فراخوانی برگشتی) onTrimMemory را پیادهسازی نمیکنند یا به آن احترام نمیگذارند.
برای callback (پاسخ تماس) onTrimMemory، دو سیگنال وجود دارد که باید به آنها توجه کنیم.
یکی UI_HIDDEN است که مثل این است که کاربر دکمه صفحه اصلی را فشار داده باشد و بلافاصله بعداز فشار دادن دکمه صفحه اصلی نیست، بلکه مدتی بعداز فشار دادن دکمه صفحه اصلی است، یعنی «برنامه شما درواقع در وضعیتی است که هیچ میانای کاربری قابلمشاهدهای برای کاربر وجود ندارد.»
و میتوانیم انتظار داشته باشیم که «هی، کاربر از برنامهتان استفاده نمیکند.
باید منابع را برای سیستمعامل آزاد کنید.»
بنابراین باید اندازه حافظه نهان را کاهش دهیم.
و دیگری TRIM_MEMORY_BACKGROUND است، یعنی برنامه ازقبل برای مدتی طولانی در پسزمینه بوده است و اکنون سیستمعامل واقعاً به منابع نیاز دارد.
در این مرحله، باید حافظه را تا حد امکان، تقریباً ۱۰۰٪، آزاد کنید.
با انجام این کار، بله، احتمال و خطر ازکار افتادن برنامهتان توسط سیستمعامل را نیز کاهش میدهید.
چون مثلاً «هی، آنها حافظه میخواهند، شما به آنها میدهید، سپس آن سیستمعامل آن را مدیریت میکند.»
اگر نه، سیستمعامل چارهای ندارد و باید کاری انجام دهد.
Fung Lam
بنابراین اشتباه بزرگ دیگری که میبینم این است که «سلام، من همین الان اشاره کردم که برنامه شما، اگر برنامه، اگر به پسزمینه برود، باید از حافظه کمتری استفاده کند.»
اما دیدم که برنامهها در پسزمینه واقعاً از حافظه بیشتری استفاده میکنند، حافظه بیشتر.
چرا؟
بههمین دلیل است که من تحقیق میکنم.
سعی کردم دلیلش را بفهمم.
و یکی از دلایل این است که «هی، در برنامهشان، پویانمایی یا گردونه دارند.
برای مثال، هر ۱۵ ثانیه یک تصویر جدید بارگیری میکنند و تصویر جدیدی به کاربر نشان میدهند.»
اما توسعهدهنده پویانمایی را هنگام رفتن برنامه به پسزمینه متوقف نکرده است.
بنابراین، حتی اگر برنامه در پسزمینه باشد، کد همچنان درحال اجرا است، همچنان تصویر جدید دریافت میکند، تصویر جدید را کدگشایی میکند، و از حافظه بیشتری استفاده میکند.
بنابراین این درواقع کاری است که برنامه نباید انجام دهد.
بله.
وقتی برنامه به پسزمینه میرود، باید همه پویانماییها، ذخیرهسازی در حافظه نهان، یا کارهای نامربوط را متوقف کنیم.
بنابراین اگر از چرخه عمر فعالیت آگاه هستید، رویداد onPause، onStop وجود دارد، مطمئن شوید که onStop، onPause، onDestroyView را برای توقف پویانمایی، بله، برای توقف درخواستهای کشیدن تصویر، یا موارد دیگر مرتبط با کاری که نیاز به توجه کاربر ندارد، پیادهسازی کنید.
override fun onStop() {
super.onStop()
// 1. Stop animations so they don't decode next frames in background!
bannerAnimationView.cancelAnimation()
animatedImageDrawable?.stop()
// 2. Cancel next-image buffering / prefetch queues!
nextImagesBufferingJob?.cancel()
Glide.with(this).pauseRequests()
}
override fun onDestroyView() {
super.onDestroyView()
bannerAnimationView.clearAnimation() // Release resources!
}Fung Lam
I know I have talked so many case with you, but I said, "I have more than 15 cases."
میدانم اگر همه موارد را با شما درمیان بگذارم ممکن است همه چیز را فراموش کنید.
وقتی به خانه برمیگردید، فقط در Google جستجو میکنید: «سلام، چطور میتوانم مشکلات حافظهام را برطرف کنم؟»
بنابراین، بهخاطر سپردن آن را برایتان آسان میکنم.
بله.
من آن را به یک چارچوب بسیار شناخته شده 3R پیوند خواهم داد تا به شما کمک کند آنها را به خاطر بسپارید.
این چارچوب 3R، فکر میکنم همه باید درباره آن شنیده باشند.
اولین مورد کاهش است، نحوه کاهش حافظه هنگام بار کردن تصویر.
البته، همانطور که اشاره کردم، میتوانیم نمونهکاهی کنیم و وقتی تصویر را از سرور دریافت میکنیم، ابعاد درست آن را بهدست آوریم.
عمق رنگ را بهینهسازی میکنیم یا درصورت امکان از Bitmap سختافزاری استفاده میکنیم.
و اگر از CDN استفاده نمیکنید، از CDN استفاده کنید.
بنابراین وقتی تصویر را از سرور دریافت میکنیم، فقط تصویری را دریافت میکنیم که بهترین تناسب را با اندازه دارد.
استفاده مجدد، بنابراین ما از حافظه نهان استفاده میکنیم تا بارگذاری تصویر در حافظه مجدداً استفاده شود و تصویر بیتمپ تکراری در حافظه خود نداشته باشیم.
و البته سعی کنید چند بارگذارنده تصویر در کدتان نداشته باشید، چون کارایی کمتری دارد.
کاهش، استفاده مجدد، و مورد باقیمانده چیست؟
آیا میدانید؟
بازیافت.
مجدداً امتحان میکنید؟
بازیافت، بله، بازیافت.
بله، ما کاهش، استفاده مجدد، و بازیافت را در دبستان یاد گرفتیم، هرچند کدنویسی را در دبستان یاد نمیگیریم.
آره، بازیافت، آره.
بله، درباره نحوه آزاد کردن حافظه است تا برنامههای دیگر یا سیستمعامل بتوانند از حافظه استفاده کنند.
در آنجا چیزهایی مثل بازیافت وجود دارد.
ابتدا اشاره کردم که رویداد onTrimMemory وجود دارد، مثلاً دو سیگنال وجود دارد که سیستمعامل وقتی واسط کاربر شما پنهان است یا سیستمعامل واقعاً به منابع نیاز دارد برای شما ارسال میکند.
بله، سعی کن این درخواست را انجام دهی.
و البته، اگر پویانمایی، بارکننده تصویر، یا کار دیگری دارید که در پیشزمینه اتفاق میافتد، مطمئن شوید که آن را وقتی که در رویداد onPause، onStop است متوقف کنید.
پس کاهش دهید، دوباره استفاده کنید، بازیافت کنید.
بهخاطر سپردن آن آسان است، نه؟
Fung Lam
اکنون به بخش کمتر جالب راهنمای تلویزیون برمیگردیم.
چگونه از جعبهابزار مختلف برای شناسایی و اندازهگیری مؤثر این مشکل حافظه استفاده میکنیم؟
بنابراین، اولین مورد «کنسول Google Play»، «معیارهای کلیدی Android»، و Reporting API است.
بله، صدک نودم را در «کنسول Google Play» بررسی کنید.
اکنون کجا هستید؟
یا اگر از صفحه وب بازدید نمیکنید، از Reporting API استفاده کنید.
همچنان میتوانید از API برای دریافت مقدار استفاده کنید.
بله، مطمئن شوید، بله، آستانه ۲۰۰ مگابایت و ۴۰۰ مگابایت است.
مطمئن شوید که از آستانه فراتر نرفتهاید.
همچنین فرمان ADB وجود دارد که به شما کمک میکند رونوشت پشته را ضبط کنید یا رویدادی را برای شبیهسازی ارسال کنید، مثلاً، برنامه من اکنون در مرحله UI_HIDDEN است.
adb shell dumpsys meminfo <pkg> # watch Native Heap!
adb shell am send-trim-memory <pkg> UI_HIDDENاگر از «استودیو Android» استفاده میکنید، خوب است.
بااینحال، این همیشه دوست خوبی برای شما است.
میتوانید از نمایهساز Android Studio برای ضبط رونوشت پشته استفاده کنید تا واسط کاربریای داشته باشید که ببینید واقعاً مصرف حافظه شما چقدر است و تخصیص حافظه واقعاً چیست.
و البته، درباره هوش مصنوعی با Gemini در «استودیو Android» یا دیگر مهارتهای Android موجود صحبت میکنیم.
این ابزار به شما کمک میکند رونوشت پشته را تجزیهوتحلیل کنید و بااستفاده از ردگیریهای Perfetto SQL به شما کمک میکند Bitmap را پیدا کنید تا معمولاً مشخص شود مالک حافظه نهان کیست.
آیا تصویر من با Glide بار میشود یا با Coil؟
آیا نشت حافظه دیگری در برنامهام وجود دارد؟
میدانم برخیاز شما میگویید: «راستش من دیگر از IDE استفاده نمیکنم.
من مهندس پیامواره هستم.
من از، بله، من فقط پیامواره را با عامل هوش مصنوعی ارسال میکنم تا کد را بنویسد.»
بله، میدانم.
در اوقات فراغتم کمی کدنویسی حسی هم انجام میدهم.
بنابراین، من این را فقط بهعنوان نمونه با شما همرسانی میکنم.
بله، این فقط بخشی از پیاموارهای است که برای پژوهش درباره ۵۰۰ برنامه استفاده میکنم.
بله، فقط بخشی از آن.
میدانید که صفحهنمایش بزرگ نیست.
اگر بیشتر میگذاشتم، بهسختی میتوانستید آن را ببینید.
اما این به شما ایدهای میدهد که چگونه میتوانید از هوش مصنوعی برای خودکارسازی، ثبت استفاده از حافظه، ازجمله رونوشت پشته، و تجزیهوتحلیل و ارائه یافتهها به شما استفاده کنید.
و بله، میتوانم اینجا یک جمله دیگر هم اضافه کنم، مثلاً «لطفاً کد اصلاحشده را هم برای کدم ارائه کن.»
بله، امیدوارم این بتواند ترسهای شما را کاهش دهد، مثلاً بگویید: «هی، حافظه دامنه بزرگی است.
من در دانشگاه در رشته علوم رایانه، سیستمعامل یا معماری سیستم رایانه نخواندهام.
اما درواقع، این کار آنقدرها هم ترسناک نیست، آنقدرها هم سخت نیست.»
بله، این کار برای شما آسان است تا برنامهتان را تجزیهوتحلیل کنید.
لطفاً آن را امتحان کنید.
اما البته هوش مصنوعی اشتباه میکند، بنابراین دانش بنیادی درباره نحوه استفاده از حافظه و همچنین نحوه اختصاص حافظه توسط سیستمعامل همچنان مهم است.
به همین دلیل است که به سخنرانی من میآیند و ویدیو ما را در YouTube تماشا میکنند.
Fung Lam
بنابراین، برای خلاصه کردن، چند نکته مهم داریم.
ابتدا، کاهش دهید.
مطمئن شوید اندازه تصویر درست باشد.
تصویر را به تصویر کوچکتر تقسیم میکنید تا همه چیز را در حافظه بار نکنید، بله، فقط برای بار کردن تصویری که برای کاربر در حافظه قابلمشاهده است.
هرزمان ممکن بود، از bitmapConfig یا RGB_565 سختافزاری استفاده کنید.
استفاده مجدد، بنابراین مطمئن شوید که استراتژی حافظه نهان دارید، از بارکننده تصویر استفاده کنید تا به شما کمک کند تعداد بیتمپهای تکراری را در حافظه کاهش دهید.
و البته، از چند تصویر Bitmap استفاده نکنید، حتی اگر سعی دارید اندازه حافظه پنهان پیشفرض آن بارکننده تصویر را بازنویسی کنید.
و ما دوباره استفاده نمیکنیم، بازیافت میکنیم.
بله.
مدیریت حافظه برنامهتان باید با وضعیت برنامه سازگار باشد.
اگر واسط کاربر پنهان باشد، بله، باید از حافظه کمتری استفاده کنید.
اگر سیستمعامل واقعاً از شما میخواهد حافظه را آزاد کنید، تا جایی که میتوانید حافظه آزاد کنید.
و تخلیه منابع را متوقف کنید، چه Bitmap باشد، چه برق، یا منابع محاسباتی.
بله، زمانسنج برنما را متوقف کنید، پویانمایی را متوقف کنید، جمعآوری تصویر را متوقف کنید.
و درنهایت، حتماً نظارت کنید.
بهطور منظم به «کنسول Google Play» بروید.
مطمئن شوید که این یک تمرین است.
بله، این به شما کمک میکند تا بفهمید، هی، وضعیت فعلی صدک ۹۰ استفاده از حافظه برنامه من در کل جمعیت کاربران چیست؟
این کار همچنین به شما کمک میکند تا مشکل را زودتر شناسایی کنید.
بله.
برای نمایهسازی محلی، میتوانید از تحلیلگر حافظه «استودیو Android» استفاده کنید.
ما همچنین چند فرمان ADB داریم.
ما همچنین مهارت Android داریم.
اما مطمئن شوید که میخواهید آمار مربوط به کاربر واقعی را ببینید و «کنسول Google Play» مکان مناسبی برای شما است.
همچنین Google Play Reporting API وجود دارد که میتوانید بااستفاده از API آن را دریافت کنید و نیازی به ورود به سیستم «کنسول Play» نیست.
مثل همیشه، اگر میخواهید بیشتر بدانید، به developer.android.com/memory مراجعه کنید.
با این برنامه، مصرف را کاهش دهید، دوباره استفاده کنید، و بازیافت کنید.
ما میتوانیم یک اکوسیستم Android بهتر بسازیم. امیدوارم باهم بتوانیم اکوسیستم Android بهتری بسازیم تا بر چالشی که با آن مواجه هستیم غلبه کنیم.
متشکریم و امیدواریم از کدنویسی لذت ببرید.
بهزودی در فضای باز ساعت اداری خواهم داشت، بنابراین اگر سؤالی دارید، حتماً بیایید.
و سخنران بعدی، خب، کمی وقت دارد، پس لطفاً منتظر بمانید تا سخنران بعدی روی صحنه بیاید و بسیار سپاسگزارم.
// 1. Ask CDN for the exact UI card dimensions (e.g. 540x675)
val sizedUrl = "$cdnBaseUrl?w=$cardWidthPx&h=$cardHeightPx&fmt=webp"
// 2. Enforce exact downsampling + Hardware Bitmap config
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(sizedUrl)
.size(cardWidthPx, cardHeightPx) // Never use Size.ORIGINAL!
.precision(Precision.EXACT) // Downsample to exact card size
.bitmapConfig(Bitmap.Config.HARDWARE)
.build(),
contentDescription = product.title
)// Query active window bounds instead of hardcoding 1080 / 1920
val bounds = WindowMetricsCalculator.getOrCreate()
.computeCurrentWindowMetrics(activity).bounds
// Clamp target dimensions so Bitmap NEVER exceeds the active window
val safeWidth = minOf(requestedWidthPx, bounds.width())
val safeHeight = minOf(requestedHeightPx, bounds.height())
val bitmap = Bitmap.createBitmap(safeWidth, safeHeight, config)// Strip volatile query tokens (?from=list / ?sig=...) for cache key
val canonicalKey = MemoryCache.Key(productUrl.substringBefore("?"))
// On Checkout Screen: reuse the 200x200 Bitmap already loaded in the List!
AsyncImage(
model = ImageRequest.Builder(context)
.data(productUrl)
.placeholderMemoryCacheKey(canonicalKey) // Instant zero-decode reuse!
.memoryCacheKey(canonicalKey)
.diskCacheKey(productUrl.substringBefore("?"))
.build(),
contentDescription = product.title
)val totalBudgetBytes = computeAdaptiveCacheBytes(context)
val numberOfLoaders = 2 // e.g., Team A (Glide) + Team B (Coil)
// Approach A (With Stats): Weight by image traffic (70% Team A / 30% Team B)
val loaderABudget = (totalBudgetBytes * 0.70).toInt()
val loaderBBudget = (totalBudgetBytes * 0.30).toInt()
// Approach B (Simple Fix without stats): Divide by number of loaders!
val simpleSplitBudget = totalBudgetBytes / numberOfLoaders
MemoryCache.Builder()
.maxSizeBytes(simpleSplitBudget)
.build()override fun onStop() {
super.onStop()
// 1. Stop animations so they don't decode next frames in background!
bannerAnimationView.cancelAnimation()
animatedImageDrawable?.stop()
// 2. Cancel next-image buffering / prefetch queues!
nextImagesBufferingJob?.cancel()
Glide.with(this).pauseRequests()
}
override fun onDestroyView() {
super.onDestroyView()
bannerAnimationView.clearAnimation() // Release resources!
}adb shell dumpsys meminfo <pkg> # watch Native Heap!
adb shell am send-trim-memory <pkg> UI_HIDDEN