温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

不想被面试官虐?Android知识汇总,你必须知道的Handler八大问题!

发布时间:2020-08-07 08:32:19 来源:ITPUB博客 阅读:197 作者:yilian 栏目:移动开发

前言

handler机制几乎是 Android面试时必问的问题,虽然看过很多次 handler源码,但是有些面试官问的问题却不一定能够回答出来,趁着机会总结一下面试中所覆盖的 handler知识点。

1、讲讲 Handler 的底层实现原理?

下面的这幅图很完整的表现了整个 handler机制。

不想被面试官虐?Android知识汇总,你必须知道的Handler八大问题!

要理解handler的实现原理,其实最重要的是理解 Looper的实现原理, Looper才是实现 handler机制的核心。任何一个 handler在使用 sendMessage或者 post时候,都是先构造一个 Message,并把自己放到 message中,然后把 Message放到对应的 LooperMessageQueueLooper通过控制 MessageQueue来获取 message执行其中的 handler或者 runnable。 要在当前线程中执行 handler指定操作,必须要先看当前线程中有没有 looper,如果有 looperhandler就会通过 sendMessage,或者 post先构造一个 message,然后把 message放到当前线程的 looper中, looper会在当前线程中循环取出 message执行,如果没有 looper,就要通过 looper.prepare()方法在当前线程中构建一个 looper,然后主动执行 looper.loop()来实现循环。

梳理一下其实最简单的就下面四条:

1、每一个线程中最多只有一个 Looper,通过 ThreadLocal来保存, Looper中有 Message队列,保存 handler并且执行 handler发送的 message

2、在线程中通过 Looper.prepare()来创建 Looper,并且通过 ThreadLocal来保存 Looper,每一个线程中只能调用一次 Looper.prepare(),也就是说一个线程中最多只有一个 Looper,这样可以保证线程中 Looper的唯一性。

3、 handler中执行 sendMessage或者 post操作,这些操作执行的线程是 handlerLooper所在的线程,和 handler在哪里创建没关系,和 Handler中的 Looper在那创建有关系。

4、一个线程中只能有一个 Looper,但是一个 Looper可以对应多个 handler,在同一个 Looper中的消息都在同一条线程中执行。

2、Handler机制,sendMessage和post(Runnable)的区别?

要看 sendMessagepost区别,需要从源码来看,下面是几种使用 handler的方式,先看下这些方式,然后再从源码分析有什么区别。  例1、 主线程中使用 handler

  //主线程
          Handler mHandler = new Handler(new Handler.Callback() {
              @Override
              public boolean handleMessage(@NonNull Message msg) {
                  if (msg.what == 1) {
                      //doing something
                  }
                  return false;
              }
          });
          Message msg = Message.obtain();
          msg.what = 1;
          mHandler.sendMessage(msg);

上面是在主线程中使用 handler,因为在 Android中系统已经在主线程中生成了 Looper,所以不需要自己来进行 looper的生成。如果上面的代码在子线程中执行,就会报

  Can't create handler inside thread " + Thread.currentThread()
                          + " that has not called Looper.prepare()

如果想着子线程中处理 handler的操作,就要必须要自己生成 Looper了。

例2 、子线程中使用handler

          Thread thread=new Thread(new Runnable() {
              @Override
              public void run() {
                  Looper.prepare();
                  Handler handler=new Handler();
                  handler.post(new Runnable() {
                      @Override
                      public void run() {
                      }
                  });
                  Looper.loop();
              }
          });

上面在 Thread中使用 handler,先执行 Looper.prepare方法,来在当前线程中生成一个 Looper对象并保存在当前线程的 ThreadLocal中。 看下 Looper.prepare()中的源码:

  //prepare
      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));
      }
  //Looper
      private Looper(boolean quitAllowed) {
          mQueue = new MessageQueue(quitAllowed);
          mThread = Thread.currentThread();
      }

可以看到 prepare方法中会先从 sThreadLocal中取如果之前已经生成过 Looper就会报错,否则就会生成一个新的 Looper并且保存在线程的 ThreadLocal中,这样可以确保每一个线程中只能有一个唯一的 Looper

另外:由于 Looper中拥有当前线程的引用,所以有时候可以用 Looper的这种特点来判断当前线程是不是主线程。

      @RequiresApi(api = Build.VERSION_CODES.KITKAT)
      boolean isMainThread() {
          return Objects.requireNonNull(Looper.myLooper()).getThread() == 
  Looper.getMainLooper().getThread();
      }

sendMessage vs post

先来看看 sendMessage的代码调用链:

不想被面试官虐?Android知识汇总,你必须知道的Handler八大问题!

enqueueMessage源码如下:

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

enqueueMessage的代码处理很简单, msg.target = this;就是把当前的 handler对象给 message.target。然后再讲 message进入到队列中。

post代码调用链:

不想被面试官虐?Android知识汇总,你必须知道的Handler八大问题!

调用 post时候会先调用 getPostMessage生成一个 Message,后面和 sendMessage的流程一样。下面看下 getPostMessage方法的源码:

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

