目录
扩展:下图是在手机桌面点击应用图标,内部都发生了什么?涉及哪些通讯方式?如何打开了activity?
二、Handler引起的内存泄漏,最终是谁持有了Acitivity?
五、子线程中维护的Looper,消息队列中无消息时的处理方案是什么?有什么用?主线程呢?
六、Handler如何发送延迟消息的?延迟的原理是什么?是不是阻塞的?
八、Handler没有消息处理是阻塞的还是非阻塞的?为什么不会有ANR产生?
首先看下下面的面试题,根据问题进行源码分析。
扩展:下图是在手机桌面点击应用图标,内部都发生了什么?涉及哪些通讯方式?如何打开了activity?
1、在Launhaer界面点击按钮时,它会通过startActivity去打开Activity,但是此时没有Activity怎么办呢?通过AMS检测这些Activity的状态,发现此时Activity状态为不存在,那么,它会发送创建进程的请求到Zygote进程。Zygote进程就会去fork一个新的APP进程,新的进程是由AMS来进行管理。AMS交给自己的代理ActivityManagerProxy,这个代理首先会发送attach Application,因为首先要先启动Application才能启Activity,此时又回到了AMS里面进行进程间通讯,在AMS里面发送一个消息realStartActivityLocked,这个消息会启动ApplicationThreadProxy,之后ApplicationThreadProxy会发送scheduleLaunchaActivity消息去启动ApplicationThraed,之后发送消息7、8最终启动了Activity.onCrate。
2、此流程涉及了三种通讯,红色的Binder通讯和粉丝的Socket通讯以及绿色的Handler通讯。
3、为什么会有Socket通讯呢?因为此时进程还没有启动,Zygote进程属于C++层即流程图中2、3属于Linux中的流程。
4、红色属于跨进程间的通讯,所有跨进程间的通讯一定是Binder通讯!!!!!只要是跨进程通讯,其底层原理一定是Binder通讯。比如Activity之间的通讯,广播怎么传播的,service的底层原理等都是Binder通讯。
5、App进程内部的通讯都是通过Handler通讯。下面我们主要就是分析Handler通讯。
基础:
handler作用:
1)传递消息Message
//传递的数据
Bundle bundle = new Bundle();
bundle.putString("msg", "传递我这个消息");
//发送数据
Message message = Message.obtain();
message.setData(bundle); //message.obj=bundle 传值也行
message.what = 0x11;
handler.sendMessage(message);
//数据的接收
final Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (msg.what == 0x11) {
Bundle bundle = msg.getData();
String date = bundle.getString("msg");
}
}
};
2)子线程通知主线程更新ui
//创建handler
final Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (msg.what == 0x11) {
//更新ui
......
}
}
};
new Thread(new Runnable() {
@Override
public void run() {
//FIXME 这里直接更新ui是不行的
//还有其他更新ui方式,runOnUiThread()等
message.what = 0x11;
handler.sendMessage(message);
}
}).start();
常用api
//消息
Message message = Message.obtain();
//发送消息
new Handler().sendMessage(message);
//延时1s发送消息
new Handler().sendMessageDelayed(message, 1000);
//发送带标记的消息(内部创建了message,并设置msg.what = 0x1)
new Handler().sendEmptyMessage(0x1);
//延时1s发送带标记的消息
new Handler().sendEmptyMessageDelayed(0x1, 1000);
//延时1秒发送消息(第二个参数为:相对系统开机时间的绝对时间,而SystemClock.uptimeMillis()是当前开机时间)
new Handler().sendMessageAtTime(message, SystemClock.uptimeMillis() + 1000);
//避免内存泄露的方法:
//移除标记为0x1的消息
new Handler().removeMessages(0x1);
//移除回调的消息
new Handler().removeCallbacks(Runnable);
//移除回调和所有message
new Handler().removeCallbacksAndMessages(null);
根据问题进行源码分析
一、线程切换的原理?
答:线程切换是由Handler来实现的。因为主线程是咱们的显示线程,所有的子线程和主线程的通讯都是通过Handler完成的。
举个例子:Retrofit可以完成切换线程,它可以自动切换到主线程中,OK则不可以,是因为Retrofit的源码中有通过Handler向主线程发送消息,将子线程切换到了主线程,所以Retrofit不可用于java后台的使用,因为它和android是强关联的。
如下图所有,通过上文的常用API发的消息,最后都会指向sendMessageAtTime();方法,最终都会调用最核心的方法MessageQueue.enqueuMessage()方法。这个方法将msg消息加入消息队列。此时为消息入队列。
此时我们重点分析一下这个方法:
当前出入的消息为msg时,首先判断mMessages消息队列中的第一个消息mMessages.prev,要是当前队列中消息为空,或者msg立即发送,则将该消息插到消息队列mMessages的头部;反之,则会在一个for的死循环中遍历消息队列并将传入消息msg插到单链表中合适的位置。事实上,消息队列是按照消息处理的时间when,按照从近到远的顺序排列的,最先要执行的任务放在消息队列的头部,依次排列。
/**
* 发消息最核心的方法;将消息msg按照延迟时间when顺序加入队列。
* 队列中的Message触发时间是有先后顺序的。当消息加入消息队列时,会从队列头开始遍历,直到找到消息应该插入的合适位置,
* 以保证所有消息的时间顺序(内部遍历队列中Message,找到when比当前Message的when大的Message,将Message插入到该Message之前,如果没找到则将Message插入到队列最后)。
* 一般是当前队列为空的情况下,next那边会进入睡眠,需要的时候MessageQueue这边会唤醒next方法。
* @param msg
* @param when 代表:实际执行的时间 = SystemClock.uptimeMillis() + delayMillis(即现在的时间+需要延迟的时间)
* @return
*/
boolean enqueueMessage(Message msg, long when) {
// 每一个普通Message必须有一个target-handler
if (msg.target == null) {
throw new IllegalArgumentException("Message must have a target.");
}
//此处加锁目的:为了消息进队列时能一个一个的进,进完一个,释放锁,再进下一个消息
synchronized (this) {
//已在使用状态(检查信息是否有使用过)
if (msg.isInUse()) {
throw new IllegalStateException(msg + " This message is already in use.");
}
//消息在退出状态->回收msg,被回收到消息池
if (mQuitting) {
IllegalStateException e = new IllegalStateException(
msg.target + " sending message to a Handler on a dead thread");
Log.w(TAG, e.getMessage(), e);
msg.recycle();
return false;
}
//标记使用状态,记录执行时间
msg.markInUse();
msg.when = when;
Message p = mMessages;
boolean needWake;
//p为null(代表MessageQueue没有消息) 或者msg的触发时间是队列中最早的, 则进入该该分支
//when什么时候会等于0,在调用sendMessageAtFrontOfQueue时
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 {
//将消息按时间顺序插入到MessageQueue。一般地,不需要唤醒事件队列,除非
//消息队头存在阻塞,并且同时Message是队列中最早的异步消息
needWake = mBlocked && p.target == null && msg.isAsynchronous();
Message prev;
for (;;) {
prev = p;
p = p.next;
//一直循环到没有消息(p==null)或者循环到当前延迟的时间小于下一个消息的时间。停止循环
if (p == null || when < p.when) {
break;
}
if (needWake && p.isAsynchronous()) {
needWake = false;
}
}
//将msg插入到此队列循环出来的位置的下一个位置
msg.next = p; // invariant: p == prev.next
prev.next = msg;
}
//去唤醒消息队列所在的线程
if (needWake) {
nativeWake(mPtr);
}
}
return true;
}
以上是消息入队列,那么消息又是如何出队列呢?
Handler中sendMessageDelayed方法只是将消息按照要执行的先后顺序插入到消息队列中的,插入好了并不意味着就会按照设定的延时时间处理消息,那Handler时如何延时处理该消息的呢?
消息出队列,是通过类Looper的loop方法,此方法里面有个死循环,此死循环通过Message msg=queue.next()从队列类MessageQueue中取消息,取出消息后调用msg.target.dispatchMessage(msg)方法,这个方法中调用了handlerMessage(),之后将在主线程(调用线程,也可能是子线程)中更新UI或者处理消息。
首先我们知道,Looper.loop()之后,线程就进入了消息监听的阶段:此时我们重点关注Looper类,此类中的loop方法:循环取出messagequeue消息队列中的消息,并分发出去。再把分发后的Message回收到消息池,以便重复利用。
public static void loop() {
final Looper me = myLooper(); //从存储区拿出looper
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
final MessageQueue queue = me.mQueue; //获取Looper对象中的消息队列
......
//进入loop的主循环方法
for (;;) {
Message msg = queue.next(); //一直出入阻塞状态,知道取到消息
if (msg == null) { //没有消息,则退出循环
return;
}
......
//target是handler,此处用于分发Message
msg.target.dispatchMessage(msg);
......
msg.recycleUnchecked(); //将Message放入消息池
}
}
当Handler中没有可用消息的时候,上面代码会一直阻塞在queue.next()的地方,直到消息返回,才会调用dispatchMessage进行消息的处理,要是返回的msg为空,那么Handler就会结束消息监听,不再监听任何消息。
通过Message msg=queue.next()从队列类MessageQueue中取消息,如何取得呢
可以看到这里也是一个for(;;)循环遍历队列,核心变量就是nextPollTimeoutMillis。可以看到,计算出nextPollTimeoutMillis后就调用nativiePollOnce这个native方法。这里的话大概可以猜到他的运行机制,因为他是根据执行时间进行排序的,那传入的这个nextPollTimeoutMillis应该就是休眠时间,类似于java的sleep(time)。休眠到下一次message的时候就执行。那如果我在这段时间又插入了一个新的message怎么办,所以handler每次插入message都会唤醒线程,重新计算插入后,再走一次这个休眠流程。
//不停提取下一条message
Message next() {
final long ptr = mPtr;
//判断是否退出消息循环
if (ptr == 0) {
return null;
}
int pendingIdleHandlerCount = -1;
//代表下一个消息到来前,还需要等待的时长
int nextPollTimeoutMillis = 0;
for (;;) {
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}
//native层阻塞cpu。如果被阻塞,唤醒事件队列
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
........
//获取到了非异步消息
if (msg != null) {
//任务执行时间大于现在的时间
if (now < msg.when) {
//设置下一次轮询的超时时长
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
mBlocked = false;//指定为非阻塞任务
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
//设置消息的使用状态,即flags |= FLAG_IN_USE
msg.markInUse();
return msg; //成功地获取MessageQueue中的下一条即将要执行的消息
}
} else {
//表示消息队列中无消息,会一直等待下去
nextPollTimeoutMillis = -1;
}
......
}
上面是阻塞的从消息队列中获取可用消息的过程。其中nativePollOnce方法是一个native方法,其内部会根据传入的nextPollTimeoutMillis,在延迟这么长时间之后唤醒线程从消息队列中读取消息,内部调用的是epoll_wait方法。
我们知道当线程中没有新消息要处理的时候,线程处于休眠状态,当其他线程向Handler的消息队列中写入消息,这一动作并不会唤醒当前线程处理该消息,还需要向线程的eventfd中写入数据,从而唤醒休眠的线程开始处理数据,此处也是一样的,nativePollOnce函数内部会调用epoll_wait方法,设置超时时间为nextPollTimeoutMillis,epoll_wait在这个超时时间之后,就会唤醒线程,开始处理消息队列中的消息。当队列中没有消息时nextPollTimeoutMillis置为-1.此时nativePollOnce处于无限等待状态,直到调用quit()方法退出线程,或者收到新的消息。
next方法中,每次会从消息队列mMessages中获取链表中头部的消息,要是头部消息的设定执行的时间要比当前时间大,说明消息队列中所有的消息都还没有到可执行的时间,这是因为消息队列中消息在插入消息队列的时候,按照执行时间的先后顺序已经排序好了。这种情况下,会计算出一个等待时间,传递到nativePollOnce函数中,让native层在这个等待时间之后再唤醒线程读取消息队
nativiePollOnce这个native方法可以通过名字知道,他用的是linux中的epoll机制,具体是调用了epoll_wait这个方法。
int epoll_wait(int epfd, struct epoll_event * events, intmaxevents, int timeout);
这个epoll和select一样都是linux的一个I/O多路复用机制,主要原理就不深入了,这里大概了解一下I/O多路复用机制和它与Select的区别就行。
Linux里的I/O多路复用机制:举个例子就是我们钓鱼的时候,为了保证可以最短的时间钓到最多的鱼,我们同一时间摆放多个鱼竿,同时钓鱼。然后哪个鱼竿有鱼儿咬钩了,我们就把哪个鱼竿上面的鱼钓起来。这里就是把这些全部message放到这个机制里面,那个time到了,就执行那个message。
epoll与select的区别:epoll获取事件的时候采用空间换时间的方式,类似与事件驱动,有哪个事件要执行,就通知epoll,所以获取的时间复杂度是O(1),select的话则是只知道有事件发生了,要通过O(n)的事件去轮询找到这个事件。
dispatchMessage将消息分发给接口Handler.Callback的方法handleMessage(),然后由主线程的去更新UI或者处理消息。
/**
* 此处处理发送过来的消息,并将消息传给handleMessage
*/
public void dispatchMessage(@NonNull Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
通过上述流程,我们可以看出Handler的工作流程如下图:
其中关联子线程和主线程的主要类就是MessageQueue,从整个流程可以看出:
子线程发消息:handler.sendMessage- - - >MessageQueue.enqueuMessage()----->然后存到MessageQueue。
主线程:取消息:activityThread.main-----> Looper.loop----->MessageQueue.next()------>handlerMessage().
二、Handler引起的内存泄漏,最终是谁持有了Acitivity?
答:activityThread.
匿名内部类Handler持有外部类activity的引用,但是这个引用最终传给了谁呢?
通过核心方法MessageQueue.enqueuMessage()中将handler的引用赋值给了msg.target。此时Message持有了handler的引用。msg将值传给了queue.enqueueMessage,此时MessageQueue类持有了Msg,MessageQueue的引用mQueue是在Looper中创建的。所以此时Looper持有了msg,Looper是在main方法创建的。即在activityThread中一直运行着looper.loop()。所以当时在调用位置以匿名内部类的形式创建handler实例时,匿名内部类持有外部类的引用,所以此时handler持有activity,然后赋值给了msg.target,最终由activityThread持有,如果msg一直存在,那么activityThread就一直持有此activity的引用,造成此acativity一直不能被释放,造成内存泄漏。所以将handler置为static就可以了,因为静态内部类不持有外部类的引用了。
通过源码分析上述描述:
1、分析activityThread:
主线程中Looper,MessageQueue,Handler三者关联的思路:
主线程-->prepareMainLooper()(内部调用prepare() ,去实例化Looper,Looper实例化同时创建了messagequeue,一一对应关系)-->主线程中的handler获取当前线程的Looper
子线程-->直接通过Looper.prepare()去实例化Looper,Looper实例化同时创建了messagequeue(一一对应关系) -->实例化Handler同时获取当前子线程的Looper
在Handler类中,有上图可知,发送消息最后都会指向Handler中的enqueueMessage方法,此方法将handler的引用(在activity中创建匿名类Handler时,将activity的引用传给了handler)赋值给了msg,
msg将值传给了queue.enqueueMessage,此时MessageQueue类持有了Msg。
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
long uptimeMillis) {
//将Handler的引用this赋值给了msg
msg.target = this;
msg.workSourceUid = ThreadLocalWorkSource.getUid();
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}
从下图可以看出MessageQueue的引用mQueue是在Looper中创建的。所以此时Looper持有了msg,而Looper的引用又是谁创建的呢?
/*
*在创建Looper实例的时候创建MessageQueue的引用
*/
private Looper(boolean quitAllowed) {
//创建MessageQueue的引用
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}
/**
*在从初始化Handler时候从looper类中获取MessageQueue的引用
*/
public Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async) {
mLooper = looper;
//从looper类中获取MessageQueue的引用
mQueue = looper.mQueue;
mCallback = callback;
mAsynchronous = async;
}
Looper是在main方法创建的。即在activityThread中一直运行着looper.loop()。
ActivityThread中的main函数如下:
public static void main(String[] args) {
......
//启动Looper
Looper.prepareMainLooper();
......
//区别:子线程是创建handler;
//主线程是通过getHandler()获取内部类实例
if(sMainThreadHandler==null){
sMainThreadHandler=thread.getHandler();
}
......
//开始循环loop
Looper.loop();
}
三、looper什么时候启动循环的?
答:APP中的每一个点击、界面刷新等所有操作都是基于消息的,所以message是APP的心脏,一直处于跳动状态,即looper.loop()一直处于运行循环中,从上图可知,在应用已启动时ActivityThread就会触发main函数,此时就开始运行looper.loop()。
四、handlerThread的原理?
主要考察的是并发编程的知识,多线程、锁等知识点。(异步存在形式有thread,handlerThead,asyncTask,线程池,intentService)
handlerThread的基本使用:
public class MainActivity extends AppCompatActivity {
private HandlerThread thread;
static Handler mHandler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//创建一个HandlerThread并启动它
thread = new HandlerThread("MyHandlerThread");
thread.start();
//使用HandlerThread的looper对象创建Handler
mHandler = new Handler(thread.getLooper(), new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
//这个方法是运行在 handler-thread 线程中的,可以执行耗时操作,因此不能更新ui,要注意
if (msg.what == 0x1) {
try {
Thread.sleep(3000);
Log.e("测试: ", "执行了3s的耗时操作");
} catch (InterruptedException e) {
e.printStackTrace();
}
//这个方法是运行在 handler-thread 线程中的,可以执行耗时操作,因此不能更新ui,要注意
// ((Button) MainActivity.this.findViewById(R.id.button)).setText("hello");
}
return false;
}
});
//停止handlerthread接收事件
findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
thread.quit();
}
});
//运行
mHandler.sendEmptyMessage(0x1);
}
首先handlerThread也是一个子线程,继承thread,不过内部比普通线程多了一个Looper,因为存在looper才能存在handler,不然handler的存在就没有意义了。
通过源码分析:在调用start()方法时就会启动run方法,此时开始启动Looper,之后使用HandlerThread的looper对象创建Handler,如果此时还没有启动完成Looper(此流程在子线程),那么getLooper就会获取为null,所以此处添加锁机制,使用的内置锁,这个锁是的开锁和关锁都是在JVM中由JVM自动完成的,所以叫做内置锁。
@Override
public void run() {
mTid = Process.myTid();
Looper.prepare();
//此时是在子线程中初始化Looper,而其他获取looper引用的位置可能在其他线程(主线程),所以此处加锁是等初始化完成后再由其他位置调用调用。
synchronized (this) {
// notifyAll();
mLooper = Looper.myLooper();
//notifyAll含义是锁所在的代码块执行完毕后再释放所有被加锁的位置。所以notifyAll放在上面也没问题
notifyAll();
}
Process.setThreadPriority(mPriority);
onLooperPrepared();
Looper.loop();
mTid = -1;
}
/**
* 供调用者获取Looper的引用mLooper
*/
public Looper getLooper() {
if (!isAlive()) {
return null;
}
// 调用者获取ooperL,加锁来判断是否mLooper存在,如果不存在等待mLooper引用的创建,创建成功后被notifyAll唤醒。
synchronized (this) {
while (isAlive() && mLooper == null) {
try {
wait();
} catch (InterruptedException e) {
}
}
}
return mLooper;
}
几个地方要注意:
a.handleMessage()可以做耗时操作,但是不能更新ui,因为此时的handler处于子线程
b.如果不手动的调用HandlerThread.quit()或者HandlerThread..quitSafely()方法,HandlerThread会将持续的接收新的任务事件。
c.只有handleMessage()方法执行完,这轮的任务才算完成,HandlerThread才会去执行下一个任务。而且在此次执行时,即使手动的去调用quit()方法,HandlerThread的此次任务也不会停止。但是,会停止下轮任务的接收。
举例:
//耗时任务换成这个,点击按钮执行quit()方法,发现此次任务依旧执行
for (int i = 0; i < 99999999; i++) {
Log.e("测试: ", "输出" +i);
}
d.即使多次执行mHandler.sendEmptyMessage(0x1),任务队列中的任务依然只能一个一个的被处理。上一任务结束,开始执行下一个。
五、子线程中维护的Looper,消息队列中无消息时的处理方案是什么?有什么用?主线程呢?
答:让线程退出。
如果线程不退出,Looper.loop是一个死循环,就会一直执行,此时子线程一直占用资源,就会引起内存泄漏。
让线程停止接收事件的2种方法。(上文中的handlerThread也适用)
第一个就是quit(),实际上执行了MessageQueue中的removeAllMessagesLocked方法,该方法的作用是把MessageQueue消息池中所有的消息全部清空,无论是延迟消息(带Delayed的)还是非延迟消息。
第二个就是quitSafely(),执行了MessageQueue中的removeAllFutureMessagesLocked方法,该方法只会清空MessageQueue消息池中所有的延迟消息,并将消息池中所有的非延迟消息派发出去让Handler去处理,quitSafely相比于quit方法安全之处在于清空消息之前会派发所有的非延迟消息。
void quit(boolean safe) {
if (!mQuitAllowed) {
throw new IllegalStateException("Main thread not allowed to quit.");
}
synchronized (this) {
if (mQuitting) {
return;
}
mQuitting = true;
if (safe) {
removeAllFutureMessagesLocked();
} else {
removeAllMessagesLocked();
}
// We can assume mPtr != 0 because mQuitting was previously false.
nativeWake(mPtr);
}
}
六、Handler如何发送延迟消息的?延迟的原理是什么?是不是阻塞的?
我们从上文分析MessageQueue的enqueuMessage(Message msg, long when)方法时可知,传入的when即为:实际执行的时间 = SystemClock.uptimeMillis() + delayMillis(即现在的时间+需要延迟的时间)。通过for(;;)遍历消息队列,一直循环到没有消息(p==null)或者循环到当前延迟的时间小于下一个消息的时间时,将msg插入到此位置的下一个位置。
所以就是根据实际时间将msg插入单链表的队列中,nativePollOnce函数内部会调用epoll_wait方法,设置超时时间为nextPollTimeoutMillis,epoll_wait在这个超时时间之后,就会唤醒线程,开始处理消息队列中的消息。所以其延迟的原理就是由nativePollOnce函数根据时间顺序去处理不同的消息,达到nextPollTimeoutMillis就处理消息,其他时间就休眠,通过next()使其处于阻塞状态。
七、我们使用Message应该如何创建它?
//2种创建消息方法
//1.通过handler实例获取
Handler handler = new Handler();
Message message=handler.obtainMessage();
//2.通过Message获取
Message message=Message.obtain();
//源码中第一种获取方式其实也是内部调用了第二种:
public final Message obtainMessage(){
return Message.obtain(this);
}
Message obtain()时Message的复用机制(享元模式)(比如RecycleView的bindView)其目的为了减少资源浪费。
假设创建Message都通过new message,由于App中所有操作都是通过消息处理的,那么很多的message占用大量内存,而且在他们不用的时候也不会立即释放回收,而是等待GC,那么由于message占用大量内存,就会频繁的触发GC,频繁的释放掉,GC的时候就会造成卡顿,回收各种msg,所以使用复用,而不是new message()
(JVM可达性分析,不可达的就回收,new 对象,不用的时候不是立即回收,而是被JVM标记为可清除,等内存不足时,在通过可达性分析,不可达的回收,GC的时候会停止所有的线程,就会造成卡顿)
八、Handler没有消息处理是阻塞的还是非阻塞的?为什么不会有ANR产生?
没有消息就会阻塞,主线程一直处于休眠状态。。。。。。
对于线程即是一段可执行的代码,当可执行代码执行完成后,线程生命周期便该终止了,线程退出。而对于主线程,我们是绝不希望会被运行一段时间,自己就退出,那么如何保证能一直存活呢?简单做法就是可执行代码是能一直执行下去的,死循环便能保证不会被退出,例如,binder线程也是采用死循环的方法,通过循环方式不同与Binder驱动进行读写操作,当然并非简单地死循环,无消息时会休眠。但这里可能又引发了另一个问题,既然是死循环又如何去处理其他事务呢?通过创建新线程的方式。真正会卡死主线程的操作是在回调方法onCreate/onStart/onResume等操作时间过长,会导致掉帧,甚至发生ANR,looper.loop本身不会导致应用卡死。
主线程的死循环一直运行是不是特别消耗CPU资源呢? 其实不然,这里就涉及到Linux pipe/epoll机制,简单说就是在主线程的MessageQueue没有消息时,便阻塞在loop的queue.next()中的nativePollOnce()方法里,此时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生,通过往pipe管道写端写入数据来唤醒主线程工作。这里采用的epoll机制,是一种IO多路复用机制,可以同时监控多个描述符,当某个描述符就绪(读或写就绪),则立刻通知相应程序进行读或写操作,本质同步I/O,即读写是阻塞的。 所以说,主线程大多数时候都是处于休眠状态,并不会消耗大量CPU资源。
ANR的原理:如下图在各种场景下都有一个触发ANR的时间点,这是通过Message来设置的一个定时器,达到定时器的事件,还没有完成相应的操作,就会发生ANR。但是主线程阻塞,处于休眠状态,和ANR完全没有关系。处于完全不同的体系中