51dev.com IT技术开发者社区

51dev.com 技术开发者社区

Android学习笔记040之Handler

Android互联网报道阅读(17)2019-09-18 收藏0次评论

android开发中我们常常会用到多线程,但是进行UI界面的更新只能在UI线程,而请求网络获取数据不能在UI线程,这就涉及到了线程之间的通信问题,Android系统给我提供了一个线程间通信的解决办法–Handler,下面我们来介绍一下Handler:

1、Handler简介

Handler是Android操作系统提供的线程通信工具,它主要有两个作用:将消息或者Runnable在主线程中的某个地方执行;安排一个动作在另外一个线程中执行。每一个Handler对象都会维护两个队列:消息队列和Runnable队列,都是由Android操作系统提供。Handler可以通过消息队列来发送、接收、处理消息;通过Runnable队列来启动、结束、休眠线程。也就是说,Handler可以很方便的实现线程之间的通信。

2、Handler机制

Handler机制产生的原因:一个APP启动之后就会自动创建一个UI线程来管理UI控件和处理对事件的分发,当界面上的一个控件,比如Button响应了点击事件之后,我们需要进行长时间的耗时操作,比如联网获取数据,但是Android的机制是在UI线程中5秒内没有响应就会ANR异常,所以我们需要将耗时操作放在子线程中处理,子线程处理完成之后去更新UI线程的界面,但是,我们知道UI线程是不安全的,所以子线程不能直接更新UI线程中的界面,所以Android系统提供了Handler机制来解决这个问题。

Handler运行在UI线程,它通过接受子线程传递过来的Message更新UI线程的控件。有几个核心类:Message、Looper、Handler、MessageQueue,下面我们来学习一下这几个核心类:

2.1、Message

Message消息类,主要实现对消息的封装和指定对消息的操作形式。Message定义的变量和方法有:

public int what:变量,用于定义此Message属于何种操作

public Object obj:变量,用于定义此Message传递的信息数据,通过它传递信息

public int arg1:变量,传递一些整型数据时使用

public int arg2:变量,传递一些整型数据时使用

常用的方法:

copyFrom(Message o):复制一个消息

getCallback():获取当前消息执行时的回调对象

getData():获取在Message中传递的Bundle对象

getTarget():获取到处理当前消息的Handler对象

getWhen():获取到处理当前消息的时间,单位是毫秒

obtain(Handler h, int what, int arg1, int arg2, Object obj):获取到Message对象,重载方法有:obtain(Handler h, int what, Object obj)、obtain(Handler h, int what)、obtain(Handler h)、obtain(Handler h, Runnable callback)、obtain()、obtain(Handler h, int what, int arg1, int arg2)、obtain(Message orig)

setData(Bundle data):传递一个Bundle对象

setTarget(Handler target):对应getTarget()

recycle():将一个Message对象释放到消息池中

常用的Message的方法和变量就是这些了,不过使用的时候也是有一些注意事项的:

Message可以直接New出来,但是尽量使用obtain()方法从消息池中获取,节省资源

如果只是需要携带简单的int类型消息,推荐使用Message.arg1和Message.arg2,比Bundle节省内存

用Message.what来标志不同的消息,以便处理,关于Message就简单介绍到这里,附上Message的国内镜像API

2.2、Looper

Looper消息管道,Handler处理Message需要一个管道,Looper只有两个方法:prepare()和loop(),其中prepare()是初始化的方法。在Android的Activity中,系统帮我们启动了一个Looper对象,但是在我们自定义的类中,需要我们自己去启动一个Looper对象,只需要实现下面的代码,就可以将一个普通的线程变成Looper线程:

class LooperThread extends Thread {
  public Handler mHandler;
  public void run() {
        //将当前线程初始化为Looper线程
      Looper.prepare();

      ......
        //循环处理消息
      Looper.loop();
  }

 }

Looper是循环者的意思,Looper通过调用prepare()方法进行初始化,我们来分析一下prepare()方法:

public class Looper {
// 每个线程中的Looper对象其实是一个ThreadLocal,即线程本地存储(TLS)对象
private static final ThreadLocal sThreadLocal = new ThreadLocal();
// Looper内的消息队列
final MessageQueue mQueue;
// 当前线程
Thread mThread;
//其他属性
// 每个Looper对象中有它的消息队列,和它所属的线程
private Looper() {
    mQueue = new MessageQueue();
    mRun = true;
    mThread = Thread.currentThread();
}
// 我们调用该方法会在调用线程的TLS中创建Looper对象
public static final void prepare() {
    if (sThreadLocal.get() != null) {
        // 试图在有Looper的线程中再次创建Looper将抛出异常
        throw new RuntimeException("Only one Looper may be created per thread");
    }
    sThreadLocal.set(new Looper());
}
// 其他方法
}

Looper内部维护着一个MessageQueue对象,每一个Thread中只能有一个Looper对象。prepare()方法的核心功能就是将Looper对象定义为ThreadLocal,不明白ThreadLocal的可以去看一下这篇文章:理解ThreadLocal

调用了loop()方法,Looper才真正开始工作,loop()不断的从自己的MessageQueue中取出任务执行,我们看一下loop方法代码:

public static final void loop() {
    Looper me = myLooper();  //得到当前线程Looper
    MessageQueue queue = me.mQueue;  //得到当前looper的MQ


    Binder.clearCallingIdentity();
    final long ident = Binder.clearCallingIdentity();
    // 开始循环
    while (true) {
        Message msg = queue.next(); // 取出message
        if (msg != null) {
            if (msg.target == null) {
                // message没有target为结束信号,退出循环
                return;
            }
            // 日志。。。
            if (me.mLogging!= null) me.mLogging.println(
                    ">>>>> Dispatching to " + msg.target + " "
                    + msg.callback + ": " + msg.what
                    );
            // 非常重要!将真正的处理工作交给message的target,即后面要讲的handler
            msg.target.dispatchMessage(msg);
            // 还是日志。。。
            if (me.mLogging!= null) me.mLogging.println(
                    "<<<<< Finished to    " + msg.target + " "
                    + msg.callback);

            // 下面没看懂,同样不影响理解
            final long newIdent = Binder.clearCallingIdentity();
            if (ident != newIdent) {
                Log.wtf("Looper", "Thread identity changed from 0x"
                        + Long.toHexString(ident) + " to 0x"
                        + Long.toHexString(newIdent) + " while dispatching to "
                        + msg.target.getClass().getName() + " "
                        + msg.callback + " what=" + msg.what);
            }
            // 回收message资源
            msg.recycle();
        }
    }
}

除了prepare()和loop()方法之外,Looper还提供了一些其他的方法:

myLooper():获取当前线程Looper对象

getThread():获取到当前Looper所属的线程

quit():退出Looper的循环

最后我们总结一下Looper:

每一个线程只能有一个Looper对象,而且Looper是一个ThreadLocal

Looper中维护着一个MessageQueue,loop()方法不断地从MessageQueue中取出任务执行

普通的线程可以通过Looper变成一个Looper线程

2.3、Handler

Handler消息处理类,Handler主要是往MessageQueue中添加消息和处理消息,但是只处理自己添加的消息。即是:通知MessageQueue它要执行一个任务,在取到自己的时候执行那个任务,这个过程是异步的。这里我们需要了解一下同步和异步的概念,举一个简单的例子:

张三跟李四打电话,张三想要再跟王五通话,那么就必须要等到跟李四结束通话才可以。这个就是同步通信,就是发送一个请求,等待这个请求完成返回结果才会继续处理下面的通信。

张三同时给李四和王五发邮件,发送完成之后,不需要等待李四和王五读完邮件就可以去做其他的事。这个就是异步,简单的说就是:异步就是发送一个请求,但是不需要阻塞等待这个请求完成,这个请求完成之后,通过回调通知调用者这个请求的结果。

