从源码的角度谈谈面试常客Handler的内部原理

栏目: 后端 · 发布时间: 5年前

内容简介:我们都知道,在进行Android应用开发的时候,主线程(又称为UI线程)不能进行网络请求一类的耗时操作,必须开启一个子线程来处理;但是在子线程里面又不能进行更新UI的操作,更新UI必须在主线程里操作。那么当子线程进行完耗时操作时如何通知主线程更新UI呐?这个时候Handler就孕育而生了。Handler被称之为Android内部消息机制,他的作用是在子线程进行完耗时操作的时发送消息通知主线程来更新UI。这里采用实例化一个Thread线程并通过线程阻塞sleep()模拟耗时操作。我们可以从上面代码看到,当线

我们都知道,在进行Android应用开发的时候,主线程(又称为UI线程)不能进行网络请求一类的耗时操作,必须开启一个子线程来处理;但是在子线程里面又不能进行更新UI的操作,更新UI必须在主线程里操作。那么当子线程进行完耗时操作时如何通知主线程更新UI呐?这个时候Handler就孕育而生了。

Handler被称之为Android内部消息机制,他的作用是在子线程进行完耗时操作的时发送消息通知主线程来更新UI。

使用

private Handler handler = new Handler(){
    @Override
    public void handleMessage(Message msg) {
        if (msg.what == 1){
            Toast.makeText(JavaDemo.this, "更新UI操作", Toast.LENGTH_SHORT).show();
        }
    }
};

@Override
protected void onCreate(Bundle savedInstanceState) {


    new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                Thread.sleep(5000);
                handler.sendEmptyMessage(1);
            } catch (InterruptedException e) {
                e.printStackTrace();

            }
        }
    }).start();
}
复制代码

这里采用实例化一个Thread线程并通过线程阻塞sleep()模拟耗时操作。我们可以从上面代码看到,当线程完成耗时操作之后,我们使用sendEmptyMessage()将消息发送给主线程中的handler,覆写主线程中handler的handMessage()方法并在这个方法里进行更新UI的操作。 这里需要注意一点,其实我们这么写handler是错误的,会引发内存泄露的问题,具体如何引起的我们后面再分析。

源码分析

handler的源码主要是由 LooperMessageQueueHandlerThreadLocal 几个部分组成。下面一个一个来进行分析。

Looper:

Looper主要由两部分东西组成,prepare()和loop()。首先来看 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));
}
复制代码

可以看到,在prepare()中主要做了两件事。第一,判断ThreadLocal中是否能够取出Looper对象,如果不为空,则抛出**"一个线程只能有一个Looper"**的异常。这就代表说在一个线程里有且仅可以创建一个Looper,如果多次调用prepare()方法创建Looper则程序会抛出异常。如果发现线程之中没有Looper,那么便会new一个Looper将其set进入ThreadLocal当中去。那么这个ThreadLocal又是什么?

ThreadLocal:

ThreadLocal被称为线程内部存储类。他有一个特点就是在A线程里面进行set()存储的数据,只能在A线程get()取出。

final ThreadLocal<Boolean> threadLocal = new ThreadLocal<>();
threadLocal.set(true);

new Thread("thread1"){
    @Override
    public void run() {
        
        Log.i("thread1",threadLocal.get() + "");
    }
}.start();
复制代码

我们看到上面的例子,在主线程中将true放入了ThreadLocal中,之后在子线程试图从中取出,结果发现此时报null。可见,在主线程中存储的数据必须在主线程才可以取出。那么我们再从ThreadLocal内部代码看看为什么会出现这种操作。

public void set(T value) {
	Thread currentThread = Thread.currentThread();
	Values values = values(currentThread);
	if (values == null) {
		values = initializeValues(currentThread);
	}
	values.put(this, value);
}
复制代码

我们看到set()方法中,首先会去获取当前线程currentThread,之后通过values从当前线程中获取数据。判断这个数据是否为空**“if (values == null)” ,为空则调用initializeValues()方法赋初值,否则将获取到的value值put()进入values中 “values.put(this, value)”**。

接着来看get()方法。

