1. ホーム
  2. Android

Android Handlerのメッセージングメカニズムの最も完全な説明(継続的に追加される)。

2022-02-11 11:34:29
<パス

 この記事では、Androidの開発で最もよく使われるHandlerについて、その使用過程で遭遇する様々な疑問について詳しく解説しています。

ハンドラ


 Androidの開発では、時間のかかる処理をサブスレッド(ワークスレッド)に入れて実行し、その結果をUIスレッド(メインスレッド)に伝えることがよくありますが、Androidに詳しい人なら、UI更新はメインスレッドでしかできないことを知っているはずです。そこで、ここで問題になるのが、このスレッドに
子スレッドからメインスレッドにデータを渡すのはどうでしょうか?
 Androidは、子スレッドのデータをメインスレッドに渡すために、Handlerというメッセージパッシングの仕組みを提供してくれています。実際、Handlerの原理を知ると、Handlerは子スレッドのデータをメインスレッドに渡すだけでなく、任意の2つのスレッドのデータを渡すことを実現できることがわかっています。
 次に、Handlerの仕組みと使い方を詳しく見ていきましょう。
 まず、Handlerの最も一般的な使い方をみてみましょう。

private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what) {
                case MESSAGE_WHAT:
                    Log.d(TAG, "main thread receiver message: " + ((String) msg.obj));
                    break;
            }
        }
    };
    
    private void sendMessageToMainThreadByWorkThread() {
        new Thread(){
            @Override
            public void run() {
                Message message = mHandler.obtainMessage(MESSAGE_WHAT);
                message.obj = "I am message from work thread";
                mHandler.sendMessage(message);
            }
        }.start();
    }
    /*
    * Normally we create a Handler in the main thread.
    * and then override the handlerMessage method of that Handler. You can see that the method passes in a parameter Message.
    * This parameter is the message we pass from other threads.
    *
    * We're looking at how the message is passed in the child thread, which gets an instance of Message through the Handler's objectMessage() method.
    * Let's take a look at a few properties of Message.
    * Message.what------------------> used to identify the int value of the message, through the value of the main thread can determine the source of the message from different places
    * Message.arg1/Message.arg2----->Message initial definition of the two variables used to pass the int type value
    * Message.obj-------------------> used to pass any instantiated object
    * Finally, the Message is sent out via sendMessage.
    *
    * The thread where the Handler is located will receive the specific message through the handlerMessage method, how can we determine the source of the message? By what value, of course.
    * How's that for simple?
    */



 冒頭で述べたように、Handlerは子スレッドからメインスレッドにデータを送るだけでなく、任意の2つのスレッドの間で通信を行うためのものです。
 2つの子スレッドがどのように通信しているかを見てみましょう。
 単純に、一方のスレッドでHandlerを作成し、もう一方のスレッドではそのHandlerへの参照を保持してsendMessageを呼び出すだけです
 実践しないプログラムは書けませんので、コードを叩いて見てみましょう

private Handler handler;
    private void handlerDemoByTwoWorkThread() {
        Thread hanMeiMeiThread = new Thread() {
            @Override
            public void run() {
// Looper.prepare();
                handler = new Handler() {
                    @Override
                    public void handleMessage(Message msg) {
                        Log.d(TAG, "hanMeiMei receiver message: " + ((String) msg.obj));
                        Toast.makeText(MainActivity.this, ((String) msg.obj), Toast.LENGTH_SHORT).show();
                    }
                };
// Looper.loop();
            }
        };
        Thread liLeiThread = new Thread() {
            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Message message = handler.obtainMessage();
                message.obj = "Hi MeiMei";
                handler.sendMessage(message);
            }
        };
        hanMeiMeiThread.setName("HanMeiMei Thread");
        hanMeiMeiThread.start();
        liLeiThread.setName("LiLei Thread");
        liLeiThread.start();

        /*
        * Done, we created two Threads, liLeiThread and hanMeiMeiThread two threads, very familiar names ah!
        * Not much different from the previous code hanMeiMeiThread created the Handler, and liLeiThread sent the message through the Handler.
        * Only here we only send a message, so we do not use what to mark
        * Run it and see, can our LiLei dial MeiMei?
        * Uh-oh, something went wrong.
        * 05-13 17:08:17.709 20673-20739/? E/AndroidRuntime: FATAL EXCEPTION: HanMeiMei Thread
                                                   Process: design.wang.com.designpatterns, PID: 20673
                                                   java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()
                                                       at android.os.Handler.
(Handler.java:200)
                                                       Handler.
(Handler.java:114)
        * Can't create handler inside thread that has not called Looper.prepare()
        * -----------" It says we created handler that has not called Looper.prepare();
        * Okay, let's call the method before instantiating the Handler and see. Plus isn't it reporting no more errors.
        * Wait, although no error is reported, but hanMeiMeiThread also did not receive a message ah, where is the message? Do not rush.
        * We add Looper.loop(); after the Handler instantiation to see if the message is received.
        * This is why?
        * Next we go to see how the Handler is implemented to send a message it, figure out the principle, the reason here is also understood.
        */



    }



 さて、半日の売り込みが終わり、いよいよ本題に入ります。
 まず、サブスレッドでインスタンス化する際にLooper.prepare()を呼ばないとエラーが報告される理由を見てみましょう。

