تحميل الصور وتخزينها مؤقتًا وإدارة الذاكرة
تحميل الصور وتخزينها مؤقتًا وإدارة الذاكرة
تُعدّ الصور في الغالب المساهم الأكبر في استهلاك الذاكرة في تطبيقات Flutter. فصورة شبكية بدقة كاملة، حين تُفكَّك إلى بكسلات خام، يمكن أن تستهلك عشرات الميغابايتات لكل إطار. لذا فإن فهم كيفية تحميل الصور بكفاءة، وتخزينها مؤقتًا بذكاء، وإزالتها من الذاكرة حين تنتهي الحاجة إليها، أمرٌ لا غنى عنه لبناء تطبيقات سلسة ذات جودة إنتاجية.
لماذا تُهمّ ذاكرة الصور؟
تُفكّك Flutter كل صورة إلى نقطة بيانات RGBA خام قبل دمجها على الشاشة. فصورة بدقة 4000×3000 بكسل مُفكَّكة بالدقة الكاملة تستهلك نحو 48 ميغابايت من ذاكرة GPU (4000 × 3000 × 4 بايت). حين تعرض قائمة عشرات هذه الصور في آنٍ واحد، يمكن أن يتجاوز الحجم الكلي للذاكرة حدود الجهاز ويُسبّب خطأ OutOfMemoryError أو يجعل نظام التشغيل يُنهي العملية.
تحتفظ ذاكرة التخزين المؤقت للصور في Flutter (PaintingBinding.instance.imageCache) بالنقطات المُفكَّكة في الذاكرة كي لا تحتاج الصورة ذاتها إلى إعادة تحميل أو فك تشفير حين تعود إلى الظهور. تحتفظ الذاكرة المؤقتة الافتراضية بما يصل إلى 1,000 صورة أو 100 ميغابايت من البيانات المُفكَّكة، أيهما بلغ حده أولًا.
استخدام cached_network_image
لا يُبقي ودجت Image.network المدمج في Flutter الصور على القرص بين تشغيلات التطبيق. تُضيف حزمة cached_network_image ذاكرة مؤقتة دائمة على القرص تجعل الصور المُنزَّلة تبقى بعد إعادة التشغيل. كما توفر الحزمة دعمًا مدمجًا لودجت العنصر النائب وودجت الخطأ.
أضف الاعتمادية إلى pubspec.yaml:
dependencies:
cached_network_image: ^3.3.1
الاستخدام الأساسي مع عنصر نائب وودجت خطأ:
import 'package:cached_network_image/cached_network_image.dart';
CachedNetworkImage(
imageUrl: 'https://example.com/photo.jpg',
placeholder: (context, url) => const CircularProgressIndicator(),
errorWidget: (context, url, error) => const Icon(Icons.broken_image),
fit: BoxFit.cover,
width: 300,
height: 200,
)
CachedNetworkImage على Image.network في أي تطبيق إنتاجي. تمنع ذاكرة القرص المؤقتة وحدها مئات الطلبات الشبكية الزائدة في كل جلسة، مما يُحسّن بشكل ملحوظ الأداء المُدرَك على الاتصالات البطيئة.تغيير حجم أهداف الفك باستخدام cacheWidth و cacheHeight
حتى عند استخدام ذاكرة القرص المؤقتة، لا تزال Flutter تُفكّك الصورة بدقتها الكاملة في الذاكرة افتراضيًا. إن عرضت صورة بدقة 4K في مصغّرة بحجم 150×150 بكسل، فأنت تهدر كميات هائلة من الذاكرة. يوجّه غلاف ResizeImage والمعاملان cacheWidth / cacheHeight في ودجات Image مُفكِّكَ الصور لينتج نقطة بيانات مُصغَّرة كي يتناسب حجمها في الذاكرة مع حجم العرض الفعلي.
// فكّك الصورة بحجم 300x300 بكسل منطقي بصرف النظر عن دقة المصدر.
// تضرب Flutter في نسبة بكسل الجهاز داخليًا.
Image.network(
'https://example.com/large-photo.jpg',
cacheWidth: 300,
cacheHeight: 300,
fit: BoxFit.cover,
)
// المعادل لأصل محلي:
Image.asset(
'assets/images/banner.jpg',
cacheWidth: 600, // 300 dp * نسبة بكسل الجهاز 2x
cacheHeight: 400,
)
// مع CachedNetworkImage استخدم memCacheWidth / memCacheHeight:
CachedNetworkImage(
imageUrl: 'https://example.com/photo.jpg',
memCacheWidth: 300,
memCacheHeight: 300,
)
القاعدة الذهبية: اضبط cacheWidth / cacheHeight على أبعاد البكسل المنطقي للودجت مضروبةً في نسبة بكسل الجهاز (MediaQuery.of(context).devicePixelRatio). يضمن ذلك عرضًا حادًا دون فك تشفير بكسلات أكثر مما تستطيع الشاشة إظهاره.
cacheWidth / cacheHeight للصور التي ستُحرِّكها لاحقًا إلى حجم أكبر (مثل انتقال Hero إلى عرض ملء الشاشة). ستبدو النقطة المُصغَّرة ضبابية عند التكبير. بدلًا من ذلك، حمّل الصورة بدقتها الكاملة لعرض التفاصيل والمصغّرة للقائمة.إزالة الصور من الذاكرة المؤقتة
تنمو ذاكرة الصور المؤقتة تلقائيًا لكنها لا تتقلص ما لم تبلغ حدود سعتها. في الجلسات طويلة الأمد — مثل خلاصة التمرير اللانهائي — تتراكم الإدخالات القديمة وقد ترفع الذاكرة الكلية فوق العتبات الآمنة. توفر Flutter واجهتَي برمجة لإزالة الصور عند الطلب:
// 1. إزالة صورة واحدة بمفتاح ImageProvider الخاص بها
final provider = CachedNetworkImageProvider('https://example.com/photo.jpg');
await provider.evict();
// 2. مسح ذاكرة الصور المؤقتة في الذاكرة الحية فورًا
PaintingBinding.instance.imageCache.clear();
// 3. مسح الصور الحية أيضًا (الصور المعروضة حاليًا على الشاشة)
PaintingBinding.instance.imageCache.clearLive();
// 4. تقليل حدود الذاكرة المؤقتة للتحكم في الذاكرة (استدعِ مرة واحدة عند البدء)
void configureCacheLimits() {
PaintingBinding.instance.imageCache.maximumSize = 200; // حد أقصى 200 صورة
PaintingBinding.instance.imageCache.maximumSizeBytes =
50 << 20; // 50 ميغابايت
}
configureCacheLimits() في main() قبل runApp() لفرض حدود أضيق منذ البداية. على الأجهزة محدودة الذاكرة (Android منخفض المواصفات) فكّر في حدود بمقدار 30 ميغابايت و100 صورة.الاستجابة لضغط الذاكرة
تُوجّه Flutter إشارات ضغط الذاكرة من نظام التشغيل إلى WidgetsBindingObserver. تجاوز didHaveMemoryPressure() لمسح الذاكرات المؤقتة بشكل استباقي حين يُحذّر نظام التشغيل من أن تطبيقك معرّض لخطر الإنهاء:
class _MyWidgetState extends State<MyWidget>
with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didHaveMemoryPressure() {
// أطلق جميع الصور المؤقتة حين يُشير نظام التشغيل إلى انخفاض الذاكرة
PaintingBinding.instance.imageCache.clear();
PaintingBinding.instance.imageCache.clearLive();
}
}
ملخص
تتطلب إدارة الصور الفعّالة في Flutter ثلاث استراتيجيات متكاملة تعمل معًا:
- التخزين المؤقت على القرص عبر
cached_network_imageيُزيل الطلبات الشبكية الزائدة. - تغيير حجم هدف الفك عبر
cacheWidth/cacheHeight(أوmemCacheWidth/memCacheHeight) يُقيّد حجم النقطة في الذاكرة بما تحتاجه الشاشة فعلًا. - الإزالة الاستباقية عبر
imageCache.clear()وإزالة كل مزوّد على حدة بـevict()وخفض حدود الذاكرة المؤقتة يُبقي تطبيقك ضمن الحدود الآمنة للذاكرة في الجلسات الطويلة.
مُطبَّقة معًا، يمكن لهذه التقنيات خفض استهلاك الذاكرة المتعلقة بالصور بنسبة 80–90% في واجهات القوائم الثقيلة المعتادة دون أي فقدان مرئي في الجودة.