安卓开发之Handler机制学习

我们知道Android为了线程安全,并不允许我们在UI线程外直接更新UI(多线程并发操作更新很可能导致安全问题),不能在UI线程中执行耗时操作(UI线程超过5s没有响应用于请求会导致ANR),也不能在UI线程中执行网络操作(很可能耗时)。因此我们想要实现UI界面更新可以通过Handler来通知UI组件更新,也可以直接使用runOnUiThread()来更新,同时,Android官方也提供了AsyncTask这个封装好的轻量级异步任务类。因此,分别来学习下这几种方式。当然,平常使用中更多的肯定会用一些成熟的第三方框架,以后有机会再学习。

线程与进程

学习操作系统的时候,我们知道进程和线程的主要区别是:
进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位。
系统在运行的时候会为每个进程分配不同的内存空间,因此进程都有独立的代码和数据空间(程序上下文),进程之间的切换开销较大,而系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
一个进程可包含多个线程。

Android OS也是如此:参考博文
应用程序(Application):为了完成特定任务,用某种语言编写的一组静态代码。
进程(Process) :一个运行中的程序可以看作是一个进程,是Android OS调度与资源分配的基本单位,操作系统会为每个进程分配一
段内存空间,程序依次执行代码加载 -> 执行 -> 执行完毕的流程。
线程(Thread):比进程更小的执行单元, 一个进程可包含多个线程,线程需要放在一个进程中才能执行。线程是由程序负责管理
的,而进程则是由系统进行调度的。
多线程(Multithreading):并行地执行多条指令,将CPU的时间片按照调度算法,轮流分配给各个线程,由于时间很短,用户并不会
感觉到OS在执行多个任务,但是实际上是在执行多任务,只不过分时执行。

Handler在UI线程中机制概述

我们这里主要关心的是Handler在UI线程中的情况,在非UI线程的子线程情况之后再讨论,执行流程图大致如下:
在这里插入图片描述
**UI线程:**主线程,系统在创建UI线程的时候会初始化一个Looper对象,同时也会创建一个与其关联的MessageQueue(消息队列)。

**Handler:**作用就是发送与处理消息(包括Message或者Runnable对象)。如果希望Handler正常工作,在当前线程中要有一个Looper对象,UI线程由于默认初始化一个Looper对象,因此可以直接使用,并且会关联到主线程的MessageQueue中;但是如果Handler是在非UI线程的子线程中,就得自己创建一个Looper对象,并且会自动创建一个与其关联的MessageQueue。
通俗点说,通过它可以很轻松的将一个子线程任务切换到Handler所在的线程中去执行(也就是在子线程中发送消息,在UI线程中获取并处理消息)。按照这种说法,如果Handler在UI线程中,那么就可以通过Handler切换到UI线程(当然,切换到UI线程之后,UI线程不能做的事例如网络请求、耗时操作等,Handler也不能处理),这样就可以更新UI了。
上面说到Handler可以发送的消息包括Message或者Runnable对象,那么Handler的发送方法(发送的结果就是将消息插入消息队列中)就有两种,有Delay的一般都带有延迟操作,后面会具体分析怎么延迟:
1)Post:通过Post把一个Runnable对象入队到消息队列中。它的方法有:post(Runnable)、postAtTime(Runnable,long)、postDelayed(Runnable,long),post本质实际上调用的其实还是底下2)中的sendxxx方法。
2)sendMessage:通过sendMessage把一个包含消息数据的Message对象插入到消息队列中。它的方法有:sendEmptyMessage(int)、sendMessage(Message)、sendMessageAtTime(Message,long)、sendMessageDelayed(Message,long)。

Message:Handler接收与处理的消息,可以是简单的数据,也可以是复杂的对象等。 当发送Message对象时,可以有两种方法:
1)借助Message的setData(Bundle budle)与getData(Bundle budle)方法,我们之前在学习活动之间的数据通信以及序列化知识时学习过,Bundle不仅提供了getXXX(key)与putXXX(key,value)方法传递简单的数据,还提供了putParcelable(key,value)、putSerializable(key,value)的方法传递对象。那么就可以通过这种方法发送消息:Message msg=Message.obtain();msg.setData(bundle);
sendMessage(msg)。
2)借助Message的几个属性完成。Message自带了几个属性:
int arg1:参数一,用于传递不复杂的数据,复杂数据使用方法1)中setData()传递。
int arg2:参数二,用于传递不复杂的数据,复杂数据使用方法1)中setData()传递。
Object obj:传递一个任意的对象。
int what:定义的消息码,一般用于设定消息的标志。
因此这里就可以直接Message.obj =XXX或者Message.what=XXX。我们看sendMessage的两个方法sendEmptyMessage(int)和sendMessage(Message)。假如我们要发送消息标志为0x123的消息,分别使用这两种方法就可以写成:sendEmptyMessage(0x123)以及
Message msg=Message.obtain(); msg.what = 0x123; sendMessage(msg)。

