Android面试笔记——Handler

1. Handler 的作用

在Android为了保障线程安全,规定只能由主线程来更新UI信息。而在实际开发中,会经常遇到多个子线程都去操作UI信息的情况,那么就会导致UI线程不安全。这时,我们就需要借助 Handler 作为媒介,让 Handler 通知主线程按顺序一个个去更新UI,避免UI线程不安全。

那么,子线程要更新UI的信息时,我们就需要将要更新的消息传递到 UI主线程中,再由主线程完成更新,从而实现工作线程对UI的更新处理,最终完成异步消息的处理(如图1所示)。

image

2. Handler 相关概念解释

主要涉及的有:处理器(Handler)、消息(Message)、消息队列(Message Queue)、循环器(Looper)。

概念 定义 作用
Message 线程间通讯的数据单元(即Handler接受/处理的对象) 存储需要操作的信息
Message Queue 一种数据结构(先进先出) 存储Handler发来的消息
Handler 主线程与子线程的通讯媒介<br />线程消息的处理者 添加消息(Message)到消息队列(Message Queue)<br />处理由循环器(Looper)分配过来的消息(Message)。
Looper Message Queue 与 Handler的通讯媒介 消息获取:循环取出essage Queue中的Message <br />消息分发:将取出的Message发送给对应的Handler

3.Handler 工作流程

src=http%3A%2F.png
648037-20190725145839044-240090020.png

4.Handler 使用

4.1子线程向主线程发消息

我们一般使用handler发送消息,只需要两步,首先是创建一个Handler对象,并重写handleMessage方法,就是上图中的3(Message.target.handleMeesage),然后需要消息通信的地方,通过Handler的sendMessage方法发送消息(这里我们创建了一个子线程,模拟子线程向主线程发送消息)。代码如下:

public class MainActivity extends Activity {
    private static final String TAG = "MainActivity";

    private Handler mHandler;
    private Button btnSendeToMainThread;
    private static final int MSG_SUB_TO_MAIN= 100;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // 1.创建Handler,并重写handleMessage方法
        mHandler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
                // 处理消息
                switch (msg.what) {
                    case MSG_SUB_TO_MAIN:
                        // 打印出处理消息的线程名和Message.obj
                        Log.e(TAG, "接收到消息: " +    Thread.currentThread().getName() + ","+ msg.obj);
                        break;
                    default:
                        break;
                }
            }
        };

        btnSendeToMainThread = (Button) findViewById(R.id.btn_sendto_mainthread);
        btnSendeToMainThread .setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 创建一个子线程,在子线程中发送消息
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        Message msg = Message.obtain();
                        msg.what = MSG_SUB_TO_MAIN;
                        msg.obj = "这是一个来自子线程的消息";
                        // 2.发送消息
                        mHandler.sendMessage(msg);
                    }
                }).start();
            }
        });
    }
}

4.2主线程向子线程发消息

handler需要与looper绑定,在主线程开始的时候会自动创建一个looper,而在子线程中需要我们自己去创建looper。所以使用Handler通信之前需要有以下三步:

  1. 调用Looper.prepare()
  2. 创建Handler对象
  3. 调用Looper.loop()

代码如下:

// 创建一个子线程,并在子线程中创建一个Handler,且重写handleMessage
        new Thread(new Runnable() {
            @Override
            public void run() {
                Looper.prepare();
                subHandler = new Handler() {
                    @Override
                    public void handleMessage(Message msg) {
                        super.handleMessage(msg);
                        // 处理消息
                        switch (msg.what) {
                            case MSG_MAIN_TO_SUB:
                                Log.e(TAG, "接收到消息: " +  Thread.currentThread().getName() + ","+ msg.obj);
                                break;
                            default:
                                break;
                        }
                    }
                };
                Looper.loop();
            }
        }).start();

        btnSendToSubThread = (Button) findViewById(R.id.btn_sendto_subthread);
        btnSendToSubThread.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Message msg = Message.obtain();
                msg.what = MSG_MAIN_TO_SUB;
                msg.obj = "这是一个来自主线程的消息";
                // 主线程中发送消息
                subHandler.sendMessage(msg);
            }
        });

5. Handler机制原理

5.1 Looper.prepare()

    public static void prepare() {
        prepare(true);
    }

    private static void prepare(boolean quitAllowed) {
        // 规定了一个线程只有一个Looper,也就是一个线程只能调用一次Looper.prepare()
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        // 如果当前线程没有Looper,那么就创建一个,存到sThreadLocal中
        sThreadLocal.set(new Looper(quitAllowed));
    }

