Android 要点学习笔记(一)Handler消息机制

一、什么是进程、什么是线程,有和区别?

进程和线程的基本定义是:进程是分配资源的基本单位,线程是独立运行和独立调度的基本单位;
通俗的来讲一个应用程序一般就是一个进程(进程名:默认就是包名),我们访问数据是以进程为单位的(一般情况下进程之间是不允许直接访问到对方的数据,除非使用跨进程间通信)
而线程是cpu执行的基本,一个进程可以包含一个或多个线程

二、为什么主线程不能执行耗时操作?

我们在打开一个应用程序时,AMS的startProcessLocked()方法中启动进程时,会为进程创建好主线程,也就是下图:


1.png

传递的"android.app.ActivityThread"即为我们通俗意义上的“主线程”
进程启动的时候,会通过创建ActivityThread并调用其main(String[] args)方法开启主线程,因此进程和主线程是一 一对应的;
大家都知道主线程是用来处理UI渲染绘制的,同时主线程也负责处理AMS对四大组件的调度分发处理,最终完成执行;
如果在主线程中执行耗时操作,线程里的loop循环会阻塞,导致事件停止分发,最直观的就是界面卡住了,阻塞时间超过5秒直接ANR了;

三、为什么子线程无法更新UI界面?

由于Android的UI控件不是“线程安全”的,如果多线程并发访问,可能导致UI控件出现未知问题
(线程安全是指,多线程编程中线程安全的代码会通过同步机制,保证各个线程都能正确执行)
那么为什么UI控件不加同步机制,让它本省线程安全呢?
加锁会使UI访问逻辑变得很复杂,加锁会降低UI的访问效率,因为加锁会阻塞某些线程的执行

四、简述Handler机制?Handler机制的4个要素?

Handler是跨线程通信机制,因为主线程不能执行耗时操作,需要在子线程中执行耗时操作,而子线程中不能直接对UI进行操作;所以当有耗时操作后的UI界面更新时,需要使用线程和Handler跨线程通信机制更新主线程UI界面;

四个要素:
Message(消息):需要被传递的消息
MessageQueue(消息队列):负责存储管理消息,单链表维护,插入和删除有优势;
Handler(消息处理器):负责发送、处理消息
Looper(消息池):负责关联线程和消息的分发,Looper创建时会创建一个 MessageQueue;
Looper.loop()后创建循环持续从MessageQueue那新的消息传递给Handler处理;

五、 Handler机制的流程?

1、应用程序创建进程时,主线程ActivityThread也被创建
在主线程的main()函数中创建mainLooper并执行Looper.loop();

1.png

2、四大组件里的Handler创建时
会获取主线程的Looper并拿到Looper的MessageQueue

2.png

3、Handler通过sendMessage发送消息,最终调用到queue.enqueueMessage
在消息队列中添加该消息

3.png
------------------------enqueueMessage()源码及分析如下:------------------------
boolean enqueueMessage(Message msg, long when) {
    if (msg.isInUse()) {//消息是否占用
        throw new AndroidRuntimeException(msg + " This message is already in use.");
    } else if (msg.target == null) {//msg.target即Handler,不能为空
        throw new AndroidRuntimeException("Message must have a target.");
    } else {
        synchronized(this) {
            if (this.mQuitting) {//消息队列的持有者Looper已经quit了
                RuntimeException e = new RuntimeException(msg.target + " sending message to a Handler on a dead thread");
                Log.w("MessageQueue", e.getMessage(), e);
                return false;
            } else {
                msg.when = when;//延迟时间赋值给msg
                Message p = this.mMessages;//消息队列
                boolean needWake;
                if (p != null && when != 0L && when >= p.when) {
                    //消息队列不为空,延时不为零,延时大于队列第一个消息的延时时长
                    needWake = this.mBlocked && p.target == null && msg.isAsynchronous();
                    //while循环依次比较延时时长,时长越长,插入的位置越靠后
                    while(true) {
                        Message prev = p;
                        p = p.next;
                        if (p == null || when < p.when) {
                            msg.next = p;
                            prev.next = msg;
                            break;
                        }

                        if (needWake && p.isAsynchronous()) {
                            needWake = false;
                        }
                    }
                } else {
                    //反之就是:消息队列为空 或 延时为零 或延时时长小于队列第一个消息的延时时长
                    msg.next = p;
                    this.mMessages = msg;
                    needWake = this.mBlocked;
                }

                if (needWake) {
                    nativeWake(this.mPtr);
                }

                return true;
            }
        }
    }
}