// Let's first look at the reason for the error when new Handler();. The source code analysis will be explained later and only the key parts will be posted.
// Here is where the above exception is thrown in the Handler constructor, and you can see that it is thrown because the mLooper object is empty.
mLooper = Looper.myLooper();
        if (mLooper == null) {
            throw new RuntimeException(
                "Can't create handler inside thread that has not called Looper.prepare()");
        }
/*
  Now that we've seen the cause of the exception, let's look at what the Looper.prepare() method does.
*/
public static void prepare() {
    prepare(true);
}

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));
}
/*
 You can see that this method creates a Looper() at the current thread, ThreadLocal is mainly used to maintain the local variables of the thread  
*/
 private Looper(boolean quitAllowed) {
    mQueue = new MessageQueue(quitAllowed);
    mThread = Thread.currentThread();
}
// And inside the Looper's constructor creates another MessageQueue() object for us.


 これで、Handler機構の主要なオブジェクトであるLooper、MessageQueue、Messageがうまく引き出せた。
 実際には、このようなことはありません。アプリの初期化時にActivityThreadのmainメソッドが実行されるので、ActivityThreadのmain()メソッドが何をしているのかがわかります。

        Looper.prepareMainLooper();
        ActivityThread thread = new ActivityThread();
        thread.attach(false);
        if (sMainThreadHandler == null) {
            sMainThreadHandler = thread.getHandler();
        }
        if (false) {
            Looper.myLooper().setMessageLogging(new
                    LogPrinter(Log.DEBUG, "ActivityThread"));
        }
        // End of event ActivityThreadMain.
        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
        Looper.loop();
/*
 There is only one truth, yes Android has already called Looper.prepareMainLooper() for us when creating the main thread
 and Looper.loop() methods, so we can create the Handler directly in the main thread.
*/



 それでは、Handlerからメッセージを送信する処理に移りましょう。

// Calling the Handler with different parameter methods to send a Message will eventually call this method
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);
    }


 sendMessage のキーは enqueueMessage() で、これは内部的に messageQueue の enqueueMessage メソッドを呼び出します。

boolean enqueueMessage(Message msg, long when) {
        ...
        synchronized (this) {
            if (mQuitting) {
                IllegalStateException e = new IllegalStateException(
                        msg.target + " sending message to a Handler on a dead thread");
                Log.w(TAG, e.getMessage(), e);
                msg.recycle();
                return false;
            }

            msg.markInUse();
            msg.when = when;
            Message p = mMessages;
            boolean needWake;
            if (p == null || when == 0 || when < p.when) {
                // New head, wake up the event queue if blocked.
                msg.next = p;
                mMessages = msg;
                needWake = mBlocked;
            } else {
                needWake = mBlocked && p.target == null && msg.isAsynchronous();
                Message prev;
                for (;;) {
                    prev = p;
                    p = p.next;
                    if (p == null || when < p.when) {
                        break;
                    }
                    if (needWake && p.isAsynchronous()) {
                        needWake = false;
                    }
                }
                msg.next = p; // invariant: p == prev.next
                prev.next = msg;
            }

            // We can assume mPtr ! = 0 because mQuitting is false.
            if (needWake) {
                nativeWake(mPtr);
            }
        }
        return true;
    }
    /* You can see from the code that Message is stored in MessageQueue by storing the Message to the previous Message.next. 
      This forms a chained list, and also ensures that the Message list is time-sensitive.
    */



 実際に Message は Handler の対応するスレッドの MessageQueue に送られるわけですが、どうやって Message を取り出しているのでしょうか。
 注意深く見ていると、例外が発生する半日前にLoop.prepare()メソッドの説明はありましたが、Loop.loop()メソッドの説明はなかったことに早くも気づかれたのではないでしょうか?同時に、前の例でも見たが、Looper.loop()メソッドを呼び出さない場合は、ハンドラは、メッセージを受け付けていませんので、我々は、メッセージの取得と確かに無関係とは言えない推測を思い切ってすることができます もちろんオフ疑惑は十分ではありませんが、我々はまた、我々の疑惑を証明するために真実を見つける必要がありますか?だから我々は何を待っている、最初にloop()メソッドを見てみましょう。

