在这跌宕起伏有上一顿没下一顿的行情里面,哥们一直感觉一天天呆在工位上噼里啪啦敲着那些业务代码显的特别的与社会脱轨,所以在我一天天的对着bug祈祷,追着老板骚扰的不懈努力下,终于迎来了职业生涯第三次企业人才输出,哥们终于毕业啦~又可以去不同公司跟不同的面试官聊各种各样的面试题了,尽管做了两三年的前端,但是我心里还是有数,自己要在外面这个庞大的人才市场上争得一碗饭吃,必须依然投身在android岗上,不可能依靠自己少量的前端经验跟别人拼,拼不过的,但是说实话自己真的很久没有碰android了,一些基础的都记不太清了,所以还是需要适当的复习下,给自己出了点面试题,看看自己能不能答上来,也为接下去的面试做下准备。
1.有哪些原因会导致App启动过慢
- Application中初始化了较多的三方sdk
- Application中出现了耗时操作比如I/O
- 入口页面渲染比较耗时
- 系统内存不足
2.对称加密与非对称加密有什么区别?分别有哪些算法的实现?
- 对称加密是一种加密解密啊都使用同一个密钥的加密方式,优点是方便快速,缺点是易受攻击,常见算法比如AES
- 非对称加密是一种加解密使用不同密钥的加密方式,优点是具有更高的安全性与可靠性,缺点是加解密速度较慢,常见算法比如RSA 下面是一个RSA的加密demo
3.什么是HashMap里面的哈希冲突?又是如何解决的?
如果要知道什么是哈希冲突,必须先要知道HashMap
的工作原理,HashMap
底层结构是一个哈希表,这个表是一个数组,数组的元素是链表或者红黑树,当我们给HashMap
添加一个键值对的时候,键就会转化成这个数组的索引,每个索引对应着数组的一个元素,那么所谓的哈希冲突就是多个键转换成了同一个索引,散列到了数组的同一个元素上了 ,那么当出现哈希冲突的时候,HashMap
是这么解决的
- 对于较小的哈希表,
HashMap
会使用链表来存储同一个索引上的键值对 - 对于较大的哈希表,
HashMap
会使用红黑树来存储同一个索引上的键值对
4.Android中为什么主线程不会因为Looper.loop()的死循环导致卡死
我们知道Android主线程一直围绕着主线程的消息队列Message Queue
来进行,Looper.loop()
就是逐个从消息队列中取出消息并一一处理,在处理Message Queue的同时,也在监控着有没有新的输入事件要处理,如果有就处理输入事件,无论是处理消息还是处理输入事件,都是在一个线程内逐个执行,任何一个事件或者消息没有执行完,是不会执行下一个的,所以我们所说的ANR其实就是主线程正好卡在了某个消息或者事件上,导致后面的消息或者事件没有得到处理造成的
5.一个线程可以有几个Handler,可以有几个Looper
一个线程可以有多个Handler
,这个从我们平时开发中就可以了解到,我们可以在一个UI主线程中创建一个Handler
用来统一处理所有ui刷新操作,也可以创建多个Handler
用来处理不同的UI刷新操作,而一个线程只能有一个Looper
,这个在Looper
类中就能找到原因
在Looper
类中维护着一个静态final
的ThreadLocal
的变量sThreadLocal
,而这个sTheadLocal
最终也会存储在Thread
类里面的ThreadLocalMap
里面
这就说明了在一个线程里面,Looper
都是唯一的,它们都是通过一个唯一键值sThreadLocal
一起存储在了ThreadLocalMap
里面,除此之外,当我们去调用Looper.prepare
去设置Looper
的时候,也可以看出Looper
的唯一性
当sThreadLocal
中如果已经设置过Looper
,那么就会抛出异常,表明每个线程中只能有一个Looper
被创建
6.Looper是如何知道消息是从哪个Handler发送过来的
之前说了一个线程可以有多个Handler
但只能有一个Looper
,那么当这些Handler
一起发送消息的时候,Looper
是如何知道这些消息是从哪个Handler
发送过来的呢?这个问题就要从发送消息这里说起,当我们从一个sendMessage
方法逐个点进去的时候,会最终到达一个叫enququMessage
的方法
这个方法里面的第一行代码就说明了已经将发送过来的消息与当前Handler
绑定在一起了,后面这个消息无论到哪,从target
里面就能看出这个消息的所属Handler
是哪个,然后再看Looper
里面分发消息的地方,在Looper
类中的loopOnce
方法中,经过一系列对消息的处理,最终会到达下面这段代码
分发消息的时候,就是使用的msg.target
来进行分发,将这个消息从哪来就发送回哪去
7.说一下ThreadPoolExecutor各个参数的作用
ThreadPoolExecutor
是用来构建线程池的java类,它的构造函数总共提供了七个参数,它们分别所发挥的作用如下
corePoolSize
:核心线程的数量,就算这些线程是被闲置的,也不会被回收,不过有个例外,就是当设置了allowCoreThreadTimeOut
为true的时候,核心线程也会在keepAliveTime
后被回收maximumPoolSize
:线程池中允许的最大线程数量keepAliveTime
:非核心线程的超时时间,不过上面也提到了,当allowCoreThreadTimeOut
设置为true的时候,核心线程也会在超时时间后被回收unit
:keepAliveTime
的时间单位workQueue
:阻塞队列,当提交的线程数量超出核心线程数量后,超出部分就会被塞到阻塞队列中等待threadFactory
:线程工厂,用来创建新的线程handler
:拒绝策略,当队列已满且无法添加新任务的时候,线程池用来拒绝线程的策略,常见的策略有以下几个AbortPolicy
:抛出异常,终止任务DiscardPolicy
:直接丢弃DiscardOldestPolicy
:丢弃队列中最老的,添加新任务CallerRunsPolicy
:使用调度线程执行任务
8.ANR在什么场景下会触发
ANR机制主体实现在系统层,所有与ANR相关的消息,都会经过系统进程(system_server)调度,然后派发到应用进程完成对消息的实际处理,以下四种场景会触发ANR
- InputDispatching Timeout: 按键或触摸事件在特定时间内无响应,这个特定时间是5s
- Service Timeout:Service在特定时间内无法处理,如果是前台服务这个特定时间是20s,如果是后台服务,这个特定时间是200s
- BroadcastQueue Timeout:BroadcastReceiver在特定时间内无法处理完成,如果是前台广播,这个特定时间是10s,如果是后台广播,这个特定时间是60s
- ContentProvider Timeout:内容提供者执行超时,超时时间是10s
9.View的绘制流程
这个问题算是经典面试题,基本每个面试官准备面试题的时候都会来一个View的绘制流程,这个问题简单的来说就是分为三个过程onMeasure(测量)->onLayout(布局)->onDraw(绘制),而如果从源码的角度来说的话,就得从ViewRootImpl
这个类里面说起,里面有一个方法叫 scheduleTraversals
,如果在这个类里面全局搜一下的话,会发现有很多地方都会有调用到这个方法
比如说在我们熟悉的requestLayout
里面就会调用这个方法
说明啥,说明我们暂时可以把这个方法看成是一个刷新的作用,继续点到这个方法里面,会注意到这么一行代码
这个代码里面有mTraversalRunnable
这么个东西,点进去后会发现它其实是一个线程,这个线程就只执行了一个方法doTraversal
而在doTraversal
里面,就会看到有执行了一个performTraversals
这个方法,这个方法很关键了,点进去后会看到有一大段代码,省去一些无关的,会看到里面有调用这三个方法
看方法名字就知道,这三个就是分别去执行绘制流程的三个步骤的,比如在performMeasure
里面,会直接去调用View
的measure
方法
而在measure
方法里面,就会去调用自定义View
的时候会去重写的onMeasure
方法,到这里整个View
的绘制流程就说完了
10.事件分发机制
又是一个经典面试题,事件分发机制主要是要清楚dispatchTouchEvent
,onInterceptTouchEvent
以及onTouchEvent
三个方法的作用
dispatchTouchEvent
:用来在View
或者ViewGroup
中分发事件,在这个方法里面,会去判断事件是由当前View
来处理,还是分发给它的子View
onInterceptTouchEvent
:用于拦截dispatchTouchEvent
分发下来的事件,如果返回true
表示拦截成功,事件不传给子View
,如果返回false
,那么事件继续传递给子View
onTouchEvent
:当事件分发下来后,如果事件没有被拦截,那么就由当前View
或者ViewGroup
来处理该事件
11.RecycleView每一级缓存的作用以及使用场景
mChangedScrap
:存放可见范围内item的更新,并且调用了notifyItemChanged
、notifyItemRangeChanged
这类方法通知更新mAttachedScrap
:临时存放已添加或者已删除item,使用场景是列表滚动,或者删除item后调用notifyItemRemoved
通知更新mCachedViews
:滚动过程中,存放未被重新使用且状态无变化的item,一般用于列表滚动过程中将已经移出屏幕的item快速且无加载得滑动回来mViewCacheExtension
:主要提供给开发人员自定义缓存层级mRecyclerPool
:按照不同itemType
分别存放超出mCachedViews
限制并移出屏幕的item
12.为什么说Lambda表达式会有性能问题,有什么方式可以避免这种问题?
kotlin语言中,可以说随处都可以看见lambda表达式的身影,它是造就kotlin代码简介的功臣之一,但是如果因为仅仅是要让代码简介而滥用lambda表达式,可能会造成一定的性能隐患,我们举个例子
这里有个简单的Activity,在Activity中只是调用了函数myFunction
,由于myFunction
的参数是一个函数类型,所以调用方可以将整个函数入参以lambda形式展示,使得代码看起来更加简洁易懂,但是在简洁的背后却是付出了一定的性能代价,我们将这段代码转成字节码在看下
可以看到我们的lambda表达式在转成字节码后,变成了一个Function0
的内部类,执行lambda的本质其实就是调用这个内部类里面的invoke
方法,所以现在我们就知道了,看似简洁的lambda背后其实就是创建了一个内部类的实例,而这里仅仅实现的功能只是打印一句话而已,那么有没有办法可以避免这种问题吗,这个时候就要使用内联函数,这里改下代码
这里在myFunction
前面增加了一个inline
的操作符,这个时候就将函数变成内联函数了,转换成内联函数后有什么区别呢?再看下字节码
可以发现变成内联函数后,就没有内部类了,而是直接执行的lambda内部的代码,这也就是内联函数的特性,会将lambda表达式内部的代码移到函数外面执行,不过这里也看出内联函数当遇到lambda表达式里面代码量比较庞大的时候,就不适用了,因为移动的代价比较大
13.noinline和crossinline分别是做什么用的
我们现在已经知道了inline
作用,那么假如有这么个函数,接受两个函数类型的参数,其中参数a需要做内联,参数b不需要,我们是该让这个函数变成内联还是不变呢,
显然这个时候inline
已经无法满足需求了,需要使用另一个操作符noinline
,用法就是哪个参数不想让它被内联,就将操作符加在这个参数的前面,修改后的代码如下
首先使用noinline
的前提还是被修饰的函数必须是个内联函数,然后才是在目标参数前加上noinline
操作符,这个时候看下转换成字节码后的代码长什么样
我们可以看到paramB
依然是一个内部类,而paramA
已经被内联出去了,这个就是操作符noinline
的作用,再来看一下另一个操作符crossinline
,这个操作符是干什么用的呢,假如我们的函数类型的参数要执行的是一个比较耗时的任务,那么我们肯定是要将这个操作放在线程中进行的,但是当我们将函数放在线程中后会发现报错了
意思是参数所在的域跟内联函数不是一个域,所以需要使用crossline
操作符来修饰,这个就是crossline
的用途
14.创建协程有哪几种方式?有什么区别?
创建协程有两种方式,使用launch
和async
创建,可以看下下面这段示例代码
这两种方式的共同点是都能执行协程里面的代码块,执行一遍这段代码后,可以正常看到输出
区别是使用launch
创建的协程会返回一个Job
对象,我们可以使用这个对象来管理协程的生命周期,比如我们在上述代码中加入一行代码,如下所示
我们在launch
创建出来的协程的Job
对象上调用cancel
函数,意思就是取消launch
协程的执行,那么这段代码执行的结果就不一样了
由于调用了cancel
函数,第一个协程就不执行了,再来看下async
函数在创建协程后返回的是什么
居然返回的是一个Deferred
,这个是什么东西呢?其实它也是一个Job
,是Job
的一个子类
也就是将协程的执行结果封装到Deferred
里面,如果想要获取这个结果,可以调用await
函数
这段代码中,我们在async
协程内增加了一段字符串的输出,并且在后面将这段输出打印出来,这个时候的执行结果如下
可以看到协程的执行结果通过await
已经拿到了,并且如果async
内的操作需要耗时一段时间才将输出结果,await
也会一直等到协程执行完以后才将结果输出,下面看个例子
我们在async
函数内添加了两秒的延迟,并且将结果得到前与得到后的时间打印出来,可以看下await
是不是等到协程执行完以后才输出结果
从输出时间上可以看到await
的确是在协程执行结束后再输出结果的,所以总的来说,如果仅仅只是想控制协程的生命周期,使用launch
创建协程就行,如果想要获得协程的执行结果,就使用async
创建协程
15.为什么不建议使用GlobalScope来创建协程?
首先要明确一点,使用GlobalScope
创建的协程是顶层协程,它的作用域是全局的,如果在某一个独立的Activity
中使用GlobalScope
来创建协程的话,那就意味着除非开发人员在页面销毁的时候手动cancel
掉这个协程,不然这个协程依然在运行,容易导致内存泄漏
16.如何理解协程的挂起和恢复
任何协程的作用域里面都是需要执行挂起函数的,而区别挂起函数的方式很简单,就是看函数前有没有关键字suspend
修饰,比如我们经常使用的延迟函数delay
,它就是一个挂起函数
现在比如说我想要在一个runBlocking
里面延迟输出一段话,那么代码就该这么写
将这段代码转成字节码后就是下面这个样子
从这里就很明显的看到,每一个挂起函数的初始状态就是SUSPENDED
挂起状态的,挂起知道了,又该如何理解恢复呢,一样是在上面这段字节码里面,我们看到多了一个Continuation
这么一个东西,这个单词的中文名就是"继续"的意思,应该跟恢复有关系,当我们点到Continuation
里面的时候,发现它其实是一个接口
这个接口里面提供了一个叫resumeWith
的函数,从注释上就能看出,协程的恢复就是调用这个函数,那么什么时候调用呢?既然Continuation
是个接口,那么必然会有一个impl的实现类,事实上也的确有这么一个实现类,它叫BaseContinuationImpl
,在里面有resumeWith
的逻辑,我们看下怎么做的
resumeWith
函数内是一个循环体,循环体内不断调用invokeSuspend
函数,如果invokeSuspend
函数返回的状态一直是挂起的话,那么就直接return,如果不是挂起的话,就执行恢复逻辑,在我们这个例子里面,invokeSuspend
在delay
的时间内一直都是返回的挂起状态,只有等到时间到了,才返回Unit.INSTANCE
,协程也就恢复了
17.关键字tailrec是做什么用的?
从字面意思上就能看出,关键字tailrec
是用来处理尾递归算法的,什么是尾递归呢,尾递归是函数调用完自己之后没有其他操作,可以看下下面这段代码
这个就是一个尾递归的函数,功能是输入一个值,然后从这个值开始一直累加比它小1的数,直到累加的值变成0,可以验证一下,比如将目标值设为50
结果可以很快得出
但是当我们将目标值改大一点呢?比如100000
这个时候在运行一下,程序就报错了,报的是StackOverflowError
造成这个的原因是每一次递归都需要开一个方法栈,递归次数多了就会有损性能,所以这个时候就需要用到关键字tailrec
来解决这个问题,在刚才的函数前加上tailrec
后我们再看下结果如何
这个时候再执行一下,程序也可以正常得出结果了
至于原理,我们可以将代码转换成字节码就可以发现
tailrec
可以将一个递归函数转换成寻常的循环叠加函数,这样就避免了不断开辟方法栈,避免出现栈溢出的问题
总结
暂时面试题就做到这里吧,得亏复习了一下,要不然真就全忘了,有些题目在回答之前,我是一点印象也没有了,以前张嘴就来,现在还要翻下资料,好在答完以后以前的一些知识点也都回忆起来,其实除了这些八股文类型的面试题,有些面试题是因人而异的,没法写下来,比如做过哪些性能优化,做过哪些架构上工作,或者遇到过印象最深刻的线上问题是啥等等,这些也都要准备下,不说了,祝被优化的兄弟们早日重新回到岗位上,下篇文章见,拜拜~