再写13道Android面试题,裁员无非是关机再重启

5,148 阅读14分钟

前言

“你恐怕要领大礼包了”这是一大早刚到公司领导喊我去会议室跟我说的话,刚听见我还有点懵,以为要给我什么好吃的,然后就反应过来了,我又被裁了,这工作是去年入职的,到现在刚好要一年了,虽说我知道目前这种行情裁员是家常便饭,不是啥新鲜事,但是像我这样连续三份工作都是以喜提大礼包的方式结束的,还是有点心累,咋这么背呢,是不是脸太丑了啊,唉~又要去面试了。

再写面试题

去年失业两个月就写了几篇面试题相关的文章,不是我写面试题有瘾,而是现在每一个面试机会都非常的来之不易,面一个少一个,咱多多少少肯定是要准备一些的,一个正常的面试一轮下来,问点项目,考点八股,做点算法那是必须要经历的,其中项目因人而异只能自己准备,算法刷点题,主要算法思想到位了也不会算你差,最恶心的就是八股,又多又杂,问啥的都有,跟开盲盒一样,开到啥算啥,而且现在有点水平的面试官会揪着一个八股使劲往深里问,看着只问了一题,其实问了很多题了,就看你能回答多少,看你基础牢固不牢固,所以就不得不要求自己每个知识点都要会,那么闲话少说,开始这篇的面试题。

Q1.简述一下垃圾回收算法

1.标记-清除算法

  • 原理:从GC Roots开始标记所有被引用的对象,GC Roots可以是栈中的变量,静态变量等,而那些被标记的对象我们称为存活对象,当这个标记过程结束之后,会再次遍历一遍,将那些未被标记的对象清除掉
  • 优点:操作方便
  • 缺点:效率不高,无论是标记过程还是清除过程,都需要遍历一次堆,另外也会产生大量不连续的内存碎片

2.标记-复制算法

  • 原理:将内存分成相等的两块,每次只使用其中一块,等到这块内存使用完之后,将存活的对象复制到另一块,然后将原来已使用的内存块整个清除
  • 优点:效率高,只需要复制存活的对象,并且清理内存的过程也很简单,同时也解决了内存不连续的问题
  • 缺点:空间浪费,只能使用一半内存,另一半只是用来存放复制后的对象,并且当存活对象较多的时候,也会降低复制效率

3.标记-整理算法

  • 原理:第一步同清除算法一样,首先会先标记存活对象,接着会把存活对象全部移动到内存的一边,这个过程会将一些垃圾对象变成存活对象,类似于赋值操作,完事之后,将内存剩下的部分清理掉
  • 优点:解决了上述清除算法的内存不连续问题以及复制算法的浪费空间问题
  • 缺点:移动对象时候效率会比较低

4.分代收集算法

  • 原理:基于对象的存活周期不同,将内存划分为新生代和老年代,其中回收新生代里面的对象会采用复制算法,回收老年代里面的对象会采用清除算法或者整理算法
  • 优点:提高了回收效率,降低对性能的影响
  • 缺点:增加了垃圾回收的实现复杂度

Q2.为什么Android中的数据传输推荐使用Parcelable而不是Serializable

虽然设计Serializable的目的就是为了将对象序列化之后进行传输,它是通过ObjectInputStreamObjectOutputStream来实现序列化与反序列化的,但是它有一个明显的缺陷那就是效率低,消耗大,因为在这个过程中会不断进行反射,创建临时变量,频繁触发GC,这种缺点如果出现在后端那当然可以忽略不计,但是如果是在Android上,那么这个缺陷将会被无限放大,因为对于Android来讲,内存是很珍贵的,不能随便被浪费,所以才设计出了Parcelable来代替SerializableParcelable序列化的方式不会那么复杂,不需要使用反射,而是用的自定义的序列化方式,在性能上要比Serializable做的好,更加适合在Android系统上实现数据序列化传输

Q3.说一下synchronized的原理

虽然现在市面上的Android岗位基本都要求kotlin为主,但是还是会存在一些岗位要求应聘者java跟kotlin都要能掌握,问java的话无非要么数据结构,要么就是多线程,要么就是锁,这里就来说下synchronized的原理,这个锁可以锁在方法上,也可以锁住部分代码块

image.png

上面这段代码就是锁住代码块的例子,当synchronized锁代码块的时候,它可以锁住一个类对象,或者一个实例对象,或者任意一个对象,反正怎么着都离不开一个对象,那是因为每个对象都会关联一个Monitor对象,当一个线程持有这个Monitor对象的时候,表示这个线程被锁住了,其他线程只能等待这个线程释放了锁之后,才可以去争夺这个Monitor,将这段Java代码编译成字节码后,就可以清晰的看到锁的持有与释放的过程

image.png

通过javac CoffeeGood.javajavap -v CoffeeGood.class就获取到了上述字节码,其中画红圈的部分就表示这个同步代码块的入口跟出口,当执行了monitorenter指令的时候,就表示线程已经持有了锁了,当monitorexit指令被执行的时候,就表示该锁已经被释放,而我们用同样的方式去查看同步方法的字节码后会发现,有一点不一样

image.png image.png

同步方法的时候,不会看到有monitorentermonitorexit指令,而是在方法的flags处有个ACC_SYNCHRONIZED的标记位,当线程执行到这个方法的时候,就会看有没有这个标记位,有的话就持有锁

Q4.说说对volatile的理解

  1. 可见性:当被volatile修饰的变量发生写操作的时候,都会同步到主内存中,其他线程如果想要访问该变量,都必须从主内存中读取,变量的变化对其他线程都是可见的
  2. 有序性:通过插入内存屏障使得指令可以按照load->add->save的顺序执行下去,防止指令重排

对于有序性的解释我们举个例子,对于Android开发来讲volatile最常用的地方就是用双重校验锁的方式来写单例,我们都知道单单使用synchronized也可以实现一个懒汉式的单例模式,就像下面这段代码

image.png

正常来讲在多线程环境下,都是只会存在一个CoffeeGood的实例,它的指令大致顺序是JVM开辟一个内存->在内存上进行实例初始化->将实例的地址赋给变量,但是JVM为了提高运行效率,不影响单线程的运行结果,会将创建实例的指令进行重排,这就导致了某个线程走到instance判空的那一步后发现instance已经被赋值了,但其实内部的实例并没有初始化,这就容易导致空指针,这个问题的解决方式就是在变量前加上volatile关键字,有了这个关键字,就相当于在变量的读跟写之间加了个内存屏障,任何读操作都必须在写操作之后,这样就能保证任何线程访问到instance的时候,它都已经完成初始化工作

Q5.说一下事件分发

这个问题基本每次面试都会被问到,算是送分题,但是答错了面试肯定没戏,下面是自己手画的一个事件分发的流程图

事件分发.drawio.png
  1. 事件先是来到了ActivitydispatchTouchEvent函数,这个函数里面如果返回true或者false,那么事件就在当前函数里面消费,如果返回super,事件就传到ViewGroupdispatchTouchEvent函数
  2. ViewGroupdispatchTouchEvent函数中,返回true就在当前函数消费事件,返回false就将事件传递到ActivityonTouchEvent函数中,返回super就将事件传递到ViewGrouponInterceptTouchEvent
  3. ViewGrouponInterceptTouchEvent中,返回true就将事件传递到同样在ViewGrouponTouchEvent函数中,返回false或者super就向下传递到ViewdispatchTouchEvent函数中
  4. ViewdispatchTouchEvent函数中返回super就将事件传递给ViewonTouchEvent函数,返回false就将事件传递给ViewGrouponTouchEvent函数

Q6.说一下对MeasureSpec的三种测量模式的理解

image.png
  • UNSPECIFIED:当前View可以任意取值,没有被限制
  • EXACTLY:直接使用父布局给定的尺寸
  • AT_MOST:当前视图设置的尺寸不能超过给定的尺寸

Q7.说一下App的启动流程

记得去年还能说的上来的,今年就忘记了,这次手画了一个时序图,加深一下对这个流程的理解

image.png

Q8.Binder实现进程通信的原理是啥

  1. 首先由Binder驱动创建一个接收缓存区
  2. 然后再创建一个内存缓存区,并且建立两个映射关系,一个是内存缓存区与接收缓存区之间的映射,另一个是接收缓存区与接收进程用户空间地址的映射关系
  3. 接下来是发送数据阶段,先由发送进程通过系统调用copy_from_user()函数将数据复制到内存缓存区,也就是完成了仅有的一次数据拷贝,然后通过上述建立好的映射关系,将数据映射到了接收进程的用户空间

Q9.描述一下Binder中ServerManager的作用

由于Binder是基于C/S结构,在用户空间分为Client,Server和ServerManager,而Binder驱动存在于内核空间,Client与Server都是独立的进程,他们俩之间无法直接Binder通信,所以需要一个桥梁把他俩连接起来,ServerManager就扮演着这个桥梁的角色,里面维护着一张表,value是Server中Binder实例的引用,Client通过Binder服务名称去获取这个引用,然后经过Binder驱动将这个引用转换成代理对象