4、主线程执行了Looper.loop(),开始消息循环不断轮询调用MessageQueue.next()
取得队列中下一个消息Message,并调用Handler的dispatchMessage(msg)传递给Handler

4.png

MessageQueue.next()函数从消息队列取出消息源码分析如下:


Message next() {
    int pendingIdleHandlerCount = -1;
    int nextPollTimeoutMillis = 0;

    while(true) {
        while(true) {
            if (nextPollTimeoutMillis != 0) {
                Binder.flushPendingCommands();
            }

            nativePollOnce(this.mPtr, nextPollTimeoutMillis);
            synchronized(this) {
                long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = this.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) {
                        this.mBlocked = false;
                        if (prevMsg != null) {
                            prevMsg.next = msg.next;
                        } else {
                            this.mMessages = msg.next;
                        }

                        msg.next = null;
                        msg.markInUse();
                        return msg;
                    }

                    nextPollTimeoutMillis = (int)Math.min(msg.when - now, 2147483647L);
                } else {
                    nextPollTimeoutMillis = -1;
                }
                
                if (this.mQuitting) {//Looper.quit(),也就是退出程序了
                    this.dispose();
                    return null;
                }
                
                ………………此处省略………………
                this.mBlocked = true;//msg为空,且没有退出程序,Blocked在next()循环中
            }
        }

………此处省略………
}

5、Handler最终调用重写的handleMessage处理消息

5.png

六、Looper.loop()是死循环,为什么不会造成应用卡死?

这是一个很有意思的问题,经过Handler消息机制的深入学习,我们已经知道:

1、一个程序能正常运行、组件能正常调度、界面能正常刷新,后面是消息机制不停在起作用;
2、Looper.loop()循环和MessageQueue.next()循环,就是为了不断把消息拿出,让消息机制不会停止;
3、当消息队列为空时,只是界面和组件暂时不需要更新,从MessageQueue.next()代码可以看到,消息队列为空循环不会停止,只有循环不停止等到有新的消息入列时才会及时取出来;
4、也就是说,恰恰是Looper.loop()的死循环,才能保证消息的及时发出,才能保持消息机制的正常运行;

通过上面的描述我们其实已经知道了,即应用卡死(即ANR)和主线程阻塞是没什么关系的;
ANR全称Application Not Responding,即应用程序无响应,指的是界面阻塞,即消息队列阻塞;

七、那么在主线程里做耗时操作,会造成ANR呢?

ANR,Application Not Responding即应用程序无响应,是界面阻塞

1、应用程序的界面更新,是由主线程的消息处理机制完成的
2、主线程的消息处理机制,是由Looper.loop()和MessageQueue.next()的死循环不断取出消息,来维持消息机制不断更新的
3、主线程中新增耗时操作,就会把Looper.loop()的循环暂停住,等新增的耗时操作完成后Looper.loop()的循环才会继续走下去
4、loop循环停止一段时间,相当于消息机制就停止了一段时间,这段时间内无法响应用户操作,界面UI无法更新
5、当这个时间超过5s,即引发ANR

八、一个线程能否有多个Looper,能否有多个Handler,Handler和Looper之间关系?

1、一个线程只能有一个Looper,并且只有一个MessageQueue被Looper持有
2、一个线程可以有多个Handler,Handler发送的Message会被标记上Target

Handler和Looper是多对一的关系,各个Handler发送的Message会被标记上Target,根据延时长短被先后放入到一个MessageQueue中,在Looper.loop()时依次取出msg,根据msg的target发送回给指定的Handler处理

