6、Android线程间通信-Android强化课程笔记

1、多线程使用场景

1.1、线程与多线程

线程是程序中一个单一的顺序控制流程,而在单个程序中同时运行多个线程以完成不同的工作,称为多线程。

1.2、进程与线程

在linux中,进程和线程时类似的,线程是运行在进程里面的,一个App是运行在进程里面的。

1.3、生活中的多线程

如下图就能生动地展现多线程。
这里写图片描述

2、Android ANR的产生

2.1、ANR简介

  • ANR(Application Not Response)即应用程序无响应。一般会弹出对话框进行提示,这个时候可以选择等待,也可以选择强制关闭应用。
  • 出现ANR的原因:

    1. 系统繁忙
    2. app没有优化好
  • 正常情况下,一个流畅合理的应用程序是不能出现ANR的,否则用户体验对大打折扣,因此在程序里对响应性能的设计很重要。

  • 出现ANR的三种情况:

    1. 主要类型按键或触摸事件在特定事件(5秒)内无响应。
    2. BroadcastReceiver在特定时间(10秒)内无法处理完成。
    3. 小概率类型Service在特定的时间内无法处理完成。

2.2、代码演示

1.新建一个项目,在布局文件中添加一个按钮,给按钮添加一个单击事件,代码如下:

mButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                try {
                    Log.d(TAG, "onClick: start");
                    Thread.sleep(10000);
                    Log.d(TAG, "onClick: end");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

2.然后点击按钮,发现按钮点下去就没反应了这时候在屏幕上乱点几下,会弹出来一个窗口,如下所示,这就是应用出现了ANR。
这里写图片描述

拓展:在Android4.0之后的开发者选项中有一个ANR选项,如下图所示:
这里写图片描述

3、线程的两种实现方式

定义线程的方法:
1. 扩展java.lang.Thread类;
2. 实现Runnable接口

3.1、Thread类的使用方法

3.1.1、线程的状态及生命周期

  1. 创建(new)
  2. 就绪(runnable)
  3. 运行(running)
  4. 阻塞(blocked)、睡眠或等待一定的时间(time waiting)、waiting(等待被唤醒)
  5. 消亡(dead)
    1. 当需要一个线程来执行某个子任务时,就创建了一个线程。
    2. 但是线程只有满足需要的条件,才能进入就绪状态。
    3. 线程进入就绪状态后,不能马上获得CPU的执行时间
    4. 线程不能继续运行的原因:
      • 用户主动让线程休眠
      • 用户主动让线程等待
      • 被同步块阻塞
    5. 当由于突然中断或子任务执行完毕,线程就会消亡。
    6. 线程中常用方法:
      • start()方法:启动线程
      • run()方法:不需要用户调用,继承Thread类必须重写run()方法
      • sleep()方法:相当于让线程睡眠,交出CPU,让CPU去执行其他任务。但是有一点要非常注意,sleep方法不会释放锁,也就是说如果当前线程持有对某个对象的锁,则即使调用sleep方法,其他线程也无法访问这个对象。
      • yield()方法:调用yield方法会让当前线程交出CPU权限,让CPU去执行其他线程,它跟sleep方法类似,同样不会释放锁,但是yield不能控制具体的交出CPU的时间,另外,yield方法只能让拥有相同优先级的线程有获取CPU执行时间的机会。
        注意,调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间,这一点是和sleep方法不一样的。
      • join()方法:
        join方法有三个重载版本:
        假如在主线程中,调用thread.join方法,则main方法会等待thread线程执行完毕或者等待一定的时间。如果调用的是无参join方法,则等待thread执行完毕;如果调用的是指定了时间参数的join方法,则等待一定的时间。

        join();
        join(long millis);//参数为毫秒
        join(long millis,int nanoseconds);//第一个参数是毫秒,第二个参数是纳秒

      • interrupt()方法:顾名思义,即中断的意思。单独调用interrupt方法可以使得处于阻塞状态的线程抛出一个异常,也就说,它可以用来中断一个正处于阻塞状态的线程;另外,通过interrupt方法和isInterrupted()方法来停止正在运行的线程。
    7. 以下为线程的完整生命周期图:
      这里写图片描述

3.1.2、上下文切换

  1. CPU在一个时刻只能运行一个线程,当在运行一个线程的过程中去运行另一个线程时,叫做上下文切换。
  2. 在切换时,需要保存线程的状态。需要保存那些数据呢?
    • 程序计数器的值
    • CPU寄存器状态
      线程的上下文切换,实际上就是存储和恢复CPU状态的过程,它使得线程执行能够从中断点恢复执行。

3.1.3、Thread类的使用-模拟卖票

1.创建一个名为`Thread`的项目,新建一个名为`SaleTickets`类并让其继承自`Thread`类,重写其run()方法,添加一个票数的成员变量、构造方法及卖票的方法。
public class SaleTickets extends Thread {
    private static final String TAG = "Thread";
    private int mTickets = 0;

    public SaleTickets(int tickets) {
        this.mTickets = tickets;
    }

    @Override
    public void run() {
        super.run();
        while (mTickets > 0) {
            saleTicket();
        }
        Log.d(TAG, Thread.currentThread().getName() + "票卖完了");
    }

    //卖票
    private void saleTicket() {
        try {
            Thread.sleep(1000);//休眠1秒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        mTickets--;
        Log.d(TAG, Thread.currentThread().getName()+"卖了1张票,还剩" + mTickets + "张票");
    }
}
2.在xml布局文件中添加一个按钮,在MainActivity中为按钮添加事件,开启一个线程,这里模拟开启一个窗口卖票。
public class MainActivity extends AppCompatActivity {
    private Button mBtnStartWork;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mBtnStartWork = findViewById(R.id.btnStartWork);
        startSaleTickets();
    }

    private void startSaleTickets() {
        mBtnStartWork.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                SaleTickets thread1 = new SaleTickets(5);
                thread1.start();
            }
        });
    }
}
3.运行程序,点击按钮,查看日志打印结果,符合预期。 ![这里写图片描述](https://img-blog.csdn.net/20180727205352765?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2NoYWl4aW5nc2k=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70) 4.接下来,我们将线程增加至四条,修改`startSaleTickets`方法。
    private void startSaleTickets() {
        mBtnStartWork.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                SaleTickets thread1 = new SaleTickets(3);
                SaleTickets thread2 = new SaleTickets(4);
                SaleTickets thread3 = new SaleTickets(5);
                SaleTickets thread4 = new SaleTickets(6);
                thread1.start();
                thread2.start();
                thread3.start();
                thread4.start();
            }
        });
    }
