Handler 面试相关

Handler机制整体流程;Looper.loop()为什么不会阻塞主线程;IdHandler(闲时机制);postDelay()的具体实现;post()与sendMessage()区别;使用Handler需要注意什么问题,怎么解决的?

https://www.jianshu.com/p/a77af781f678

第一个问题:Android中为什么主线程不会因为Looper.loop()里的死循环卡死?

loop在没有消息的时候会沉眠,释放CPU,只有在有消息的时候在会被再次唤醒,阻碍线程的不是Looper.loop(),是loop中调用handler进行主线程操作才阻碍了线程

 Activity 的生命周期都有对应的 case 条件了,ActivityThread 有个 getHandler 方法,得到这个 handler 就可以发送消息,然后 loop 里就分发消息,然后就发给 handler, 然后就执行到 H(Handler )里的对应代码。所以这些代码就不会卡死~,有消息过来就能执行。举个例子,在 ActivityThread 里的内部类 ApplicationThread 中就有很多 sendMessage 的方法。 

简单的来说:ActivityThread的main方法主要就是做消息循环,一旦退出消息循环,那么你的程序也就可以退出了。 

从消息队列中取消息可能会阻塞,取到消息会做出相应的处理。如果某个消息处理时间过长,就可能会影响UI线程的刷新速率,造成卡顿的现象。

如果你了解下linux的epoll你就知道为什么不会被卡住了,先说结论:阻塞是有的,但是不会卡住 

主要原因有2个

  1. epoll模型 

当没有消息的时候会epoll.wait,等待句柄写的时候再唤醒,这个时候其实是阻塞的。

  1. 所有的ui操作都通过handler来发消息操作。 

比如屏幕刷新16ms一个消息,你的各种点击事件,所以就会有句柄写操作,唤醒上文的wait操作,所以不会被卡死了。

这里涉及线程,先说说说进程/线程: 

进程:每个app运行时前首先创建一个进程,该进程是由Zygote fork出来的,用于承载App上运行的各种Activity/Service等组件。进程对于上层应用来说是完全透明的,这也是google有意为之,让App程序都是运行在Android Runtime。大多数情况一个App就运行在一个进程中,除非在AndroidManifest.xml中配置Android:process属性,或通过native代码fork进程。 

线程:线程对应用来说非常常见,比如每次new Thread().start都会创建一个新的线程。该线程与App所在进程之间资源共享,从Linux角度来说进程与线程除了是否共享资源外,并没有本质的区别,都是一个task_struct结构体,在CPU看来进程或线程无非就是一段可执行的代码,CPU采用CFS调度算法,保证每个task都尽可能公平的享有CPU时间片。

 

总结:

这里涉及线程,先说说说进程/线程,进程:每个app运行时前首先创建一个进程,该进程是由Zygote fork出来的,用于承载App上运行的各种Activity/Service等组件。进程对于上层应用来说是完全透明的,这也是google有意为之,让App程序都是运行在Android Runtime。大多数情况一个App就运行在一个进程中,除非在AndroidManifest.xml中配置Android:process属性,或通过native代码fork进程。

线程:线程对应用来说非常常见,比如每次new Thread().start都会创建一个新的线程。该线程与App所在进程之间资源共享,从Linux角度来说进程与线程除了是否共享资源外,并没有本质的区别,都是一个task_struct结构体,在CPU看来进程或线程无非就是一段可执行的代码,CPU采用CFS调度算法,保证每个task都尽可能公平的享有CPU时间片。

有了这么准备,再说说死循环问题:

对于线程既然是一段可执行的代码,当可执行代码执行完成后,线程生命周期便该终止了,线程退出。而对于主线程,我们是绝不希望会被运行一段时间,自己就退出,那么如何保证能一直存活呢?简单做法就是可执行代码是能一直执行下去的,死循环便能保证不会被退出,例如,binder线程也是采用死循环的方法,通过循环方式不同与Binder驱动进行读写操作,当然并非简单地死循环,无消息时会休眠。但这里可能又引发了另一个问题,既然是死循环又如何去处理其他事务呢?通过创建新线程的方式。

真正会卡死主线程的操作是在回调方法onCreate/onStart/onResume等操作时间过长,会导致掉帧,甚至发生ANR,looper.loop本身不会导致应用卡死。

 

 

第二个问题?

 

 

第三个问题:postDelay()的具体实现