九、在子线程直接new Handler可以么?需要怎么做?

不可以,看源码可以知道

直接new Handler,会报RuntimeException
"Can't create handler inside thread that has not called Looper.prepare()"
就是,无法在没有 Looper.prepare()之前就在线程中创建Handler

public class Handler {
    
    public Handler() {
        this((Handler.Callback)null, false);
    }
    
    public Handler(Handler.Callback callback, boolean async) {
        this.mLooper = Looper.myLooper();
        if (this.mLooper == null) {
            throw new RuntimeException("Can't create handler inside thread that has not called Looper.prepare()");
        } else {
            this.mQueue = this.mLooper.mQueue;
            this.mCallback = callback;
            this.mAsynchronous = async;
        }
    }
    …………
}

在四大组件中可以直接new Handler是因为,主线程ActivityThread的main()函数中会自动创建Looper对象无需我们自己管理;
在子线程中,正确的方法是:
先Looper.prepare()
然后new Handler()
最后Looper.loop()让消息队列运行起来
不过我们不建议子线程中再加Handler
因为Looper.loop()直接子线程就阻塞了,无法再做其他的耗时操作


6.png

十、Message要如何创建?哪种创建方法最好?

有三种方式:
第一种是新建了实例
后两种方式比较好,直接复用了消息池中已有的Message实例

Message msg1 = new Message(); 
Message msg2 = Message.obtain(); 
Message msg3 = handler.obtainMessage();

十一、Handler的PostDelay()方法使用后,消息队列是如何处理的?

查看源码:
postDelayed()直接调用的sendMessageDelayed()
并通过getPostMessage(Runnable r)获取Message实例
最后还是调用到MessageQueue.enqueueMessage()函数将消息添加到消息链表


public final boolean postDelayed(Runnable r, long delayMillis) {
    return this.sendMessageDelayed(getPostMessage(r), delayMillis);
}

private static Message getPostMessage(Runnable r) {
    Message m = Message.obtain();
    m.callback = r;
    return m;
}

public final boolean sendMessageDelayed(Message msg, long delayMillis) {
    if (delayMillis < 0L) {
        delayMillis = 0L;
    }

    return this.sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}

public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
    MessageQueue queue = this.mQueue;
    if (queue == null) {
        RuntimeException e = new RuntimeException(this + " sendMessageAtTime() called with no mQueue");
        Log.w("Looper", e.getMessage(), e);
        return false;
    } else {
        return this.enqueueMessage(queue, msg, uptimeMillis);
    }
}

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

    return queue.enqueueMessage(msg, uptimeMillis);
}

十二、Handler使用时应注意哪些问题?有什么解决方法?

Handler在使用时很容易遇到内存泄漏问题

在发送消息是无论调用的是handler.sendMessage或handler.sendMessageDelayed最终都会调用到
enqueueMessage()将msg添加到MessageQueue中,如下图源码:
发送出去的msg都会持有handler实例
(这是因为消息执行时,也会根据msg持有的handler实例将msg发回给对应的Handler执行)


package android.os;

import android.os.IMessenger.Stub;
import android.util.Log;
import android.util.Printer;

public class Handler {
    ………… 
    private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
        msg.target = this;//msg将会持有Handler实例
        if (this.mAsynchronous) {
            msg.setAsynchronous(true);
        }
    
        return queue.enqueueMessage(msg, uptimeMillis);
    }
}

所以Handler在使用时遇到内存泄漏问题的原因是:
handler发送的延时消息会持有handler实例,而handler我们很多时候是使用的匿名内部类,
java的特性匿名内部类会隐式持有外部类对象的引用
也就是说,如果在延时过程中Activity等组件退出了,msg持有handler,handler会持有activity,
这就造成了activity的内存无法回首,造成activity内存泄漏

解决办法:
Handler声明时使用静态内部类,持有外部类的若应用

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

推荐阅读更多精彩内容