这篇是之前Looper、Handler、Message以及MessageQueue之间的关系后续版本,将更加详细讲解一下之前没提到的细节内容。
先说一下这篇文章主要要解决的几个点:
- 为何Activity中直接new Hander()调用不报错;
- 为何之前文章提到的MessageQueue内部是一个死循环,但是UI并不卡顿;
- Handler.postDelay()实现的原理是什么;
- 子线程使用Handler的注意点;
- 一些其他的知识点;
为何Activity中直接new Hander()调用不报错
之前文章提到过要在线程(包括UI线程)中使用Handler,就需要Looper.prepare()以及Looper.loop()相关操作,但是为什么Activity中可以直接使用Handler呢?这个问题在之前文章中提到过,是因为系统已经帮我们做了Looper相关的准备操作,我们来看一下具体实现。
Android进程启动过程中会创建一个ActivityThread对象,并调用其中的main(),用以接收系统的各种操作。这个ActivityThread对象就是我们常说的UI线程,其实并不是一个线程。在main()中我们就能看到Looper初始化相关的操作。
public static void main(String[] args) {
...
Looper.prepareMainLooper();
...
ActivityThread thread = new ActivityThread();
thread.attach(false, startSeq);
if (sMainThreadHandler == null) {
sMainThreadHandler = thread.getHandler();
}
...
Looper.loop();
throw new RuntimeException("Main thread loop unexpectedly exited");
}
这里我们只看到了MainLooper初始化操作,并没有发现它跟Activity有何关联,接着往下看。
ActivityThread有一个成员变量mH,是一个H类型的对象,这个H是是Handler的子类,内部用于处理4大组件的启动、生命周期相关方法以及其他一些系统的回调。当然系统的启动过程太过冗长,这里不做细致的分析,现在只需要知道mH这个Handler对象。
final H mH = new H();
就是这个mH接收了来自ActivityManagerService传递过来的启动Activity的消息,并调用了ActivityThread中的performLaunchActivity()来创建一个Activity并调用相关生命周期方法()
/** Core implementation of activity launch. */
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
Activity activity = null;
try {
java.lang.ClassLoader cl = appContext.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
StrictMode.incrementExpectedActivityCount(activity.getClass());
r.intent.setExtrasClassLoader(cl);
r.intent.prepareToEnterProcess();
if (r.state != null) {
r.state.setClassLoader(cl);
}
} catch (Exception e) {
if (!mInstrumentation.onException(activity, e)) {
throw new RuntimeException(
"Unable to instantiate activity " + component
+ ": " + e.toString(), e);
}
}
....
return activity;
}
这里我们可以知道,Activity运行在mH内部。再看最上面ActivityThread的创建,可以看出mH中调用的方法也是在MainLooper中执行,Activity在MainLooper中执行,所以我们并不需要先初始化Looper再使用Handler。
为何之前文章提到的MessageQueue内部是一个死循环,但是UI并不卡顿
首先要明白一个问题,UI卡顿其实是MessageQueue中某些方法,比如onCreate()、onResume()等生命周期的回调执行过长,导致UI绘制某些帧丢失甚至ANR。MessageQueue虽然是个死循环,但是当消息队列为空时,只是阻塞消息队列,当时并不占用cpu资源,所以并不会引起UI的卡顿。至于为什么MessageQueue可以做到没有消息时阻塞队列,有消息时可以唤起继续工作的,读者可以查阅Handler机制在Native层中的实现,主要是Linux中的epoll。
当然你还有疑问为什么要将MessageQueue写成死循环,这是因为UI线程需要一直接收系统,以及其他组件发送过来的事件,中间可能会有空闲的情况,为了保证MessageQueue不退出,就只能写成死循环了。
Handler.postDelay()实现的原理是什么
前面的文章我们可以知道,不管Handler通过什么形式发送消息,最后都会调用到enqueueMessage()。这个方法内部调用了MessageQueue的enqueueMessage()。
boolean enqueueMessage(Message msg, long when) {
if (msg.target == null) {
//除了系统的同步控制,所有的msg都必须要有target来处理msg
throw new IllegalArgumentException("Message must have a target.");
}
if (msg.isInUse()) {
throw new IllegalStateException(msg + " This message is already in use.");
}
synchronized (this) {
if (mQuitting) {
//循环队列正在退出,直接将msg回收
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入队操作,标记msg的使用状态,处理时间等
msg.markInUse();
msg.when = when;
Message p = mMessages;
boolean needWake;
if (p == null || when == 0 || when < p.when) {
//如果当前队列为空,
//或者msg需要立即处理,
//或者当前消息队列中头消息处理的时间大于当前消息,
//都是将头消息置为当前消息,
//同时如果当前队列阻塞,则唤起消息队列进入循环
msg.next = p;
mMessages = msg;
needWake = mBlocked;
} else {
//进入这个分支,是将msg按执行时间先后插入消息链表中
//正常情况下我们不需要唤醒队列,除非当前消息对列正在
//同步消息控制,同时新消息为异步消息
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;
prev.next = msg;
}
// We can assume mPtr != 0 because mQuitting is false.
if (needWake) {
nativeWake(mPtr);
}
}
return true;
}
上面的方法我们可以看到,处理了几种情况都在注释里做了说明,下面再来看看消息具体是如何取出来的
Message next() {
//ptr = 0 当前队列正在退出
if (ptr == 0) {
return null;
}
int pendingIdleHandlerCount = -1; // -1 only during first iteration
//这个nextPollTimeoutMillis用来控制消息队列是否要阻塞,以及阻塞多久
//nextPollTimeoutMillis = 0 表示不阻塞
//nextPollTimeoutMillis = -1 表示一直阻塞,直到被唤醒
//nextPollTimeoutMillis > 0 表示阻塞这么长时间之后唤醒
int nextPollTimeoutMillis = 0;
for (;;) {
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}
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) {
//当前消息队列被同步控制,找到第一条异步的消息
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
if (msg != null) {
if (now < msg.when) {
//第一条异步消息尚未到执行时间,计算需要阻塞的时间
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
//找到了符合条件的消息,直接返回
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 {
//没有符合要求的异步消息或者消息队列为空,直接进入阻塞状态,等待唤醒
nextPollTimeoutMillis = -1;
}
// Process the quit message now that all pending messages have been handled.
if (mQuitting) {
dispose();
return null;
}
//首次循环的时候执行设置的IdleHandler
if (pendingIdleHandlerCount < 0
&& (mMessages == null || now < mMessages.when)) {
pendingIdleHandlerCount = mIdleHandlers.size();
}
....
// 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;
}
}
上面消息的enqueueMessage()以及next()取出操作可以很清晰的看到Handler.postDelay()的实现原理,就是消息按执行时间入队,并延迟处理。
子线程使用Handler的注意点
子线程中需要我们手动准备Looper,然后很方便的进行线程间通信,但是在消息处理完成之后要手动将Looper退出,以免造成内存泄漏。 从SDK 23 开始系统提供一个HandlerThread供我们方便的在子线程中使用Handler;
其他
Handler造成的内存泄漏
当我们在Activity内部直接创建一个非静态的Handler成员变量时,IDE就会提醒我们,这样使用Handler可能会造成内存泄漏,我们来分析一下原因在哪里。
- Handler所有发送的Message都有一个target指向发送的Handler,用于后续处理回调
- 非静态内部类隐式的持有外部类的一个引用
- 匿名内部类也隐式的持有外部类的一个引用
好,根据上面3点,我们来看一下Activity的引用:
public class MainActivity extends AppCompatActivity {
private Handler mHandler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mHandler = new Handler();
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
//do something
}
}, 10 * 60 * 1000);
}
}
当这个runnable消息非发送出去之后,mHandler被msg持有,并安排到10分钟之后执行,同时MainActivity也一直被mHandler和runnable持用。 当MainActivity退出时,因为Looper是UI线程的Looper,我们无法退出,msg还在消息队列中尚未执行,会阻止MainActivity被系统回收,就造成了内存泄漏。
避免这种情况方法有很多种这里提2种常用的方式:
-
定义静态Handler对象,并传人UI线程Looper
static Handler sHandler = new Handler(Looper.getMainLooper()); -
将我们需要的Handler定义成静态内部类,同时使用弱引用保存对Activity的引用
public static class MyHandler extends Handler { private final WeakReference<Activity> mActivityWf; public MyHandler(WeakReference<Activity> activityWf) { mActivityWf = activityWf; } }上面提到的同步控制
消息队列的同步控制是通过postSyncBarrier()这个方法来实现, 说是Barrier,其实就是在队列的头部插入一条特殊的msg
public int postSyncBarrier() {
return postSyncBarrier(SystemClock.uptimeMillis());
}
private int postSyncBarrier(long when) {
// Enqueue a new sync barrier token.
// We don't need to wake the queue because the purpose of a barrier is to stall it.
synchronized (this) {
final int token = mNextBarrierToken++;
final Message msg = Message.obtain();
msg.markInUse();
msg.when = when;
msg.arg1 = token;
Message prev = null;
Message p = mMessages;
if (when != 0) {
while (p != null && p.when <= when) {
prev = p;
p = p.next;
}
}
if (prev != null) { // invariant: p == prev.next
msg.next = p;
prev.next = msg;
} else {
msg.next = p;
mMessages = msg;
}
return token;
}
}
代码其实也挺简单的,就是在消息队列适当的位置插入一条没有target的msg,next()出去消息时,如果发现个同步控制消息,则它之后的同步消息就暂时不出列。
默认情况下,我们构建的Handler发送的都是同步消息,除非在Handler构建的时候指定发送的所有消息类型都为异步消息。
当然一般我们也不需要同步控制,虽然当前方法是public修饰的,但是hide,系统并没有打算将此方法开放给我们使用。
我们再来看一下,系统哪里用到了这个同步控制。
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
这是ViewRootImpl中的一个方法,这个方法执行之后,系统就开始进行页面的绘制流程,我们看到这里调用postSyncBarrier(),这么做的目的就是要让UI绘制尽快完成,以免造成卡顿。