安卓面试题 4 – 关于 Handler 机制

8,217 阅读5分钟
原文链接: www.iwfu.me

1- 为什么安卓要使用Handler:

因为安卓更新ui只能在主线程中执行,而在实际情况中经常要在子线程访问UI。为什么系统不允许子线程访问UI呢,因为AndroidUI不是线程安全的,如果在多线程操控UI可能会导致UI控件处于不可预知的状态;那为什么系统不给UI控件的访问加锁呢?首先上锁机制会让UI访问的逻辑变得复杂,其次加锁机制会降低访问效率。因此,安卓之所以提供Handler是为了解决在子线程不能更新UI的矛盾。

2 – 消息机制的原理:

安卓消息机制主要包括:
– Looper
– Handler
– MessageQueue

对于Message Queue:

指的是消息队列,,即存放供handler处理的消息。MessageQueue主要包括两个操作:插入和读取。读取本身会伴随删除操作。虽然名字叫队列,但其实内部实现是通过单链表的形式实现的(单链表在插入和删除上比较有优势,不是连续存储)。

对于Looper:

Looper在消息机制中进行消息循环,像一个泵,不断地从MessageQueue中查看是否有新消息并提取,交给handler处理。Handler机制一定要Looper,在线程中通过Looper.prepare()为当前线程创建一个Looper,并使用Looper.loop()来开启消息的读取。为什么在平常Activity主线程使用时没有使用到Looper呢?因为对于主线程(UI线程),会自动创建一个Looper 驱动消息队列获取消息,所以Looper可以通过getMainLooper获取到主线程的Looper。

通过quit/quitSafely可以退出Looper,区别在于quit会直接退出,quitSafely会把消息队列已有的消息处理完毕后才退出Looper。

对于Handler

Handler可以发送和接收消息。例如使用sendMessage就可以发送向消息队列插入一条消息,MessageQueue的next()方法就将消息返回给Looper,Looper收到消息后交由Handler处理,调用handMessage处理消息。

补充:

MessageQueue是什么时候创建的?

对于Looper,在线程主动调用Looper.prepare可以为当前线程主动创建一个Looper对象(主线程会自动生成一个Looper对象),对于Handler,由自己主动创建,而MessageQueue再什么时候创建的呢?在Looper的构造方法中:

  1. private Looper(boolean quitAllowed) {
  2.         mQueue = new MessageQueue(quitAllowed);
  3.         mRun = true;
  4.         mThread = Thread.currentThread();
  5. }

生成了一个MessageQueue!,因为一个线程对应了一个Looper对象,所以可以认为一个线程也对应一个MessageQueue对象,每一个MessageQueue都不能脱离Looper存在.

ThreadLocal在Handler机制中的作用

ThreadLocal是一个线程内部的数据存储类,通过它可以在制定的线程中存储数据,数据存储以后,只有在指定线程中可以获取到存储的数据,,对于其他线程就获取不到数据。一般来说,当某些数据是以线程为作用域而且不同线程需要有不同的数据副本的时候,可以考虑使用ThreadLocal。比如对于Handler,它要获取当前线程的Looper,很显然Looper的作用域就是线程,并且不同线程具有不同的Looper,(来自安卓开发艺术探索)

参考Looper.prepare()源码:

  1. public static final void prepare() {
  2.         if (sThreadLocal.get() != null) {
  3.             throw new RuntimeException(“Only one Looper may be created per thread”);
  4.         }
  5.         sThreadLocal.set(new Looper(true));
  6. }

sThreadLocal是一个ThreadLocal对象,可以在一个线程中存储变量。可以看到,在第5行,将一个Looper的实例放入了ThreadLocal。

Looper,Handler,MessageQueue的引用关系?

一个handler持有一个消息队列的引用和它构造时所属线程的Looper的引用.
也就是说,一个handler必定有它对应的消息队列和Looper,
一个线程至多能有一个Looper,也就至多能有一个消息队列.
一个线程中可以有多个handler.

在主线程中new了handler对象后,这个handler对象自动和主线程自动生成的Looper以及消息队列关联上了.
子线程中拿到主线程中handler的引用,发送消息后,消息对象就会发送到target属性对应的那个handler对应的消息队列中去,由对应的Looper来取出处理(子线程msg–>主线程handler–>主线程messageQueue–>主线程Looper—>主线程Handler的handMessage).
而消息发送到主线程handler,那么也就是发送到主线程的消息队列,用主线程中的Looper轮询.

Handler的post方法原理?

为了方便可以这么写:

  1. mHandler.post(new Runnable()
  2.         {
  3.             @Override
  4.             public void run()
  5.             {
  6.                 Log.e(“TAG”, Thread.currentThread().getName());
  7.                 mTxt.setText(“yoxi”);
  8.             }
  9.         });

然后run方法中可以写更新UI的代码,其实这个Runnable并没有创建什么线程,而是发送了一条消息,下面看源码:

  1. public final boolean post(Runnable r)
  2.    {
  3.       return  sendMessageDelayed(getPostMessage(r), 0);
  4.    }

最终和handler.sendMessage一样,调用了sendMessageAtTime,然后调用了enqueueMessage方法,给msg.target赋值为handler,最终加入MessagQueue.

可以看到,这里msg的callback和target都有值,那么会执行哪个呢?

其实上面已经贴过代码,就是dispatchMessage方法:

  1. public void dispatchMessage(Message msg) {
  2.        if (msg.callback != null) {
  3.            handleCallback(msg);
  4.        } else {
  5.            if (mCallback != null) {
  6.                if (mCallback.handleMessage(msg)) {
  7.                    return;
  8.                }
  9.            }
  10.            handleMessage(msg);
  11.        }
  12.    }

第2行,如果不为null,则执行callback回调,也就是我们的Runnable对象。

参考原文:http://blog.csdn.net/lmj623565791/article/details/38377229

Handler导致内存泄露问题?

一般我们写Handler:

Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
mImageView.setImageBitmap(mBitmap);
}
}

当使用内部类(包括匿名类)来创建Handler的时候,Handler对象会隐式地持有一个外部类对象(通常是一个Activity)的引用,而常常在Activity退出后,消息队列还有未被处理完的消息,此时activity依然被handler引用,导致内存无法回收而内存泄露。

在Handler中增加一个对Activity的弱引用(WeakReference):

static class MyHandler extends Handler {
WeakReference mActivityReference;

MyHandler(Activity activity) {
mActivityReference= new WeakReference(activity);
}

@Override
public void handleMessage(Message msg) {
final Activity activity = mActivityReference.get();
if (activity != null) {
mImageView.setImageBitmap(mBitmap);
}
}
}

对于上面的代码,用户在关闭Activity之后,就算后台线程还没结束,但由于仅有一条来自Handler的弱引用指向Activity,所以GC仍然会在检查的时候把Activity回收掉。这样,内存泄露的问题就不会出现了。