English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية

شرح مفصل لميكانيكية إرسال الرسائل في Android وبرمجة أمثلة

ميكانيكية الرسائل في Android

1. تلخيص

عند بدء تطبيق Android، سيكون هناك سطر رئيسي افتراضي (UI Thread)، حيث سيتم ربط قائمة الرسائل (MessageQueue) به، وسيتم تعبئة جميع العمليات بكتابة رسائل في قائمة الرسائل ثم تقديمها إلى السطر الرئيسي للمعالجة. لضمان عدم مغادرة السطر الرئيسي، سيتم وضع عمليات قائمة الرسائل في حلقة دائمة، مما يجعل البرنامج يقوم بتنفيذ حلقة دائمة، حيث يتم في كل دور من الدوران استخراج رسالة واحدة من قائمة الرسائل الداخلية، ثم استدعاء الدالة المعالجة للرسالة (handlerMessage)، بعد إكمال معالجة الرسالة، يستمر في الحلقة. إذا كانت قائمة الرسائل فارغة، فإن السطر سيكون معطلًا في انتظار الرسالة. لذلك لن يغادر. كما هو موضح في الشكل التالي:

ما هي العلاقة بين المعالج (Handler)، وLooper، وMessage؟

في بعض الحالات، عند إكمال العمليات الطويلة في مسار فرعي، تحتاج إلى تحديث واجهة المستخدم، والأكثر شيوعًا هي استخدام المعالج لوضع رسالة على مسار واجهة المستخدم، ثم معالجة الرسالة في طريقة handlerMessage الخاصة بالمعالج. وكل معالج يربط بقائمة الرسائل (MessageQueue)، ويتولى Looper إنشاء قائمة الرسائل، وكل Looper يربط بسطر (يتم تضمينه من خلال ThreadLocal). في الحالة الافتراضية، قائمة الرسائل الواحدة هي قائمة الرسائل للمسار الرئيسي.

هذا هو المبدأ الأساسي لميكانيكية الرسائل في Android، إذا كنت ترغب في معرفة المزيد، سنبدأ من الشيفرة المصدرية.

2. تفسير الشيفرة المصدرية

(1) بدء دورة الرسائل Looper في المسار الرئيسي ActivityThread

public final class ActivityThread {
  public static void main(String[] args) {
    //كود مرفوع
    //1. إنشاء Looper لدورة الرسائل
    Looper.prepareMainLooper();
    ActivityThread thread = new ActivityThread();
    thread.attach(false);
    if (sMainThreadHandler == null) {
      sMainThreadHandler = thread.getHandler();
    }
    AsyncTask.init();
    //2. تنفيذ دورة الرسائل
    Looper.loop();
    throw new RuntimeException("Main thread loop unexpectedly exited");
  }
}

يستخدم ActivityThread طريقة Looper.prepareMainLooper() لإنشاء قائمة الرسائل للمسار الرئيسي، ثم تنفيذ Looper.loop() لبدء قائمة الرسائل. يربط المعالج بين قائمة الرسائل والسطر.

(2) ربط المعالج بين قائمة الرسائل والسطر

public Handler(Callback callback, boolean async) {
    //كود مرفوع
    //الحصول على Looper
    mLooper = Looper.myLooper();
    if (mLooper == null) {
      throw new RuntimeException(
        "Can't create handler inside thread that has not called Looper.prepare()"
    }
    //الحصول على مجموعة الرسائل
    mQueue = mLooper.mQueue;
  }

المعالج يستخدم داخليًا طريقة Looper.getLooper() للحصول على عنصر Looper وتوصيله، والحصول على قائمة الرسائل. إذن كيف يعمل Looper.getLooper()؟

  public static @Nullable Looper myLooper() {}}
    return sThreadLocal.get();
  }
  public static @NonNull MessageQueue myQueue() {
    return myLooper().mQueue;
  }
  public static void prepare() {
    prepare(true);
  }
  // إعداد Looper للسطر الحالي
  private static void prepare(boolean quitAllowed) {
    if (sThreadLocal.get() != null) {
      throw new RuntimeException("Only one Looper may be created per thread");
    }
    sThreadLocal.set(new Looper(quitAllowed));
  }
  // إعداد Looper لسطر UI
  public static void prepareMainLooper() {
    prepare(false);
    synchronized (Looper.class) {
      if (sMainLooper != null) {
        throw new IllegalStateException("The main Looper has already been prepared.");
      }
      sMainLooper = myLooper();
    }
  }

