美文网首页Android面试题面试基础架构
字节跳动一面:Android中为什么主线程不会因为Looper.

字节跳动一面:Android中为什么主线程不会因为Looper.

作者: 木木玩Android | 来源:发表于2020-11-18 09:13 被阅读0次

    前言

    现在大厂面试一般都有 3-4 轮技术面,1 轮的 HR 面。就字节跳动而言的话,是有 4 轮技术面的,前两轮主要是问基础和项目实现,第 3 轮是交叉面,两个面试官,主要是问项目实现和拓展。第 4 轮是部门老大面,主要就问一些架构、技术和业务的理解、个人发展比较抽象的东西了。再就是最后的HR面,HR面主要就是跟你聊聊天,看看你的个人稳定性、价值观、主动性之类的。

    前些天就刚好和我一个在字节跳动的经常做面试官的朋友聊到了面试这一块的事情,颇有感概。他一般负责的都是一面,在面试中他通常都会问道一个问题:“Android中为什么主线程不会因为Looper.loop()里的死循环卡死?”,然后再深入追问,通过这个问题可以考察到面试者4个方面的知识:**Process/Thread,Android Binder IPC,Handler/Looper/MessageQueue消息机制,Linux pipe/epoll机制。这些知识点也正是字节跳动非常注重的技术点,而就在这问题上他刷掉了90%的面试者。

    深入细节

    app程序入口中为主线程准备好了消息队列


    而根据Looper.loop()源码可知里面是一个死循环在遍历消息队列取消息


    而且并也没看见哪里有相关代码为这个死循环准备了一个新线程去运转,但是主线程却并不会因为Looper.loop()中的这个死循环卡死,为什么呢?

    举个例子,像Activity的生命周期这些方法这些都是在主线程里执行的吧,那这些生命周期方法是怎么实现在死循环体外能够执行起来的?

    问题解析

    根据上述表述,总结需要具体回答的三个方面

    • 1.Android中为什么主线程不会因为Looper.loop()里的死循环卡死?
    • 2.没看见哪里有相关代码为这个死循环准备了一个新线程去运转?
    • 3.Activity的生命周期这些方法这些都是在主线程里执行的吧,那这些生命周期方法是怎么实现在死循环体外能够执行起来的?

    回答解析
    (1) Android中为什么主线程不会因为Looper.loop()里的死循环卡死?

    这里涉及线程,先说说说进程/线程,进程:每个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本身不会导致应用卡死。

    (2) 没看见哪里有相关代码为这个死循环准备了一个新线程去运转?

    事实上,会在进入死循环之前便创建了新binder线程,在代码ActivityThread.main()中:

    public static void main(String[] args) {
            ....
    
            //创建Looper和MessageQueue对象,用于处理主线程的消息
            Looper.prepareMainLooper();
    
            //创建ActivityThread对象
            ActivityThread thread = new ActivityThread(); 
    
            //建立Binder通道 (创建新线程)
            thread.attach(false);
    
            Looper.loop(); //消息循环运行
            throw new RuntimeException("Main thread loop unexpectedly exited");
        }
    

    thread.attach(false);便会创建一个Binder线程(具体是指ApplicationThread,Binder的服务端,用于接收系统服务AMS发送来的事件),该Binder线程通过Handler将Message发送给主线程,具体过程可查看 startService流程分析,这里不展开说,简单说Binder用于进程间通信,采用C/S架构。

    另外,ActivityThread实际上并非线程,不像HandlerThread类,ActivityThread并没有真正继承Thread类,只是往往运行在主线程,该人以线程的感觉,其实承载ActivityThread的主线程就是由Zygote fork而创建的进程。

    主线程的死循环一直运行是不是特别消耗CPU资源呢? 其实不然,这里就涉及到Linux pipe/e****poll机制,简单说就是在主线程的MessageQueue没有消息时,便阻塞在loop的queue.next()中的nativePollOnce()方法里,详情见Android消息机制1-Handler(Java层),此时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生,通过往pipe管道写端写入数据来唤醒主线程工作。这里采用的epoll机制,是一种IO多路复用机制,可以同时监控多个描述符,当某个描述符就绪(读或写就绪),则立刻通知相应程序进行读或写操作,本质同步I/O,即读写是阻塞的。 所以说,主线程大多数时候都是处于休眠状态,并不会消耗大量CPU资源。

    (3) Activity的生命周期是怎么实现在死循环体外能够执行起来的?

    ActivityThread的内部类H继承于Handler,通过handler消息机制,简单说Handler机制用于同一个进程的线程间通信。

    Activity的生命周期都是依靠主线程的Looper.loop,当收到不同Message时则采用相应措施:
    在H.handleMessage(msg)方法中,根据接收到不同的msg,执行相应的生命周期。

    比如收到msg=H.LAUNCH_ACTIVITY,则调用ActivityThread.handleLaunchActivity()方法,最终会通过反射机制,创建Activity实例,然后再执行Activity.onCreate()等方法;
    再比如收到msg=H.PAUSE_ACTIVITY,则调用ActivityThread.handlePauseActivity()方法,最终会执行Activity.onPause()等方法。 上述过程,我只挑核心逻辑讲,真正该过程远比这复杂。

    主线程的消息又是哪来的呢?当然是App进程中的其他线程通过Handler发送给主线程,请看接下来的内容:


    最后,从进程与线程间通信的角度,通过一张图加深大家对App运行过程的理解:

    system_server进程是系统进程,java framework框架的核心载体,里面运行了大量的系统服务,比如这里提供ApplicationThreadProxy(简称ATP),ActivityManagerService(简称AMS),这个两个服务都运行在system_server进程的不同线程中,由于ATP和AMS都是基于IBinder接口,都是binder线程,binder线程的创建与销毁都是由binder驱动来决定的。

    App进程则是我们常说的应用程序,主线程主要负责Activity/Service等组件的生命周期以及UI相关操作都运行在这个线程; 另外,每个App进程中至少会有两个binder线程 ApplicationThread(简称AT)和ActivityManagerProxy(简称AMP),除了图中画的线程,其中还有很多线程,比如signal catcher线程等,这里就不一一列举。

    Binder用于不同进程之间通信,由一个进程的Binder客户端向另一个进程的服务端发送事务,比如图中线程2向线程4发送事务;而handler用于同一个进程中不同线程的通信,比如图中线程4向主线程发送消息。

    结合图说说Activity生命周期,比如暂停Activity,流程如下:

    1. 线程1的AMS中调用线程2的ATP;(由于同一个进程的线程间资源共享,可以相互直接调用,但需要注意多线程并发问题)

    2. 线程2通过binder传输到App进程的线程4;

    3. 线程4通过handler消息机制,将暂停Activity的消息发送给主线程;

    4. 主线程在looper.loop()中循环遍历消息,当收到暂停Activity的消息时,便将消息分发给ActivityThread.H.handleMessage()方法,再经过方法的调用,最后便会调用到Activity.onPause(),当onPause()处理完后,继续循环loop下去。

    以上,就是关于这个问题回答的所有思路和具体解析参考,觉得对你有参考意义,还请大家随手点赞支持一下。

    1546页全网最全Android大厂面试真题

    本真题资料适应于实习 & 初级工程师 & 中级工程师,高级工程师勉强吧。附带详细答案解析,旨在能帮助广大Android学习者找到心仪的Offer。

    文档内容总共7大模块:

    • 1.Java部分
    • 2.Kotlin部分
    • 3.Android部分
    • 4.移动UI框架部分(Flutter为主)
    • 5.数据结构与算法部分
    • 6.常用的开源库部分
    • 7.计算机网络认识

    由于篇幅原因,以下只有局部内容展示,需要此完整文档的朋友,点击此处获取更多Android进阶学习资料笔记、面经真题,请在我的Github中查看。

    1.Java部分

    1.1 操作系统相关

    • 1.什么是操作系统?(校招&实习)
    • 2.什么是线程,什么是进程?(校招&实习)

    1.2 JDK&JVM&JRE

    • 1.JDK & JVM & JRE分别是什么以及它们的区别?(校招&实习)
    • 2.解释一下为什么Java可以跨平台?(校招&实习)

    1.3 面向过程 & 面向对象

    • 1.什么是面向过程 & 什么是面向对象 & 区别?(校招&实习)
    • 2.给我说说Java面向对象的特征以及讲讲你代码中凸显这些特征的经验。(校招&实习)
    • 3.什么是重载 & 什么是重写 & 区别。(校招&实习)
    • 4.谈谈你对this和super的认识。(校招&实习)
    • 5.接口和抽象类的区别。(校招&实习)
    • 6.静态属性和静态方法能被继承吗?静态方法又是否能被重写呢?(校招&实习)
    • 7.给我说说权限修饰符特性。(校招&实习)
    • 8.给我谈谈Java中的内部类。(校招&实习)
    • 9.闭包和内部类的区别?
    • 10.Java多态的实现机制是什么?
    • 11.谈谈你对对象生命周期的认识?
    • 12.static关键字的作用?(校招&实习)
    • 13.final关键字的作用。(校招&实习)

    1.4 八大基本数据类型&引用类型

    • 1.说说Java中的8大基本类型 & 内存中占有的字节 & 什么是引用类型?(校招&实习)
    • 2.什么是拆箱 & 装箱,能给我举栗子吗?(校招&实习)

    1.5 数组

    • 1.能说说多维数组在内存上是怎么存储的吗?
    • 2.你对数组二次封装过吗?说说封装了什么

    2.Android 部分

    2.1 Activity

    • 1.Activity是什么?(校招&实习)
    • 2.典型情况下的Activity生命周期?(校招&实习)
    • 3.异常情况下的Activity的生命周期 & 数据如何保存和恢复? (校招&实习)
    • 4.从Activity A跳转到Activity B之后,然后再点击back建之后,它们的生命周期调用流程是什么?(校招&实习)
    • 5.如何统计Activity的工作时间?(校招&实习)
    • 6.给我说说Activity的启动模式 & 使用场景。(校招&实习)
    • 7.如何在任意位置关掉应用所有Activity & 如何在任意位置关掉指定的Activity?(校招&实习)
    • 8.Activity的启动流程(从源码角度解析)?
    • 9.启动一个其它应用的Activity的生命周期分析。
    • 10.Activity任务栈是什么?在项目中有用到它吗?说给我听听
    • 11.什么情况下Activity不走onDestory?
    • 12.什么情况下Activity会单独执行onPause?(校招&实习)
    • 13.a->b->c界面,其中b是SingleInstance的,那么c界面点back返回a界面,为什么?
    • 14.如果一个Activity弹出一个Dialog,那么这个Acitvity会回调哪些生命周期函数呢?
    • 15.Activity之间如何通信 & Activity和Fragment之间通信 & Activity和Service之间通信?
    • 16.说说Activity横竖屏切换的生命周期。(校招&实习)
    • 17.前台切换到后台,然后在回到前台时Activity的生命周期。
    • 18.下拉状态栏时Activity的生命周期?
    • 19.Activity与Fragment的生命周期比较?
    • 20.了解哪些Activity常用的标记位Flags?
    • 21.谈谈隐式启动和显示启动Activity的方式?
    • 22.Activity用Intent传递数据和Bundle传递数据的区别?为什么不用HashMap呢?
    • 23.在隐式启动中Intent可以设置多个action,多个category吗 & 顺便讲讲它们的匹配规则?
    • 24.Activity可以设置为对话框的形式吗?(校招&实习)
    • 25.如何给Activity设置进入和退出的动画?
    • 26.Activity使用Intent传递数据是否有限制 & 如果传递一个复杂的对象,例如一个复杂的控件对象应该怎么做?
    • 27.在Activity中可以多次调用setContentView方法吗?说说不同时机第二次调用setContentView会发生什么?
    • 28.说说分别在Activity里每一个生命周期函数里调用finish方法后,接下来Activity的生命周期如何回调?
    • 29.有什么方法可以启动一个没有在AndroidManifest.xml中注册过的Activity?
    • 30.在Activity进行配置时,catrgory和action的区别是什么?
    • 31.activity中分别在onCreate,onStart,onResume中调用finish后的生命周期如何回调?
    • 32.锁定屏与解锁屏幕,Activity 生命周期?
    • 33.如何设置Activity进入和退出的动画?
    • 34.谈谈你对Activity中onNewIntent()方法的认识?
    • 35.如果一个Activity启动比较慢,需要优化,你觉得可以从哪些方面入手?
    • 36.Activity之间传递数据的方式Intent是否有大小限制,如果传递的数据量偏大,有哪些方案?
    • 37.了解scheme跳转协议吗?谈一谈
    • 38.谈谈你对Activity的Context的认识?
    • 39.如何在Application中获取当前Activity实例?
    • 40.Activity进程优先级?
    • 41.Activity出现ANR的条件有哪些 & 解决方案?

    内容截图展示

    第一部分目录及部分内容截图 第二部分目录及部分内容截图 部分解析内容截图

    需要此完整文档的朋友,点击此处获取更多Android进阶学习资料笔记、面经真题,请在我的Github中查看。

    相关文章

      网友评论

        本文标题:字节跳动一面:Android中为什么主线程不会因为Looper.

        本文链接:https://www.haomeiwen.com/subject/vymqiktx.html