Handler的前世今生——面试篇

言不必当,极口称是,
未交此人,故意底毁;
卑庸可耻,不足与论事。
——《冰鉴》


1. 背景

2020年注定是不平凡的一年,“金三银四”怕是被疫情给变了性质,希望面试者都能进入自己心仪的公司。

今天面试了一个5年左右的Android开发者,感觉java基础和Android知识都比较不错。在面试Android岗位时,Handler总是绕不开的一个话题 (PS:如果一切顺利,就不存在这篇文章啦)。

下面是我提问的问题:

  1. 大致讲一下Handler的工作原理;
  2. 一个线程可以有多个Looper吗? 怎么保证的?
  3. send 和 post 这两种发送消息的方式(刚才面试者说到本质上post还是使用的send方式),你认为两者在使用场景上有什么不同?

晴天霹雳…

原来还有人认为Handler.post(Runnable) 这种方式是处理异步的,这是典型的形式主义错误。


2.再谈post方式

Handler的post发送消息不是处理异步消息的。

2.1 post方式不是异步
 	/**
     * Handle system messages here.
     */
    public void dispatchMessage(Message msg) {
    	// post 方式发送的消息
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }
 private static void handleCallback(Message message) {
        message.callback.run();
    }

通过上述代码,我们发现此处并没有出现Thread的身影,更没有出现线程启动的start()这足以证明post方式是同步方式,其Runnable中的代码执行和Handler所在的线程一致。(PS:可以联想Thread 不启动start,直接调用run()的逻辑)


2.1 send 和 post方式的使用场景
2.1.1 send方式

结合我们在开发中的使用场景,如果我们发送消息的类型比较多,我们一般更倾向于使用send方式 ,然后结合下面的代码根据情况判断执行逻辑。(PS : 如果阅读过源码,我们会发现源码中很多使用该种方式的)

private static Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what) {
                case 0:
                    //TODO 
                    break;
                case 1:
                	// TODO
                	break;
                ...
            }
        }
    };
2.1.2 post方式

与其认为Post Runnable,倒不如直接理解为Post 出 代码块

如果消息类型只有1个,使用上述代码就显得不太合理啦。这时候就体现出post的快捷方便啦。

new Handler().post(new Runnable() {
        @Override
        public void run() {
              //TODO 
        }
    });

相信大家在实际的开发中,也能体会到两者用法上的稍许区别。

一定不要理解错误,即:不要在Handler.post() 或者 Handler.postDelay()中做耗时操作。

3. Handler 的内存泄漏

3.1 什么是内存泄漏?

简而言之,申请的内存,用完不能如数归还甚至(无法归还)

假如整个内存就是一家银行(这里的 “借” 指的是无息贷款啊)

  1. 正常现象: 借多少及时还多少;
  2. 内存泄漏:借多少还一部分(甚至不还) [不及时还款照样是内存泄漏],(银行倒闭了你才有偿还能力那还有个屁用)
  3. 内存溢出:没有人能从银行借出来钱,即 银行倒闭

内存泄漏的最终结果就是内存溢出(Out of memory)

点击至:知乎高赞

3.2 Handler的内存泄漏

在面试过程中,我发现有很大一部分的面试者,对于Handler的内存泄漏存在认识不足的情况,这里特地总结一下,希望能够帮助大家。
这里不再对GC相关知识展开介绍。

Handler的内存泄漏问题主要发生在两个场景:

  1. (匿名)内部类默认持有外部类的引用;
  2. 逻辑漏洞导致;

这里简要分析一下:

场景1: 在开发中存在这两种写法(PS:AS会自动提示我们可能发生内存泄漏)

This Handler class should be static or leaks might occur

a.匿名内部类的形式
	private Handler handler = new Handler() {
        public void handleMessage(android.os.Message msg) {
            // TODO 
        }
    };

对于a形式,我的同事曾经直接在变量前加上static关键字,然后底气十足的告诉我这样可以吧… (有此想法的童鞋,麻烦再翻译一下上面那段英文,关键是AS的提示依然健在)

b.内部类的形式
  class MyHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            // TODO 
        }
    }

相信大家总会遇见各种奇葩队友,他知道将内部类声明为静态的,所以出现了下面的局面(场面一度失控)

public class TestHandlerActivity extends AppCompatActivity {
	// 这里应该是不得已而为之,将其声明为静态变量
    private static TextView tvHello;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test_handler);
        tvHello = findViewById(R.id.tv_hello);
    }
	// 注意这里:确实是静态内部类
    static class TestHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            tvHello.setText("Hello");
        }
    }
    
 	@Override
    protected void onDestroy() {
        super.onDestroy();
        // 补救措施
        tvHello = null;
    }   
}

这种写法就会导致所有的View全部要声明为static类型。最可恨的是:内存泄漏的问题将会更加严重。

所有的View在初始化的时候,都会有Context,即当前Activity。
此时将View设置为static,若Activity生命周期结束后,该静态变量没有被回收,则持有Activity,将导致Activity无法被回收。所以内存泄漏就出现啦。
当然有解决办法:在onDestory()中将该变量置为空即可。

如果View过多,鬼见愁啊…

建议大家深入了解static关键字的含义

关于更多的内存泄漏场景:Android 内存优化


公布最佳答案:

弱引用(WeakReference) : GC执行则必被回收。

关于Java的四种引用类型:强、软、弱、虚

 static class MyHandler extends Handler {
        WeakReference<Activity> weakReference;

        public MyHandler(Activity activity) {
            weakReference = new WeakReference<>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            
            TestHandlerActivity activity = (TestHandlerActivity) weakReference.get();
            // 有人对此有疑问
            if(activity != null){
                activity.tvHello.setText("Hello");
            }

        }
    }

可能有的同学会存在疑惑?如果GC回收了Activity,那更新UI的操作是不是就出问题啦。
这里我们要明确一点,大家要搞明白在什么情况下该Activity才会被回收,自然就明白啦。(PS : 就好比银行让你还款也是在还款日让你还而不是随便的让你还啊)

上面这种场景的引用链

执行耗时任务的线程 ----> Handler ----> Activity
该线程一定持有Handler的引用,否则无法发消息。


场景2: 逻辑漏洞问题

想象一下,银行已经下班啦(Activity被销毁),
有些人还在赶往银行的路上——Handler的延时功能 sendMessageDelayed()postDelayed()
另一些人还在银行门口的排队呢—— MessageQueue中的待处理消息

引用链:

MessageQueue —> Message —> Handler —> Activity

解决办法:

  @Override
    protected void onDestroy() {
        super.onDestroy();
        // 移除所有消息
        if(handler != null){
            handler.removeCallbacksAndMessages(null);
            handler = null;
        }
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章