handler
一,handler简介及其使用场景
(1)handler简介
Handler主要用于异步消息的处理,通过消息队列,进行线程间的通讯。 这种机制通常用来处理相对耗时比较长的操作。
(2)handler使用场景
1,子线程通过handler更新ui
2,线程间的通讯,子线程间也可以通过handler来实现通讯
3,安卓系统内部使用。Android系统中遍布着Handler。事件监测(卡顿,生命周期切换,anr监测…)
二,handler工作原理分析
(1)handler工作流程
大致流程就是handler.post或者handler.send发送消息,消息进入消息队列,然后通过looper轮询取出消息,然后进行分发处理。值得注意的是,普通消息和异步消息结构体中,都有target属性,来告诉处理的线程,是哪个handler来处理消息。因为一个线程可能有多个handler,looper取出后,就是根据target来分发的。
(2)Message
(1)message结构
主要注意:
what:区别handler发送的消息类型
arg1,aeg2 :两个整形数据,如果数据量量少(比如消息仅仅作为信号),可直接使用
obj:发送的单个对象
data:bundle类型,用于数据较多的场景
replyTo,sendingUid:用于跨进程通讯
(2)message复用的实现
1-我们创建message,有三种方法
直接new 一个message
调用Handler类的 obtainMessage()方法
/*
obtainMessage()方法是还是调用了Message类的方法
只是指定了 target指定了是自己(this),还有初始化了 what ,以及对象 obj
下面我们分析Message类的 obtain()方法
/*
public static Message obtain(Handler h, int what, Object obj) {
Message m = obtain();
m.target = h;
m.what = what;
m.obj = obj;
return m;
}
调用Message类的 obtain()方法。
/**
* Return a new Message instance from the global pool. Allows us to
* avoid allocating new objects in many cases.
*/
public static Message obtain() {
synchronized (sPoolSync) {
if (sPool != null) {
Message m = sPool;
sPool = m.next;
m.next = null;
m.flags = 0; // clear in-use flag
sPoolSize--;
return m;
}
}
return new Message();
}
注意:
1-复用的Message取自一个单链表(消息使用完成后,会插入单链表)
2-有一个同步锁 sPoolSync ,防止多线程造成混乱
3-如果链表为空,则直接new一个 message
2-消息插入复用链表的时机和方式
两个时机:
1-消息被取出并消费后
loop()->从MessageQueue里取出消息msg-> msg.recycleUnchecked()
2-消息入队列时,handler所处线程已经移除messagequeue
MessageQueue类 enqueueMessage方法-> msg.recycle();
代码片段
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;
}
recycle()方法
主要有个判断消息是否正在被使用的操作 通过 message的flags属性是否为1(正在被使用)判断。如果没有在使用,则执行 recycleUnchecked()方法
正常消息入队流程:获取消息 msg->enqueque ->没有执行 recycle方法->markInUse()->…
非正常:获取消息 msg->enqueque ->handler所在线程 消息队列退出,执行 recycle方法,消息被回收,没有入队
recycleUnchecked 方法 主要就是清空消息,把消息插入队头。
因为是链表,所以也用message的next属性
详细内容:
https://blog.csdn.net/yangsenhao211423/article/details/106852828
(3)Messagequeue
数据结构:链表,方便插入(因为有插队消息,延时消息的存在,必须方便插入)
1-普通消息
普通消息,插队消息,延时消息的区别:delayMillis
//这里普通消息,delayMillis为0,但是还是有SystemClock.uptimeMillis(),
安卓系统开机到现在的时间
return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
最终调用
//插队消息,uptimeMillis直接为0,所以一定插在队首
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
msg.target = this;
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}
enqueueMessage()方法流程:
判断是否target是否为空
判断该消息是否已经被使用
加锁
判断handler所在线程消息队列是否退出
为消息增加正在被使用表示
判断消息是否插在队列头部
根据when的大小来排序插入
注意:这里面代码里有几个线程唤醒的时机
1-如果插入队头,如果线程原来是阻塞的,那么唤醒
2-如果队头是屏障消息,而且线程阻塞,将要插入的msg是异步消息,且前面没有异步消息(最早被执行的异步消息),那么唤醒。具体阻塞/唤醒内容在最后讨论
2-同步屏障与异步消息
什么是同步屏障
target=null的消息,在取消息时,如果消息是屏障消息,则取出屏障后的异步消息执行。
同步屏障创建
//MessageQueue类
//屏障消息 target为空,直接在MessageQueue类里加入链表,不走enquue方法
public int postSyncBarrier() {
return postSyncBarrier(SystemClock.uptimeMillis());
}
next()方法里,通过target判断是不是屏障消息,是则找到屏障后的第一个异步消息
if (msg != null && msg.target == null) {
// Stalled by a barrier. Find the next asynchronous message in the queue.
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
注意,同步屏障使用后,记得移除,否则屏障一直存在,导致屏障后普通消息不能执行【loop()方法里,死循环执行 Message msg = queue.next()。而queue.next()方法,每次都是从队头开始取消息】
异步消息和普通消息的区别
除了标志位:isAsynchronous 不同之外,没有什么区别。
异步消息发送有两种方式
1-直接创建异步handler。这个handler一直发异步消息
2-创建消息时,标志区改为true
同步屏障典型的使用场景
Android应用框架中为了更快的响应UI刷新事件在ViewRootImpl.scheduleTraversals中使用了同步屏障
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
//设置同步障碍,确保mTraversalRunnable优先被执行
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//内部通过Handler发送了一个异步消息
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
//mTraversalRunnable调用了performTraversals执行measure、layout、draw
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
https://blog.csdn.net/asdgbc/article/details/79148180
3-阻塞与唤醒(pipe/epoll机制)
因为Looper里loop()方法是死循环取消息,在没有消息需要取的时候,这个的循环会造成资源浪费,所以在不需要时,则·阻塞线程。避免资源浪费
阻塞时机:nativePollOnce(ptr, nextPollTimeoutMillis)方法
队列为空(nextPollTimeoutMillis=-1 一直阻塞), 消息执行时机未到
唤醒时机:
消息入队时,若插入的消息在链表最前端
有同步屏障时插入的是最早被执行的异步消息
移除同步屏障时,若消息列表为空或者同步屏障后面不是异步消息时
线程移除messagequeue时,执行Message的quit方法。
epoll IO多路复用模型实现机制
(1)什么是阻塞与唤醒
答:操作系统为了支持多任务,实现了进程调度的功能,会把进程分为“运行”和“等待”等几种状态。运行状态是进程获得cpu使用权,正在执行代码的状态;等待状态是阻塞状态。操作系统会根据进程调度算法执行各个运行状态的进程,由于速度很快,看上去就像是同时执行多个任务。
处于运行状态的进程会被工作队列所引用。
等待队列是个非常重要的结构,它指向所有需要等待该事件的进程
,也就是说,每个需要阻塞和唤醒的事件都一个等待队列。阻塞和唤醒过程就是进程在这两个队列之间调度
Select机制流程:操作系统把需阻塞和唤醒的进程分别加入事件们的等待队列中,当某个事件触发,唤醒进程,遍历事件列表,找到触发事件,获取数据。
select的缺点:
其一,每次调用select都需要将进程加入到所有监视socket的等待队列,每次唤醒都需要从每个队列中移除。这里涉及了两次遍历,而且每次都要将整个fds列表传递给内核,有一定的开销。正是因为遍历操作开销大,出于效率的考量,才会规定select的最大监视数量,默认只能监视1024个socket。
其二,进程被唤醒后,程序并不知道哪些socket收到数据,还需要遍历一次。
epoll机制在selet的基础上增加了内核维护的就绪列表rdlist(就绪列表),将需要监听的事件加入监视列表。当某个事件触发,该事件就会被加入就绪列表,不要去遍历无关的整个事件列表。
监视列表:红黑树结构,方便的添加和移除,还要便于搜索,以避免重复添加
就绪列表:双向链表结构,便于插入。
借鉴于
https://blog.csdn.net/sunxianghuang/article/details/105028062
(4)Looper
Looper类:
用于为线程运行消息循环的类。默认线程没有与它们相关联的消息喜欢;所以要在运行循环的线程中调用prepare(),然后调用loop()让它循环处理消息,直到循环停止。
与消息循环的交互是通过Handler类
注意:如果开启一个子线程需要接收handler消息,就必须执行两个方法。
主线程里不需要我们写,是因为安卓源码已经帮我们写好。自己开启的子线程需要写
Looper.prepare():准备本线程的Looper(包括准备消息队列之类),并保存在ThreadLocal(后面会讨论)里
//主线程不允许退出,quitAllowed为false。而子线程为true
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));
}
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}
Looper.loop():循环从messageQueue里取并分发消息。这里就不贴代码了。
1-ThreadLocal机制
什么是threadLocal机制
答:ThreadLocal 用作保存每个线程独享的对象,为每个线程都创建一个副本,这样每个线程都可以修改自己所拥有的副本, 而不会影响其他线程的副本,确保了线程安全。这里的副本是相对于线程间上的副本,和可见性中拷贝进程的副本不是一个意义
ThreadLocal 用作每个线程内需要独立保存信息,以便供其他方法更方便地获取该信息的场景。
threadLocal通过get()方法获取本线程的ThreadLocalMap(以threadLocal为key,储存线程对象)
一个 Thread 里面只有一个ThreadLocalMap ,而在一个 ThreadLocalMap 里面却可以有很多的 ThreadLocal,每一个 ThreadLocal 都对应一个 value。
注意的点:
(1)entry对象
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
key为threadlocal的弱应用,value为强引用。
为什么key为弱应用?
若key为强引用,那么threadLocal对象由于被自己内部ThreadLocalMap 所引用,所以对象在堆空间里回收不了。
value为强引用导致theadlocal内存泄露
当value为强引用,那么如果该ThreadLocal所在的线程没有被及时回收(例如在线程池里常驻),而导致value一直不为null,甚至存在theadLocal为null,但value不为null(null,对象)的情况(虽然threadlLocal本身也有清理空key的方法,但也有力所不及的情况),导致内存泄露。
如何避免?
使用完ThreadLocal变量后,要手动调用remove()方法来清理ThreadLocalMap(一般在finally代码块中)。finally应该被重视利用,用来做一些善后工作,写高质量代码。
(2)threadLocalMap
数据结构:数组
元素 :Entry<key,value>
添加元素过程:可结合源码看
从头开始遍历数组
若key存在,则替换value
若key为null,则替换Entry的key和value
否则在数组末尾new一个Entry放入数组,再去清理key为null的entry。
清理之后,判断是否需要扩容。即先添加,再清理,再判断扩容。(这个过程后面再写笔记分析)
具体意义:除了Looper对象的储存,同一个runable在两个线程里跑,使用threadLocal,可以保证变量或者对象独立。
https://www.cnblogs.com/cy0628/p/15086201.html
(5)IdleHandler介绍与使用场景
IdleHandler会在MessageQueue中没有Message要处理或者要处理的Message都是延时任务的时候得到执行(代码里,是在阻塞之前执行idlehandle的,因为阻塞在下一次循环里执行),这种特性很重要,因为当MessageQueue中没有Message要处理或者要处理的Message都是延时任务的时候,就表明当前线程为空闲状态。
//在nextPollTimeoutMillis被赋值之后(说明即将阻塞,因为不阻塞得话,已经return了)
if (pendingIdleHandlerCount < 0
&& (mMessages == null || now < mMessages.when)) {
pendingIdleHandlerCount = mIdleHandlers.size();
}
if (pendingIdleHandlerCount <= 0) {
//没有idlehandler的话,直接continue,进入下一个循环阻塞,不会走到
// nextPollTimeoutMillis = 0;这句
// No idle handlers to run. Loop and wait some more.
mBlocked = true;
continue;
}
if (mPendingIdleHandlers == null) {
mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
}
mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
}
// Run the idle handlers.
// We only ever reach this code block during the first iteration.
for (int i = 0; i < pendingIdleHandlerCount; i++) {
final IdleHandler idler = mPendingIdleHandlers[i];
mPendingIdleHandlers[i] = null; // release the reference to the handler
boolean keep = false;
try {
keep = idler.queueIdle();
} catch (Throwable t) {
Log.wtf(TAG, "IdleHandler threw exception", t);
}
if (!keep) {
synchronized (this) {
mIdleHandlers.remove(idler);
}
}
}
// Reset the idle handler count to 0 so we do nt run them again.
//mIderHandlers执行条件是pendingIdleHandlerCount=-1
//所以idlehandler只执行一遍,防止进入死循环。
pendingIdleHandlerCount = 0;
// While calling an idle handler, a new message could have been delivered
// so go back and look again for a pending message without waiting.
//执行完idlehandler之后,赋值0,防止线程阻塞,导致第一个消息无法执行(存在idlehandler执行完,第一个消息时间已经过去的情况,导致线程无法被唤醒)
nextPollTimeoutMillis = 0;
使用场景:页面第一帧回执完成检测
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Looper.getMainLooper().getQueue().addIdleHandler(new MessageQueue.IdleHandler() {
@Override
public boolean queueIdle() {
Log.e("DBL", "queueIdle");
// UI第一帧绘制完成(可以理解为页面可见)
return false; // 返回false表示MessageQueue在执行完这段代码后将该IdleHandler删除,反之不删除,下一次继续执行
}
});
}
@Override
protected void onResume() {
super.onResume();
Log.e("DBL", "onResume");
}
注意:这样的闲时机制(jetpack),处理时机不确定。使用时需要谨慎,因为可能存在idlehander任务一直无法执行的情况
参考大佬博客
https://www.jianshu.com/p/eb7d15ac052a
一,handler常见问题分析
(1)内存泄露问题
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case 1:
mTextView.setText(msg.obj + "");
break;
default:
break;
}
}
};
之前基础太差,不知道为什么这里Handler被声明为内部类了。其实这里相当于继承Handler,然后重写了handlerMessage方法,是一个内部类。然后被实例化了。我的理解是这样的。
因为内部类(非静态)和匿名内部类会持有外部引用,所以如果消息队列里有消息没有处理完,外部Activity就不能被回收。
解决办法:静态内部类+弱引用
https://www.jianshu.com/p/804e774d9f76
(2)非ui线程操作view
我们知道非ui线程操作view可以使用hander和view.post
但是,在resume之前,子线程是可以直接操作view的
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
-----------------▶// checkThread()干的活就是检测当前线程是否为主线程
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
(3)View.post与handler.post的区别
(1)handler.post()
在工作线程中执行耗时任务,当任务完成时,会返回UI线程,一般是更新UI。这时有两种方法可以达到目的。
一种是handler.sendMessage。发一个消息,再根据消息,执行相关任务代码。
另一种是handler.post®。r是要执行的任务代码。意思就是说r的代码实际是在UI线程执行的。可以写更新UI的代码,其实就是一个runable。
(2)view.post
view.post(),也是执行一个runable,例如用来获取view宽高。
源码
/**
* <p>Causes the Runnable to be added to the message queue.
* The runnable will be run on the user interface thread.</p>
*
* @param action The Runnable that will be executed.
*
* @return Returns true if the Runnable was successfully placed in to the
* message queue. Returns false on failure, usually because the
* looper processing the message queue is exiting.
*
* @see #postDelayed
* @see #removeCallbacks
*/
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}
// Postpone the runnable until we know on which thread it needs to run.
// Assume that the runnable will be successfully placed after attach.
getRunQueue().post(action);
return true;
}
从代码可以看出,分为两种情况,attachInfo不为空(view已经attach到window),则通话主线程的handler来执行runable。否则就缓存起来(mRunQueue),等到view被attach(view的dispatchAttachedToWindow()方法被执行)被主线程的handler执行。
1-attachInfo赋值
dispatchAttachedToWindow(AttachInfo info, int visibility)方法,view被添加。在我们熟悉的ViewRootImpl类里performTraversals() 方法被调用
dispatchAttachedToWindow(AttachInfo info, int visibility)方法,view被卸载
https://www.cnblogs.com/dasusu/p/8047172.html