从上面的代码可以看出,一个线程最多只有一个Looper对象。当没有Looper对象时,去创建一个Looper,并存放到sThreadLocal中,sThreadLocal是一个static的ThreadLocal对象,它存储了Looper对象的副本,并且可以通过它取得当前线程在之前存储的Looper的副本。如下图:

Looper的构造方法:

    private Looper(boolean quitAllowed) {
        // 创建了MessageQueue,并供Looper持有
        mQueue = new MessageQueue(quitAllowed);
        // 让Looper持有当前线程对象
        mThread = Thread.currentThread();
    }

这里主要就是创建了消息队列MessageQueue,并让它供Looper持有,因为一个线程最大只有一个Looper对象,所以一个线程最多也只有一个消息队列。然后再把当前线程赋值给mThread。

MessageQueue的构造方法没有什么可讲的,它就是一个消息队列,用于存放Message。

所以Looper.prepare()的作用主要有以下三点

  1. 创建Looper对象
  2. 创建MessageQueue对象,并让Looper对象持有
  3. 让Looper对象持有当前线程

5.2 new Handler()

    public Handler() {
        this(null, false);
    }

    public Handler(Callback callback, boolean async) {
      // 不相关代码
       ......
        //得到当前线程的Looper,其实就是调用的sThreadLocal.get
        mLooper = Looper.myLooper();
        // 如果当前线程没有Looper就报运行时异常
        if (mLooper == null) {
            throw new RuntimeException(
                "Can't create handler inside thread that has not called Looper.prepare()");
        }
        // 把得到的Looper的MessagQueue让Handler持有
        mQueue = mLooper.mQueue;
        // 初始化Handler的Callback
        mCallback = callback;
        mAsynchronous = async;
    }

Handler的创建过程主要有以下几点

  1. 创建Handler对象
  2. 得到当前线程的Looper对象,并判断是否为空
  3. 让创建的Handler对象持有Looper、MessageQueu、Callback的引用

5.3 Looper.loop()

   public static void loop() {
        // 得到当前线程的Looper对象
        final Looper me = myLooper();
        if (me == null) {
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
        // 得到当前线程的MessageQueue对象
        final MessageQueue queue = me.mQueue;
        
        // 无关代码
        ......
        
        // 死循环
        for (;;) {
            // 不断从当前线程的MessageQueue中取出Message,当MessageQueue没有元素时,方法阻塞
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }
            // Message.target是Handler,其实就是发送消息的Handler,这里就是调用它的dispatchMessage方法
            msg.target.dispatchMessage(msg);
            // 回收Message
            msg.recycleUnchecked();
        }
    }

首先还是判断了当前线程是否有Looper,然后得到当前线程的MessageQueue。接下来,就是最关键的代码了,写了一个死循环,不断调用MessageQueue的next方法取出MessageQueue中的Message,注意,当MessageQueue中没有消息时,next方法会阻塞,导致当前线程挂起,后面会讲到。

拿到Message以后,会调用它的target的dispatchMessage方法,这个target其实就是发送消息时用到的Handler。所以就是调用Handler的dispatchMessage方法,代码如下:

    public void dispatchMessage(Message msg) {
        // 如果msg.callback不是null,则调用handleCallback
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            // 如果 mCallback不为空,则调用mCallback.handleMessage方法
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            // 调用Handler自身的handleMessage,这就是我们常常重写的那个方法
            handleMessage(msg);
        }
    }

可以看出,这个方法就是从MessageQueue中取出Message以后,进行分发处理。

首先,判断msg.callback是不是空,其实msg.callback是一个Runnable对象,是Handler.post方式传递进来的参数,后面会讲到。而hanldeCallback就是调用的Runnable的run方法。

然后,判断mCallback是否为空,这是一个Handler.Callback的接口类型,之前说了Handler有多个构造方法,可以提供设置Callback,如果这里不为空,则调用它的hanldeMessage方法,注意,这个方法有返回值,如果返回了true,表示已经处理 ,不再调用Handler的handleMessage方法;如果mCallback为空,或者不为空但是它的handleMessage返回了false,则会继续调用Handler的handleMessage方法,该方法就是我们经常重写的那个方法。

关于从MessageQueue中取出消息以后的分发,如下面的流程图所示:

5.4发送消息

使用Handler发送消息主要有两种,一种是sendMessage方式,还有一个post方式,不过两种方式最后都会调用到sendMessageDelayed方法。

sendMessage方法传入的是Message,将Message传入Message Queue。