源码中调用 sendMessageDelayed(getPostMessage(r), delayMillis); 这个方法 参数一是 Message 对象,参数二是延时的时间

阅读之前先问大家一个问题:Handler.postDelayed()是先delay一定的时间,然后再放入messageQueue中,还是先直接放入MessageQueue中,然后在里面wait delay的时间?为什么?如果你不答不上来的话,那么此文值得你看看?

答案:先发消息,在延时等待

https://www.cnblogs.com/ldq2016/p/9260783.html

 

先说一下:是先发消息后延时的,MessageQueue.next() 在这个方法中进行了延时

1. postDelay()一个10秒钟的Runnable A、消息进队,MessageQueue调用nativePollOnce()阻塞,Looper阻塞;

2. 紧接着post()一个Runnable B、消息进队,判断现在A时间还没到、正在阻塞,把B插入消息队列的头部(A的前面),然后调用nativeWake()方法唤醒线程;

3. MessageQueue.next()方法被唤醒后,重新开始读取消息链表,第一个消息B无延时,直接返回给Looper;

4. Looper处理完这个消息再次调用next()方法,MessageQueue继续读取消息链表,第二个消息A还没到时间,计算一下剩余时间(假如还剩9秒)继续调用nativePollOnce()阻塞;

5. 直到阻塞时间到或者下一次有Message进队;

这样,基本上就能保证Handler.postDelayed()发布的消息能在相对精确的时间被传递给Looper进行处理而又不会阻塞队列了。

 

Handler.postDelayed()的调用路径

一步一步跟一下Handler.postDelayed()的调用路径:

  1. Handler.postDelayed(Runnable r, long delayMillis)
  2. Handler.sendMessageDelayed(getPostMessage(r), delayMillis)
  3. Handler.sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis)
  4. Handler.enqueueMessage(queue, msg, uptimeMillis)
  5. MessageQueue.enqueueMessage(msg, uptimeMillis)

最后发现Handler没有自己处理Delay,而是交给了MessageQueue处理

 

总结:另外,这里在阅读原文的基础上添加一点思考内容:

MessageQueue会根据post delay的时间排序放入到链表中,链表头的时间小,尾部时间最大。因此能保证时间Delay最长的不会block住时间短的。当每次post message的时候会进入到MessageQueue的next()方法,会根据其delay时间和链表头的比较,如果更短则,放入链表头,并且看时间是否有delay,如果有,则block,等待时间到来唤醒执行,否则将唤醒立即执行。

所以handler.postDelay并不是先等待一定的时间再放入到MessageQueue中,而是直接进入MessageQueue,以MessageQueue的时间顺序排列和唤醒的方式结合实现的。使用后者的方式,我认为是集中式的统一管理了所有message,而如果像前者的话,有多少个delay message,则需要起多少个定时器。前者由于有了排序,而且保存的每个message的执行时间,因此只需一个定时器按顺序next即可。

 

第四个问题:post()与sendMessage()区别

post()与sendMessage()区别,本质上是没区别的,看源码都是调用 sendMessageDelayed(getPostMessage(r), delayMillis); 这个方法 参数一是 Message 对象,参数二是延时的时间 最后走的 sendMessageAtTime 这个方法

只是用法上的区别:

 private TextView tv_up;
    private String new_str = "";

    /*post方法解决UI更新问题handler创建方式*/
    private Handler handler_post = new Handler();

    /*sendMessage方法解决UI更新问题handler创建方式*/
    Handler handler_senM = new Handler() {
        public void handleMessage(Message msg) {
            if (msg.what == 1) {
                /*sendMessage方法更新UI的操作必须在handler的handleMessage回调中完成*/
                tv_up.setText(new_str);
            }
        };
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        new Thread(new Runnable() {
            @Override
            public void run() {
                new_str = "更新UI";

                /*sendMessage方法解决UI更新发送消息给handler(主线程中的handler)*/
                handler_senM.sendEmptyMessage(1);


                /*post方法解决UI更新,直接在runnable里面完成更新操作,这个任务会被添加到handler所在线程的消息队列中,即主线程的消息队列中*/
                handler_post.post(new Runnable() {
                    @Override
                    public void run() {
                        tv_up.setText(new_str);
                    }
                });
            }
        }).start();
    }

第五个问题:使用Handler需要注意什么问题,怎么解决的?

https://blog.csdn.net/weixin_30500105/article/details/95117094

