Handler系列---基本使用

Handler系列—基本使用

前言

记得刚开始学习 Android 那会,我是使用的 Bmob 作为后端服务的,那个时候进行网络请求的时候,使用的是 Bmob 的 SDK,在回调里面不像现在的 OKHTTP 请求直接回调的是主线程,当初 Bmob 的回调是在子线程的,我就经常遇到下面的异常:

CalledFromWrongThreadException

相信很多人在刚学习 Android 的时候都遇到过这个异常,出现这个异常的原因就是在子线程中进行了 UI 界面更新的操作。

后来我了解到:

  • Android 不能在 UI 线程做耗时操作
  • Android 不能在子线程中更新 UI

Android 是单线程模型,UI 操作并不是线程安全的,并且这些操作必须在 UI 线程执行。但是我们的有些操作必须是在子线程中进行。

比如我们需要加载一个很大的文件,然后显示在主线程上,如果直接在主线程中进行,你会遇到 Android 中大名鼎鼎的 ANR (Android Not Response)异常的。

解决办法就是在自子下线程中加载文件,然后让子线程通知子线程更新 UI 就可以了。

那怎么在子线程中通知主线程呢?这就用到了 Android 中的消息机制。

这里先来讲下 Handler 的基本用法,以及一些特殊的小知识,下篇文章我们再详细的讲 Andoroid 的消息机制。

如果了解 Handler 使用的可以直接不用往下看了,直接去看

Handler系列—源码分析

Handler 使用

下面通过几个实例来讲下 Handler 的使用:

  1. 使用 Handler 的 post() 方法更新 UI
  2. 使用 Handler 的 sendMessage() 方法更新 UI
  3. 使用 runOnUiThread() 方法更新 UI
  4. 使用 View 的 post() 方法更新 UI
  5. 子线程中创建 Handler(handler1)发送消息,在子线程中的Handler(handler1) 中处理,然后发送给主线程(mHandler) 去更新 UI
  6. 在子线程中更新 UI (对的,你没看错,就是在子线程中更新 UI )
  7. 消除使用 Handler 的过程中出现内存泄漏

你可能看到第3、4 都没 Handler 字样,少年别急,具体的原理会在后面的文章中讲到,先继续往下看。

现在 Activity 中创建 Handler:


TextView mShow;

/**
 * 主线程中的Handler
 */
private Handler mHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);
        mShow.setText("msg.what = " + msg.what + " 的时候 : \n\n" + "msg.obj = " + msg.obj.toString());
    }
};

篇幅有限,下面仅仅是部分关键代码,需要看完整代码的请到这里看
地址

1、使用 Handler 的 post() 方法更新 UI

使用 Handler 的 post() 方法更新 UI,是通过 post 出一个 Runnable 的方式,代码如下:

//使用post方法直接更新ui线程
new Thread(new Runnable() {
    @Override
    public void run() {
        mHandler.post(new Runnable() {
            @Override
            public void run() {
                mShow.setText("使用post方法直接更新ui线程");
            }
        });
    }
}).start();

界面展示:

2、使用 Handler 的 sendMessage() 方法更新 UI

这种方式主要是通过 Handler 对象发送出去了一个 Message 对象的方式更新 UI。

使用方法如下:

new Thread(new Runnable() {
    @Override
    public void run() {
        Message message = mHandler.obtainMessage(7, "子线程中发布消息,更新主线程");
        mHandler.sendMessage(message);
    }
}).start();

运行结果如下:

3、使用 runOnUiThread() 方法更新 UI

这种方式很简单,可以直接在 runOnUiThread() 方法里面更新 UI:

new Thread(new Runnable() {
    @Override
    public void run() {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                mShow.setText("使用runOnUiThread更新ui线程");
            }
        });
    }
}).start();

运行结果:

4、使用 View 的 post() 方法更新 UI

这种事利用我们界面上的 View 的 post() 方法来更新 UI:

new Thread(new Runnable() {
    @Override
    public void run() {
        mButton5.post(new Runnable() {
            @Override
            public void run() {
                mShow.setText("通过View的post方法更新ui");
            }
        });
    }

运行结果:

5、子线程中创建 Handler(handler1)发送消息,在子线程中的Handler(handler1) 中处理,然后发送给主线程(mHandler) 去更新 UI

这个例子我们在子线程中创建了一个 Handler 对象(threadHandler),然后通过子线程的 threadHandler 对象发送 Message,在子线程中接收 Message ,然后再用主线程的 mHandler 发送消息到主线程显示,代码如下:

new Thread(new Runnable() {
    @Override
    public void run() {
        Looper.prepare();
        Handler threadHandler;
        threadHandler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
                if (msg.what == 1) {
                    mHandler.sendMessage(mHandler.obtainMessage(1, "子线程中创建Handler(handler1)发送消息,在子线程中的Handler(handler1)中处理,然后发送给主线程(mHandler)去更新ui"));
                }
            }
        };
        threadHandler.sendEmptyMessage(1);
        Looper.loop();
    }
}).start();

结果:

6、在子线程中更新 UI (对的,你没看错,就是在子线程中更新 UI )

前面我们说了 Android 是单线程模型,更新 UI 界面需要在主线程进行,为什么这里又说可以在子线程更新呢?

在子线程更新 UI 是有附加条件的:
必须在 Activity 的 onResume 方法之前进行更新 UI 操作,也就是必须在 onResume 之前完成 View 的绑定,然后在子线程中更新 View,下面我们通过例子看一下:

我们先定义一个 Button 叫 mButton4,继续用上面的 TextView(mShow) 。

public class HandlerTestActivity extends BaseActivity {
    TextView mShow;
    Button mButton4;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentLayout(R.layout.a_activity_handler_test);
        mShow = findViewById(R.id.show);
        mButton4 = findViewById(R.id.button4);
        mButton4.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
            beforeOnResumeClick();
            }
        });
        // 写在OnResume之前执行点击事件的话,可以在子线程更新UI线程
        mButton4.performClick();
    }
    /**
    * 测试在onResume之前调用Thread更新ui
    */
    private void beforeOnResumeClick() {
        new Thread() {
            @Override
            public void run() {
                super.run();
                mShow.setText("测试在onResume之前调用Thread更新ui");
            }
        }.start(); 
    }  
}

运行结果如图所示:

上面结果成功证明了可以在子线程中进行更新 UI 操作,只不过需要特定的条件:必须在onResume之前进行,具体原理是什么?我会在下篇文章进行讲解。

7、消除使用 Handler 的过程中出现内存泄漏

可能有的小伙伴在使用 Handler 的过程中,会遇到内存泄漏的情况,下面就来讲下 Handler 中的内存泄漏是怎么产生的,以及解决内存泄漏的几种方法。

7.1 使用 Handler 时,内存泄漏是怎么产生的:

我们文章刚开始的时候,有这样一段代码:

/**
 * 主线程中的Handler
 */
private Handler mHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);
        mShow.setText("msg.what = " + msg.what + " 的时候 : \n\n" + "msg.obj = " + msg.obj.toString());
    }
};

可以看到,我们在 Activity 中生成了 Handler 对象 mHandler,但是这和内存泄漏有什么关系呢?下面来慢慢分析:

由于在 Java 中的非静态内部类匿名内部类都会隐式的持有当前类的外部引用,上面我们使用的 Handler 是非静态内部类,所以其对象 mHandler 会持有当前 Activity 的引用。

如果这个时候退出当前 Activity ,但是 mHandler 因为一些耗时操作没有被回收,也就导致已经退出的 Activity 没有被回收,本应该被回收的 Activity 对象仍然存在于堆内存中,这就产生了内存泄漏,如果这种情况多发生几次,就会发生 OOM(Out Of Memory),从而使我们的应用崩溃。

7.2 使用 Handler 时,避免内存泄漏

避免使用 Handler 时发生内存泄漏大概有三种方法来解决:

7.2.1 把 Handler 对象声明成静态的

比如把上面的改写成:

/**
 * 主线程中的Handler
 */
private static Handler mHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);
        mShow.setText("msg.what = " + msg.what + " 的时候 : \n\n" + "msg.obj = " + msg.obj.toString());
    }
};
7.2.2 使用弱引用

如果我们在 Handler 中需要持有 Activity 或者 Context 对象,那么我们可以通过使用弱引用(WeakReference)来解决

弱引用和强引用(我们平时说的对象的引用)相对,有如下特点:GC 在回收时会忽略掉弱引用,就算弱引用指向了某对象,但是只要该对象没有被强引用指向,那么该对象就会被 GC 检查的时候回收掉。
具体可参考:强引用、弱引用、软引用、虚引用

比如我们有个这样的 Handler :

/**
 * 主线程中的Handler
 */
private Handler mHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);
        Toast.makeText(activity, "测试内存泄漏", Toast.LENGTH_SHORT).show();
    }
};

mHandler 不是静态的,并且在 Handler 内部我们持有了一个 activity 的引用,如果我们这个时候 关闭当前的 activity ,但是 mHandler 在执行一个耗时操作,导致 mHandler 对象没有被释放, mHandler 持有 activity 的引用,导致 activity 对象也没有被释放,那么就会导致内存泄漏。

我们可以通过以下使用弱引用的方法解决:


private MyHandler mMyHandler;
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_safe_handler_demo);
    mMyHandler = new MyHandler(this);
}

private static class MyHandler extends Handler {
    WeakReference<SafeHandlerDemoActivity> mWeakReference;
    public MyHandler(SafeHandlerDemoActivity activity) {
        mWeakReference = new WeakReference<>(activity);
    }
    @Override
    public void handleMessage(Message msg) {
        SafeHandlerDemoActivity activity = mWeakReference.get();
        if (activity != null) {
            Toast.makeText(activity, "测试内存泄漏", Toast.LENGTH_SHORT).show();
        }
    }
}

这样就算在 mHandler 有耗时操作,并且关闭当前 Activity 的情况下也不会导致内存泄漏,因为 mHandler 持有的 activity 的弱引用,当 GC 检查到 弱引用时,弱引用会被回收掉。从而也就释放了 Activity ,但是这种方法有个缺点,就是虽然 GC 会来回收 弱引用,但是并不是立即回收的,可能会有延迟,导致对象仍然在内存中,性能比较低。

7.2.3 在 onDestory() 的时候销毁 Handler 的对象

这个就很简单了,我们在 onDestory() 的代码里面添加:

@Override
protected void onDestroy() {
    super.onDestroy();
    // 移除所有消息
    mMyHandler.removeCallbacksAndMessages(null);
    // 也可移除指定的单条消息 what就是message中的what
    // mMyHandler.removeMessages(what);
}
7.2.4 Handler 内存泄漏小结

原因:

当 Activity 销毁的时候,如果 Handler 对象持有 Activity 的引用,导致 Activity 对象不能被销毁,一直存留在内存中,导致内存越来越大,导致 OOM ;

解决办法:
1、通过设置 Handler 对象为静态的
2、使用弱引用解决内存泄漏,但是需要等到 Handler 对象中的任务都执行完,才会释放 activity 内存,不如直接 static 释放的快
3、在 onDestory() 方法里面移除所有未执行的任务

其中第一种和第二种是通过 Java 垃圾回收方面来解决的

而第三种是则是从业务层面解决问题,当我退出的时候,我要清理掉所有任务。

最后

好了,Handler 的使用就先介绍到这里,源码已经上传到 Github有兴趣的可以去看看。

下面我们来看下第二篇 Handler系列—源码分析

猜你喜欢

转载自blog.csdn.net/Sean_css/article/details/79767180