Handler 常见打开姿势及踩坑分析

1. 引文

handler 基本定义:先直接看看最权威的官方定义

  • A Handler allows you to send and process {@link Message} and Runnable
  • objects associated with a thread's {@link MessageQueue}. Each Handler
  • instance is associated with a single thread and that thread's message
  • queue. When you create a new Handler it is bound to a {@link Looper}.
  • It will deliver messages and runnables to that Looper's message
  • queue and execute them on that Looper's thread.
  • <p>There are two main uses for a Handler: (1) to schedule messages and
  • runnables to be executed at some point in the future; and (2) to enqueue
  • an action to be performed on a different thread than your own.

大意就是:
Handler 允许与唯一的线程绑定,主要作用:

  1. 允许延期执行任务
  2. 允许切换线程执行任务

主要API:
post(Runnable)、
postAtTime(Runnable、long)、
postDelayed(Runnable、long)、
sendEmptyMessage(int)、
sendMessage(msg)、
sendMessageAtTime(msg、long)
sendMessageDelayed(msg、long)方法来完成的。

** 文末会给简单的demo示例 **

2. 先看常见错误

 @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        tv = findViewById(R.id.tv);
        Log.d(TAG, "1--》" + Thread.currentThread().getId());

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (Exception e) {
                    Log.e(TAG, e.toString());
                }
                tv.setText("oncreate方法,无耗时,改变Tv 文本");

            }
        }).start();
    }

如果手抖写出上述代码,那恭喜你,喜提下列报错:

 android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
        at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6891)
        at android.view.ViewRootImpl.invalidateChildInParent(ViewRootImpl.java:1083)
        at android.view.ViewGroup.invalidateChild(ViewGroup.java:5205)
        at android.view.View.invalidateInternal(View.java:13656)
        at android.view.View.invalidate(View.java:13620)

这个比较简单,子线程不允许更新Ui (really?)

3. 正常打开姿势

android更新UI的姿势有:
runOnUiThread();
handler.post();
handler.sendMessage();
view.post();
简单的demo界面


image.png

3.1 runOnUiThread

Activity中的API ,使用简单粗暴,不过必须在Activity的上下文环境使用

 public void runOnUIBtnOnclick(View view) {
     runOnUiThread(new Runnable() {  // Activity 方法,只能在Activity中使用
         @Override
         public void run() {
             tv.setText("runOnUiThread 改变Tv 文本"); 
         }
     });
    }

3.2 handler.post()

使用比较方便,主要用来切换线程。
基本的使用

 Handler handler = new Handler(Looper.myLooper());
 handler.post(new Runnable() {
                    @Override
                    public void run() {
                        tv.setText("切换线程,改变Tv 文本"); 
                    }
                });

来看下线程是如何切换的

 @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        tv = findViewById(R.id.tv);
        Log.d(TAG, "1--》" + Thread.currentThread().getId());
...
 public void changeThreadBtnOnclick(View view) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                Log.d(TAG, "2---》" + Thread.currentThread().getId());
                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        Log.d(TAG, "3---》" + Thread.currentThread().getId());
                        tv.setText("切换线程,改变Tv 文本"); 
                    }
                });
            }
        }).start();
    }

测试的结果是:

2020-12-31 20:40:19.544 8170-8170/com.gavin.handlerdemo D/MainActivity: 1--》1
2020-12-31 20:42:15.357 8170-8208/com.gavin.handlerdemo D/MainActivity: 2---》346
2020-12-31 20:42:15.365 8170-8170/com.gavin.handlerdemo D/MainActivity: 3---》1

可以看出,2处的线程的确是子线程,3处是主线程
总结一下:
注意不是写在子线程方法里的就是子线程执行....

3.3 handler.sendMessage()

这个主要用来传递消息,消息常见的做法是子线程向主线程发送来通知更新UI,不过,主线程一样可以向子线程发消息。
下面造一个死循环,主线程向子线程发消息,子线程收到后,回传主线程,然后循环下去。 这样演示的目的,是为了警示一下,退出时,不反注册这个callback 后果可能很严重。

 private static String TAG = "SecActivity";
    TextView tv;
    Handler threadHandler;

    Handler mainHandler = new Handler(Looper.myLooper()) {
        @Override
        public void handleMessage(@NonNull Message msg) {
            if (msg.obj != null) {
                Log.e(TAG,  "主线程收到了呼叫, \"" + msg.obj + "\"" );
            } else {
                Log.e(TAG,  "主线程收到了呼叫");
            }
            Message message = new Message();
            message.obj = "这是来自主线程的呼叫!!!";
            if (threadHandler != null) {
                threadHandler.sendMessageDelayed(message, 1000);
            }
        }
      };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_sec);
        tv = findViewById(R.id.tv);
        HandlerThread thread = new HandlerThread("HandlerThread"); // 使用HandlerThread主要是方便获取Looper
        thread.start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                threadHandler = new Handler(thread.getLooper()) {
                    @Override
                    public void handleMessage(@NonNull Message msg) {
                        if (msg.obj != null) {
                            Log.e(TAG,  "子线程收到了呼叫, \"" + msg.obj + "\"" );
                        } else {
                            Log.e(TAG,  "子线程收到了呼叫");
                        }
                        Message message = new Message();
                        message.obj = "这是来自子线程的呼叫!!!";
                        mainHandler.sendMessageDelayed(message,1000);

                    }
                };
            }
        }).start();
    }

    public void testMsgTransform(View view) {
        mainHandler.sendEmptyMessageDelayed(0, 100); // 发送一个空消息,被自身的handleMessage 捕获。谁发送 谁接收。
    }

得到的结果:

2020-12-31 20:48:29.521 8170-8170/com.gavin.handlerdemo E/SecActivity: 主线程收到了呼叫
2020-12-31 20:48:30.523 8170-8221/com.gavin.handlerdemo E/SecActivity: 子线程收到了呼叫, "这是来自主线程的呼叫!!!"
2020-12-31 20:48:31.526 8170-8170/com.gavin.handlerdemo E/SecActivity: 主线程收到了呼叫, "这是来自子线程的呼叫!!!"
2020-12-31 20:48:32.527 8170-8221/com.gavin.handlerdemo E/SecActivity: 子线程收到了呼叫, "这是来自主线程的呼叫!!!"
2020-12-31 20:48:33.529 8170-8170/com.gavin.handlerdemo E/SecActivity: 主线程收到了呼叫, "这是来自子线程的呼叫!!!"
2020-12-31 20:48:34.531 8170-8221/com.gavin.handlerdemo E/SecActivity: 子线程收到了呼叫, "这是来自主线程的呼叫!!!"
2020-12-31 20:48:35.533 8170-8170/com.gavin.handlerdemo E/SecActivity: 主线程收到了呼叫, "这是来自子线程的呼叫!!!"
2020-12-31 20:48:36.537 8170-8221/com.gavin.handlerdemo E/SecActivity: 子线程收到了呼叫, "这是来自主线程的呼叫!!!"
2020-12-31 20:48:37.537 8170-8170/com.gavin.handlerdemo E/SecActivity: 主线程收到了呼叫, "这是来自子线程的呼叫!!!"

总结一下:

  1. Handler 可以有很多个,不过每个Handler 只能绑定一个线程, 只能有一个Looper,Handler 发出的消息,最终要由自己的handleMessage 消费,是真正的肥水不流外人田。

  2. 即使你关闭了页面,你会发现,这个死循环还在,它的生命周期是app的生命周期,不关闭,后果真的很严重。
    关闭方法:

  @Override
    protected void onDestroy() {
        super.onDestroy();
        //如果这里不移除 callback,这个callback会一直存在,容易引发内存泄漏
        mainHandler.removeCallbacksAndMessages(null);
        threadHandler.removeCallbacksAndMessages(null);
    }

3.4 view.post()

  public void viewpostTest(View view) {
        tv.post(new Runnable() {
            @Override
            public void run() {
                tv.setText("view.post方法,改变Tv 文本");
            }
        });
    }

view.post与Handler.post 基本类似,不过也有所不同,这里不继续展开了。

4. 子线程真的就不能更新Ui 么?

答案是一般都不能更新,但是面试时,你就得回答可以更新。。。
可以先验证下效果。

 @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        tv = findViewById(R.id.tv);
        Log.d(TAG, "1--》" + Thread.currentThread().getId());

         new Thread(new Runnable() {
            @Override
            public void run() {
                tv.setText("oncreate方法,无耗时,改变Tv 文本");
            }
        }).start();
    }

可以看出页面并未报错,正常展示。
不过如果你加上延迟,就是这么一段话

   new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                tv.setText("oncreate方法,无耗时,改变Tv 文本");
            }
        }).start();

就会喜提上面提到的崩溃了。
为啥呢?

看报错提示:

 android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
        at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6891)

这个报错是ViewRootImpl 给出的。
流程是这样:
onreate方法,并不会给出这个效验,因此程序正常执行,Ui 也确实更新了。
但在Activity 的onresume 阶段,会调用ViewRootImpl 的方法,然后checkThread去校验这个更新Ui的线程是否为主线程。
具体的代码相对复杂,简单讲的话,可以看下onresume的注释


image.png

大意是View 变为可见时,会调用ViewRootImpl performTraversals方法。

因此如果子线程没有耗时,初始进入时,还没来的及执行onresume中的方法,因此在ViewRootImpl checkThread 执行前已经抢先完成了UI 更新,就不会有问题。 但一旦执行耗时方法,就会触发checkThread机制。

5. 内存泄漏问题

以上代码,仅作为测试demo是没有问题的。但上线的话,会存在内存泄漏问题。
( 这里假设ondestroy 时,没有removeCallbacksAndMessages)
原因主要是:
通过匿名内部类实例化的Handler类对象,隐式的持有外部Activity对象。
Activity退出时,Handler 的生命周期与APP 生命周期相同,因其持有Activity实例,导致Activity 也不能被GC 回收,引发内存泄漏。

解决方案:
通过创建继承Handler的静态内部类,并使用弱引用来避免Handler对象持有外部类对象的强引用。

 
    public MyHandler mHandler = new MyHandler(this);
 
    //静态内部类
    static class MyHandler extends Handler {
 
        private WeakReference<Context> weakReference;
 
        MyHandler(Context context) {
            weakReference = new WeakReference<>(context);
        }
 
     @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            MainActivity mainActivity = mWeakReference.get();
            if (mainActivity != null) {
                tv.setText(msg.obj + "");
        }

6. demo 地址

https://github.com/jjbheda/handlerDemo

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,427评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,551评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,747评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,939评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,955评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,737评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,448评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,352评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,834评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,992评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,133评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,815评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,477评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,022评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,147评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,398评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,077评论 2 355

推荐阅读更多精彩内容