可以看到 getPostMessage中会先生成一个 Messgae,并且把 runnable赋值给 messagecallback.消息都放到 MessageQueue中后,看下 Looper是如何处理的。

      for (;;) {
          Message msg = queue.next(); // might block
          if (msg == null) {
              return;
          }
          msg.target.dispatchMessage(msg);
      }

Looper中会遍历 message列表,当 message不为 null时调用 msg.target.dispatchMessage(msg)方法。看下 message结构:

不想被面试官虐?Android知识汇总,你必须知道的Handler八大问题!

也就是说 msg.target.dispatchMessage方法其实就是调用的 Handler中的dispatchMessage方法,下面看下 dispatchMessage方法的源码:

      public void dispatchMessage(@NonNull Message msg) {
          if (msg.callback != null) {
              handleCallback(msg);
          } else {
              if (mCallback != null) {
                  if (mCallback.handleMessage(msg)) {
                      return;
                  }
              }
              handleMessage(msg);
          }
      }
  //
   private static void handleCallback(Message message) {
          message.callback.run();
      }

因为调用 post方法时生成的 message.callback=runnable,所以 dispatchMessage方法中会直接调用  message.callback.run();也就是说直接执行 post中的 runnable方法。 而 sendMessage中如果 mCallback不为 null就会调用 mCallback.handleMessage(msg)方法,否则会直接调用 handleMessage方法。

总结  post方法和 handleMessage方法的不同在于, postrunnable会直接在 callback中调用 run方法执行,而 sendMessage方法要用户主动重写 mCallback或者 handleMessage方法来处理。

3、Looper会一直消耗系统资源吗?

首先给出结论, Looper不会一直消耗系统资源,当 LooperMessageQueue中没有消息时,或者定时消息没到执行时间时,当前持有 Looper的线程就会进入阻塞状态。

不想被面试官虐?Android知识汇总,你必须知道的Handler八大问题!

下面看下 looper所在的线程是如何进入阻塞状态的。 looper阻塞肯定跟消息出队有关,因此看下消息出队的代码。

消息出队

     Message next() {
          // Return here if the message loop has already quit and been disposed.
          // This can happen if the application tries to restart a looper after quit
          // which is not supported.
          final long ptr = mPtr;
          if (ptr == 0) {
              return null;
          }
          int nextPollTimeoutMillis = 0;
          for (;;) {
              if (nextPollTimeoutMillis != 0) {
                  Binder.flushPendingCommands();
              }
              nativePollOnce(ptr, nextPollTimeoutMillis);
              // While calling an idle handler, a new message could have been delivered
              // so go back and look again for a pending message without waiting.
           	  if(hasNoMessage)
           	  {
           	  nextPollTimeoutMillis =-1;
           	  }
          }
      }

上面的消息出队方法被简写了,主要看下面这段,没有消息的时候 nextPollTimeoutMillis=-1

 	if(hasNoMessage)
           	{
           	nextPollTimeoutMillis =-1;
           	}

看for循环里面这个字段所其的作用:

   if (nextPollTimeoutMillis != 0) {
                  Binder.flushPendingCommands();
              }
    nativePollOnce(ptr, nextPollTimeoutMillis);

Binder.flushPendingCommands();这个方法的作用可以看源码里面给出的解释:

      /**
       * Flush any Binder commands pending in the current thread to the kernel
       * driver.  This can be
       * useful to call before performing an operation that may block for a long
       * time, to ensure that any pending object references have been released
       * in order to prevent the process from holding on to objects longer than
       * it needs to.
       */

也就是说在用户线程要进入阻塞之前跟内核线程发送消息,防止用户线程长时间的持有某个对象。再看看下面这个方法: nativePollOnce(ptr, nextPollTimeoutMillis);nextPollingTimeOutMillis=-1时,这个 native方法会阻塞当前线程,线程阻塞后,等下次有消息入队才会重新进入可运行状态,所以 Looper并不会一直死循环消耗运行内存,对队列中的颜色消息还没到时间时也会阻塞当前线程,但是会有一个阻塞时间也就是 nextPollingTimeOutMillis>0的时间。

当消息队列中没有消息的时候looper肯定是被消息入队唤醒的。

消息入队

  boolean enqueueMessage(Message msg, long when) {
          if (msg.target == null) {
              throw new IllegalArgumentException("Message must have a target.");
          }
          if (msg.isInUse()) {
              throw new IllegalStateException(msg + " This message is already in use.");
          }
          synchronized (this) {
              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;
              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 {
                  // Inserted within the middle of the queue.  Usually we don't have to wake
                  // up the event queue unless there is a barrier at the head of the queue
                  // and the message is the earliest asynchronous message in the queue.
                  needWake = mBlocked && p.target == null && msg.isAsynchronous();
                  Message prev;
                  for (;;) {
                      prev = p;
                      p = p.next;
                      if (p == null || when < p.when) {
                          break;
                      }
                      if (needWake && p.isAsynchronous()) {
                          needWake = false;
                      }
                  }
                  msg.next = p; // invariant: p == prev.next
                  prev.next = msg;
              }
              // We can assume mPtr != 0 because mQuitting is false.
              if (needWake) {
                  nativeWake(mPtr);
              }
          }
          return true;
      }