في فئة Looper، الطريقة myLooper()، يتم الحصول عليها من خلال sThreadLocal.get()، يتم استدعاء طريقة prepare() في طريقة prepareMainLooper()، في هذه الطريقة يتم إنشاء جسم Looper، ويتم تعيين الجسم في sThreadLocal(). بهذا يتم ربط الكومة بالسطر. من خلال طريقة sThreadLocal.get()، يتم ضمان عدم قدرة السطور المختلفة على الوصول إلى كومة الرسائل الخاصة بالسطر الآخر.

لماذا يجب أن يتم إنشاء الـHandler الذي يُستخدم لتحديث UI في السطر الرئيسي؟

لأن الـHandler يجب أن يكون مرتبطًا بمحفظة الرسائل الخاصة بالسطر الرئيسي، حتى يتم تنفيذ handlerMessage في سطر UI، في هذه الحالة يكون سطر UI آمنًا.

(3) دورة الرسائل، معالجة الرسائل

إن إنشاء دورة الرسائل يتم من خلال طريقة Looper.loop(). والكود المصدر كالتالي:

/**
   * تشغيل قائمة الرسائل في هذا النقطة. تأكد من إجراء
   * {@link #quit()} لإتمام الدائرة.
   */
  public static void loop() {
    final Looper me = myLooper();
    if (me == null) {
      throw new RuntimeException("لا يوجد "Looper"؛ لم يتم إجراء "Looper.prepare()" في هذا النقطة.");
    }
    //استخراج قائمة الرسائل
    final MessageQueue queue = me.mQueue;
    //دائرة لا تنتهي، أي دائرة الرسائل
    for (;;) {
      //استخراج الرسالة، قد يكون هناك تأخير
      Message msg = queue.next(); // قد يكون هناك تأخير
      if (msg == null) {
        //لا يوجد رسالة يعني أن قائمة الرسائل تترك
        return;
      }
      //معالجة الرسالة
      msg.target.dispatchMessage(msg);
      //استعادة الرسالة
      msg.recycleUnchecked();
    }
  }

من البرنامج المذكور يمكننا رؤية أن معنى طريقة "loop()" هو إنشاء دائرة لا تنتهي، ثم إخراج الرسائل من قائمة الرسائل بشكل فردي، وأخيرًا معالجة الرسائل. بالنسبة لـ "Looper": يتم إنشاء كائن "Looper" من خلال "Looper.prepare()" (يتم تضمين قائمة الرسائل في كائن "Looper")، ويتم حفظه في "sThreadLocal"، ثم يتم تنفيذ دائرة الرسائل من خلال "Looper.loop()"، وهذه الخطوات تظهر عادةً كأزواج.

public final class Message implements Parcelable {
  //معالجة "target"
  Handler target; 
  //callback من نوع "Runnable"
  Runnable callback;
  //الرسالة التالية، قائمة الرسائل مخزنة بشكل سلس
  Message next;
}

من المصدر يمكن رؤية أن "target" نوع "Handler". في الواقع، إنه دورة واحدة، حيث يتم إرسال الرسائل إلى قائمة الرسائل من خلال "Handler"، ويقوم قائمة الرسائل بتوزيع الرسائل على "Handler" للمعالجة. في فئة "Handle":

// function تعامل الرسائل، يجب على الفرعية تغييرها
public void handleMessage(Message msg) {
}
private static void handleCallback(Message message) {
    message.callback.run();
  }
// توزيع الرسائل
public void dispatchMessage(Message msg) {
    إذا (msg.callback != null) {
      handleCallback(msg);
    }
      إذا (mCallback != null) {
        إذا (mCallback.handleMessage(msg)) {
          return;
        }
      }
      handleMessage(msg);
    }
  }

