查看原文
其他

深入探讨Android异步精髓Handler

2017-10-30 谷哥的小弟 开发者技术前线

众所周知,Android的UI是在其主线程中进行刷新的,所以Google建议开发人员切勿在主线程中进行耗时的操作否则很容易导致应用程序无响应(ANR)。鉴于此几乎接近硬性的要求,我们常把耗时的操作(比如网络请求)置于子线程中进行;但是子线程不能直接访问UI。



前言

众所周知,Android的UI是在其主线程中进行刷新的,所以Google建议开发人员切勿在主线程中进行耗时的操作否则很容易导致应用程序无响应(ANR)。鉴于此几乎接近硬性的要求,我们常把耗时的操作(比如网络请求)置于子线程中进行;但是子线程不能直接访问UI。

至此,这个矛盾就凸显出来了:

主线程可以刷新UI,但不能执行耗时操作
子线程可以执行耗时操作 ,但是不能直接刷新UI


嗯哼,那有没有一个东西可以调和并化解这个矛盾呢?当然是有的,Google采用Handler把主线程和子线程精巧地联系起来——子线程中进行耗时的业务逻辑,然后利用Handler通知主线程刷新UI。除此以外,还有别的方式可以实现类似的操作么?答案是肯定的,我们也可以利用AsyncTask或者IntentService进行异步的操作。这两者又是怎么做到的呢?其实,在AsyncTask和IntentService的内部亦使用了Handler实现其主要功能。抛开这两者不谈,当我们打开Android源码的时候也随处可见Handler的身影。所以,Handler是Android异步操作的核心和精髓,它在众多领域发挥着极其重要甚至是不可替代的作用。

在此,对Handler的工作原理和实现机制进行系统的梳理。

ThreadLocal简介及其使用

对于线程Thread大家都挺熟悉的了,但是对于ThreadLocal可能就要陌生许多了。虽然我们对于它不太了解,但是它早在JDK1.2版本中就已问世并且被广泛的使用,比如Hibernate,EventBus,Handler都运用了ThreadLocal进行线程相关的操作。如果单纯地从ThreadLocal这个名字来看,它带着浓浓的“本地线程”的味道; 然而,喝一口之后才发现根本就不是这个味儿。其实,ThreadLocal并不是用来操作什么本地线程而是用于实现不同线程的数据副本。当使用ThreadLocal维护变量时,它会为每个使用该变量的线程提供独立的变量副本;每一个线程都可以独立地改变自己的副本并且不会影响其它线程所持有的对应的副本。所以,ThreadLocal的实际作用并不与它的名字所暗含的意义相吻合,或许改称为ThreadLocalVariable(线程本地变量)会更合适一些。

接下来,我们通过一个实例来瞅瞅ThreadLocal的使用方式

  1.    /**

  2.     * 原创作者:

  3.     * 谷哥的小弟

  4.     *

  5.     * 博客地址:

  6.     * http://blog.csdn.net/lfdfhl

  7.     */

  8.    private void testThreadLocal(){

  9.        mThreadLocal.set("东京热");

  10.        new HotThread1().start();

  11.        new HotThread2().start();

  12.        hot3=mThreadLocal.get();

  13.        try{

  14.            Thread.sleep(1000*4);

  15.            Log.i(TAG,"HotThread1获取到的变量值: "+hot1);

  16.            Log.i(TAG,"HotThread2获取到的变量值: "+hot2);

  17.            Log.i(TAG,"MainThread获取到的变量值: "+hot3);

  18.        }catch (Exception e){

  19.        }

  20.    }

  21.    private class HotThread1  extends Thread{

  22.        @Override

  23.        public void run() {

  24.            super.run();

  25.            mThreadLocal.set("北京热");

  26.            hot1=mThreadLocal.get();

  27.        }

  28.    }

  29.    private class HotThread2  extends Thread{

  30.        @Override

  31.        public void run() {

  32.            super.run();

  33.            mThreadLocal.set("南京热");

  34.            hot2=mThreadLocal.get();

  35.        }

  36.    }

查看输出结果:

HotThread1获取到的变量值: 北京热
HotThread2获取到的变量值: 南京热
MainThread获取到的变量值: 东京热
在这段代码中使用ThreadLocal保存String类型的数据,并且在主线程和两个子线程中为ThreadLocal设置了不同的值,然后再将这些值分别取出。结合输出日志可以发现:在不同的线程中访问了同一个ThreadLocal对象,但是通过mThreadLocal.get()得到的值却是不一样的;也就是说:它们之间没有发生相互的影响而是保持了彼此的独立。明白了ThreadLocal的这个特性之后,我们再去理解Looper的工作机制就会容易得多了。

Looper、线程、消息队列的关系

Google官方建议开发人员使用Handler实现异步刷新UI,我们在平常的工作中也很好地采纳了这个提议:首先在主线程中建立Handler,然后在子线程中利用handler.sendMessage(message)发送消息至主线程,最终消息在handleMessage(Message msg) {}得到相应的处理。这个套路,大家都再熟悉不过了;现在换个角度,我们试试在子线程中建立Handler

  1. private class LooperThread  extends Thread{

  2.        @Override

  3.        public void run() {

  4.            super.run();

  5.            Handler handler=new Handler();

  6.            //doing something

  7.        }

  8.    }

此处的代码很简单:LooperThread继承自Thread,并且在其run( )方法中新建一个Handler。
嗯哼,再运行一下,喔哦,报错了:

Can’t create handler inside thread that has not called Looper.prepare().
咦,有点出师不利呢,刚开始试就出错了…….没事,生活不就是无尽的挫折和希望嘛,这点小事嘛也不算。既然是在调用Handler的构造方法时报的错那就从该构造方法的源码入手,一探究竟:

  1. public Handler() {

  2.    this(null, false);

  3. }

  4. public Handler(Callback callback) {

  5.    this(callback, false);

  6. }

  7. public Handler(Callback callback, boolean async) {

  8.    if (FIND_POTENTIAL_LEAKS) {

  9.        final Class<? extends Handler> klass = getClass();

  10.        if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&

  11.                (klass.getModifiers() & Modifier.STATIC) == 0) {

  12.            Log.w(TAG, "The following Handler class should be static or leaks might occur");

  13.        }

  14.    }

  15.    mLooper = Looper.myLooper();

  16.    if (mLooper == null) {

  17.        throw new RuntimeException

  18.        ("Can't create handler inside thread that has not called Looper.prepare()");

  19.    }

  20.    mQueue = mLooper.mQueue;

  21.    mCallback = callback;

  22.    mAsynchronous = async;

  23. }

请注意第20行代码:
如果mLooper == null那么系统就会抛出刚才的错误:Can’t create handler inside thread that has not called Looper.prepare()。这句话的意思是:如果在线程内创建handler必须调用Looper.prepare()。既然这个提示已经提示了我们该怎么做,那就加上这一行代码:

  1. private class LooperThread  extends Thread{

  2.        @Override

  3.        public void run() {

  4.            super.run();

  5.            Looper.prepare();

  6.            Handler handler=new Handler();

  7.            System.out.println("add code : Looper.prepare()");

  8.            //doing something

  9.        }

  10.    }

嘿嘿,果然不再报错了,运行一下:

既然Looper.prepare()解决了这个问题,那我们就去瞅瞅在该方法中做了哪些操作:

  1. public static void prepare() {

  2.    prepare(true);

  3. }

  4. private static void prepare(boolean quitAllowed) {

  5.    if (sThreadLocal.get() != null) {

  6.        throw new RuntimeException("Only one Looper may be created per thread");

  7.    }

  8.    sThreadLocal.set(new Looper(quitAllowed));

  9. }

从这段源码及其注释文档我们可以看出:

在prepare()中利用一个Looper来初始化当前线程或者说初始化一个带有Looper的线程。
请注意第14行代码,它是这段源码的核心,现对其详细分析:

sThreadLocal.set(new Looper(quitAllowed));
在该行代码中一共执行了两个操作

(1) 构造Looper

  1. private Looper(boolean quitAllowed) {

  2.    mQueue = new MessageQueue(quitAllowed);

  3.    mThread = Thread.currentThread();

  4. }

在Looper的构造方法中初始化了一个消息队列MessageQueue和一个线程Thread。从这可看出:一个Looper对应着一个消息队列以及当前线程。
当收到消息Message后系统会将其存入消息队列中等候处理。至于Looper,它在Android的消息机制中担负着消息轮询的职责,它会不间断地查看MessageQueue中是否有新的未处理的消息;若有则立刻处理,若无则进入阻塞。

(2) 将此Looper保存到sThreadLocal中。 
此处的sThreadLocal是定义在Looper类中的一个ThreadLocal类型变量

static final ThreadLocal sThreadLocal = new ThreadLocal();
Looper是framework中的一个类,sThreadLocal是它的一个static final变量。当在某一个Thread中执行Looper.prepare()时系统就会将与该Thread所对应的Looper保存到sThreadLocal中。不同的线程对着不同的Looper,但它们均由系统保存在sThreadLocal中并且互不影响,相互独立;并且可以通过sThreadLocal.get()获取不同线程所对应的Looper.

在调用prepare()方法后需要调用loop()方法开始消息的轮询,并且在需要的时候调用quit()方法停止消息的轮询
假若再次执行Looper.prepare()系统发现sThreadLocal.get()的值不再为null于是抛出异常:
Only one Looper may be created per thread,一个线程只能创建一个Looper!

小结:

一个线程对应一个Looper
一个Looper对应一个消息队列
一个线程对应一个消息队列
线程,Looper,消息队列三者一一对应
所以,在一个子线程中使用Handler的方式应该是这样的:

  1. class LooperThread extends Thread {

  2.    public Handler mHandler;

  3.    public void run() {

  4.        Looper.prepare();

  5.        mHandler = new Handler() {

  6.            public void handleMessage(Message msg) {

  7.            }

  8.        };

  9.        Looper.loop();

  10.      }

  11.  }

看到这个范例,有的人可能心里就犯嘀咕了:为什么我们平常在MainActivity中使用Handler时并没有调用Looper.prepare()也没有报错呢?
这是因为UI线程是主线程,系统会自动调用Looper.prepareMainLooper()方法创建主线程的Looper和消息队列MessageQueue

Message的发送和处理过程

在讨论完Looper、线程、消息队列这三者的关系之后我们再来瞅瞅Android消息机制中对于Message的发送和处理。

平常最常用的方式:
handler.sendMessage(message)——>发送消息
handleMessage(Message msg){}——>处理消息

先来分析消息的入队。
Handler可以通过post()postAtTime()postDelayed()postAtFrontOfQueue()等方法发送消息,除了postAtFrontOfQueue()之外这几个方法均会执行到sendMessageAtTime(Message msg, long uptimeMillis)方法,源码如下:

  1. public boolean sendMessageAtTime(Message msg, long uptimeMillis) {

  2.    MessageQueue queue = mQueue;

  3.    if (queue == null) {

  4.        RuntimeException e = new RuntimeException(

  5.                this + " sendMessageAtTime() called with no mQueue");

  6.        Log.w("Looper", e.getMessage(), e);

  7.        return false;

  8.    }

  9.    return enqueueMessage(queue, msg, uptimeMillis);

  10. }

  11. public final boolean sendMessageAtFrontOfQueue(Message msg) {

  12.    MessageQueue queue = mQueue;

  13.    if (queue == null) {

  14.        RuntimeException e = new RuntimeException(

  15.                this + " sendMessageAtTime() called with no mQueue");

  16.        Log.w("Looper", e.getMessage(), e);

  17.        return false;

  18.    }

  19.    return enqueueMessage(queue, msg, 0);

  20. }

  21. private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {

  22.    msg.target = this;

  23.    if (mAsynchronous) {

  24.        msg.setAsynchronous(true);

  25.    }

  26.    return queue.enqueueMessage(msg, uptimeMillis);

  27. }

在这里可以看到sendMessageAtTime()内部又调用了enqueueMessage(),在该方法内的重要操作:

第一步:

给msg设置了target,请参见代码第25行

此处的this就是当前Handler对象本身。在这就指明了该msg的来源——它是由哪个Handler发出的,与此同时也指明了该msg的归宿——它该由哪个Handler处理。不难发现,哪个Handler发出了消息就由哪个Handler负责处理。

第二步:

将消息放入消息队列中,请参见代码第29行

enqueueMessage(msg,uptimeMillis)中将消息Message存放进消息队列中,距离触发时间最短的message排在队列最前面,同理距离触发时间最长的message排在队列的最尾端。若调用sendMessageAtFrontOfQueue()方法发送消息它会直接调用该enqueueMessage(msg,uptimeMillis)让消息入队只不过时间为延迟时间为0,也就是说该消息会被插入到消息队列头部优先得到执行。

直觉告诉我们此处的消息队列mQueue就是该线程所对应的消息队列。可是光有直觉是不够的甚至是不可靠的。我们再回过头瞅瞅Handler的构造方法,从源码中找到确切的依据

  1. public Handler(Callback callback, boolean async) {

  2.    if (FIND_POTENTIAL_LEAKS) {

  3.        final Class<? extends Handler> klass = getClass();

  4.        if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&

  5.                (klass.getModifiers() & Modifier.STATIC) == 0) {

  6.            Log.w(TAG, "The following Handler class should be static or leaks might occur");

  7.        }

  8.    }

  9.    mLooper = Looper.myLooper();

  10.    if (mLooper == null) {

  11.        throw new RuntimeException

  12.        ("Can't create handler inside thread that has not called Looper.prepare()");

  13.    }

  14.    mQueue = mLooper.mQueue;

  15.    mCallback = callback;

  16.    mAsynchronous = async;

  17. }

(1) 获取Looper,请参见代码第10行
(2) 利用Looper的消息队列为mQueue赋值,请参见代码第15行
(3) 为mCallback赋值,,请参见代码第16行
(4) 为mAsynchronous赋值,,请参见代码第17行

嗯哼,看到了吧,这个mQueue就是从Looper中取出来的。在之前我们也详细地分析了Looper、线程、消息队列这三者的一一对应关系,所以此处的mQueue正是线程所对应的消息队列。

看完了消息的入队,再来分析消息的出队。
请看Looper中的loop()方法源码:

  1. public static void loop() {

  2.    final Looper me = myLooper();

  3.    if (me == null) {

  4.        throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");

  5.    }

  6.    final MessageQueue queue = me.mQueue;

  7.    // Make sure the identity of this thread is that of the local process,

  8.    // and keep track of what that identity token actually is.

  9.    Binder.clearCallingIdentity();

  10.    final long ident = Binder.clearCallingIdentity();

  11.    for (;;) {

  12.        Message msg = queue.next(); // might block

  13.        if (msg == null) {

  14.            // No message indicates that the message queue is quitting.

  15.            return;

  16.        }

  17.        // This must be in a local variable, in case a UI event sets the logger

  18.        final Printer logging = me.mLogging;

  19.        if (logging != null) {

  20.            logging.println(">>>>> Dispatching to " + msg.target + " " +

  21.                    msg.callback + ": " + msg.what);

  22.        }

  23.        final long traceTag = me.mTraceTag;

  24.        if (traceTag != 0) {

  25.            Trace.traceBegin(traceTag, msg.target.getTraceName(msg));

  26.        }

  27.        try {

  28.            msg.target.dispatchMessage(msg);

  29.        } finally {

  30.            if (traceTag != 0) {

  31.                Trace.traceEnd(traceTag);

  32.            }

  33.        }

  34.        if (logging != null) {

  35.            logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);

  36.        }

  37.        // Make sure that during the course of dispatching the

  38.        // identity of the thread wasn't corrupted.

  39.        final long newIdent = Binder.clearCallingIdentity();

  40.        if (ident != newIdent) {

  41.            Log.wtf(TAG, "Thread identity changed from 0x"

  42.                    + Long.toHexString(ident) + " to 0x"

  43.                    + Long.toHexString(newIdent) + " while dispatching to "

  44.                    + msg.target.getClass().getName() + " "

  45.                    + msg.callback + " what=" + msg.what);

  46.        }

  47.        msg.recycleUnchecked();

  48.    }

  49. }

我们发现关于消息的处理是在一个死循环中就行的,请参见代码第13-55行。也就是说在该段代码中Looper一直在轮询消息队列MessageQueue。假若消息队列中没有未处理的消息(即queue.next()==null)则其进入阻塞block状态,假若消息队列中有待处理消息(即queue.next()!=null)则利用msg.target.dispatchMessage(msg)将该消息派发至对应的Handler。

到了这,可能有的人会有一个疑问:系统怎么知道把消息发送给哪个Handler呢?
嘿嘿,还记不记得enqueueMessage()中系统给msg设置了target从而确定了其目标Handler么?嗯哼,所以只要通过msg.target.dispatchMessage(msg)就可以将消息派发至对应的Handler了。那在dispatchMessage()中又会对消息做哪些操作呢?我们继续跟进源码

  1. public void dispatchMessage(Message msg) {

  2.    if (msg.callback != null) {

  3.        handleCallback(msg);

  4.    } else {

  5.        if (mCallback != null) {

  6.            if (mCallback.handleMessage(msg)) {

  7.                return;

  8.            }

  9.        }

  10.        handleMessage(msg);

  11.    }

  12. }

哇哈,看到这,心情就舒畅多了,基本上回到了我们熟悉的地方;在此处对Message消息进行了处理,我们来瞅瞅主要的步骤

第一步:
处理Message的回调callback,请参见代码第3行
比如调用handler.post(Runnable runnable)时,该runnable就会被系统封装为Message的callback。
关于这点在源码中也有非常直观的体现:

  1. private static Message getPostMessage(Runnable r) {

  2.   Message m = Message.obtain();

  3.   m.callback = r;

  4.   return m;

  5. }

第二步:
处理Handler的回调callback,请参见代码第6行
比如执行Handler handler=Handler(Callback callback)时就会将callback赋值给mCallback,关于这点已经在介绍Handler构造方法时分析过了,不再赘述。

第三步:
调用handleMessage()处理消息Message,请参见代码第10行
handleMessage()的源码如下:

  1. public void handleMessage(Message msg) {

  2. }

嗯哼,它是一个空的方法。所以Handler的子类需要覆写该方法,并在其中处理接收到的消息。

梳理Handler工作机制

至此,关于Handler的异步机制及其实现原理已经分析完了。在此,对其作一个全面的梳理和总结。

Android异步消息机制中主要涉及到:Thread、Handler、MessageQueue、Looper,在整个机制中它们扮演着不同的角色也承担着各自的不同责任。

Thread

负责业务逻辑的实施。
线程中的操作是由各自的业务逻辑所决定的,视具体情况进行。

Handler

负责发送消息和处理消息。
通常的做法是在主线程中建立Handler并利用它在子线程中向主线程发送消息,在主线程接收到消息后会对其进行处理

MessageQueue

负责保存消息。
Handler发出的消息均会被保存到消息队列MessageQueue中,系统会根据Message距离触发时间的长短决定该消息在队列中位置。在队列中的消息会依次出队得到相应的处理。

Looper

负责轮询消息队列。
Looper使用其loop()方法一直轮询消息队列,并在消息出队时将其派发至对应的Handler.

为了更好地理解这几者的相互关系及其作用,请参见如下示图

这里写图片描述

使用Handler的错误姿势及其潜在风险

关于Handler的具体用法,尤其是那些常规的使用方式在此就不再一一列举了。
我们要讨论和分析的是在开发中不恰当地使用Handler的方式及其带来的潜在风险。

第一个问题:
利用handler.post(Runnable runnable)执行耗时操作
请看如下示例:

  1. /**

  2. * 原创作者:

  3. * 谷哥的小弟

  4. *

  5. * 博客地址:

  6. * http://blog.csdn.net/lfdfhl

  7. */

  8. public class MainActivity extends AppCompatActivity {

  9.    private TextView mTextView;

  10.    private Handler mHandler;

  11.    private ImageView mImageView;

  12.    private Resources mResources;

  13.    private static final String TAG="stay4it";

  14.    @Override

  15.    protected void onCreate(Bundle savedInstanceState) {

  16.        super.onCreate(savedInstanceState);

  17.        setContentView(R.layout.activity_main);

  18.        init();

  19.    }

  20.    private void init(){

  21.        mResources=getResources();

  22.        mHandler=new Handler();

  23.        mTextView=(TextView) findViewById(R.id.textView);

  24.        mImageView =(ImageView) findViewById(R.id.imageView);

  25.        mImageView.setOnClickListener(new OnClickListenerImpl());

  26.        long threadID=Thread.currentThread().getId();

  27.        Log.i(TAG,"主线程的线程ID="+threadID);

  28.    }

  29.    private class OnClickListenerImpl implements View.OnClickListener {

  30.        @Override

  31.        public void onClick(View v) {

  32.            new TestThread().start();

  33.        }

  34.    }

  35.    private class TestThread extends Thread{

  36.        @Override

  37.        public void run() {

  38.            super.run();

  39.            mHandler.post(new Runnable() {

  40.                @Override

  41.                public void run() {

  42.                    String text=mResources.getString(R.string.text);

  43.                    long threadID=Thread.currentThread().getId();

  44.                    Log.i(TAG,"在post(Runnable r)里的run()获取到线程ID="+threadID);

  45.                    mTextView.setText(text);

  46.                }

  47.            });

  48.        }

  49.    }

  50. }

运行一下,观察效果:

这里写图片描述

我们在开发中可能会做如上的操作:在主线程中创建Handler,然后在子线程里利用handler.post(Runnable runnable)执行某些操作甚至是耗时的操作。可是这么做合适么?我们来看看主线程的ID和在Runnable的run()方法里获取到的线程ID,输出日志如下:

主线程的线程ID=1
在post(Runnable r)里的run()获取到线程ID=1
在这里我们发现在两处获得的线程ID是同一个值,也就是说Runnable的run()方法并不是在一个新线程中执行的,而是在主线程中执行的。
为什么明明把handler.post(Runnable runnable)放入到子线程中了但是Runnable的run()却在主线程中执行呢?
其实,这个问题在之前的分析中已经提到了:调用handler.post(Runnable runnable)时,该runnable会被系统封装为Message的callback。所以,handler.post(Runnable runnable)和handler.sendMessage(Message message)这两个不同的方法在本质上是相同的——Handler发送了一条消息。在该示例中handler是在主线程中创建的,所以它当然会在主线程中处理消息;如此以来该Runnable亦会在主线程中执行;所以,在Runnable的run()方法中执行耗时的操作是不可取的容易导致应用程序无响应。

那么,调用view.post(Runnable runnable)会在子线程中执行还是主线程中执行呢?
我们来瞅瞅它的实现:

  1. public boolean post(Runnable action) {

  2.    final AttachInfo attachInfo = mAttachInfo;

  3.    if (attachInfo != null) {

  4.        return attachInfo.mHandler.post(action);

  5.    }

  6.    getRunQueue().post(action);

  7.    return true;

  8. }

看到这段源码就无需再做过多的解释了,它依然是在主线程中执行的,原理同上。

那么,调用Activity.runOnUiThread(Runnable runnable)方法会在子线程中执行还是主线程中执行呢?

  1. public final void runOnUiThread(Runnable action) {

  2.    if (Thread.currentThread() != mUiThread) {

  3.        mHandler.post(action);

  4.    } else {

  5.        action.run();

  6.    }

  7. }

嗯哼,这段源码就更简单了。如果当前线程是UI线程,那么该Runnable会立即执行;如果当前线程不是UI线程,则使用handler的post()方法将其放入UI线程的消息队列中。

小结:
handler.post(Runnable runnable)view.post(Runnable runnable)Activity.runOnUiThread(Runnable runnable)的runnable均会在主线程中执行,所以切勿在其run()方法中执行耗时的操作

第二个问题: 
Handler导致的潜在内存泄露
请看如下示例:

  1. import android.os.Bundle;

  2. import android.os.Handler;

  3. import android.os.Message;

  4. import android.support.v7.app.AppCompatActivity;

  5. import android.util.Log;

  6. /**

  7. * 原创作者:

  8. * 谷哥的小弟

  9. *

  10. * 博客地址:

  11. * http://blog.csdn.net/lfdfhl

  12. */

  13. public class MainActivity extends AppCompatActivity {

  14.    private Handler mHandler;

  15.    private static final String TAG="stay4it";

  16.    @Override

  17.    protected void onCreate(Bundle savedInstanceState) {

  18.        super.onCreate(savedInstanceState);

  19.        setContentView(R.layout.activity_main);

  20.        init();

  21.    }

  22.    private void init() {

  23.        mHandler = new Handler() {

  24.            @Override

  25.            public void handleMessage(Message msg) {

  26.                super.handleMessage(msg);

  27.                Log.i(TAG,"handle message");

  28.            }

  29.        };

  30.        Message message=Message.obtain();

  31.        message.what=9527;

  32.        mHandler.sendMessage(message);

  33.        mHandler.postDelayed(new Runnable() {

  34.            @Override

  35.            public void run() {

  36.            }

  37.        }, 1000 * 20);

  38.    }

  39. }

以上是我们在工作中常见的对于Handler的使用方式,为了更形象地说明问题特意把Runnable所延迟时间设置得比较久。如此操作,猛地一看觉得没啥不妥当的地方;但是简单地分析一下这段代码,却发现它存在潜在的内存泄露风险。

内部类new Handler(){}持有外部类MainActivity的引用
内部类new Runnable(){}持有外部类MainActivity的引用
new Runnable(){}会被封装成Message``的callback且Message会持有Handler的引用
handler发送了延迟消息,所以消息队列持该Runnable的引用
综合这几者,可理解为消息间接地持有了MainActivity的引用
现在假设这么一种情况:进入该Activity后在20秒以内的任意时间旋转屏幕,此时会导致Activity重新绘制。但是通过postDelayed()发出的Runnable还未被执行,所以消息队仍列持有Runnable的引用,而Runnable也依然持有Activity的引用,故此时Activity所占内存并不能向期望的那样被回收,这样就可能会造成内存的泄漏。

优化该问题的方式有多种,在此展示其中一种比较好的实现方式:

  1. /**

  2. * 原创作者:

  3. * 谷哥的小弟

  4. *

  5. * 博客地址:

  6. * http://blog.csdn.net/lfdfhl

  7. */

  8. public class MainActivity extends AppCompatActivity {

  9.    private Activity mActivity;

  10.    private static final String TAG="stay4it";

  11.    @Override

  12.    protected void onCreate(Bundle savedInstanceState) {

  13.        super.onCreate(savedInstanceState);

  14.        setContentView(R.layout.activity_main);

  15.        init();

  16.    }

  17.    private void init(){

  18.        mActivity=this;

  19.        BetterHandler betterHandler = new BetterHandler(mActivity);

  20.        Message message=Message.obtain();

  21.        message.what=9527;

  22.        betterHandler.sendMessage(message);

  23.        betterHandler.postDelayed(new BetterRunnable(), 1000 * 20);

  24.    }

  25.    private static class BetterRunnable implements Runnable {

  26.        @Override

  27.        public void run() {

  28.            Log.i(TAG,"Runnable run()");

  29.        }

  30.    }

  31.    private static class BetterHandler extends Handler {

  32.        private WeakReference<Activity> activityWeakReference;

  33.        public BetterHandler(Activity activity) {

  34.            activityWeakReference = new WeakReference<>(activity);

  35.        }

  36.        @Override

  37.        public void handleMessage(Message msg) {

  38.            super.handleMessage(msg);

  39.            if (activityWeakReference.get() != null) {

  40.                Log.i(TAG,"handle message");

  41.            }

  42.        }

  43.    }

  44. }

看到这段代码,我们发现了一个陌生的东西WeakReference。什么是WeakReference呢?它有什么特点呢?

从JDK1.2开始,Java把对象的引用分为四种级别,这四种级别由高到低依次为:强引用、软引用、弱引用和虚引用。

强引用 
我们一般使用的就是强引用,垃圾回收器一般都不会对其进行回收操作。当内存空间不足时Java虚拟机宁愿抛出OutOfMemoryError错误使程序异常终止,也不会回收具有强引用的对象

软引用 
如果一个对象具有软引用(SoftReference),在内存空间足够的时候GC不会回收它,如果内存空间不足了GC就会回收这些对象的内存空间。

弱引用 
如果一个对象具有弱引用(WeakReference),那么当GC线程扫描的过程中一旦发现某个对象只具有弱引用而不存在强引用时不管当前内存空间足够与否GC都会回收它的内存。由于垃圾回收器是一个优先级较低的线程,所以不一定会很快发现那些只具有弱引用的对象。为了防止内存溢出,在处理一些占用内存大而且生命周期较长的对象时候,可以尽量使用软引用和弱引用.

虚引用 
虚引用(PhantomReference)与其他三种引用都不同,它并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。所以,虚引用主要用来跟踪对象被垃圾回收器回收的活动,在一般的开发中并不会使用它

嗯哼,在了解这四种引用之后我们继续分析刚才的代码。在该示例中做了如下主要操作:

将Handler和Runnable定义为Activity的静态内部类

这两者定义为静态内部类后它们就不再持有外部类(Activity)的引用,具体代码请参见示例中BetterHandlerBetterRunnable的实现

在BetterHandler内使用弱引用WeakReference持有Activity

在完成这两步操作之后,我们再来分析刚才的场景:进入该Activity后在20秒以内的任意时间旋转屏幕导致Activity重新绘制。此时,消息持有Handler的引用,但Handler对象不再持有Activity的强引用,所以系统可以回收该Activity从而避免了内存泄露的发生。对于这样的做法,可能有的人觉得不是特别好理解,那我再换一种直白的通俗的描述:如果直接将Activity传入BetterHandler中并且不对其使用WeakReference那么它依然是一个强引用,这和之前未优化的代码相比是没有任何差别的。假若把Activity传进BetterHandler之后并用WeakReference“包裹”了它,使之不再是一个强引用而变成了一个弱引用。当Activity发生重绘时,GC发现对于这个Activity没有强引用而只存在一个弱引用,那么系统就将其回收。

handleMessage( )对Activity进行非空判断

因为Activity可能已经被GC回收,所以在处理消息时要判断Activity是否为null,即if(activityWeakReference.get() != null)从而避免异常的发生。

后语

我想能看到这的人已经不多了。

大家都知道,Handler在Android体系中占有具有举足轻重的作用,只有掌握了Handler的实现原理和工作机制才可能更全面,更深入地掌握Android开发技能。在本文中我力图详细地阐述Handler相关技术,所以文章的篇幅偏长了。如果你耐着性子看到了此处,请为自己点个赞;你的坚持和努力不会白费,它们会让你变得更好。


支持原作者可以进行 打赏,我会联系作者进行打款!如果你觉得有用,就支持下作者吧!


   版权声明:本文原创作者CSDN专家:谷哥的小弟 

   地址:http://blog.csdn.net/lfdfhl



您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存