post方法代码:

   public final boolean post(Runnable r)
   {
      return  sendMessageDelayed(getPostMessage(r), 0);
   }

   private static Message getPostMessage(Runnable r) {
       // 构造一个Message,并让其callback执行传来的Runnable
       Message m = Message.obtain();
       m.callback = r;
       return m;
   }

可以看到,post方法只是先调用了getPostMessage方法,用Runnable去封装一个Message,然后就调用了sendMessageDelayed,把封装的Message加入到MessageQueue中。

所以使用handler发送消息的本质都是:把Message加入到Handler中的MessageQueue中去。

6.Handler的内存泄漏

Handler的常用方式:

public class HandlerActivity extends AppCompatActivity {
    private static final String TAG = "HandlerActivity";
    private Handler mHandler;
    private Button btn;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_handler);

        // 匿名内部类
        mHandler = new Handler(){
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
                // 处理消息
            }
        };

        btn = (Button) findViewById(R.id.btn);
        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 发送延时100s的消息
                mHandler.sendEmptyMessageDelayed(100, 100 * 1000);
            }
        });
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // 使用leakcanary做内存泄漏检测
        RefWatcher refWatcher = MyApplication.getRefWatcher(this);
        if (refWatcher != null) {
            refWatcher.watch(this);
        }
    }
}

但是会有一个问题,我们进入这个页面然后点击按钮,发送一个延时100s的消息,再退出这个Activity,这时候可能导致内存泄漏。

根本原因是因为我们创建的匿名内部类Handler对象持有了外部类Activity的对象,我们知道,当使用handler发送消息时,会把handler作为Message的target保存到MessageQueue,由于延时了100s,所以这个Message暂时没有得到处理,这时候它们的引用关系为MessageQueue持有了Message,Message持有了Handler,Handler持有了Activity,如下图所示

当退出这个Activity时,因为Handler还持有Activity,所以gc时不能回收该Activity,导致了内存泄漏。

解决方案:

静态内部类+弱引用

静态内部类是不会引用外部类的对象的,但是既然静态内部类对象没有持有外部类的对象,那么我们怎么去调用外部类Activity的方法呢?答案是使用弱引用。代码如下:

public class HandlerActivity extends AppCompatActivity {
    private static final String TAG = "HandlerActivity";
    private Handler mHandler;
    private Button btn;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_handler);
        
        // 创建Handler对象,把Activity对象传入
        mHandler = new MyHandler(HandlerActivity.this);

        btn = (Button) findViewById(R.id.btn);
        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 发送延时100s的消息
                mHandler.sendEmptyMessageDelayed(100, 100 * 1000);
            }
        });
    }

    // 静态内部类
    static class MyHandler extends Handler {
        private WeakReference<Activity> activityWeakReference;
        public  MyHandler(Activity activity) {
            activityWeakReference = new WeakReference<Activity>(activity);
        }
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            // 处理消息
            if (activityWeakReference != null) {
                Activity activity = activityWeakReference.get();
                // 拿到activity对象以后,调用activity的方法
                if (activity != null) {

                }
            }
        }
    }
}

首先,我们自定义了一个静态内部类MyHandler,然后创建MyHandler对象时传入当前Activity的对象,供Hander以弱应用的方式持有,这个时候Activity就被强引用和弱引用两种方式引用了,我们继续发起一个延时100s的消息,然后退出当前Activity,这个时候Activity的强引用就不存在了,只存在弱引用,gc运行时会回收掉只有弱引用的Activity,这样就不会造成内存泄漏了。

但这个延时消息还是存在于MessageQueue中,得到这个Message被取出时,还是会进行分发处理,只是这时候Activity被回收掉了,activity为null,不能再继续调用Activity的方法了。所以,其实这是Activity可以被回收了,而Handler、Message都不能被回收。

至于为什么使用弱引用而没有使用软引用,其实很简单,对比下两者回收前提条件就清楚了

  1. 弱引用(WeakReference): gc运行时,无论内存是否充足,只有弱引用的对象就会被回收
  2. 软引用(SoftReference): gc运行时,只有内存不足时,只有软引用的对象就会被回收

很明显,当我们Activity退出时,我们希望不管内存是否足够,都应该回收Activity对象,所以使用弱引用合适。

7.Handler面试常见问题

1、线程、Looper、Handler之间的关系如下:

  • 一个线程只能绑定一个Looper,一个MessageQueue;但一个Thread可以有多个Handler。
  • 一个Looper可绑定多个Handler,一个MessageQueue。
  • 一个Handler只能绑定一个Looper。
image