public static void loop() {
//You can see that you can't call this method until you call Looper.prepare(), or else you'll throw another exception
        final Looper me = myLooper();
        if (me == null) {
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
        final MessageQueue queue = me.mQueue;

        // Make sure the identity of this thread is that of the local process,
        // and keep track of what that identity token actually is.
        Binder.clearCallingIdentity();
        final long ident = Binder.clearCallingIdentity();

        for (;;) {
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            return; }

            // This must be in a local variable, in case a UI event sets the logger
            final Printer logging = me.mLogging;
            if (logging ! = null) {
                logging.println(">>>>>> Dispatching to " + msg.target + " " +
                        msg.callback + ": " + msg.what);
            }

            final long traceTag = me.mTraceTag;
            if (traceTag ! = 0) {
                Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
            }
            try {
                msg.target.dispatchMessage(msg);
            } finally {
                if (traceTag ! = 0) {
                    Trace.traceEnd(traceTag);
                }
            }

            if (logging ! = null) {
                logging.println("<<<<<< Finished to " + msg.target + " " + msg.callback);
            }

            // Make sure that during the course of dispatching the
            // identity of the thread wasn't corrupted.
            final long newIdent = Binder.clearCallingIdentity();
            if (identity ! = newIdent) {
                Log.wtf(TAG, "Thread identity changed from 0x"
                        + Long.toHexString(identity) + " to 0x"
                        + Long.toHexString(newIdent) + " while dispatching to "
                        + msg.target.getClass().getName() + " "
                        + msg.callback + " what=" + msg.what);
            }

            msg.recycleUnchecked();
        }
    }