public T get() {
        // Optimized for the fast path.
        Thread currentThread = Thread.currentThread();
        Values values = values(currentThread);
        if (values != null) {
            Object[] table = values.table;
            int index = hash & values.mask;
            if (this.reference == table[index]) {
                return (T) table[index + 1];
            }
        } else {
            values = initializeValues(currentThread);
        }

        return (T) values.getAfterMiss(this);
    }
复制代码

从代码中可以看到,get()方法的操作其实和set()差不多,都是先获取当前线程,如果values不为空则将值返回,如果为空则先赋初值然后再返回初始值。由于set()和get()方法都涉及到了从currentThread()中获取数据,这也就解释了为什么在一个线程中存储数据必须要在相同线程中才能取的到的原因。上述只是对ThreadLocal这个类做简单的分析,其实这个类内部还有很多东西,由于篇幅原因再加上ThreadLocal并非这篇文章重点,所以这里我们只是简单叙述,有机会专门写一篇来讲解ThreadLocal。

上面说了在判断ThreadLocal中取出来的数据为空时会去new一个Looper,并把他添加进ThreadLocal中,那我们来看看new出的这个Looper的构造方法。

private Looper(boolean quitAllowed) {
    mQueue = new MessageQueue(quitAllowed);
    mThread = Thread.currentThread();
}
复制代码

构造方法非常简单,里面实例化一个消息队列MessageQueue,并且还会获取当前线程。 也就是说消息队列此时已经和当前线程绑定,其作用的区域为当前实例化Looper的线程 。我们再来看loop()。

public static @Nullable Looper myLooper() {
        return sThreadLocal.get();
}


public static void loop() {
    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;


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

        
        try {
            msg.target.dispatchMessage(msg);
        } finally {
            if (traceTag != 0) {
                Trace.traceEnd(traceTag);
            }
        }

       

        msg.recycleUnchecked();
    }
}

   
复制代码

这里对代码进行了一些删减。可以看到首先会调用myLooper()去获取一个Looper对象。而从myLooper()的源码看到从ThreadLocal里取出在prepare()中存入的Looper对象。先判断对象是否为空,若为空,则抛出异常告诉程序在调用loop()方法之前必须要有一个Looper。 这也就说在使用的时候,prepare()方法必须要在loop()方法之前被调用

之后通过Looper对象获取消息队列MessageQueue,进入一个死循环 for( ; ; ) ,调用MessageQueue的next()方法,不断从消息队列里获取消息Message,如果获取的消息为空,则return跳出循环,如果不为空,则将msg消息交给msg.target.dispatchMessage(msg)去处理,那么这个dispatchMessage()又是什么,其实这个就是handler,不过我们后面再分析。最后调用recycleUnchecked()方法回收。到此Looper源码分析完成。

Handler:

一般使用handler的时候,我们都会先new实例化一个handler对象,那么我们就从handler的构造方法讲起。

public Handler(Callback callback, boolean async) {
    
    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;
}
复制代码

从构造方法我们可以看到,仍然是先调用myLooper()方法,从ThreadLocal中取出Looper对象,之后判断对象是否为空,为空则抛异常,不为空则获取MessageQueue消息队列,这样Handler也就和消息队里进行了绑定。

之后在使用handler的时候一般都会使用sendMessage()方法去发送消息。看看这个方法内部做了什么操作。

public final boolean sendMessage(Message msg)
{
    return sendMessageDelayed(msg, 0);
}


public final boolean sendMessageDelayed(Message msg, long delayMillis)
{
        if (delayMillis < 0) {
            delayMillis = 0;
        }
        return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}


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()里面调用sendMessageDelayed(),sendMessageDelayed()又调用了sendMessageAtTime(),最终发现其实所有的发送消息方法最后都会来到sendMessageAtTime()方法里,于是着重看这个方法。这个方法里先获取消息队列MessageQueue,然后将队列queue、发送的消息msg以及延时时间uptimeMillis一起传入到enqueueMessage()里去。

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
    msg.target = this;
    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
}
复制代码

