编程进阶网编程进阶网
  • 基础组成体系
  • 程序编程原理
  • 异常和IO系统
  • 六大设计原则
  • 设计模式导读
  • 创建型设计模式
  • 结构型设计模式
  • 行为型设计模式
  • 设计模式案例
  • 面向对象思想
  • 基础入门
  • 高级进阶
  • JVM虚拟机
  • 数据集合
  • Java面试题
  • C语言入门
  • C综合案例
  • C标准库
  • C语言专栏
  • C++入门
  • C++综合案例
  • C++专栏
  • HTML
  • CSS
  • JavaScript
  • 前端专栏
  • Swift
  • iOS入门
  • 基础入门
  • 开源库解读
  • 性能优化
  • Framework
  • 方案设计
  • 媒体音视频
  • 硬件开发
  • Groovy
  • 常用工具
  • 大厂面试题
  • 综合案例
  • 网络底层
  • Https
  • 网络请求
  • 故障排查
  • 专栏
  • 数组
  • 链表
  • 栈
  • 队列
  • 树
  • 递归
  • 哈希
  • 排序
  • 查找
  • 字符串
  • 其他
  • Bash脚本
  • Linux入门
  • 嵌入式开发
  • 代码规范
  • Markdown
  • 开发理论
  • 开发工具
  • Git管理
  • 百宝箱
  • 开源协议
  • 技术招聘
  • 测试经验
  • 职场提升
  • 技术模版
  • 关于我
  • 目标清单
  • 学习框架
  • 育儿经验
  • 我的专栏
  • 底层能力
  • 读书心得
  • 随笔笔记
  • 职场思考
  • 中华历史
  • 经济学故事
  • 基础组成体系
  • 程序编程原理
  • 异常和IO系统
  • 六大设计原则
  • 设计模式导读
  • 创建型设计模式
  • 结构型设计模式
  • 行为型设计模式
  • 设计模式案例
  • 面向对象思想
  • 基础入门
  • 高级进阶
  • JVM虚拟机
  • 数据集合
  • Java面试题
  • C语言入门
  • C综合案例
  • C标准库
  • C语言专栏
  • C++入门
  • C++综合案例
  • C++专栏
  • HTML
  • CSS
  • JavaScript
  • 前端专栏
  • Swift
  • iOS入门
  • 基础入门
  • 开源库解读
  • 性能优化
  • Framework
  • 方案设计
  • 媒体音视频
  • 硬件开发
  • Groovy
  • 常用工具
  • 大厂面试题
  • 综合案例
  • 网络底层
  • Https
  • 网络请求
  • 故障排查
  • 专栏
  • 数组
  • 链表
  • 栈
  • 队列
  • 树
  • 递归
  • 哈希
  • 排序
  • 查找
  • 字符串
  • 其他
  • Bash脚本
  • Linux入门
  • 嵌入式开发
  • 代码规范
  • Markdown
  • 开发理论
  • 开发工具
  • Git管理
  • 百宝箱
  • 开源协议
  • 技术招聘
  • 测试经验
  • 职场提升
  • 技术模版
  • 关于我
  • 目标清单
  • 学习框架
  • 育儿经验
  • 我的专栏
  • 底层能力
  • 读书心得
  • 随笔笔记
  • 职场思考
  • 中华历史
  • 经济学故事
  • 1.1App启动流程梳理
  • 1.2ActivityThread分析
  • 1.3Context设计思想
  • 2.1Activity基础介绍
  • 2.2Activity启动流程
  • 2.3Activity布局创建
  • 2.4Activity布局绘制
  • 2.5Service基础介绍
  • 2.6Service启动流程
  • 2.7Receiver广播基础
  • 2.8Receiver深入原理
  • 2.9ContentProvider分析
  • 2.10Fragment实践
  • 2.11Intent深入思考
  • 3.1Paint和Canvas
  • 3.2View的绘制基础
  • 3.3onMeasure流程设计
  • 3.4onLayout流程设计
  • 3.5onDraw流程设计
  • 3.6View工作原理
  • 3.7View刷新设计流程
  • 3.8自定义View控件
  • 3.9自定义ViewGroup控件
  • 4.1Handler基础使用
  • 4.2消息机制流程分析
  • 4.3Handler深度解析
  • 4.4Message深度理解
  • 4.5MessageQueue解析
  • 4.6Looper深度解析
  • 4.7理解Handler同步屏障
  • 4.8ThreadLocal分析
  • 4.9ThreadLocal场景
  • 5.1View事件设计思考
  • 5.2View滑动冲突处理
  • 5.3View事件源码分析
  • 5.4View事件总结案例
  • 6.1DecorView设计思想
  • 6.2视图的载体View
  • 6.3视图管理者Window
  • 6.4窗口管理服务WMS
  • 6.5布局解析者Inflater
  • 7.1AsyncTask深入介绍
  • 7.2HandlerThread设计
  • 7.3IntentService设计
  • 8.1IPC通信方式介绍
  • 8.2AIDL进程间通信
  • 8.7Binder通信机制设计
  • /zh/android/basic/9.1注解设计思想和原理.html
  • 9.2APT技术设计详解
  • 9.3APT多种案例实践

4.1Handler基础使用

目录介绍

  • 01.Handler必备知识点
  • 02.Handler常见使用方式
  • 03.子线程中定义Handler
  • 04.post方法使用的说明
  • 05.Handler建议指定线程
  • 06.不建议子线程访问UI
  • 07.子线程更新UI方式
  • 08.解决Handler内存泄漏

01.Handler必备知识点

消息机制作用:

跨线程通信。当子线程中进行耗时操作后需要更新UI时,通过Handler将有关UI的操作切换到主线程中执行。

消息机制四要素:

  1. Message(消息):需要被传递的消息,其中包含了消息ID,消息处理对象以及处理的数据等,由MessageQueue统一列队,最终由Handler处理。
  2. MessageQueue(消息队列):用来存放Handler发送过来的消息,通过 Handler 发送的消息并非是立即执行的,需要存入一个消息队列中来依次执行,内部通过单链表的数据结构来维护消息列表,等待Looper的抽取。
  3. Handler(处理者):负责Message的发送及处理。通过 Handler.sendMessage() 向消息池发送各种消息事件;通过 Handler.handleMessage() 处理相应的消息事件。
  4. Looper(消息泵):通过Looper.loop()不断地从MessageQueue中抽取Message,按分发机制将消息分发给目标处理者。Looper 不断从 MessageQueue 中获取消息并将之传递给消息处理者(即是消息发送者 Handler 本身)进行处理。

具体流程

  1. 发送消息:Handler.sendMessage()发送消息时,会通过MessageQueue.enqueueMessage()向MessageQueue中添加一条消息;
  2. 取出消息:通过Looper.loop()开启循环后,不断轮询调用MessageQueue.next();
  3. 处理消息:调用目标Handler.dispatchMessage()去传递消息,目标Handler收到消息后调用Handler.handlerMessage()处理消息。
image
image

02.Handler常见使用方式

handler机制大家都比较熟悉呢。在子线程中发送消息,主线程接受到消息并且处理逻辑。如下所示

一般handler的使用方式都是在主线程中定义Handler,然后在子线程中调用mHandler.sendXx()方法,这里有一个疑问可以在子线程中定义Handler吗?

public class MainActivity extends AppCompatActivity {

    private TextView tv ;

    /**
     * 在主线程中定义Handler,并实现对应的handleMessage方法
     */
    public static Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            if (msg.what == 101) {
                Log.i("MainActivity", "接收到handler消息...");
            }
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        tv = (TextView) findViewById(R.id.tv);
        tv.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                new Thread() {
                    @Override
                    public void run() {
                        // 在子线程中发送异步消息
                        mHandler.sendEmptyMessage(101);
                    }
                }.start();
            }
        });
    }
}

03.子线程中定义Handler

直接在子线程中创建handler,看看会出现什么情况?博客

运行后可以得出在子线程中定义Handler对象出错,难道Handler对象的定义或者是初始化只能在主线程中?其实不是这样的,错误信息中提示的已经很明显了,在初始化Handler对象之前需要调用Looper.prepare()方法。

为何会报错?

Handler的工作是依赖于Looper的,而Looper(与消息队列)又是属于某一个线程(ThreadLocal是线程内部的数据存储类,通过它可以在指定线程中存储数据,其他线程则无法获取到),其他线程不能访问。

Handler就是间接跟线程是绑定在一起了。因此要使用Handler必须要保证Handler所创建的线程中有Looper对象并且启动循环。因为子线程中默认是没有Looper的,所以会报错。

tv.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        new Thread() {
            @Override
            public void run() {
                Handler mHandler = new Handler() {
                    @Override
                    public void handleMessage(Message msg) {
                        if (msg.what == 1) {
                            Log.i(TAG, "在子线程中定义Handler,接收并处理消息");
                        }
                    }
                };
            }
        }.start();
    }
});

如何正确运行。在这里问一个问题,在子线程中可以吐司吗?

答案是可以的,只不过又条件。具体如下所示:

tv.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        new Thread() {
            @Override
            public void run() {
                Looper.prepare();
                Handler mHandler = new Handler() {
                    @Override
                    public void handleMessage(Message msg) {
                        if (msg.what == 1) {
                            Log.i(TAG, "在子线程中定义Handler,接收并处理消息");
                        }
                    }
                };
                //获取Looper对象
                mLooper = Looper.myLooper();
                Looper.loop();
                //在适当的时候退出Looper的消息循环,防止内存泄漏
                mLooper.quit();
            }
        }.start();
    }
});

