前言
马上就要到金九银十面试季了,需要找工作的小伙伴们要行动起来开始准备复习了。
想要杀出重围,必然要有万全的准备。除了一份美观的简历,还必须刷一刷近几年技术面试的面题。
今天,就给大家总结了这篇2023年最新的Android面试题合集,有大厂面经和技术题解,希望能对有需求的小伙伴们有所帮助!
Java篇
一、Java异常机制中,异常Exception与错误Error区别
这道题想考察什么?
在开发时需要时候需要自定义异常时,应该选择定义Excption还是Error?编写的代码触发Excption或者Error分别代表什么?
考察的知识点
Java异常机制
考生应该如何回答
在Java中存在一个Throwable可抛出类,Throwable有两个重要的子类,一个是Error,另一个则是Exception。
Error是程序不能处理的错误,表示程序中较严重问题。例如,Java虚拟机运行错误(Virtual MachineError),当 JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError等等。这些错误发生时,JVM一般会选择线程终止。这些错误是不可查的,它们在程序的控制和处理能力之外,而且绝大多数是程序运行时不允许出现的状况。
Exception是程序可以处理的异常。而Exception又分为运行时异常(RuntimeException)与非运行时异常。
-
运行异常
运行时异常,又称不受检查异常 。所谓的不受检查,指的是Java编译检查时不会告诉我们有这个异常,需要在运行时候才会暴露出来,比如下标越界,空指针异常等。
-
非运行时异常
RuntimeException之外的异常我们统称为非运行时异常,比如IOException、SQLException,是必须进行处理的异常(检查异常) ,如果不处理(throw到上层或者try-catch),程序就不能编译通过 。
二、说说反射的应用场景,哪些框架?
这道题想考察什么?
是否了解反射相关的理论知识
考察的知识点
- 反射机制
- 反射在框架中的应用
- 获取Class类的主要方式
考生应该如何回答
反射是Java程序开发语言的特征之一,它允许动态地发现和绑定类、方法、字段,以及所有其他的由于有所产生的的元素。通过反射,能够在需要时完成创建实例、调用方法和访问字段的工作。
反射机制主要提供功能有:
- 在运行时判断任意一个对象所属的类
- 在运行时构造任意一个类的对象
- 在运行时判断任意一个类所具有的成员变量和方法
- 在运行时调用任意一个对象的方法,通过反射甚至可以调用到private修饰的方法
- 生成动态代理
反射在框架中用的非常多。我们可以利用反射机制在Java程序中,动态的去调用一些protected甚至是private的方法或类,这样可以很大程度上满足我们的一些比较特殊需求。例如Activity的启动过程中Activity的对象的创建。
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
//省略
Activity activity = null;
try {
java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
StrictMode.incrementExpectedActivityCount(activity.getClass());
r.intent.setExtrasClassLoader(cl);
r.intent.prepareToEnterProcess();
if (r.state != null) {
r.state.setClassLoader(cl);
}
}
//上面代码可知Activity在创建对象的时候调用了mInstrumentation.newActivity();
public Activity newActivity(ClassLoader cl, String className,
Intent intent)
throws InstantiationException, IllegalAccessException,
ClassNotFoundException {
//这里的className就是在manifest中注册的Activity name.
return (Activity)cl.loadClass(className).newInstance();
}
//Activity对象的创建是通过反射完成的。java程序可以动态加载类定义,而这个动态加载的机制就是通过ClassLoader来实现的.
Java的每个.class文件里承载了类的所有信息内容,包含父类、接口、属性、构造、方法等;这些class文件会在程序运行状态下通过ClassLoad类机制加载到虚拟机内存中并产生一个对象实例,然后这个对象就可以对这块内存地址进行引用;一般我们通过New一个对象创建就行,而利用反射我们通过JVM查找并指定加载的类名,就可以创建对象实例,而在java中,类的对象被实例化可以在内存中被引用时,就可以获取该类的所有信息。可以说反射是所有插件化框架的基础。
//反射获取类的实例化对象
Class<?> cls=Class.forName("com.fanshe.Person"); //forName(包名.类名)
Person p=(Person)cls.newInstance();
//或
Person p = new Person();
Class<?> cls=p.getClass();
Person p2=(Person)cls.newInstance();
//或
Class<?> cls=Person.Class();
Person p=(Person)cls.newInstance();
//反射获取类的属性的值
Field getField = cls.getDeclaredField(fieldName);//传入属性名
Object getValue =getField.get(newclass);//传入实例值
//静态属性不用借助实例
Field getField = cls.getDeclaredField(fieldName);
Object getValue =getField.get(null);
反射在插件化、热修复、Retrofit等等几乎所有的常见框架中均有使用。
三、JVM DVM ART的区别
这道题想考察什么?
Android虚拟机与Java虚拟机的差异
考察的知识点
- JVM虚拟的基本知识
- DVM虚拟机的基本知识
- ART虚拟机的基本知识
考生应该如何回答
JVM
JVM是基于栈的虚拟机,对于基于栈的虚拟机来说,每一个运行时的线程,都有一个独立的栈。栈中记录了方法调用的历史,每有一次方法调用,栈中便会多一个栈桢。最顶部的栈桢称作当前栈桢,其代表着当前执行的方法。基于栈的虚拟机通过操作数栈进行所有操作。
在JVM中执行字节码,将
int a = 1;
int b = 2;
int c = a + b;
编译为字节码,得到的指令为:
ICONST_1 #将int类型常量1压入操作数栈
ISTORE 0 #将栈顶int类型值存入局部变量0
ICONST_2
ISTORE 1
ILOAD 0 #从局部变量表加载0:int类型数据
ILOAD 1
IADD #执行int类型加法
ISTORE 2
数据会不断在操作数栈与局部变量表之间移动。
Dalvik
Dalvik虚拟机( Dalvik Virtual Machine ),简称Dalvik VM或者DVM。DVM是Google专门为Android平台开发的虚拟机,它运行在Android运行时库中。
与JVM区别
基于的架构不同
DVM是基于寄存器的虚拟机,并没有JVM栈帧中的操作数栈,但是有很多虚拟寄存器。其实和操作数栈相同,这些寄存器也存放在运行时栈中,本质上就是一个数组。与JVM相似,在Dalvik VM中每个线程都有自己的PC和调用栈,方法调用的活动记录以帧为单位保存在调用栈上。
与JVM版相比,Dalvik无需从栈中读写数据,所需的指令更少,数据也不再需要频繁的移动。
#int a = 1
const/4 v0, #int 1 // #1
#int b = 2
const/4 v1, #int 2 // #2
#int c = a + b
add-int v2, v0, v1
执行的字节码不同
- 在Java中,Java类会被编译成一个或多个.class文件,打包成jar文件,而后JVM会通过相应的.class文件和jar文件获取相应的字节码。
- DVM会用dx工具将所有的.class文件或者jar文件转换为一个.dex文件,然后DVM会从该.dex文件读取指令和数据。
.jar文件在中包含多个.class文件,每个.class文件里面包含了该类的常量池、类信息、属性等等。当JVM加载该.jar文件的时候,会加载里面的所有的.class文件,JVM的这种加载方式很慢,对于内存有限的移动设备并不合适。
而.dex文件中也包含很多类信息,但是dex将多个类信息整合在一起了,多个类复用常量池等区域。
ART
ART(Android Runtime)是Android 4.4发布的,用来替换Dalvik虚拟,Android 4.4默认采用的还是DVM,系统会提供一个选项来开启ART。在Android 5.0时,默认采用ART。
与DVM的区别
Dalvik下应用在安装的过程,会执行一次优化,将dex字节码进行优化生成odex文件。
ART能够兼容Dalvik中的字节码执行。但是ART 引入了预先编译机制(Ahead Of Time),在Android4.4到Android 7.0以下的设备中安装应用时,ART 会使用 dex2oat 程序编译应用,将应用中dex编译成本地机器码。但是此过程会导致应用安装变慢。
因此,从Android N(Android 7.0)开始,ART 混合使用AOT、JIT与解释执行:
1、最初安装应用时不进行任何 AOT 编译(避免安装过慢),运行过程中解释执行,对经常执行的方法进行JIT,经过 JIT 编译的方法将会记录到Profile配置文件中。
2、当设备闲置和充电时,编译守护进程会运行,根据Profile文件对常用代码进行 AOT 编译。待下次运行时直接使用。
四、泛型是什么,泛型擦除呢?
这道题想考察什么?
泛型
考察的知识点
泛型的特点和优缺点以及泛型擦除
考生应该如何回答
泛型就是一种就是一种不确定的数据类型。在Java中有着重要的地位,在面向对象编程及各种设计模式中都有非常广泛的应用。
泛型的优点
我们为什么需要使用泛型:
- 适用于多种数据类型执行相同的代码,例如两个数据相加:
public int addInt(int x,int y){
return x+y;
}
public float addFloat(float x,float y){
return x+y;
}
不同的类型,我们就需要增加不同的方法,但是使用泛型那我们的代表将变为:
public <T> T addInt(T x,T y){
return x+y;
}
- 编译检查,例如下面代码
List<String> list = new ArrayList();
list.add(10);//①
list.add("享学");
String name = list.get(2);//②
因为我们指定了List泛型类型为String,因此在代码1处编译时会报错。而在代码2处,不再需要做类型强转。
泛型的缺点
- 静态域或者方法里不能引用泛型变量,因为泛型是在new对象的时候才知道,而类的构造方法是在静态变量之后执行。
- 不能捕获泛型类对象
泛型擦除
Jdk中实现的泛型实际上是伪泛型,例如泛型类 Fruit<T> ,编译时 T 会被擦除,成为 Object。但是泛型擦除会带来一个复杂的问题:
桥方法
有如下代码:
public class Parent<T> {
public void setSrc(T src){
}
}
public class Child extends Parent<String>{
@Override
public void setSrc(String src) {
super.setSrc(src);
}
}
Parent类是一个泛型类,在经过编译时泛型擦除后其中setSrc(T) 将会变为setSrc(Object);而Child类继承与Parent并且指定了泛型类型为String。那么经过编译后这两个类应该变为:
public class Parent {
public void setSrc(Object src){
}
}
public class Child extends Parent{
@Override
public void setSrc(String src) {
super.setSrc(src);
}
}
父类存在setSrc(Object),而子类则是setSrc(String)。这明显是两个不同的方法,按照Java的重写规则,子类并没有重写父类的方法,而是重载。
所以实际上子类中存在两个setSrc方法。一个自己的,一个是继承自父类的:
public void setSrc(String src)
public void setSrc(Object src)
那么当我们:
Parent o = new Child();
o.setSrc("1");
此时o实际类型是Child,静态类型是Parent。按照Java规则,会调用父类中的setSrc(Object),如:
public class A{
public void setValue(Object value){
System.out.println("Object");
}
}
public class B extends A{
public void setValue(String value){
System.out.println("String");
}
}
public static void main(String[] args) {
A a = new B();
a.setValue("1");
a.setValue(11);
}
上诉代码会输出两次”Object“。然而在泛型中却不符合此规则,因为 Java 编译器帮我们处理了这种情况,在泛型中引入了"Bridge Method"—桥方法。通过查看Child.class的字节码文件 :
public void setSrc(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: invokespecial #2 // Method Parent.setSrc:(Ljava/lang/Object;)V
5: return
LineNumberTable:
line 4: 0
line 6: 5
public void setSrc(java.lang.Object);
descriptor: (Ljava/lang/Object;)V
flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: checkcast #3 // class java/lang/String
5: invokevirtual #4 // Method setSrc:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 1: 0
}
可以看到 Child 类中有两个 setSrc方法,一个参数为 String 类型,一个参数为 Object 类型,参数为 Object 类型。而参数为Object的setSrc方法可以在flags中看到ACC_BRIDGE和ACC_SYNTHETIC 。其中ACC_BRIDGE用于说明这个方法是由编译生成的桥接方法,ACC_SYNTHETIC说明这个方法是由编译器生成,并且不会在源代码中出现。
在setSrc(Object)桥方法可以看到实际上会使用checkcast先进行类型转换检查,然后执行invokevirtual调用setSrc(String)方法,这样就避免了我们还能调用到父类的方法。
Android篇
一、Activity 与 Fragment 之间常见的几种通信方式?
viewModel 做数据管理,activity 和 fragment 公用同个viewModel 实现数据传递
二、LaunchMode 的应用场景?
LaunchMode 有四种,分别为 Standard, SingleTop,SingleTask 和 SingleInstance,每种模式的实现原理一楼都做了较详细说明,下面说一下具体使用场景:
- Standard: Standard模式是系统默认的启动模式,一般我们 app 中大部分页面都是由该模式的页面构成的,比较常见的场景是:社交应用中,点击查看用户A信息->查看用户A 粉丝->在粉丝中挑选查看用户B信息->查看用户A粉丝... 这种情况下一般我们需要保留用户操作 Activity栈的页面所有执行顺序。
- SingleTop: SingleTop 模式一般常见于社交应用中的通知栏行为功能,例如:App 用户收到几条好友请求的推送消息,需要用户点击推送通知进入到请求者个人信息页,将信息页设置为 SingleTop 模式就可以增强复用性。
- SingleTask: SingleTask 模式一般用作应用的首页,例如浏览器主页,用户可能从多个应用启动浏览器,但主界面仅仅启动一次,其余情况都会走onNewIntent,并且会清空主界面上面的其他页面。
- SingleInstance: SingleInstance 模式常应用于独立栈操作的应用,如闹钟的提醒页面,当你在A应用中看视频时,闹钟响了,你点击闹钟提醒通知后进入提醒详情页面,然后点击返回就再次回到A的视频页面,这样就不会过多干扰到用户先前的操作了。
三、BroadcastReceiver 与LocalBroadcastReceiver 有什么区别?
- BroadcastReceiver 是跨应用广播,利用Binder机制实现,支持动态和静态两种方式注册方式。
- LocalBroadcastReceiver 是应用内广播,利用Handler 实现,利用了IntentFilter的match功能,提供消息的发布与接收功能,实现应用内通信,效率和安全性比较高,仅支持动态注册。
四、谈一谈startService和bindService的区别,生命周期以及使用场景?
- 生命周期上的区别 执行startService时,Service会经历onCreate- >onStartCommand。当执行stopService时,直接调用onDestroy方法。调用者如果没有stopService,Service 会一直在后台运行,下次调用者再起来仍然可以stopService。执行bindService时,Service会经历onCreate- >onBind。这个时候调用者和Service绑定在一起。调用者调用unbindService方法或者调用者Context不存在了(如Activity被finish了),Service就会调用onUnbind- >onDestroy。这里所谓的绑定在一起就是说两者共存亡了。多次调用startService,该Service只能被创建一次,即该Service的onCreate方法只会被调用一次。但是每次调用startService,onStartCommand方法都会被调用。Service的onStart方法在API 5时被废弃,替代它的是onStartCommand方法。第一次执行bindService时,onCreate和onBind方法会被调用,但是多次执行bindService时,onCreate和onBind 方法并不会被多次调用,即并不会多次创建服务和绑定服务。
- 调用者如何获取绑定后的Service的方法 onBind回调方法将返回给客户端一个IBinder接口实例,IBinder允许客户端回调服务的方法,比如得到Service运行的状态或其他操作。我们需要IBinder对象返回具体的Service对象才能操作,所以说具体的Service对象必须首先实现Binder对象。
- 既使用startService又使用bindService的情况 如果一个Service又被启动又被绑定,则该Service会一直在后台运行。首先不管如何调用,onCreate始终只会调用一次。对应startService调用多少次,Service的onStart 方法便会调用多少次。Service的终止,需要unbindService和stopService同时调用才行。不管startService与bindService的调用顺序,如果先调用unbindService,此时服务不会自动终止,再调用stopService之后,服务才会终止;如果先调用stopService,此时服务也不会终止,而再调用unbindService或者之前调用bindService的Context不存在了(如Activity被finish的时候)之后,服务才会自动停止。 那么,什么情况下既使用startService,又使用bindService呢? 如果你只是想要启动一个后台服务长期进行某项任务,那么使用startService便可以了。如果你还想要与正在运行的Service取得联系,那么有两种方法:一种是使用broadcast,另一种是使用bindService。前者的缺点是如果交流较为频繁,容易造成性能上的问题,而后者则没有这些问题。因此,这种情况就需要startService和bindService一起使用了。 另外,如果你的服务只是公开一个远程接口,供连接上的客户端(Android的Service是C/S架构)远程调用执行方法,这个时候你可以不让服务一开始就运行,而只是bindService,这样在第一次bindService的时候才会创建服务的实例运行它,这会节约很多系统资源,特别是如果你的服务是远程服务,那么效果会越明显(当然在Servcie创建的是偶会花去一定时间,这点需要注意)。
- 本地服务与远程服务 本地服务依附在主进程上,在一定程度上节约了资源。本地服务因为是在同一进程,因此不需要IPC,也不需要AIDL。相应 bindService会方便很多。缺点是主进程被kill后,服务变会终止。远程服务是独立的进程,对应进程名格式为所在包名加上你指定的android:process字符串。由于是独立的进程,因此在Activity所在进程被kill的是偶,该服务依然在运行。缺点是该服务是独立的进程,会占用一定资源,并且使用AIDL进行IPC稍微麻烦一点。对于startService来说,不管是本地服务还是远程服务,我们需要做的工作都一样简单。
五、谈谈你对 Activity.runOnUiThread 的理解?
一般是用来将一个runnable绑定到主线程,在runOnUiThread源码里面会判断当前runnable是否是主线程,如果是直接run,如果不是,通过一个默认的空构造函数handler将runnable post 到looper里面,创建构造函数handler,会默认绑定一个主线程的looper对象
六、子线程能否更新UI?为什么?
子线程是不能直接更新UI的 注意这句话,是不能直接更新,不是不能更新(极端情况下可更新)绘制过程要保持同步(否则页面不流畅),而我们的主线程负责绘制ui,极端情况就是,在Activity的onResume(含)之前的生命周期中子线程都可以进行更新ui,也就是onCreate,onStart和onResume,此时主线程的绘制还没开始。
七、 谈谈 Handler 机制和原理?
首先在UI线程我们创建了一个Handler实例对象,无论是匿名内部类还是自定义类生成的Handler实例对象,我们都需要对handleMessage方法进行重写,在handleMessage方法中我们可以通过参数msg来写接受消息过后UIi线程的逻辑处理,接着我们创建子线程,在子线程中需要更新UI的时候,新建一个Message对象,并且将消息的数据记录在这个消息对象Message的内部,比如arg1,arg2,obj等,然后通过前面的Handler实例对象调用sendMessge方法把这个Message实例对象发送出去,之后这个消息会被存放于MessageQueue中等待被处理,此时MessageQueue的管家Looper正在不停的把MessageQueue存在的消息取出来,通过回调dispatchMessage方法将消息传递给Handler的handleMessage方法,最终前面提到的消息会被Looper 从MessageQueue中取出来传递给handleMessage方法。
八、简述一下 Android 中 UI 的刷新机制?
1、界面刷新的本质流程
- 通过ViewRootImpl的 scheduleTraversals()进行界面的三大流程。
- 调用到scheduleTraversals()时不会立即执行,而是将该操作保存到待执行队列中。并给底层的刷新信号注册监听。
- 当 VSYNC信号到来时,会从待执行队列中取出对应的scheduleTraversals()操作,并将其加入到主线程 的消息队列中。
- 主线程从 消息队列中取出并执行三大流程:onMeasure()-onLayout()-onDraw()
2、同步屏障的作用
- 同步屏障用于阻 塞 住所有的同步消息(底层VSYNC的回调onVsync方法提交的消息是异步消息)
- 用于保证界面刷新功能的performTraversals()的优先执行。
3、同步屏障的原理?
- 主线程的Looper会一直循环调用MessageQueue的next方法并且取出队列头部的Message执行,遇到同步屏障(一种特殊消息)后会去寻找异步消息执行。如果没有找到异步消息就会一直阻塞下去,除非将同步屏障取出,否则永远不会执行同步消息。
- 界面刷新操作是异步消息,具有最高优先级
- 我们发送的消息是同步消息,再多耗时操作也不会影响UI的刷新操作
九、谈谈Android的事件分发机制?
当点击的时候,会先调用顶级viewgroup的dispatchTouchEvent,如果顶级的viewgroup拦截了此事件(onInterceptTouchEvent返回true),则此事件序列 由顶级viewgroup处理。如果顶级viewgroup设置setOnTouchListener,则会回调接口中的onTouch,此时顶级的viewgroup中的onTouchEvent不再回调,如果不设 置setOnTouchListener则onTouchEvent会回调。如果顶级viewgroup设置setOnClickListener,则会回调接口中的onClick。如果顶级viewgroup不拦截事件,事件就会向下传递给他的子view,然后子view就会调用它的dispatchTouchEvent方法。
十、谈谈如何优化ListView?
ViewHolder什么的持有View预加载/懒加载数据什么的 大招:用RecyclerView替换ListView 绝招:直接删除控件
十一、谈谈自定义LayoutManager的流程?
- 确定Itemview的LayoutParams generateDefaultLayoutParams
- 确定所有itemview在recyclerview的位置,并且回收和复用itemview onLayoutChildren
- 添加滑动canScrollVertically
十二、谈一谈获取View宽高的几种方法?
- OnGlobalLayoutListener获取
- OnPreDrawListener获取
- OnLayoutChangeListener获取
- 重写View的onSizeChanged()
- 使用View.post()方法
十三、Kotlin 中infix 关键字的原理和使用场景?
使用场景是用来修饰函数,使用了 infix 关键字的函数称为中缀函数,使用时可以省略 点表达式和括号。让代码看起来更加优雅,更加语义化。原理不过是编译器在语法层面给与了支持,编译为 Java代码后可以看到就是普通的函数调用。kotlin 的很多特性都是在语法和编译器上的优化。
十四、Kotlin中的可见性修饰符有哪些?相比于Java有什么区别?
kotlin存在四种可见性修饰符,默认是public。 private、protected、internal、public
- private、protected、public是和java中的一样的。
- 不同的是java中默认是default修饰符(包可见),而kotlin存在internal修饰符(模块内部可见)。
- kotlin可以直接在文件顶级声明方法、变量等。其中protected不能用来修饰在文件顶级声明的类、方法、变量等。构造方法默认是public修饰,可以使用可见性修饰符修饰constructor关键字来改变构造方法的可见性。
十五、请谈谈你对Binder机制的理解?
Binder机制:
- 为了保证进程空间不被其他进程破坏或干扰,Linux中的进程是相互独立或相互隔离的。
- 进程空间分为用户空间和内核空间。用户空间不可以进行数据交互;内核空间可以进行数据交互,所有进程共用一个内核空间。
- Binder机制相对于Linux内传统的进程间通信方式: (1)性能更好;Binder机制只需要拷贝数据一次,管道、 消息队列、Socket等都需要拷贝数据两次;而共享内存虽然不需要拷贝,但实现复杂度高。 (2)安全性更高;Binder机制通过UID/PID在内核空间添加了身份标识,安全性更高。
- Binder跨进程通信机制:基于C/S架构,由Client、Server、ServerManager和Binder驱动组成。
- Binder驱动实现的原理:通过内存映射,即系统调用了mmap()函数。
- Server Manager的作用:管理Service的注册和查询。
- Binder驱动的作用: (1)传递进程间的数据,通过系统 调用mmap()函数; (2)实现线程的控制,通过Binder 驱动的线程池,并由Binder驱动自身进行管理。
- Server进程会创建很多线程处理Binder请求,这些线程采用Binder驱动的线程池,由Binder驱动自身进行管理。一个进程的Binder线程池默认最大是16个,超过的请求会 阻塞等待空闲的线程。
- Android中进行进程间通信主要通过Binder类(已经实现了IBinder接口),即具备了跨进程通信的能力
Android Framework篇
一、你了解 Android 系统启动流程吗?
当按电源键触发开机,首先会从ROM中预定义的地方加载引导程序BootLoader 到 RAM中,并执行BootLoader程序启动Linux Kernel,然后启动用户级别的第一个进程: init 进程。init 进程会解析init.rc脚本做一些初始化工作,包括挂载文件系统创建工作目录以及启动系统服务进程等,其中系统服务进程包括 Zygote、service manager、media 等。在 Zygote中会进一步去启动 system_ server进程,然后在 system_server 进程中会启动AMS、WMS、PMS等服务,等这些服务启动之后,AMS中就会打开Launcher应用的home Activity,最终就看到了手机的“桌面”。
二、system_server 为什么要在 Zygote 中启动,而不是由 init 直接启动呢?
Zygote 作为一个孵化器,可以提前加载一些资源,这样 fork()时基于 Copy-On-Write 机制创建的其他进程就能直接使用这些资源,而不用重新加载。 比如 system_server 就可以直接使用 Zygote 中的 JNI 函数、共享库、常用的类、以及主题资源。
三、为什么要专门使用 Zygote 进程去孵化应用进程,而不是让system_server去孵化呢?
首先 system_server 相比 Zygote 多运行了 AMS、 WMS 等服务,这些对一个应用程序来说是不需要的。另外进程的fork()对多线程不友好,仅会将发起调用的线程拷贝到子进程,这可能会导致死锁,而system_server 中肯定是有很多线程的。
四、能具体说说是怎么导致的死锁的吗?
在POSIX 标准中,fork的行为是这样的:复制整个用户空间的数据(通常使用 copy-on-write 的策略,所以可以实现的速度快)以及所有系统对象,然后仅复制当前线程到子进程。这里:所有父进程中别的线程,到了子进程中都是突然蒸发掉的对于锁来说,从 OS 看,每个锁有一个所有者,即最后一次 lock 它的线程。假设这么一个环境,在 fork 之前,有一个子线程 lock 了某个锁,获得了对锁的所有权。fork 以后,在子进程中,所有的额外线程都人间蒸发了。而锁却被正常复制了,在子进程看来,这个锁没有主人,所以没有任何人可以对它解锁。 当子进程想 lock 这个锁时,不再有任何手段可以解开了。
五、Zygote 为什么不采用 Binder 机制进行 IPC 通信?
Binder机制中存在Binder线程池,是多线程的,如果Zygote采用Binder的话就存在上面说的fork()与多线程的问题了。其实严格来说,Binder机制不一定要多线程,所谓的Binder线程只不过是在循环读取Binder驱动的消息而已,只注册一个Binder线程也是可以工作的,比如service manager 就是这样的。实际上Zygote尽管没有采取Binder机制,它也不是单线程的,但它在fork()前主动停止了其他线程,fork()后重新启动了。
六、Binder有什么优势
性能方面
- 共享内存 0次数据拷贝
- Binder 1次数据拷贝
- Socket/管道/消息队列 2次数据拷贝
稳定性方面
- Binder:基于C/S架构,客户端 (Client) 有什么需求就丢给服务端 (Server) 去完成,架构清晰、职责明确又相互独立,自然稳定性更好
- 共享内存:虽然无需拷贝,但是控制复杂,难以使用
- 从稳定性的角度讲,Binder机制是优于内存共享的。
安全性方面
- 传统的IPC没有任何安全措施,安全依赖上层协议来确保。
- 传统的IPC方法无法获得对方可靠的进程用户ID/进程UI (UID/PID) ,从而无法鉴别对方身份。
- 传统的IPC只能由用户在数据包中填入UID/PID,容易被恶意程序利用。
- 传统的IPC访问接入点是开放的,无法阻止恶意程序通过猜测接收方地址获得连接。
- Binder既支持实名Binder,又支持匿名Binder,安全性高。
七、Binder是如何做到一次拷贝的
主要是因为Linux是使用的虚拟内存寻址方式,它有如下特性:
- 用户空间的虚拟内存地址是映射到物理内存中的。
- 对虚拟内存的读写实际上是对物理内存的读写,这个过程就是内存映射。
- 这个内存映射过程是通过系统调用mmap()来实现的。
- Binder借助了内存映射的方法,在内核空间的接收方用户控件的数据缓存区之间做了一层内存映射,就相当于直接拷贝到了接收方用户空间的数据缓存区,从而减少了一次数据拷贝。
八、MMAP的内存映射原理了解吗?
MMAP内存映射的实现过程,总的来说可以分为三个阶段: (一)进程启动映射过程,并在虚拟地址空间中为映射创建虚拟映射区域
- 进程在用户空间调用库函数mmap,原型:void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
- 在当前进程的虚拟地址空间中,寻找一段空闲的满足要求的连续的虚拟地址
- 为此虚拟区分配一个vm_area_struct结构,接着对这个结构的各个域进行了初始化
- 将新建的虚拟区结构 (vm_area_struct) 插入进程的虚拟地址区域链表或树中
(二)调用内核空间的系统调用函数mmap(不同于用户空间函数),实现文件物理地址和进程虚拟地址的一一映射关系
- 为映射分配了新的虚拟地址区域后,通过待映射的文件指针,在文件描述符表中找到对应的文件描 述 符,通过文件描述符,链接到内核“已打开文件集”中该文件的文件结构体 (struct file) , 每个 文件结构体维护着和这个已打开文件相关各项信息。
- 通过该文件的文件结构体,链接到file_operations模块,调用内核函数mmap,其原型为: int mmap(struct file *filp, struct vm_area_struct *vma),不同于用户空间库 函数。
- 内核mmap函数通过虚拟文件系统inode模块定位到文件磁盘物理地址。
- 通过remap_pfn_range函数建立页表,即实现了文件地址和虚拟地址区域的映射关系。此时,这 片 虚拟地址并没有任何数据关联到主存中。 注:前两个阶段仅在于创建虚拟区间并完成地址映射,但是并没有将任何文件数据的拷贝至主存。 真正的文件读取是当进程发起读或写操作时。进程的读或写操作访问虚拟地址空间这一段映射地址,通过查询页表,发现这一段地址并不在物 理页面上。因为目前只建立了地址映射,真正的硬盘数据还没有拷贝到内存中,因此引发缺页异常。
(三)进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存(主存)的拷贝 注:前两个阶段仅在于创建虚拟区间并完成地址映射,但是并没有将任何文件数据的拷贝至主存。真正的文件读取是当进程发起读或写操作时。进程的读或写操作访问虚拟地址空间这一段映射地址,通过查询页表,发现这一段地址并不物理页面上。因为目前只建立了地址映射,真正的硬盘数据还没有拷贝到内存中,因此引发缺页异常。
- 缺页异常进行一系列判断,确定无非法操作后,内核发起请求调页过程。
- 调页过程先在交换缓存空间 (swap cache) 中寻找需要访问的内存页,如果没有则调用nopage 函 数把所缺的页从磁盘装入到主存中。
- 之后进程即可对这片主存进行读或者写的操作,如果写操作改变了其内容,一定时间后系统会自动回写脏页面到对应磁盘地址,也即完成了写入到文件的过程。
注 :修改过的脏页面并不会立即更新回文件中,而是有一段时间的延迟,可以调用msync()来强制同步,这样所写的内容就能立即保存到文件里了
九、Binder机制是如何跨进程的
- Binder驱动
- 在内核空间创建一块接收缓存区,
- 实现地址映射:将内核缓存区、接收进程用户空间映射到同一接收缓存区
- 发送进程通过系统调用 (copy_from_user) 将数据发送到内核缓存区。由于内核缓存区和 接收进程用户空间存在映射关系,故相当于也发送了接收进程的用户空间,实现了跨进程通信
十、简述下Handler机制的总体原理
- Looper 准备和开启轮循: Looper#prepare()初始化线程独有的 Looper 以及 MessageQueue Looper#loop()开启死循环读取 MessageQueue 中下一个满足执行时间的 Message 尚无 Message 的话,调用 Native 侧的 pollOnce()进入无限等待存在 Message,但执行时间 when 尚未满足的话,调用 pollOnce()时传入剩余时长参数进入有限等待
- Message 发送、入队和出队: Native 侧如果处于无限等待的话:任意线程向 Handler 发送 Message 或 Runnable 后,Message 将按照 when 条件的先后,被插 入 Handler 持有的 Looper 实例所对应的 MessageQueue 中适当的位置。 MessageQueue 发现有合适的 Message 插入后将调用 Native 侧的 wake() 唤醒无限等待的线程。这将促使 MessageQueue 的读取继续进入下一次循环,此刻 Queue 中已有满足条件的 Message 则 出队返回给 Looper Native 侧如果处于有限等待的话:在等待指定时长后 epoll_wait 将返回。线程继续读取 MessageQueue, 此 刻因为时长条件将满足将其出队 Looper 处理Message 的实现:
- Looper 得到 Message 后回调 Message 的 callback 属性即 Runnable,或依据 target 属性即 Handler,去执行 Handler 的回调。存在 mCallback属性的话回调 Handler$Callback 反之,回调 handleMessage()
十一、 Looper 存在哪?如何可以保证线程独有?
Looper实例被管理在静态属性sThreadLocal中ThreadLocal内部通过ThreadLocalMap持有Looper,key为ThreadLocal实例本身,value即为Looper实例每个Thread都有一个自己的ThreadLocalMap,这样可以保证每个线程对应一个独立的 Looper实例,进而保证 myLooper()可以获得线程独有的 Looper 彩蛋:一个 App 拥有几个 Looper 实例?几个 ThreadLocal 实例?几个 MessageQueue 实例?几个Message 实例?几个Handler实例一个线程只有一个 Looper实例一个 Looper 实例只对应着一个 MessageQueue实例一个MessageQueue 实例可对应多个 Message 实例,其从 Message 静态池里获取,存在50的上限一个线程可以拥有多个 Handler 实例,其Handler 只是发送和执行任务逻辑的入口和出口 ThreadLocal 实例是静态的,整个进程共用一个实例。每个 Looper 存放的 ThreadLocalMap 均弱引用它作为 key
十二、如何理解 ThreadLocal 的作用?
首先要明确并非不是用来切换线程的,只是为了让每个线程方便程获取自己的 Looper 实例,见 Looper#myLooper()后续可供 Handler 初始化时指定其所属的 Looper 线程也可用来线程判断自己是否是主线程
十三、ActivityManagerService是什么?什么时候初始化的?有什么作用?
ActivityManagerService 主要负责系统中四大组件的启动、切换、调度及应用进程的管理和调度等工作,其职责与操作系统中的进程管理和调度模块类似。ActivityManagerService进行初始化的时机很明确,就是在 SystemServer进程开启的时候,就会初始化ActivityManagerService。(系统启动流程)如果打开一个App的话,需要AMS去通知zygote进程,所有的Activity的生命周期AMS来控制
十四、ActivityThread是什么?ApplicationThread是什么?他们的区别
1、ActivityThread 在 Android中它就代表了 Android的主线程,它是创建完新进程之后,main函数被加载,然后执行一个loop的循环使当前线程进入消息循环,并且作为主线程。 2、ApplicationThread ApplicationThread是 ActivityThread的内部类,是一个Binder对象。在此处它是作为IApplicationThread对象的server端等待client端的请求然后进行处理,最大的client就是AMS。
十五、 Instrumentation是什么?和ActivityThread是什么关系?
AMS与 ActivityThread之间诸如Activity的创建、暂停等的交互工作实际上是由Instrumentation具体操作的。每个 Activity都持有一个Instrumentation对象的一个引用,整个进程中是只有一个Instrumentation。mInstrumentation的初始化在ActivityThread::handleBindApplication函数。可以用来独立地控制某个组件的生命周期。Activity的startActivity方法。 startActivity会调用mInstrumentation .execStartActivity(); mInstrumentation 掉用 AMS, AMS 通过 socket 通信告知 Zygote 进程 fork 子进程。
Flutter篇
Dart部分面试题
一、Dart 语言的特性?
- Productive(生产力高,Dart的语法清晰明了,工具简单但功能强大)
- Fast(执行速度快,Dart提供提前优化编译,以在移动设备和Web上获得可预测的高性能和快速启动。)
- Portable(易于移植,Dart可编译成ARM和X86代码,这样Dart移动应用程序可以在iOS、Android和其他地方运行)
- Approachable(容易上手,充分吸收了高级语言特性,如果你已经知道C++,C语言,或者Java,你可以在短短几天内用Dart来开发)
- Reactive(响应式编程)
二、Dart的一些重要概念?
在Dart中,一切都是对象,所有的对象都是继承自ObjectDart是强类型语言,但可以用var或 dynamic来声明一个变量,Dart会自动推断其数据类型,dynamic类似c#没有赋初值的变量都会有默认值null Dart支持顶层方法,如main方法,可以在方法内部创建方法 Dart支持顶层变量,也支持类变量或对象变量 Dart没有public protected private等关键字,如果某个变量以下划线(_)开头,代表这个变量在库中是私有的
三、Dart 当中的 「..」表示什么意思?
Dart 当中的「..」意思是「级联操作符」,为了方便配置而使用。「..」和「.」不同的是调用「..」后返回的相当于是 this,而「.」返回的则是该方法返回的值。
四、 Dart 的作用域
Dart 没有「public」「private」等关键字,默认就是公开的,私有变量使用下划线开头。
五、Dart 是不是单线程模型?是如何运行的?
Dart 是单线程模型,Dart 在单线程中是以消息循环机制来运行的,其中包含两个任务队列,一个是“微任务队列” microtask queue,另一个叫做“事件队列” event queue。入口函数 main() 执行完后,消息循环机制便启动了。首先会按照先进先出的顺序逐个执行微任务队列中的任务,当所有微任务队列执行完后便开始执行事件队列中的任务,事件任务执行完毕后再去执行微任务,如此循环往复,生生不息。
六、 Dart 多任务如何并行的?
刚才也说了,既然 Dart 不存在多线程,那如何进行多任务并行? Dart 当中提供了一个 类似于新线程,但是不共享内存的独立运行的 worker - isolate。 那他们是如何交互的? 在dart中,一个Isolate对象其实就是一个isolate执行环境的引用,一般来说我们都是通过当前的isolate去控制其他的isolate完成彼此之间的交互,而当我们想要创建一个新的Isolate可以使用Isolate.spawn方法获取返回的一个新的isolate对象,两个isolate之间使用SendPort相互发送消息,而isolate中也存在了一个与之对应的ReceivePort接受消息用来处理,但是我们需要注意的是,ReceivePort和SendPort在每个isolate都有一对,只有同一个isolate中的ReceivePort才能接受到当前类的SendPort发送的消息并且处理。
七、Widget和element和RenderObject之间的关系
首先我详细说下当时的情景,面试官问我Widget和Element之间是不是一对多的关系,如果是增加一个Widget之后,这个关系又是什么。 这部分还是没有很好地答案,现在只是一个猜想,如果添加了一个widget,Element树遍历后面所有的Element看类型是否发生改变,有的话再重建RenderObject。Element和Widget之间应该还是一对一的关系,因为每个Widget的context都是独一无二的。
Flutter部分面试题
一、Flutter 是什么?
Flutter是谷歌的移动UI框架,可以快速在iOS和Android上构建高质量的原生用户界面。 Flutter可以与现有的代码一起工作。在全世界,Flutter正在被越来越多的开发者和组织使用,并且Flutter是完全免费、开源的。
二、Flutter 特性有哪些?
- 快速开发(毫秒级热重载)
- 绚丽UI(内建漂亮的质感设计Material Design和Cupertino Widget和丰富平滑的动画效果和平台感知)
- 响应式(Reactive,用强大而灵活的API解决2D、动画、手势、效果等难题)
- 原生访问功能
- 堪比原生性能
三、Flutter 中的生命周期
initState()表示当前 State 将和一个 BuildContext 产生关联,但是此时BuildContext 没有完全装载完成,如果你需要在该方法中获取 BuildContext ,可以 new Future.delayed(const Duration(seconds: 0, (){//context}); 一下。
didChangeDependencies()在 initState()之后调用,当 State 对象的依赖关系发生变化时,该方法被调用,初始化时也会调用。deactivate()当 State 被暂时从视图树中移除时,会调用这个方法,同时页面切换时,也会调用。dispose() Widget 销毁了,在调用这个方法之前,总会先调用 deactivate()。didUpdateWidge 当 widget 状态发生变化时,会调用。
通过 StreamBuilder 和 FutureBuilder 我们可以快速使用 Stream 和 Future 快速构建我们的异步控件,Flutter 中 runApp 启动入口其实是一个 WidgetsFlutterBinding ,它主要是通过BindingBase 的子类 GestureBinding 、ServicesBinding 、SchedulerBinding 、PaintingBinding 、SemanticsBinding 、 RendererBinding 、WidgetsBinding 等,通过 mixins 的组合而成的。 Flutter 中的 Dart 的线程是以事件循环和消息队列的形式存在,包含两个任务队列,一个是 microtask 内部队列,一个是event 外部队列,而 microtask 的优先级又高于event。因为 microtask 的优先级又高于 event,同时会阻塞event 队列,所以如果microtask 太多就可能会对触摸、绘制等外部事件造成阻塞卡顿哦。
Flutter 中存在四大线程,分别为 UI Runner、GPU Runner、IO Runner, Platform Runner (原生主线程),同时在 Flutter 中可以通过 isolate 或者compute 执行真正的跨线程异步操作。
四、Android 启动页
Android 中 Flutter 默认启动时会在 FlutterActivityDelegate.java 中读取 AndroidManifset.xml内 meta-data 标签,其中 io.flutter.app.android.SplashScreenUntilFirstFrame 标志位如果为 ture ,就会启动 Splash 画面效果(类似IOS的启动页面)。启动时原生代码会读取 android.R.attr.windowBackground 得到指定的 Drawable , 用于显示启动闪屏效果,之后并且通过 flutterView.addFirstFrameListener,在onFirstFrame 中移除闪屏。
五、Flutter 和 Dart的关系是什么?
Flutter是一个使用Dart语言开发的跨平台移动UI框架,通过自建绘制引擎,能高性能、高保真地进行移动开发。 Dart囊括了多数编程语言的优点,它更符合Flutter构建界面的方式。
六、Widget 和 element 和 RenderObject 之间的关系?
- Widget是用户界面的一部分,并且是不可变的。
- Element是在树中特定位置Widget的实例。
- RenderObject是渲染树中的一个对象,它的层次结构是渲染库的核心。
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 不会被重建,只是更新而已。
七、mixin extends implement 之间的关系?
继承(关键字 extends)、混入 mixins (关键字 with)、接口实现(关键字 implements)。这三者可以同时存在,前后顺序是extends -> mixins -> implements。 Flutter中的继承是单继承,子类重写超类的方法要用@Override,子类调用超类的方法要用super。 在Flutter中,Mixins是一种在多个类层次结构中复用类代码的方法。mixins的对象是类,mixins绝不是继承,也不是接口,而是一种全新的特性,可以mixins多个类,mixins的使用需要满足一定条件
八、. 使用mixins的条件是什么?
因为mixins使用的条件,随着Dart版本一直在变,这里讲的是Dart2.1中使用mixins的条件:
- mixins类只能继承自object
- mixins类不能有构造函数
- 一个类可以mixins多个mixins类
- 可以mixins多个类,不破坏Flutter的单继承
九、Flutter main future mirotask 的执行顺序?
普通代码都是同步执行的,结束后会开始检查microtask中是否有任务,若有则执行,执行完继续检查microtask,直到microtask列队为空。最后会去执行event队列(future)
十、Stream 两种订阅模式?
Stream有两种订阅模式:单订阅(single) 和 多订阅(broadcast)。 单订阅就是只能有一个订阅者,而广播是可以有多个订阅者。这就有点类似于消息服务(Message Service)的处理模式。单订阅类似于点对点,在订阅者出现之前会持有数据,在订阅者出现之后就才转交给它。而广播类似于发布订阅模式,可以同时有多个订阅者,当有数据时就会传递给所有的订阅者,而不管当前是否已有订阅者存在。 Stream 默认处于单订阅模式,所以同一个 stream 上的 listen 和其它大多数方法只能调用一次,调用第二次就会报错。但 Stream 可以通过 transform() 方法(返回另一个 Stream)进行连续调用。通过Stream.asBroadcastStream() 可以将一个单订阅模式的 Stream 转换成一个多订阅模式的 Stream,isBroadcast 属性可以判断当前 Stream 所处的模式。
十一、Flutter中的Widget、State、Context 的核心概念?是为了解决什么问题?
-
Widget: 在Flutter中,几乎所有东西都是Widget。将一个Widget想象为一个可视化的组件(或与应用可视化方面交互的组件),当你需要构建与布局直接或间接相关的任何内容时,你正在使用Widget。 Widget树: Widget以树结构进行组织。包含其他Widget的widget被称为父Widget(或widget容器)。包含在父widget中的widget被称为子Widget。
-
Context: 仅仅是已创建的所有Widget树结构中的某个Widget的位置引用。简而言之,将context作为widget树的一部分,其中context所对应的widget被添加到此树中。一个context只从属于一个widget,它和widget 一样是链接在一起的,并且会形成一个context树。
-
State: 定义了StatefulWidget实例的行为,它包含了用于”交互/干预“Widget信息的行为和布局。应用于State的任何更改都会强制重建Widget。
这些状态的引入,主要是为了解决多个部件之间的交互和部件自身状态的维护
十二、Widget 唯一标识Key有那几种?
在flutter中,每个widget都是被唯一标识的。这个唯一标识在build或rendering阶段由框架定义。该标识对应于可选的Key参数,如果省略,Flutter将会自动生成一个。 在flutter中,主要有4种类型的Key:GlobalKey(确保生成的Key在整个应用中唯一,是很昂贵的,允许element在树周围移动或变更父节点而不会丢失状态)、LocalKey、UniqueKey、ObjectKey。
Kotlin篇
一、请简述一下什么是 Kotlin?它有哪些特性?
kotlin和java一样也是一门jvm语言最后的编译结果都是.class文件,并且可以通过kotlin的.class文件反编译回去java代码,并且封装了许多语法糖,其中我在项目中常用的特性有:
- 扩展,(使用非集成的方式 扩张一个类的方法和变量):比方说 px和dp之间的转换 之前可能需要写个Util现在,通过扩展Float的变量 最后调用的时候仅仅是 123.dp这样px转成dp了
- lamdba表达式,函数式编程. lamdba表达式并不是kotlin 的专利,java中也有,但是有限制, 像setOnClickListener一样, 接口方法只有一个的情况才能调用, 而在kotlin中对接口的lambda也是如此,有这样的限制,但是他更推荐你使用闭包的方式而不是实现匿名接口的方式去实现这样的功能,闭包对lambda没有接口这么多的限制,另外就是函数式编程 在java8中提供了streamApi对集合进行mapsortreduce等等操作,但是对androidapi有限制,为了兼容低版本,几乎不可能使用streamApi
- 判空语法 省略了许多ifxxx==null的写法 也避免了空指针异常 aaa?.toString ?: "空空如也" 当aaa为空的时候 它的值被"空空 如也"替代 aaa?.let{ it. bbb } 当aaa不为空时 执行括号内的方法
- 省略了findViewById ,使用kotlin 就可以直接用xml中定义的id 作为变量获取到这个控件,有了这个 butterknife就可以淘汰了,使用databinding也能做到,但是,非常遗憾,databinding的支持非常不好,每次修改视图,都不能及时生成,经常要rebulid才能生成.
- 默认参数 减少方法重载 fun funName(a :Int ,b:Int =123) 通过如上写法 实际在java中要定义两个写法 funName(a)和funName(a,b)
二、Kotlin内置标准函数let的原理是什么?
使用端的感受:
1.在使用的时候,任何的类型,都可以.let出来使用,这是为什么呢? 因为标准let内置函数内部对泛型进行了let函数扩展,意味着所有的类型都等于泛型,所以任何地方都是可以使用let函数的。
2.所有类型.let {} 其实是一个匿名的Lambda表达式,Lambda表达式的特点是,最后一行会自动被认为是返回值类型,所以在表达式返回Boolean,那么当前的let函数就是Boolean类型,以此类推。
fun main() {
val r1 = "Derry".let {
true
it.length
}
println(r1)
val r2 = 123.let {
999
"【${it}】"
}
println(r2)
}
根据上面分析的两点使用感受,来分析他的原理:
1.inline : 是因为函数有lambda表达式,属于高阶函数,高阶函数规范来说要加inline
2.<T, R> T.let : T代表是要为T而扩展出一个函数名let(任何类型都可以 万能类型.let), R代表是Lambda表达式最后一行返回的类型
3.block: (T) -> R : Lambda表达式名称block 输入参数是T本身 输出参数是R 也就是表达式最后一行返回推断的类型
4.: R { : R代表是Lambda表达式最后一行返回的类型,若表达式返回类型是Boolean, 那么这整个let函数的返回类型就是Boolean
// inline : 是因为函数有lambda表达式,属于高阶函数,高阶函数规范来说要加inline
// <T, R> T.let : T代表是要为T而扩展出一个函数名let(任何类型都可以 万能类型.let), R代表是Lambda表达式最后一行返回的类型
// block: (T) -> R : Lambda表达式名称block 输入参数是T本身 输出参数是R 也就是表达式最后一行返回推断的类型
// : R { : R代表是Lambda表达式最后一行返回的类型,若表达式返回类型是Boolean, 那么这整个let函数的返回类型就是Boolean
inline fun <T, R> T.let(block: (T) -> R): R {
println("你${this}.let在${System.currentTimeMillis()}这个时间点调用了我")
/*contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}*/
// 调用Lambda表达式
// 输入参数this == T == "Derry" / 123,
// 输出参数:用户返回String类型,就全部是返回String类型
return block(this)
}
总结:Kotlin内置标准let函数,运用了 高阶函数特性与Lambda,控制环节交给用户完成,用户在自己的Lambda表达式中,若返回Boolean,整个let函数 与 Lambda返回 都全部是Boolean
为了保证所有的类型都能正常使用let,给泛型增加了扩展函数let,所以所有的地方都可以使用let函数。
三、Kotlin语言的run高阶函数的原理是什么?
run在Kotlin语法中使用端的感受:
1.在使用的时候,任何的类型,都可以.run出来使用,这是为什么呢? 因为标准run内置函数内部对泛型进行run函数扩展,意味着所有的类型都等于泛型,所以任何地方都是可以使用run函数的。
2.所有类型.run{} 其实是一个匿名的Lambda表达式,Lambda表达式的特点是,最后一行会自动被认为是返回值类型,例如在表达式返回Boolean,那么当前的run函数就是Boolean类型,例如在表达式返回Int类型,那么当前的run函数就是Int类型,以此类推。
fun main() {
val r1 : Int = "Derry".run {
true
length
}
println(r1)
val r2 : String = 123.run {
999
"【${it}】"
}
println(r2)
}
根据上面分析的两点使用感受,来分析他的原理:
1.inline : 是因为函数有lambda表达式,属于高阶函数,高阶函数规范来说要加inline
2.<T, R> T.run : T代表是要为T而扩展出一个函数名run(任何类型都可以 万能类型.run), R代表是Lambda表达式最后一行返回的类型
3.block: T.() -> R : Lambda表达式名称block 输入参数是T本身 输出参数是R 也就是表达式最后一行返回推断的类型
4.: R { : R代表是Lambda表达式最后一行返回的类型,若表达式返回类型是Boolean, 那么这整个run函数的返回类型就是Boolean
5.T.() 是让lambda表达式里面持有了this(run函数), (T) 是让lambda表达式里面持有了it(let函数)
/*
1.inline : 是因为函数有lambda表达式,属于高阶函数,高阶函数规范来说要加inline
2.<T, R> T.run : T代表是要为T而扩展出一个函数名run(任何类型都可以 万能类型.run), R代表是Lambda表达式最后一行返回的类型
3.block: T.() -> R : Lambda表达式名称block 输入参数是T本身 输出参数是R 也就是表达式最后一行返回推断的类型
4.: R { : R代表是Lambda表达式最后一行返回的类型,若表达式返回类型是Boolean, 那么这整个run函数的返回类型就是Boolean
5.T.() 是让lambda表达式里面持有了this(run函数), (T) 是让lambda表达式里面持有了it(let函数)
*/
public inline fun <T, R> T.run(block: T.() -> R): R {
println("你${this}.run在${System.currentTimeMillis()}这个时间点调用了我")
/*contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}*/
// 调用Lambda表达式
// 输入参数this == T == "Derry" / 123,
// 输出参数:用户返回String类型,就全部是返回String类型
return block()
}
总结:Kotlin内置标准run函数,运用了 高阶函数特性与Lambda,控制环节交给用户完成,用户在自己的Lambda表达式中,若返回Boolean,整个run函数 与 Lambda返回 都全部是Boolean
为了保证所有的类型都能正常使用run,给泛型增加了扩展函数run,所以所有的地方都可以使用run函数。
四、Kotlin语言泛型的形变是什么?
形变一共分为三个区域:不变,协变,逆变
不变
不变指的是:这个泛型,可以是生产者,也可以是消费者,此泛型没有任何泛型继承相关的概念,可以理解是完全独立出来的泛型
例如:下面案例中,此泛型既可以是生产者,也可以是消费者
// 不变
class StudentSetGets<IO> {
private var item : IO? = null
// 消费者
fun set(value : IO) {
println("你传递进来的内容是:$value")
item = value
}
// 生产者
fun get() = item
}
协变
协变指的是,这个泛型,只能是生产者,此泛型有泛型继承相关的概念存在,可以理解此泛型,可以接收此泛型类型的子类型
例如:下面案例中,此泛型只能是生产者,说白了,只能给用户端,读取泛型,却不能修改泛型
class MyStudentGet<out T>(_item : T) {
private val item = _item
fun get() : T = item
}
逆变
逆变指的是,这个泛型,只能是消费者,此泛型有泛型父类转子类的强转相关的概念存在,可理解此泛型,可以接收此泛型类型的父类型
例如:下面案例中,此泛型只能是消费者,说白了,只能给用户端,修改泛型,却不能读取泛型
class MyStudentSet<in T>() {
fun set(value: T) = println("你传递进来的内容是:$value")
}
结论
为什么协变只能读取泛型,不能修改泛型?
答:因为 例如 = 泛型接收端是Object,而泛型具体端是String,由于具体端有很多很多Object的子类,
而泛型会被泛型擦除,所以无法明确你到底要修改那个子类啊
为什么逆变只能修改泛型,不能读取泛型?
答:因为 例如 = 泛型接收端是String,而泛型具体端是Object,由于接收端是String,而读取时,
会读取到String的父类,但是接收端是String,你却读取到String的父类,这个本来就是不合理的
五、Kotlin协程在工作中有用过吗?
虽然对于一些人来说比如刚开始的我,协程(Coroutines) 是一个新的概念,但是协程这个术语早在1958年就被提出并用于构建汇编程序,协程是一种编程思想,并不局限于特定的语言,就像Rx也是一种思想,并不局限于使用Java实现的RxJava。不同语言实现的协程库可能名称或者使用上有所不同,但它们的设计思想是有相似之处的。
kotlinx.coroutines是由JetBrains开发的kotlin协程库,可以把它简单的理解为一个线程框架 。但是协程不是线程,也不是新版本的线程,它是基于线程封装的一套更上层工具库,我们可以使用kotlin协程库提供的api方便的灵活的指定协程中代码执行的线程、切换线程,但是不需要接触线程Thread类。说到这里,大家可能就会想到Android的AsyncTask或者RxJava的Schedulers,没错,从某种意义上来说它们和协程是相通的,都解决了异步线程切换的问题,然而协程最重要的是通过非阻塞挂起和恢复实现了异步代码的同步编写方式,把原本运行在不同线程的代码写在一个代码块{}里,看起来就像是同步代码。
// 注意:在真实开发过程中,MainScope作用域用的非常常用
MainScope().launch(){ // 注意:此协程块默认是在UI线程中启动协程
// 下面的代码看起来会以同步的方式一行行执行(异步代码同步获取结果)
val token = apiService.getToken() // 网络请求:IO线程,获取用户token
val user = apiService.getUser(token)// 网络请求:IO线程,获取用户信息
nameTv.text = user.name // 更新 UI:主线程,展示用户名
val articleList = apiService.getArticleList(user.id)// 网络请求:IO线程,根据用户id获取用户的文章集合哦
articleTv.text = "用户${user.name}的文章页数是:${articleList.size}页" // 更新 UI:主线程
}
协程并不是从操作系统层面创立的新的运行方式,代码是运行在线程中的,线程又是运行在进程中的,协程也是运行在线程中的,所以才说它是基于线程封装的库。然而有人会拿协程与线程比较,问协程是不是比线程效率更高?如果理解了协程是基于线程封装就应该知道,协程并没有改变代码运行在线程中的原则,单线程中的协程执行时间并不会比不用协程少,它们之间没有可比性,因为它们根本不属于同一类事物;协程也不是为了线程而生的,它是为了解决因为多线程带来的编码上的不便的问题而出现的。
2.那这样说的话,协程到底有什么用?
在Android开发中,通常会将耗时操作放到子线程中,然后通过回调的方式将结果返回后切换主线程更新UI,但是实际开发过程中可能遇到很多奇怪而合理的需求,它们可能是:
一个页面需要同时并发请求多个接口,当所有接口都请求完成需要做一些合并处理然后更新UI 按照惯例,我们可能会为每个接口请求设置一个boolean标志,每当一个接口请求完将对应的boolean值改为true,当最后一个接口请求完成发现所有标志都为true再更新UI,这样就能达到并发请求的目的,然而管理这么多boolean值累不累?优雅不优雅? 初级程序员可能干脆来个单线程,一个接口请求完成后,再请求另一个接口,直到最后一个接口返回数据,玩暴力美学啊,本来能同时干的事情非得一件件干,你让用户浪费他宝贵的时间合适吗?浪费用户高配的性能过瘾吗? 高级一点的可能就上RxJava了,通过RxJava的zip操作符,达到发射一次,将结果合并处理的目的,但是说实话到现在还有很多人不会用
RxJava 先调用接口1获取数据,然后拿到接口1的结果作为参数调用接口2,然后将接口2的数据展示出来 按照惯例,我们可能会调用接口1,然后在接口1的回调中获取数据再嵌套调用接口2 高级一点的可能就上RxJava了
会引发回调地狱问题:
/**RetrofitClient单例*/
object RetrofitClient {
/**log**/
private val logger = HttpLoggingInterceptor.Logger {
FLog.i(this::class.simpleName, it)
}
private val logInterceptor = HttpLoggingInterceptor(logger).apply {
level = HttpLoggingInterceptor.Level.BODY
}
/**OkhttpClient*/
private val okHttpClient = OkHttpClient.Builder()
.callTimeout(10, TimeUnit.SECONDS)
.addNetworkInterceptor(logInterceptor)
.build()
/**Retrofit*/
private val retrofit = Retrofit.Builder()
.client(okHttpClient)
.baseUrl(ApiService.BASE_URL)
.addCallAdapterFactory(RxJava3CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.build()
/**ApiService*/
val apiService: ApiService = retrofit.create(ApiService::class.java)
}
/**接口定义*/
interface ApiService {
companion object {
const val BASE_URL = "https://www.wanandroid.com"
}
/*获取文章树结构*/
@GET("tree/json")
fun getTree(): Call<ApiResult<MutableList<Tree>>>
/*根据数结构下某个分支id,获取分支下的文章*/
@GET("article/list/{page}/json")
fun getArticleList(
@Path("page") page: Int,
@Query("cid") cid: Int
): Call<ApiResult<Pagination<Article>>>
}
/**ViewModel*/
class SystemViewModel : BaseViewModel(){
private val remoteRepository : SystemRemoteRepository by lazy { SystemRemoteRepository() }
val page = MutableLiveData<Pagination<Article>>()
fun getArticleList() {
remoteRepository.getArticleList(){
page.value = it
}
}
}
/**数据仓库*/
class SystemRemoteRepository{
/**
* 1. 展示回调嵌套,回调地狱
*/
fun getArticleList(responseBack: (result: Pagination<Article>?) -> Unit) {
/**1. 获取文章树结构*/
val call:Call<ApiResult<MutableList<Tree>>> = RetrofitClient.apiService.getTree()
//同步(需要自己手动切换线程)
//val response : Response<ApiResult<MutableList<Tree>>> = call.execute()
//异步回调
call.enqueue(object : Callback<ApiResult<MutableList<Tree>>> {
override fun onFailure(call: Call<ApiResult<MutableList<Tree>>>, t: Throwable) {
}
override fun onResponse(call: Call<ApiResult<MutableList<Tree>>>, response: Response<ApiResult<MutableList<Tree>>>) {
FLog.v("请求文章树结构成功:"+response.body())
/**2. 获取分支id下的第一页文章*/
val treeid = response.body()?.data?.get(0)?.id
//当treeid不为null执行
treeid?.let {
RetrofitClient.apiService.getArticleList(0, treeid)
.enqueue(object : Callback<ApiResult<Pagination<Article>>> {
override fun onFailure(call: Call<ApiResult<Pagination<Article>>>, t: Throwable) {
}
override fun onResponse(call: Call<ApiResult<Pagination<Article>>>, response: Response<ApiResult<Pagination<Article>>>) {
//返回获取的文章列表
responseBack(response.body()?.data)
}
})
}
}
})
}
}
其实上面数据仓库中的方法是应该拆分为2个方法的,第二个接口请求拆分为方法后还可以复用(分页获取),这里仅仅为了展示嵌套回调的需求。可以看到仅仅是两层回调嵌套,可读性就已经很差了,然而这种需求并非不常见的甚至会出现更多层嵌套,这时候就会写出深>形的代码,非常不雅观而且不易于代码复用及后期维护。
有人会说将上面的嵌套回调拆分为2个方法,在第一个接口请求完成之后再调用另一个方法请求文章列表,这不就消除了嵌套回调了吗?从视觉上来说确实是消除了,但是从逻辑上来说嵌套依然存在,而且这种方式会让两个方法之间形成很强的业务关联,对代码维护带来的挑战不比嵌套回调小。代码就不展示了,就是简单的将两个请求拆分。
Rx解决回调地狱
使用Retrofit+RxJava组合通过Rx的链式调用就能消除嵌套回调:
interface ApiService {
...
/**RxJava方式*/
@GET("tree/json")
fun getTreeByRx(): Observable<ApiResult<MutableList<Tree>>>
@GET("article/list/{page}/json")
fun getArticleListByRx(
@Path("page") page: Int,
@Query("cid") cid: Int
): Observable<ApiResult<Pagination<Article>>>
}
class SystemRemoteRepository{
/**
* 2. Retrofit+RxJava消除回调嵌套
*/
fun getArticleListByRx(responseBack: (result: Pagination<Article>?) -> Unit) {
/**1. 获取文章树结构*/
val observable1: Observable<ApiResult<MutableList<Tree>>> = RetrofitClient.apiService.getTreeByRx()
observable1.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
//使用当前Observable发出的值调用给定的Consumer,然后将其转发给下游
.doOnNext({
FLog.v("1请求文章树成功,切换到主线程处理数据:${Thread.currentThread()}")
})
.observeOn(Schedulers.io())
.flatMap({
FLog.v("2请求文章树成功,IO线程中获取接口1的数据,然后将被观察者变换为接口2的Observable:${Thread.currentThread()}")
if(it?.errorCode == 0){
//当treeid不为null执行
it?.data?.get(0)?.id?.let { it1 -> RetrofitClient.apiService.getArticleListByRx(0, it1) }
}else{
//请求错误的情况,发射一个Error
Observable.error({
Throwable("获取文章树失败:${it.errorCode}:${it.errorMsg}")
})
}
})
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object: Observer<ApiResult<Pagination<Article>>> {
override fun onComplete() {}
override fun onSubscribe(d: Disposable?) {}
override fun onNext(t: ApiResult<Pagination<Article>>?) {
FLog.v("3请求文章列表成功:${t?.data}")
responseBack(t?.data)
}
override fun onError(e: Throwable?) {
FLog.e("3请求失败:${e?.message}")
}
})
}
}
Retrofit+RxJava确实消除了回调的嵌套,但是还是避免不了回调(Observer观察者可看作是回调),链式调用处理异步数据流确实比传统的嵌套回调好了太多,但是代码量不减反增,而且我们需要在正确的位置准确的插入不同的操作符用来处理异步数据,对于不熟悉Rx的同学来说也是很头痛的,所以还不是很好,下面将出现协程来解决
协程来解决此问题:
使用协程就可以让我们摆脱因为多线程带来的各种编码上的不便:
class SystemViewModel : BaseViewModel(){
private val remoteRepository : SystemRemoteRepository by lazy { SystemRemoteRepository() }
val page = MutableLiveData<Pagination<Article>>()
fun getArticleList() {
viewModelScope.launch { //主线程开启一个协程
// 网络请求:IO线程
val tree : ApiResult<MutableList<Tree>> = RetrofitClient.apiService.getTreeByCoroutines()
// 主线程
val cid = tree?.data?.get(0)?.id
if(cid!=null){
// 网络请求:IO线程
val pageResult : ApiResult<Pagination<Article>> = RetrofitClient.apiService.getArticleListByCoroutines(0, cid)
// 主线程
page.value = pageResult.data!!
}
}
}
}
/**接口定义,Retrofit从2.6.0版本开始支持协程*/
interface ApiService {
/*获取文章树结构*/
@GET("tree/json")
suspend fun getTreeByCoroutines(): ApiResult<MutableList<Tree>>
/*根据数结构下某个分支id,获取分支下的文章*/
@GET("article/list/{page}/json")
suspend fun getArticleListByCoroutines(
@Path("page") page: Int,
@Query("cid") cid: Int
): ApiResult<Pagination<Article>>
}
运行上面的代码,尽然成功了,刚刚发生了什么?这不就是同步调用吗?跟上面的同步有什么不一样吗?看起来差不多啊,确实差不多,就是在定义接口时,方法前加了个suspend关键字,调用接口的时候用viewModelScope.launch {}包裹。既然可以运行成功,就说明请求接口并不是在主线程中进行的,然而有的同学不信,他在getArticleList()方法中的任意位置通过Thread.currentThread()打印的结果都是Thread[main,5,main],这不就是在主线程中调用的吗?上述协程中的代码是在主线程执行没错,但是接口请求的方法却是在子线程中执行的。
原因就在于我们定义接口的时候使用了suspend关键字,它的意思是挂起、暂停,函数被加了这个标记就称它为挂起函数,需要注意的是,suspend关键字并没有其他重要的作用,它仅仅标识某个函数是挂起函数,可以在函数中调用其他挂起函数,但是只能在协程中调用它。所以上面两个接口都被定义为挂起函数了,挂起函数只能在协程中调用,那谁是协程?
其实在kotlin协程库中是有一个类AbstractCoroutine来表示协程的,这个抽象类有很多子类代表不同的协程,但是这些子类都是private的,并没有暴露给我们,所以你在其他文章中看到别人说viewModelScope.launch{}包裹起来的闭包(代码块)就是协程也没问题,但这个代码块的真正意义是协程需要执行的代码。当在协程中调用到挂起函数时,协程就会在当前线程(主线程)中被挂起,这就是协程中著名的非阻塞式挂起,主线程暂时停止执行这个协程中剩余的代码,注意:暂停并不是阻塞等待(否则会ANR),而是主线程暂时从这个协程中被释放出来去处理其他Handler消息,比如响应用户操作、绘制View等等。
那挂起函数谁执行?这得看挂起函数内部是否有切换线程,如果没有切换线程当然就是主线程执行了,所以挂起函数不一定就是在子线程中执行的,但是通常在定义挂起函数时都会为它指定其他线程,这样挂起才有意义。比如上面定义的suspend的请求接口,Retorift在执行请求的时候就切换到了子线程并挂起主线程,当请求完成(挂起函数执行完毕)返回结果时,会通知主线程:我该干的都干完了,下面的事你接着干吧,主线程接到通知后就会拿到挂起函数返回的结果继续执行协程里面剩余的代码,这叫做协程恢复(resume)。如果又遇到挂起函数就会重复这个过程,直到协程中的代码被执行完。
通过协程可以彻底去除回调,使用同步的方式编写异步代码。什么是同步调用?调用一个方法能直接拿到方法的返回值,尽管这个方法是耗时的、在其他线程执行的,也能直接得到它的返回值,然后再执行下面的代码,协程不是通过等待的方式实现同步,而是通过非阻塞挂起实现看起来同步的效果。
总结:
协程的目的是,简化复杂的异步代码逻辑,用同步的代码写出复杂的异步代码逻辑。
最后
面试题的目的不是为了让大家背题,而是从不同维度帮助大家复习,取长补短。最主要的还是增长自身实力,自己实力上去了,不管面试在怎么问,也还是难不住你。
详细关注公众号:Android老皮 ,会持续更新最新的中大厂面试题以及进阶学习笔记、Android最新动态