من خلال البرنامج المذكور أعلاه يمكن ملاحظة أن method dispatchMessage هو مجرد method توزيع، إذا كان callback من نوع Runnable فارغ، يتم تنفيذ method handleMessage لتعامل الرسالة، وينبغي أن يكون هذا method فارغ، سنكتب كود تحديث واجهة المستخدم في هذا function؛ إذا كان callback غير فارغ، يتم تنفيذ method handleCallback لتعامل، هذا method سيقوم بتهيئة method run في callback. في الواقع هذا هما نوعان من توزيع Handler، مثل post(Runnable callback) يكون callback غير فارغ، وعندما نستخدم Handler لتسليم رسائل، عادة لا نضبط callback، لذا يتم تنفيذ handleMessage.

 public final boolean post(Runnable r)
  {
    return sendMessageDelayed(getPostMessage(r), 0);
  }
  public String getMessageName(Message message) {
    إذا (message.callback != null) {
      return message.callback.getClass().getName();
    }
    return "0x" + Integer.toHexString(message.what);
  }
  public final boolean sendMessageDelayed(Message msg, long delayMillis)
  {
    إذا (delayMillis < 0) {
      delayMillis = 0;
    }
    return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
    MessageQueue queue = mQueue;
    if (queue == null) {
      RuntimeException e = new RuntimeException(
          this + " sendMessageAtTime() called with no mQueue\
      Log.w("Looper", e.getMessage(), e);
      return false;
    }
    return enqueueMessage(queue, msg, uptimeMillis);
  }

من خلال النظر في البرنامج المذكور، يمكن رؤية أن في post(Runnable r)، سيتم تلميع Runnable إلى Message object، وسيتم تعيين Runnable على Message object callback، وأخيرًا سيتم إدراج هذا الوجهة في قائمة الرسائل.sendMessage هو تنفيذ مشابه:

public final boolean sendMessage(Message msg)
  {
    return sendMessageDelayed(msg, 0);
  }

سواء كان يتم إرسال Runnable أو Message، فإنه سيتم استدعاء sendMessageDelayed(msg, time)方法. سيدفع Handler النبأ إلى MessageQueue، وسيقرأ Looper النبأ باستمرار من MessageQueue، وسيقوم بتفويض النبأ من خلال dispatchMessage من Handler، وبالتالي يتم إنتاج النبأ بشكل مستمر، وإضافته إلى MessageQueue، وتعامل النبأ من قبل Handler، ويبدأ تطبيق Android في العمل.

3. التحقق

new Thread(){
  Handler handler = null;
  public void run () {
    handler = new Handler();
  ;
.start();

هل هناك مشكلة في هذا الكود؟

Looper الموضوع هو ThreadLocal، أي أن كل thread يستخدم Looper الخاص به، يمكن أن يكون هذا Looper فارغًا. ولكن، عند إنشاء.Handler في thread فرعي، إذا كان Looper فارغًا، فإنه سيظهر استثناء.

public Handler(Callback callback, boolean async) {
    //كود مرفوع
    //الحصول على Looper
    mLooper = Looper.myLooper();
    if (mLooper == null) {
      throw new RuntimeException(
        "Can't create handler inside thread that has not called Looper.prepare()"
    }
    //الحصول على مجموعة الرسائل
    mQueue = mLooper.mQueue;
  }

عندما يكون mLooper فارغًا، يتم إلقاء استثناء. هذا لأن لم يتم إنشاء مكتبة Looper، لذا سيعود sThreadLocal.get() إلى null. المبدأ الأساسي لـ Handler هو أن يكون مرتبطًا بمجموعة الرسائل، وأن تُرسل الرسائل إلى مجموعة الرسائل، و如果没有 مجموعة الرسائل، فإنه لا يكون هناك حاجة لـ Handler، بينما تكون مجموعة الرسائل معبأة في Looper، لذا لا يمكن إنشاء Handler إلا إذا كان Looper غير فارغ. الحل هو كالتالي:

new Thread(){
  Handler handler = null;
  public void run () {
    //إنشاء Looper الحالي وربطه بThreadLocal
    Looper.prepare()
    handler = new Handler();
    //تشغيل دائرة الرسائل
    Looper.loop();
  ;
.start();

إذا قمت بإنشاء Looper فقط دون تشغيل دائرة الرسائل، فإنه لا يلقي استثناءً لكنه لن يكون فعالًا أيضًا من خلال post أو sendMessage() من خلال handler. لأن الرسائل ستُضاف إلى قائمة الرسائل، ولكن لم يتم تشغيل دائرة الرسائل، ولن يتم الحصول على الرسائل من قائمة الرسائل وتنفيذها.

شكرًا على القراءة، آمل أن تساعدكم، شكرًا لدعمكم لهذا الموقع!

سيكون لك أيضًا