
الوقت المقدر للقراءة 15 دقيقة.
قبل البدء
إذا لم تكن تملك فكرة عن مفهوم تعدد الخيوط أنصحك بالاطلاع على السلسة التي بعنوان “تعدد الخيوط في لغة الجافا”.
مقدمة
تحتاج تطبيقات الأندرويد إلى استخدام مفهوم تعدد الخيوط لإنجاز المهام الطويلة في خيط مختلف عن خيط واجهة المستخدم، ولتحقيق هذا الأمر تم إيجاد العديد من الطرق ولكل منها إيجابيات وسلبيات.
قبل البدء علينا أن نضع في الحسبان الأمور التالية:
- يجب إنجاز المهام الطويلة في خيط مستقل حتى لا تؤثر على واجهة المستخدم.
- تعديل واجهة المستخدم يجب أن يتم فقط عبر الخيط المخصص للواجهة ولنسميه خيط الواجهة.
الطريقة التقليدية
لقد شرحت هذه الطريقة بشكل مفصل في وقت سابق واختصاراً للوقت سأقوم بتجاوز شرح هذه الطريقة، ولمن يرغب بالاطلاع عليها يمكنه مراجعة الرابط التالي
مشكلة هذه الطريقة هي التعقيد وصعوبة التطبيق، بالإضافة إلى عدم الأمان، ويقصد بعدم الأمان الحاجة إلى ضبط عمليات المزامنة للمتحولات بشكل دقيق وأي خطأ قد يسبب توقف التطبيق عن العمل، كما أن التواصل بين الخيط الفرعي وواجهة المستخدم ليس بالأمر السهل.
الصف AsyncTask
هو صف مجرد مخصص لإنجاز المهام الطويلة نسبياً، ولكن يفضل ألا تزيد مدة التنفيذ عن عدة ثوان (عدة ثوان هو زمن تنفيذ طويل بالنسبة لكود برمجي).
يمكن استخدام هذا الصف في الحالات التالية (على سبيل المثال وليس الحصر):
- كتابة ملف صغير على القرص
- عملية حسابية معقدة
- تسجيل الدخول البعيد
- قراءة ملف صغير
- طلب JSON من خدمة وب
كيفية الاستخدام
لفهم كيفية استخدام هذا الصف أنشئ مشروعاً جديداً باستخدام أندرويد ستوديو وقم ببناء واجهة تحوي على زر وTextView وقم بإعطائه معرف فريد وليكن مثلاً counterTV.
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.mutasem4it.android.asynctaskexample.MainActivity">
<TextView
android:textSize="24sp"
android:id="@+id/counterTV"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0" />
<Button
android:layout_below="@+id/counterTV"
android:onClick="countClick"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Count to 10"
/>
</RelativeLayout>
هذه الواجهة عبارة عن زر ومربع يحوي رقم 0، سنقوم ببرمجة تطبيق يقوم بالعد حتى 10 وتعديل الواجهة مع كل خطوة عدّ مع تأخير زمني بسيط حتى نلاحظ كيف تعمل المهمة في الخلفية.
وسيكون كود الجافا المبدئي كالتالي:
public class MainActivity extends AppCompatActivity {
TextView counterTV;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
counterTV= (TextView) findViewById(R.id.counterTV);
}
}
قبل استخدام الصف AsyncTask علينا أن ندرك عدة أمور، أولها ما هو نمط البيانات الذي سأحصل عليه كنتيجة لتنفيذ الخيط، مثلاً في عملية تحميل نص من الانترنت تكون النتيجة من نمط String وفي عملية قراءة ملف من القرص ستكون من نمط File مثلاً، سأقوم في مثالنا باعتبار أن نتيجة تنفيذ الخيط هي عبارة عن رسالة نصية من نمط String.
الأمر الثاني الذي يجب علينا معرفته، هو نوع البيانات المرحلية التي يجب إرسالها إلى واجهة المستخدم، فمثلاً أثناء عملية قراءة ملف من القرص يتم في كل مرحلة إرسال رقم من نمط Integer يعبر عن نسبة تقدم عملية القراءة مثلاً 1% ثم 2% ثم 3% …وهكذا، وكذلك في مثالنا أثناء عملية العد سيتم ارسال رقم Integer يعبر عن العدد الحالي.
الآن في منطقة تعريف الأعضاء البيانية قم بإضافة الكود التالي وتابع الشرح
AsyncTask task = new AsyncTask<String, Integer, String>() {
@Override
protected String doInBackground(String[] objects) {
for (int i = 1; i < 11; i++) {
publishProgress(i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return "Finished";
}
@Override
protected void onProgressUpdate(Integer[] values) {
Integer last = values[values.length - 1];
counterTV.setText(last + "");
}
@Override
protected void onPostExecute(String o) {
Toast.makeText(MainActivity.this, o.toString(), Toast.LENGTH_LONG).show();
}
};
في السطر الأول قمنا بإنشاء كائن من الصف AsyncTask ولاحظ أثناء عملية إنشاء الكائن قمت بتمرير وسطاء (قوالب) ضمن إشارتي <>
<String, Integer, String>
هذه الوسطاء هي على الترتيب من اليسار إلى اليمين:
- نمط وسطاء الدالة execute (سنشرحها بشكل لاحق)
- نمط البيانات المرحلية (الرقم الذي يعبر عن العدد الحالي)
- نمط البيانات النهائية (ناتج تنفيذ الخيط)
وبما أن هذا الصف صف مجرد فنحن بحاجة لتحقيق الدالة المجرد doInBackground. وقد استخدمت هنا طريقة الصف المجهول لتحقيق هذه الدالة.
ضمن الصف المجهول ستجد ثلاث دوال هي على الترتيب
doInBackground
onProgressUpdate
onPostExecute
ضمن الدالة doInBackground يجب وضع الشيفرة البرمجية المسؤولة عن المهمة الطويلة (عملية العدّ).
وضمن الدالة onProgressUpdate يجب وضع الشيفرة المسؤولة عن معالجة النتائج المرحلية (تحديث العداد في واجهة المستخدم).
وفي الدالة onPostExecute يجب وضع الشيفرة المسؤولة عن معالجة النتيجة النهائية (إظهار رسالة تفيد بانتهاء عملية العدّ).
الدالة doInBackground
ضمن هذه الدالة يجب وضع الشيفرة المسؤولة عن العملية التي تستغرق وقتاً طويلاً نسبياً (عملية العد)، وللسهولة قمت بوضع شيفرة وهمية عبارة عنا حلقة for تتكرر 10 مرات، ومع كل تكرار أقوم بعملية تأخير زمني بسيط.
لاحظ أنني ضمن حلقة for استخدمت الدالة publishProgress والتي تقوم بإرسال النتائج المرحلية إلى الدالة onProgressUpdate لذلك قمت بإرسال المتحول i عبر تمريره كوسيط للدالة publishProgress.
بعد إرسال النتائج المرحلية استخدمت الدالة sleep لأحداث تأخير زمني بمقدار 100 ميلي ثانية وبما أن الحلقة تتكرر 10 مرة فإن الزمن الكلي لتنفيذ الحلقة هو 1 ثانية وهو زمن كافي لملاحظة التغيرات على واجهة المستخدم (إذا لم تتمكن من ملاحظة التغيرات قم بزيادة هذا التأخير إلى 200).
الدالة onProgressUpdate
بعد كل استدعاء للدالة publishProgress يتم استدعاء الدالة onProgressUpdate بشكل تلقائي.
ضمن هذه الدالة نقوم باستقبال النتائج المرحلية الواردة من الدالة doInBackground عبر الوسيط values وهو عبارة عن مصفوفة تحوي جميع البيانات المرحلية التي تم استقبالها حتى الآن، على سبيل المثال في بداية التنفيذ تكون المصفوفة بالشكل التالي:
{1}
ثم
{1, 2}
{1, 2, 3}
وهكذا حتى انتهاء عملية العد، ولكننا في كل مرة نحتاج فقط لآخر قيمة كي نقوم بتحديث العداد في الواجهة لذلك قمنا بجلب آخر قيمة عبر التعليمة
Integer last = values[values.length - 1];
وثم قمنا بإرسال هذه القيمة إلى واجهة المستخدم.
الدالة onPostExecute
يتم تنفيذ هذه الدالة بعد انتهاء الدالة doInBackground وتتيح لنا الحصول على النتيجة النهائية لتنفيذ الخيط وعمل التعديلات اللازمة على واجهة المستخدم.
هذه الدالة تعمل في خيط الواجهة أي لديها قدرة التعديل على واجهة المستخدم بدون أي مشاكل في حين أن الدالة doInBackground تعمل في خيط مستقل وتعديل واجهة المستخدم من خلالها قد يسبب الكثير من المشاكل.
لحد الآن قمنا فقط بتجهيز كائن من الصف AsyncTask لتنفيذ مهمة العدّ، لكن عملية العد الفعلي لم تبدأ، لجعلها تبدأ نستدعي الدالة execute ولكي نربط البداية بالنقر على الزر نضع استدعاء هذه الدالة في حدث النقر على الزر ويصبح الكود النهائي كما يلي:
package com.mutasem4it.android.asynctaskexample;
import android.app.Dialog;
import android.os.AsyncTask;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
TextView counterTV;
AsyncTask task = new AsyncTask<String, Integer, String>() {
@Override
protected String doInBackground(String[] objects) {
for (int i = 1; i < 11; i++) {
publishProgress(i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return "Finished";
}
@Override
protected void onProgressUpdate(Integer[] values) {
Integer last = values[values.length - 1];
counterTV.setText(last + "");
}
@Override
protected void onPostExecute(String o) {
Toast.makeText(MainActivity.this, o.toString(), Toast.LENGTH_LONG).show();
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
counterTV = (TextView) findViewById(R.id.counterTV);
}
public void countClick(View view) {
task.execute();
}
}
ملاحظة رقم 1
يتيح الصف AsyncTask إمكانية تمرير وسطاء للدالة doInBackground عند الاستدعاء عبر الدالة execute حيث نقوم بتمرير الوسطاء على شكل varArgs إلى الدالة execute التي بدورها ترسل الوسيط على شكل مصفوفة إلى الدالة doInBackground كالتالي:
الاستدعاء
task.execute(arg1,arg2,arg3);
استخدام الوسطاء في الدالة doInBackground
void doInBackground(String[] objects) {
objects[0]//arg1
objects[1]//arg2
objects[2]//arg3
//..etc
}
ملاحظة رقم 2
يتميز الصف AsyncTask بسهولة الاستخدام للتعامل مع واجهة المستخدم لكنه يحوي الكثير من المشاكل أهمها المزامنة بين دورة حياة المهمة ودورة حياة الواجهة التي بدأت المهمة، بمعنى أدق عند تنفيذ الدالة onDestroy() لايتم انهاء المهمة تلقائياً وإنما تستمر بالعمل في الخلفية، ففي مثالنا السابق لو قام المستخدم بتدوير شاشة الهاتف المحمول إلى الوضع العرضي قبل انتهاء العداد، هذا سيؤدي إلى تدمير الواجهة واستمرار عمل المهمة وعند انتهاء المهمة ومحاولتها لتحديث الواجهة بالنتائج سوف يحصل خطأ لأن الواجهة قد دمرت.
لحل هذه المشكلة يمكن استخدام الصف IntentService والذي سيكون عنوان القسم التالي.