Handler Message源码分析

Handler用于异步消息处理:
当发出一个消息之后,首先进入一个消息队列,发送消息的函数同步返回,而另一个部分在消息队列中逐一将消息取出,然后对消息进行处理。

1、Handler内存泄漏问题
2、在子线程创建Handler报错Looper没有prepare?
3、textview.setText()只能在主线程执行?有点问题
4、new Handler()的两种写法
5、ThreadLocal用法和原理

1、Handler引起的内存泄漏问题

如下代码,当在子线程中休眠或做了耗时操作后,再用Handler发送消息。此时如果Activity已经destroy了,但是Handler仍然会发送消息到消息队列里,产生了严重的内存泄漏。

        new Thread(new Runnable() {
            @Override
            public void run() {
                //内存泄漏问题
                Message message = new Message();
                message.obj = "胡军";
                message.what = 2;
                SystemClock.sleep(3000);
                handler1.sendMessage(message);
            }
        }).start();

如何解决:
(1)用handler1.sendMessageDelayed(message,3000);发送延时消息。
在Activity被消耗后,将消息移除handler1.removeMessages(2);

(2)在Activity销毁后,将Handler置空,这样就不会在延时结束后调用sendMessage了。

2、为什么不能在子线程创建Handler

会报错:Can't create handler inside thread that has not called Looper.prepare();

这里看看Handler的构造方法源码:
public Handler(){}
public Handler(Callback callback){}

mLooper = Looper.myLooper();
    public static @Nullable Looper myLooper() {
        return sThreadLocal.get();
    }
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
...
}

也就是默认Looper是从当前Thread里获取Looper。
但是在新生成的子线程里,并没有生成Looper。这样就会报错。

        mLooper = Looper.myLooper();
        if (mLooper == null) {
            throw new RuntimeException(
                "Can't create handler inside thread " + Thread.currentThread()
                        + " that has not called Looper.prepare()");
        }

在子线程里,需要先prepare,在当前thread写入相应Looper到ThreadLocal里。

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

而在主线程里,已经默认生成了一个Looper.
ActivityThread.java里的main()方法中:

        Looper.prepareMainLooper();

而Looper里面有个static的变量sMainLooper用来存储主线程的Looper,任何时候都能获取到该主线程Looper。
看Looper.prepareMainLooper();都做了什么:
首先调用了prapare方法,生成了一个Looper放入ThreadLocal里,这是用来存储和取出和线程相关的变量的,之后会进行详细说明。这里将生成的主线程Looper放入ThreadLocal后,然后利用myLooper()方法,即从当前线程取出Looper,主线程里就是主线程Looper,然后赋值给sMainLooper。

        prepare(false);
        synchronized (Looper.class) {
            if (sMainLooper != null) {
                throw new IllegalStateException("The main Looper has already been prepared.");
            }
            sMainLooper = myLooper();
        }
    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));
    }
    public static @Nullable Looper myLooper() {
        return sThreadLocal.get();
    }

3、textview.setText()只能在主线程执行?有点问题

如下,在onCreate()里调用,生成的子线程里setText()是没有报错,且执行成功的了。为什么呢?

        new Thread(new Runnable() {
            @Override
            public void run() {
                Log.d(TAG, "run: "+Thread.currentThread().getName());
                textView.setText("胡军");
            }
        }).start();

分析下setText()的源码:
setText() --> checkForRelayout() -->requestLayout() --> mParent.requestLayout();
而这个mParent.requestLayout();调用的是父类ViewParent的requestLayout()方法。

接口ViewParent的实现类ViewRootImpl,里面有requestLayout()的实现:
requestLayout() --> checkThread()

    void checkThread() {
        if (mThread != Thread.currentThread()) {
            throw new CalledFromWrongThreadException(
                    "Only the original thread that created a view hierarchy can touch its views.");
        }
    }

这里,mThread是在ViewParent初始化时设置的

        mThread = Thread.currentThread();

所以,"Only the original thread that created a view hierarchy can touch its views."错误并不是指必须在UI主线程调用setText(),而是需要在创建ViewParent的线程里调用setText()。
我们使用的ViewParent都是在主线程调用的,所以setText()就需要在主线程中调用。

当setText()足够快,在检查线程前就更新完成,则不会报错。

4、new Handler()的两种写法

    //Handler有下面两种构造方式
    private Handler handler1 = new Handler(new Handler.Callback() {
        @Override
        public boolean handleMessage(@NonNull Message msg) {
            return false;
        }
    });

    private Handler handler2 = new Handler() {
        @Override
        public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);
        }
    };

看源码:
在启动一个Thread后,会生成一个Looper循环。
比如主线程里,有

Looper.prepareMainLooper();
...
Looper.loop();

在loop()方法里,启动了一个无限轮训的获取Message队列的循环。

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

当取到Message后,会调用

msg.target.dispatchMessage(msg);

这里,Target就是发送这个Message的Handler,在调用Handler发送Message时,会将Message的Target设置为发送的Handler。

    /**
     * Handle system messages here.
     */
    public void dispatchMessage(@NonNull Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }

首先会回调msg自己的Callback,没有就回调初始化Handler时给的Callback,再不行才回调重写的Handler的handleMessage()方法。

其中,msg自己的Callback,是调用Handler的post方法时,设置给Message的。就完成了Handler.post(runnable)里的run回调。
这里有个知识点,调用Activity.runOnUIThread(),其实内部就是用主线程的Handler.post()方法实现的。

5、ThreadLocal用法和原理

作用是把参数存储在线程相关的map里,在不同的线程里存储,当在相应的线程里能读取出当前线程存储的参数。

下面的例子里,在ThreadLocal里存储String,则在不同的线程里存储不同的String,在不同的线程里,就能读取出这个线程存储的String。

        val threadLocal = object : ThreadLocal<String>() {
            override fun initialValue(): String? {
                return "默认值"
            }
        }
        threadLocal.set("胡军")
        println("当前Thread:${Thread.currentThread().name},get=${threadLocal.get()}")

        Thread(Runnable {
            println("当前Thread:${Thread.currentThread().name},get=${threadLocal.get()}")
            //当使用完成后,建议remove掉。否则不用的线程越来越多,占用内存越来越多
            threadLocal.remove()
        },"子线程1").start()

        Thread(Runnable {
            threadLocal.set("胡军2")
            println("当前Thread:${Thread.currentThread().name},get=${threadLocal.get()}")

            //当使用完成后,建议remove掉。否则不用的线程越来越多,占用内存越来越多
            threadLocal.remove()
        },"子线程2").start()

ThreadLocal源码分析

    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

在Looper里,保存了一个ThreadLocal和主线程的Looper,并且都是全局唯一静态的。Looper就是用ThreadLocal来保存不同线程里的Looper的。

    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
    private static Looper sMainLooper;  // guarded by Looper.class

Handler+Message的原理分析

Handler+Message原理图

1、首先看主线程:
应用启动时,在主线程ActivityThread.class里找到main()方法
ActivityThread.main() --> Looper.prepareMainLooper() --> Looper.prepare() --> Looper.sThreadLocal.set(new Looper(quitAllowed)); --> Looper.sMainLooper = myLooper();

这里就完成了主线程的Looper的生成。之后在主线程里都是调用
Looper.sMainLooper作为主线程Looper。

2、看Looper代码
构造方法:

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

初始化Looper的消息队列mQueue,以及赋值给该Looper运行的线程mThread.
由于主线程Looper只有一个,所以整个主线程只有一个消息队列mQueue。

3、看Handler代码
构造方法:

Handler(){}
Handler(Callback callback){}
Handler(Looper looper){}
Handler(boolean async){}
Handler(Callback callback, boolean async){}
Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async){}

有一堆构造方法,如果不指定Looper,则会从Looper.myLooper()获取。
所以当当前Thread并没有Looper时,则找不到Looper,会报错。只有在当前Thread里调用了Looper.prerare()才可以找到Looper。

发送消息(存储消息):
Handler有很多个发送消息的方法,

sendMessage(@NonNull Message msg)
sendMessageDelayed(@NonNull Message msg, long delayMillis)
sendEmptyMessage(int what)

//post方法,将Runnable赋予Message
post(@NonNull Runnable r)
postDelayed(@NonNull Runnable r, long delayMillis)

实际上,上面所有的发送方法,最后都调用了一个方法:

    private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
            long uptimeMillis) {
        msg.target = this;
        msg.workSourceUid = ThreadLocalWorkSource.getUid();

        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
    }

最后调用了MessageQueue的enqueueMessage()方法。

boolean enqueueMessage(Message msg, long when) {

//如果是第一条消息,赋值给MessageQueue里的变量mMessages
mMessages = msg;

//如果不是第一条,或者需要插入之前的消息前面,
//利用message的next来形成链式的排列。

}

取出消息(消费)
调用Looper.loop()方法后,就开始不断轮训消息队列。
其中,主线程的Looper在ActivityThread里main()方法中,

main(){
...
        Looper.prepareMainLooper();
...
        Looper.loop();
}

分析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.");
        }
//拿到Looper里的MessageQueue
        final MessageQueue queue = me.mQueue;

...
//开始不断轮训MessageQueue
        for (;;) {
//首先拿到Queue里的Message
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }
...
try {
                msg.target.dispatchMessage(msg);
...}
}
}

上面分析得到,所有的消息最后在handleMessage或在Callback的run方法里执行,而执行的线程就是loop()方法被调用时处于的线程。
主程序里调用了loop()方法,所有的主线程消息都在主线程中运行了。

问题
loop()启动了一个无限死循环,如何避免导致anr呢?
里面有挺好的垃圾回收机制,开始前调用了Binder.clearCallingIdentity();
循环中调用了

            if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
                Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
            }

一旦需要等待时,或还没有执行到执行的时候,会调用NDK里面的JNI方法,释放当前时间片,这样就不会引发ANR异常了。

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