裸辞-疫情-闭关-复习-大厂offer(一)

32,568 阅读1小时+

引子

2022 年 3 月辞职,没多久上海爆发疫情,蜗居在家准备面试。在经历 1 个月的闭关和 40+ 场 Android 面试后,拿到一些 offer。

总体上说,有如下几种面试题型:

  1. 基础知识
  2. 算法题
  3. 项目经历
  4. 场景题

场景题,即“就业务场景给出解决方案”,考察运用知识解决问题的能力。这类题取决于临场应变、长期积累、运气。

项目经历题取决于对工作内容的总结提炼、拔高升华、运气

  1. 争取到什么样的资源
  2. 安排了怎么样的分工
  3. 搭建了什么样的架构
  4. 运用了什么模式
  5. 做了什么样的取舍
  6. 采用了什么策略
  7. 做了什么样的优化
  8. 解决了什么问题

力争把默默无闻的“拧螺丝”说成惊天动地的“造火箭”。(这是一门技术活)

但也不可避免地会发生“有些人觉得这是高大上的火箭,有些人觉得不过是矮小下的零件”。面试就好比相亲,甲之蜜糖乙之砒霜是常有的事。除非你优秀到解决了某个业界的难题。

算法题取决于刷题,运气,相较于前两类题,算法题可“突击”的成分就更多了。只要刷题足够多,胜算就足够大。大量刷,反复刷。

基础知识题是所有题型中最能“突击”的,它取决于对“考纲”的整理复习、归纳总结、背诵、运气。Android 的知识体系是庞杂的,对于有限的个人精力来说,考纲是无穷大的。

这不是一篇面经,把面试题公布是不讲武德的。但可以分享整个复习稿,它是我按照自己划定的考纲整理出的全部答案,由于篇幅太长,决定把全部内容分成两篇分享给大家,这一篇的内容是 Android 和 Java & Kotlin。(必有遗漏,欢迎补充~)

整个复习稿分为如下几大部分:

  1. Android
  2. Java & Kotlin
  3. 设计模式 & 架构
  4. 多线程
  5. 网络
  6. OkHttp & Retrofit
  7. Glide

Android

绘制流程

  1. 画多大?(测量measure)
  2. 画在哪?(定位layout)
  3. 画什么?(绘制draw)
  • 测量、定位、绘制都是从View树的根结点开始自顶向下进行地,即都是由父控件驱动子控件进行地。父控件的测量在子控件件测量之后,但父控件的定位和绘制都在子控件之前。
  • 父控件测量过程中ViewGroup.onMeasure(),会遍历所有子控件并驱动它们测量自己View.measure()。父控件还会将父控件的布局模式与子控件布局参数相结合形成一个MeasureSpec对象传递给子控件以指导其测量自己(3*3的表格,如果孩子是wrapcontent,根据measureChildWithMargin,则孩子的模式是AtMost)。View.setMeasuredDimension()是测量过程的终点,它表示View大小有了确定值。
  • 第一次加载onMeasure()至少调用两次,最多调用5次(都是有ViewRootImpl调用performMeasure()),会进行多次测量尝试,总是希望以更小的窗口大小进行绘制,如果不行则扩大
  • 通过 MEASURED_DIMENSION_SET 强制指定需要为 measuredDimension赋值,否则抛异常
  • 父控件在完成自己定位之后,会调用ViewGroup.onLayout()遍历所有子控件并驱动它们定位自己View.layout()。子控件总是相对于父控件左上角定位。View.setFrame()是定位过程的终点,它表示视图矩形区域以及相对于父控件的位置已经确定。
  • 控件按照绘制背景,绘制自身,绘制孩子的顺序进行。重写onDraw()定义绘制自身的逻辑,父控件在完成绘制自身之后,会调用ViewGroup.dispatchDraw()遍历所有子控件并驱动他们绘制自己View.draw()
  • 为什么只能在主线程绘制界面:因为重绘指令会有子视图一层层传递给父视图,最终传递到ViewRootImpl,它在每次触发View树遍历时都会调用ViewRootImpl.checkThread()

MeasureSpec

MeasureSpec用于在View测量过程中描述尺寸,它是一个包含了布局模式和布局尺寸的int值(32位),其中最高的2位代表布局模式,后30位代表布局尺寸。它包含三种布局模式分别是

  1. UNSPECIFIED:父亲没有规定你多大
  2. EXACTLY:父亲给你设定了一个死尺寸
  3. AT_MOST:父亲规定了你的最大尺寸

同步消息屏障

  • ViewRootImpl 将遍历view树包装成一个Runnable并抛到Choreographer, 在抛之前会向主线程消息队列中抛同步屏障
  • 同步屏障也是一个Message,只不过 target 等于null
  • 取下一条message的算法中,若遇到同步屏障,则会越过同步消息,向后遍历找第一条异步消息找到则返回(Choreographer抛的异步消息),若没有找到则会执行epoll挂起
  • 当执行到遍历View树的 runnable时,ViewRootImpl会移除同步屏障

Choreographer

  • 将和ui相关的任务与vsync同步的一个类。
  • 每个任务被抽象成CallbackRecord,同类任务按时间先后顺序组成一条任务链CallbackQueue。四条任务链存放在mCallbackQueues[]数组结构中
  • 触摸事件,动画,View树遍历都会被抛到编舞者,并被包装成CallbackRecord并存入链式数组结构,当Choreographer收到一个Vsync就会依次从输入,动画,绘制这些链中取出任务执行
  • 当vsync到来时,会向主线程抛异步消息(执行doFrame)并带上消息生成时间,当异步消息被执行时,从任务链上摘取所有以前的任务,并按时间先后顺序逐个执行。

关于 Choreographer 的详细分析可以点击读源码长知识 | Android卡顿真的是因为”掉帧“?

绘制模型

  • 1.软件绘制 2.硬件加速绘制
  • 软件绘制就是调用ViewRootImpl持有的 surface.lockCanvas(),获取和SurfaceFlinger共享的匿名内存,并往里面填充像素数据。这些操作发生在主线程。
  • 硬件加速和软件绘制的分歧点是在ViewRootImpl中,如果开启了硬件加速则调用mHardwareRenderer.draw,否则drawSoftware。
  • 硬件加速绘制分为两个步骤
    1. 在主线程构建DrawOp树:从根视图触发,自顶向下地为每一个视图构建DrawOp。(使用DisplayListCanvas缓存DrawOp,最终存储到RenderNode)
    2. 向RenderThread发一个DrawFrameTask,唤醒它进行渲染
      1. 进行DrawOp合并
      2. 调用gpu命令进行绘制,gpu向匿名共享内存写内容
      3. 最终将填充好的raw Buffer提交给SurfaceFlinger合成显示。
  • RenderThread 是单例,每个进程只有一个。
  • cpu 不擅长图形计算,硬件加速即是把这些计算交由 GPU 实现(图形计算转成 gpu指令),由此加入了RenderNode(相当于View),和 DisplayList(相当于view的绘制内容)。当重绘请求发起时,只更新 displyList

Android 应用冷启动流程

冷启动就是在 Launcher 进程中开启另一个引用 Activity 的过程。这是一个 Launcher 进程和 AMS,应用进程和 AMS,WMS 双向通信的过程:

  • Launcher 进程和 AMS 说:“我要启动Activity1”

  • AMS创建出 Activity1 对应的 ActivityRecord 以及 TaskRecord,通知 Launcher 进程执行 onPause()

  • Launcher 执行 onPause(),并告知 AMS

  • 启动一个 starting window,AMS 请求 zygote 进程 fork一个新进程

  • 在新进程中,构建ActivityThread,并调用main(),在其中开启主线程消息循环。

  • AMS 开始回调Activity1的各种生命周期方法。

  • 当执行到 Activity.onAttch()时,PhoneWindow 被构建。

  • 当执行到 Activity.onCreate()时,setContentView()会被委托给 PhoneWindow,并在其中构建DecorView,再根据主题解析系统预定义文件,作为 DecorView 的孩子,布局文件中肯定有一个 id 为 content 的容器控件,他将成为 setContentView 的父亲。

  • 当执行到 Activity.onResume()时,DecorView 先被设置为 invisible,然后将其添加到窗口,此过程中会构建 ViewRootImpl 对象,它是 app 进行和 WMS 双向通信的纽带。ViewRootImpl.requestLayout()会被调用,以触发View树自顶向下的绘制。

  • View 树遍历,会被包装成一个任务抛给 Choreographer。在此之前 ViewRootImpl 会向主线程消息队列抛一个同步消息屏障。以达到优先遍历异步消息的效果。

  • Choreographer 将任务暂存在链式数组结构中,然后注册监听下一个 vsync 信号。

  • 待下一个 vsync 信号到来之时,Choreographer 会从链上摘取所有比当前时间更早的任务,并将他们包装成一个异步消息抛到主线程执行。

  • 异步消息的执行,即是从顶层视图开始,自顶向下,逐个视图进行 measure,layout,draw的过程。

  • ViewRootImpl 持有一个 surface,它是原始图形缓冲区的一个句柄,原始图形缓冲区是一块存放像素数据的内存地址,这块内存地址由app进程和SurfaceFlinger共享。当 app进程执行完上述步骤时,就意味着像素数据已经填入该块内存,于是 app 通知 SurfaceFlinger 像素数据已经就绪,可以进行合成并渲染到屏幕了。

  • 当 DecorView 完成渲染后,就会被设置为 visible,界面展示出来。

