• 1

  • 474

  • Favorite

关于 Handler 的一切

None

关注Linux

2 months ago

话说在前头

上文自己一个Handler机制对 Handler 机制进行了一番探索和思考,对 Handler 的设计意图有了自己的认识和思考,那么这篇文章我们将从 android 系统 Handler 机制的源码出发,来看一看 android 系统 Handler 机制的设计,对比一下上文中自己写的 Handler 机制。

阐述逻辑如下:

  1. 从源码分析 Handler。
    1. 弄清同步屏障和异步消息的本质。
    2. 如何使用 ThreadLocal 保持线程独有?
  2. 开发者对 Handler 机制的使用
  3. 平常说的主线程的问题
  4. Handler 使用时需要注意的问题。
  5. 面试中常见的 Handler 相关问题。
  6. 总结

主线程 Handler 的创建与消息处理

知道为什么 Activity 的 onCreate() / onStart() 等生命周期函数都是运行在主线程当中吗?在下面的主线程 Handler 的创建和消息处理过程分析中,这个问题将会得到解答。

分析主线程 Handler 创建与消息处理源码

主线程的创建和使用方式和自定义一个Handler机制文章中的 Handler 机制使用方式一样,首先创建 Looper ,然后向 Looper 中的消息容器发送消息,最后 Looper 开启死循环获取消息/处理消息。

//ActivityThread.java
public static void main(String[] args) {
		......
    // 创建 Looper
    Looper.prepareMainLooper();
  	// App 启动流程入口,会发送向 Looper 的消息队列中添加一系列的消息以启动 App。
    ActivityThread thread = new ActivityThread();
    thread.attach(false, startSeq);
		// 获取 Handler 
    if (sMainThreadHandler == null) {
        sMainThreadHandler = thread.getHandler();
    }
		// 启动 looper,开启死循环处理消息
    Looper.loop();
		......
}
复制代码

主线程 Looper 创建过程

下面就直接看 looper 的创建过程,这里的重点有两处。

  1. 使用构造函数创建了一个 Looper 并且是否允许退出参数传入了 false,表示该 Looper 不允许开发者调用主动退出函数。
  2. 利用 ThreadLocal 对象的特性,保证单个线程只有唯一的一个 Looper 对象实例。ThreadLocal 的特性在后面有讲述。
	// Looper.java
	// 给当前线程创建 Looper,为什么说给当前线程?  
	public static void prepareMainLooper() {
   		// 创建 Looper ,false 表示该 Looper 不允许开发者调用退出函数退出 looper 死循环。
        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) {
      	// 如果该线程中已经有一个 Looper 就不允许创建多个 Looper。
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
      	// 创建一个 Looper 实例,传入是否允许该 Looper 退出死循环的参数,并添加到该线程的 ThreadLocal 中
        sThreadLocal.set(new Looper(quitAllowed));
    }

		// Looper 构造函数
    private Looper(boolean quitAllowed) {
      	// 初始化 MessageQueue ,传入是否允许退出函数
        mQueue = new MessageQueue(quitAllowed);
        mThread = Thread.currentThread();
    }

		// 获取该线程的 Looper
    public static @Nullable Looper myLooper() {
        return sThreadLocal.get();
    }
复制代码

loop() 过程分析

loop() 方法中也有几个重点。

  1. MessageQueue.next() 方法,该方法主要作用是从 MessageQueue 中获取消息,其中也是利用了一个死循环从不断的获取消息,当没有消息队列中消息时就利用 epoll 机制进入沉睡,等待新消息到来时进行唤醒。
  2. Logging 在消息处理前后进行日志输出,这一点是很多性能监控框架用来监控 FPS 值的一种机制。
  3. Message 的处理,Handler.dispatchMessage() 函数,在面试中经常会被问到。主要是对 Message 中当有 Callback 和 Handler 中有定义 Callback 时 Message 由谁来处理的顺序问题。
public static void loop() {
		// 死循环
    for (;;) {
      	// 重点!!!!重点!!!!重点!!!!!!该 queue 即为 Looper 构造函数中的 MessageQueue
      	// 该 next() 方法表示从 MessageQueue 中获取消息,如果没有消息在该函数中会调用 Linux 的 
        // epoll 机制陷入沉睡状态,等待有消息加入 MessageQueue 时被唤醒。
        Message msg = queue.next(); // might block
        if (msg == null) {
            // 从 MessageQueue 中获取不到消息就表示退出死循环,为空的具体的可以看 MessageQueue.quit() 函数。
            return;
        }
        // 消息被执行前输出消息,可以在 FPS 监控中常用
        final Printer logging = me.mLogging;
        if (logging != null) {
            logging.println(">>>>> Dispatching to " + msg.target + " " +
                    msg.callback + ": " + msg.what);
        }
				......
        // 重点!!! target 为向 MessageQueue 发送消息的 Handler 实例。
        // 处理 msg 消息。
        msg.target.dispatchMessage(msg);
				......
        // 消息执行完成后输出信息,对应之前的 println()
        if (logging != null) {
            logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
        }
    }
}
复制代码