Runnable :Runnable对象的run()方法的代码,就执行在UI线程上。

**MessageQueue:**消息队列,用来存放Handler发送过来的消息,内部通过单链表的数据结构来维护消息列表。Looper先进先出管理
Message,在初始化Looper对象时会创建一个与之关联的MessageQueue。

**Looper:**每个线程只能够有一个Looper,管理MessageQueue,不断地从中取出Message分发给对应的Handler处理。

Handler在UI线程中Demo

Demo参考plokmju博文

发送消息为Runnable时

无网络请求的情况:
首先定义布局文件,1个textview和两个按钮:
在这里插入图片描述
修改MainAvtivity代码,在子线程中使用post和postDelayed两种方式发送Runnable对象,Runnable对象的run()方法的代码,就执行在UI线程上:
在这里插入图片描述
效果如下,点击post按钮:
在这里插入图片描述
点击postDelayed按钮,延迟3s后:
在这里插入图片描述
这里看到postDelayed确实延迟了3s后更新了UI,我们知道,发送消息实质上是插入了消息队列中,然后looper不断地从MessageQueue中取出消息分发给对应的Handler处理。这里再次强调消息队列的本质是单链表而非栈,那么postDelayed延迟时间更新是怎么实现的呢?为什么Runnable对象的run()方法的代码或者说这个例子中没有使用的sendxxx的handleMessage方法,就执行在UI线程上?参考AndyJennifer 博文

扫描二维码关注公众号,回复: 11506535 查看本文章

postDelayed本质上调用的是sendMessageDelayed方法:

public final boolean postDelayed(Runnable r, long delayMillis)
    {
        return sendMessageDelayed(getPostMessage(r), delayMillis);
    }

而sendMessageDelayed实际上又调用了sendMessageAtTime方法,这里注意第二个参数是SystemClock.uptimeMillis()相对时间而不是System.currentTimeMillis()绝对时间,因为在handler受到阻塞,挂起状态,睡眠等状况下,这些时候是不应该执行handler 的;但是如果使用绝对时间的话,就会抢占资源来执行当前handler的内容,这就存在问题了。sendMessageDelayed方法如下:

public final boolean sendMessageDelayed(Message msg, long delayMillis)
    {
        if (delayMillis < 0) {
            delayMillis = 0;
        }
        return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
    }

sendMessageAtTime方法如下,又调用了enqueueMessage方法,enqueueMessage方法是MessageQueue用来进行插入操作的,对应的读取并删除操作的是next方法:

public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
        MessageQueue queue = mQueue;
        if (queue == null) {
            RuntimeException e = new RuntimeException(
                    this + " sendMessageAtTime() called with no mQueue");
            Log.w("Looper", e.getMessage(), e);
            return false;
        }
        return enqueueMessage(queue, msg, uptimeMillis);
    }

在sendMessageAtTime方法中就发现,postDelayed最终通过enqueueMessage方法直接将消息插入到消息队列中了,并且将延迟时间作为参数也传了进去。而不是延迟了 延迟时间后再进行插入的。这里虽然直接插入了,但是插入的位置肯定和延迟时间有关。继续查看enqueueMessage方法:

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
        msg.target = this;//设置message.target为当前Handler对象
        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);//获取当前MessageQueue.将消息加入队列中
    }

msg.target被复制为当前的Hanlder对象,继续查看MessageQueue的enqueueMessage

boolean enqueueMessage(Message msg, long when) {
        if (msg.target == null) {
            throw new IllegalArgumentException("Message must have a target.");
        }
        if (msg.isInUse()) {
            throw new IllegalStateException(msg + " This message is already in use.");
        }

        synchronized (this) {
            //如果当前消息循环已经结束,直接退出
            if (mQuitting) {
                IllegalStateException e = new IllegalStateException(
                        msg.target + " sending message to a Handler on a dead thread");
                Log.w(TAG, e.getMessage(), e);
                msg.recycle();
                return false;
            }

            msg.markInUse();
            msg.when = when;//传入的延迟时间
            Message p = mMessages;//mMessages是头部消息
            boolean needWake;//是否需要唤醒
            //如果队列中没有消息,或者当前进入的消息比消息队列中的消息等待时间短,那么就放在消息队列的头部
            if (p == null || when == 0 || when < p.when) {
                //如果阻塞则唤醒消息队列
                msg.next = p;//单链表操作
                mMessages = msg;//将传入的msg作为头部消息
                needWake = mBlocked;//如果当前的队列是出于阻塞的状态,那么mBlocked就会为true
            } else {
                //判断唤醒条件
                needWake = mBlocked && p.target == null && msg.isAsynchronous();
                Message prev;
                //循环遍历消息队列,把当前进入的消息放入合适的位置(比较等待时间)
                for (;;) {
                    prev = p;
                    p = p.next;
                    if (p == null || when < p.when) {
                        break;
                    }
                    if (needWake && p.isAsynchronous()) {
                        needWake = false;
                    }
                }
                //将消息插入合适的位置
                msg.next = p; 
                prev.next = msg;
            }

            //如果需要唤醒,调用nativeWake,结束等待
            if (needWake) {
                nativeWake(mPtr);
            }
        }
        return true;
    }

由上述方法可以看到,postDelayed最终是在插入消息队列中时使用到了延迟时间:在将消息插入到消息队列中时,如果当前消息的延迟时间比链表头部短或者消息队列中没有消息,就将这个消息插入到头部,反之循环遍历消息队列依次比较等待时间,把当前进入的消息放入合适的位置。头部对应的消息就是looper即将取出的消息。

唤醒的方式分为两种情况:
1)如果消息队列本来就为空,那么插入的这个消息就是头部消息,就需要唤醒线程来处理这个消息。
2)消息队列中有消息,但是链表头部消息执行时间大于当前时间,也就是时间还没到,这个时候线程就会阻塞以等待时间到了再执行,进入阻塞前,会把mBlocked标志位置为true。这时候假如我们插入的消息的延迟时间比头部小或者没有延迟时间,就插入头部,到时间或者立即唤醒当前线程处理消息。

那么接下来就看下looper的工作原理,UI线程中由于已经默认初始化一个Looper对象,因此这里就不用再创建Looper了,Looper中最重要的方法是loop,只有调用了loop后,消息系统才会真正起作用(也就是开启消息循环),loop方法如下:

 public static void loop() {
        final Looper me = myLooper();
        if (me == null) {
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
        final MessageQueue queue = me.mQueue;
        ......
        ......
        for (;;) {//死循环,获取消息队列中的消息
            Message msg = queue.next(); //调用next方法进行消息读取与删除,没有消息时next方法阻塞,导致loop方法也阻塞
            if (msg == null) {
                //唯一退出循环的方式就是next方法返回值为空
                return;
            }

        ......
        ......
            try {
             //获取消息后,执行发送消息的handler的dispatchMessage方法。
                msg.target.dispatchMessage(msg);
                end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
            } finally {
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }
         ......
         ......
            }

            msg.recycleUnchecked();
        }
    }

上面可以看到loop方法是个死循环,里面最重要的方法就是next()方法,而且唯一退出循环的方式就是next()方法返回值为空。当消息队列中没有消息时next方法会阻塞,导致loop方法也阻塞,如果有新消息了,就调用msg.target.dispatchMessage方法进行消息处理,这时已经是在执行Handler对象的方法了,可以说,在这里完成了线程的切换进行消息处理,也就可以回答为什么Runnable对象的run()方法的代码,就执行在UI线程上。
我们继续详细解析,next()方法如下:

Message next() {
     ......
     ......
     for (;;) {
         synchronized (this) {
          ......
          ......
         if (msg != null) {
           if (now < msg.when) {
                nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        //遍历消息列表,取出消息
                        mBlocked = false;
                        if (prevMsg != null) {
                            prevMsg.next = msg.next;
                        } else {
                            mMessages = msg.next;
                        }
                        msg.next = null;
                        if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                        msg.markInUse();
                        return msg;
                    }
                }
              ......
              ......
        }
      }
    }

next()方法会一直从MessageQueue中去获取消息,没有消息的话,next()方法一直会阻塞在这里,直到获取消息后返回这条消息并将其从单链表中删除。返回的消息会调用Handler对象的dispatchMessage方法,具体方法如下:

public void dispatchMessage(Message msg) {
        if (msg.callback != null) {//首先判断msg.callback是否为空
            handleCallback(msg);//不为空,走handleCallback
        } else {
            if (mCallback != null) {//第二步,判断Handler的callBack
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);//第三步,执行Handler的handleMessage方法
        }
    }

重点就在第一步,Message对象的callback是一个Runnable 对象,实际上就是Handler post方法所传递的Runnable参数。原因如下:
Handler使用post方法发送Runnable对象时,都会调用getPostMessage(Runnable r) 方法,且该方法都会将Runnable封装在Message对象的callback属性上。因此上面才说Message对象的callback是一个Runnable 对象。

  private static Message getPostMessage(Runnable r) {
        Message m = Message.obtain();
        m.callback = r;//Message对象的callback是一个Runnable 对象
        return m;
    }

如果Runnable对象参数不为空,就走handleCallback方法,handleCallback方法如下:

 private static void handleCallback(Message message) {
        message.callback.run();//看到了熟悉的run方法,也就是Runnable的run方法,这是消息的最终处理方法,且运行在Handler所在线程
    }

到这里是不是就看到了,最终消息的处理在先在loop方法中调用了Handler对象的dispatchMessage方法切换到Handler所在线程,这里是UI线程进行处理,由于Runnable对象不为空,就调用了handleCallback方法进行消息处理,最终handleCallback调用了Runnable的run方法中处理,这个run就是我们post方法的Runnable对象参数。

那么如果msg.callback为空,就走的是判断mCallback是否为空。通过Callback 可以采用如下方式来创建Handler 对象: Handler handler = new Handler(callback)。可以用来创建一个Handler的实例但并不需要派生Handler的子类。在日常开发中,创建Handler最常见的方式就是派生一个Handler 的子类并重写其handleMessage方法来处理具体的消息,而Callback给我们提供了另外一种使用Handler 的方式,当我们不想派生子类时,就可以通过Callback来实现。

public interface Callback {
        public boolean handleMessage(Message msg);
    }

上述两个都不满足时,最后调用Handler对象的handleMessage方法处理消息。这里就看到了大致原理就是从调用Handler对象的dispatchMessage方法切换线程到最终的Handler对象的handleMessage方法处理消息。那么接下来就看下sendxxx方法。

发送消息为Message时

有网络请求的情况:

学习下上节课的Bitmap加载,为了解决ARN问题,采用了Handler机制来异步下载,这里没有考虑OOM与ML的情况。

public class MainActivity extends AppCompatActivity {

     private Button button;
     private ImageView imageView;
     private ProgressDialog dialog;
     private static String image_path = "http://ww4.sinaimg.cn/bmiddle/786013a5jw1e7akotp4bcj20c80i3aao.jpg";

     private Handler handler = new Handler(){
         @Override
         public void handleMessage(@NonNull Message msg) {
                 Bitmap bitmap = (Bitmap) msg.obj;
                 imageView.setImageBitmap(bitmap);
                 dialog.dismiss();
             }
     };

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

        dialog = new ProgressDialog(this);
        dialog.setTitle("提示消息");
        dialog.setMessage("正在下载");
        dialog.setCancelable(false);

        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                new Thread(new dlThread()).start();
                dialog.show();
            }
        });
    }

    public class dlThread implements Runnable{

        @Override
        public void run() {
            OkHttpClient okHttpClient = new OkHttpClient();
            Request request = new Request.Builder().url(image_path).build();
            okHttpClient.newCall(request).enqueue(new Callback() {
                @Override
                public void onFailure(Call call, IOException e) {
                }
                @Override
                public void onResponse(Call call, Response response) throws IOException {
                    InputStream inputStream = response.body().byteStream();
                    Bitmap  bitmap = BitmapFactory.decodeStream(inputStream);
                    Message msg = Message.obtain();//推荐用obtain()获取Message
                    msg.obj = bitmap;
                    handler.sendMessage(msg);
                }
            });
        }
    }

    private void bindviews() {
        button = findViewById(R.id.button);
        imageView = findViewById(R.id.imageview);
    }
}

可以看到,最后是在Handler的handleMessage方法中处理消息的。最后别忘了加网络权限,安卓6之后要改为运行时权限,这里4.4就直接加了:
在这里插入图片描述
最终效果如下:
在这里插入图片描述

Handler在非UI线程中

UI线程中默认提供了Looper,如果我们在非UI线程中创建Handler时,就得自己创建Looper了,方法如下:
1 )直接调用Looper.prepare()方法即可为当前线程创建Looper对象,而它的构造器会创建配套的MessageQueue;
2 )创建Handler对象,重写handleMessage( )方法就可以处理来自于其他线程的信息了!
3 )调用Looper.loop()方法启动Looper

看一个Handler在非UI线程中简单的例子:

public class MainActivity extends AppCompatActivity {
    private Button button;
    private EditText editText;
    dlThread dt;

    public class dlThread extends Thread{
        public Handler handler;//Handler定义在子线程中
        @Override
        public void run() {
            Looper.prepare();//创建Looper
            handler = new Handler(){//消息处理
                @Override
                public void handleMessage(@NonNull Message msg) {
                    if(msg.what == 0x123){
                        int number = msg.getData().getInt("number");//getData得到的是Bundle对象
                        number = number * number;
                        Toast.makeText(MainActivity.this,"计算平方结果为:" + number,Toast.LENGTH_SHORT).show();
                    }
                }
            };
            Looper.loop();//上面说的loop方法
        }
    }
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        bindviews();
        dt = new dlThread();
        dt.start();
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Message msg = Message.obtain();
                Bundle bundle = new Bundle();
                bundle.putInt("number",Integer.parseInt(editText.getText().toString()));
                msg.what = 0x123;
                msg.setData(bundle);//前面说的利用Bundle传递信息
                dt.handler.sendMessage(msg);
            }
        });
    }

    private void bindviews() {
        button = findViewById(R.id.button);
        editText = findViewById(R.id.edit);
    }
}

效果如下:

在这里插入图片描述

Handler导致的内存泄漏

参考AndyJennifer

和之前学习Bitmap的时候一样,Handler异步机制也会导致ML问题。

public class HandlerLeakageActivity extends BaseActivity {

    public static final int UPDATE_UI = 1;
    
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            if (msg.what == UPDATE_UI) {
                updateUI();
            }
        }
    };

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_handler_leakage);
        Message message = Message.obtain();
        message.what = UPDATE_UI;
        mHandler.sendMessageDelayed(message, 1000 * 3600 * 24);//发送延时24小时消息
    }
    
    //更新ui
    private void updateUI() {...}
 }

在HandlerLeakageActivity 中创建了内部类Handler,同时发送了一个延时为24小时的消息。当HandlerLeakageActivity 收到这个延迟消息后在handleMessage中更新UI。造成ML原因在于:由于Handler是非静态内部类,因此会持有有外部类HandlerLeakageActivity的引用;而Message最终是通过Looper的loop()方法取出后,分发给相应的Handler来处理消息,因此Message会持有Handler的引用,而且还是强引用,这时就算我们退出HandlerLeakageActivity,由于消息队列中还有消息,并且延迟消息的迟迟不能被取出执行,导致HandlerLeakageActivity不会被GC。

简单解释的话:
HandlerLeakageActivity finish之后,handler发送的消息最终仍然是通过handler去处理的,而此时Handler内部非静态类还持有HandlerLeakageActivity的实例引用,GC在回收HandlerLeakageActivity时发现HandlerLeakageActivity还存在有强引用关系,就不回去回收HandlerLeakageActivity,导致ML。

解决方法和前面提到的一样,静态内部类+弱引用。

public class HandlerLeakageActivity extends BaseActivity {
    public static final int UPDATE_UI = 1;
    
    private MyHandler mHandler = new MyHandler(this);
    //使用静态内部类
    private static class MyHandler extends Handler {

        private final WeakReference<HandlerLeakageActivity> mWeakReference;
        MyHandler(HandlerLeakageActivity activity) {
            mWeakReference = new WeakReference<HandlerLeakageActivity>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            HandlerLeakageActivity activity = mWeakReference.get();
            if (activity != null) {
                activity.updateUI();
            }
        }
    }
        //更新ui
    private void updateUI() {...}
   }

这样静态内部类就不会不会持有外部类引用,GC在回收时就可以直接回收了。而采用弱引用WeakReference 包裹外部类的对象是因为在更新UI时可能会使用到外部类的成员变量或方法,如果不采用弱引用,直接使用强引用,这显然又会导致Activity内存泄露。

上述就是Handler的基础知识,后面有新的认识会添加进去的~

猜你喜欢

转载自blog.csdn.net/weixin_42011443/article/details/106889114