zoukankan      html  css  js  c++  java
  • 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),欢迎有心人关注。博客园分享技术,公众号分享心智

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

  • 相关阅读:
    几种常用的曲线
    0188. Best Time to Buy and Sell Stock IV (H)
    0074. Search a 2D Matrix (M)
    0189. Rotate Array (E)
    0148. Sort List (M)
    0859. Buddy Strings (E)
    0316. Remove Duplicate Letters (M)
    0452. Minimum Number of Arrows to Burst Balloons (M)
    0449. Serialize and Deserialize BST (M)
    0704. Binary Search (E)
  • 原文地址:https://www.cnblogs.com/qianguyihao/p/4799730.html
Copyright © 2011-2022 走看看