MessageQueue.next() 方法分析

next() 方法中的重点有几个。

  1. epoll 机制。 关于 epoll 机制,这个主要是 Linux 方面的知识,我也还是一知半解,这里推荐系列文章,写的可以,值得一看。

  2. 如何处理延时 Message 消息和按顺序执行?

    这里分为两步处理的,第一步是在向 MessageQueue 中会根据消息加入时间和延时时间进行排序,加入时间在前和延时时间短的 Message 排在队列的前面。第二步是在 MessageQueue 获取到要执行的 Message 之后,会判断其执行时间是否是当前,若不是,则会计算时间差,使用该时间差调用 epoll 机制进入定时睡眠。

  3. 同步屏障的概念以及同步消息处理。

    为了优先执行某些 Message ,如:渲染 UI 、响应用户操作等消息因此设计了同步障碍这个机制。当有 Message 需要优先执行时,向 MessageQueue 的头部插入一条 target(即处理消息的Handler) 为空的消息(设置同步障碍),并将需要执行的 Message 的是否是同步消息的标志设置为 true(isAsynchronous()返回值为 true)。在后续的 next() 方法中,当碰到同步障碍时就会选择性的优先执行同步消息。

  4. 处理 looper 退出。

    退出的处理也是在 MessageQueue 中处理,由 MQ 的退出触发 Looper 的退出。主要流程是移除队列中所有 Msg。由是否移除即将处理的 msg 为去区别点,分两种方法进行消息移除,一种是保留即将执行的 msg,消息执行完毕之后退出,一种是直接移除所有 msg,直接退出。

  5. IdleHandler 的使用。

    当 MessageQueue 中后续没有消息执行时,如果有 IdlerHander 就是执行 IdlerHander,即当 Handler 空闲时执行。

    另外我们要注意,同一 IderHander 会被触发多次,我们需要处理这种情况。这里举例一种,实现 IderHandler 接口,queueIdle() 返回 false ,待执行完毕后,即会移除该 IderHandler。

// MessageQueue.java
Message next() {
    // mPtr 为 c++ 层指针,当 MessageQueue 需要退出时,会将该值赋为 0 
    final long ptr = mPtr;
    if (ptr == 0) {
        return null;
    }
		// IdleHandler 数量
    int pendingIdleHandlerCount = -1; // -1 only during first iteration
  	// 利用 epoll 机制沉睡的时间
    int nextPollTimeoutMillis = 0;
    for (;;) {
				// 利用 epoll 机制进入沉睡,
        nativePollOnce(ptr, nextPollTimeoutMillis);
        synchronized (this) {
            // 尝试获取 Message
            final long now = SystemClock.uptimeMillis();
            Message prevMsg = null;
            Message msg = mMessages;
          	// 判断当前 Message 是否是同步障碍,标志就是 msg.target 为空
            if (msg != null && msg.target == null) {
                // 寻找同步消息执行,标志是 msg.isAsynchronous() 
                do {
                    prevMsg = msg;
                    msg = msg.next;
                } while (msg != null && !msg.isAsynchronous());
            }
            if (msg != null) {
              	// msg.when 会在消息被发送时进行赋值
                if (now < msg.when) {
                    // 当前消息是否到了执行时刻,没到将 epoll 沉睡时间计算出来
                    nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                } else {
                    // 到了?返回目标 msg ,同时安排好下一个 msg。
                    mBlocked = false;
                    if (prevMsg != null) {
                        prevMsg.next = msg.next;
                    } else {
                        mMessages = msg.next;
                    }
                    msg.next = null;
                    if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                    msg.markInUse();
                    return msg;
                }
            } else {
                // 没有更多的 msg 将该值赋值为-1,将会在 nativePollOnce() 函数中陷入沉睡等待唤醒。
                nextPollTimeoutMillis = -1;
            }
            // 如果启动调用了退出函数(主线程不允许退出),则处理退出流程,返回 Null 值,在 Looper 中会得到 null 值会直接退出 loop() 中的死循环,该线程若无其它操作,运行完毕后将会自然退出。
            if (mQuitting) {
                dispose();
                return null;
            }
						// 注意!!!能运行到这里的时间点,当 MQ 中没有更多消息时才会运行到这里
            // 计算 IdleHandler 的数量,准备触发 
            if (pendingIdleHandlerCount < 0
                    && (mMessages == null || now < mMessages.when)) {
                pendingIdleHandlerCount = mIdleHandlers.size();
            }
            if (pendingIdleHandlerCount <= 0) {
                // 没有 IdleHandler 执行,进行下一次循环。
                mBlocked = true;
                continue;
            }
						...
        }
        // 循环执行已添加的 IdleHandler
        for (int i = 0; i < pendingIdleHandlerCount; i++) {
            final IdleHandler idler = mPendingIdleHandlers[i];
            mPendingIdleHandlers[i] = null; // release the reference to the handler
            boolean keep = false;
          	...
            // 判断是否移除该 IdleHandler
            keep = idler.queueIdle();
            if (!keep) {
                synchronized (this) {
                    mIdleHandlers.remove(idler);
                }
            }
        }
				...
    }
}
复制代码