5.再次运行程序,日志打印结果如下。 ![这里写图片描述](https://img-blog.csdn.net/20180727205406195?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2NoYWl4aW5nc2k=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70)

3.1.4、Thread类的使用-join方法

1.修改`startSaleTickets`方法,代码如下:
private void startSaleTickets() {
        mBtnStartWork.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                SaleTickets thread1 = new SaleTickets(5);
                thread1.start();
                try {
                    Log.d(TAG, "Wait thread done!");
                    thread1.join();
                    Log.d(TAG, "Join returned!");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
    }
2.运行程序,发现按钮点击后并没有回弹,直至任务执行完毕,按钮才弹回来(`具体原因参考上面join方法的介绍`),而日志打印结果也一致。 ![这里写图片描述](https://img-blog.csdn.net/2018072720542296?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2NoYWl4aW5nc2k=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70)

其他方法的介绍和具体的代码演示请参考这篇博客:Java Thread 的使用

3.2、实现Runnable接口

1.新建一个名为`Thread2`的项目,新建一个名为`SaleTickets`的类并实现`Runnable`接口,并实现其run方法,具体代码请参考上面项目中`SaleTickets`的类的代码,代码是一样的。 2.在MainActivity中代码中使用这个类实现模拟卖票。
public class MainActivity extends AppCompatActivity {
    private Button mBtnStartWork;
    private SaleTickets mSaleTickets;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mBtnStartWork = findViewById(R.id.btnStartWork);
        mSaleTickets = new SaleTickets(10);
        mBtnStartWork.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                new Thread(mSaleTickets, "队伍1").start();
            }
        });
    }
}
3.运行代码,查看日志打印结果。 ![这里写图片描述](https://img-blog.csdn.net/2018072720543491?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2NoYWl4aW5nc2k=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70)

4、线程间的通信

线程间通信的相关组件有:Handler、Looper、MessageQueue、Thread。

4.1、职责与关系

4.1.1、职责

  1. Message:消息,其中包含了消息ID,消息处理对象以及处理的数据等。Message由MessageQueue统一列队,终由Handler处理。
  2. Handler:处理者,负责Message的发送及处理。使用Handler,我们需要实现handleMessage(Message msg)方法,以对特定的Message进行处理。
  3. MessageQueue:消息队列,用来存放Handler发送过来的消息,并按照先进先出(FIFO,即first in first out)的规则执行。
  4. Looper:消息泵,不断地从MessageQueue中抽取Message执行。一个MessageQueue需要一个Looper。
  5. Thead:线程,负责调度整个消息循环,即消息循环的执行场所。

4.1.2、关系

Handler、Looper和MessageQueue的关系如下图所示。

这里写图片描述

  • Handler、Looper和MessageQueue是简单的三角关系,Looper和MessageQueue是一一对应的。
  • 创建一个Looper的同时,会创建一个MessageQueue。
  • 多个Handler可以共用同一个Looper和MessageQueue,这样的话,这些Handler就运行在同一个线程里了。

具体请参见博客:Android之消息机制Handler,Looper,Message解析

4.2、线程与更新

在主线程(UI线程)里,如果创建Handler时不传入Looper对象,那么将直接使用主线程的Looper对象(系统已经帮我们创建了);在其他线程里,如果创建Handler时不传入Looper对象,那么,这个Handler将不能接受处理消息,在这种情况下,通用的做法是:

    class LooperThread extends Thread {
        public Handler mHandler;

        @Override
        public void run() {
            super.run();
            Looper.prepare();
            mHandler=new Handler(){
                @Override
                public void handleMessage(Message msg) {
                    super.handleMessage(msg);
                }
            };
            Looper.loop();
        }
    }

即在创建Handler之前,为该线程准备好一个Looper(Looper.prepare),然后让这个Looper跑起来(Looper.loop),抽取Message,这样,Handler才能正常工作。
因此,Handler处理消息总是在创建Handler的线程里运行,而我们在消息处理中,不乏更新UI的操作,不正确的线程直接更新UI将引发异常,因此,需要时刻关心Handler在哪个线程里创建的。Handler既是消息的发起者,又是消息的处理者。

那么如何更新UI才能不出现异常呢?

在SDK中提供了4种方式可以从其他线程访问UI线程:

  1. Activity.runOnUiThread(Runnable)
  2. View.post(Runnable)
  3. View.postDelayed(Runnable,long)
  4. Handler
    不确定当前线程时,更新UI时尽量调用post方法。

猜你喜欢

转载自blog.csdn.net/chaixingsi/article/details/81226042
今日推荐