赞
踩
这一份Android面试题集,适应于实习 & 初级工程师 & 中级工程师,高级工程师勉强吧。笔者在搜集问题的同时,也花了大半年的时间整理答案。
目前我就职于某大厂安卓高级工程师职位,在当下大环境下也想为安卓工程师出一份力,通过我的技术经验整理了面试经常问的题,答案部分会是一篇文章或者几篇文章,都是我认真看过并且觉得不错才整理出来,大家知道高级工程师不会像刚入门那样被问的问题一句话两句话就能表述清楚,所以我通过过滤好文章来帮助大家理解。希望Android求职者认真研读,准备面试,并顺利找到Offer。
如果你是Android学习者,请订阅笔者的Android知识体系总结(全方面覆盖Android知识结构,面试&进阶),本篇文章中的问题可以从文章中找到答案,谢谢支持。查阅答案,请star项目[https://github.com/Android-Alvin/Android-LearningNotes
](https://github.com/Android-Alvin/Android-P7/blob/master/Android%E5%BC%80%E5%8F%91%E8%BF%98%E4%B8%8D%E4%BC%9A%E8%BF%99%E4%BA%9B%EF%BC%9F%E5%A6%82%E4%BD%95%E9%9D%A2%E8%AF%95%E6%8B%BF%E9%AB%98%E8%96%AA%EF%BC%81.md)
本面试题集只应对于Android求职者,有7个模块:
Java部分
Kotlin部分
Android部分
移动UI框架部分(Flutter为主)
数据结构与算法部分
常用的开源库部分
计算机网络认识
kotlin被设计出来并被Google推广,主要有以下优势:
首先,我们了解下Java的工作原理:
Java 代码是经过编译才能运行的。首先会编译成class文件,然后通过java虚拟机运行,在Android中也就是ART。
所以,任何语言只要能被编译成符合规格的class文件,就能被java虚拟机运行,也就能运行在我们的Android手机上,kotlin亦是如此。
Tools -> Kotlin -> Show Kotlin Bytecode
再点击Decomplie还可以反编译成Java文件。
String a;
如果要调用对象的参数,必须判空:
- if (a!=null){
- Log.d("lz","length="+a.length());
- }else{
- Log.d("lz","length=null");
- }
-
val a: String ? = null
1)赋值的时候,可以直接使用?来表示这个对象可能为空,如果为空则表达式结果也为空,而不用进行非空判断。
- //如果 b 非空,就返回 b.length,否则返回 null
- val length = b?.length
- //如果 b 非空,就返回 String类型的b,否则返回 null
- val str = b as? String
-
也就是通过问号来表示对象为空则整个表达式结果为空,而不会报错空指针。
2)如果需要设定为空的时候返回的表达式值不为空,可以用操作符?:来表示,也叫Elvis操作符。
- //b为空则表达式返回-1
- val length = b?.length ?: -1
-
3)如果要将值转换为非空类型,就可以使用 !!来标识非空,但是这种操作符就有可能会抛出空指针异常,如果实际对象为空的话。所以这种操作符相当于去除了空判断。
- //如果b为空,空指针异常
- val length = b!!.length
-
val,全称value,声明一个不可变的变量,这种变量在初始赋值之后就再也不能重新赋值了,所以相当于java中的final变量。
var,全称variable(可变的),所以是用来声明一个可变的变量,可以重复赋值。
kotlin中这么设计的原因主要是把不可变变量 这种因素和可变变量拿到同一级来设计,也就是说我们以后编码设计变量的时候,必须要考虑这个变量是不可变还是可变的,养成良好习惯,不是以前在java中需要添加final这种稍微繁琐的举动。
扩展函数,其实就是扩展类的函数,可以在已有的类中添加新的方法,比继承更加简洁优雅方便。
- fun Activity.showToast( msgId:Int){
- Toast.makeText(this,msgId,Toast.LENGTH_SHORT).show()
- }
这样任何的Activity里面就可以直接调用showToast方法来展示Toast了。
- var <T> MutableList<T>.lastData: T
- //获取List中最后一个对象
- get() = this[this.size - 1]
- //设置List中最后一个对象的值
- set(value) {
- this[this.size - 1] = value
- }
用法:
- var strs = mutableListOf<String>()
- strs.lastData="heihei"
- Log.e(TAG,"lastdata= ${strs.lastData}")
这里还涉及到两个知识点:
kotlin中,在使用对象的get和set方法,可以直接省略,直接使用属性名即可,会根据表达式的实际功能来添加对应的set或者get方法。
kotlin中,对于$符号表示 串模板,就是可计算的代码片段,可以将其计算结果链接到字符串中。
kotlin这个扩展功能确实设计的很巧妙,那就一起来研究下它的原理:
按照上面的方法,也就是Tools -> Kotlin -> Show Kotlin Bytecode -> Decomplie, 我们得到showToast扩展函数和使用代码所对应的java代码:
- //扩展函数
- public final class UtilsKt {
- public static final void showToast(@NotNull Activity $this$showToast, int msgId) {
- Intrinsics.checkParameterIsNotNull($this$showToast, "$this$showToast");
- Toast.makeText((Context)$this$showToast, msgId, 0).show();
- }
- }
-
- //使用
- UtilsKt.showToast(this, 1900026);
-
可以看到所谓的扩展函数不过就是自动生成一个带有当前对象的函数,扩展函数的所在类被public final修饰,函数被public static final修饰,然后扩展的那个类被作为方法的一个参数传进去,这样就跟我们用java的时候写的工具类很像。
然后使用的时候就跟我们使用工具类一样调用工具类的方法即可。
在同一个包名下,是不可以定义相同类相同方法名的扩展方法的。但是,在不同包名下,是可以定义的。
比如我在不同的包名下定义了相同的扩展方法:
- //Utils2.kt
- package com.example.studynote.kotlin
-
- fun Activity.showToast(msg:String){
- Toast.makeText(this,msg,Toast.LENGTH_LONG).show()
- }
-
-
-
- //Utils.kt
- package com.example.studynote
-
- fun Activity.showToast(msg:String){
- Toast.makeText(this,msg,Toast.LENGTH_SHORT).show()
- }
具体会用哪个呢?就要看你导入的包是哪个了~
肯定是不能的,如果一个类的扩展方法和它已有方法同名,是可以编译过的。
但是调用的时候会优先调用类中本来就有的方法,而不是扩展方法。
kotlin中一般会把;省略,但是有两种情况还是会用到:
- enum class Color {
- RED,
- BLACK,
- BLUE,
- GREEN,
- WHITE;
-
- fun getTopColor():Color {
- return BLACK
- }
- }
-
var test="nihao" ; var test2="heihei"
- fun getInt():Int{
- "jimu".let {
- println(it.length)
- return 0
- }
- }
-
- fun getInt(): Int {
- return ArrayList<String>().apply {
- add("jimu")
- }.size
- }
-
- fun getInt(): Int {
- return with(ArrayList<String>()){
- add("jimu")
- size
- }
- }
-
- fun getInt(): Int {
- return ArrayList<String>().run{
- add("jimu")
- size
- }
- }
-
上篇说过,Kotlin有空限制,所以有些变量如果不想设置为空的时候初始化该怎么做呢?这就用到延迟初始化了,lateinit和by lazy都能实现。
lateinit用于修饰var变量,它会让编译器暂时忽略初始化这个事情,到后面用的时候我们在进行初始化,但是不能用到基本数据类型,比如int,double这种。
- lateinit var test: String
-
by lazy用于val类型的变量,它会暂时不进行初始化,并且在第一次使用的时候自动调用我们设置好的表达式进行初始化。
- val str by lazy {
- println("Init lazy")
- "Hello World"
- }
kotlin中构造函数分为主构造函数和次构造函数。
主构造函数没有函数体,直接定义在类名后。每个类都会默认带一个不带参数的构造函数,也可以直接定义参数,如果需要在构造函数中进行初始化工作,可以用init关键字:
- class Student {
- }
-
- class Student(var name: String) {
- init {
- Log.e(TAG,"name=$name")
- }
- }
-
- class Student constructor(var name: String) {
- init {
- Log.e(TAG,"name=$name")
- }
- }
-
除了类名后这种主构造函数,其他的构造函数方法就是通过constructor关键字来定义次构造函数,一个类可以定义多个次构造函数。如果主构造函数和次构造函数同时存在的时候,次构造函数必须调用主构造函数。
- class Student{
- private val username: String
- constructor(username: String){
- this.username = username
- }
- }
-
-
- class Student(username: String) {
- private var username: String
- private var age: Int
-
- init {
- this.username = username
- this.age = 10
- }
-
- constructor(username: String, age: Int) : this(username) {
- this.age = age
- }
- }
-
Kotlin协程是对线程的一种封装,同样是用来解决并发任务(异步任务)的方案,可以理解为一种线程框架,特点是挂起时不需要阻塞线程,更好的解决了线程切换,魔鬼调用的问题。
简单举个例子,具体的说明大家可以翻翻以前的文章——协程三问。
- GlobalScope.launch(Dispatchers.Main) {
- var name = ioTask()
- updateUI(name)
- var name1 = ioTask()
- updateUI(name1)
- var name2 = ioTask()
- updateUI(name2)
- }
-
- private suspend fun ioTask(): String {
- var name = ""
- withContext(Dispatchers.IO) {
- //耗时操作,比如网络接口访问
- name = "jimu"
- }
- return name
- }
-
一般指时间插值器TimeInterpolator,是设置 属性值 从初始值过渡到结束值 的变化规律,比如匀速,加速,减速等等。可以通过xml属性和java代码设置。
系统默认的插值器是AccelerateDecelerateInterpolator,即先加速后减速。
- //匀速插值器设置
- android:interpolator="@android:anim/linear_interpolator"
-
- alphaAnimation.setInterpolator(new LinearInterpolator());
属性动画中,插值器的含义就是要设置时间和属性的变化关系,也就是根据动画的进度(0%-100%)通过逻辑计算 计算出当前属性值改变的百分比。比如匀速关系就是动画进度和属性值改变的进度保持一致,50%时间进度就完成了属性值50%的变化。
- //自定义匀速插值器
- public class MyLinearInterpolator implements TimeInterpolator {
-
- @Override
- public float getInterpolation(float input) {
- return input;
- }
-
又叫类型估值算法TypeEvaluator,用来设置 属性值 从初始值过渡到结束值 的变化具体数值,刚才介绍的插值器是指变化规律,而这个估值器是决定具体的变化数值,是用来协助插值器完成动画设置。
比如属性动画设置:
- ObjectAnimator anim = ObjectAnimator.ofObject(view, "scale", new IntEvaluator(),1,10);
-
- //系统估值器类型
- IntEvaluator:针对整型属性
- FloatEvaluator:针对浮点型属性
- ArgbEvaluator:针对Color属性
可以看看IntEvaluator源码,其实就是根据三个参数—估值小数(fraction),开始值(startValue)和 结束值(endValue)然后计算具体属性变化的值:
- public class IntEvaluator implements TypeEvaluator<Integer> {
-
- public Integer evaluate(float fraction, Integer startValue, Integer endValue) {
- int startInt = startValue;
- return (int)(startInt + fraction * (endValue - startInt));
- }
- }
所以要实现一个完整的属性动画,需要估值器和插值器进行协同工作:
1. Dart 当中的 「…」表示什么意思? Dart 当中的 「…」意思是 「级联操作符」,为了方便配置而使用。「…」和「.」不同的是 调用「…」后返回的相当于是 this,而「.」返回的则是该方法返回的值 。
2. Dart 的作用域 Dart 没有 「public」「private」等关键字,默认就是公开的,私有变量使用 下划线 _开头。
3. Dart 是不是单线程模型?是如何运行的? Dart 是单线程模型,运行的的流程如下图。
简单来说,Dart 在单线程中是以消息循环机制来运行的,包含两个任务队列,一个是“微任务队列” microtask queue,另一个叫做“事件队列” event queue。
当Flutter应用启动后,消息循环机制便启动了。首先会按照先进先出的顺序逐个执行微任务队列中的任务,当所有微任务队列执行完后便开始执行事件队列中的任务,事件任务执行完毕后再去执行微任务,如此循环往复,生生不息。
4. Dart 是如何实现多任务并行的? 前面说过, Dart 是单线程的,不存在多线程,那如何进行多任务并行的呢?其实,Dart的多线程和前端的多线程有很多的相似之处。Flutter的多线程主要依赖Dart的并发编程、异步和事件驱动机制。
简单的说,在Dart中,一个Isolate对象其实就是一个isolate执行环境的引用,一般来说我们都是通过当前的isolate去控制其他的isolate完成彼此之间的交互,而当我们想要创建一个新的Isolate可以使用Isolate.spawn方法获取返回的一个新的isolate对象,两个isolate之间使用SendPort相互发送消息,而isolate中也存在了一个与之对应的ReceivePort接受消息用来处理,但是我们需要注意的是,ReceivePort和SendPort在每个isolate都有一对,只有同一个isolate中的ReceivePort才能接受到当前类的SendPort发送的消息并且处理。
5. 说一下Dart异步编程中的 Future关键字? 前面说过,Dart 在单线程中是以消息循环机制来运行的,其中包含两个任务队列,一个是“微任务队列” microtask queue,另一个叫做“事件队列” event queue。
在Java并发编程开发中,经常会使用Future来处理异步或者延迟处理任务等操作。而在Dart中,执行一个异步任务同样也可以使用Future来处理。在 Dart 的每一个 Isolate 当中,执行的优先级为 : Main > MicroTask > EventQueue。
6. 说一下Dart异步编程中的 Stream数据流? 在Dart中,Stream 和 Future 一样,都是用来处理异步编程的工具。它们的区别在于,Stream 可以接收多个异步结果,而Future 只有一个。 Stream 的创建可以使用 Stream.fromFuture,也可以使用 StreamController 来创建和控制。还有一个注意点是:普通的 Stream 只可以有一个订阅者,如果想要多订阅的话,要使用 asBroadcastStream()。
7. Stream 有哪两种订阅模式?分别是怎么调用的? Stream有两种订阅模式:单订阅(single) 和 多订阅(broadcast)。单订阅就是只能有一个订阅者,而广播是可以有多个订阅者。这就有点类似于消息服务(Message Service)的处理模式。单订阅类似于点对点,在订阅者出现之前会持有数据,在订阅者出现之后就才转交给它。而广播类似于发布订阅模式,可以同时有多个订阅者,当有数据时就会传递给所有的订阅者,而不管当前是否已有订阅者存在。
Stream 默认处于单订阅模式,所以同一个 stream 上的 listen 和其它大多数方法只能调用一次,调用第二次就会报错。但 Stream 可以通过 transform() 方法(返回另一个 Stream)进行连续调用。通过 Stream.asBroadcastStream() 可以将一个单订阅模式的 Stream 转换成一个多订阅模式的 Stream,isBroadcast 属性可以判断当前 Stream 所处的模式。
8. await for 如何使用? await for是不断获取stream流中的数据,然后执行循环体中的操作。它一般用在直到stream什么时候完成,并且必须等待传递完成之后才能使用,不然就会一直阻塞。
- Stream<String> stream = new Stream<String>.fromIterable(['不开心', '面试', '没', '过']);
- main() async{
- await for(String s in stream){
- print(s);
- }
- }
- 复制代码
9. 说一下 mixin机制? mixin 是Dart 2.1 加入的特性,以前版本通常使用abstract class代替。简单来说,mixin是为了解决继承方面的问题而引入的机制,Dart为了支持多重继承,引入了mixin关键字,它最大的特殊处在于: mixin定义的类不能有构造方法,这样可以避免继承多个类而产生的父类构造方法冲突。
mixins的对象是类,mixins绝不是继承,也不是接口,而是一种全新的特性,可以mixins多个类,mixins的使用需要满足一定条件。
1. 请简单介绍下Flutter框架,以及它的优缺点? Flutter是Google推出的一套开源跨平台UI框架,可以快速地在Android、iOS和Web平台上构建高质量的原生用户界面。同时,Flutter还是Google新研发的Fuchsia操作系统的默认开发套件。在全世界,Flutter正在被越来越多的开发者和组织使用,并且Flutter是完全免费、开源的。Flutter采用现代响应式框架构建,其中心思想是使用组件来构建应用的UI。当组件的状态发生改变时,组件会重构它的描述,Flutter会对比之前的描述,以确定底层渲染树从当前状态转换到下一个状态所需要的最小更改。
优点
缺点
2. 介绍下Flutter的理念架构 其实也就是下面这张图。
由上图可知,Flutter框架自下而上分为Embedder、Engine和Framework三层。其中,Embedder是操作系统适配层,实现了渲染 Surface设置,线程设置,以及平台插件等平台相关特性的适配;Engine层负责图形绘制、文字排版和提供Dart运行时,Engine层具有独立虚拟机,正是由于它的存在,Flutter程序才能运行在不同的平台上,实现跨平台运行;Framework层则是使用Dart编写的一套基础视图库,包含了动画、图形绘制和手势识别等功能,是使用频率最高的一层。
3. 介绍下FFlutter的FrameWork层和Engine层,以及它们的作用
Flutter的FrameWork层是用Drat编写的框架(SDK),它实现了一套基础库,包含Material(Android风格UI)和Cupertino(iOS风格)的UI界面,下面是通用的Widgets(组件),之后是一些动画、绘制、渲染、手势库等。这个纯 Dart实现的 SDK被封装为了一个叫作 dart:ui的 Dart库。我们在使用 Flutter写 App的时候,直接导入这个库即可使用组件等功能。
Flutter的Engine层是Skia 2D的绘图引擎库,其前身是一个向量绘图软件,Chrome和 Android均采用 Skia作为绘图引擎。Skia提供了非常友好的 API,并且在图形转换、文字渲染、位图渲染方面都提供了友好、高效的表现。Skia是跨平台的,所以可以被嵌入到 Flutter的 iOS SDK中,而不用去研究 iOS闭源的 Core Graphics / Core Animation。Android自带了 Skia,所以 Flutter Android SDK要比 iOS SDK小很多。
4. 介绍下Widget、State、Context 概念
5. 简述Widget的StatelessWidget和StatefulWidget两种状态组件类
StatelessWidget: 一旦创建就不关心任何变化,在下次构建之前都不会改变。它们除了依赖于自身的配置信息(在父节点构建时提供)外不再依赖于任何其他信息。比如典型的Text、Row、Column、Container等,都是StatelessWidget。它的生命周期相当简单:初始化、通过build()渲染。
StatefulWidget: 在生命周期内,该类Widget所持有的数据可能会发生变化,这样的数据被称为State,这些拥有动态内部数据的Widget被称为StatefulWidget。比如复选框、Button等。State会与Context相关联,并且此关联是永久性的,State对象将永远不会改变其Context,即使可以在树结构周围移动,也仍将与该context相关联。当state与context关联时,state被视为已挂载。StatefulWidget由两部分组成,在初始化时必须要在createState()时初始化一个与之相关的State对象。
6. StatefulWidget 的生命周期 Flutter的Widget分为StatelessWidget和StatefulWidget两种。其中,StatelessWidget是无状态的,StatefulWidget是有状态的,因此实际使用时,更多的是StatefulWidget。StatefulWidget的生命周期如下图。
7. 简述Widgets、RenderObjects 和 Elements的关系 首先看一下这几个对象的含义及作用。
Widget会被inflate(填充)到Element,并由Element管理底层渲染树。Widget并不会直接管理状态及渲染,而是通过State这个对象来管理状态。Flutter创建Element的可见树,相对于Widget来说,是可变的,通常界面开发中,我们不用直接操作Element,而是由框架层实现内部逻辑。就如一个UI视图树中,可能包含有多个TextWidget(Widget被使用多次),但是放在内部视图树的视角,这些TextWidget都是填充到一个个独立的Element中。Element会持有renderObject和widget的实例。记住,Widget 只是一个配置,RenderObject 负责管理布局、绘制等操作。
在第一次创建 Widget 的时候,会对应创建一个 Element, 然后将该元素插入树中。如果之后 Widget 发生了变化,则将其与旧的 Widget 进行比较,并且相应地更新 Element。重要的是,Element 不会被重建,只是更新而已。
8. 什么是状态管理,你了解哪些状态管理框架? Flutter中的状态和前端React中的状态概念是一致的。React框架的核心思想是组件化,应用由组件搭建而成,组件最重要的概念就是状态,状态是一个组件的UI数据模型,是组件渲染时的数据依据。
Flutter的状态可以分为全局状态和局部状态两种。常用的状态管理有ScopedModel、BLoC、Redux / FishRedux和Provider。详细使用情况和差异可以自行了解。
9. 简述Flutter的绘制流程
Flutter的绘制流程如下图所示。
Flutter只关心向 GPU提供视图数据,GPU的 VSync信号同步到 UI线程,UI线程使用 Dart来构建抽象的视图结构,这份数据结构在 GPU线程进行图层合成,视图数据提供给 Skia引擎渲染为 GPU数据,这些数据通过 OpenGL或者 Vulkan提供给 GPU。
10. 简述Flutter的线程管理模型 默认情况下,Flutter Engine层会创建一个Isolate,并且Dart代码默认就运行在这个主Isolate上。必要时可以使用spawnUri和spawn两种方式来创建新的Isolate,在Flutter中,新创建的Isolate由Flutter进行统一的管理。 事实上,Flutter Engine自己不创建和管理线程,Flutter Engine线程的创建和管理是Embeder负责的,Embeder指的是将引擎移植到平台的中间层代码,Flutter Engine层的架构示意图如下图所示。
在Flutter的架构中,Embeder提供四个Task Runner,分别是Platform Task Runner、UI Task Runner Thread、GPU Task Runner和IO Task Runner,每个Task Runner负责不同的任务,Flutter Engine不在乎Task Runner运行在哪个线程,但是它需要线程在整个生命周期里面保持稳定。
11. Flutter 是如何与原生Android、iOS进行通信的? Flutter 通过 PlatformChannel 与原生进行交互,其中 PlatformChannel 分为三种:
同时 Platform Channel 并非是线程安全的 ,更多详细可查阅闲鱼技术的 《深入理解Flutter Platform Channel》
12. 简述Flutter 的热重载 Flutter 的热重载是基于 JIT 编译模式的代码增量同步。由于 JIT 属于动态编译,能够将 Dart 代码编译成生成中间代码,让 Dart VM 在运行时解释执行,因此可以通过动态更新中间代码实现增量同步。
热重载的流程可以分为 5 步,包括:扫描工程改动、增量编译、推送更新、代码合并、Widget 重建。Flutter 在接收到代码变更后,并不会让 App 重新启动执行,而只会触发 Widget 树的重新绘制,因此可以保持改动前的状态,大大缩短了从代码修改到看到修改产生的变化之间所需要的时间。
另一方面,由于涉及到状态的保存与恢复,涉及状态兼容与状态初始化的场景,热重载是无法支持的,如改动前后 Widget 状态无法兼容、全局变量与静态属性的更改、main 方法里的更改、initState 方法里的更改、枚举和泛型的更改等。
可以发现,热重载提高了调试 UI 的效率,非常适合写界面样式这样需要反复查看修改效果的场景。但由于其状态保存的机制所限,热重载本身也有一些无法支持的边界。
由于文章内容比较多,篇幅不允许,部分未展示内容以截图方式展示 。如有需要获取完整的资料文档的朋友点击我的GitHub免费获取。
喜欢本文的读者,记得给个关注+点赞,整理不易,且行且珍惜!
在CSDN看到好的文章,又点赞的读者,思想都比一般人豁达!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。