
تطبيق iFunny الذي نعمل عليه متوفر في المتاجر لأكثر من خمس سنوات. خلال هذا الوقت ، كان على الفريق المتنقل متابعة العديد من الأساليب والهجرات المختلفة بين الأدوات ، وقبل عام كان هناك وقت للتبديل من حل مكتوب ذاتيًا والبحث في اتجاه شيء "أكثر شيوعًا" وانتشارًا. هذا المقال عبارة عن ضغط صغير حول ما تمت دراسته ، وما هي الحلول التي تم النظر فيها ، وما الذي انتهى إليه الأمر.
لماذا نحتاج كل هذا؟لنقرر على الفور تكريمًا لماهية هذه المقالة ولماذا تبين أن هذا الموضوع مهم لفريق تطوير Android:
- هناك العديد من السيناريوهات عندما تحتاج إلى تشغيل المهام خارج إطار واجهة المستخدم النشطة
- يفرض النظام عددًا كبيرًا من القيود على بدء هذه المهام ؛
- لقد أصبح من الصعب للغاية الاختيار بين الحلول الحالية ، حيث أن كل أداة لها مزاياها وعيوبها.
التسلسل الزمني لتطور الأحداث
أندرويد 0
AlarmManager ، معالج ، الخدمة
في البداية ، تم تنفيذ حلولهم لبدء المهام المستندة إلى الخلفية على أساس الخدمات. كانت هناك أيضًا آلية تربط المهام بدورة الحياة وتمكنت من إلغاؤها واستعادتها. يناسب هذا الفريق لفترة طويلة ، حيث أن المنصة لم تفرض أي قيود على مثل هذه المهام.
تنصح Google بالقيام بذلك بناءً على المخطط التالي:

في نهاية عام 2018 ، لا يوجد أي معنى لفهم ذلك ، يكفي لتقييم حجم الكارثة.
في الواقع ، لم يهتم أحد بحجم العمل الذي يحدث في الخلفية. فعلت التطبيقات ما أرادوا وعندما أرادوا.
الايجابيات :
متاح في كل مكان
في متناول الجميع.
سلبيات :
يقيد النظام العمل بكل الطرق ؛
لا تطلق من قبل الشرط.
واجهة برمجة التطبيقات (API) ضئيلة للغاية وتحتاج إلى كتابة الكثير من التعليمات البرمجية.
أندرويد 5. المصاصة
Jobcheduler
بعد 5 (!) سنوات ، أقرب إلى عام 2015 ، لاحظت Google أن المهام قد تم إطلاقها بشكل غير فعال. بدأ المستخدمون يتذمرون بانتظام من انخفاض هواتفهم ببساطة عن طريق الاستلقاء على طاولة أو في جيبهم.
مع إصدار Android 5 ، ظهرت أداة مثل JobScheduler. هذه هي الآلية التي يمكن من خلالها تنفيذ العديد من الأعمال في الخلفية ، والتي تم تحسينها وتبسيطها نظرًا للنظام المركزي لإطلاق هذه المهام والقدرة على وضع شروط لعملية الإطلاق هذه.
في التعليمات البرمجية ، كل هذا يبدو بسيطًا للغاية: يتم الإعلان عن خدمة تأتي فيها أحداث البداية والنهاية.
من الفروق الدقيقة: إذا كنت ترغب في أداء العمل بشكل غير متزامن ، فأنت بحاجة إلى بدء الدفق من onStartJob ؛ الشيء الرئيسي هو عدم نسيان استدعاء طريقة jobFinished في نهاية العمل ، وإلا فلن يترك النظام WakeLock ، لن تعتبر مهمتك مكتملة وستفقد.
public class JobSchedulerService extends JobService { @Override public boolean onStartJob(JobParameters params) { doWork(params); return false; } @Override public boolean onStopJob(JobParameters params) { return false; } }
من أي مكان في التطبيق ، يمكنك بدء هذا العمل. يتم تنفيذ المهام في عمليتنا ، ولكن يتم البدء بها على مستوى IPC. هناك آلية مركزية تتحكم في تنفيذها وتستيقظ التطبيق فقط في اللحظات الضرورية لذلك. يمكنك أيضًا ضبط العديد من شروط التشغيل ونقل البيانات من خلال Bundle.
JobInfo task = new JobInfo.Builder(JOB_ID, serviceName) .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED) .setRequiresDeviceIdle(true) .setRequiresCharging(true) .build(); JobScheduler scheduler = (JobScheduler) context.getSystemService(JOB_SCHEDULER_SERVICE); scheduler.schedule(task);
بشكل عام ، بالمقارنة مع لا شيء ، كان هذا بالفعل شيء. لكن هذه الآلية متوفرة فقط مع API 21 ، وعند إصدار Android 5.0 سيكون من الغريب التوقف عن دعم جميع الأجهزة القديمة (لقد مرت 3 سنوات ، وما زلنا ندعم أربع).
الايجابيات :
واجهة برمجة التطبيقات بسيطة ؛
شروط الاطلاق.
سلبيات :
متاح بدءا من API 21في الواقع ، فقط مع API 23 ؛
من السهل أن نخطئ.
أندرويد 5. المصاصة
مدير شبكة GCM
تم تقديم عرض تمثيلي لـ JobScheduler - مدير شبكة GCM. هذه مكتبة توفر وظائف مماثلة ، لكنها عملت بالفعل مع واجهة برمجة التطبيقات 9. صحيحًا ، لقد تطلب الأمر خدمات Google Play في المقابل. على ما يبدو ، فإن الوظيفة اللازمة لتشغيل JobScheduler ، بدأت تقدم ليس فقط من خلال إصدار Android ، ولكن أيضًا على مستوى GPS. تجدر الإشارة إلى أن مطوري الإطار غيروا رأيهم بسرعة كبيرة وقرروا عدم ربط مستقبلهم بنظام GPS. شكرا لهم على ذلك.
كل شيء يبدو متطابقة تماما. نفس الخدمة:
public class GcmNetworkManagerService extends GcmTaskService { @Override public int onRunTask(TaskParams taskParams) { doWork(taskParams); return 0; } }
إطلاق المهمة نفسها:
OneoffTask task = new OneoffTask.Builder() .setService(GcmNetworkManagerService.class) .setTag(TAG) .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED) .setRequiresCharging(true) .build(); GcmNetworkManager mGcmNetworkManager = GcmNetworkManager.getInstance(this); mGcmNetworkManager.schedule(task);
هذا التشابه في العمارة تمليه الوظيفة الموروثة والرغبة في الحصول على انتقال بسيط بين الأدوات.
الايجابيات :
API مماثلة ل JobScheduler.
متاح بدءا من API 9.
سلبيات :
يجب أن يكون لديك خدمات Google Play
من السهل أن نخطئ.أندرويد 5. المصاصة
WakefulBroadcastReceiver
بعد ذلك ، سأكتب بضع كلمات عن إحدى الآليات الأساسية المستخدمة في JobScheduler ومتاحة مباشرة للمطورين. هذا هو WakeLock ومقره WakefulBroadcastReceiver.
باستخدام WakeLock ، يمكنك منع النظام من ترك التعليق ، أي إبقاء الجهاز في حالة نشطة. هذا ضروري إذا أردنا القيام ببعض الأعمال المهمة.
عند إنشاء WakeLock ، يمكنك تحديد إعداداته: اضغط على وحدة المعالجة المركزية أو الشاشة أو لوحة المفاتيح.
PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE) PowerManager.WakeLock wl = pm.newWakeLock(PARTIAL_WAKE_LOCK, "name") wl.acquire(timeout);
بناءً على هذه الآلية ، يعمل WakefulBroadcastReceiver. نبدأ الخدمة وعقد WakeLock.
public class SimpleWakefulReceiver extends WakefulBroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { Intent service = new Intent(context, SimpleWakefulService.class); startWakefulService(context, service); } }
بعد انتهاء الخدمة اللازمة من العمل ، نطلقها من خلال طرق مماثلة.
من خلال 4 إصدارات ، سيتم إهمال برنامج BroadcastReceiver هذا ، وسيتم وصف البدائل التالية على developer.android.com:
- JobScheduler
- Syncadapter
- DownloadManager
- FLAG_KEEP_SCREEN_ON لـ Window.
أندرويد 6. نسيم عليل
DozeMode: النوم أثناء التنقل
ثم بدأت Google في تطبيق تحسينات متعددة للتطبيقات التي تعمل على الجهاز. ولكن ما هو الأمثل للمستخدم هو قيود للمطور.
كانت الخطوة الأولى هي DozeMode ، والتي تضع الجهاز في وضع السكون إذا كان خاملاً لفترة معينة. في الإصدارات الأولى ، استمرت ساعة ، في الإصدارات اللاحقة ، تم تخفيض مدة النوم إلى 30 دقيقة. بشكل دوري ، يستيقظ الهاتف ، ويقوم بجميع المهام المعلقة وينام مرة أخرى. نافذة DozeMode تتوسع بشكل كبير. يمكن تتبع جميع التحولات بين وسائط من خلال بنك التنمية الآسيوي.
عند حدوث DozeMode ، يتم فرض القيود التالية على التطبيق:
- يتجاهل النظام كل WakeLock ؛
- AlarmManager تأخرت.
- JobScheduler لا يعمل ؛
- SyncAdapter لا يعمل ؛
- الوصول إلى الشبكة محدود.
يمكنك أيضًا إضافة التطبيق الخاص بك إلى القائمة البيضاء بحيث لا يندرج تحت قيود DozeMode ، لكن على الأقل تجاهلت Samsung هذه القائمة تمامًا.
أندرويد 6. نسيم عليل
AppStandby: التطبيقات غير النشطة
يحدد النظام التطبيقات غير النشطة ويفرض جميع القيود نفسها عليها في DozeMode.
يتم إرسال الطلب إلى العزلة إذا:
- ليس لديها عملية في المقدمة ؛
- ليس لديه إشعار نشط ؛
- لم تتم إضافته إلى قائمة الاستبعاد.
أندرويد 7. نوغة
تحسينات الخلفية. Svelte
Svelte هو مشروع تحاول Google من خلاله تحسين استهلاك ذاكرة الوصول العشوائي عن طريق التطبيقات والنظام نفسه.
في Android 7 ، ضمن إطار هذا المشروع ، تقرر أن البث الضمني ليس فعالًا للغاية ، نظرًا لأن عددًا كبيرًا من التطبيقات يستمع إليها ، وينفق النظام قدرًا كبيرًا من الموارد عند حدوث هذه الأحداث. لذلك ، تم حظر أنواع الأحداث التالية للإعلان في البيان:
- CONNECTIVITY_ACTION ؛
- ACTION_NEW_PICTURE ؛
- ACTION_NEW_VIDEO.
أندرويد 7. نوغة
FirebaseJobDispatcher
في الوقت نفسه ، تم نشر نسخة جديدة من إطار عمل إطلاق المهمة - FirebaseJobDispatcher. في الواقع ، كان GCM NetworkManager المكتمل ، والذي تم ترتيبه قليلاً وجعله أكثر مرونة قليلاً.
بصريا ، بدا كل شيء هو نفسه تماما. نفس الخدمة:
public class JobSchedulerService extends JobService { @Override public boolean onStartJob(JobParameters params) { doWork(params); return false; } @Override public boolean onStopJob(JobParameters params) { return false; } }
الفرق الوحيد بينه هو القدرة على تثبيت برنامج التشغيل الخاص به. السائق هو الفئة التي كانت مسؤولة عن استراتيجية إطلاق المهمة.
لم يتغير إطلاق المهام نفسها بمرور الوقت.
FirebaseJobDispatcher dispatcher = new FirebaseJobDispatcher(new GooglePlayDriver(context)); Job task = dispatcher.newJobBuilder() .setService(FirebaseJobDispatcherService.class) .setTag(TAG) .setConstraints(Constraint.ON_UNMETERED_NETWORK, Constraint.DEVICE_IDLE) .build(); dispatcher.mustSchedule(task);
الايجابيات :
API مماثلة ل JobScheduler.
متاح بدءا من API 9.
سلبيات :
يجب أن يكون لديك خدمات Google Play
من السهل أن نخطئ.
كان من المشجع تثبيت برنامج التشغيل الخاص بي للتخلص من GPS. لقد بحثنا ، ولكن وجدنا في النهاية ما يلي:


تعرف Google ذلك ، لكن تظل هذه المهام مفتوحة لعدة سنوات.
أندرويد 7. نوغة
وظيفة الروبوت من Evernote
نتيجةً لذلك ، لم يتمكن المجتمع من الوقوف وظهر حل عصامي في شكل مكتبة من Evernote. لم يكن الحل الوحيد ، ولكن كان الحل من Evernote هو الذي كان قادراً على ترسيخ نفسه و "الانخراط في الناس".
من الناحية المعمارية ، كانت هذه المكتبة أكثر ملاءمة من سابقاتها.
لقد ظهر الكيان المسؤول عن إنشاء المهام. في حالة JobScheduler ، تم إنشاؤها من خلال التفكير.
class SendLogsJobCreator : JobCreator { override fun create(tag: String): Job? { when (tag) { SendLogsJob.TAG -> return SendLogsJob() } return null } }
هناك فئة منفصلة ، وهي المهمة نفسها. في JobScheduler ، تم إلقاء كل هذا في مفتاح داخل onStartJob.
class SendLogsJob : Job() { override fun onRunJob(params: Params): Result { return doWork(params) } }
إطلاق المهام مطابق ، ولكن بالإضافة إلى الأحداث الموروثة ، أضاف Evernote أيضًا مهامه الخاصة ، مثل بدء تشغيل المهام اليومية والمهام الفريدة والتشغيل داخل النافذة.
new JobRequest.Builder(JOB_ID) .setRequiresDeviceIdle(true) .setRequiresCharging(true) .setRequiredNetworkType(JobRequest.NetworkType.UNMETERED) .build() .scheduleAsync();
الايجابيات :
API مريحة.
مدعومة على جميع الإصدارات ؛
لا تحتاج إلى خدمات Google Play.
سلبيات :
حل الطرف الثالث.
الرجال بنشاط دعم مكتبتهم. على الرغم من وجود عدد قليل من المشكلات الحرجة ، إلا أنها عملت على جميع الإصدارات وعلى جميع الأجهزة. ونتيجة لذلك ، اختار فريق Android لدينا في العام الماضي حلاً من Evernote ، نظرًا لأن المكتبات من Google تقطع طبقة كبيرة من الأجهزة التي لا يمكنها دعمها.
في الداخل ، عملت على حلول من Google ، في الحالات القصوى - مع AlarmManager.
أندرويد 8. أوريو
حدود تنفيذ الخلفية
دعنا نعود إلى حدودنا. مع ظهور Android الجديد ، ظهرت تحسينات جديدة. وجد الرجال من Google مشكلة أخرى. هذه المرة تحول كل شيء إلى الخدمات والبث (نعم ، لا شيء جديد).
startService إذا التطبيقات في الخلفيةالبث الضمني في البيان
أولاً ، تم حظر بدء الخدمات من الخلفية. في "إطار القانون" ظلت الخدمات الأمامية فقط. الخدمات الآن يمكن أن يقال أن يتم إهمالها.
القيد الثاني هو نفس البث. هذه المرة أصبح من المحظور تسجيل جميع البث الضمني في البيان. البث الضمني هو بث ، ليس مخصصًا لتطبيقنا فقط. على سبيل المثال ، هناك إجراء ACTION_PACKAGE_REPLACED ، وهناك ACTION_MY_PACKAGE_REPLACED. لذلك ، أول واحد هو ضمني.
ولكن لا يزال من الممكن تسجيل أي بث من خلال Context.registerBroadcast.
أندرويد 9. فطيرة
العامل
على هذا التحسين قد توقف بعد. ربما بدأت الأجهزة تعمل بسرعة وبدقة من حيث استهلاك الطاقة ؛ ربما اشتكى المستخدمون أقل من ذلك.
في Android 9 ، تعامل مطورو الإطار بدقة مع أداة بدء المهام. في محاولة لحل جميع المشكلات الملحة ، تم تقديم مكتبة على Google I / O لبدء تشغيل المهام الأساسية لـ WorkManager.
تحاول Google مؤخرًا تشكيل رؤيتها لبنية تطبيق Android وتمنح المطورين الأدوات اللازمة لذلك. لذلك كانت هناك مكونات معمارية مع LiveData و ViewModel و Room. WorkManager يشبه تكملة معقولة لنهجهم والنموذج.
إذا تحدثنا عن كيفية ترتيب WorkManager بالداخل ، فلا يوجد تقدم تكنولوجي فيه. في الواقع ، هذه مجموعة من الحلول الحالية: JobScheduler و FirebaseJobDispatcher و AlarmManager.
createBestAvailableBackgroundScheduler static Scheduler createBestAvailableBackgroundScheduler(Context, WorkManager) { if (Build.VERSION.SDK_INT >= MIN_JOB_SCHEDULER_API_LEVEL) { return new SystemJobScheduler(context, workManager); } try { return tryCreateFirebaseJobScheduler(context); } catch (Exception e) { return new SystemAlarmScheduler(context); } }
رمز الاختيار بسيط جدا. ولكن تجدر الإشارة إلى أن JobScheduler متاح بدءًا من واجهة برمجة التطبيقات 21 ، لكنهم يستخدمونها فقط مع واجهة برمجة التطبيقات 23 ، نظرًا لأن الإصدارات الأولى كانت غير مستقرة إلى حد ما.
إذا كان الإصدار أقل من 23 ، فمن خلال الانعكاس نحاول العثور على FirebaseJobDispatcher ، وإلا فإننا نستخدم AlarmManager.
تجدر الإشارة إلى أن المجمع أصبح مرنا للغاية. هذه المرة ، قام المطورون بتقسيم كل شيء إلى كيانات منفصلة ، ويبدو معمارياً أنه مناسب:
- العامل - منطق العمل ؛
- WorkRequest - منطق إطلاق المهمة ؛
- WorkRequest.Builder - المعلمات ؛
- القيود - الظروف ؛
- WorkManager - مدير يدير المهام ؛
- WorkStatus - حالة المهمة.