2、子线程中创建 Handler 对象

不可以在子线程中直接调用 Handler 的无参构造方法,因为 Handler 在创建时必须要绑定一个 Looper 对象

Looper.prepare();
Handler handler = new Handler();
// 这一步可别可少了
Looper.loop();

3、Handler 是如何与 Looper 关联的?

(1)通过构造方法传参

Looper looper = .....;
Handler handler = new Handler(looper);

(2)直接调用无参构造方法自动绑定

// Handler.java:192
public Handler(Callback callback, boolean async) {
    if (FIND_POTENTIAL_LEAKS) {
        final Class<? extends Handler> klass = getClass();
        if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
                (klass.getModifiers() & Modifier.STATIC) == 0) {
            Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
                klass.getCanonicalName());
        }
    }

    mLooper = Looper.myLooper(); // 就是这里
    if (mLooper == null) {
        throw new RuntimeException(
            "Can't create handler inside thread that has not called Looper.prepare()");
    }
    mQueue = mLooper.mQueue;
    mCallback = callback;
    mAsynchronous = async;
}

4、Looper 是如何与 Thread 关联的

Looper 与 Thread 之间是通过 ThreadLocal 关联的,这个可以看 Looper.prepare() 方法

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 中有一个 ThreadLocal 类型的 sThreadLocal静态字段,Looper通过它的 getset 方法来赋值和取值。

由于 ThreadLocal是与线程绑定的,所以我们只要把 LooperThreadLocal 绑定了,那 LooperThread 也就关联上了

5、在子线程中如何获取当前线程的 Looper

Looper.myLooper()
    
    // Looper.java:203
public static @Nullable Looper myLooper() {
    return sThreadLocal.get();
}
//获取主线程looper
Looper.getMainLooper()

6、Looper.loop() 会退出吗?

不会自动退出,但是我们可以通过 Looper.quit() 或者 Looper.quitSafely() 让它退出。

两个方法都是调用了 MessageQueue.quit(boolean) 方法,当 MessageQueue.next() 方法发现已经调用过 MessageQueue.quit(boolean) 时会 return null 结束当前调用,否则的话即使 MessageQueue 已经是空的了也会阻塞等待

7、MessageQueue#next 在没有消息的时候会阻塞,如何恢复?

当其他线程调用 MessageQueue#enqueueMessage 时会唤醒 MessageQueue,这个方法会被 Handler#sendMessageHandler#post 等一系列发送消息的方法调用。

boolean enqueueMessage(Message msg, long when) {
    // 略
    synchronized (this) {
        // 略
        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 {
            // 略
        }
        if (needWake) {
            nativeWake(mPtr); // 唤醒
        }
    }
    return true;
}

8、Looper.loop() 方法是一个死循环为什么不会阻塞APP

线程是一段可执行的代码,当可执行代码执行完成后,线程生命周期便该终止了,线程退出。而对于主线程肯定不能运行一段时间后就自动结束了,那么如何保证一直存活呢??简单的做法就是可执行代码能一直执行下去,死循环便能保证不会被退出。把所有要做的任务放到循环中去做就不会觉得卡了。

9、子线程更新UI的方式

  1. Handler的sendMessage方式
  2. Handler的post方式
  3. Activity的runOnUiThread方法
  4. View的post方式

10、总结

Android中,有哪些是基于Handler来实现通信的?
答:App的运行、更新UI、AsyncTask、Glide、RxJava等

处理Handler消息,是在哪个线程?一定是创建Handler的线程么?
答:创建Handler所使用的Looper所在的线程

消息是如何插入到MessageQueue中的?
答: 是根据when在MessageQueue中升序排序的,when=开机到现在的毫秒数+延时毫秒数

当MessageQueue没有消息时,它的next方法是阻塞的,会导致App ANR么?
答:不会导致App的ANR,是Linux的pipe机制保证的,阻塞时,线程挂起;需要时,唤醒线程

子线程中可以使用Toast么?
答:可以使用,但是Toast的显示是基于Handler实现的,所以需要先创建Looper,然后调用Looper.loop。

Looper.loop()是死循环,可以停止么?
答:可以停止,Looper提供了quit和quitSafely方法

Handler内存泄露怎么解决?
答: 静态内部类+弱引用 、Handler的removeCallbacksAndMessages等方法移除MessageQueue中的消息

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,869评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,716评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,223评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,047评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,089评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,839评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,516评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,410评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,920评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,052评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,179评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,868评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,522评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,070评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,186评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,487评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,162评论 2 356

推荐阅读更多精彩内容