上面可以看到消息入队之后会有一个

  if (needWake) {
                nativeWake(mPtr);
            }

方法,调用这个方法就可以唤醒线程了。另外消息入队的时候是根据消息的 delay时间来在链表中排序的, delay时间长的排在后面,时间短的排在前面。如果时间相同那么按插入时间先后来排,插入时间早的在前面,插入时间晚的在后面。

4、android的Handle机制,Looper关系,主线程的Handler是怎么判断收到的消息是哪个Handler传来的?

Looper是如何判断 Message是从哪个 handler传来的呢?其实很简单,在 1中分析过, handlersendMessage的时候会构建一个 Message对象,并且把自己放在 Messagetarget里面,这样的话 Looper就可以根据 Message中的 target来判断当前的消息是哪个 handler传来的。

5、Handler机制流程、Looper中延迟消息谁来唤醒Looper?

从3中知道在消息出队的 for循环队列中会调用到下面的方法。

  nativePollOnce(ptr, nextPollTimeoutMillis);

如果是延时消息,会在被阻塞 nextPollTimeoutMillis时间后被叫醒, nextPollTimeoutMillis就是消息要执行的时间和当前的时间差。

6、Handler是如何引起内存泄漏的?如何解决?

在子线程中,如果手动为其创建 Looper,那么在所有的事情完成以后应该调用 quit方法来终止消息循环,否则这个子线程就会一直处于等待的状态,而如果退出 Looper以后,这个线程就会立刻终止,因此建议不需要的时候终止 Looper

  Looper.myLooper().quit()

那么,如果在 HandlerhandleMessage方法中(或者是run方法)处理消息,如果这个是一个延时消息,会一直保存在主线程的消息队列里,并且会影响系统对 Activity的回收,造成内存泄露。

具体可以参考 Handler内存泄漏分析及解决

总结一下,解决 Handler内存泄露主要2点

1 、有延时消息,要在 Activity销毁的时候移除 Messages

2、 匿名内部类导致的泄露改为匿名静态内部类,并且对上下文或者 Activity使用弱引用。

7、handler机制中如何确保Looper的唯一性?

Looper是保存在线程的 ThreadLocal里面的,使用 Handler的时候要调用 Looper.prepare()来创建一个 Looper并放在当前的线程的 ThreadLocal里面。

      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));
      }

可以看到,如果多次调用 prepare的时候就会报 Only one Looper may be created per thread,所以这样就可以保证一个线程中只有唯一的一个 Looper

8、Handler 是如何能够线程切换,发送Message的?

handler的执行跟创建 handler的线程无关,跟创建 looper的线程相关,加入在子线程中创建一个 Handler,但是 Handler相关的 Looper是主线程的,这样,如果 handler执行 post一个 runnable,或者 sendMessage,最终的 handle Message都是在主线程中执行的。

        Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
                Looper.prepare();
                Handler handler=new Handler(getMainLooper());
                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        Toast.makeText(MainActivity.this,"hello,world",Toast.LENGTH_LONG).show();
                    }
                });
                Looper.loop();
            }
        });
        thread.start();

心里话

不论是什么样的大小面试,要想不被面试官虐的不要不要的,只有刷爆面试题题做好全面的准备,除了这个还需要在平时把自己的基础打扎实,这样不论面试官怎么样一个知识点里往死里凿,你也能应付如流啊~

如果文字版的 handle汇总还有些不懂得话,我给大家准备了三星架构师讲解的 2小时视频, Handler面试需要的所有知识都在这,可以好好学一学!

不想被面试官虐?Android知识汇总,你必须知道的Handler八大问题!

当然,面试的时候肯定不会只问 handle,还有其他内容,附上大厂面试题整理的合集,这是我的学习笔记,进行了分类,循序渐进,由基础到深入,由易到简。将内容整理成了五个章节

学习PDF大全+字节跳动真题+简历模板

计算机基础面试题、数据结构和算法面试题、 Java面试题、 Android面试题、其他扩展面试题、非技术面试题总共五个章节354页。

不想被面试官虐?Android知识汇总,你必须知道的Handler八大问题!

还有一份 Android学习 PDF大全,这份 Android学习 PDF大全真的包含了方方面面了

内含 Java基础知识点、 Android基础、 Android进阶延伸、算法合集等等

不想被面试官虐?Android知识汇总,你必须知道的Handler八大问题!
字节跳动真题解析、  Android 知识大全 PDF、简历模板可以关注我看个人简介或者 私信我免费获取

面试时 HR也是不可以忽略的环节,我们经常也会遇到很多关于简历制作,职业困惑、 HR经典面试问题回答等有关面试的问题。

有全套简历制作、春招困惑、 HR面试等问题解析参考建议。

不想被面试官虐?Android知识汇总,你必须知道的Handler八大问题!

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI