flutter 面试题
https://juejin.cn/post/6844904199726039054

重点掌握

JVM 原理

JVM底层细节实现
JVM 运行时:
线程独享:

  • 虚拟机栈
    • 虚拟机栈中主要包括栈帧,而栈帧包括:局部变量、操作数栈、动态链接、完成出口

在实际代码中,一个线程是可以运行多个方法的。如下代码:
main -> A -> B -> C, 运行代码,线程1来运行,就会有一个对应的虚拟机栈,同时在执行每个方法的时候都会打包成一个栈帧。

  • 本地方法栈 : native的关键字的方法是在C/C++中实现的
  • 程序计数器:指向当前线程正在执行的字节码的指令地址,程序计数器是唯一不会发生OOM的内存溢出。

线程共享:

  • 方法区

GC:主要是可达性分析策略,如果一个对象它可达到GC Root那么就不能被回收,如果一个对象没有GCRoot 那么下一次执行GC的时候就会被回收掉

ClassLoader

程序在启动的时候,并不会一次性加载程序所要用的所有class文件,而是根据程序的需要,通过Java的类加载机制(ClassLoader)来动态加载某个class文件到内存当中的,从而只有class文件被载入到了内存之后,才能被其它class所引用。所以ClassLoader就是用来动态加载class文件到内存当中用的
当一个ClassLoader 实例需要加载某个类时,它会试图在亲自搜索这个类之前先把这个任务委托给它的父类加载器,这个过程是由上而下依次检查的,首先由顶层的类加载器Bootstrap CLassLoader进行加载,如果没有加载到,则把任务转交给Extension CLassLoader视图加载,如果也没有找到,则转交给AppCLassLoader进行加载,还是没有的话,则交给委托的发起者,由它到指定的文件系统或者网络等URL中进行加载类。还没有找到的话,则会抛出CLassNotFoundException异常。否则将这个类生成一个类的定义,并将它加载到内存中,最后返回这个类在内存中的Class实例对象。

类加载机制常用的场景是在组件化,比如ARouter框架,编译项目的过程中,它会通过APT技术,扫描所有有 ARouter规定的注解类,然后它会将这些信息组和保存到一个class文件到build的某个目录中,当ARouter 初始化的时候会从build目录中获取class文件,通过ClassLoader加载class文件到内存中,最后就会得到class实例对象,然后将对象中的信息保存到ARouter中的Map集合中,当进行组件通信的时候,它就会从集合中获取某个key的value,而这个value就是Activity或fragment得到全类名,得到这个全类名就可以进行跳转转。

为什么使用双亲委托模型

JVM在判断两个class是否相同时,不仅要判断两个类名是否相同,还要判断是否是同一个类加载器加载的。
避免重复加载,父类已经加载了,则子CLassLoader没有必要再次加载。 考虑安全因素,假设自定义一个String类,除非改变JDK中CLassLoader的搜索类的默认算法,否则用户自定义的CLassLoader如法加载一个自己写的String类,因为String类在启动时就被引导类加载器Bootstrap CLassLoader加载了。

在Android开发中,不管是插件化还是组件化,都是基于Android系统的类加载器ClassLoader来设计的。只不过Android平台上虚拟机运行的是Dex字节码,一种对class文件优化的产物,传统Class文件是一个Java源码文件会生成一个.class文件,而Android是把所有Class文件进行合并、优化,然后再生成一个最终的class.dex,目的是把不同class文件重复的东西只需保留一份,在早期的Android应用开发中,如果不对Android应用进行分dex处理,那么最后一个应用的apk只会有一个dex文件。
Android中常用的类加载器有两种,DexClassLoader和PathClassLoader,它们都继承于BaseDexClassLoader。区别在于调用父类构造器时,DexClassLoader多传了一个optimizedDirectory参数,这个目录必须是内部存储路径,用来缓存系统创建的Dex文件。而PathClassLoader该参数为null,只能加载内部存储目录的Dex文件。所以我们可以用DexClassLoader去加载外部的apk文件,这也是很多插件化技术的基础。

热修复

View的加载流程

Android的Activity、PhoneWindow和DecorView的关系
DecorView是顶级View,本质是一个FrameLayout它包含两部分,标题栏和内容栏,都是FrameLayout。内容栏id是content,也就是activity中设置setContentView的部分,最终将布局添加到id为content的FrameLayout中。 获取content:ViewGroup content=findViewById(android.id.content) 获取设置的View:getChildAt(0).

每个Activity都包含一个Window对象,Window对象通常是由PhoneWindow实现的。 PhoneWindow:将DecorView设置为整个应用窗口的根View,是Window的实现类。它是Android中的最基本的窗口系统,每个Activity均会创建一个PhoneWindow对象,是Activity和整个View系统交互的接口。 DecorView:是顶层视图,将要显示的具体内容呈现在PhoneWindow上,DecorView是当前Activity所有View的祖先,它并不会向用户呈现任何东西。

View的事件分发机制

当一个点击事件产生后,它的传递过程将遵循如下顺序:
Activity -> Window -> View
事件总是会传递给Activity,之后Activity再传递给Window,最后Window再传递给顶级的View,顶级的View在接收到事件后就会按照事件分发机制去分发事件。如果一个View的onTouchEvent返回了FALSE,那么它的父容器的onTouchEvent将会被调用,依次类推,如果所有都不处理这个事件的话,那么Activity将会处理这个事件。
对于ViewGroup的事件分发过程,大概是这样的:如果顶级的ViewGroup拦截事件即onInterceptTouchEvent返回true的话,则事件会交给ViewGroup处理,如果ViewGroup的onTouchListener被设置的话,则onTouch将会被调用,否则的话onTouchEvent将会被调用,也就是说:两者都设置的话,onTouch将会屏蔽掉onTouchEvent,在onTouchEvent中,如果设置了onClickerListener的话,那么onClick将会被调用。如果顶级ViewGroup不拦截的话,那么事件将会被传递给它所在的点击事件的子view,这时候子view的dispatchTouchEvent将会被调用

View 的事件分发:
dispatchTouchEvent -> onTouch(setOnTouchListener) -> onTouchEvent -> onClick
onTouch和onTouchEvent的区别 两者都是在dispatchTouchEvent中调用的,onTouch优先于onTouchEvent,如果onTouch返回true,那么onTouchEvent则不执行,及onClick也不执行。

Handler 原理

彻底搞定 Handler 源码

Binder 原理

  • 进程间通信机制
  • Binder也是一个驱动设备
  • Binder.java -> 实现了IBinder接口 跨进程能力

Linux的进程间通信有:管道、信号量、socket、共享内存等等,Android 专门增加的Binder进行进程间通信

  1. 内存被操作系统划分为:用户空间和内核空间,用户空间时用户程序代码运行的地方,内核空间时内核
  2. 内核空间是共享的,用户空间是隔离的。
  3. 假设发送进程有一个文件1.txt,要把这个文件拷贝到接收进程,在传统的IPC要如何做呢?
  4. 1. 先把1.txt拷贝到内核空间
  5. 2. 然后在从内核空间拷贝到接收进程
  6. 3. 其实就是和发快递送快递是一样的逻辑,小A给小B发送快递,快递员(内核空间) 拿到快递,进行了一次拷贝
  7. ,然后快递员送快递,到小B,小B拿到快递又进行了一次拷贝。
  8. Binder中接收进程和内核进程是共享物理内存的。同样是发送快递,小A发送快递,快递员
  9. (内核空间)上门进行了一次拷贝,然后把快递放到快递柜中(共享内存),通知小B你快递到了
  10. ,小B直接从快递柜(共享内存)拿快递即可。整个过程只用了一次拷贝。
  11. 那么内存共享是如何共享的呢?通过MMAP的一个技术。
  12. Linux 通过将一个虚拟内存区域与一个磁盘上的对象关联起来,以初始化这个虚拟内存区域的内容,这个过程称为内存映射(memory mapping).
  13. 对文件进行mmap,会在进程的虚拟内存分配地址空间,创建映射关系。实现这样的映射关系后,就可以采用指针的方式读写操作这一段内存,
  14. 而系统会自动回写到对应的文件磁盘上。
  15. 虚拟内存可以理解为—>**地球仪**
  16. 物理内存可以理解为—> **地球**
  17. 虚拟内存和物理内存是存在映射关系的。物理内存是真正存储东西的,虚拟内存不能存储。
  18. 用户空间能够直接操作文件吗?是不可以的,因为所有的系统资源管理都是在内核空间完成的。
  19. 比如读写磁盘文件、分配回收内存、从网络接口读写数据等等。用户空间通过系统调用让内核空间完成这些功能。
  20. MMAP就是将`user buffer` `disk`关联起来,你操作user buffer 就是操作`disk`而不用再去通过内核进程拷贝了。
  21. > 记住:MMAP能够让虚拟内存和指定物理内存直接联系起来
  22. 磁盘、内存条都是物理内存。 那么MMAP是如何实现的?会在以后讲解
  23. > 复习:Binder的数据传输原理:用户内核发送方进行,通过 `copy_form_user`,进行一次拷贝到内核空间,
  24. 、因为内核和接收方有一块物理内存共享区域,所以就不用再拷贝了。这就是Binder的数据传输原理。

image.png

ASM原理

用户从Launcher程序点击应用图标可启动应用的入口Activity,Activity启动时需要多个进程之间的交互,Android系统中有一个zygote进程专用于孵化Android框架层和应用层程序的进程。还有一个system_server进程,该进程里运行了很多binder service。例如ActivityManagerService,PackageManagerService,WindowManagerService,这些binder service分别运行在不同的线程中,其中ActivityManagerService负责管理Activity栈,应用进程,task。