Surface

  • 它是原始图像缓冲区的一个句柄。即raw buffer的内存地址,raw buffer是保存像素数据的内存区域,通过Surface的canvas 可以将图像数据写入这个缓冲区
  • Surface类是使用一种称为双缓冲的技术来渲染
  • 这种双缓冲技术需要两个图形缓冲区GraphicBuffer,其中一个称为前端缓冲区frontBuffer,另外一个称为后端缓冲区backBuffer。前端缓冲区是正在渲染的图形缓冲区,而后端缓冲区是接下来要渲染的图形缓冲区,当vsync到来时,交换前后缓冲区的指针
  • 部分刷新是通过前端缓冲区拷贝像素到后端缓冲区,并且合并脏区以缩小它。
  • 每个ViewRootImpl都持有一个Surface。

SurfaceFlinger

  • SurfaceFlinger 是由 init 进程启动的运行在底层的一个系统进程,它的主要职责是合成和渲染多个Surface,并向目标进程发送垂直同步信号 VSync,并在 vsync 产生时合成帧到frame buffer
  • SurfaceFlinger持有BufferQueue消费者指针,用于从BufferQueue中取出图形数据进行合成后送到显示器
  • View.draw()绘制的数据是如何流入SurfaceFlinger进行合成的?
  1. Surface.lockCanvas()从BufferQueue中取出图形缓冲区并锁定
  2. View.draw()将内容绘制到Canvas中的Bitmap,就是往图形缓冲区填充数据
  3. Surface.unlockCanvasAndPost()解锁缓冲区并将其入队BufferQueue,然后通知SurfaceFlinger进行合成,在下一个vsync到来时进行合成(app直接喝surfaceFlinger通信)
  • 应用进程通过Anonymous Shared Memory将数据传输给SurfaceFlinger,因为Binder通信数据量太小

Surfaceview

  • 一个嵌入View树的独立绘制表面,他位于宿主Window的下方,通过在宿主canvas上绘制透明区域来显示自己
  • 虽然它在界面上隶属于view hierarchy,但在WMS及SurfaceFlinger中都是和宿主窗口分离的,它拥有独立的绘制表面,绘制表面在app进程中表现为Surface对象,在系统进程中表现为在WMS中拥有独立的WindowState,在SurfaceFlinger中拥有独立的Layer,而普通view和其窗口拥有同一个绘制表面
  • 因为它拥有独立与宿主窗口的绘制表面,所以它独立于主线程的刷新机制。
  • 它的背景是属于宿主窗口的绘制表面,所以如果背景不透明则会盖住它的绘制内容

TextureView

  1. SurfaceView 拥有独立的绘制表面,而TextureView和View树共享绘制表面
  2. TextureView通过观察BufferQueue中新的SurfaceTexture到来,然后调用invalidate触发View树重绘,如果有View叠加在TextureView上面,它们的脏区有交集,则会触发不必要的重绘,所以他的刷新操作比SurfaceView更重
  3. TextureView 持有 SurfaceTexture,它是一个GPU纹理
  4. SurfaceView 有双缓冲机制,绘制更加流畅
  5. TextureView 在5.0之前在主线程绘制,5.0之后在RenderThread绘制。

界面卡顿

  • 刷新率是屏幕每秒钟刷新次数,即每秒钟去buffer中拿帧数据的次数, 帧率是GPU每秒准备帧的速度,即gpu每秒向buffer写数据的速度,
  • 卡顿是因为掉帧,掉帧是因为写buffer的速度慢于取buffer的速度。对于60HZ的屏幕,每隔16.6ms显示设备就会去buffer中取下一帧的内容,没有取到就掉帧了
  • 当扫描完一个屏幕后,设备需要重新回到第一行以进入下一次的循环,此时有一段时间空隙,称为VerticalBlanking Interval(VBI), Vsync就是 VBI 发生时产生的垂直脉冲,这是最好的交换双缓冲的时间点,交换双缓冲是交换内存地址,瞬间就完成了
  • 一帧的显示要经历如下步骤:cpu计算画多大,画在哪里,画什么,然后gpu渲染计算结果存到buffer,显示器每隔一段时间从buffer取帧。若没有取到帧,只能继续显示上一帧。
  • VSYNC=Vertical Synchronization垂直同步,它就是为了保证CPU、GPU生成帧的速度和display刷新的速度保持一致
  • VSYNC信号到来意味着,交换双缓冲的内存地址,font buffer 和 back buffer 互换,这个瞬间 back font就供下一帧使用, project butter(4.1)以后, Vsync一到就 GPU和cpu就开始渲染下一帧的数据
  • 双缓冲机制:用两个buffer存储帧内容,屏幕始终去font buffer取显示内容,GPU始终向back buffer存放准备好的下一帧,每个Vsync发生吃就是他们交换之时,若下一帧没有准备好,则就错过一次交换,发生掉帧
  • 三缓冲机制:为了防止耗时的绘制任务浪费一个vsync周期,用三个buffer存储帧。当前显示buffer a中内容,正在渲染的下一帧存放在buffer b中,当VSYNC触发时,buffer b中内容还没有渲染好,此时buffer a不能清除,因为下一帧需要继续显示buffer a,如果没有第三个buffer,cpu和gpu要白白等待到下一个VSYNC才有可能可以继续渲染后序帧
  • View的重绘请求最少要等待两个vsync 才能显示到屏幕上:重绘请求首先会包装成一个runnable,存放在Choreographer中,下一个Vsync到来之时,就是它执行之时,执行完毕,形成的帧数据会放在back buffer中,等到下一个Vsync到来时才会显示到屏幕上