这样程序已经不会报错,那么这说明初始化Handler对象的时候我们是需要调用Looper.prepare()的,那么主线程中为什么可以直接初始化Handler呢?

难道是主线程创建handler对象的时候,会自动调用Looper.prepare()方法的吗?这个可以看博客

一个线程中,Handler可以是多个,Looper必须只有一个。一个线程如何有多个Looper就会报错,如果你是设计者,你可以想一下,可以有多个消息发送者,但只能有一个消息轮询器处理消息!

避免子线程手动创建looper。下面这种使用方式,是非常危险的一种做法

在子线程中,如果手动为其创建Looper,那么在所有的事情完成以后应该调用quit方法来终止消息循环,否则这个子线程就会一直处于等待的状态,而如果退出Looper以后,这个线程就会立刻终止,因此建议不需要的时候终止Looper。(【 Looper.myLooper().quit(); 】)

new Thread(new Runnable() {
    @Override
    public void run() {
        Looper.prepare();
        Toast.makeText(MainActivity.this, "run on Thread", Toast.LENGTH_SHORT).show();
        Looper.loop();
    }
}).start();

04.post方法使用的说明

Handler的post方法实现很简单,如下所示

mHandler.post(new Runnable() {
    @Override
    public void run() {

    }
});

public final boolean post(Runnable r){
   return  sendMessageDelayed(getPostMessage(r), 0);
}

view的post方法也很简单,如下所示

public boolean post(Runnable action) {
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
        return attachInfo.mHandler.post(action);
    }
    ViewRootImpl.getRunQueue().post(action);
    return true;
}

public void post(Runnable action) {
    postDelayed(action, 0);
}

public void postDelayed(Runnable action, long delayMillis) {
    final HandlerAction handlerAction = new HandlerAction(action, delayMillis);

    synchronized (this) {
        if (mActions == null) {
            mActions = new HandlerAction[4];
        }
        mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction);
        mCount++;
    }
}

可以发现其调用的就是activity中默认保存的handler对象的post方法

05.Handler建议指定线程

先来看一下使用案例,下面这两个有何区别?

Handler handler1 = new Handler();
Handler handler2 = new Handler(Looper.getMainLooper());

06.不建议子线程访问UI

这是因为Android的UI控件不是线程安全的,如果在多线程中并发访问可能会导致UI控件处于不可预期的状态,那么为什么系统不对UI控件的访问加上锁机制呢?缺点有两个:

  • ①首先加上锁机制会让UI访问的逻辑变得复杂
  • ②锁机制会降低UI访问的效率,因为锁机制会阻塞某些线程的执行。

所以最简单且高效的方法就是采用单线程模型来处理UI操作。为什么说子线程不能更新UI?

  1. 子线程是不能直接更新UI的。Android实现View更新有两组方法,分别是invalidate和postInvalidate。
  2. 前者在UI线程中使用,后者在非UI线程即子线程中使用。换句话说,在子线程调用 invalidate 方法会导致线程不安全。
  3. 熟悉View工作原理的人都知道,invalidate 方法会通知 view 立即重绘,刷新界面。
  4. 作一个假设,现在用 invalidate 在子线程中刷新界面,同时UI线程也在用 invalidate 刷新界面,这样会不会导致界面的刷新不能同步?这就是invalidate不能在子线程中使用的原因。博客

07.子线程更新UI方式

7.1 子线程更新UI方式

主要有这些方法

  1. 主线程中定义Handler,子线程通过mHandler发送消息,主线程Handler的handleMessage更新UI
  2. 用Activity对象的runOnUiThread方法
  3. 创建Handler,传入getMainLooper,使用handler.post更新UI
  4. View.post(Runnable r)

相似处:跟进去看源码,发现其实它们的实现原理都还是一样,最终都是通过Handler发送消息来实现的。

7.2 runOnUiThread更新UI

如何使用代码如下所示

new Thread(new Runnable() {
    @Override
    public void run() {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                tv_0.setText("滚犊子++++");
            }
        });
    }
}).start();

看看源码,如下所示

在runOnUiThread程序首先会判断当前线程是否是UI线程,如果是就直接运行,如果不是则post,这时其实质还是使用的Handler机制来处理线程与UI通讯。博客

@Override
public final void runOnUiThread(Runnable action) {
    if (Thread.currentThread() != mUiThread) {
        mHandler.post(action);
    } else {
        action.run();
    }
}

7.3 handler.post更新UI

Looper在哪个线程创建,就跟哪个线程绑定,并且Handler是在他关联的Looper对应的线程中处理消息的。

在子线程中,是否也可以创建一个Handler,并获取MainLooper,从而在子线程中更新UI呢?首先我们看到,在Looper类中有静态对象sMainLooper,并且这个sMainLooper就是在ActivityThread中创建的MainLooper。