Handler可以直接New出来,也可以通过自定义类继承Handler实现。Handler创建的时候就会默认关联一个Looper对象,默认构造关联的是当前的Looper,但是可以通过set关联Looper。一个线程可以有多个Handler,但是只能有一个Looper。创建完成Handler之后,我们就可以进行接收和发送消息:

Handler发送消息:Handler提供的发送消息的方法有:

post(Runnable)

postAtTime(Runnable, long)

postDelayed(Runnable, long)

sendEmptyMessage(int)

sendMessage(Message)

sendMessageAtTime(Message, long)

sendMessageDelayed(Message, long)

通过上面这些方法,Handler可以向MessageQueue中发送消息。msg.target就是Handler对象,这样可以保证Looper处理到该Message的时候可以获取到该Handler;post发出的Message的Callback是Runnable对象。

Handler读取消息:Handler读取消息主要通过两个方法dispatchMessage(Message msg)与handleMessage(Message msg)。其中handleMessage(Message msg)和Runnable对象的run方法由开发者实现逻辑,Handler读取消息有两个特点:

Handler可以在任意线程发送消息,这些消息会存放在MessageQueue中

Handler是在它关联的Looper中处理消息的,

Handler解决了Android开发中子线程不能更新UI的问题,Android中UI线程也是一个Looper线程,在主线程中创建的Handler会默认关联该Looper。Looper在Android中应用很多,Handler可以在主线程中创建,然后传递给工作线程,工作线程获取到主线程的Handler引用之后,在工作线程完成任务可以传递消息给主线程更新UI。下面我们通过例子体会一下Handler的使用吧:

3、Handler的使用

Handler可以在UI线程中创建,也可以在子线程中创建,不过,在子线程中创建需要我们自己去创建一个Looper对象,下面我们实现一下这两种使用:

3.1、在UI线程中使用

在UI线程中创建Handler不需要我们自己去创建一个Looper,所以使用非常简单。

public class ProgressDialogActivity extends AppCompatActivity {
private Button btn_circle_progress;
private Button btn_progress_01;
private ProgressDialog progressDialog;
private int maxProgress = 100;
private int progress = 0;
Handler handler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);
        if (msg.what == 111) {
            progressDialog.setProgress(progress);
        }
        if (progress >= maxProgress) {
            progressDialog.dismiss();
        }
    }
};

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_progress_dialog);
    btn_circle_progress = (Button) findViewById(R.id.btn_circle_progress);
    btn_circle_progress.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            //通过show方法创建一个进度条对话框
            ProgressDialog.show(ProgressDialogActivity.this, "通讯中", "正在通讯,请稍候...", false, true);
        }
    });
    btn_progress_01 = (Button) findViewById(R.id.btn_progress_01);
    btn_progress_01.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            //通过new方法创建一个对话框
            progressDialog = new ProgressDialog(ProgressDialogActivity.this);
            //设置进度条的最大值
            progressDialog.setMax(maxProgress);
            //设置标题
            progressDialog.setTitle("正在下载");
            //设置提示信息内容
            progressDialog.setMessage("安装包正在下载,马上就好...");
            //设置是否允许点击进度条对话框外面取消对话框
            progressDialog.setCancelable(false);
            //设置进度条的样式,这个为长方形
            progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
            //设置是否显示进度,false显示
            progressDialog.setIndeterminate(false);
            //将进度条对话框show出来
            progressDialog.show();
            //新建一个线程更新进度
            new Thread(new Runnable() {
                @Override
                public void run() {
                    while (progress < maxProgress) {
                        //模拟耗时方法
                        sleep();
                        progress += 2;
                        handler.sendEmptyMessage(111);
                    }
                }
            }).start();
        }
    });
}

private void sleep() {
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}
}

通过Handler发送消息去更新进度条,实现效果如下:

3.2、在子线程中使用

在子线程中使用需要我们自己创建一个Looper,流程是:

直接调用Looper.prepare()方法就可以为当前线程创建Looper对象

创建Handler对象,重写handleMessage()方法处理消息

调用Looper.loop()方法启动Looper

下面是具体实现的例子:

这里实现的一个非常简单的例子,点击按钮,发送消息,显示一个吐司,下面是实例代码:

package com.example.webservicedemo;

import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Toast;

/**
 * Created by Devin on 2016/8/9.
 */
public class SonHandlerActivity extends AppCompatActivity {

private HandThread mThread;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_son);

    mThread = new HandThread();
    mThread.start();
    findViewById(R.id.btn_show_toast).setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            mThread.mHandler.sendEmptyMessage(0001);
        }
    });
}

class HandThread extends Thread {
    public Handler mHandler;

    @Override
    public void run() {
        super.run();
        //调用prepare()方法,j为当前线程创建Looper对象
        Looper.prepare();
        //创建Handler对象
        mHandler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
                if (msg.what == 0001) {
                    Toast.makeText(SonHandlerActivity.this, "显示一个吐司", Toast.LENGTH_SHORT).show();
                }
            }
        };
        //调用Looper.loop()启动Looper
        Looper.loop();
    }
}
}

实现效果图:

关于Handler使用就简单介绍到这里,下面我们需要处理一个非常严重的问题:Handler内存泄漏问题

4、Handler内存泄漏问题

Java使用有向图机制,通过GC自动检查内存中的对象,当一个对象没有被任何引用指向的时候,GC会回收该对象,但是,当该对象有外部引用的时候,GC就不会回收。

Android中使用Handler很多都是内部类创建Handler,这样创建Handler对象会隐式地持有一个外部类对象(例如Activity)的引用,而Handler常常会有一个耗时的后台线程,这个后台线程执行完任务之后,通过消息机制通知Handler更新界面。但是有这样一种情况:在后台线程操作的时候,用户关闭了对应的Activity,但是,该后台线程持有Handler,Handler又持有Activity,这样就会导致该Activity不会被GC回收,这样就会导致内存泄漏。另外,如果你执行了Handler的postDelayed()方法,该方法会将你的Handler装入一个Message,并把这条Message推到MessageQueue中,那么在你设定的delay到达之前,会有一条MessageQueue -> Message -> Handler -> Activity的链,导致你的Activity被持有引用而无法被回收。

内存泄漏会导致虚拟机占用内存过高,导致OOM(内存溢出),对于一个Android应用来说,这样容易导致程序占用内存超过系统限制,从而导致程序崩溃。

使用Handler内存泄漏的解决办法:

第一种方法:

1.在关闭Activity的时候停掉你的后台线程。线程停掉了,就相当于切断了Handler和外部连接的线,Activity自然会在合适的时候被回收。

2.如果你的Handler是被delay的Message持有了引用,那么使用相应的Handler的removeCallbacks()方法,把消息对象从消息队列移除就行了。

第二种方法:

将Handler声明成静态,这样不会被外部持有。但是这样就会出现无法更新UI的问题,解决办法就是:在Handler中添加对Activity的弱引用,具体代码如下:

static class MyHandler extends Handler {
WeakReference mActivityReference;

MyHandler(Activity activity) {
    mActivityReference= new WeakReference(activity);
}

@Override
public void handleMessage(Message msg) {
    final Activity activity = mActivityReference.get();
    if (activity != null) {
        mImageView.setImageBitmap(mBitmap);
    }
}
}

这样我们就可以操作Activity中的对象,Handler也不会被外部对象所持有,解决了非静态内部对象被外部对象持有导致的内存泄漏问题。

关于WeakReference弱引用:我们所说的弱引用,与强引用像对。GC在回收时会忽略掉弱引用,即就算有弱引用指向某对象,但只要该对象没有被强引用指向(实际上多数时候还要求没有软引用,但此处软引用的概念可以忽略),该对象就会在被GC检查到时回收掉。对于上面的代码,用户在关闭Activity之后,就算后台线程还没结束,但由于仅有一条来自Handler的弱引用指向Activity,所以GC仍然会在检查的时候把Activity回收掉。这样,内存泄露的问题就不会出现了。

Handler就介绍到这里了

以上就是Android学习笔记040之Handler的全部内容,请多关注【51DEV】IT技术开发者社区。