同步屏障和异步消息

在上面我们已经说明了什么是同步障碍、何为添加同步障碍。而设计同步障碍的原因就是为了优先执行某些消息,这里的代表就是系统中更新 UI ,这里可以去看ViewRootImpl.scheduleTraversals()函数(View三大步骤的地方哦)和 UI 更新相关机制,这里就不做展开。这里我们来看 MessageQueue.postSyncBarrier() 函数,这里是添加同步障碍的地方,同时结合上一步骤处理 msg 的逻辑来看。

另一个设置异步消息就十分简单了,调用 Message.setAsynchronous() 函数,传入 true 即可(处理逻辑见上一步骤分析)。

// MessageQueue.java
private int postSyncBarrier(long when) {
    synchronized (this) {
        final int token = mNextBarrierToken++;
      	/// 重点!!!这里的 msg 没有 traget,在处理逻辑中有体现。
        final Message msg = Message.obtain();
        msg.markInUse();
        msg.when = when;
        msg.arg1 = token;
				// 后面的操作就是将承担同步障碍作用的 msg 添加到队列头
        return token;
    }
}
复制代码

ThreadLocal

首先明确该类被设计出来的作用是什么,解决了怎样的问题。

ThreadLocal 类出现在 jdk 1.2 中,此时 java 的 synchronized 关键字提供的锁机制还未优化(没有锁升级,直接重量级锁)。当程序中某些变量只能被一个线程使用,若使用 synchronized 关键提供线程安全会增加系统开销,而 ThreadLocal 就提供了在不加锁的情况下,保证某个变量只能被一个线程访问的机制。使用简便,只要 set() / get() 一番即可。

  • ThreadLocal 只是一个工具类,提供 set() / get() / remove() 等操作函数接口。
  • ThreadLocal.ThreadLocalMap 真正保存数据的地方。
  • Thread.threadLocals 成员变量,ThreadLocal 访问时通过 Thread.currentThread() 来进行访问,保证了其他线程不能访问。

关于使用内存泄露的问题

该问题是面试中常问。由于 ThreadLocal.ThreadLocalMap 保存数据构建 Entry 时 key 值(TheadLocal)使用了弱引用,当作为 key 的 ThreadLocal 被回收后,当时持有 ThreadLocal 还未停止,这时存在这 Thread.threadLocals 指向 value 的一条引用,导致 key 已经为空的 value 对象实例无法回收,出现内存泄露问题。

内存泄露问题推荐阅读 > www.cnblogs.com/aspirant/p/…

Thradlocal 详细分析 > www.cnblogs.com/fsmly/p/110…

如何使用 Handler 机制?

使用流程

在日常开发工作当中,多是使用主线程的 Handler 做事情,很少会在其他线程中使用。当然,想要使用也很简单,只需要三步。

  1. Looper.prepare() 函数创建 Looper / MessageQueue,使用该方法创建的 Looper 是可以退出地。
  2. 创建处理发送和处理消息的 Handler。
  3. Looper.loop() 启动死循环处理消息。

如何退出 Looper ?主线程的 Handler 的 Looper 为什么不能退出?

退出

直接调用 Looper.myLooper().quit() 或者 Looper.myLooper().quitSafely() 方法即可,关于两者的区别在上文中有阐述。

主线程 Handler 为什么不能退出?

不是不能退出,是开发者不能调用主线程 Looper 退出的方法,调用会抛出异常。

原因在于,Looper 的死循环的作用不单单是处理消息,同时还保证了主线程无外力作用下永远在运作的机制,iOS / Windows 等开发框架都有相似的机制。

Looper 死循环的退出即表示 App 主线程退出,App 将退出运行。

Handler 机制的使用?

HandlerThread

该类继承自 Thread,内部创建了一个使用使用了 Handler 机制,start() 之后开启了 Looper 的死循环处理消息,Looper 可以退出,Message 消息再该线程中处理。可以利用其执行有序的任务!