تم توريث شروط التشغيل من JobScheduler.
تجدر الإشارة إلى أن المشغل لتغيير URI يظهر فقط مع API 23. بالإضافة إلى ذلك ، يمكنك الاشتراك في تغيير ليس فقط URI معين ، ولكن أيضًا كل المتداخلة فيه باستخدام العلامة في الطريقة.
إذا تحدثنا عنا ، ثم في مرحلة ألفا ، فقد تقرر التبديل إلى WorkManager.
هناك عدة أسباب لذلك. يحتوي Evernote على بعض الأخطاء الهامة التي يعد مطورو المكتبة بإصلاحها مع الانتقال إلى إصدار مع WorkManager متكامل. وهم أنفسهم متفقون على أن قرار Google ينفي مزايا Evernote. بالإضافة إلى ذلك ، يتناسب هذا الحل مع هيكلنا ، حيث نستخدم مكونات الهندسة المعمارية.
علاوة على ذلك ، أود أن أوضح مع مثال بسيط كيف نحاول استخدام هذا النهج. في الوقت نفسه ، ليس من الأهمية بمكان أن يكون لديك WorkManager أو JobScheduler.


دعونا نلقي نظرة على مثال لحالة بسيطة للغاية: النقر على إعادة النشر أو ما شابه.
تحاول جميع التطبيقات الآن الابتعاد عن حظر الطلبات على الشبكة ، لأن هذا يجعل المستخدم يشعر بالتوتر ويجعله ينتظر ، على الرغم من أنه في الوقت الحالي يمكنه إجراء عمليات شراء داخل التطبيق أو مشاهدة الإعلانات.
في مثل هذه الحالات ، تتغير البيانات المحلية أولاً - يرى المستخدم على الفور نتيجة تصرفه. ثم في الخلفية يوجد طلب للخادم ، إذا فشل ، تتم إعادة تعيين البيانات إلى حالتها الأولية.
بعد ذلك ، سأعرض مثالاً عن كيف يبدو معنا.
JobRunner يحتوي على منطق لبدء المهام. أساليبه تصف تكوين المهام وتمرير المعلمات.
JobRunner.java fun likePost(content: IFunnyContent) { val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build() val input = Data.Builder() .putString(LikeContentJob.ID, content.id) .build() val request = OneTimeWorkRequest.Builder(LikeContentJob::class.java) .setInputData(input) .setConstraints(constraints) .build() WorkManager.getInstance().enqueue(request) }
المهمة نفسها داخل WorkManager هي كما يلي: نأخذ المعرّف من المعلمات ونستدعي الطريقة على الخادم لتعجب هذا المحتوى.
لدينا فئة أساسية تحتوي على المنطق التالي:
abstract class BaseJob : Worker() { final override fun doWork(): Result { val workerInjector = WorkerInjectorProvider.injector() workerInjector.inject(this) return performJob(inputData) } abstract fun performJob(params: Data): Result }
أولاً ، يتيح لك الابتعاد قليلاً عن المعرفة الواضحة للعامل. كما أنه يحتوي على منطق حقن التبعية من خلال WorkerInjector.
WorkerInjectorImpl.java @Singleton public class WorkerInjectorImpl implements WorkerInjector { @Inject public WorkerInjectorImpl() {} @Ovierride public void inject(Worker job) { if (worker instanceof AppCrashedEventSendJob) { Injector.getAppComponent().inject((AppCrashedEventSendJob) job); } else if (worker instanceof CheckNativeCrashesJob) { Injector.getAppComponent().inject((CheckNativeCrashesJob) job); } } }
إنه ببساطة يوجه المكالمات إلى Dagger ، لكنه يساعدنا في الاختبار: نحن نحل محل تطبيقات الحاقن وننفذ البيئة اللازمة في المهام.
fun void testRegisterPushProvider() { WorkManagerTestInitHelper.initializeTestWorkManager(context) val testDriver = WorkManagerTestInitHelper.getTestDriver() WorkerInjectorProvider.setInjector(TestInjector())
class LikePostInteractor @Inject constructor( val iFunnyContentDao: IFunnyContentDao, val jobRunner: JobRunner) : Interactor { fun execute() { iFunnyContentDao.like(getContent().id) jobRunner.likePost(getContent()) } }
Interactor هو الكيان الذي يسحب ViewController لبدء مرور البرنامج النصي (في هذه الحالة ، مثله). نضع علامة على المحتوى محليًا على أنه "تم الرفع" ونرسل المهمة للتنفيذ. إذا فشلت المهمة ، تتم إزالة مثل.
class IFunnyContentViewModel(val iFunnyContentDao: IFunnyContentDao) : ViewModel() { val likeState = MediatorLiveData<Boolean>() var iFunnyContentId = MutableLiveData<String>() private var iFunnyContentState: LiveData<IFunnyContent> = attachLiveDataToContentId(); init { likeState.addSource(iFunnyContentState) { likeState.postValue(it!!.hasLike) } } }
نحن نستخدم مكونات الهندسة المعمارية من Google: ViewModel و LiveData. هذا هو ما يشبه ViewModel لدينا. نحن هنا نربط تحديث الكائن في DAO بحالة الإعجاب.
IFunnyContentViewController.java class IFunnyContentViewController @Inject constructor( private val likePostInteractor: LikePostInteractor, val viewModel: IFunnyContentViewModel) : ViewController { override fun attach(view: View) { viewModel.likeState.observe(lifecycleOwner, { updateLikeView(it!!) }) } fun onLikePost() { likePostInteractor.setContent(getContent()) likePostInteractor.execute() } }
ViewController ، من ناحية ، تشترك في تغيير حالة ما شابه ، من ناحية أخرى ، يبدأ مرور النص الذي نحتاجه.
وهذا عمليا هو الكود الذي نحتاجه. يبقى لإضافة سلوك العرض نفسه مع ما شابه وتنفيذ DAO الخاص بك ؛ إذا كنت تستخدم Room ، فقم فقط بتسجيل الحقول في الكائن. يبدو بسيط جدا وفعالة.
لتلخيص
JobScheduler ، مدير شبكة GCM ، FirebaseJobDispatcher:- لا تستخدمها
- لا تقرأ مقالات عنها بعد الآن
- لا تشاهد التقارير
- لا أعتقد أي واحد للاختيار.
وظيفة Android بواسطة Evernote:- داخل سوف يستخدمون WorkManager.
- الأخطاء الحرجة غير واضحة بين الحلول.
مدير العمل:- واجهة برمجة تطبيقات المستوى 9+ ؛
- مستقلة عن خدمات Google Play ؛
- التسلسل / InputMergers.
- نهج رد الفعل.
- الدعم من جوجل (أريد أن أصدق ذلك).