requestLayout() vs invalidate()

  • 其实两个函数都会自底向上传递到顶层视图ViewRootImpl中
  • requestLayout()会添加两个标记位PFLAG_FORCE_LAYOUT,PFLAG_INVALIDATED,而invalidate()只会添加PFLAG_INVALIDATED(所以不会测量和布局)
  • invalidate(),会检查主线程消息队列中是否已经有遍历view树任务,通过ViewRootImpl.mWillDrawSoon是否为true,若有则不再抛
  • invalidate表示当前控件需要重绘,会标记PFLAG_INVALIDATED,重绘请求会逐级上传到根视图(但只有这个view会被重绘,因为其他的父类没有PFLAG_INVALIDATED,并且携带脏区域.初始脏区是发起view的上下左右,逐级向上传递后每次都会换到父亲的坐标系(平移 left,top)。
  • View.measure()和View.layout()会先检查是否有PFLAG_FORCE_LAYOUT标记,如果有则进行测量和定位
  • View.requestLayout()中设置了PFLAG_FORCE_LAYOUT标记,所以测量,布局,有可能触发onDraw是因为在在layout过程中发现上下左右和之前不一样,那就会触发一次invalidate,所以触发了onDraw。
  • postInvalidate 向主线程发送了一个INVALIDATE的消息

Binder

  • Linux内存空间 = 内核空间(操作系统+驱动)+用户空间(应用程序)。为了保证内核安全,它们是隔离的。内核空间可访问所有内存空间,而用户空间不能访问内核空间。

  • 用户程序只能通过系统调用陷入内核态,从而访问内核空间。系统调用主要通过 copy_to_user() 和 copy_from_user() 实现,copy_to_user() 用于将数据从内核空间拷贝到用户空间,copy_from_user() 将数据从用户空间拷贝到内核空间。

  • Android 将 Binder driver 挂载为动态内存(LKM:Loadable Kernel Module),通过它以 mmap 方式将内核空间与接收方用户空间进行内存映射(用户空间一块虚拟内存地址和内核空间虚拟内存地址指向同一块物理地址),这样就只需发送方将数据拷贝到内核就等价于拷贝到了接收方的用户空间。

  • Binder 通信优点:1. 安全性好:为发送方添加UID/PID身份信息。2. 性能更佳:传输过程只要一次数据拷贝,而Socket、管道等传统IPC手段都至少需要两次数据拷贝。

  • 通过Binder实现的跨进程通信是c/s模式的,客户端通过远程服务的本地代理像服务端发请求,服务端处理请求后将结果返回给客户端

  • binder通信的大小限制是1mb-8kb(Binder transaction buffer),这是mmap内存映射的大小限制(单个进程),其中异步传输oneway传输限制是同步的一半(1mb-8kb)/2,内核允许Binder传输的最大限制是4M(mmap的最大空间4mb)

  • 在生成的stub中有一个asInterface():它用于将服务端的IBinder对象转换成服务接口,这种转化是区分进程的,如果客户端和服务端处于同一个进程中,此方法返回的是服务端Stub对象本身,否则新建一个远程服务的本地代理

  • aidl 中的 oneway 表示异步调用,发起RPC之前不会构建parcel reply

  • aidl 中的 in 表示客户端向服务端传送值并不关心值的变化,out表示服务端向客户端返回值

进程通信方式(IPC)

  1. Messenger:不支持RPC(Remote Procedure Call) ,低并发串行通信,并发高可能等待时间长。
  • 它是除了aidl之外的创建Remote bound service的方法
  • 它可以实现客户端和服务器的双向串行通信(来信和回信)
  • 服务器和客户端本质上是通过拿到对方的Handler像对方发送消息。但Handler不能跨进程传递,所以在外面包了一层Messenger,它继承与IBinder。服务端和客户端分别将定义在自己进程中的Messenger传递给对方,通过Messenger相互发送消息,其实是Messenger中的Handler发送的,服务端将自己的Messenger通过onServiceConnected()返回,客户端通过将Messenger放在Message.replyTo字段发送给服务器
  1. AIDL:支持RPC 一对多并发通信
  • 存在多线程问题:跨进程通信是不同进程之间线程的通信,如果有多个客户端发起请求,则服务端binder线程池就会有多个线程响应。即服务端接口存在多线程并发安全问题。
  • RemoteCallbackList用于管理跨进程回调:其内部有一个map结果保存回调,键是IBinder对象,值是回调,服务端持有的接口必须是RemoteCallbackList类型的
  • 远程服务运行在binder线程池,客户端发出请求后被挂起,如果服务耗时长,客户端可能会产生anr,所以需要新启线程请求服务
  • AIDL 进程通信流程:
    1. 通过IInterface定义服务后会自动生成stub和proxy
    2. 服务器通过实现stub来实现服务逻辑并将其以IBinder形式返回给客户端
    3. 客户端拿到IBinder后生成一个本地代理对象(通过asInterface()),通过代理对象请求服务,代理会构建两个Parcel对象,一个data用来传递参数,另一个reply用来存储服务端处理的结果,并调用BinderProxy的transact()发起RPC(Remote Procedure Call),同时将当前进程挂起。所以如果远程调用很费时,不能在UI线程中请求服务
    4. 这个请求通过Binder驱动传递到远程的Stub.onTransact()调用了服务真正的实现
    5. 返回结果:将返回值写入reply parcel并返回
  1. 文件:通过读写同一个文件实现数据传递。(复杂对象需要序列化),并发读可能发生数据不是最新的情况,并发写可以破坏数据,适用于对同步要求低的场景
  2. ContentProvider:一对多进程的数据共享,支持增删改查
  3. Bundle:仅限于跨进程的四大组件间传递数据,且只能传递Bundle支持的数据类型

Bundle

  • 使用ArrayMap存储结构,省内存,查询速度稍慢,因为是二分查找,适用于小数据量
  • 使用Parcelable接口实现序列化,而hashmap使用serializable

Parcel

  • 将各种类型的数据或对象的引用进行序列化和反序列化,经过mmap直接写入内核空间。另一个进程可以直接读取这个内核空间(因为做了mmap,不需要另一次copy_to_user())

  • 使用复用池(是一个Parcel数组),获取Parcel对象

  • parcel 存取数据顺序需要保持一致,因为parcel在一块连续的内存地址,通过首地址+偏移量实现存取

持久化方式

  1. SharedPreference
    • 以xml文件形式存储在磁盘
    • 读写速度慢,需要解析xml文件
    • 文明存储,安全性差
    • 是线程安全的,但不是进程安全的
    • 调用写操作后会先写入内存中的map结构,调用commit或者apply才会执行写文件操作
    • 可能造成 anr:
    1. Activity或者service onstop的时候,sp会等待写任务结束,如果任务迟迟没有结束则会造成anr
    2. getSharedPreference会开启线程读文件,调用getString会调用wait()等待读文件结束,如果文件大,则会造成主线程阻塞。
    • sp一共有三个锁:写文件锁,读内存map的锁,写editor中map的锁
  2. SQLite
  3. 文件
  4. DataStore
    • 基于Flow,提供了挂起方法而不是阻塞方法。

    • 基于事物更新数据

    • 支持protocol buffer

    • 不支持部分刷新

Service

  • 分类
    1. started service

      • 生命周期和启动他的组件无关,必须显示调用stopservice()才能停止
    2. bound service

      • 生命周期和启动他的组件绑定,组件都销毁了 他也销毁
      • Local Bound Service :为自己应用程序的组件提供服务
      • Remote Bound Service:为其他应用的组件提供服务
        • 1.aidl
        • 2.Messenger
      • bound service如何和绑定组件生命周期联动:在绑定的时候会将serviceConnection保存在LoadedApk的ArrayMap结构中,当Activity finish的时候,会遍历这个结构逐个解绑
  • service默认是后台进程
  • Service和Activity通信
    1. 通过 Intent 传入startService
    2. 发送广播,或者本地广播
    3. bound service,通过方法调用通信

IntentService

  • 他是Service和消息机制的结合,它适用于后台串行处理一连串任务,任务执行完毕后会自销毁。
  • 它启动时会创建HandlerThread和Handler,并将HandlerThread的Looper和Handler绑定,每次调用startService()时,通过handler发送消息到新线程执行
  • 但是它没有将处理结果返回到主线程,需要自己实现(可以通过本地广播)

广播

  • 是一种用观察者模式实现的异步通信
  • 有静态注册和动态注册两种方式,静态注册生命周期比动态注册长(在应用安装后就处于监听状态)
    • 静态注册广播接收器时只要定义了intent-filter 则android:exported属性为true 表示该广播接收器是跨进程的
    • 静态注册广播容易被攻击:其他App可能会针对性的发出与当前App intent-filter相匹配的广播,由此导致当前App不断接收到广播并处理;
    • 静态注册广播容易被劫持:其他App可以注册与当前App一致的intent-filter用于接收广播,获取广播具体信息。
    • 通过manifest注册的广播是静态广播
    • 静态广播是常驻广播,常驻广播在应用退出后依然可以收到
    • 动态注册的不是常驻广播,它的生命周期痛注册组件一致
    • 动态注册要等到启动他的组件启动时时才注册
    • 动态广播优先级比静态高
  • 广播接收者BroadcastReceiver通过Binder机制向AMS进行注册,广播发送者通过binder机制向AMS发送广播,广播的Intent和Receiver会被包装在BroadcastRecord中,多个BroadcastRecord组成队列
  • onReceive()回调执行时间超过10s 会发生anr,如果有耗时操作需要使用IntentService处理,不建议新建线程
  • 分为有序广播和无序广播
    • 无序广播:所有广播接收器接受广播的先后顺序不确定,广播接收者无法阻止广播发送给其他接收者。
    • 有序广播:广播接收器收到广播的顺序按预先定义的优先级从高到低排列 接收完了如果没有丢弃,就下传给下一个次高优先级别的广播接收器进行处理,依次类推,直到最后
      • 自定义Intent并通过sendOrderBroadcast()发送
      • 可以通过在intent-filter中设置android:priority属性来设置receiver的优先级,优先级相同的receiver其执行顺序不确定,如果BroadcastReceiver是代码中注册的话,且其intent-filter拥有相同android:priority属性的话,先注册的将先收到广播
      • 使用setResult系列函数来结果传给下一个BroadcastReceiver
      • getResult系列函数来取得上个BroadcastReceiver返回的结果
      • abort系列函数来让系统丢弃该广播,使用该广播不再传送到别的BroadcastReceiver
  • 还可以分为本地广播和跨进程广播
    • 本地广播仅限于在应用内发送广播,向LocalBroadCastManager注册的接收器都存放在本地内存中,跨进程广播都注册到system_server进程

消息机制

  • Android 主线程存在一个无限循环,该循环在不停地从消息队列中取消息。消息是按时间先后顺序插入到链表结构的消息队列中,最旧的消息在队头,最新的消息在队尾。
  • Looper通过无限循环从消息队列中取消息并分发给其对应的Handler,并回收消息
  • Handler 是用于串行通信的工具类。
  • 消息池:链式结构,静态,所有消息共用,取消息时从头部取,消息分发完毕后头部插入
  • Android消息机制共有三种消息处理方式,它们是互斥的,优先级从高到低分别是1. Runnable.run() 2. Handler.callback 3. 重载Handler.handleMessage()
  • 若消息队列中消息分发完毕,则调用natviePollOnce()阻塞当前线程并释放cpu资源,当有新消息插入时或者超时时间到时线程被唤醒
  • idleHandler 是在消息队列空闲时会被执行的逻辑,每拿取一次消息有且仅有一次机会执行.通过queueIdle()返回true表示每次取消息时都会执行,否则执行一次就会被移出
  • 同步消息屏障是一种特殊的同步消息,他的target为null, 在 MessageQueue.next()中遇到该消息,则会遍历消息队列优先执行所有异步消息,若遍历到队列尾部还是没有异步消息,则阻塞会调用epoll,直到异步消息到来或者同步屏障被移出
  • 使用epoll实现消息队列的阻塞和唤醒,Message.next()是个无限循环,若当前无消息可处理会阻塞在nativePollOnce(),若有延迟消息,则设置超时,没有消息时主线程休眠不会占用cpu
  • epoll是一个IO事件通知机制 监听多个文件描述符上的事件 epoll 通过使用红黑树搜索被监控的文件描述符(内核维护的文件打开表的索引值)
  • epoll最终是在epoll_wait上阻塞
  • nativeWake() 唤醒是通过往管道中写入数据,epoll监听写入事件,epoll_wait()就返回了

触摸事件

  • 触摸事件的传递是从根视图自顶向下“递”的过程,触摸事件的消费是自下而上“归”的过程。
  • 触摸事件由ViewRootImpl通过ViewRootHandler接收到,然后存取一个链式队列,再逐个分发给Activity接收到触摸事件后,会传递给PhoneWindow,再传递给DecorView,由DecorView调用ViewGroup.dispatchTouchEvent()自顶向下分发ACTION_DOWN触摸事件。
  • ACTION_DOWN事件通过ViewGroup.dispatchTouchEvent()从DecorView经过若干个ViewGroup层层传递下去,最终到达View。View.dispatchTouchEvent()被调用。
  • View.dispatchTouchEvent()是传递事件的终点,消费事件的起点。它会调用onTouchEvent()或OnTouchListener.onTouch()来消费事件。
  • 每个层次都可以通过在onTouchEvent()或OnTouchListener.onTouch()返回true,来告诉自己的父控件触摸事件被消费。只有当下层控件不消费触摸事件时,其父控件才有机会自己消费。
  • ACTION_MOVE和ACTION_UP会沿着刚才ACTION_DOWN的传递路径,传递给消费了ACTION_DOWN的控件,如果该控件没有声明消费这些后序事件,则它们也像ACTION_DOWN一样会向上回溯让其父控件消费。
  • 父控件可以通过在onInterceptTouchEvent()返回true来拦截事件向其孩子传递。如果在孩子已经消费了ACTION_DOWN事情后才进行拦截,父控件会发送ACTION_CANCEL给孩子。

滑动冲突

父子都可以消费滑动事件时会发生滑动冲突:

  1. 父控件主动:父控件只拦截自己滑动方向上的事件(通过在onInterceptTouchEvent中返回true实现),其余事件不拦截继续传递给子控件
  2. 子控件主动:子控件要求父控件进行拦截或者不拦截(通过getParent().requestDisallowInterceptTouchEvent(true)实现)

ArrayMap & HashMap

  • 存储结构:arrayMap用一个数组存储key的哈希值,用另一个数组存储key和value(挨着i和i+1),而HashMap用一个Entry结构包裹key,value,所以HashMap更加占用空间。
  • 访问方式:arrayMap通过二分查找key数组,时间复杂度是o(log2n),HashMap通过散列定位方式,时间复杂度是o(n),
  • ArrayMap 删除键值对时候会进行数组平移以压缩数组
  • ArrayMap 插入键值对时可能发生数组整体平移以腾出插入位置

SparseArray & HashMap

  1. SparseArray用于存放键值对,键是int,值是Object。
  2. SparseArray用两个长度相等的数组分别存储键和值,同一个键值对所在两个数组中的索引相等。
  3. SparseArray比HashMap访问速度更慢,因为二分查找速度慢于散列定位。
  4. SparseArray比HashMap更节省空间,因为不需要创建额外的Entry存放键值对。
  5. SparseArray中存放键的数组是递增序列。
  6. SparseArray删除元素时并不会真正删除,而是标记为待删除元素,在合适的时候会将后面的元素往前挪覆盖掉待删除元素。待删除元素在没有被覆盖前有可能被复用。

recyclerview

  1. Recycler有4个层次用于缓存ViewHolder对象,优先级从高到底依次为ArrayList<ViewHolder> mAttachedScrapArrayList<ViewHolder> mCachedViewsViewCacheExtension mViewCacheExtensionRecycledViewPool mRecyclerPool。如果四层缓存都未命中,则重新创建并绑定ViewHolder对象

  2. 缓存性能:

    缓存重新创建ViewHolder重新绑定数据
    mAttachedScrapfalsefalse
    mCachedViewsfalsefalse
    mRecyclerPoolfalsetrue
  3. 缓存容量:

    • mAttachedScrap:没有大小限制,但最多包含屏幕可见表项。
    • mCachedViews:默认大小限制为2,放不下时,按照先进先出原则将最先进入的ViewHolder存入回收池以腾出空间。
    • mRecyclerPool:对ViewHolderviewType分类存储(通过SparseArray),同类ViewHolder存储在默认大小为5的ArrayList中。
  4. 缓存用途:

    • mAttachedScrap:用于布局过程中屏幕可见表项的回收和复用。
    • mCachedViews:用于移出屏幕表项的回收和复用,且只能用于指定位置的表项,有点像“回收池预备队列”,即总是先回收到mCachedViews,当它放不下的时候,按照先进先出原则将最先进入的ViewHolder存入回收池。
    • mRecyclerPool:用于移出屏幕表项的回收和复用,且只能用于指定viewType的表项
  5. 缓存结构:

    • mAttachedScrap:ArrayList
    • mCachedViews:ArrayList
    • mRecyclerPool:对ViewHolder按viewType分类存储在SparseArray中,同类ViewHolder存储在ScrapData中的ArrayList中

ItemDecoration

  • 用于绘制ItemView以外的内容,有两个回调onDraw和onDrawOver,区别是绘制的顺序有先后,onDraw()会在RecyclerView.onDraw()中调用,表示绘制RecyclerView自身的内容,会在绘制孩子之前,所以出现在孩子下面,而onDrawOver() 是在 RecyclerView.draw()中调用,在绘制孩子之后调用,所以会出现在孩子上方

view生命周期

构造View --> onFinishInflate --> onAttachedToWindow --> onMeasure --> onSizeChanged --> onLayout --> onDraw --> onDetachedFromWindow

Bitmap

  • raw和drawable和sdcard,这些不带dpi的文件夹中的图片被解析时不会进行缩放(inDensity默认为160)
  • 获取bitmap宽高:若inJustDecodeBounds为true,则不会把bitmap图片的像素加载到内存(实际是在Native层解码了图片,但是没有生成Java层的Bitmap),只是获取该bitmap的原始宽(outWidth)和高(outHeight)
  • 在4.4之前如果复用bitmap则不支持在native层缩放,缩放放到java层,将原来bimap基础上新建bitmap,销毁原来的,这样效率低, 4.4以后支持在native层做缩放
  • 8.0,bitmap像素数据在native,6.0以前finalize释放native的bitmap,之后通过注册NativeAllocationRegistry(简化了Cleaner的使用),将native资源的大小计入GC触发的策略之中。即使java堆增长缓慢,而native堆增长快速也同样会触发gc
  • Cleaner用于回收native堆中的像素数据:Cleaner继承自虚引用,虚引用指向对象被回收时,虚引用对象会进入ReferenceQueue,异步线程ReferenceQueueDaemon会在ReferenceQueue.wait()上等待,只有对象被回收,然后遍历引用队列,若存在Cleaner则调用clean方法释放native内存
  • Bitmap大小=长* 宽 * 像素大小
  • BitmapFactory.Options
    • 复用bitmap:加载时设置inBitmap表示使用之前bitmap对象使用过的内存 而不是重新开辟新内存(如果被复用的Bitmap == 返回的被加载的Bitmap,那么说明复用成功了)。复用条件是,图像是可变的isMutable为true
    • inPreferredConfig
      • 只有当图片是webp 或者png24的时候,inPreferredConfig才会有效果。
      • ALPHA_8 : 图片只有alpha值,没有RGB值,一个像素占用一个字节
      • RGB_565:2字节
      • ARGB_8888 : 一个像素占用4个字节
    • inSampleSize:为2的幂次,表示压缩宽高为原来的1/2,像素密度不变,按需加载,按ImageView大小加载,计算inSampleSize的算法是,原始宽高不停除2,inSampleSize不停乘2,直到原始宽高小于需求宽高。
  • 回收bitmap(Bitmap.recycle()):只是释放图片native对象的内存,并且去除图片像素数据的引用,让图片像素数据可以被垃圾回收
  • Bitmap占用内存大小因素:
    1. 图片的原始宽高(即我们在图片编辑软件中看到的宽高)
    2. 解码图片时的Config配置(即每个像素占用几个字节)
    3. 解码图片时的缩放因子(即inTargetDensity/inDensity)
  • BitmapRegionDecoder用于图片分块加载
  • jpg 色彩丰富,没有兼容性问题,不支持透明度,和动画,适用于摄影作品。jpg在高对比度的场景下效果不好,比如黑色文字在白色的背景上
  • png包括透明度适用于图标,因为大面积的重复颜色,适用于无损压缩
  • webp包括有损和无损两个方式,webp的浏览器支持不佳,webp支持动画和透明度,之前动画只能用gif,透明度只能选png,有损压缩后的webp的解码速度慢,比gif慢2倍
  • 图片缩放比例 scale = 设备分辨率 / 资源目录分辨率 如:1080x1920的图片显示xhdpi中的图片,scale = 480 / 320 = 1.5,图片的宽高会乘以scale

ANR

  • KeyDispatchTimeout:View的点击事件或者触摸事件在特定的时间(5s)内无法得到响应。
  • BroadcastTimeout:广播onReceive()函数运行在主线程中,在特定的时间(10s)内无法完成处理。
  • ServiceTimeout:Service的各个生命周期函数在特定时间(20s)内无法完成处理

Lifecycle

  • 让任何组件可以作为观察者观察界面生命周期
  • 通过LifecycleRegistry,它持有所有观察者,通过注册ActivityLifecycleCallbacks 实现生命周期的分发,如果是29 以下则将ReportFragment添加到activity中
  • 监听应用前后台切换:通过registerActivityLifecycleCallbacks,然后在维护一个活跃activity的数量,ProcessLifecycleOwner为我们做了这件事情 用于监听应用前后台切换,ProcessLifecycleOwner的初始化通过ContentProvider实现

恢复数据

  1. onSaveInstanceState()+onRestoreInstanceState():会进行序列化到磁盘,耗时,杀进程依然存在
  2. Fragment+setRetainInstance():数据保存在内存,配置发生变化时数据依然存在,但杀进程后数据不存在
  3. onRetainNonConfigurationInstance() + getLastNonConfigurationInstance():数据保存在内存,配置发生变化时数据依然存在,但杀进程后数据不存在

进程优先级

一共有五个进程优先级

  1. 前台进程(Foreground process):该进程中有前台组件正在运行,oom_adj:FOREGROUND_APP_ADJ=0
    • 正在交互的Activity,Activity.onResume()
    • 前台服务
    • Service.onCreate() onStart()正在执行
    • Receiver.onReceive()正在执行
  2. 可见进程(Visible process) VISIBLE_APP_ADJ = 1
    • 正在交互的Activity,Activity.onPause()
  3. 服务进程(Service process)
    • 后台服务
  4. 后台进程(Background process) BACKUP_APP_ADJ = 3
    • 不可见Activity Activity.onStop()
    • 后台进程优先级等于后台服务,所以长时间后台任务最后其服务
  5. 空进程(Empty process):不包含任何组件的进程,Activity 在退出的时候进程不会销毁, 会保留一个空进程方便以后启动. 但在内存不足时进程会被销毁

按下返回键退出应用,此时应用进程变成缓存进程,随时可能被杀掉

按下home键退出应用,此时应用是不可见进程

LruCache

  • ~是内存缓存,持有一个 LinkedHashMap 实例
  • ~用LinkedHashMap作为存储结构,且LinkedHashMap按访问顺序排序,最新的结点在尾部,最老的结点在头部

启动优化

  1. 视觉优化:windowBackground设置一张图片(成为StartingWindow的Decorview的背景)
  2. 初始化任务优化:可以异步初始化的,放异步线程初始化,必须在主线程但可以延迟初始化的,放在IdleHandler中,
  3. ContentProvider 优化:去掉没有必要的contentProvider
  4. 缩小main dex:MultidexTransform 解析所有manifest中声明的组件生成manifest_keep.txt,再查找manifest_keep.txt中所有类的直接引用类,将其保存在maindexlist.txt中,最后将maindexlist.txt中的所有class编译进main.dex。multiDex优化,自行解析AndroidManifest,自定义main.dex生成逻辑,将和启动页相关的代码分在主dex中,减小主dex大小,加快加载速度。
  5. multiDex.install 异步化,在4.4以下的机型MultiDex.install()耗时。它会先解压apk,遍历其中的dex文件,然后压缩成对应的zip文件(这是第一次的逻辑,第二次启动时已经有zip文件则直接读取。)然后通过反射,将其余的dex追加到DexPathList的尾部。这个过程中的压缩成zip可以免去,以提升速度。可以将这个过程放在单独的一个进程中做(在attachBaseContext中,开启一个死循环等待multidex完成),而且该进程有一个activity界面展示loading,但加载完毕后通知主进程,在跳转到闪屏页

LiveData

  • LiveData 的数据观察者在内部被包装成另一个对象(实现了 LifecycleEventObserver 接口),它同时具备了数据观察能力和生命周期观察能力
  • LiveData 内部会将数据观察者进行封装,使其具备生命周期感知能力。当生命周期状态为 DESTROYED 时,自动移除观察者
  • LiveData 的观察者会维护一个“值的版本号”,用于判断上次分发的值是否是最新值,“新观察者”被“老值”通知的现象叫“粘性”。因为新观察者的版本号总是小于最新版号,且添加观察者时会触发一次老值的分发
  • 粘性不应该成为一个问题,网上有很多关于粘性的解决方案,详见LiveData 面试题库、解答、源码分析
  • 在高频数据更新的场景下使用 LiveData.postValue() 时,会造成数据丢失。因为“设值”和“分发值”是分开执行的,之间存在延迟。值先被缓存在变量中,再向主线程抛一个分发值的任务。若在这延迟之间再一次调用 postValue(),则变量中缓存的值被更新,之前的值在没有被分发之前就被擦除了。

ViewModel

  • ViewModel 实例被存储在ViewModelStore的map中, 在配置发生变化时onRetainNonConfigurationInstance会被调用(ViewModelStore的map对象会被存储在NonConfigurationInstances中),并会返回这个对象.在恢复ViewModel时再getLastNonConfigurationInstance中再次获取

  • Activity 实现了LifecycleOwner,等onDestroy时会尝试(非配置变化时)调用 store的 clear(遍历了 viewmodel的clear)

  • ViewModel 在 Fragment 中不会因配置改变而销毁的原因其实是因为其声明的 ViewModel 是存储在 FragmentManagerViewModel 中的,而 FragmentManagerViewModel 是存储在宿主 Activity 中的 ViewModelStore 中,又因 Activity 中 ViewModelStore不会因配置改变而销毁,故 Fragment 中 ViewModel 也不会因配置改变而销毁。

DiskLruCache

  • 内部有一个线程池(只有一个线程)用于清理缓存
  • 内部有一个LinkedHashMap结构代表内存中的缓存,键是key,值是Entry实体(key+file文件)
  • ~有一个journal文件缓存操作的日志文件,构造时会读取日志文件并将其转换成LinkedHashMap存储在内存
  • 取缓存时,先读取内存中的Entry,然后将其转换成Snapshot对象,可以从Snapshot中拿到输入流
  • 写缓存时,新建Entry实体并存在LinkedHashMap中,将其转换成Editor对象,可以从中拿到输出流
  • 通过LinkedHashMap实现LRU替换
  • 每一个Cache项有四个文件,两个状态(DIRTY,CLEAN),每个状态对应两个文件:一个文件存储Cache meta数据,一个文件存储Cache内容数据

编译打包流程

  1. 打包资源,res下文件转换成二进制,asset目录
  2. 编译java文件为class文件
  3. 将class文件转换为dex文件
  4. 将资源和dex打包到apk
  5. 签名

launch mode

taskAffinity与allowTaskReparenting配合:我们可以在AndroidManifest.xml为Activity配置android:allowTaskReparenting属性,表示允许此Activity更换其从属的任务栈。设置此属性的Activity一但当前Task切换到了后台,就会回到它“倾向”的任务栈中

  1. singleTask 全局唯一,先检查是否有和待启动Activity的taskAffinity相同的task(若未显示指定,则默认和app的第一个activity拥有相同的taskAffinity,为包名),若无则新建task并新建Activity压栈,若有则把该task移到前台并在该task中寻找待启动activity,若找到则将该task中该activity之上的所有activity弹出,让其成为栈顶(此时onCreate()不被调,onNewIntent()被调),若没有找到则新建Activity实例并压栈

  2. singleInstance 全局唯一,如果不存在待启动Activity,则新建task来容纳待启动activity,并且该task不能放入其他activity,若存在,则将对应的task移到前台 该类型只能在manifest中指定,不能通过Intent.setFlag() 该类型只能作用于Task栈底的Activity

  3. standard 全局不唯一,每次都在当前task中新建一个实例(待启动activity.onCreate()每次都会被调用)

  4. singleTop 全局不唯一,只有当Activity位于task的栈顶时,该activity实例才会被重复利用(onNewIntent()被调用而不是onCreate()),否则都会新建实例

ActivityManagerService

  • 在SystemServer中被启动,通过SystemServiceManager.startService(ActivityManagerService.Lifecycle.class)启动
  • 负责四大组件的启动切换调度
  • AMS 构造的时候
    1. 创建了两个线程,一个是工作线程,一个是和进程启动相关的线程
    2. 启动了低内存检测LowMemDetector,通过epoll机制获取低内存通知
    3. 构建ActiveServices管理service
    4. 构建ProviderMap管理contentProvider
    5. 初始化ActivityTaskManager
    6. 开启Watchdog,监听线程阻塞
    7. 创建OomAdjuster用于调整进程的优先级等级
    8. 创建BatteryStatsService和ProcessStatsService用于管理电量状态和进程状态
  • 获取AMS对象,是通过 ServiceManager.getService() 获取一个 IBinder 对象,然后通过asInterface()获取本地对象或者远程对象的本地代理(Android 10中所有系统服务都通过AIDL接口来获取,在Android10以前获取服务不是通过直接通过AIDL接口的,而是通过ActivityManagerNative来转发,本质还是通过AIDL生成类Stub来获取)
  • 它负责管理Activity,它通过ActivityStackSupervisor管理Activity调度,ActivityStackSupervisor实例在它构造函数中被创建
  • 它会和应用进程双向通信以完成启动Activity,
  • 它维护所有进程信息,包括系统进程,它通过ProcessRecord来维护进程运行时的状态信息,需要将应用进程绑定到ProcessRecord才能开始一个Application的构建

动画

  1. view animation
    • 也称为补间动画:定义关键帧,其余帧由系统补齐,有点像导航
    • 局限于view 局限于位移 旋转 透明度 缩放
    • 用父控件的Transformation中的matrix,将matrix应用到View 上,每次绘制的时候会检查动画是否完成,若没完成则调用invalidate(),ViewRootImpl向编舞者跑了一个遍历View树的任务,会有同步消息屏障
    • 不能监听动画变化的过程
    • 还能响应原有位置的触摸事件,是因为会将 matrix 反向运算
  2. property animation
    • 构建值变化的完整序列 并将其运用到视图属性上
    • 不仅仅适用于view 可用于任何提供了getter和setter的对象的任何属性上
    • 通过向 Choreographer 不停post 一个动画类型任务(在绘制任务之前执行), 当前帧完毕后若动画未结束继续post 没有同步消息屏障
    • 可监听动画变化过程
    • Interpolator决定值变化的速度(根据时间流逝的百分比计算出当前属性值改变的百分比)
    • 若是硬件加速,则直接修改RenderNode中的相关属性,不需要重新构建DisplayList,若是软件绘制,则会触发invalidate
  3. 帧动画:就像连环画一样,一张张图片连续播放。AnimationDrawable 会在动画播放之前将所有帧都加在到内存,耗内存。

关于帧动画的性能优化可以点击Android性能优化 | 帧动画OOM?优化帧动画之SurfaceView逐帧解析

ConstraintLayout 性能

  • 额外的封装,会将容器控件包装成ConstraintWidgetContainer,子控件包装成ConstraintWidget
  • 在测量布局的时候,会想把所有的ConstraintWidget移除,然后遍历所有子控件并重新添加ConstraintWidget,如果子控件有约束则将其连接到锚点,构建依赖图,深度遍历依赖图,进行求解(Cassowary 算法),最终得到相对于父亲的上下左右。

Java & Kotlin

string

string是final类型的char数组,表示引用不会改变

final

表示引用指向不能变,但其指向的变量是可变的

泛型

  • 泛型的目的是类型参数化,即用变量表示类型
  • 提升安全性:泛型可以把使用Object的错误提前到编译后,而不是运行后,提升安全性
  • 消除强转:没有泛型的时候,都用Object代替,因为Object可以强转成任何类型
  • PECS是在使用泛型时为了遵守里氏替换原则必须准守的原则,使用泛型增加代码适用性时保证了类型安全。
  1. PE:Producer extends 实现协变效果:泛型类和类型参数的抽象程度具有相同的变化方向。泛型类只生产泛型,即泛型只会出现在类方法的返回值位置,kotlin中用out表示,java用extend表示
  2. CS:consumer super 实现逆变效果:泛型类只消费泛型,即泛型只出现在类方法的参数位,kotlin中用in表示,java用super表示。
  • 类型参数的父子关系是否会延续到外部类上,若延续的叫协变,否则父子关系转向了,这叫逆变,若没有父子关系则叫不变型 ,泛型是不变型
  • 类型擦除:为了兼容1.5以前的代码,即编译后的实参类型是Object或者上界
  • 当子类覆盖或者实现父类方法时,方法的形参要比父类方法的更为宽松;
  • 当子类覆盖或者实现父类方法时,方法的返回值要比父类的更严格。
  • 如果在编译的时候就保存了泛型类型到字节码中,那么在运行时我们就可以通过反射获取到,如果在运行时传入实际的泛型类型,这个时候就会被擦除,反射获取不到当前传入的泛型实际类型
  • Kotlin reified 可避免类型擦除,它会将方法内联到执行的地方,并且对泛型对象进行instanceof 的分类讨论。

关于泛型的详细分析可以点击Kotlin 进阶 | 不变型、协变、逆变

java对象生命周期

  1. 创建:为对象分配内存,构造对象
  2. 应用:至少一个强引用指向它
  3. 不可见:不再持有强引用(程序执行超出了对象的作用域)
  4. 不可达:没有强引用指向
  5. 收集:准备gc,会执行 finalize()
  6. 终结:等待垃圾回收
  7. Deallocated:回收完成

类加载

  • 编译:javac 命令把 .java 文件编译成字节码(.class 文件)
  • 运行:jvm执行.class
  • 类加载过程:加载---链接---初始化
  1. 类加载:jvm把.class作为二进制流读入内存,并实例化一个Class对象,jvm 并不是一次性把所有类都加在到内存,而是执行过程中遇到没有加载的才加载,并只加载一次(Android加载的dex)
  2. 验证:二进制合法性校验
  3. 准备:为类变量在方法区赋初始值
  4. 解析:将类名,方法名,字段名替换为内存地址
  5. 初始化:对类的主动引用,包括new 调用静态方法,使用静态字段
  6. 使用:
  7. 卸载: 统计类加载耗时:反射BaseDexClassLoader的 pathList,写入自定义的 PathClassLoader(装饰者增加耗时统计)
  • 类加载器 PathClassLoader:只能加载应用包内的dex
  • 类加载器 DexClassLoader:可以加载任意位置的 dex
  • 类加载器 BaseDexClassLoader:持有 DexPathList,结构如下, BaseDexClassLoader(DexPathList(Element数组(DexFile(多个Class))))
  • DexPathList: 将 dex 文件转换成 element 存入数组(dexElements),findClass()是遍历Elements并进行类名匹配。
  • Android 类加载过程:Dex 文件在类加载器中被包装成 Element,Element以数组形式被DexPathList 持有,加载类时通过遍历Element数组进行类名匹配查找,只要把新的Dex文件插入到 Element头部即可实现热修(反射)。
  • 双亲委托:类加载器加载类时,将加载请求逐级向上委托,直到BootStrapClassloader,真正的加载从顶层开始,逐级向下查找。避免了类重复加载,以及安全,因为系统类总是由上层类加载器加载,无法通过自定义篡改

类构造顺序

  • 先父亲,再孩子
  • 先静态再非静态
  • 先字段,后构造器(字段先后有定义顺序决定)
  • 先代码块 后构造方法

HashMap

  • 存储结构是开散列表:地址向量+同义词子表=数组+单链表。
  • 解决哈希冲突的办法是拉链法:将相同散列地址的键值存放在同义词子表中。
  • capacity为啥要为2的幂次,是为了用位与运算代替取模运算,提高性能。
  • 为啥loadFactor 是0.75,因为中庸,若为1,则频繁冲突,若为更小值,则会频繁扩容。
  • 构造~时,并没有初始化地址向量,而是要等到put操作是才构造
  • 遍历HashMap的顺序是从地址向量的第一个开始,先从前到后遍历同义词子表,然后下一个同义词子表
  • HashMap通过hash算法先定位到地址向量中对应的位置,然后遍历同义词子表
  • HashMap不是线程安全的,当~扩容的时候要进行迁移,多线程并发put会导致迁移出环。建议使用Hashtable或者ConcurrentHashMap。Hashtable将put和get方法都加上了synchronized,性能较差

WeakHashMap

  • 用于存放键值对,当发生gc时,其中的键值对可能被回收。适用于对内存敏感的缓存
  • 存放键值对的Entry继承自WeakReference。当发生gc时,Entry被回收并加入到ReferenceQueue中
  • 访问~时,会将已经gc的键值对从中删除(通过遍历ReferenceQueue)

LinkedHashMap

  • 是一个有序 map,可以按插入顺序或者访问顺序排列
  • 在 hashMap 基础上增加了头尾指针形成双向链表,继承 Node 添加前后结点的指针,每次构建结点时会将他链接到链尾。
  • 若是按访问顺序排序,存取键值对的时候会将其拆下插入到链尾,链头是最老的结点,满时会被移出
  • 按访问顺序来排序是LRU缓存的一种实现。

ThreadLocal

  • 用于将对象和当前线程绑定(将对象存储在当前线程的ThreadLocalMap结构中)
  • ThreadLocalMap是一个类似HashMap的存储结构,键是ThreadLocal对象的弱引用,值是要保存的对象
  • set()方法会获取当前线程的ThreadLocalMap对象
  • threadLocal内存泄漏:key是弱引用,gc后被回收,value 被entry持有,再被ThreadLocalMap持有,再被线程持有,如果线程没有结束,则value无法访问到,也无法回收,方案是及时remove掉不用的value
  • threadlocal 会自动清理key为null 的entry

内存泄漏

内存泄漏是因为堆内存无法释放 android内存泄漏就是生命周期长的对象持有了生命周期较短对象的引用

  1. 静态成员变量(单例)
    • 静态成员变量的生命周期和整个app一样,如果它持有短生命周期的对象则会导致这些对象内存泄露
    • 静态变量在不使用时需要置空
    • 静态变量使用弱引用持有Activity
    • 单例持有App context而不是Activity Contex
    • 静态方法可以被子类隐藏,而不是重写
  2. 非静态内部类
    • 匿名内部类持有外部类引用
    • handler是典型的匿名内部类,handler中的消息持有handler引用,如果有未处理完的消息,则会导致handler外层类内存泄露,Looper -> MessageQueue -> Message -> Handler -> Activity,解决办法是静态内部类+Activity弱引用,并且在activity退出时清除所有消息
    • new Thread()是典型的匿名内部类,如果Activity退出后Thread还在执行则会引起Activity内存泄露
  3. 集合类
    • 集合对象会持有孩子的引用,需要及时清除且置空
  4. webview内存泄露
    • WebView在加载网页后会长期占用内存而不能被释放,因此我们在Activity销毁后要调用它的destory()方法来销毁它以释放内存

引用

  1. 强引用
  • 通过=显式的将对象A赋值给变量a,则A就存在一个强引用a
  • 强引用需要显式的置null 以告诉gc该对象可以被回收
  • 在一个方法的内部有一个强引用,这个引用保存在栈中,而真正的引用内容(Object)保存在堆中。当这个方法运行完成后就会退出方法栈,则引用内容的引用不存在,这个Object会被回收。但是如果这个object是全局的变量时,就需要在不用这个对象时赋值为null,因为强引用不会被垃圾回收
  • 清空list时需要遍历所有元素将其置null
  1. 软引用
  • 如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
  1. 弱引用
  • 弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
  1. 虚引用
  • 虚引用主要用来跟踪对象被垃圾回收器回收的活动,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。用于在对象被回收时做一些事情

软引用、弱引用、虚引用的构造方法均可以传入一个ReferenceQueue与之关联。在引用所指的对象被回收后,引用(reference)本身将会被加入到ReferenceQueue之中,此时引用所引用的对象reference.get()已被回收 (reference此时不为null,reference.get()此时为null)。在一个非强引用所引用的对象回收时,如果引用reference没有被加入到被关联的ReferenceQueue中,则表示还有引用所引用的对象还没有被回收。如果判断一个对象的非强引用本该出现在ReferenceQueue中,实际上却没有出现,则表示该对象发生内存泄漏。

接口和抽象类

  • 类可以实现很多个接口,但是只能继承一个抽象类
  • 类如果要实现一个接口,它必须要实现接口声明的所有方法。但是,类可以不实现抽象类声明的所有方法,当然,在这种情况下,类也必须得声明成是抽象的。

字符串常量池

  • JVM为了减少字符串对象的重复创建,其维护了一个特殊的内存,这段内存被成为字符串常量池
  • 字符串常量池实现的前提条件是java中的String对象是不可变的,否则多个引用指向同一个变量的时候并改变了String对象,就会发生错乱
  • 字符串常量池是用时间换空间,cpu需要在常量池中寻找是否有相同字符串
  • 字符串构造方式
  1. 字面量形式:String str = "droid" 使用这种形式创建字符串时,JVM首先会对这个字面量进行检查,如果字符串常量池中存在相同内容的字符串对象的引用,则将这个引用返回,否则新的字符串对象被创建,然后将这个引用放入字符串常量池,并返回该引用
  2. 新建对象形式:String str = new String("droid"); 使用这种形式创建字符串时,不管字符串常量池中是否有相同内容,新的字符串总是会被创建。 对于上面使用new创建的字符串对象,如果想将这个对象的引用加入到字符串常量池,可以使用intern方法。调用intern后,首先检查字符串常量池中是否有该对象的引用,如果存在,则将这个引用返回给变量,否则将引用加入并返回给变量。String str4 = str3.intern();

异常

  • Exception和Error都继承于Throwable
  • Exception是程序错误
  • Exception又分为checked Exception(编译时异常)和unchecked Exception(运行时)。checked Exception在代码里必须显式的进行捕获,这是编译器检查的一部分。unchecked Exception也就是运行时异常,类似空指针异常、数组越界等,通常是可以避免的逻辑错误
  • Error是比程序更加低层的错误, 包括虚拟机错误OutOfMemoryError,StackOverFlowError

注解

注解为代码添加一些额外的信息,以便稍后可以读取这些信息。这些信息可以帮助代码检查,编译时生成代码以减少模板代码

  • 元注解
  1. @Retention:定义注解生命周期
    • RetentionPoicy.SOURCE注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;用于做一些检查性的操作,比如 @Override
    • RetentionPoicy.CLASS:注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期;用于在编译时进行一些预处理操作,比如生成一些辅助代码(编译时注解即是编写生成代码的代码),ButterKnife 使用编译时注解,即在编译时通过自定义注释解析器AbstractProcessor读取注解并由此生成java文件(在里面调用了 findViewById)
    • RetentionPoicy.RUNTIME:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;用于在运行时去动态获取注解信息
  2. @Target:定义了Annotation所修饰的对象范围

内存模型

dalvik虚拟机内存空间被划分成多个区域 = 虚拟机栈+ 程序计数器+ 方法区+ 堆+ 本地方法栈

  • 方法区:方法区主要是存储已经被 JVM 加载的类信息(版本、字段、方法、接口)、常量、静态变量、即时编译器编译后的代码和数据。该区域被各个线程共享的内存区域。
  • 堆区:又称动态内存分配,存放所有用通过new创建的类对象(包括该对象其中的所有成员变量),也就是对象的实例。这部分内存在不使用时将会由 Java 垃圾回收器来负责回收
    • 堆内存没有可用的空间存储生成的对象,JVM会抛出java.lang.OutOfMemoryError
    • 堆内存分为新生代和老年代和永生代,新生代又分为Eden、From Survivor、To Survivor三个区域
      • 永生代用于存放class信息
  • 虚拟机栈 :虚拟机栈是线程私有的数据结构,它用来描述 Java 方法执行的内存模型,每个方法被执行的时候,JVM 都会在虚拟机栈中创建一个栈帧
    • 栈帧(Stack Frame)
      • 一个线程包含多个栈帧,而每个栈帧内部包含局部变量表、操作数栈、动态连接、返回地址等
      • 局部变量表是变量值的存储空间,我们调用方法时传递的参数,以及在方法内部创建的局部变量都保存在局部变量表中。在 Java 编译成 class 文件的时候,就会在方法的 Code 属性表中的 max_locals 数据项中,确定该方法需要分配的最大局部变量表的容量。
      • 在方法退出后都需要返回到方法被调用的位置,程序才能继续执行。而虚拟机栈中的“返回地址”就是用来帮助当前方法恢复它的上层方法执行状态。
    • 如果栈内存没有可用的空间存储方法调用和局部变量,JVM会抛出java.lang.StackOverFlowError
  • 本地方法栈和虚拟机栈类似,只不过用于执行native方法
  • 程序计数器:每个线程都需要一个程序计数器,用于记录正在执行指令的地址

GC

  • 垃圾定义:有两种定义垃圾的方法
    1. 引用计数:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;计数器为0的对象就是垃圾
    2. 可到达性:从GC Roots作为起点,向下搜索它们引用的对象,可以生成一棵引用树,树的节点视为可达对象,反之视为不可达。不可到达的对象是垃圾,被定义为垃圾的对象不代表马上会被回收,还会去检查是否要执行finalize方法
  • GC种类:Minor GC、Full GC ( 或称为 Major GC )
    • 垃圾回收回收不可到达的对象,即没有引用的对象,可到达的对象一定被根引用
    • Minor GC 是发生在新生代中的垃圾收集动作,所采用的是copy and sweep(经过6次gc还存活的对象会被放到老年代)
    • Full GC 是发生在老年代的垃圾收集动作,所采用的是mark and sweep
    • 分代回收(generational collection):每个对象记录有它的世代(generation)信息。所谓的世代,是指该对象所经历的垃圾回收的次数。世代越久远的对象,在内存中存活的时间越久
  • GC回收算法
    • copy and sweep:内存被分为两个区域。对象总存活于两个区域中的一个。当垃圾回收启动时,Java程序暂停运行。JVM从根出发,找到可到达对象,将可到达对象复制到空白区域中并紧密排列,修改由于对象移动所造成的引用地址的变化。最后,直接清空对象原先存活的整个区域,使其成为新的空白区域。适用于存活对象少,垃圾对象多的场景
    • mark and sweep:每个对象将有标记信息,用于表示该对象是否可到达。当垃圾回收启动时,Java程序暂停运行。JVM从根出发,找到所有的可到达对象,并标记(mark)。随后,JVM需要扫描整个堆,找到剩余的对象,并清空这些对象所占据的内存堆,缺点是容易产生内存碎片。适用于存活对象多,垃圾对象少的场景
    • 分代回收算法:老年代每次gc只有少量对象被回收,而新生代有大量对象被回收,对于新生代采用copy and sweep,对老年代采用mark and sweep。

OOM类型

  1. 堆内存不足
  2. 无足够的连续内存空间
  3. 文件描述符超过数量限制
  4. 线程数量超过限制
  5. 虚拟内存不足

内存优化

  1. 使用内存友好的数据结构 SpareseArray,ArrayMap
  2. 避免内存泄漏,避免长生命周期对象持有短生命周期对象
  3. 使用池结构,复用对象避免内存抖动。
  4. 根据手机内存大小,设置内存缓存的大小。
  5. 多进程,扩大可使用内存。
  6. 通过ComponentCallback2 监听内存吃紧,进行内存缓存的释放。

LeakCanary

  • 通过ActivityLifecycleCallbacks监听Activity生命周期,在onActivityDestroy时获取Activity实例,并为其构建弱引用并关联引用队列。
  • 起异步线程,观察ReferenceQueue是否有Activity的弱引用,如果有则说明回收成功,否则回收失败
  • 回收失败后会手动触发一次gc,再监听ReferenceQueue,如果还是回收失败,则dump内存
  • LeakCanary 通过contentProvider安装
  • 当一个Activity的onDestory方法被执行后,说明该Activity的生命周期已经走完,在下次GC发生时,该Activity对象应将被回收

equals()

  • equals() 定义在JDK的Object.java中。可以定义两个对象是否相等的逻辑
  • "=="相等判断符用于比较基本数据类型和引用类型数据。 当比较基本数据类型的时候比较的是数值,当比较引用类型数据时比较的是引用(指针)即指向堆内存的地址
  • ==的语义是固定的,而equals()的语义是自定义的

hashCode()

  • hashCode() 的作用是获取哈希码,它实际上是返回一个int整数。仅仅当创建并某个“类的散列表”(关于“散列表”见下面说明)时,该类的hashCode() 才有用,作用是:确定该类的每一个对象在散列表中的位置,Java集合中本质是散列表的类,如HashMap,Hashtable,HashSet
  • HashMap 如果使用equals判断key是否重复,需要逐个比较,时间复杂度为O(n),但如果使用hashCode(),因为它是一个int值。所以可以直接作为数组结构的某个索引值,如果该索引位置没有内容则表示key没有重复,复杂度为O(1)
  • 如果两个对象相等,那么它们的hashCode()值一定相同。这里的相等是指,通过equals()比较两个对象时返回true。
  • 如果两个对象hashCode()相等,它们并不一定相等。因为在散列表中,hashCode()相等,即两个键值对的哈希值相等。然而哈希值相等,并不一定能得出键值对相等。补充说一句:“两个不同的键值对,哈希值相等”,这就是哈希冲突。

sealed class

  • 是一个继承结构固定的抽象类,即在编译时已经确定了子类数量,不能在运行时动态新增
  • 它的子类只能声明在同一个包名下
  • 是一个抽象类,且构造方法是私有的,它的孩子是final类,而且孩子声明必须嵌套在sealed class内部。
  • 枚举的局限性 限制枚举每个类型只允许有一个实例 限制所有枚举常量使用相同的类型的值

crossinline

在具有inline 特性的同时,避免非局部返回,因为直接return掉函数会影响原有功能,crossinline的lambda内部必须使用局部返回,比如return@foo

sequence

  • ~是惰性的:中间操作不会被执行,只有终端操作才会(toList())
  • ~的计算顺序和迭代器不同:~是对一个元素应用全部的操作,然后第二个元素应用全部操作,而迭代器是对列表所有元素应用第一个操作,然后对列表所有元素应用第二个操作

虚拟内存

  • 每个应用访问的地址空间是虚拟地址空间,所以可以无穷大,Linux负责将虚拟内存转换为物理地址。
  • 为了方便将虚拟内存地址和物理地址进行映射,内存空间被分割成若干个页(通常是4kb大小)
  • Memory Management Unit(MMU)这个硬件专门用于将虚拟地址转换为物理地址。它通过查询映射表得到物理地址
  • 虚拟地址分为高4位的页号,和后面的偏移量,每次通过页号查询映射表得到物理地址的页号,然后再将偏移量拼在后面得到物理地址

kotlin 空安全

  • 编译成java后是通过if判空实现空安全的
  • kotlin和java交互的时候空安全被破坏,可以通过在java代码添加@NotNull注解进行非空约束

Channel

  • 是一个挂起队列,和java 中 blocking queue 类似
  • 生产者叫 SendChannel,消费者叫 ReceiveChannel
  • 生产者和消费者之间有一条缓冲通道
  • 执行多线程同时生产,多线程同时消费
  • flow 只有订阅才生产数据,Channel 发送数据和订阅无关,所以是热流

协程

  • 是建立在线程之上的,更轻量级的(用户态),更容易控制生命周期(结构化并发)的计算单元。
  • 借助于suspend方法实现用户态非抢占式的并发调度,协程通过挂起主动让出执行权(普通的线程映射为内核线程,内核线程的调度是抢占cpu时间片)
  • 比使用线程池更容易取消异步操作,享受结构化并发,异常处理
  • 挂起方法并不会挂起线程,因为就像调用一个带回调的方法一样,它挂起的是协程剩下的代码。

结构化并发

java 线程间的并发是没有级联关系的,所以是非结构的

  1. 结束一个线程时,怎么同时结束这个线程中创建的子线程?
  2. 当某个子线程在执行时需要结束兄弟线程要做怎么做?
  3. 如何等待所有子线程都执行完了再结束父线程? 这些问题都可以通过共享标记位、CountDownLatch 等方式实现。但这两个例子让我们意识到,线程间没有级联关系;所有线程执行的上下文都是整个进程,多个线程的并发是相对整个进程的,而不是相对某一个父线程。

CPS

  • Continuation Passing Style,传递剩余的计算,将剩余的计算作为一个回调传递给方法。

suspend

  • cps+状态机: 每个suspend 方法都构建一个continuation不经济,一个协程块中的suspend方法会共用一个 continuation(持有一个label)。将原先不同的continuation写在了不同的 switch case 分支内,以挂起点为分割点。每执行一个分支点,label 就+1,表示进入下一个分支。挂起方法会被多次调用(invokeSuspend),因为label值不同每次都会走不同的分支
  • suspend 的返回值标志着挂起方法有没有被挂起

Dispatcher

  • 调度器CoroutineDispatcher是一个ContinuationInterceptor。通过interceptContinuation()将continuation包装成DispatchedContinuation
  • 不同的调度器通过重写 dispatch方法实现不同的线程调度。有些是通过handler抛一个runnable,有些是向线程池抛一个
  • default 属于cpu运算密集型:线程被阻塞时,cpu是在忙着运算
  • io 属于io型:线程被阻塞时,cpu是闲着。

Job

  • 可以被取消的任务
  • 他有六种状态:new-active-completing-completed-cancelling-canceled
  • 只有延迟启动的协程的job才会处于new 状态,其他都处于active状态,completing意味着自己的活干完了,在等子协程。 cancelling 是在取消协程之前最后的清理资源的机会。
  • 新建的协程会继承父亲的CoroutineContext,除了其中的job,新协程会新建job并成为父job的子job

背压

  • 生产速度大于消费速度
  • 使用缓冲区,阻塞队列
  • 缓冲区大小 & 缓冲区满之后的策略(丢弃最新,最久,挂起)

异常处理

  • 在 coroutineScope中,异常是向上传播的,只要任意一个子协程发生异常,整个scope都会执行失败,并且其余的所有子协程都会被取消掉;
  • 在 supervisorScope中,异常是向下传播的,一个子协程的异常不会影响整个 scope的执行,也不会影响其余子协程的执行;(重写了childCancelled并返回false) CancellationException 异常总是被忽略

取消协程

  • 每个启动的协程都会返回一个job,调用 job.cancel()会让job处于canceling状态,然后在下一个挂起点抛出CancellationException ,如果协程中没有挂起点,则协程不能被取消。因为每个suspend 方法都会检查job是否活跃,若不活跃则抛出CancellationException ,这个异常只是给上层一次关闭资源的机会,可以通过try-catch 捕获

  • 对于没有挂起的协程,需要通过while(isActive)来检查job是否被取消 或者 yield()

  • 当协程抛出CancellationException 后,在启动协程将会被忽略

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