IntentService

继承自 Service 拥有 Serivce 的特性,同时内部使用率 HandlerThread,当 onStart() 函数触发时,向 HandlerThread 中的 MessageQueue 中添加一条 Message ,自建的 Handler 处理完毕之后立即调用 stopSelf() 函数停止 Service 的运行。可以利用其在后台执行任务,执行完成之后不需要管理,自动停止。当然,记得和一般 Service 一样清单文件注册。

那些有关主线程的问题

子线程更新 UI

Only the original thread that created a view hierarchy can touch its views. 当我们在子线程操作 UI 时系统会抛出这样一个错误。首先明确,这个错误并不是说不能在主线程更新 UI ,而是只能在创建了 ViewRootImpl 实例的线程中更新 UI。抛出这个错误的是 ViewRootImpl 中的 checkThread() 函数,目的是检测当前请求更新 UI 的线程是不是创建 ViewRootImpl 的线程,为什么需要检测呢?原因在于在调用 requestlayout() 函数后,会向当前线程的 Looper 中添加一条同步屏障,若当前更新 UI 的线程不是创建 ViewRootImpl 的线程,那么该同步屏障不会起作用,那么后续极有可能不回第一时间响应用户操作更新 UI 。

其实子线程是可以更改 UI 的,在 onCreate() / onResume() 函数中都是可以的。具体原因,可以去看 ViewRootImpl 创建时机和 setContentView() 过程。

推荐文章:关于 TextView 子线程更新文字问题 / 为什么要 checkThread()

主线程耗时操作

为什么主线程不能执行耗时操作?从设计上来考虑,单线程更新 UI ,若在 UI 线程执行耗时操作,会导致 UI 更新延迟。因此在 UI 线程(主线程)执行操作时,系统会添加耗时检测机制,UI 线程超过一定时间无响应即会弹出我们常见的 ANR 警告弹框。

ANR 是触发的?又是如何被抓到的?

关于这篇问题,强烈推荐 gityuan 大佬的系列文章。

理解 ANR 触发原理

Handler 使用时哪些需要注意的问题

处理内存泄漏问题

问题

Handler 存在内存泄露的问题主要是 Java 语言中,非静态内部类和匿名内部类会持有外部类的引用,而这个外部类通常就是 Activity 了。当存在延时 Message 还未触发或者有子线程运行耗时任务又持有 Handler 引用,就极有可能引发内存泄露。

解决办法

  1. Activity 销毁前主动移除耗时 Message 和停止耗时任务的处理。
  2. 不要以非静态内部类和匿名内部类的方式使用 Handler。

如何正确创建 Message

这里主要当 Message 创建频繁时,使用优化手段进行优化。也就是 java 语言中优化对象的几种方式。

Message 中的 obtain() 函数已经提供了对象池供我们优化使用。

面试常见问题

epoll 机制

这个就推荐文章了,我也还在研究学习中,如果有了自己的理解,会有文章出来讲讲的。

www.jianshu.com/p/dfd940e7f…

同步屏障

之前有讲过哦,不记得了????

滑上去自爱喽一眼吧!

线程同步 / 切换线程如何处理?

首先,Looper 和 MessageQueue 中都是使用率 synchronized 来保证线程安全。

线程切换?线程之间共享资源,向目标线程中的 MessageQuque 中添加 Message ,目标线程进行处理就完成了线程切换。

IdleHander 的机制和使用?

通过 Looper 获取到 MessageQueue 添加 IdleHander 即可。

该机制是当 MessageQueue 中没有 Message 了(即空闲时)或者处在一个延时消息的执行时,就会调用 IdleHander 执行。注意!这个执行时机是比较模糊的。

当 IdleHander 的 queueIdle() 返回 true 时表示保留该 IdlerHander ,下次继续执行,为 false 则移除。

系统在如下几个地方使用了

推荐阅读 www.wanandroid.com/wenda/show/…

总结

Handler 机制的设计目的是提供一种单线程消息处理机制,而 UI 框架的单线程更新机制又使得 Handler 天然的能完成能承担这个任务,这个呢是 Handler 的重点,是 Handler 的本质。清楚了本质,那么诸如其他的同步屏障、异步消息和 ThreadLocal 等,为了实现这个本质中所采取的方法,也能很快的分而治之的弄明白。

总之,对于 adnroid frmework 的学习之路,我还是推荐自己思考、依靠源码、踩在巨人的肩膀上的方法路线进行学习。

免责声明:文章版权归原作者所有,其内容与观点不代表Unitimes立场,亦不构成任何投资意见或建议。

Linux中文社区

474

Relevant articles

未登录头像

No more data