Q10.SharedFlow与StateFlow的区别

  • SharedFlow是广播事件流,StateFlow是状态事件流
  • SharedFlow不需要初始值,StateFlow需要初始值
  • 当有新的订阅者的时候,SharedFlow可配置发送历史数据的数量,StateFlow只能发送一个
  • 连续发送值的时候,SharedFlow会将所有数据都发送出去,而StateFlow会经过去重操作后再发送
  • SharedFlow可配置自己的背压策略,而StateFlow无法配置,默认只缓存最新的值

Q11.SharedFlow构造函数中replay和extraBufferCapacity作用是啥

用过SharedFlow的人肯定知道它的构造函数中有三个参数,其中有两个Int类型参数replayextraBufferCapacity,这俩参数是干嘛用的呢?首先说一下replay,就像它字面意思那样是重放的意思,我们都知道SharedFlow是热流,无论有没有订阅者它都会发送数据,如果在发送完数据后再订阅,那么数据就收不到了,replay的意思就是重新接收最新已经发送过的数据的数量,下面是一段代码示例

image.png

创建了一个SharedFlow其中参数replay设置为0,两个协程,第一个负责发送数据,延迟一秒后开启另一个协程去订阅数据,我们知道它啥数据也接收不到

image.png

但是如果将replay设置为2,结果就不一样了,是可以接收到之前发送数据中最新的两条

image.png

看完replay再来看下第二个参数extraBufferCapacity,额外的缓存容量,同样通过代码来理解下它的作用

image.png

先将extraBufferCapacity设置为0,上游同样正常发数据,下游处理数据时间延长为300毫秒,这段代码运行后是这样的

image.png

刚开始上游就发了两条数据就挂起了,直到300毫秒后下游处理完数据,上游才会接着发,这种处理数据的方式放在前端页面上就很容易出现卡顿,而这里解决的办法就是设置额外缓存容量extraBufferCapacity,这就相当于开辟了一个缓存空间,当下游正在处理数据时候,上游可以先把数据放在缓存空间里面,等下游处理完数据后,从缓存空间里面拿下一条数据,更改extraBufferCapacity为1后结果如下

image.png

可以看到开始发送数据的时候,比之前多发了一条,这个多出来的一条就是被放在了缓存空间里面的

Q12.协程如何捕获异常

可以用try-catch没毛病,还有一个类叫CoroutineExceptionHandler也是用来捕获协程的异常的,要使用它来捕获的话首先得先创建一个CoroutineExceptionHandler的实例

image.png

然后自定义一个协程,将上述创建出来的CoroutineExceptionHandler作为CoroutineContext的一部分传入到CoroutineScope的构造函数中

image.png

这样协程中抛出的异常就能被捕获到了

image.png

不过这种方式的捕获只适用于launch一个协程,如果是用async启动一个协程的,那只能老老实实在Deferred.await()外层套上try-catch来捕获

image.png

Q13.CoroutineStart有什么作用

launch一个协程的时候都会习惯性的省略前两个参数,只用最后一个lambda参数,这就导致如果一旦问起来前两个参数的作用,有人可能就会说不上来,现在来看下其中第二个参数CoroutineStart是做什么用的

CoroutineStart.DEFAULT

默认参数,但凡我们启动一个协程,如果不配置start参数的话,它默认就是CoroutineStart.DEFAULT模式

image.png

这种模式下的协程会立即执行,并且在调用cancel后会立即终止,并且抛出异常

CoroutineStart.LAZY

懒加载一个协程,对于懒加载我们应该都不陌生,只有需要它的时候才去加载,这里也是一样,只有在调用start或者join函数的时候协程才会执行

image.png

上述代码中虽然在runBlocking中开启了一个子协程,但是我们看到最终执行结果中并没有打印出子协程中的内容,原因就是子协程使用了懒加载模式,这个时候只有主动调用start函数,协程才会执行

image.png

CoroutineStart.ATOMIC

这个值跟DEFAULT模式有点相似,但区别的地方在于协程被取消的时机不一样,来看下面这段代码

image.png

上述代码中,虽然子协程在开始执行前调用了cancel函数,但是我们看到输出中子协程还是执行,原因就在于使用了ATOMIC模式,在开始执行前是不能被取消的

image.png

CoroutineStart.UNDISPATCHED

这个可以理解为无视线程调度,或者子协程与父协程共用一个线程上下文,看个例子

image.png

runBlocking开启了一个父协程,内部一个子协程,子协程内部进行了上下文的切换,存在线程的调度,由于协程是异步的,所以打印顺序应该是先父再子

image.png

顺序如上图所示,也看到了父子协程所在的上下文是不一样的,现在将子协程的CoroutineStart改为UNDISPATCHED又会怎么样呢

image.png

这个时候,所有协程都在一个上下文里面,执行顺序也将无视调度,顺序执行下去

最后

就写到这,有哪些不对的地方欢迎指正,我会及时修改,祝各位面试顺利