注意内存泄漏,加入 WeakReference 弱引用,把Activity 放到弱引用中,

 

uploading.4e448015.gif转存失败重新上传取消

由上图大家可以看到,提示说 Handler 这样写就是一个内部类,其实我也没搞明白,这怎么就成了内部类了,不过话说回来既然是内部类,这么写就有问题了,大家都知道内部类会隐形(或者说默认)持有外部类的引用,这样当外部类销毁的时候,内部类由于被执行中的线程引用了,这时候外部类就不会被GC 回收,就会造成内存泄漏。

 

我们知道,Handler是和Looper以及MessageQueue一起工作的,在Android中,一个应用启动后,系统默认会创建一个为主线程服务的Looper对象,该Looper对象用于处理主线程的所有Message对象,它的生命周期贯穿于整个应用的生命周期。在主线程中使用的Handler都会默认绑定到这个Looper对象。在主线程中创建Handler对象,它会立即关联到主线程Looper对象的MessageQueue,这时发送到MessageQueue中的Message对象都会只有这个Handler对象的引用,这样在Looper处理消息时常能回调到Handler的handlerMessage方法。因此,如果Message还没有被处理完成,那么Handler对象也就不会被垃圾回收。

  在上面的代码中,将Handler的实例声明为HandlerActivity类的内部类。而在Java语言中,非静态内部匿名类会持有外部类的一个隐式的引用,这样就可能会导致外部类无法被垃圾回收。因此,最终由于MessageQueue中Message还没处理完成,就会持有Handler对象的引用,而非静态的Handler对象会持有外部类HandlerActivity的引用,这个Activity无法被垃圾回收,从而导致内存泄漏。

https://www.jianshu.com/p/804e774d9f76

①先说handler导致activity内存泄露的原因:

handler发送的消息在当前handler的消息队列中,如果此时activity finish掉了,那么消息队列的消息依旧会由handler进行处理,若此时handler声明为内部类(非静态内部类),我们知道内部类天然持有外部类的实例引用,这样在GC垃圾回收机制进行回收时发现这个Activity居然还有其他引用存在,因而就不会去回收这个Activity,进而导致activity泄露。

②为何handler要定义为static?

因为静态内部类不持有外部类的引用,所以使用静态的handler不会导致activity的泄露

③为何handler要定义为static的同时,还要用WeakReference 包裹外部类的对象?

这是因为我们需要使用外部类的成员,可以通过"activity. "获取变量方法等,如果直接使用强引用,显然会导致activity泄露。

 

静态内部类的Handler的写法

解决内存泄露

public class MainActivity extends AppCompatActivity {

    TextView mTextView;

    MyHandler mMyHandler;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTextView = findViewById(R.id.tv_handler);
        mMyHandler = new MyHandler(MainActivity.this);
        doInBackground();
    }

    private static class MyHandler extends Handler {
        private WeakReference mWeakReference;

        public MyHandler(MainActivity activity) {
            mWeakReference = new WeakReference<>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            MainActivity mainActivity = mWeakReference.get();
            switch (msg.what) {
                case 1:
                    if (mainActivity != null) {
                        mainActivity.mTextView.setText(msg.obj + "");
                    }
                    break;
                default:
                    break;
            }
        }
    }

    private void doInBackground() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    //模拟耗时操作
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //耗时操作完成,发送消息给UI线程
                Message msg = Message.obtain();
                msg.what = 1;
                msg.obj = "更新UI";
                mMyHandler.sendMessage(msg);
            }
        }).start();
    }

}

请忽略activity的弱引用,因为关系不太,之所以弱引用activity,是为了访问activity对象的成员

 

 

抛出一个问题,handler是怎么进行线程间的通讯的呢?

可以从下面这个方面回答,把知识点提炼一下:

一个handler 只能绑定一个 looper,但是多个线程(也就是多个 handler )可以往同一个 looper 发送消息,发消息的时候都是通过aHandler.sendMessage() 或者 aHandler.post() 的方式,这里发送出去的消息会持有该发送 handler 的引用,如这里发送出去的消息就会持有 aHandler 的引用,把消息发送到 aHandler 对应的 looper 内部维护的 MessageQueue 中,轮询分发消息的时候,就会根据 message 找到所持有的 Handler 的引用,找到对应的 Handler 然后传递消息。

你可能感兴趣的:(面试)