إدارة ذاكرة التطبيق: التسريبات والتخلص من الموارد ودورة حياة الكائنات
إدارة ذاكرة التطبيق: التسريبات والتخلص من الموارد ودورة حياة الكائنات
تُعدّ تسريبات الذاكرة من أدق مشكلات الأداء وأشدها ضرراً في تطبيقات Flutter. يحدث تسريب الذاكرة عندما لا يعود كودك بحاجة إلى كائن ما، لكن لا يمكن لمجمّع البيانات المهملة (Garbage Collector) استرداد الذاكرة المشغولة به لأن شيئاً ما لا يزال يحمل مرجعاً إليه. مع مرور الوقت، تتراكم الكائنات المسرَّبة فيتصاعد استهلاك التطبيق للذاكرة العشوائية، مما يُفضي إلى بطء الأداء وتقطّع الواجهة وفي نهاية المطاف انهيار التطبيق بسبب نفاد الذاكرة. إن فهم دورة حياة الكائنات في Dart ومعرفة متى وكيف تُحرِّر الموارد بدقة يُعدّ مهارة أساسية في تطوير Flutter الاحترافي.
ما هي دورة حياة الكائنات في Dart؟
يستخدم Dart كومة ذاكرة مُدارة بجامع البيانات المهملة. حين لا يُشير أي مرجع حي إلى كائن ما، تُعلّمه الآلة الافتراضية لـ Dart على أنه غير قابل للوصول فيستردّه جامع البيانات المهملة. تنشأ المشكلة حين تحتفظ ودجات Flutter أو واجهات برمجية خارجية بمراجع حية لكائنات بعد إزالة الودجت من الشجرة. ومن أبرز المسببات:
AnimationController— يحمل مرجعاً لـTickerProviderومستمعاً لـ vsyncTextEditingControllerوScrollController— يحتفظان بمستمعين داخليين وموارد نظام أساسيStreamSubscription— تحمل دالة إغلاق تُقيّدthis(كائن حالة الودجت)FocusNode— يُسجّل نفسه ضمن نظام التركيزPageControllerوTabController— يرتبطان بأقربTickerProvider
الدالة dispose() ومتى تستدعيها
يمكن لأي صنف فرعي من State تجاوز دالة دورة الحياة dispose(). يستدعيها Flutter مرة واحدة تحديداً، مباشرةً بعد إزالة الودجت نهائياً من الشجرة. هذا هو الموضع الصحيح والوحيد لتحرير الموارد التي يمسك بها كائن الحالة.
اجعل استدعاء super.dispose() دائماً آخر تعليمة في التجاوز لضمان تنفيذ الإطار لعمليات تنظيفه الخاصة.
النمط الصحيح لـ dispose()
class VideoPlayerScreen extends StatefulWidget {
const VideoPlayerScreen({super.key});
@override
State<VideoPlayerScreen> createState() => _VideoPlayerScreenState();
}
class _VideoPlayerScreenState extends State<VideoPlayerScreen>
with SingleTickerProviderStateMixin {
late final AnimationController _fadeController;
late final TextEditingController _searchController;
late final FocusNode _searchFocus;
StreamSubscription<PlaybackEvent>? _playbackSub;
@override
void initState() {
super.initState();
_fadeController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 400),
);
_searchController = TextEditingController();
_searchFocus = FocusNode();
// الاشتراك في دفق مع الإمساك بمرجع
_playbackSub = playerService.events.listen((event) {
if (mounted) setState(() {/* تحديث واجهة المستخدم */});
});
}
@override
void dispose() {
// إلغاء اشتراك الدفق أولاً
_playbackSub?.cancel();
// التخلص من المتحكمات بترتيب عكسي للتهيئة
_fadeController.dispose();
_searchController.dispose();
_searchFocus.dispose();
// استدعاء super.dispose() دائماً في النهاية
super.dispose();
}
@override
Widget build(BuildContext context) => const Placeholder();
}
StreamSubscription هو أكثر أسباب تسريب الذاكرة شيوعاً في Flutter. تُقيّد دالة الإغلاق الخاصة بالاشتراك مرجعاً لكائن State، مما يُبقيه حياً بعد إزالة الودجت من الشجرة. استدعِ دائماً subscription.cancel() في dispose().الدفقات والمستمعون والحارس mounted
حين يُصدر دفق حدثاً بعد التخلص من الودجت، فإن استدعاء setState() على كائن حالة منتهٍ يُلقي استثناءً. احرص على تحصين كل دالة رد نداء غير متزامنة بخاصية mounted:
استخدام mounted لتحصين ردود النداء غير المتزامنة
void _loadData() async {
final result = await repository.fetchItems();
// قد يكون الودجت قد تمّ التخلص منه أثناء الانتظار
if (!mounted) return; // حارس آمن — لا تستدعِ setState بعد هذا
setState(() {
_items = result;
});
}
// نسخة مستمع الدفق
_sub = stream.listen((data) {
if (mounted) {
setState(() => _value = data);
}
});
استخدام Flutter DevTools للكشف عن تسريبات الذاكرة
تبويب الذاكرة في Flutter DevTools هو الأداة المرجعية للتحقق من أن الكائنات تُجمَّع كبيانات مهملة. مسار العمل كالتالي:
- الخطوة 1 — التقاط لقطة أساسية: انتقل إلى تبويب الذاكرة في DevTools أثناء عرض الشاشة المُختبَرة. انقر Take Snapshot.
- الخطوة 2 — استنباط التسريب: انتقل بعيداً عن الشاشة (افرق المسار) لكي يُفترض أن الودجت قد تُخلِّص منه وجُمِع.
- الخطوة 3 — فرض جمع البيانات المهملة وإعادة اللقطة: انقر زر GC في DevTools ثم التقط لقطة ثانية.
- الخطوة 4 — المقارنة: استخدم عرض Diff لرؤية الكائنات التي ازداد عددها بين اللقطتين. إن ظلّ صنف
Stateالخاص بك ظاهراً بفارق موجب، فلم يُجمَع — لديك تسريب.
تُظهر DevTools أيضاً آثار التخصيص — يمكنك رؤية السطر بالضبط الذي خصّص الكائن الباقي، مما يُيسّر تتبّع استدعاء dispose() المفقود.
cancel_subscriptions في flutter_lints وقاعدة dispose_controllers (المتوفرة عبر package:lint) لرصد الموارد غير المتخلَّص منها في وقت التحليل، قبل أن تصل إلى أي جهاز.Provider وRiverpod: التخلص التلقائي
عند استخدام Riverpod، تُدمَّر المزودات (Providers) المُعلَنة بـ autoDispose تلقائياً حين ينفصل آخر مستمع، مما يمنع التسريبات على مستوى إدارة الحالة. يُفضَّل دائماً استخدام autoDispose للمزودات التي تمتلك دفقات أو مؤقتات:
نمط autoDispose في Riverpod
// مزود يُلغى تلقائياً حين تتوقف شجرة الودجات عن الاستماع
final timerProvider = StreamProvider.autoDispose<int>((ref) {
// يُستدعى ref.onDispose حين يُدمَّر المزود
ref.onDispose(() => debugPrint('Timer provider disposed'));
return Stream.periodic(
const Duration(seconds: 1),
(count) => count,
);
});
// في ConsumerWidget — لا حاجة لتجاوز dispose يدوياً
class TimerWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final tick = ref.watch(timerProvider);
return tick.when(
data: (value) => Text('Tick: $value'),
loading: () => const CircularProgressIndicator(),
error: (e, _) => Text('Error: $e'),
);
}
}
ملخص
يستلزم منع تسريبات الذاكرة في Flutter إدارة موارد منضبطة على كل طبقة من طبقات تطبيقك. القواعد الأساسية هي: (1) تجاوز dispose() دائماً في كل State يمتلك متحكماً أو اشتراكاً أو عقدة تركيز؛ (2) إلغاء StreamSubscriptions قبل استدعاء super.dispose()؛ (3) تحصين ردود النداء غير المتزامنة بفحص mounted؛ و(4) التحقق من عمليات التنظيف باستخدام مسار لقطة الفرق في تبويب الذاكرة بـ DevTools. إن تبنّيت هذه العادات من البداية سيظل تطبيقك سريعاً ومستقراً وخالياً من التسريبات طوال دورة حياته.