• Android(java)学习笔记145:Handler消息机制的原理和实现


     联合学习 Android 异步消息处理机制 让你深入理解 Looper、Handler、Message三者关系

     

    1. 首先我们通过一个实例案例来引出一个异常:

    (1)布局文件activity_main.xml

     1 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
     2     xmlns:tools="http://schemas.android.com/tools"
     3     android:layout_width="match_parent"
     4     android:layout_height="match_parent"
     5     tools:context=".MainActivity" >
     6 
     7     <TextView
     8         android:id="@+id/tv"
     9         android:layout_width="wrap_content"
    10         android:layout_height="wrap_content"
    11         android:layout_centerHorizontal="true"
    12         android:layout_centerVertical="true"
    13         android:text="@string/hello_world" />
    14 
    15 </RelativeLayout>

    (2)MainActivity.java

    package com.itheima.testthread;
    
    import android.app.Activity;
    import android.os.Bundle;
    import android.os.Handler;
    import android.os.Message;
    import android.widget.EditText;
    import android.widget.TextView;
    
    public class MainActivity extends Activity {
        
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            System.out.println(Thread.currentThread().getName());
            final TextView tv = (TextView) findViewById(R.id.tv);
            new Thread() {
                public void run() {
                    for (int i = 0; i < 100; i++) {
                           tv.setText("哈哈哈"+i);
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                };
            }.start();
        }
    
    }

    (3)布署程序到模拟器上,出现如下效果,程序直接stop;并且直接报出异常错误: 


    报错:

    android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

    意思:从错误线程调用的异常。谁创建的view,谁才能修改编辑view,只有主线程才可以修改view(所有的View都是主线程创建的)

    上面的报错是ViewRootImpl对UI操作做了验证,这个验证工作是由ViewRootImpl的checkThread方法来完成的,如下所示:

    void checkThread() {
            if(mThread != Thread.currentThread())  {
                    throw new CalledFromWrongThreadException(" Only the
                    original thread that created a  view hierarchy can touch its views.");
             }
    }

    总结:上面报这个CalledFromWrongThreadException错误,是因为只有主线程才能修改View更新UI,但是我们这里却自己新建一个线程new Thread()其中run()的内容涉及到更新UI,这是不允许的,所有才会出现这样的错误警告。

    但是往往我们编写程序的时候设计多个线程需要控制UI显示,但是我们上面已经说过了UI更新只能交给主线程,为了解决这个矛盾,解放程序员,google开发出来消息机制用来子线程和主线程通信,子线程要更新UI,就会发送消息数据给主线程,这样主线程就会更新相应的UI界面,从而解决这个矛盾

    (4)系统为什么不允许在子线程中访问UI呢 ?

    这是因为Android的UI控件不是线程安全的,如果在多线程中并发访问可能会导致UI控件处于不可预期的状态,那为什么系统不对UI控件的访问加锁呢? 缺点有两个:

    首先加上锁机制会让UI访问的逻辑变得复杂;其次锁机制会降低UI访问的效率,这是因为锁机制会阻塞某些线程的执行。

    鉴于这两个缺点,最简单且高效的方法就是采用单线程模型去处理UI操作,对于开发者来说也不是很麻烦,只需要通过Handler切换一下UI访问的执行线程即可。

    2. 消息机制的原理和实现

    消息机制实现逻辑图

      这里handler负责把外面的子线程的消息发到message queue消息队列之中,然后looper会以无限循环的形式查看message queue里面是不是有新消息,如果有的话,looper就把消息发送到handler,交给handler的handlemessage根据信息,更新UI,否则就一直等待。

      Looper中还有一个特殊的概念,那就是ThreadLocal,ThreadLocal不是线程,它的作用是可以在每个线程中存储数据。我们知道,Handler创建的时候会采用当前的线程的Looper来构造消息循环系统,那么Handler内部如何获取当前线程的Looper呢 ? 这就是要使用ThreadLocal了,ThreadLocal可以在不同线程中互不干扰地存储并提供数据,通过ThreadLocal可以轻松获取每个线程的Looper。当然需要注意的是,线程默认是没有Looper,如果需要使用Handler就必须为线程创建Looper。我们经常提到的主线程,也叫UI线程,它就是ActivityThread,ActivityThread被创建时候就会初始化Looper,这也就是主线程中默认可以使用Handler的原因。

    使用消息机制目的

    Android系统的主线程安全的系统,别的线程不可以修改ui线程的界面,如果子线程里面想要更新UI,就必须采用消息机制处理。

    使用handler消息处理器编写代码步骤

    (1)在主线程里面声明消息处理器handler;

    private Handler  handler = new  Handler()  { };

    (2)子线程想要更新UI,利用消息机制

    Message  msg  =  new Message();
    msg.what  消息类型
    msg.obj     具体消息携带的数据
    handler.sendMessage(msg);

    (3)系统内部有消息队列message queue和消息looper,轮询到消息,交给handler去处理:

    (4)重写handler的方法处理消息:

    public  void  handleMessage (Message msg)  {
                         //运行在主线程,更新UI
    
    };

    接下来我们就修改上面的案例,就更好理解这个消息机制:

    MainActivity.java:

     1 package com.itheima.testthread;
     2 
     3 import android.app.Activity;
     4 import android.os.Bundle;
     5 import android.os.Handler;
     6 import android.os.Message;
     7 import android.widget.EditText;
     8 import android.widget.TextView;
     9 
    10 public class MainActivity extends Activity {
    11     private TextView tv;
    12     private Handler handler = new Handler() {
    13         public void handleMessage(Message msg) {
    14             // 运行在主线程,更新ui
    15             String str = (String) msg.obj;
    16             tv.setText(str);
    17             EditText et;
    18             
    19         };
    20     };
    21 
    22     @Override
    23     protected void onCreate(Bundle savedInstanceState) {
    24         super.onCreate(savedInstanceState);
    25         setContentView(R.layout.activity_main);
    26         System.out.println(Thread.currentThread().getName());
    27         tv = (TextView) findViewById(R.id.tv);
    28         new Thread() {
    29             public void run() {
    30                 for (int i = 0; i < 100; i++) {
    31                     // tv.setText("哈哈哈"+i);
    32                     Message msg = new Message();
    33                     msg.obj = "哈哈哈" + i;
    34                     handler.sendMessage(msg);
    35                     try {
    36                         Thread.sleep(100);
    37                     } catch (InterruptedException e) {
    38                         e.printStackTrace();
    39                     }
    40                 }
    41             };
    42         }.start();
    43     }
    44 
    45 }

    acitivity_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:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerHorizontal="true"
            android:layout_centerVertical="true"
            android:text="@string/hello_world" />
    
    </RelativeLayout>

    3. Hanlder之 --- 传递Message --- 传递Runnable对象

     (1)简介:

      一般来说在工作线程中执行耗时任务,当任务完成时,会返回UI线程,一般是更新UI。这时有两种方法可以达到目的。
      一种是:handler.sendMessage。发一个消息,,它是一个Message,再根据消息,执行相关任务代码。
      另一种是:handler.post(r)。r是要执行的任务代码,r是Runnable对象。意思就是说r的代码实际是在UI线程执行的。可以写更新UI的代码。(工作线程是不能更新UI的)

    (2) 传递Message。用于接受子线程发送的数据, 并用此数据配合主线程更新UI。

      在Android中,对于UI的操作通常需要放在主线程中进行操作。如果在子线程中有关于UI的操作,那么就需要把数据消息作为一个Message对象发送到Handler的消息队列中,然后,有Handler中的handlerMessge方法处理传过来的数据信息,并操作UI。当然,Handler对象是在主线程中初始化的,以为它需要绑定在主线程的消息队列中。

    类sendMessage(Message msg)方法实现发送消息的操作。在初始化Handler对象时重写的handleMessage方法来接收Messgae并进行相关操作:

      1 //Handler处理子线程消息代码示例
      2 public class Activity01 extends Activity
      3 {
      4     //声明ProgressBar对象
      5     private ProgressBar m_ProgressBar;
      6     private ProgressBar m_ProgressBar2;
      7     private Button mButton01;
      8     protected static final int GUI_STOP_NOTIFIER = 0x108;
      9     protected static final int GUI_THREADING_NOTIFIER = 0x109;
     10     
     11     public int intCounter=0;
     12     
     13     @Override
     14     public void onCreate(Bundle savedInstanceState)
     15     {
     16         super.onCreate(savedInstanceState);
     17         //设置窗口模式,,因为需要显示进度条在标题栏
     18         requestWindowFeature(Window.FEATURE_PROGRESS);
     19         setProgressBarVisibility(true);
     20         setContentView(R.layout.main);
     21         //取得ProgressBar
     22         m_ProgressBar = (ProgressBar) findViewById(R.id.ProgressBar01);
     23         m_ProgressBar2= (ProgressBar) findViewById(R.id.ProgressBar02);
     24         mButton01 = (Button)findViewById(R.id.Button01);
     25         m_ProgressBar.setIndeterminate(false);
     26         m_ProgressBar2.setIndeterminate(false);
     27         //当按钮按下时开始执行,
     28         mButton01.setOnClickListener(new Button.OnClickListener()
     29         {
     30             @Override
     31             public void onClick(View v)
     32             {
     33                 // TODO Auto-generated method stub
     34                 //设置ProgressBar为可见状态
     35                 m_ProgressBar.setVisibility(View.VISIBLE);
     36                 m_ProgressBar2.setVisibility(View.VISIBLE);
     37                 //设置ProgressBar的最大值
     38                 m_ProgressBar.setMax(100);
     39                 //设置ProgressBar当前值
     40                 m_ProgressBar.setProgress(0);
     41                 m_ProgressBar2.setProgress(0);
     42                 //通过线程来改变ProgressBar的值
     43                 new Thread(new Runnable() {
     44                     public void run()
     45                     {
     46                         for (int i = 0; i < 10; i++)
     47                         {
     48                             try
     49                             {
     50                                 intCounter = (i + 1) * 20;
     51                                 Thread.sleep(1000);
     52                                 if (i == 4)
     53                                 {
     54                                     Message m = new Message();
     55                                     m.what = Activity01.GUI_STOP_NOTIFIER;
     56                                     Activity01.this.myMessageHandler.sendMessage(m);
     57                                     //将message发送到消息队列
     58                                     break;
     59                                 }
     60                                 else
     61                                 {
     62                                     Message m = new Message();
     63                                     m.what = Activity01.GUI_THREADING_NOTIFIER;
     64                                     Activity01.this.myMessageHandler.sendMessage(m);
     65                                     //将message发送到消息队列
     66                                 }
     67                             }
     68                             catch (Exception e)
     69                             {
     70                                 e.printStackTrace();
     71                             }
     72                         }
     73                     }
     74                 }).start();
     75             }
     76         });
     77     }
     78 
     79     //通过匿名类复写Handler类中的handleMessage方法,用于接收传递到消息队列中的Message,并进行UI操作。
     80     Handler myMessageHandler = new Handler()
     81     {
     82     // @Override
     83         public void handleMessage(Message msg)
     84         {
     85             switch (msg.what)
     86             {
     87             //ProgressBar已经是对大值
     88             case Activity01.GUI_STOP_NOTIFIER:
     89                     m_ProgressBar.setVisibility(View.GONE);
     90                     m_ProgressBar2.setVisibility(View.GONE);
     91                     Thread.currentThread().interrupt();
     92                     break;
     93             case Activity01.GUI_THREADING_NOTIFIER:
     94                     if (!Thread.currentThread().isInterrupted())
     95                     {
     96                         // 改变ProgressBar的当前值
     97                         m_ProgressBar.setProgress(intCounter);
     98                         m_ProgressBar2.setProgress(intCounter);
     99                         // 设置标题栏中前景的一个进度条进度值
    100                         setProgress(intCounter*100);
    101                         // 设置标题栏中后面的一个进度条进度值
    102                         setSecondaryProgress(intCounter*100);//
    103                     }
    104                     break;
    105             }
    106             super.handleMessage(msg);
    107         }
    108     };
    109 }

    以上的例子中,子线程只是对进度条的参数进行了变更,并将结果以message形式发送到消息队列中去,子线程的内部并未进行UI操作,而是在重写的Handler的handlerMessage方法中操作了UI界面。

    (3)传递Runnable对象。用于通过Handler绑定的消息队列,安排不同操作的执行顺序。

      Handler对象在进行初始化的时候,会默认的自动绑定消息队列。利用类post方法,可以将Runnable对象发送到主线程的消息队列中,按照队列的机制按顺序执行不同的Runnable对象中的run方法。

     1 public class HandlerActivity extends Activity {
     2     //声明两个按钮控件
     3     private Button startButton = null;
     4     private Button endButton = null;
     5     
     6     @Override
     7     public void onCreate(Bundle savedInstanceState) {
     8         super.onCreate(savedInstanceState);
     9         setContentView(R.layout.main);
    10         //根据控件的ID得到代表控件的对象,并未这两个按钮设置相应的监听器
    11         startButton = (Button)findViewById(R.id.startButton);
    12         startButton.setOnClickListener(new StartButtonListener());
    13         endButton = (Button)findViewById(R.id.endButton);
    14         endButton.setOnClickListener(new EndButtonListener());
    15     }
    16     
    17     class StartButtonListener implements OnClickListener{
    18         @Override
    19         public void onClick(View v) {
    20         //调用Handler的post方法,将要执行的线程对象添加到队列当中
    21         handler.post(updateThread);
    22         }
    23     }
    24     
    25     class EndButtonListener implements OnClickListener{
    26         @Override
    27         public void onClick(View v) {
    28         handler.removeCallbacks(updateThread);
    29         }
    30     }
    31     
    32     //创建一个Handler对象
    33     Handler handler = new Handler();
    34     //将要执行的操作写在线程对象的run方法当中
    35     Runnable updateThread = new Runnable(){
    36         @Override
    37         public void run() {
    38             System.out.println("UpdateThread");
    39             //在run方法内部,执行postDelayed或者是post方法
    40             handler.postDelayed(updateThread, 3000);
    41         }
    42     };
    43 }

    程序的运行结果就是每隔3秒钟,就会在控制台打印一行UpdateTread。这是因为实现了Runnable接口的updateThread对象进入了空的消息队列即被立即执行run方法,而在run方法的内部,又在3000ms之后将其再次发送进入消息队列中。

    注意:

      post方法虽然发送的是一个实现了Runnable接口的类对象,但是它并非创建了一个新线程,而是执行了该对象中的run方法。也就是说,整个run中的操作和主线程处于同一个线程

    (4)多线程中传递Runnable对象

      这样对于那些简单的操作,似乎并不会影响。但是对于耗时较长的操作,当它被加入到消息队列中之后执行会占用很长的时间,以至于处于同一线程的其他操作无法继续执行,就会出现“假死”。为了解决这个问题,就需要使得handler绑定到一个新开启线程的消息队列上,在这个处于另外线程的上的消息队列中处理传过来的Runnable对象和消息。

    具体操作方法如下:

     1 public class HandlerTest2 extends Activity {
     2     @Override
     3     protected void onCreate(Bundle savedInstanceState) {
     4         // TODO Auto-generated method stub
     5         super.onCreate(savedInstanceState);
     6         setContentView(R.layout.main);
     7         //打印了当前线程的ID
     8         System.out.println("Activity-->" + Thread.currentThread().getId());
     9         
    10         //生成一个HandlerThread对象
    11         HandlerThread handlerThread = new HandlerThread("handler_thread");
    12         
    13         //在使用HandlerThread的getLooper()方法之前,必须先调用该类的start(),同时开启一个新线程;
    14         handlerThread.start();
    15         
    16         //将由HandlerThread获取的Looper传递给Handler对象,即由处于另外线程的Looper代替handler初始化时默认绑定的消息队列来处理消息。——关键是这句
    17         MyHandler myHandler = new MyHandler(handlerThread.getLooper());
    18         Message msg = myHandler.obtainMessage();
    19         
    20         //将msg发送到目标对象,所谓的目标对象,就是生成该msg对象的handler对象
    21         Bundle b = new Bundle();
    22         b.putInt("age", 20);
    23         b.putString("name", "Jhon");
    24         msg.setData(b);
    25         msg.sendToTarget(); //将msg发送到myHandler
    26     }
    27     
    28     //定义类,这里只是定义MyHandler一个类,并未和新线程相关起来
    29     class MyHandler extends Handler{
    30         public MyHandler(){
    31         }
    32         public MyHandler(Looper looper){
    33             super(looper);
    34         }
    35         
    36         @Override
    37         public void handleMessage(Message msg) {
    38             Bundle b = msg.getData();
    39             int age = b.getInt("age");
    40             String name = b.getString("name");
    41             System.out.println("age is " + age + ", name is" + name);
    42             System.out.println("Handler--->" + Thread.currentThread().getId());
    43             System.out.println("handlerMessage");
    44         }
    45     }
    46 }

    这样,当使用sendMessage方法传递消息或者使用post方法传递Runnable对象时,就会把它们传递到与handler对象绑定的处于另外一个线程的消息队列中,它们将在另外的消息队列中被处理。而主线程还会在发送操作完成时候继续进行,不会影响当前的操作。

    这里需要注意,这里用到的多线程并非由Runnable对象开启的而是ThreadHandler对象开启的Runnable对象只是作为一个封装了操作的对象被传递,并未产生新线程

  • 相关阅读:
    JS 中的foreach和For in比较
    SQL 查询CET使用领悟
    .NET开源项目
    asp.net获取客户端IP方法(转载)
    jQuery Mobile 基础(第四章)
    jQuery Mobile 基础(第三章)
    jQuery Mobile 基础(第二章)
    机器学习笔记之梯度下降法
    特征脸是怎么提取的之主成分分析法PCA
    word2vec初探
  • 原文地址:https://www.cnblogs.com/hebao0514/p/4770426.html
Copyright © 2020-2023  润新知