• Android中关于Handler的若干思考


    在之前的博文中,讲过一些和Handler有关的知识,例如:

    Android 多线程----AsyncTask异步任务详解 

    Android多线程----异步消息处理机制之Handler详解

    今天再把Handler的知识回顾总结一下。

    本文包括与Handler有关的以下问题:

      (1)Handler的作用

      (2)为什么Android中要设计为只能在UI线程中去更新UI呢?

      (3)Handler的两个异常

      (4)Handler、Looper MessageQueue之间的关系(源码角度)

      (5)跟线程相关的Handler,即HandlerThread(源码角度分析)

      (6)主线程往子线程发消息

     

    一、Handler的作用:

    (1)在非UI线程中完成耗时操作,在UI线程中去更新UI。

    (2)可以在主线程中发送延时消息。

    二、为什么Android中要设计为只能在UI线程中去更新UI呢?

    (1)解决多线程并发问题(根本原因)

    (2)提高界面更新的性能问题

    (3)架构设计的简单

    你可能会说,既然是担心多线程并发问题,那我在子线程中加锁进行更新UI行不行呢?你这样想的话,会容易造成UI卡顿的,而且性能也不好。

    注1:大部分面试者很难去说出一个令面试官满意的答案。

    注2:关于多线程,这里举一个例子,比如说银行取款的问题。正常情况下,银行卡余额不能少于取款金额,如果多线程进行取款的话,就会造成线程不安全。

    注3:Android中之所以说架构简单,是因为帮我们封装了很多更新UI的操作。

    三、Handler的两个异常:

    在使用Handler时,经常会出现以下两个异常:

    (1)CalledFromWrongThreadException:这种异常是因为尝试在子线程中去更新UI,进而产生异常。

    (2)Can't create handle inside thread that ha not called Looper.prepared:是因为我们在子线程中去创建Handler,而产生的异常。

    我们接下来通过代码来把这两个异常演示一下。

    1、子线程中更新UI的异常:

    (1)activity_main.xml:

    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                    xmlns:tools="http://schemas.android.com/tools"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    tools:context=".MainActivity">
    
        <TextView
            android:id="@+id/tv"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/hello_world"/>
    
        <Button
            android:id="@+id/btn"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="按钮"/>
    
    </RelativeLayout>

     上方代码中,一个文本,一个按钮,代码比较简单。

    (2)MainActivity.java:

     1 import android.app.Activity;
     2 import android.os.Bundle;
     3 import android.view.View;
     4 import android.widget.Button;
     5 import android.widget.TextView;
     6 
     7 public class MainActivity extends Activity {
     8 
     9     private TextView tv;
    10     private Button btn;
    11 
    12     @Override
    13     protected void onCreate(Bundle savedInstanceState) {
    14         super.onCreate(savedInstanceState);
    15         setContentView(R.layout.activity_main);
    16         tv = (TextView) findViewById(R.id.tv);
    17         btn = (Button) findViewById(R.id.btn);
    18 
    19         //点击按钮后,尝试在子线程中更新UI
    20         btn.setOnClickListener(new View.OnClickListener() {
    21             @Override
    22             public void onClick(View v) {
    23 
    24                 new Thread(new Runnable() {
    25                     @Override
    26                     public void run() {
    27                         tv.setText("smyhvae");  //子线程中更新UI
    28                     }
    29                 }).start();
    30 
    31             }
    32         });
    33     }
    34 }

    上方代码中,核心代码是第27行:点击按钮后,在子线程中更新UI。

    运行程序后,点击按钮,效果如下:

    b40c8233-1367-4c80-b3c1-42e03ab1345f

    此时,我们来看一下后台的log日志:

    678ff542-e443-4276-92ab-9a810a38ff88

    上图中报的错误日志就是因为我们在子线程中去更新UI。

    解决方案:

      在子线程中创建Message消息,通过Handler发给主线程,之后在Handler的handleMessage方法中获得Message消息,进而处理更新UI界面。代码如下:

     1 import android.app.Activity;
     2 import android.os.Bundle;
     3 import android.os.Handler;
     4 import android.os.Message;
     5 import android.view.View;
     6 import android.view.View.OnClickListener;
     7 import android.widget.Button;
     8 import android.widget.TextView;
     9 
    10 public class MainActivity extends Activity implements OnClickListener {
    11     public static final int UPDATE_TEXT = 1;
    12     private TextView text;
    13     private Button changeText;
    14 
    15     //程序一加载,直接在主线程中创建Handler
    16     private Handler handler = new Handler() {
    17         public void handleMessage(Message msg) {
    18             switch (msg.what) {
    19             case UPDATE_TEXT:
    20                 text.setText("Nice to meet you");
    21                 break;
    22             default:
    23                 break;
    24             }
    25         }
    26     };
    27 
    28     @Override
    29     protected void onCreate(Bundle savedInstanceState) {
    30         super.onCreate(savedInstanceState);
    31         setContentView(R.layout.activity_main);
    32         text = (TextView) findViewById(R.id.text);
    33         changeText = (Button) findViewById(R.id.change_text);
    34         changeText.setOnClickListener(this);
    35     }
    36 
    37     @Override
    38     public void onClick(View v) {
    39         switch (v.getId()) {
    40         case R.id.change_text:
    41             new Thread(new Runnable() {
    42                 @Override
    43                 public void run() {
    44                     Message message = new Message();
    45                     message.what = UPDATE_TEXT;
    46                     handler.sendMessage(message);
    47                 }
    48             }).start();
    49             break;
    50         default:
    51             break;
    52         }
    53     }
    54 }

     上方第44行代码也可以换成:

    Message message = handler.obtainMessage();

      

    2、在子线程中创建Handler的异常:

    MainActivity.java:

     1 import android.app.Activity;
     2 import android.os.Bundle;
     3 import android.os.Handler;
     4 import android.widget.TextView;
     5 
     6 public class MainActivity extends Activity {
     7 
     8     private TextView tv;
     9 
    10     @Override
    11     protected void onCreate(Bundle savedInstanceState) {
    12         super.onCreate(savedInstanceState);
    13         setContentView(R.layout.activity_main);
    14         tv = (TextView) findViewById(R.id.tv);
    15 
    16         //尝试在子线程中去创建Handler
    17         new Thread(new Runnable() {
    18             @Override
    19             public void run() {
    20                 new Handler();
    21             }
    22         }).start();
    23     }
    24 }

     运行程序后, 报错如下:

    4b57cbb1-9c58-49e2-b3b8-84e97e34b859

      

    四、Handler、Looper MessageQueue之间的关系:(源码角度)

        如果要问到Handler,这个问题基本是面试必问的。

    原理分析:

    Handler是Android类库提供的用于发送、处理消息或Runnable对象的处理类,它结合Message、MessageQueue和Looper类以及当前线程实现了一个消息循环机制,用于实现任务的异步加载和处理。整个异步消息处理流程的示意图如下图所示:

    根据上面的图片,我们现在来解析一下异步消息处理机制

    • Message消息体,用于装载需要发送的对象。
    • Handler:它直接继承自Object。作用是:在子线程中发送Message或者Runnable对象到MessageQueue中;在UI线程中接收、处理从MessageQueue分发出来的Message或者Runnable对象。发送消息一般使用Handler的sendMessage()方法,而发出去的消息经过处理后最终会传递到Handler的handlerMessage()方法中。
    • MessageQueue用于存放Message或Runnable对象的消息队列。它由对应的Looper对象创建,并由Looper对象管理。每个线程中都只会有一个MessageQueue对象。
    • Looper是每个线程中的MessageQueue的管家负责接收和分发Message或Runnable的工作。调用Looper.loop()方法,就是一个死循环,不断地从MessageQueue中取消息:如果有消息,就取出,并调用Handler的handlerMessage()方法;如果没有消息阻塞。

    现在可以做出如下总结:

    (1)Handler负责发送消息,Looper负责接收Handler发送的消息放到MessageQueue,Looper又将消息回传给Handler自己。

    (2)一个Handler对应一个Looper对象,一个Looper对应一个MessageQueue对象(Looper内部包含一个MessageQueue),一个Handler可以生成多个Message。

    (3)Handler就是公开给外部线程的接口,用于线程间的通信。Looper是由系统支持的用于创建和管理MessageQueue的依附于一个线程的循环处理对象,而Handler是用于操作线程内部的消息队列的,所以            Handler也必须依附一个线程,而且只能是一个线程。

    (4)由于Handler是在主线程中创建的,所以此时handleMessage()方法中的代码也会在主线程中运行,于是我们在这里就可以安心地进行UI操作了

    生活中的例子:

    97e2d20a-ec78-4f28-81b5-83b2ee41f797

    上图中,可以这么理解:开会时,我(Handler)想中途离开去做别的事情,通过sendMessage发消息给领导,领导思考了一会儿,同意之后,通过Looper.looep()方法将消息回传给我,说我可以离开,然后我就调用handleMessage方法去做别的事情去了。

    注:面试的时候,如果只是从字面的角度来解释Handler、Looper MessageQueue之间的关系,并不能够真正打动面试官,倒不如再举一个生动的例子,让面试官觉得你是懂面向对象的思维的。

    五、跟线程相关的Handler,即HandlerThread(源码角度分析)

    这个问题可以看一下这篇博客:

    http://blog.csdn.net/lmj623565791/article/details/47079737

    六、主线程往子线程发消息:(考察你是否真的理解Handler机制)

      我们在平时开发的过程中,经常是子线程往主线程中发消息,让主线程更新UI。但是根据具体的项目需求,也可能会要求让你在主线程中往子线程中发消息。

     1 import android.app.Activity;
     2 import android.os.Bundle;
     3 import android.os.Handler;
     4 import android.os.Looper;
     5 import android.os.Message;
     6 import android.util.Log;
     7 import android.view.View;
     8 import android.view.View.OnClickListener;
     9 import android.widget.Button;
    10 import android.widget.TextView;
    11 
    12 public class MainActivity extends Activity implements OnClickListener {
    13     public static final int UPDATE_TEXT = 1;
    14     private TextView tv;
    15     private Button btn;
    16     private Handler handler;
    17 
    18     @Override
    19     protected void onCreate(Bundle savedInstanceState) {
    20         super.onCreate(savedInstanceState);
    21         setContentView(R.layout.activity_main);
    22         tv = (TextView) findViewById(R.id.tv);
    23         btn = (Button) findViewById(R.id.btn);
    24         btn.setOnClickListener(this);
    25         //疑问:为什么这段代码如果写在onClick方法里面会报空指针?
    26         new Thread(new Runnable() {
    27             @Override
    28             public void run() {
    29                 //1、准备Looper对象
    30                 Looper.prepare();
    31                 //2、在子线程中创建Handler
    32                 handler = new Handler() {
    33                     @Override
    34                     public void handleMessage(Message msg) {
    35                         super.handleMessage(msg);
    36                         Log.i("handleMessage:", Thread.currentThread().getName());
    37                         Log.i("后台输出", "收到了消息对象");
    38                     }
    39                 };
    40                 //3、调用Looper的loop()方法,取出消息对象
    41                 Looper.loop();
    42             }
    43         }).start();
    44 
    45     }
    46     @Override
    47     public void onClick(View v) {
    48         Log.i("onClick:", Thread.currentThread().getName());
    49         switch (v.getId()) {
    50             case R.id.btn:
    51                 Message msg = handler.obtainMessage();
    52                 handler.sendMessage(msg);
    53                 break;
    54 
    55             default:
    56                 break;
    57         }
    58     }
    59 }

    上方的第29行至41行代码:这是MainThread中发送消息,在子线程中接收消息的固定写法。上面的三个步骤再重复一下:

    • 准备Looper对象
    • 在WorkerThread当中生成一个Handler对象
    • 调用Looper的loop()方法之后,Looper对象将不断地从消息队列当中取出对象,然后调用handler的handleMessage()方法,处理该消息对象;如果消息队列中没有对象,则该线程阻塞

    注意,此时handleMessage()方法是在子线程中运行的

    后台运行效果:

    60e0e63e-2c82-45de-a787-ca7dd881141b

    小小地总结一下:

      首先执行Looper的prepare()方法,这个方法有两个作用:一是生成Looper对象,而是把Looper对象和当前线程对象形成键值对(线程为键),存放在ThreadLocal当中,然后生成handler对象,调用Looper的myLooper()方法,得到与Handler所对应的Looper对象,这样的话,handler、looper 、消息队列就形成了一一对应的关系,然后执行上面的第三个步骤,即Looper在消息队列当中循环的取数据。

    另外,在本文最开头的第一段中,我们在主线程中创建Handler也没有调用Looper.prepare()方法,为什么就没有崩溃呢?,这是由于在程序启动的时候,系统已经帮我们自动调用了Looper.prepare()方法。查看ActivityThread中的main()方法,代码如下所示:

     1 public static void main(String[] args) {  
     2     SamplingProfilerIntegration.start();  
     3     CloseGuard.setEnabled(false);  
     4     Environment.initForCurrentUser();  
     5     EventLogger.setReporter(new EventLoggingReporter());  
     6     Process.setArgV0("<pre-initialized>");  
     7     Looper.prepareMainLooper();  
     8     ActivityThread thread = new ActivityThread();  
     9     thread.attach(false);  
    10     if (sMainThreadHandler == null) {  
    11         sMainThreadHandler = thread.getHandler();  
    12     }  
    13     AsyncTask.init();  
    14     if (false) {  
    15         Looper.myLooper().setMessageLogging(new LogPrinter(Log.DEBUG, "ActivityThread"));  
    16     }  
    17     Looper.loop();  
    18     throw new RuntimeException("Main thread loop unexpectedly exited");  
    19 }  

     上方代码中,可以看到,在第7行调用了Looper.prepareMainLooper()方法,而这个方法又会再去调用Looper.prepare()方法,代码如下所示:

    1 public static final void prepareMainLooper() {  
    2     prepare();  
    3     setMainLooper(myLooper());  
    4     if (Process.supportsProcesses()) {  
    5         myLooper().mQueue.mQuitAllowed = false;  
    6     }  
    7 } 

     总结:这样基本就将Handler的创建过程完全搞明白了,总结一下就是在主线程中可以直接创建Handler对象,而在子线程中需要先调用Looper.prepare()才能创建Handler对象。

    六、为什么在有些时候子线程中是可以直接更新UI的:

    这道面试题应该是本文中最难的一个面试题了,需要好好理解。为了回答这个问题,我们需要先通过看源码去了解下面这三个问题:

    (1)Android是如何检测非UI线程去更新UI的

    (2)ViewRootImp是什么?

    (3)ViewRootImp是在哪里创建的?

    源码我就不贴出来了,这里我只是总结一下。

    答案:

      非UI线程真的不能更新UI吗? 是可以的

    解释:

      在线程中更新UI时会调用ViewParent.invalidateChild()方法检查当前的thread是否是Mainthread

    具体源码如下:

    1 final ViewParent p = mParent;
    2     if (p != null && ai != null && l < r && t < b) {
    3     final Rect damage = ai.mTmpInvalRect;
    4     damage.set(l, t, r, b);
    5     p.invalidateChild(this, damage);
    6 }

     而ViewParent是一个接口类,其实现类是ViewRootImpl,通过查看invalidateChild()方法里面的代码就可以看到会他调用checkThread()方法。checkThread()方法如下:

    1 void checkThread() {
    2     if (mThread != Thread.currentThread()) {   //检查更新UI的线程是否是MainThread
    3         throw new CalledFromWrongThreadException(
    4         "Only the original thread that created a view hierarchy can touch its views.");
    5     }
    6 }

     上面的第02行就是检查:在线程中更新UI时当前线程是否是MainThread。

    但是,ViewRootImpl这个类是在activity的onResume()方法中创建。就算在子线程中更新UI,只要在ViewRootImpl创建之前更新UI(比如,程序在执行onCreate方法时,我就去执行setText方法区更新UI),就可以逃避掉checkThread()的检查。

    关于本题,给出以下链接大家去细读一下源码吧:

    Android更新Ui进阶精解(一):

    http://www.jianshu.com/p/6de0a42a44d6

    为什么我们可以在非UI线程中更新UI:

    http://blog.csdn.net/aigestudio/article/details/43449123

    我的公众号

    下图是我的微信公众号(生命团队id:vitateam),欢迎有心人关注。博客园分享技术,公众号分享心智

    我会很感激第一批关注我的人。此时,年轻的我和你,一无所有;而后,富裕的你和我,满载而归。

  • 相关阅读:
    mysql数据引擎
    R语言入门
    springboot整合springmvc、mybatis
    svn搭建和配置
    UML常用图的几种关系的总结
    cookies和session机制
    Java总结篇系列:Java多线程(三)
    Java总结篇系列:Java多线程(一)
    Java总结篇系列:Java多线程(二)
    restframework之认证
  • 原文地址:https://www.cnblogs.com/qianguyihao/p/4799730.html
Copyright © 2020-2023  润新知