/*
Here we see that the mLooper() method takes out the looper object of the current thread, and then opens a dead loop from the looper object 
MessageQueue from the looper object, and as long as there is a Message object, the Message target will call
We can see that the target is the handler we created by the code, and we continue to analyze the distribution of the message
*/
public void dispatchMessage(Message msg) {
    if (msg.callback ! = null) {
        handleCallback(msg);
    } else {
        if (mCallback ! = null) { if (mCallback.handleMessage(msg); } else { if (mCallback !
            if (mCallback.handleMessage(msg)) { if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}
/* Well, it's clear here
As you can see, if we set the callback (Runnable object), then the handleCallback method will be called directly
*/
private static void handleCallback(Message message) {
        message.callback.run();
    }
// that is, if we set the callback (Runnable) object when initializing the Handler, then the run method is called directly. For example, we often write the runOnUiThread method, which ends up being executed in the main thread because the Handler is created in the main thread: runOnUiThread(run)
runOnUiThread(new Runnable() {
            @Override
            public void run() {
                
            }
        });
public final void runOnUiThread(Runnable action) {
      if (Thread.currentThread() ! = mUiThread) {
          mHandler.post(action);
      } else {
          action.run();
      }
  }
  /*
And if the msg.callback is empty
There is a situation when the Handler is created using the constructor method with Callback, the handleMessgae method of Callback will be executed, and it will determine whether the callback will be intercepted or not based on the return value of its method.  
If there is no interception by Callback, the Handler's handleMessage method will be executed. (Handler(Callback) constructor usage scenario is not encountered at the moment, welcome to add)
  */



 ここまでで、スレッド間でHandlerを使用する方法が明確になったのではないでしょうか。

結論から言うと

  1. ハンドラを使用する場合、ハンドラで生成されたスレッドは、各スレッドに1つずつ、固有のLooperオブジェクトを保持する必要があり、各スレッドのLooperはThreadLocalで保証されます ThreadLocalの詳しい解説はこちら ,
    Looperオブジェクトは一意のMessageQueueを保持するため、1つのスレッドで複数のハンドラを持つことができます。
  2. メッセージは MessageQueue にリストで格納されるのではなく、受信したメッセージは前のメッセージキューに預けられる。
    Messageの次で、取り出すときは、先頭のMessageを経由した順番に取り出すことができる。
  3. Looperオブジェクトはloop()メソッドでデッドループを開き、Looper内部のMessageQueueから常にMessageを取り出しています。
    そして、メッセージはハンドラを経由してハンドラのあるスレッドに配信される。

最後に、私自身の理解から描いたフローチャートを添付します。



ハンドラが追加されます。

1. Handlerを使用する際に注意すべき点として、メモリリークが挙げられます。

なぜメモリリークが発生するのか?
まず、Handlerはスレッド間通信に使われるので、新しく開かれたスレッドはHandlerの参照を保持することになります。
ActivityなどでHandlerを作成した場合、それが非静的な内部クラスであると、メモリリークの原因となることがあります。

  1. まず、非静的な内部クラスは暗黙的に外部クラスへの参照を保持することになるので、他のスレッドがHandlerを保持し、そのスレッドが破棄されない場合、Activityは常にHandlerから参照され、リコールを起こさないということになる。
  2. また、MessageQueueに未処理のMessageがある場合、そのMessageのターゲットもActivityなどの参照を保持していることになり、これもメモリリークの原因になります。
解決策

 (1). 静的内部クラス+弱参照を使う。

  静的内部クラスは、外部クラスへの参照を保持していない、あなたが外部クラス関連の操作を参照する必要があるときに、あなたも弱い参照を介して外部クラス関連の操作に取得することができます、弱い参照は、リサイクルの問題のリサイクルが発生しないオブジェクトではありませんが、いくつかの方法のJAVAの参照の詳細を確認することは明らかではありません。

private Handler sHandler = new TestHandler(this);

静的クラス TestHandler extends Handler { (ハンドラ)
    private WeakReference<Activity> mActivity;
    TestHandler(アクティビティ) {
        mActivity = new WeakReference<>(activity);
    }

    オーバーライド
    public void handleMessage(Message msg) { (メッセージ)
        super.handleMessage(msg)を実行します。
        Activity Activity = mActivity.get()。
        if (アクティビティ ! = null) {
            //TODO
        }
    }
}


 (2). 外側のクラスオブジェクトが破壊されたときに、メッセージの MessageQueue を空にする。例えば、ActivityのonDestroyでメッセージを空にする。

@Override
protected void onDestroy() {
    handler.removeCallbacksAndMessages(null);
    super.onDestroy();
}


2. Handlerを使用する場合、Messageオブジェクトは通常Handler.getMessage()で取得し、内部的にMessage.get()メソッドを呼び出すので、直接Messageをnewせず、静的メソッドのMessage get() メソッドで取得してはどうかという疑問が出てきますね。

以下は、そのためのコードです。

public static Message obtain() {
        synchronized (sPoolSync) {
            if (sPool ! = null) {
                Message m = sPool;
                sPool = m.next;
                m.next = null;
                m.flags = 0; // clear in-use flag
                sPoolSize --;
                return m;
            }
        }
        return new Message();
    }


 実際には、メッセージの静的なメッセージ変数sPoolがあり、この変数は、メッセージオブジェクトをキャッシュするために使用され、あなたがメッセージオブジェクトが必要なときに取得で見ることができる、sPoolが空ではない場合は、現在のsPool(メッセージ)を返しますと、以前のsPoolの次のオブジェクトを指すsPoolます、(以前はメッセージのストレージがチェーンストレージ、メッセージの次のメッセージを介しての形式であると述べ、ここではsPool現在のメッセージ、およびその次のメッセージを指すsPool再返しますです)。sPoolSizeには現在キャッシュされているMessageの数が記録され、sPoolが空の場合はキャッシュされているMessageがなく、新しいMessageを作成する必要がある(new Message)。

 次に、sPool にキャッシュされた Message がどこから来るのかを見てみましょう。

public void recycle() {
        if (isInUse()) {
            if (gCheckRecycle) {
                throw new IllegalStateException("This message cannot be recycled because it "
                        + "is still in use.");
            }
            return;
        }
        recycleUnchecked();
    }

void recycleUnchecked() {
        // Mark the message as in use while it remains in the recycled object pool.
        // Clear out all other details.
        flags = FLAG_IN_USE;
        flags = FLAG_IN_USE; what = 0;
        arg1 = 0;
        arg2 = 0;
        obj = null;
        replyTo = null;
        sendingUid = -1;
        when = 0;
        target = null;
        callback = null;
        data = null;

        synchronized (sPoolSync) {
            if (sPoolSize < MAX_POOL_SIZE) {
                next = sPool;
                sPool = this;
                sPoolSize++;
            }
        }
    }


 recycle() は、メッセージをリサイクルするためのメソッドで、メッセージが終了した時や空になった時などに呼ばれます。
recycleUnchecked() メソッドは what, arg1, arg2, object などの値をリセットする。現在の sPool (Message キャッシュプール) のサイズがキャッシュ可能な最大数より少ない場合、リサイクルされる Message の次を sPool に指し、sPool はリサイクルされる Message オブジェクトを指します (つまり Message は sPool に指します) リクエストした Message オブジェクトを指します (つまり Message は sPool キャッシュプールの先頭に配置されます)

概要

このように、Message は内部のデータ・キャッシュ・プールを保持しており、回収されたメッセージはすぐに破棄されずにキャッシュ・プールに置かれるため、get を使って Message オブジェクトを回収することができるのです。


3. ハンドラ sendMessage の原理を説明する。

 問題を紹介します

  1. sendMessageDelayedはどのようにメッセージの遅延配信を実装するのですか?
  2. sendMessageDelayedは、ブロックすることでメッセージの遅延配信を実現していますが、新しく追加されたメッセージはブロックされるのでしょうか?

詳細な分析については、以下の記事へ移動してください。 ハンドラ上級編 - sendMessageの原理を探る


その他のハンドラに関する問題を共有し、議論することができます。