//在子线程中通过这个sMainLooper来进行更新UI操作
new Thread(new Runnable() {
    @Override
    public void run() {
        Log.e("yc", "yc 4"+Thread.currentThread().getName());
        //注意这里创建handler时,需要传入looper
        Handler handler = new Handler(getMainLooper());
        handler.post(new Runnable() {
            @Override
            public void run() {
                //Do Ui method
                tv_0.setText("滚犊子————————————---");
                Log.e("yc", "yc 5"+Thread.currentThread().getName());
            }
        });
    }
}).start();

7.4 View.post()更新UI

代码如下所示

tv_0.post(new Runnable() {
    @Override
    public void run() {
        tv_0.setText("滚犊子");
    }
});

源码原理如下所示

public boolean post(Runnable action) {
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
        return attachInfo.mHandler.post(action);
    }
    getRunQueue().post(action);
    return true;
}

private HandlerActionQueue getRunQueue() {
    if (mRunQueue == null) {
        mRunQueue = new HandlerActionQueue();
    }
    return mRunQueue;
}

View.post(Runnable r)使用注意事项

看源码的注释可知:如果view已经attached,则调用ViewRootImpl中的ViewRootHandler,放入主线程Lopper等待执行。如果detach,则将其暂存在RunQueue当中,等待其它线程取出执行。

View.post(Runnable r)很多时候在子线程调用,用于进行子线程无法完成的操作,或者在该方法中通过getMeasuredWidth()获取view的宽高。需要注意的是,在子线程调用该函数,可能不会被执行,原因是该view不是attached状态。博客

08.解决Handler内存泄漏

Handler内存泄漏问题代码

public class MainActivity extends AppCompatActivity {
    private Handler mHandler = new Handler();
    private TextView mTextView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTextView = (TextView) findViewById(R.id.text);        //模拟内存泄露
        mHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                mTextView.setText("yangchong");
            }
        }, 2000);
    }
}

造成内存泄漏原因分析

上述代码通过内部类的方式创建mHandler对象,此时mHandler会隐式地持有一个外部类对象引用这里就是MainActivity,当执行postDelayed方法时,该方法会将你的Handler装入一个Message,并把这条Message推到MessageQueue中,MessageQueue是在一个Looper线程中不断轮询处理消息,那么当这个Activity退出时消息队列中还有未处理的消息或者正在处理消息,而消息队列中的Message持有mHandler实例的引用,mHandler又持有Activity的引用,所以导致该Activity的内存资源无法及时回收,引发内存泄漏。

解决Handler内存泄露主要2点

  1. 有延时消息,要在Activity销毁的时候移除Messages
  2. 匿名内部类导致的泄露改为匿名静态内部类,并且对上下文或者Activity使用弱引用。博客

解决方案:第一种解决办法

要想避免Handler引起内存泄漏问题,需要我们在Activity关闭退出的时候的移除消息队列中所有消息和所有的Runnable。

上述代码只需在onDestroy()函数中调用mHandler.removeCallbacksAndMessages(null);就行了。

@Override
protected void onDestroy() {
    super.onDestroy();
    if(handler!=null){
        handler.removeCallbacksAndMessages(null);
        handler = null;
    }
}

解决方案:第二种解决方案

使用弱引用解决handler内存泄漏问题,关于代码案例,可以参考我的开源项目:https://github.com/yangchong211/YCAudioPlayer中的utils-share包下的ShareDialog代码

//自定义handler
public static class HandlerHolder extends Handler {
    WeakReference<OnReceiveMessageListener> mListenerWeakReference;
    /**
     * @param listener 收到消息回调接口
     */
    HandlerHolder(OnReceiveMessageListener listener) {
        mListenerWeakReference = new WeakReference<>(listener);
    }

    @Override
    public void handleMessage(Message msg) {
        if (mListenerWeakReference!=null && mListenerWeakReference.get()!=null){
            mListenerWeakReference.get().handlerMessage(msg);
        }
    }
}

//创建handler对象
private HandlerHolder handler = new HandlerHolder(new OnReceiveMessageListener() {
    @Override
    public void handlerMessage(Message msg) {
        switch (msg.what){
            case 1:
                TextView textView1 = (TextView) msg.obj;
                showBottomInAnimation(textView1);
                break;
            case 2:
                TextView textView2 = (TextView) msg.obj;
                showBottomOutAnimation(textView2);
                break;
        }
    }
});

//发送消息
Message message = new Message();
message.what = 1;
message.obj = textView;
handler.sendMessageDelayed(message,time);


即推荐使用静态内部类 + WeakReference 这种方式。每次使用前注意判空。
贡献者: yangchong211
上一篇
3.9自定义ViewGroup控件
下一篇
4.2消息机制流程分析