点击Launcher图标来启动Activity

用户在Launcher程序里点击应用图标时,会通知ActivityManagerService启动应用的入口Activity,ActivityManagerService发现这个应用还未启动,则会通知Zygote进程孵化出应用进程,然后在这个dalvik应用进程里执行ActivityThread的main方法。应用进程接下来通知ActivityManagerService应用进程已启动,ActivityManagerService保存应用进程的一个代理对象,这样ActivityManagerService可以通过这个代理对象控制应用进程,然后ActivityManagerService通知应用进程创建入口Activity的实例,并执行它的生命周期方法。

okhttp及retrofit的原理

  • OkhttpClient 实现了Call.Fctory,负责为Request 创建 Call;
  • RealCall 为Call的具体实现,其enqueue() 异步请求接口通过Dispatcher()调度器利用ExcutorService实现,而最终进行网络请求时和同步的execute()接口一致,都是通过 getResponseWithInterceptorChain() 函数实现
  • getResponseWithInterceptorChain() 中利用 Interceptor 链条,责任链模式 分层实现缓存、透明压缩、网络 IO 等功能;最终将响应数据返回给用户。

glide架构


组件化实现


常问的基础面试

面向对象思想

OOP: 继承 封装 多态 ,简单来说就是接口和抽象类,接口可以看做是一个特殊的抽象类。
抽象类和接口是不完整的类。
抽象类既可以抽象方法也可以定义非抽象方法
接口只可以定义方法名
抽象类可以定义普通的变量
接口只能定义public变量
什么时候该用抽象类,什么时候该用接口?
例如BaseActivity基类就是抽象类的使用,主要表现在继承上面,默认的一些行为不需要重复写。很大部分常用的基类就会作为抽象类。
只有需要某个功能才会实现接口,例如某个Activity要用到定位的功能,如果把定位写到基类上就不好了。
面向接口编程,不只是接口,还包括抽象类。

Android与java序列化原理

  1. Java: Serializable Android:Parcelable 提供了两个接口
  2. Serializable与Parcelable的区别:
    1. serializable:JDK 中的一个空接口,这个接口就是用来做标记的,对类打上了一个标记,可以把这个对象写到一个流中写到文件中去。如何做到序列化的呢?假设一个User类,name age属性. 类的所有信息打包写到文件中去,这样读取文件才能逆向反序列化得到这个对象 。serializable有大量的IO和反射操作。
    2. Parcelable:Android提供的一个接口,这个接口就不是一个空的实现了,序列化的时候会调用writeToParcel,记录Parcel 只记录属性的值,作用在内存之间的序列化,不能用在持久化中,不能保存在文件中这是和serializable的区别。而且Parcelable写的顺序要和读的顺序一样。效率会比Serializable高,但是Parcelable实现较为繁琐
  3. 序列化的目的就是把一个对象传输到某个地方,然后得到传输的对象,序列化:把对象的所有字段和属性打标签记录。反序列化:按照一定的顺序把对象恢复过来。序列化是一个过程。
  4. GSON等框架是如何对json字符串序列化为对象?GSON也是序列化只不过GSON的规则变了,Serializable有自己的规则,Parcelable也有自己的规则,序列化的本质并没有变,都是记录标记信息,根据标记的信息进行反序列化。

什么是单例

  • 为什么要使用单例模式?
    • 创建对象和运行开销很大,包含某些重量级的对象,如果每次都创建对象,会非常消耗内存,这时候就需要用到单例
    • 构造方法不对外开放的,一般是private
    • 通过一个静态方法或者枚举返回单例类的对象
    • 注意多线程的场景
    • 注意单例类对象在反序列化时不会重新创建对象
  • 饿汉式:程序总是创建并使用单例实例或在创建和运行时开销不太,不管他会不会用到,立马创建
  • 懒汉式:用到的时候才会初始化需要某些外部资源,双重校验单例模式:线程安全的单例模式
  • 静态内部类
  • 枚举

什么是内存泄露,java如何处理?

内存泄露:一个长生命周期的对象持有一个短生命周期的对象引用,也就是说该回收的对象,因为引用问题没有被回收,最终产生OOM

Java本身不会帮助我们定位内存泄露,而是需要开发人员通过profiler工具定位的。如果问到了,他可能会要问题如何使用工具去定位内存泄露。

Java回收机制,如何减少OOM的概率

  1. Java回收机制:主要通过可达性分析算法,如果有对象没有达到GC Root就会被回收:
    1. 那么哪些可以作为GC Root的对象:

方法区:类静态属性的对象
方法区:常量的对象
虚拟机栈中的对象(方法执行完以后 就不再GC Roots所以GC会将他们回收掉)
本地方法栈JNI
image.png
image.png

  • 减少OOM的概率
    • 尽可能少的发生内存泄漏
    • 尽可能不在循环中申请内存
    • 尽可能不在调用次数很多的函数中申请内存