在这个方法里,又调用queue.enqueueMessage()方法将发射的消息传入到消息队列MessageQueue当中去。 也就是说从handler中发送的消息其实最后全都送到了MessageQueue当中去,而之前在分析loop的时候我们看到,在loop里面又调用了MessageQueue的next()方法,把里面的消息全部交给dispatchMessage去处理。所以最后我们来看看dispatchMessage()方法

public void dispatchMessage(Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}
复制代码

在handler里面我们找到了这个方法。这个方法会根据是否有callback而去调用不同方法,如果有回调则调用handleCallback(),如果没有回调则调用handleMessage();

public void handleMessage(Message msg) {
}
复制代码

我们可以看到handleMessage()中是一个空方法,这就代表只要覆写了这个方法所有的一切就全部由我们自己来写逻辑了。

handler内部整体流程:

我们在使用handler的时候,若在主线程,由于主线程已经有一个Looper了,所以不需要在创建一个Looper**(一个线程中有且仅可以有一个Looper,不然会报错)**。若在子线程,则先调用Looper.prepare()创建一个Looper对象,之后再实例化一个Handler对象,这个Handler会和Looper中的MessageQueue进行绑定,并将sendMessage()发送的消息存储到这个绑定的消息队列当中去。然后我们调用Looper.loop()方法,不断的从消息队列MessageQueue当中取出消息交给dispatchMessage()去处理。dispatchMessage()最后调用handleMessage(),所有的逻辑都交给我们自己去处理。到此,Handler内部原理全部讲解完成。

内存泄露:

在文章开篇我写了一个例子来演示handler如何使用,并且在最后说这么使用会造成内存泄漏。那么现在来讲讲为什么这么写会造成内存泄露。

java 中非静态内部类和匿名内部类都会隐式持有当前类的外部类,由于Handler是非静态内部类所以其持有当前Activity的隐式引用,如果Handler没有被释放,其所持有的外部引用也就是Activity也不可能被释放,当一个对象不需要再使用了,本来该被回收时,而有另外一个正在使用的对象持有它的引用从而导致它不能被回收,这导致本该被回收的对象不能被回收而停留在堆内存中,这就产生了内存泄漏。

解决办法:

方法一:通过逻辑代码处理来进行保护。

我们在关闭Activity的时候停掉你的后台线程。线程停止掉了,就等于切断了handler与外部的连接,那么Activity在退出的时候就会被回收了。

如果你在写一个验证码之类的倒数计时器,用到delay方法去发送消息的时候,我们在销毁Activity的时候,应该在onDestroy()方法里面调用removeCallbacks()将消息移除掉即可。

方法二:将Handler声明为静态类

静态内部类不会持有外部类的对象,并且为了避免在静态内部类中能够使用到Activity对象,这里我们采用弱引用的方式来持有Activity对象。这里顺带说下弱引用(WeakReference),弱引用所持有的对象,不管Java内存是否满了只要调用了GC就一定会被回收掉。所以我们将最早使用的handler代码改造一下。

static class MyHandler extends Handler {
    WeakReference<Activity > mActivityReference;

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

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

可见到我们写了一个静态类来继承Handler,并且在构造方法里面用弱引用持有了传递进来的Activity对象,在handleMessage()方法里面从弱引用中取出activity对象,如果activity对象不为空,则直接进行更新UI的操作。

到此,handler所有的内容讲解完毕!

从源码的角度谈谈面试常客Handler的内部原理

以上所述就是小编给大家介绍的《从源码的角度谈谈面试常客Handler的内部原理》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

征服C指针

征服C指针

前桥和弥 / 吴雅明 / 人民邮电出版社 / 2013-2 / 49.00元

《图灵程序设计丛书:征服C指针》被称为日本最有营养的C参考书。作者是日本著名的“毒舌程序员”,其言辞犀利,观点鲜明,往往能让读者迅速领悟要领。书中结合了作者多年的编程经验和感悟,从C语言指针的概念讲起,通过实验一步一步地为我们解释了指针和数组、内存、数据结构的关系,展现了指针的常见用法,揭示了各种使用技巧。另外,还通过独特的方式教会我们怎样解读C语言那些让人“纠结”的声明语法,如何绕过C指针的陷阱......一起来看看 《征服C指针》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具