【Handlerを拾い直す】--postDelayedの原理を本当に知っていますか?
14404 ワード
Handlerはすでにありふれた知識点だが、最近振り返ったソースコードの時、Handlerは当初ソースコードを見て想像していたほど簡単ではないことに気づいた.
このブログではpostDelayedの原理を分析します.最初はあまり手間がかからないと思っていましたが、次のコードのようなインスタンスを結合する必要があります.
ここでHanlderを用いて2つのメッセージが連続して送信されているのが見えますが、1つ目は500 ms遅延していますが、この場合のHandlerはどのように実行されますか?この时私はつぶやいて、心の中で理解するかもしれませんが、口でははっきり言えません.この时、ソースコードをめくる必要があることを説明します.
ここでまず、postDelayedを利用すると、伝わる時間は、単純に遅延時間を使うのではなく、最後に現在の時間と加算されることがわかります.Handlerメカニズムを熟知しているのは,Handlerを用いてメッセージを送信すると,最終的にはMessageQueueにメッセージが挿入され,最終的にはenqueueMessageメソッドに実行されることを知っているはずである.
ここでは、最も核心的な判断条件を見ることができます.
ここで、挿入されたメッセージの遅延と現在のチェーンヘッダの時点とを比較すると、現在のチェーンヘッダの時刻よりも前であれば、新しく挿入されたメッセージは新しいメッセージヘッダとなり、チェーンヘッダよりも長い場合、
遅延メッセージは、現在のメッセージキュー内のメッセージヘッダの実行時間と比較され、ヘッダの時間より前の場合は新しいメッセージヘッダとして作成されます.そうしないと、メッセージヘッダから後ろに遍歴し、適切な場所を見つけて遅延メッセージを挿入します.
後の判断も重要です
ここでは、Handlerがスリープ待機メッセージを起動する必要があるか否かを判断するためのものであり、具体的な判断条件は
注釈により、この変数は、
ここにはいくつかの私が重点的に注釈しているところが見えます.
メッセージヘッダからメッセージを取得すると、現在の時間と比較され、遅延が必要な場合は遅延時間が計算され、nextPollTimeoutMillisに割り当てられます.
遅延が必要でない場合、メッセージヘッダは正常に取り出す、mBlockedはfalse に設定される.
idleHandler数が0の場合、mBlockedはtrueに設定されます.
そこで、実際のシーンと結びつけて、postDelayedに対するHandlerの原理を分析することができます.
メッセージキューに現在メッセージがないため、
現在のヘッダメッセージはnullであるため、新しく挿入された遅延メッセージは直接ヘッダメッセージとして機能し、mBlocked=trueであるため、needWake=trueは
このシーンは、上記の分析に続いて、遅延メッセージを挿入すると、スリープ待機のプロセスに入り、mBlocked=trueになり、メッセージを挿入します.
このときメッセージヘッダは遅延メッセージであり、新しく挿入されたメッセージは遅延メッセージではなく、遅延メッセージの時間よりも小さいに違いないので、新しく挿入されたメッセージは遅延メッセージを置き換え、新しいメッセージヘッダとなり、needWake=mBlocked=trueとなるため、
以上の分析に基づいて、実際にはHandlerに対してまた更新された認識があるに違いありません.ここでいくつかの問題を広げます.
ここではLinux pipe/epollメカニズムに関し、簡単に言えばメインスレッドのMessageQueueにメッセージがない場合にloopのqueueにブロックする.next()のnativePollOnce()メソッドでは、メインスレッドが次のメッセージが到着するかトランザクションが発生するまでCPUリソースを解放し、pipeパイプの書き込み側にデータを書き込むことでメインスレッドの動作を起動します.ここで採用するepollメカニズムは、IO多重化メカニズムであり、複数の記述子を同時に監視することができ、ある記述子が準備完了(読み取りまたは書き込み準備完了)すると、直ちに対応するプログラムに読み取りまたは書き込み操作を通知し、本質的に同期I/O、すなわち読み書きがブロックされる.したがって,主スレッドは多くの場合スリープ状態であり,CPUリソースを大量に消費することはない.
(1)Loop.loop()ではシーケンス処理メッセージであり、前のメッセージ処理に時間がかかり、完了後にwhenを超えた場合、メッセージがwhen時点で処理されることはあり得ない.(2)whenの時点が他のメッセージの処理に占有されていなくても、スレッドはスケジューリングされてcpuタイムスライスを失う可能性がある.(3)待ち時間点whenの過程でより早くエンキュー処理される可能性のあるメッセージは,優先的に処理され,(1)の可能性が増す.したがって、上述の3点から分かるように、Handlerが提供する指定処理時間のapi、例えばpostDelayed()/postAtTime()/sendMessageDelayed()/sendMessageAtTime()/sendMessageAtTime()は、指定時間までに実行されないことしか保証されず、指定された時点で実行されることは保証されない.
PostDelayed原理Handlerパイプの原理
前言
このブログではpostDelayedの原理を分析します.最初はあまり手間がかからないと思っていましたが、次のコードのようなインスタンスを結合する必要があります.
handler.postDelayed(new Runnable() {
@Override
public void run() {
}
}, 500);
handler.post(new Runnable() {
@Override
public void run() {
}
});
ここでHanlderを用いて2つのメッセージが連続して送信されているのが見えますが、1つ目は500 ms遅延していますが、この場合のHandlerはどのように実行されますか?この时私はつぶやいて、心の中で理解するかもしれませんが、口でははっきり言えません.この时、ソースコードをめくる必要があることを説明します.
ソース分析
public final boolean postDelayed(Runnable r, long delayMillis)
{
return sendMessageDelayed(getPostMessage(r), delayMillis);
}
public final boolean sendMessageDelayed(Message msg, long delayMillis)
{
if (delayMillis < 0) {
delayMillis = 0;
}
return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
ここでまず、postDelayedを利用すると、伝わる時間は、単純に遅延時間を使うのではなく、最後に現在の時間と加算されることがわかります.Handlerメカニズムを熟知しているのは,Handlerを用いてメッセージを送信すると,最終的にはMessageQueueにメッセージが挿入され,最終的にはenqueueMessageメソッドに実行されることを知っているはずである.
boolean enqueueMessage(Message msg, long when) {
...
synchronized (this) {
...
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 {
// Inserted within the middle of the queue. Usually we don't have to wake
// up the event queue unless there is a barrier at the head of the queue
// and the message is the earliest asynchronous message in the queue.
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;
}
ここでは、最も核心的な判断条件を見ることができます.
if (p == null || when == 0 || when < p.when) {
// New head, wake up the event queue if blocked.
//
} else {
//
}
ここで、挿入されたメッセージの遅延と現在のチェーンヘッダの時点とを比較すると、現在のチェーンヘッダの時刻よりも前であれば、新しく挿入されたメッセージは新しいメッセージヘッダとなり、チェーンヘッダよりも長い場合、
for
サイクルを用いて適切な時点、すなわち現在挿入される遅延よりも長い時間を探して、新しく挿入されたメッセージをこの位置に挿入することがわかる.まず結論を出すことができます遅延メッセージは、現在のメッセージキュー内のメッセージヘッダの実行時間と比較され、ヘッダの時間より前の場合は新しいメッセージヘッダとして作成されます.そうしないと、メッセージヘッダから後ろに遍歴し、適切な場所を見つけて遅延メッセージを挿入します.
後の判断も重要です
if (needWake) {
nativeWake(mPtr);
}
ここでは、Handlerがスリープ待機メッセージを起動する必要があるか否かを判断するためのものであり、具体的な判断条件は
needWake
変数に関係していることがわかる.ここでneedWake
変数はmBlocked
という変数の値と密接に関連していることがわかる.ここではmBlocked
変数の定義を見てみましょう.// Indicates whether next() is blocked waiting in pollOnce() with a non-zero timeout.
private boolean mBlocked;
注釈により、この変数は、
next()
メソッドが実行中に遅延メッセージを待ってブロックされているかどうかを示すことを先に簡単に理解することができる.next()
の方法に言及した以上、私たちは必ずこの方法を見なければなりません.Message next() {
// Return here if the message loop has already quit and been disposed.
// This can happen if the application tries to restart a looper after quit
// which is not supported.
final long ptr = mPtr;
if (ptr == 0) {
return null;
}
int pendingIdleHandlerCount = -1; // -1 only during first iteration
int nextPollTimeoutMillis = 0;
for (;;) {
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}
//native
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
// Try to retrieve the next message. Return if found.
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
if (msg != null && msg.target == null) {
// Stalled by a barrier. Find the next asynchronous message in the queue.
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
if (msg != null) {
if (now < msg.when) {
// , , nextPollTimeoutMillis
// Next message is not ready. Set a timeout to wake up when it is ready.
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
// ,
// Got a message.
// mBlocked false
mBlocked = false;
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
if (DEBUG) Log.v(TAG, "Returning message: " + msg);
msg.markInUse();
return msg;
}
} else {
// No more messages.
nextPollTimeoutMillis = -1;
}
// Process the quit message now that all pending messages have been handled.
if (mQuitting) {
dispose();
return null;
}
// If first time idle, then get the number of idlers to run.
// Idle handles only run if the queue is empty or if the first message
// in the queue (possibly a barrier) is due to be handled in the future.
if (pendingIdleHandlerCount < 0
&& (mMessages == null || now < mMessages.when)) {
pendingIdleHandlerCount = mIdleHandlers.size();
}
if (pendingIdleHandlerCount <= 0) {
// No idle handlers to run. Loop and wait some more.
// IdleHandler, mBlocked true
mBlocked = true;
continue;
}
if (mPendingIdleHandlers == null) {
mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
}
mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
}
// Run the idle handlers.
// We only ever reach this code block during the first iteration.
for (int i = 0; i < pendingIdleHandlerCount; i++) {
final IdleHandler idler = mPendingIdleHandlers[i];
mPendingIdleHandlers[i] = null; // release the reference to the handler
boolean keep = false;
try {
keep = idler.queueIdle();
} catch (Throwable t) {
Log.wtf(TAG, "IdleHandler threw exception", t);
}
if (!keep) {
synchronized (this) {
mIdleHandlers.remove(idler);
}
}
}
// Reset the idle handler count to 0 so we do not run them again.
pendingIdleHandlerCount = 0;
// While calling an idle handler, a new message could have been delivered
// so go back and look again for a pending message without waiting.
nextPollTimeoutMillis = 0;
}
}
ここにはいくつかの私が重点的に注釈しているところが見えます.
nativePollOnce(ptr, nextPollTimeoutMillis);
は、nextPollTimeoutMillis
の値に基づいてスリープするかどうかを決定し、このときnextPollTimeoutMillis
の値が0より大きい場合、next()
メソッドは、ここでスリープして起動を待つ.実際のシーン
そこで、実際のシーンと結びつけて、postDelayedに対するHandlerの原理を分析することができます.
1.メッセージキューに現在メッセージがありません。postDelayの遅延メッセージです。
メッセージキューに現在メッセージがないため、
next()
メソッドを実行するときif (msg != null) {
...
} else {
// No more messages.
nextPollTimeoutMillis = -1;
}
nextPollTimeoutMillis
が-1に設定され、idleHandlerがないためmBlocked=trueとなります.このとき、forループがnativePollOnce(ptr, nextPollTimeoutMillis);
に再実行されると、スリープ待機起動となる.このときpostDelayedがメッセージを送信、enqueueMessage
メソッドを実行すると、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 {
// Inserted within the middle of the queue. Usually we don't have to wake
// up the event queue unless there is a barrier at the head of the queue
// and the message is the earliest asynchronous message in the queue.
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);
}
現在のヘッダメッセージはnullであるため、新しく挿入された遅延メッセージは直接ヘッダメッセージとして機能し、mBlocked=trueであるため、needWake=trueは
nativeWake(mPtr);
メソッドを実行し、下位起動はスリープし、このときnext()
メソッドはnativePollOnce(ptr, nextPollTimeoutMillis);
から起動し、メッセージ実行を継続する.ここで疑問に思う人がいるかもしれませんが、このときメッセージを取るのは、私たちが挿入した遅延メッセージです.では、必ずスリープして起動を待っています.このとき、新しいメッセージの挿入がなければ、誰が遅延メッセージの実行を起動しますか?ここでnativePollOnce(ptr, nextPollTimeoutMillis);
は、実際には自動的に起動するメカニズムがあり、すなわち、nativeWake(mPtr);
の受動的な起動を利用するだけでなく、下位層が自動的に自身の実行遅延メッセージを起動する.2.メッセージ・キューにメッセージがない場合は、まず遅延メッセージを挿入し、通常の遅延しないメッセージを挿入します。
このシーンは、上記の分析に続いて、遅延メッセージを挿入すると、スリープ待機のプロセスに入り、mBlocked=trueになり、メッセージを挿入します.
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 {
// Inserted within the middle of the queue. Usually we don't have to wake
// up the event queue unless there is a barrier at the head of the queue
// and the message is the earliest asynchronous message in the queue.
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);
}
このときメッセージヘッダは遅延メッセージであり、新しく挿入されたメッセージは遅延メッセージではなく、遅延メッセージの時間よりも小さいに違いないので、新しく挿入されたメッセージは遅延メッセージを置き換え、新しいメッセージヘッダとなり、needWake=mBlocked=trueとなるため、
nativeWake(mPtr);
メソッドが実行され、スリープが呼び覚まされ、このときnext()
メソッドのforループが実行されると、息ヘッダがキャンセルされ、新たに挿入された遅延を必要としないメッセージが取り出され、実行される.続いて後続のメッセージが遅延メッセージであるため,新たなスリープ待ちが行われる.広がる
以上の分析に基づいて、実際にはHandlerに対してまた更新された認識があるに違いありません.ここでいくつかの問題を広げます.
ハンドラーの休眠は具体的に何を指していますか?UIをブロックしますか?
ここではLinux pipe/epollメカニズムに関し、簡単に言えばメインスレッドのMessageQueueにメッセージがない場合にloopのqueueにブロックする.next()のnativePollOnce()メソッドでは、メインスレッドが次のメッセージが到着するかトランザクションが発生するまでCPUリソースを解放し、pipeパイプの書き込み側にデータを書き込むことでメインスレッドの動作を起動します.ここで採用するepollメカニズムは、IO多重化メカニズムであり、複数の記述子を同時に監視することができ、ある記述子が準備完了(読み取りまたは書き込み準備完了)すると、直ちに対応するプログラムに読み取りまたは書き込み操作を通知し、本質的に同期I/O、すなわち読み書きがブロックされる.したがって,主スレッドは多くの場合スリープ状態であり,CPUリソースを大量に消費することはない.
HandlerのDelayがwhenの時間に実行されるとは限らない
(1)Loop.loop()ではシーケンス処理メッセージであり、前のメッセージ処理に時間がかかり、完了後にwhenを超えた場合、メッセージがwhen時点で処理されることはあり得ない.(2)whenの時点が他のメッセージの処理に占有されていなくても、スレッドはスケジューリングされてcpuタイムスライスを失う可能性がある.(3)待ち時間点whenの過程でより早くエンキュー処理される可能性のあるメッセージは,優先的に処理され,(1)の可能性が増す.したがって、上述の3点から分かるように、Handlerが提供する指定処理時間のapi、例えばpostDelayed()/postAtTime()/sendMessageDelayed()/sendMessageAtTime()/sendMessageAtTime()は、指定時間までに実行されないことしか保証されず、指定された時点で実行されることは保証されない.
ブログのおすすめ
PostDelayed原理Handlerパイプの原理