Android
View的事件分发流程
Android View 的事件分发流程是 Android 中处理触摸事件的核心机制,它决定了用户的触摸事件(如点击、滑动等)如何从顶层容器传递到具体的子 View 并最终被消费或处理。事件分发流程主要涉及以下三个方法:
- **
dispatchTouchEvent(MotionEvent ev)
**:事件分发方法,负责将事件传递到合适的子 View 或自身进行处理。 - **
onInterceptTouchEvent(MotionEvent ev)
**:用于拦截事件,决定是否要拦截事件并阻止它传递给子 View(只有ViewGroup
才有此方法,View
没有)。 - **
onTouchEvent(MotionEvent ev)
**:事件处理方法,决定当前 View 是否处理该事件。
1. 事件的传递机制(顶层到子 View)
Android 的触摸事件以 MotionEvent
对象的形式从最顶层的 Activity
(通常由 DecorView
表示)开始传递,逐层传递到目标 View。传递路径如下:
Activity
的dispatchTouchEvent()
处理触摸事件并传递给最顶层的ViewGroup
(通常是DecorView
)。ViewGroup
的dispatchTouchEvent()
决定是否将事件传递给子 View,或者让自己处理。ViewGroup
内部会调用onInterceptTouchEvent()
方法决定是否拦截事件:- 如果返回
true
,表示ViewGroup
自己拦截事件,不再将事件传递给子 View,并将事件传递给ViewGroup
自己的onTouchEvent()
进行处理。 - 如果返回
false
,表示不拦截事件,事件继续传递给子 View。
- 如果返回
View
的dispatchTouchEvent()
直接传递给onTouchEvent()
,决定是否处理该事件。
2. dispatchTouchEvent()
的工作流程
dispatchTouchEvent()
是整个事件分发的核心方法。它的基本逻辑是:
**对于
Activity
**:Activity
的dispatchTouchEvent()
负责将事件传递给根ViewGroup
,即DecorView
。
**对于
ViewGroup
**:dispatchTouchEvent()
首先调用onInterceptTouchEvent()
询问是否要拦截该事件。- 如果
onInterceptTouchEvent()
返回true
,则事件由ViewGroup
自己处理(传递给onTouchEvent()
)。 - 如果返回
false
,则事件传递给子 View 处理。
**对于
View
**:dispatchTouchEvent()
直接将事件交给onTouchEvent()
处理,没有拦截逻辑。
3. onInterceptTouchEvent()
- 这是
ViewGroup
独有的一个方法,用于决定是否要拦截事件。如果onInterceptTouchEvent()
返回true
,则事件不再传递给子 View,而是由当前ViewGroup
自己处理。 - 常见的
ViewGroup
(如ScrollView
或RecyclerView
)会根据事件的类型或滑动手势来决定是否拦截,例如在滑动开始时拦截事件,而点击时则不拦截。
4. onTouchEvent()
onTouchEvent()
是每个 View 的事件处理方法,负责最终处理触摸事件。onTouchEvent()
会根据事件类型(如ACTION_DOWN
、ACTION_MOVE
、ACTION_UP
等)来决定是否消费该事件。如果onTouchEvent()
返回true
,则表示该 View 处理并消费了事件,事件不会再继续向上传递。如果返回false
,事件将会继续上传,由父控件处理。
5. 事件分发流程示例
假设有一个布局层次如下:
Activity
ViewGroup A
View B
ViewGroup C
View D
触摸事件分发流程如下:
Activity
接收到事件,调用其dispatchTouchEvent()
,事件传递给根ViewGroup A
。ViewGroup A
调用dispatchTouchEvent()
,并调用onInterceptTouchEvent()
:- 如果
ViewGroup A
返回false
,事件会继续传递给其子 View。
- 如果
- 事件传递到
ViewGroup C
,ViewGroup C
同样调用dispatchTouchEvent()
并执行onInterceptTouchEvent()
:- 如果
ViewGroup C
返回false
,事件会继续传递到View D
,并最终由View D
的onTouchEvent()
处理。
- 如果
- 如果某个
ViewGroup
的onInterceptTouchEvent()
返回true
,表示拦截事件,事件将直接传递给该ViewGroup
处理,不再传递给子 View。
6. 事件的返回与消费
在事件的分发过程中,任何一个 dispatchTouchEvent()
或 onTouchEvent()
返回 true
都表示事件已经被消费。此时,事件不再继续向下或向上传递。
总结
Android 事件分发的关键在于三大方法的相互协作:
dispatchTouchEvent()
:事件的实际分发者,决定事件的走向。onInterceptTouchEvent()
:用于ViewGroup
拦截事件。onTouchEvent()
:处理并消费事件。
每个事件都从顶层的 Activity
开始,经过层层 ViewGroup
和 View
的传递和处理,最终找到处理该事件的目标 View 或被某个父级 View 拦截处理。
View的绘制流程
Android View 的绘制流程是整个 UI 渲染的关键环节。View 的绘制从根 View
开始,通常是 DecorView
,逐级传递到各个子视图。Android 的绘制流程分为三个主要阶段:测量(Measure)、布局(Layout) 和 绘制(Draw)。这些阶段分别对应三个核心方法:measure()
, layout()
, 和 draw()
。
View 绘制流程概览
整个 View 的绘制流程可以通过以下四个主要步骤来理解:
- **
measure()
**:测量每个 View 的大小,确定它的宽高。 - **
layout()
**:确定每个 View 在父容器中的位置,安排布局。 - **
draw()
**:实际绘制 View 的内容。 - 绘制子视图:
ViewGroup
会对子视图进行递归绘制。
1. 测量流程 (measure()
)
measure()
方法是 View 绘制流程的第一步,它负责计算每个 View 的大小,即宽度和高度。measure()
过程会触发 onMeasure()
,通过传递 MeasureSpec
确定每个 View 的尺寸。
MeasureSpec
是一个整数值,用来描述父控件对子控件的布局要求。它分为三种模式:- **
UNSPECIFIED
**:父控件不对子控件施加任何限制,子控件可以随意决定自己的大小。 - **
EXACTLY
**:父控件决定了子控件的大小,子控件的宽高必须严格符合父控件给定的尺寸。 - **
AT_MOST
**:父控件指定一个最大值,子控件的大小不能超过这个值。
- **
View.measure()
的核心任务是根据 MeasureSpec
计算并确定 View 的宽高。在 View
中,measure()
方法会调用 onMeasure()
,你可以重写 onMeasure()
方法来自定义测量逻辑:
1 |
|
- **
setMeasuredDimension()
**:该方法会在测量完成后被调用,告诉系统该 View 的最终宽度和高度。
对于 ViewGroup
,它不仅需要测量自己的大小,还需要测量其所有子 View 的大小。ViewGroup
会在 onMeasure()
中遍历其所有子 View,并为每个子 View 调用 measure()
方法,传递合适的 MeasureSpec
,让子 View 完成测量。
2. 布局流程 (layout()
)
测量完成后,进入布局阶段。布局阶段通过 layout()
方法来完成,layout()
方法会根据父控件分配的位置来确定 View 在屏幕中的具体坐标。
layout()
方法的参数包括left
,top
,right
, 和bottom
,它们定义了 View 在父容器中的相对位置。
layout()
会调用 onLayout()
方法,ViewGroup
需要在 onLayout()
中对子 View 进行布局:
1 |
|
对于每个 ViewGroup
,onLayout()
方法负责遍历所有子 View,并为每个子 View 调用 layout()
方法,传递具体的坐标值,完成所有子 View 的布局。
3. 绘制流程 (draw()
)
在布局完成后,进入绘制阶段。绘制阶段由 draw()
方法完成,它负责实际将 View 的内容显示在屏幕上。
draw()
方法会依次调用几个关键的绘制步骤:
- **
drawBackground()
**:绘制 View 的背景(如果设置了背景)。 - **
onDraw()
**:实际绘制 View 的内容。开发者可以重写此方法,绘制自定义内容。 - **
dispatchDraw()
**:对于ViewGroup
来说,dispatchDraw()
方法负责绘制子 View。 - **
drawChild()
**:在dispatchDraw()
中,通过drawChild()
方法绘制每个子 View。
onDraw()
示例
如果你有自定义的 View,并想绘制内容,可以重写 onDraw()
方法:
1 |
|
onDraw()
提供了一个 Canvas
对象,你可以使用 Canvas
和 Paint
类绘制文本、图形、图片等内容。
4. 绘制子视图 (dispatchDraw()
)
对于 ViewGroup
来说,除了绘制自己外,还需要绘制其子视图。dispatchDraw()
方法负责调用每个子视图的 draw()
方法。ViewGroup
中的 dispatchDraw()
通常不需要重写,系统会自动处理子视图的绘制顺序。
绘制流程中的核心方法调用顺序
绘制流程涉及的核心方法调用顺序如下:
measure()
→ 计算每个 View 的宽高,触发onMeasure()
。layout()
→ 确定每个 View 的位置,触发onLayout()
。draw()
→ 实际绘制 View 的内容,触发onDraw()
和dispatchDraw()
。
总结
Android View 的绘制流程包括以下几个关键步骤:
- 测量阶段(Measure):通过
measure()
和onMeasure()
方法计算 View 的宽高。 - 布局阶段(Layout):通过
layout()
和onLayout()
方法确定 View 的位置。 - 绘制阶段(Draw):通过
draw()
、onDraw()
和dispatchDraw()
方法绘制 View 的内容,ViewGroup
还负责绘制子视图。
这些步骤通过递归调用从根 View 开始,逐级向下传递,确保每个 View 都能正确地测量、布局和绘制。
Android中IPC方式、各种方式优缺点
在 Android 中,IPC(Inter-Process Communication,进程间通信)是指两个不同进程之间的通信机制。由于 Android 应用运行在各自独立的进程中,为了安全性和性能优化,进程间通信需要通过专门的机制来完成。Android 提供了多种 IPC 方式,每种方式各有优缺点,常见的 IPC 方式包括:
- Bundle
- Messenger
- AIDL(Android Interface Definition Language)
- ContentProvider
- BroadcastReceiver
- Socket
下面详细介绍每种 IPC 方式及其优缺点。
1. Bundle
概述:
Bundle
是 Android 内置的简单数据传递方式,通常用于在 Activity
、Service
、BroadcastReceiver
等组件之间传递数据。它主要用于在同一个应用的不同组件之间传递简单数据。
优点:
- 使用简单:不需要复杂的定义和管理,只需要打包和传递键值对。
- 速度快:适合在同一进程的组件之间传递少量数据。
- Android 内置支持:大多数 Android 组件都原生支持
Bundle
。
缺点:
- 仅支持基本类型数据:
Bundle
只能存储基本类型(如int
、boolean
)和少数系统类型(如Parcelable
对象)。 - 进程间使用受限:用于进程间通信时,只能传递少量、简单的数据,不适合复杂的数据结构。
适用场景:
- 同一应用内不同组件之间的简单数据传递。
2. Messenger
概述:
Messenger
是基于 Handler
的一种轻量级的 IPC 方式,适用于一对一的进程间通信。它通过 Message
传递数据,并支持在不同进程中通过 Handler
处理消息。
优点:
- 易于实现:通过消息的发送与接收机制,适合简单的 IPC。
- 基于
Handler
,非常适合一对一的消息通信。 - 传输过程安全:所有消息都封装在
Message
对象中。
缺点:
- 不支持复杂的数据结构:与
Bundle
一样,只适用于简单的数据类型传输。 - 不适合多线程或高并发场景:
Messenger
本质上是串行处理的,无法支持并发通信。 - 仅适用于一对一通信:不支持多个客户端同时与服务端通信。
适用场景:
- 轻量级、一对一的进程间通信,适用于少量数据传输。
3. AIDL(Android Interface Definition Language)
概述:
AIDL 是 Android 提供的支持进程间通信的工具,允许定义跨进程的接口。它适用于复杂的数据传递和多进程环境下的通信。AIDL 的原理是通过 Binder 机制实现客户端与服务端的通信。
优点:
- 强大的 IPC 支持:可以传递复杂的数据类型,如
List
、Map
,甚至自定义对象。 - 支持多进程:可以用于多客户端与同一服务端的并发通信。
- 自动生成代码:AIDL 会根据接口定义生成对应的代理类和服务端代码。
缺点:
- 实现复杂:需要编写
.aidl
文件,手动管理接口和服务端实现。 - 性能开销:由于数据序列化和反序列化的开销较大,AIDL 通信效率比
Messenger
稍低。 - 数据传递需要深度复制:对象必须实现
Parcelable
接口,数据在传递时被复制,无法共享对象的引用。
适用场景:
- 复杂的进程间通信,特别是需要在不同进程之间传递复杂数据结构时。
4. ContentProvider
概述:
ContentProvider
是 Android 用于共享数据的机制,适用于应用间的数据共享。它允许应用通过 URI 来访问和操作其他应用的数据,支持增删改查等操作。典型的例子是 Android 系统的联系人、媒体文件等都是通过 ContentProvider
共享数据的。
优点:
- 数据共享机制:适用于不同应用间的数据共享。
- 支持 SQL 风格的数据操作:提供类似数据库的
CRUD
操作接口,方便对数据进行管理。 - 安全性:通过 URI 权限控制,可以限制其他应用访问
ContentProvider
中的数据。
缺点:
- 仅适用于数据共享:
ContentProvider
适合管理和共享结构化数据,不适用于实时通信。 - 实现复杂:
ContentProvider
的实现需要处理多种操作(如查询、插入、删除等),并且需要处理 URI 匹配和权限控制。
适用场景:
- 不同应用间的数据共享,例如联系人、日历、媒体等数据。
5. BroadcastReceiver
概述:
BroadcastReceiver
是 Android 的广播机制,允许应用在进程间或应用间发送广播消息,通知其他应用或组件执行相应操作。广播可以是系统广播,也可以是应用自定义广播。
优点:
- 广泛适用:适用于一对多的通信方式,可以通知多个接收方。
- 松耦合:发送者和接收者之间不需要直接联系,方便应用内不同模块之间的通信。
- 适合全局事件通知:系统事件(如电池状态变化、网络变化)通常通过广播通知。
缺点:
- 数据传输效率较低:广播消息一般用于传递简单数据,复杂数据传输效率较低。
- 不适合实时通信:广播的通信是异步的,无法保证消息的实时性。
- 安全问题:广播可能被其他应用截获,需小心数据安全。
适用场景:
- 一对多的通信场景,适用于系统全局事件或应用内的全局消息通知。
6. Socket
概述:
Socket
是一种网络通信方式,支持进程间通过网络协议(如 TCP、UDP)进行数据传输。Android 中也可以通过 Socket
在不同进程或不同设备间通信。
优点:
- 适合网络通信:
Socket
可以在本地或远程进程间通信,甚至可以跨设备通信。 - 高度灵活:适用于需要自定义通信协议的场景。
- 支持大量数据传输:可以通过流的形式传输大量数据。
缺点:
- 实现复杂:需要手动处理网络连接、通信协议、数据包的解析与处理。
- 安全性问题:需要处理网络传输中的安全问题,如数据加密、身份认证等。
适用场景:
- 适用于大数据传输或复杂网络通信场景,尤其是在进程间或设备间需要实时通信时。
各种方式的对比总结
IPC 方式 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
Bundle | 简单易用,适合传递基本数据类型 | 只能用于传递简单数据类型,跨进程使用受限 | 应用内组件之间的简单数据传递 |
Messenger | 轻量级,基于 Handler 的一对一通信,简单且易实现 |
不支持复杂数据结构,不适合多线程和并发场景 | 简单的一对一进程间通信 |
AIDL | 支持复杂数据类型和并发通信,适合多进程场景 | 实现复杂,性能开销较大 | 复杂的进程间通信,需传递复杂数据时 |
ContentProvider | 适合应用间的数据共享,支持 SQL 风格的操作 | 实现复杂,仅适用于数据共享,无法用于实时通信 | 应用间的结构化数据共享 |
BroadcastReceiver | 支持一对多通信,适合全局消息广播和系统事件通知 | 不适合实时通信,数据传输效率低 | 一对多的全局消息广播或系统事件通知 |
Socket | 灵活,支持大数据传输和跨设备通信 | 实现复杂,需要处理网络协议和安全问题 | 大量数据的实时传输或跨设备通信 |
每种 IPC 方式适用于不同的场景,选择合适的方式可以提高开发效率和应用性能。
Binder机制的作用和原理
Binder 是 Android 中最重要的进程间通信(IPC)机制之一,Binder 的设计兼顾了高效性、安全性和灵活性,在 Android 系统中几乎所有的进程间通信都基于 Binder 机制,例如 AIDL
、Messenger
、ContentProvider
等都是通过 Binder 实现的。Binder 也是 Android 系统服务(如 ActivityManagerService
、WindowManagerService
等)的通信基础。
Binder 的作用
- 进程间通信(IPC):Binder 提供了一种高效的 IPC 机制,允许不同进程之间交换数据或请求服务。
- 安全性:Binder 内核实现了身份验证机制,可以通过 UID(用户 ID) 验证通信双方的身份,保证进程间通信的安全性。
- 高效性:Binder 使用内核缓冲区避免了数据的多次复制,减少了内存和 CPU 的消耗。
- 轻量级:与传统的 Linux IPC 机制(如管道、消息队列、共享内存等)相比,Binder 更轻量级,性能更好,设计上更适合 Android 移动设备的资源受限环境。
Binder 的原理
Binder 是 Android 特有的 IPC 机制,底层由 Linux 内核驱动支持。它的工作原理包括以下几个关键组件:
Binder 驱动:Binder 的核心是一个位于内核空间的 Binder 驱动程序(
/dev/binder
),负责管理和协调进程之间的数据传递、身份验证和资源管理。Binder 线程池:每个进程中负责处理 IPC 请求的服务端都会维护一个 Binder 线程池,这些线程负责处理来自客户端的请求。Binder 线程池的大小是动态调整的,当有新的请求时,线程池会处理该请求。
Binder 引用(Binder Reference):在 Binder 通信中,客户端和服务端通过
Binder
对象进行交互。客户端持有服务端的一个Binder
引用,实际的通信是通过引用传递的。Binder 代理(Proxy):在客户端进程中,Binder 通信通过代理对象(
Binder Proxy
)实现。代理对象封装了与服务端通信的细节,客户端通过代理对象调用服务端的方法,实际上是通过Binder
驱动将请求传递给服务端。Binder 通信流程:Binder 使用请求-响应的方式实现通信。当客户端调用代理对象的某个方法时,该调用会封装为一个
Transaction
,通过Binder
驱动传递给服务端。服务端处理完请求后,将结果通过Binder
驱动返回给客户端。
具体的通信流程:
客户端请求:
- 客户端进程通过调用
Binder
代理对象发起请求。代理对象将请求打包成Parcel
,并通过 Binder 驱动发送给服务端进程。
- 客户端进程通过调用
Binder 驱动处理:
Parcel
包含请求的详细信息,包括调用的方法、参数等。Binder 驱动将这些信息传递给服务端的 Binder 线程池。
服务端处理:
- 服务端的某个线程从 Binder 线程池中取出请求并解包
Parcel
,调用相应的业务逻辑进行处理,处理完后将结果打包成Parcel
并通过 Binder 驱动返回给客户端。
- 服务端的某个线程从 Binder 线程池中取出请求并解包
客户端接收结果:
- 客户端进程的代理对象从 Binder 驱动接收到结果并解包,返回给应用程序。
Binder 通信的几个关键概念
Binder 驱动:
Binder 的核心是一个内核态的驱动程序binder
,这个驱动程序负责管理客户端与服务端之间的通信。Binder 驱动程序主要负责以下几件事情:- 管理进程间的
Binder
句柄和引用。 - 负责将客户端请求发送给服务端,并返回结果给客户端。
- 提供跨进程的身份验证。
- 管理进程间的
Parcel:
Parcel
是 Android 中用于序列化和反序列化数据的容器。Binder 在传输数据时,必须将数据打包成Parcel
对象。Parcel
支持基本数据类型(如int
、String
),也支持复杂数据类型(如Parcelable
对象)。Binder 线程池:
每个服务端进程会维护一个Binder
线程池,负责处理来自客户端的 IPC 请求。每当客户端发起请求时,Binder 线程池中的某个线程会处理该请求。线程池的大小动态调整,能够提高服务端处理请求的效率。
Binder 的数据传输过程
数据的序列化和反序列化:Binder 通信中,所有数据通过
Parcel
序列化后进行传输。在客户端,方法调用时,参数会被写入Parcel
,传递给 Binder 驱动。在服务端,Binder 驱动将Parcel
中的数据交给服务端的线程,线程读取Parcel
并执行相应的操作。处理结果也会打包成Parcel
返回给客户端。进程身份验证:Binder 内核驱动提供了进程的身份验证机制。每个通过 Binder 通信的进程都有一个唯一的标识符(UID),Binder 驱动会验证通信双方的身份,确保只有授权的进程才能通信。
Binder 的优缺点
优点:
高效性:Binder 的数据传输通过内核共享内存的方式实现,避免了传统 IPC 机制(如管道、Socket)中数据的多次拷贝,提高了通信效率。
安全性:Binder 通过内核态的身份认证机制,确保通信的安全性。每次进程间通信时,Binder 驱动会验证通信双方的身份,防止未经授权的进程参与通信。
支持一对多通信:Binder 允许多个客户端与一个服务端通信,并且多个进程可以同时访问同一个服务端。
灵活性强:Binder 支持多种数据类型的传输,包括基本数据类型和自定义的
Parcelable
对象,适应性强。
缺点:
实现复杂:Binder 的底层实现非常复杂,涉及到内核态的驱动程序、进程间的通信协议和数据的序列化等。虽然 Android 提供了
AIDL
、Messenger
等高级抽象来简化 Binder 的使用,但直接使用 Binder 的门槛较高。性能开销:尽管 Binder 比传统的 IPC 机制效率高,但序列化和反序列化数据依然会带来一定的性能开销。特别是在传输大量数据时,性能下降明显。
Binder 与传统 Linux IPC 机制的对比
Binder 是 Android 特有的 IPC 机制,与传统的 Linux IPC 机制(如管道、消息队列、共享内存、Socket)相比,它有以下优点:
- 安全性更强:Binder 内置身份验证机制,确保通信的双方是可信任的进程。而传统的 Linux IPC 机制往往缺乏这种验证,容易受到攻击。
- 数据传输更高效:Binder 使用共享内存减少了数据的拷贝次数,提升了传输效率。相比之下,传统 IPC 机制的数据传输通常需要多次拷贝。
- 使用更简单:虽然 Binder 实现复杂,但 Android 提供了
AIDL
、Messenger
等更高层次的 API,简化了开发者的使用体验。而传统 IPC 机制的使用往往需要编写大量的低层代码。
总结
Binder 是 Android 系统中进程间通信的核心机制,提供了高效、安全、灵活的 IPC 能力。通过 Binder,Android 实现了系统服务和应用程序之间的通信,支持应用进程之间的高效数据交换。Binder 的主要优势在于它的高效性、安全性和易用性,尽管实现复杂,但 Android 提供了多种高层抽象(如 AIDL、Messenger 等)来简化开发者的使用。
AMS是如何管理Activity的
在 Android 系统中,AMS
(Activity Manager Service) 是负责管理应用进程、Activity、任务和应用生命周期的核心服务。AMS
在应用启动、Activity 切换、生命周期管理、任务栈管理等方面起着关键作用。它通过与 Binder
机制交互,协调应用进程和系统进程,管理 Activity 的创建、启动、切换、销毁等操作。
1. AMS 的基本概述
ActivityManagerService
是 Android Framework 层的一个核心服务,用于管理应用的四大组件(Activity、Service、BroadcastReceiver、ContentProvider)。它位于 system_server
进程中,负责协调不同应用进程的行为。
- 启动 Activity:管理 Activity 的启动流程。
- 管理 Activity 栈:负责维护和管理 Activity 的任务栈。
- 控制 Activity 生命周期:管理每个 Activity 的生命周期状态(如启动、暂停、恢复、销毁)。
- 管理进程和任务:通过管理任务栈(Task Stack),调度不同的任务和进程。
2. AMS 的核心角色
AMS
通过与 ActivityThread
、WindowManagerService
、PackageManagerService
等系统服务协作,管理 Activity 的启动、生命周期及任务栈。它通过 Binder
IPC 机制与应用进程通信。
- ActivityThread:应用进程的主线程,负责执行应用中的 Activity 生命周期回调,处理
AMS
发来的消息。 - **WindowManagerService (WMS)**:管理窗口的显示,
AMS
与WMS
协作来完成 Activity 的界面显示和切换。 - **PackageManagerService (PMS)**:管理应用的安装、卸载和相关信息,
AMS
通过它来检查应用的合法性。
3. AMS 如何管理 Activity
AMS
在管理 Activity 时,主要负责以下几个方面:
1. 启动 Activity
Activity 的启动是一个复杂的过程,AMS
是启动流程的核心。整个启动过程大致可以分为以下几步:
应用请求启动 Activity:
应用通过startActivity()
方法向ActivityManagerService
请求启动一个 Activity,这个请求最终会通过Binder
机制到达AMS
。1
2
3// 应用调用 startActivity()
Intent intent = new Intent(this, TargetActivity.class);
startActivity(intent);AMS 接收启动请求:
AMS
收到startActivity()
请求后,会检查请求的合法性(例如是否具有相应的权限),然后通过startActivityAsUser()
方法处理启动请求。1
2
3
4public final int startActivity(IApplicationThread caller, String callingPackage,
Intent intent, String resolvedType, ...) {
return mActivityStarter.startActivity(caller, intent, ...);
}创建或调度进程:
如果目标Activity
所在的应用进程已经存在,AMS
会将启动请求发送给对应的进程;如果进程不存在,AMS
会通过Zygote
启动一个新的进程。通过 Binder 通知 ActivityThread 启动 Activity:
AMS
通过ApplicationThreadProxy
(应用进程的 Binder 接口)向应用的ActivityThread
发送启动 Activity 的请求。ActivityThread 创建 Activity 并启动:
应用进程的ActivityThread
收到请求后,会调用Instrumentation
创建目标 Activity,并执行 Activity 的生命周期方法(如onCreate()
、onStart()
、onResume()
)。1
2
3
4public void handleLaunchActivity(ActivityClientRecord r, Intent customIntent, String reason) {
Activity activity = performLaunchActivity(r, customIntent);
activity.performResume();
}
2. Activity 的生命周期管理
AMS
负责管理每个 Activity 的生命周期状态。当应用的 Activity 切换、暂停或销毁时,AMS
会调度相应的生命周期方法。
启动生命周期:
在 Activity 启动时,AMS
会调度应用进程的ActivityThread
调用 Activity 的onCreate()
、onStart()
、onResume()
方法。暂停和恢复生命周期:
当用户切换到另一个 Activity 时,AMS
会调用当前 Activity 的onPause()
方法。随后,它会启动新的 Activity,并调用其onResume()
方法。销毁生命周期:
当AMS
检测到某个 Activity 不再需要时(如用户离开应用或内存不足时),它会调度ActivityThread
调用 Activity 的onDestroy()
方法,销毁该 Activity。
3. 任务栈管理
AMS
使用 任务栈(Task Stack)来管理 Activity。每个任务栈代表一个任务,栈中的每个元素是一个 Activity
实例。任务栈以先进后出的方式管理 Activity。
- 前台任务栈:
AMS
通过任务栈的堆栈结构管理用户当前正在使用的应用(即前台任务),栈顶的 Activity 是用户当前正在交互的 Activity。 - 后台任务栈:不在前台显示的 Activity 会被放置到后台任务栈,
AMS
可以通过回收内存来释放这些后台 Activity 的资源。
每次 AMS
启动新的 Activity 时,都会将其压入栈中,当用户按下返回键时,会从任务栈中弹出顶层的 Activity 并销毁它。
4. 进程管理
AMS
还负责管理应用的进程,包括:
- 进程启动:当某个应用首次启动时,如果该应用的进程还未运行,
AMS
会通过Zygote
启动一个新的应用进程。 - 进程优先级调整:根据进程中 Activity 的状态,
AMS
会调整应用进程的优先级。例如,前台进程优先级高,而后台进程优先级低。 - 内存回收:当系统内存不足时,
AMS
会终止优先级较低的后台进程来回收内存。
5. 处理 Activity 异常
当某个 Activity 出现异常(如 ANR 或崩溃)时,AMS
负责检测并处理这些异常。
- ANR(Application Not Responding):当某个 Activity 在主线程中执行时间过长且未响应时,
AMS
会触发 ANR 对话框,并允许用户选择强制关闭应用。 - 崩溃处理:当应用进程崩溃时,
AMS
会记录崩溃信息,并销毁相关的 Activity 或服务。
4. AMS 与 WMS 协同工作
AMS
与 WindowManagerService
(WMS)协同管理 Activity 的界面显示和窗口管理。当一个 Activity 被启动时,AMS
会通知 WMS
创建一个窗口(Window
),并将 Activity 的视图显示在该窗口中。
1. 窗口创建:
当 AMS
启动一个 Activity 时,会通过 Binder
向 WMS
发送请求,要求它创建一个窗口用于显示该 Activity 的界面。
2. 窗口显示:
WMS
接收到 AMS
的请求后,会创建一个窗口,并将该窗口的句柄返回给 Activity。Activity 通过 setContentView()
设置自己的布局后,窗口就会被渲染在屏幕上。
3. 窗口切换:
当用户在不同 Activity 之间切换时,WMS
会负责管理窗口的隐藏和显示。AMS
通知 WMS
当前 Activity 的显示或隐藏,WMS
相应地更新界面显示。
5. AMS 的任务栈和任务栈管理
AMS
中的任务栈(Task Stack)用来管理不同应用和 Activity 的执行顺序。任务栈是一个栈结构,栈顶的 Activity 是当前正在运行的 Activity。当用户启动新任务时,AMS
会创建一个新的任务栈或者向现有的任务栈中添加 Activity。任务栈的管理方式包括:
- 启动模式:Activity 的启动模式(如
standard
、singleTop
、singleTask
、singleInstance
)决定了 Activity 如何被添加到任务栈中。 - 返回栈:当用户按下返回键时,当前任务栈中的 Activity 会被依次弹出,并销毁。
总结
ActivityManagerService
是 Android 系统中负责管理 Activity、应用进程和任务栈的核心组件。它通过与 ActivityThread
、WindowManagerService
、PackageManagerService
等其他系统服务协作,管理 Activity 的启动、生命周期、任务栈以及异常处理。AMS
在整个 Android 系统架构中起着至关重要的作用,它确保了应用程序能够按照
正确的生命周期顺序进行操作,同时有效地管理系统资源和进程调度。
ActivityThread工作原理
ActivityThread
是 Android 中的一个关键类,它位于应用程序的主线程(UI 线程)中,负责管理应用的生命周期以及处理系统和应用之间的通信。ActivityThread
的主要职责是与 AMS
(ActivityManagerService)进行交互,处理来自 AMS
的消息并调度 Activity、Service、BroadcastReceiver 等组件的生命周期方法。它可以被看作是应用程序主线程的控制中心。
1. ActivityThread 的基本概述
ActivityThread
是 Android 应用进程中的核心类。它负责启动和管理应用程序的主线程,并调度执行 Activity
、Service
、BroadcastReceiver
等组件的生命周期回调。同时,它通过 Handler
机制与系统服务(如 AMS
)进行通信,将来自系统的消息调度到应用层的相应组件。
在 Android 系统启动应用程序时,ActivityThread
类会启动主线程,创建主 Looper
和 MessageQueue
,并通过消息驱动应用的各个组件执行。
2. ActivityThread 的核心组件
- **
Looper
和MessageQueue
**:ActivityThread
运行在应用程序的主线程中,主线程的执行是基于消息循环机制的。ActivityThread
会创建一个Looper
和一个MessageQueue
,用于处理来自系统和应用内部的消息。 - **
Handler
**:ActivityThread
使用Handler
来分发和处理消息。每当系统通过Binder
向应用发送请求(如启动一个Activity
),这些请求都会被封装成消息,通过Handler
分发到主线程处理。 - **
ApplicationThread
**:这是ActivityThread
与AMS
交互的 Binder 接口。AMS
通过ApplicationThread
发送消息给应用进程,ApplicationThread
作为ActivityThread
的内部类,充当应用程序和系统服务之间的桥梁。 - **
ActivityClientRecord
**:ActivityThread
中使用ActivityClientRecord
来记录每个 Activity 的状态,包括它的生命周期状态、相关的Intent
、Token
等。
3. ActivityThread 的工作流程
1. 启动流程
ActivityThread
是在应用启动时由 Zygote 进程创建的。当用户启动应用时,Zygote
进程 fork 出一个新的进程,并在新进程中启动 ActivityThread
的主线程。启动流程可以概括如下:
Zygote 启动应用进程:
当应用被启动时,ActivityManagerService
通过Zygote
创建一个新的应用进程。Zygote
负责 fork 出新的进程,并调用ActivityThread.main()
方法来启动应用。1
2
3
4
5
6public static void main(String[] args) {
Looper.prepareMainLooper();
ActivityThread thread = new ActivityThread();
thread.attach(false); // 与 AMS 绑定
Looper.loop(); // 开启消息循环
}创建
ActivityThread
实例:main()
方法中会创建一个ActivityThread
实例,并通过attach()
方法与ActivityManagerService
进行绑定,告知AMS
该进程已经启动。**绑定
ApplicationThread
**:ActivityThread.attach()
会通过ApplicationThread
将该进程与AMS
绑定,AMS
会通过 Binder 机制向该进程发送生命周期相关的消息。启动消息循环:
ActivityThread.main()
调用了Looper.loop()
,从此开始应用进程的消息循环。应用进程会不断从MessageQueue
中读取消息,并通过Handler
处理这些消息。
2. 消息处理机制
ActivityThread
通过 Handler
来处理从 AMS
和其他系统服务发送来的消息,这些消息主要涉及 Activity
、Service
、BroadcastReceiver
的启动和生命周期管理。
Handler 处理消息:
ActivityThread
使用H
类(继承自Handler
)处理不同类型的消息。每个消息会通过H.handleMessage()
方法分发,H
类会根据消息类型调用对应的处理方法。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21class H extends Handler {
public static final int LAUNCH_ACTIVITY = 100;
public static final int PAUSE_ACTIVITY = 101;
public static final int STOP_ACTIVITY = 102;
public void handleMessage(Message msg) {
switch (msg.what) {
case LAUNCH_ACTIVITY:
handleLaunchActivity((ActivityClientRecord) msg.obj);
break;
case PAUSE_ACTIVITY:
handlePauseActivity((IBinder) msg.obj);
break;
case STOP_ACTIVITY:
handleStopActivity((IBinder) msg.obj);
break;
// 其他消息处理...
}
}
}生命周期消息处理:
当应用需要启动一个Activity
时,AMS
会通过ApplicationThread
将启动请求发送给应用进程。ActivityThread
收到消息后,会调用对应的生命周期方法(如handleLaunchActivity()
),执行 Activity 的创建、启动和显示。
3. Activity 启动过程
当应用需要启动一个新的 Activity
时,ActivityThread
负责执行该 Activity
的启动过程。具体步骤如下:
收到启动 Activity 的消息:
ActivityThread
收到AMS
通过ApplicationThread
发来的LAUNCH_ACTIVITY
消息。创建 Activity 实例:
ActivityThread.handleLaunchActivity()
方法首先会调用Instrumentation.newActivity()
来创建Activity
实例。Instrumentation
是一个用于监控应用组件生命周期的类,负责实际的Activity
创建和生命周期管理。调用 Activity 生命周期:
在ActivityThread.performLaunchActivity()
中,Activity
实例被创建后,Activity
的attach()
方法会被调用,完成与窗口和上下文的绑定。接着,会调用Activity
的onCreate()
方法,开始执行 Activity 的初始化逻辑。显示 Activity:
Activity
创建完成后,ActivityThread
会通过WindowManager
将Activity
的界面显示到屏幕上。
4. Activity 生命周期管理
ActivityThread
负责管理 Activity
的生命周期,当应用的 Activity
状态发生变化时,AMS
会通过 ApplicationThread
将相应的生命周期事件通知 ActivityThread
,例如暂停、停止、销毁等操作。
暂停 Activity:
当用户切换到其他Activity
或应用时,AMS
会通知ActivityThread
调用Activity
的onPause()
方法来暂停当前Activity
。停止 Activity:
当AMS
认为某个Activity
不再需要显示时,会通知ActivityThread
调用Activity
的onStop()
方法,停止该Activity
。销毁 Activity:
当某个Activity
被销毁时,AMS
会发送销毁命令给ActivityThread
,ActivityThread
调用Activity
的onDestroy()
方法,销毁该Activity
,释放资源。
4. 与其他系统服务的协作
ActivityThread
通过 ApplicationThread
与 ActivityManagerService
(AMS) 进行通信,协调管理 Activity
、Service
和其他组件的生命周期。
与
AMS
的交互:ActivityThread
通过ApplicationThread
作为客户端接口与AMS
进行交互。AMS
负责调度应用进程的生命周期事件,ActivityThread
接收到这些事件后,通知应用中的相应组件执行对应的生命周期方法。与
WMS
的交互:ActivityThread
通过WindowManager
与WindowManagerService
(WMS) 进行交互,管理应用界面的窗口显示和调整。每次启动新的Activity
时,ActivityThread
都会请求WMS
创建一个新的窗口来显示Activity
的内容。
5. ActivityThread 的主要方法
- **
main()
**:应用进程的入口,负责启动ActivityThread
,并初始化Looper
和消息队列。 - **
attach()
**:将应用进程与AMS
进行绑定,建立进程间通信的通道。 - **
handleLaunchActivity()
**:处理Activity
的启动流程,包括创建Activity
实例、调用生命周期方法等。 - **
performLaunchActivity()
**:执行Activity
的启动,包括调用onCreate()
、onStart()
等生命周期方法。 - **
handlePauseActivity()
**:处理Activity
的暂停操作,调用onPause()
方法。 - **
handleStopActivity()
**:处理Activity
的停止操作,调用onStop()
方法。 - **
handleDestroyActivity()
**:处理Activity
的销毁操作,调用onDestroy()
方法。
6. 总结
ActivityThread
是 Android
应用进程中的核心类,负责管理 Activity
、Service
、BroadcastReceiver
的生命周期和 UI 线程的消息循环。通过与 AMS
的交互,它能够有效地调度应用的生命周期回调,并处理用户界面的显示和切换。ActivityThread
利用 Handler
机制,在主线程中调度和处理系统消息,保证了应用的正常运行。
内存抖动是什么
内存抖动(Memory Churn 或 Memory Thrashing)指的是程序在短时间内频繁地进行内存的分配和释放,导致大量的临时对象被频繁创建和销毁,进而导致垃圾回收器(GC)频繁运行,从而影响程序的性能。这种现象通常会导致应用程序出现性能下降、卡顿等问题。
内存抖动的表现
- 频繁分配和释放内存:程序中有大量的临时对象被创建,但这些对象很快就不再被使用,导致它们很快被垃圾回收。
- 垃圾回收频率增加:由于频繁创建和销毁对象,垃圾回收器需要频繁运行来回收不再使用的内存,GC 的频繁运行会导致程序暂停,从而影响性能。
- CPU 使用率增高:频繁的内存分配和回收会增加 CPU 的负担,导致 CPU 占用率增加。
- 应用卡顿或掉帧:在 Android 或其他 UI 密集型应用中,内存抖动会导致 UI 卡顿或者掉帧,影响用户体验。
内存抖动的常见原因
- 频繁创建短生命周期的对象:某些情况下,程序中会频繁创建一些生命周期很短的对象,比如循环中每次迭代都会创建新的对象,而这些对象会很快变为垃圾。
- 集合类的使用不当:在集合类(如
ArrayList
、HashMap
等)中频繁添加和删除元素,可能会导致大量对象的分配和释放,进而引发内存抖动。 - 字符串拼接:频繁进行字符串拼接会创建大量的临时字符串对象,特别是在 Java 中,字符串是不可变的,拼接操作会不断创建新的对象。
- 重复创建对象而不是重用:在某些场景下,程序会重复创建一些对象而不是复用它们,这会导致不必要的内存分配。
如何避免内存抖动
避免不必要的对象创建:尽量避免在短时间内频繁创建临时对象,可以考虑对象池技术来重用对象。
使用更高效的集合类:根据需求选择合适的集合类,例如,在频繁访问或修改时,可以使用带有优化策略的集合类来减少内存开销。
优化字符串操作:在频繁拼接字符串时,使用
StringBuilder
或StringBuffer
来替代直接的字符串拼接操作,减少不必要的对象分配。避免过度依赖自动装箱/拆箱:自动装箱(auto-boxing)和拆箱(auto-unboxing)会创建额外的对象,特别是在频繁操作基本数据类型时,应该尽量使用基本数据类型而不是包装类。
优化算法:检查程序中的算法是否有优化的空间,避免不必要的对象创建和销毁。
内存抖动对性能的影响
内存抖动可能导致应用程序的响应速度变慢,尤其是对于实时性要求较高的应用,如游戏或多媒体应用。频繁的垃圾回收不仅会消耗系统资源,还会导致程序在 GC 过程中暂停,造成应用的卡顿现象。
内存抖动的检测
在 Android 和 Java 应用中,开发者可以使用各种性能分析工具来检测内存抖动,例如:
- Android Studio Profiler:可以帮助开发者监控内存分配情况,查看对象的创建和销毁频率,找到潜在的内存抖动问题。
- **MAT (Memory Analyzer Tool)**:可以用于分析 Java 应用的堆内存,检查内存使用情况,找到潜在的内存抖动源。
通过这些工具,开发者可以识别程序中频繁创建的对象,优化代码以减少内存抖动。
Android系统启动流程是什么
Android系统的启动流程可以分为以下几个主要阶段,从设备加电到启动应用的过程如下:
1. Bootloader阶段
- 加电:当设备加电时,系统从固件(Bootloader)开始启动。Bootloader负责初始化硬件和加载内核,它是系统启动的第一步。
- Bootloader初始化:Bootloader会进行一些硬件初始化工作,例如内存、处理器、时钟等硬件资源的配置。然后,它会查找操作系统内核并将其加载到内存中。
- 启动内核:一旦内核被加载,Bootloader会将控制权交给内核,开始执行内核代码。
2. Linux内核启动阶段
- 内核初始化:Android系统基于Linux内核。内核启动时,会初始化系统的核心组件,包括内存管理、进程管理、文件系统、网络等。在这一步中,内核会设置设备树(Device Tree)并识别设备的硬件信息。
- 启动init进程:内核完成基本的初始化之后,会启动第一个用户空间进程
init
。init
是 Android 系统的第一个用户态进程,进程号为 1。
3. Init进程启动阶段
- 解析init.rc文件:
init
进程会根据init.rc
文件的配置启动系统的服务和进程。init.rc
文件定义了系统的服务、文件系统挂载、属性设置等重要配置。 - 启动Zygote进程:
init
进程会启动 Zygote 进程,Zygote 是 Android 系统中非常重要的一个进程,它负责初始化 Java 虚拟机(JVM)并加载核心的 Android 类库。几乎所有的应用进程都是由 Zygote 派生出来的。
4. Zygote和System Server启动阶段
- 启动Zygote:Zygote 进程启动后,它会预加载一些系统常用的类和资源,并开始监听特定的 socket,等待启动新的应用进程。
- 启动System Server:Zygote 还会启动一个非常重要的进程——
System Server
,它负责启动并管理 Android 的系统服务。System Server 会启动一些关键的系统服务,如 Activity Manager、Package Manager、Window Manager 等。
5. 启动Android Runtime (ART)
- ART初始化:Zygote 启动后,Android Runtime(ART)环境会被初始化。ART 是 Android 用来执行和管理应用的运行时环境,负责字节码的转换和执行。
6. 启动Launcher和应用进程
- 启动Launcher:在系统服务启动完成后,
Activity Manager
会启动 Launcher 应用,这是用户界面的主屏幕,也是用户启动其他应用的入口。 - 启动应用进程:当用户点击应用时,Launcher 会向 Zygote 发起请求,Zygote 会 fork 一个新的进程,生成应用进程。在这个新的进程中,ART 会加载应用的类并执行。
总结:
- Bootloader:初始化硬件,加载内核。
- 内核:初始化系统资源,启动 init 进程。
- Init进程:解析配置,启动 Zygote 和 System Server。
- Zygote:启动 Android Runtime (ART) 和 System Server。
- Launcher:系统界面显示,用户可以启动应用。
每一步都承担着不同的角色,最终目的是让 Android 系统能够顺利地加载并运行用户的应用。
App启动流程
Android应用程序(App)的启动过程可以分为多个阶段,从用户点击应用图标到应用界面显示的过程涉及多个组件。以下是Android应用启动流程的详细说明:
1. 用户点击应用图标
当用户点击应用图标时,Launcher(启动器)应用会发起启动应用的请求。这个请求会通过 Activity Manager 传递到系统层,进入应用启动流程。
2. Launcher请求启动应用
- 向Activity Manager发送Intent:当用户点击应用图标后,Launcher 向
ActivityManagerService
(AMS) 发送一个带有应用启动信息的Intent
。这个Intent
通常包含应用包名以及要启动的Activity
信息。 - Activity Manager 检查应用状态:AMS 检查该应用是否已经运行。如果应用已经在后台运行,则直接将应用切换到前台;如果应用还没有启动,AMS 将会启动一个新的应用进程。
3. Zygote进程派生应用进程
- 启动新的应用进程:如果应用还没有进程在运行,AMS 会通过与
Zygote
进程通信,请求 Zygote fork(派生)一个新的进程。这是因为 Android 的所有应用进程都是由 Zygote fork 出来的,这样可以共享系统的类库和资源,减少启动时间和内存占用。 - 创建新进程:Zygote fork 新进程后,新进程会通过反射机制,启动应用的入口类(通常是
ActivityThread
)。
4. ActivityThread启动
- ActivityThread的启动:新应用进程启动后,Zygote fork 出来的新进程会调用
ActivityThread
类的main()
方法,开始初始化应用的主线程。ActivityThread
是应用进程的主类,它负责管理应用的主线程、处理 UI 操作、管理 Activity 的生命周期等。 - 建立主线程Looper:
ActivityThread
初始化时,会创建一个Looper
,它是 Android 的消息循环机制,主线程中的所有消息都会在这个 Looper 中处理。
5. Activity的启动
- AMS通知启动Activity:AMS 在新进程准备好后,会通过
Binder
通信机制通知应用进程,要求启动具体的Activity
。AMS 会调用ApplicationThread
的scheduleLaunchActivity()
方法来完成通知工作。 - 创建Activity实例:
ActivityThread
收到启动Activity
的消息后,会调用performLaunchActivity()
方法,创建目标Activity
的实例,并调用它的onCreate()
方法。
6. Activity生命周期方法调用
- **onCreate()**:
Activity
的实例创建后,系统会首先调用onCreate()
方法。这个方法是Activity
生命周期的第一步,通常在这里完成界面布局的初始化、数据的加载以及组件的绑定等工作。 - onStart() 和 **onResume()**:
onCreate()
方法执行完毕后,Activity
的onStart()
和onResume()
方法也会被依次调用。在onResume()
中,Activity
将进入前台,并与用户开始交互。
7. View的渲染
- View的绘制:在
onCreate()
方法中,Activity
通常会调用setContentView()
方法加载布局资源(XML文件),创建界面上的视图层次结构。这个过程通过LayoutInflater
将 XML 布局文件解析为相应的视图对象。 - SurfaceFlinger:当
Activity
的视图层次结构完成后,系统会通过WindowManager
将这些视图提交给底层的SurfaceFlinger
进行显示。SurfaceFlinger
是 Android 的图形渲染引擎,它负责将所有应用窗口的图像合成并显示在屏幕上。
8. 用户界面显示
- 最终显示:经过前面一系列的初始化和渲染过程,
Activity
的界面会最终呈现给用户,此时应用进入可交互状态,用户可以开始操作应用的界面。
总结:
- 用户点击图标:Launcher 向 AMS 发送启动请求。
- AMS 检查应用状态:决定是切换到前台还是启动新的进程。
- Zygote fork 新进程:通过 Zygote fork 创建应用进程。
- ActivityThread 初始化:启动主线程,准备消息循环。
- 创建Activity:AMS 通知应用启动
Activity
,调用生命周期方法。 - 界面渲染:加载布局,View 被绘制,最终显示到屏幕。
这个流程确保了 Android 应用的快速启动,同时保证了内存和资源的有效利用。
WMS是如何管理Window的
在Android系统中,WindowManagerService (WMS) 是一个非常重要的系统服务,负责管理所有应用和系统窗口的显示。它控制着应用窗口的创建、布局、显示、和删除等操作。以下是WMS管理Window的主要过程和机制:
1. 窗口的分类
在Android中,窗口主要有以下几种类型:
- 应用窗口(Application Window):由普通应用创建的窗口,通常是由
Activity
通过setContentView()
加载布局来创建。 - 子窗口(Sub-Window):依附于主窗口的窗口,比如
Dialog
。 - 系统窗口(System Window):系统级别的窗口,比如状态栏、导航栏、输入法窗口等。
2. 窗口的创建过程
应用向WMS请求创建窗口:当应用启动并创建
Activity
时,Activity
通过WindowManager
来向WindowManagerService
(WMS) 请求创建一个窗口。这个请求实际上是通过WindowManagerGlobal
的addView()
方法提交的。WMS 接收并处理请求:
WindowManagerGlobal
会通过Binder机制与WindowManagerService
通信。WMS接收到创建窗口的请求后,验证窗口类型、权限等信息,接着为窗口分配一个WindowToken
,并把窗口信息加入到WMS的管理列表中。Surface的创建:
WMS
在接受窗口请求后,会为窗口创建一个对应的Surface
,这是实际用于绘制内容的地方。Surface
是通过SurfaceFlinger
来管理的,它负责最终的窗口显示和图形合成。窗口的添加和排列:WMS会将新的窗口添加到系统中的窗口列表中,并根据窗口类型、层级和Z-order(Z轴的顺序)来确定窗口的显示顺序。例如,状态栏窗口永远在最顶层,应用窗口在状态栏下面。
3. 窗口的管理和布局
布局计算:WMS负责计算每个窗口在屏幕上的位置和大小。当窗口被添加或窗口尺寸发生变化时,WMS会重新计算窗口的布局。布局计算包括窗口的坐标、大小、显示的层级等。
使用
WindowState
记录窗口状态:每个窗口在WMS中都有一个对应的WindowState
对象。这个对象存储了窗口的各种状态信息,比如位置、大小、是否可见、是否需要重新绘制等。WMS通过维护所有WindowState
对象来管理系统中的所有窗口。窗口层级的管理:Android窗口的层级管理基于Z-order,即不同类型的窗口有不同的层级。比如应用窗口的层级一般比系统窗口低,而对话框、输入法窗口等可能会显示在应用窗口的上方。WMS会根据窗口的类型和需求来调整窗口的层级顺序。
4. 窗口的绘制和显示
SurfaceFlinger负责窗口合成:WMS只是负责窗口的管理和布局,但实际的窗口绘制和图像合成是由
SurfaceFlinger
完成的。WMS在窗口布局完成后,会将窗口对应的Surface
交给SurfaceFlinger
,由它负责将各个窗口的内容合成为一张最终的图像并显示到屏幕上。刷新和重绘:当窗口的内容发生变化(比如界面更新,用户操作等),WMS会通知相应的
WindowState
,要求它重新绘制。在绘制完成后,SurfaceFlinger
会接收到新的窗口内容,并重新进行图像合成,最终更新到屏幕上。
5. 窗口的焦点管理
焦点窗口:WMS负责管理系统中焦点窗口的分配。焦点窗口是指当前接收用户输入的窗口,通常是用户正在与之交互的应用窗口。WMS会根据窗口的优先级、可见性和类型等条件决定哪个窗口可以获得焦点。
焦点切换:当用户切换应用、弹出对话框或者窗口发生变化时,WMS会处理焦点切换,确保新的焦点窗口能够正确接收到输入事件。
6. 窗口的移除
应用请求关闭窗口:当应用不再需要一个窗口时,通常会调用
WindowManager
的removeView()
方法来移除窗口。这个请求会通过Binder传递给WMS。WMS移除窗口:WMS接收到移除窗口的请求后,会将窗口从内部的管理列表中移除,并释放对应的资源(比如
Surface
和WindowToken
)。如果这个窗口是焦点窗口,WMS还会重新分配焦点给其他窗口。
7. 窗口动画
- 窗口的动画处理:WMS还负责处理窗口的显示、隐藏、缩放等动画效果。当窗口被添加、移除或改变大小时,WMS会触发窗口动画,以提供更平滑的用户体验。这些动画通常通过
Surface
的变换来实现,最终由SurfaceFlinger
合成到屏幕上。
8. 输入事件的分发
事件分发的管理:WMS还负责管理输入事件的分发。系统中的输入事件(如触摸、按键)首先会传递给WMS,然后WMS会根据窗口的焦点状态和区域来决定将事件分发给哪个窗口的应用。
确保正确的窗口接收输入:当用户点击屏幕时,WMS会根据点击位置查找对应的窗口,并将点击事件分发给该窗口。如果窗口不在焦点上或被覆盖,事件不会被传递给它。
总结:
WMS 作为 Android 系统中的核心服务,主要通过以下几方面管理窗口:
- 窗口创建与销毁:通过接收应用或系统请求创建、添加或移除窗口。
- 窗口布局和层级管理:计算窗口的位置、大小,并确定它的显示顺序。
- 窗口显示和绘制:通过与
SurfaceFlinger
协作来完成窗口内容的渲染。 - 焦点管理:决定哪个窗口可以接收用户输入,并处理焦点的切换。
- 事件分发:管理输入事件的分发,确保用户操作能够正确传递到窗口。
通过这些机制,WMS确保了Android系统中多窗口的正常运行、显示和交互。
RxJava的实现原理
RxJava概述
RxJava 是一个基于 ReactiveX 的响应式编程库,它将异步和事件驱动编程抽象为基于流的操作,使用 Observable、Observer、Scheduler 和其他核心组件来管理数据流和事件流。它能够让程序员以更简洁的方式处理异步操作,避免回调地狱,提升代码的可读性和维护性。
RxJava 的核心是 Observable
,它发射一系列的数据,Observer
订阅 Observable
,以响应 Observable
发射的每个数据项。通过运算符,开发者可以轻松地对数据流进行转换、过滤、合并等操作。
RxJava的核心组件
- Observable:数据源,它发射一系列数据,
Observable
可以是有限的或无限的。 - Observer:观察者,接收由
Observable
发射的每一个数据。 - Scheduler:调度器,用于指定在哪个线程中执行
Observable
和Observer
的工作。
RxJava 的实现基于以下关键思想:
1. Observable和Observer模式
RxJava 的基本机制是 观察者模式,Observable
是可观察的对象,而 Observer
是观察者。当 Observable
发射数据时,它会通知所有订阅它的 Observer
,Observer
会做出相应的反应。基本流程如下:
Observable
发射数据。Observer
订阅Observable
,并通过onNext()
接收数据,onError()
处理错误,onComplete()
处理完成事件。
2. 链式操作符和装饰者模式
RxJava 中的操作符(如 map()
、flatMap()
、filter()
等)通常用于转换或过滤数据流。这些操作符通过链式调用组合在一起,每个操作符会返回一个新的 Observable
,但并不会立刻执行,直到有 Observer
订阅了链条上的 Observable
。
内部实现使用了 装饰者模式。每个操作符实际上都是在现有的 Observable
上添加一层装饰,创建一个新的 Observable
,通过这种方式逐步处理数据流。
1 | Observable.just(1, 2, 3) |
在上面的代码中,每个操作符创建一个新的 Observable
实例,直到调用 subscribe()
时,才会执行整个链条,Observable
会发射转换后的数据给订阅者。
3. Schedulers和线程控制
RxJava 中引入了 Schedulers 来管理多线程操作,它提供了灵活的方式去控制代码的执行线程。RxJava 提供了几种常见的 Scheduler
:
- **Schedulers.io()**:用于I/O操作,如网络请求、文件操作等,背后是一个线程池。
- **Schedulers.computation()**:用于计算密集型工作,背后也是线程池。
- **Schedulers.newThread()**:每次都会创建一个新线程。
- **AndroidSchedulers.mainThread()**:在 Android 环境下,确保代码在主线程执行。
例如,通过 subscribeOn()
和 observeOn()
操作符,可以指定 Observable
的数据发射线程和 Observer
的数据处理线程。
1 | Observable.just(1, 2, 3) |
subscribeOn(Schedulers.io())
:指定Observable
在 I/O 线程中运行。observeOn(AndroidSchedulers.mainThread())
:指定Observer
在 Android 主线程中运行,确保 UI 更新在主线程进行。
背压问题及其解决方案
1. 什么是背压(Backpressure)?
背压 问题是指在异步流处理中,数据生产者(Observable
)的生产速度快于数据消费者(Observer
)的消费速度,导致事件堆积,内存压力增大,甚至可能导致系统崩溃。
RxJava 中的 Observable
是无限制地发射数据的,特别是当使用 Observable.interval()
或 Observable.fromIterable()
时,可能会快速产生大量数据。如果 Observer
处理速度较慢,而 Observable
发射数据过快,系统的缓冲区可能会被填满,造成背压问题。
2. 背压的应对方案
为了解决背压问题,RxJava 提供了两种类型的 Observable
来处理背压:Observable
和 Flowable
。
- Observable:不支持背压处理,如果使用它而生产速度过快会导致数据丢失或缓冲区溢出。
- Flowable:支持背压处理,可以根据消费能力控制数据的发射速率。
Flowable
是基于Reactive Streams
规范实现的,允许消费者向生产者请求一定数量的数据,从而避免生产者发射过多数据。
3. Flowable 背压模式
Flowable
支持多种背压策略,以应对不同的背压场景:
BUFFER:无限制缓冲所有未被处理的数据,直到内存耗尽。用于数据量较小时的情况。
1
2
3Flowable.just(1, 2, 3)
.onBackpressureBuffer()
.subscribe(System.out::println);DROP:当消费者来不及处理时,丢弃后续产生的数据,直到消费者可以处理为止。
1
2
3Flowable.just(1, 2, 3)
.onBackpressureDrop()
.subscribe(System.out::println);LATEST:只保留最新的一条数据,当消费者准备好时,只接收最后发射的数据,其他数据会被丢弃。
1
2
3Flowable.just(1, 2, 3)
.onBackpressureLatest()
.subscribe(System.out::println);ERROR:当出现背压问题时抛出
MissingBackpressureException
,这是最严格的模式,确保程序员能明确知道背压问题。1
2
3Flowable.just(1, 2, 3)
.onBackpressureError()
.subscribe(System.out::println);
4. Flowable请求数量管理
Flowable
使用了一种 请求机制 来解决背压问题,消费者可以通过 request()
方法明确告诉生产者需要多少数据。
1 | Flowable<Integer> flowable = Flowable.range(1, 1000) |
在这个例子中,Subscriber
通过 s.request(10)
向 Flowable
请求10个数据,Flowable
会根据请求数量控制发射速率,从而避免背压问题。
总结
RxJava 实现原理:基于观察者模式,使用
Observable
发射数据,Observer
订阅并处理数据,操作符通过链式调用实现数据流的转换和处理。Scheduler
用于管理线程。背压问题:当数据生产者发射数据的速度快于消费者处理数据的速度时,会产生背压问题,可能导致内存溢出或数据丢失。
背压解决方案:RxJava 提供了
Flowable
以及多种背压策略(BUFFER、DROP、LATEST、ERROR)来应对生产和消费速度不匹配的问题。
map 和 flatMap
RxJava
是一种基于异步编程和事件驱动的响应式编程框架,广泛用于 Android 和 Java 开发中。在 RxJava
中,map
和 flatMap
是两个常见的操作符,用于对数据流进行转换,但它们的用途和功能略有不同。
1. map
操作符
map
操作符用于将一个类型的流数据转换为另一种类型的流数据。它是一个一对一的转换操作符,即对于每个输入数据,map
生成一个对应的输出数据。
工作原理:
map
操作符接收一个Function<T, R>
函数,该函数接受一个类型T
的输入,返回一个类型R
的输出。- 每当源
Observable
发出一个数据项时,map
就会应用这个函数,将数据项转换为新的数据类型,然后将转换后的数据项发射给下游。
示例:
1 | Observable<Integer> observable = Observable.just(1, 2, 3, 4); |
输出结果:
1 | Number 1 |
- 这里,
map
将整数转换为字符串,但依旧保持数据项的一对一对应关系。
2. flatMap
操作符
flatMap
操作符与 map
类似,也用于将数据转换为另一种形式。但 flatMap
不再是一对一的转换,它可以将每个输入的元素转换为一个 Observable
,并将所有这些 Observable
合并成一个单一的 Observable
流。
工作原理:
flatMap
接收一个Function<T, ObservableSource<R>>
函数,这个函数将输入的类型T
转换为一个新的ObservableSource<R>
。flatMap
生成的多个Observable
会被“扁平化”(flatten)成单一的流,所有这些Observable
的数据会被合并并发射给下游。
示例:
1 | Observable<Integer> observable = Observable.just(1, 2, 3, 4); |
输出结果:
1 | Number 1 |
- 在这个例子中,
flatMap
将每个整数转换为两个字符串的Observable
,并将它们扁平化合并为一个单一的流。
3. 区别总结
转换关系:
map
是一对一的转换,每个源数据只生成一个输出数据。flatMap
是一对多或多对多的转换,每个源数据可以生成多个输出数据,并将这些数据合并成单一的流。
输出类型:
map
将输入数据转换为单一的其他类型数据。flatMap
将输入数据转换为Observable
,然后合并多个Observable
的输出。
合并方式:
map
不会涉及合并,直接一对一地转换。flatMap
将多个Observable
合并为一个Observable
,且通常是无序的。
使用场景:
map
适用于简单的单一转换,比如数据格式的转换。flatMap
适用于嵌套的异步操作,比如当你需要根据一个结果发起一个新的异步请求,并将所有的请求结果合并。
4. flatMap
和 concatMap
的对比
flatMap
发出的Observable
是无序的,数据项的顺序可能会打乱。- 如果你需要保持发射顺序,可以使用
concatMap
,它会按顺序将每个Observable
合并。
concatMap
示例:
1 | Observable<Integer> observable = Observable.just(1, 2, 3, 4); |
输出结果:
1 | Number 1 |
在这里,concatMap
保持了发射的顺序,而 flatMap
则可能导致顺序不一致。
总结
map
适用于一对一转换,将每个输入数据转换为一个输出数据。flatMap
适用于将输入数据转换为多个数据流,并将这些数据流合并成一个单一的数据流。flatMap
适合处理异步操作,比如网络请求的嵌套调用,而map
则适合简单的类型转换。
zip 操作符
zip
操作符是 RxJava 中一个非常有用的组合操作符,用于将多个 Observable
或 Flowable
发出的数据项进行组合。它按顺序将每个源 Observable
发出的项组合成一个新项,直到其中一个源发出完毕。
工作原理
- 输入:可以接受多个
Observable
。 - 输出:每当所有源
Observable
发出一个新项时,zip
就会将这些项组合成一个新项。 - 顺序:组合项的顺序与输入源的顺序相对应。
- 完成条件:当任意一个源
Observable
完成时,zip
将停止发出项。
示例代码
1 | Observable<Integer> source1 = Observable.just(1, 2, 3); |
输出
1 | 1A |
使用场景
- 当你需要从多个数据源组合数据时,
zip
是一个理想的选择,例如请求多个 API,然后将结果组合在一起。 - 它确保每个组合项都来自各个源
Observable
的同一“轮次”。
注意事项
- 如果输入的
Observable
数量不一样,zip
会在发出最短序列后完成。 - 你可以使用
zip
的重载版本来指定不同的合并函数,处理不同类型的数据。
AspectJ 的实现原理
AspectJ 是 Java 语言的一个扩展,用于实现面向方面编程(AOP,Aspect-Oriented Programming)。AspectJ 的实现主要通过“切面”(Aspect)、“连接点”(Join Point)、“切入点”(Pointcut)和“通知”(Advice)来完成。这些概念使得 AspectJ 可以在不修改源代码的前提下,对已有的代码功能进行扩展或修改。以下是 AspectJ 的主要实现原理:
1. 编译时织入(Compile-Time Weaving)
在 AspectJ 中,织入(Weaving)是指将切面代码插入到主程序代码中的过程。AspectJ 支持多种织入方式,其中最常见的是编译时织入。它的原理是:
- 在编译过程中,AspectJ 编译器(如
ajc
)会分析 Java 代码和 AspectJ 代码,并根据定义的切入点,将相应的切面逻辑插入到匹配的连接点上。 - 最终生成的字节码已经包含了织入的切面逻辑,所以在运行时不需要额外的处理。
编译时织入的优势在于生成的字节码已经包含了切面的逻辑,对运行时性能几乎没有影响。这种方式适用于在开发阶段已经确定好切面代码的场景。
2. 类加载时织入(Load-Time Weaving,LTW)
类加载时织入是在运行时将切面织入到 Java 类中的方法。这种方式在类被加载到 JVM 中时,通过自定义的类加载器(ClassLoader)完成织入。具体实现原理为:
- JVM 加载类时,AspectJ 的 LTW 代理类加载器会拦截类的加载过程。
- 在加载类的字节码时,代理类加载器会将切面代码根据切入点织入到指定位置,生成新的字节码,再交由 JVM 加载。
LTW 的优势在于动态性,可以根据运行时配置来织入切面代码,适合在复杂的应用中使用,例如 Spring 中的 AOP 支持。
3. 运行时代理
AspectJ 在 Spring AOP 中常见的动态代理模式下也可以实现 AOP。Spring AOP 是基于代理的(proxy-based),而 AspectJ 是基于字节码级别的修改。在一些特殊情况下,AspectJ 也可以通过代理模式来实现动态织入,但通常会依赖于 Spring 框架中的代理机制。
4. 连接点、切入点和通知的匹配机制
AspectJ 的核心原理之一是通过切入点表达式来匹配连接点。具体流程如下:
- 连接点(Join Point):程序执行过程中可能插入切面代码的位置,包括方法调用、对象初始化等。
- 切入点(Pointcut):用于定义哪些连接点需要应用通知逻辑。切入点可以使用表达式来匹配方法或类。
- 通知(Advice):具体要在连接点执行的操作代码。在程序运行时,如果某个连接点匹配了切入点的定义,就会触发通知逻辑。
5. 生成字节码并维护 AspectJ 的执行流程
AspectJ 生成的字节码在类的字节码中引入了切面逻辑。通过这个方式,编译后的类字节码会包含 AspectJ 定义的所有切面,并且在运行时确保切面能够在指定的连接点处执行。这种字节码生成和插入逻辑的实现是 AspectJ 的核心部分。
总结
AspectJ 的实现原理主要依靠字节码插桩技术,无论是编译时、类加载时还是运行时,都围绕织入机制展开。它通过定义切入点和通知,将切面逻辑以各种方式织入到目标代码中,增强了代码的模块化和可维护性。
OKHttp的实现原理
OKHttp 是一个高效的、支持 HTTP/2 的网络请求库,它被广泛用于 Android 和 Java 应用中。OKHttp 通过简洁的 API、灵活的连接池管理、对 HTTP 协议的完整支持,提供了强大的网络请求功能。下面我们深入了解 OKHttp 的实现原理及其关键机制。
OKHttp 核心组件和工作流程
OKHttp 的网络请求机制由以下几个核心组件组成:
- OkHttpClient:负责配置和管理网络请求的客户端。
- Request:封装 HTTP 请求的所有信息。
- Response:封装 HTTP 响应的所有信息。
- Call:表示一个具体的 HTTP 请求,
Call
可以被执行或取消。 - Interceptor:拦截器,负责在请求和响应之间做中间处理。
- Dispatcher:负责管理异步请求的调度和执行。
OKHttp 的请求流程通常包括以下几个步骤:
- 构建
OkHttpClient
客户端。 - 构建
Request
对象,指定请求的 URL、请求头和请求参数等。 - 通过
OkHttpClient
的newCall()
方法创建Call
对象。 - 执行
Call
,通过同步或异步方式获取响应Response
。
OKHttp 的工作流程
创建 OkHttpClient
OkHttpClient
是 OKHttp 请求的核心类,负责管理连接池、缓存、拦截器等配置。1
2
3
4
5OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build();connectTimeout
:设置连接超时时间。readTimeout
:设置读取超时时间。writeTimeout
:设置写入超时时间。
创建 Request
Request
对象封装了 HTTP 请求的信息,比如 URL、HTTP 方法、请求头和请求体。1
2
3Request request = new Request.Builder()
.url("https://example.com/api")
.build();创建 Call
Call
对象是一次 HTTP 请求的封装,负责执行网络请求并获取响应。1
Call call = client.newCall(request);
执行 Call
执行请求有两种方式:- 同步请求:
execute()
,调用此方法会阻塞当前线程,直到请求完成。 - 异步请求:
enqueue()
,调用此方法后,OKHttp 会在内部线程池中执行请求,并通过回调函数处理响应。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// 同步请求
Response response = call.execute();
// 异步请求
call.enqueue(new Callback() {
public void onFailure(Call call, IOException e) {
// 请求失败处理
}
public void onResponse(Call call, Response response) throws IOException {
// 请求成功处理
}
});- 同步请求:
OKHttp的实现细节
1. 拦截器(Interceptor)机制
拦截器是 OKHttp 中非常重要的一个概念,它允许在请求和响应的不同阶段进行拦截和处理。OKHttp 内部使用了 责任链模式 来处理网络请求,拦截器就是该模式中的核心环节。
拦截器主要分为两类:
- 应用拦截器(Application Interceptor):用于处理应用层的逻辑,如添加统一的请求头、修改请求或响应等。
- 网络拦截器(Network Interceptor):用于处理底层网络请求,例如修改网络层数据、重新发起请求等。
OKHttp 中内置了多个拦截器,如:
- 重试拦截器(RetryAndFollowUpInterceptor):用于处理失败重试、重定向等操作。
- 桥接拦截器(BridgeInterceptor):负责处理应用层与网络层的桥接,例如设置请求头、响应头、Cookie 等。
- 缓存拦截器(CacheInterceptor):负责缓存逻辑的处理,判断请求是否可以使用缓存。
- 连接拦截器(ConnectInterceptor):负责管理连接池,处理连接的创建与复用。
- 网络拦截器(NetworkInterceptor):用于处理实际的网络 I/O 操作。
拦截器通过链式调用的方式逐层传递请求和响应:
1 | public Response intercept(Chain chain) throws IOException { |
每个拦截器可以在 chain.proceed(request)
之前或之后对请求和响应进行处理。
2. 连接池(Connection Pool)
OKHttp 通过连接池来管理 HTTP 连接的复用,这大大提升了网络请求的性能,减少了不必要的 TCP 连接创建。OKHttp 默认会保持一组连接,当请求完成后,连接不会立即关闭,而是保留在连接池中,供后续请求复用。
- 连接复用:如果多个请求目标相同的主机(域名和端口相同),OKHttp 会复用已有的 TCP 连接,避免每次请求都建立新的连接。
- HTTP/2 支持:OKHttp 支持 HTTP/2 协议,允许在一个 TCP 连接上并行处理多个请求,从而进一步提升网络性能。
1 | OkHttpClient client = new OkHttpClient.Builder() |
连接池的两个关键参数:
- 最大连接数:允许复用的最大连接数。
- 连接空闲时间:当连接超过空闲时间后将被关闭。
3. 缓存机制
OKHttp 提供了完整的 HTTP 缓存机制,遵循 HTTP 的缓存规则(如 Cache-Control
、ETag
等),并允许本地缓存 GET 请求的响应。缓存拦截器通过判断缓存的有效性来决定是使用缓存还是发起新的网络请求。
1 | Cache cache = new Cache(cacheDirectory, cacheSize); |
- 缓存目录:指定缓存文件存储的位置。
- 缓存大小:限制缓存的大小。
OKHttp 的缓存拦截器会根据缓存策略决定是否从缓存中读取数据或更新缓存。
4. 异步处理和线程池
OKHttp 通过内部的 Dispatcher 类管理异步请求的执行。Dispatcher
是一个任务调度器,它使用线程池来执行异步请求,默认最多并行执行 64 个请求,并且对同一主机的请求最多并发 5 个。
1 | OkHttpClient client = new OkHttpClient.Builder() |
- Dispatcher:控制请求的调度,管理队列中正在运行的异步请求以及等待的请求。
- 线程池:用于执行异步任务,保证高效处理多个网络请求。
5. HTTP/2 和 WebSocket 支持
OKHttp 对 HTTP/2 的支持是其性能优化的一大亮点。通过 HTTP/2,多个请求可以在一个 TCP 连接上复用,减少了建立多个连接的开销。此外,OKHttp 还支持 WebSocket 协议,用于实现长连接的实时通讯。
- HTTP/2 的好处:通过共享连接来处理多个请求,减少网络延迟和资源消耗。
- WebSocket 支持:OKHttp 提供了简单的 WebSocket API,允许客户端与服务器之间保持长连接,并通过 WebSocket 传递数据。
OKHttp的整体工作原理图
1 | +---------------------+ +---------------------+ |
总结
OKHttp 是通过 拦截器、连接池、异步调度器 等核心组件来高效管理 HTTP 请求的。其设计灵活,性能优化非常出色,特别是对 HTTP/2 的支持、连接复用和缓存机制,使得它成为 Android 和 Java 开发中非常流行的网络库。
- 拦截器链:使得请求和响应可以灵活地被拦截和修改。
- 连接池:通过连接复用提升了网络请求的性能。
- 缓存机制:提供了高效的 HTTP 缓存支持,避免不必要的网络请求。
- 异步处理:通过
Dispatcher
和线程池管理异步请求的并发和调度。
Glide 请求的生命周期
Glide 的请求生命周期涉及多个阶段,从请求的创建到资源的加载和回收。理解这一生命周期有助于优化资源管理和提高应用性能。下面是一个典型的 Glide 请求生命周期:
请求开始(Initialization):
- 使用
Glide.with(context)
来创建一个RequestManager
对象。RequestManager
负责管理请求的生命周期,并与 Android 的生命周期事件同步。
- 使用
构建请求(Building the Request):
- 通过链式调用,使用
load()
方法指定图像的来源(URL、文件、资源 ID 等)。 - 使用
apply()
方法可以应用RequestOptions
,配置图像的加载方式,例如占位符、错误图像、缩放类型等。
- 通过链式调用,使用
启动请求(Request Execution):
- 调用
into(target)
方法来启动图像的加载过程,其中target
通常是一个ImageView
。 - Glide 会根据指定的
RequestOptions
和DiskCacheStrategy
决定从缓存或网络加载资源。
- 调用
资源加载(Resource Loading):
- 缓存检查:首先检查内存和磁盘缓存是否包含目标图像。
- 网络请求或解码:如果缓存未命中,Glide 会请求网络资源或解码本地文件。
- Bitmap Pooling:使用位图池复用机制以减少内存分配。
图像显示(Display):
- 一旦资源准备好,图像将被显示在目标
ImageView
中。此过程也包括任何指定的Transformation
。
- 一旦资源准备好,图像将被显示在目标
清理(Cleanup):
- 当
Activity
或Fragment
的生命周期变化(如销毁)时,Glide 会自动取消未完成的请求,释放内存资源。 - 可以手动调用例如
Glide.with(context).clear(target)
来取消加载或移除图像。
- 当
自动恢复(Automatic Lifecycle Integration):
RequestManager
和RequestBuilder
会自动响应 Android 生命周期事件(如onPause
、onResume
),以便在 UI 不可见时暂停图像加载,并在恢复时继续。
这种生命周期管理机制确保了高效的资源使用,防止内存泄漏,同时又能方便地集成到 Android 的组件生命周期中。
Handler 原理
Android 的 Handler
是 Android 系统中处理线程之间通信的一种机制。它允许你在一个线程中向其他线程发送和处理消息,最常见的场景是更新 UI。因为 Android 规定只能在主线程(UI 线程)中更新 UI,Handler
就成为了从工作线程与 UI 线程交互的重要工具。
Handler 原理概述
Handler
是基于消息队列(MessageQueue)和消息循环(Looper)实现的。核心组件包括 Handler
、Message
、Looper
和 MessageQueue
。以下是每个组件的角色:
- Handler: 用于发送消息和处理消息。它向消息队列中发送消息,并在收到消息时执行相应的操作。
- Message:
Message
是Handler
处理的消息对象,包含了消息的内容,比如数据、标识符、目标 Handler 等。 - Looper: 负责管理线程中的消息循环。每个线程都可以通过
Looper
来关联一个消息队列。主线程默认有一个Looper
,工作线程需要手动创建。 - MessageQueue: 是一个消息队列,负责存储线程中的所有消息。
Looper
会从MessageQueue
中取出消息,然后分发给相应的Handler
。
工作原理
Looper 准备: 每个线程可以通过
Looper.prepare()
方法来初始化一个Looper
实例,并将其与当前线程关联。主线程的Looper
是系统自动创建的,但子线程需要手动创建。消息发送:
Handler
可以使用sendMessage()
或post()
方法将Message
或Runnable
发送到消息队列。每个Handler
都与某个Looper
关联,它们可以发送消息到这个Looper
的MessageQueue
中。消息存储: 发送的消息被存储在
MessageQueue
中。这个队列按时间顺序排列消息,先发送的消息先处理。消息处理:
Looper
会不断地从MessageQueue
中取出消息,并分发给相应的Handler
。每个Handler
都有一个handleMessage()
方法,当消息到达时,会调用这个方法来处理消息。Looper 轮询: 一旦
Looper.loop()
被调用,线程进入消息循环,Looper
就会不断地从MessageQueue
中取消息,并将它们分发给关联的Handler
。
Handler 工作流程图
- 工作线程向主线程的
Handler
发送Message
。 MessageQueue
接收到消息并入队。Looper
不断从MessageQueue
取出消息。- 取出消息后,
Looper
分发给目标Handler
,调用Handler
的handleMessage()
进行处理。 Handler
处理完消息后,循环继续,直到消息队列为空。
示例代码
1 | // 主线程中创建 Handler |
注意事项
- 线程间通信:
Handler
主要用于线程间的通信,工作线程可以通过Handler
将消息发送到 UI 线程更新界面。 - 避免内存泄漏: 当使用
Handler
时,特别是在活动或其他组件销毁时,如果没有妥善管理,可能会导致内存泄漏。建议使用WeakReference
或者在onDestroy
中移除所有的消息。 - 主线程和子线程:
Handler
的典型用法是在子线程进行耗时操作,然后通过Handler
将结果发送到主线程更新 UI。
总结来说,Handler
结合 Looper
和 MessageQueue
实现了 Android 中线程间的通信,主要用于处理子线程与主线程的消息交互。
IdleHandler
在 Android 中,IdleHandler
是用于处理 MessageQueue
在空闲时的任务的机制。它允许开发者在事件队列没有更多待处理消息时执行一些后台任务。这是通过 Looper
和 MessageQueue
框架实现的。以下是有关 IdleHandler
的详细信息:
工作原理
MessageQueue 和 Looper:
- Android 的
MessageQueue
用于处理消息和可运行对象,它由Looper
管理。 - 当
MessageQueue
中没有更多的消息时,可以触发IdleHandler
。
- Android 的
IdleHandler 接口:
IdleHandler
是一个接口,其中包含一个queueIdle
方法。- 当
Looper
观察到消息队列空闲时,会调用此queueIdle
方法。
注册 IdleHandler:
- 可以通过
MessageQueue
的addIdleHandler
方法注册一个IdleHandler
。 - 多个
IdleHandler
可以同时添加到一个MessageQueue
中。
- 可以通过
代码示例
以下是如何使用 IdleHandler
的一个基本示例:
1 | Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() { |
使用场景
- 后台任务: 在不影响应用主线程的情况下,执行一些低优先级的背景任务,如日志记录或状态更新。
- 资源清理: 空闲时释放不必要占用的资源,比如清理缓存。
- 性能优化: 延迟不重要的工作以优化应用的性能,使得界面更流畅。
设计考量
- 任务性质: 由于
IdleHandler
执行在主线程,应确保任务对用户界面的影响最小。 - 执行频率: 长时间任务应避免使用
IdleHandler
,因为这可能影响到其他更高优先级的操作。 - 返回值:
queueIdle
方法返回true
保留IdleHandler
,返回false
表示处理完后从队列中移除,以避免反复执行。
注意事项
- 不能保证执行: 如果应用的
MessageQueue
经常忙于处理消息,IdleHandler
可能很少或不会被调用。 - 避免阻塞: 确保
queueIdle
中的代码不会阻塞主线程,以免影响应用的响应速度。
IdleHandler
是一个有效的工具, 用于在不影响应用主线操作的情况下进行后台管理和优化。适当使用可以提升 Android 应用的性能和用户体验。
Handler 消息屏障
Handler消息屏障(Message Barrier)是Android消息处理机制中的一个概念,用于控制消息队列的处理顺序。它主要用于解决异步消息处理过程中优先级的问题。在常见的使用场景中,消息屏障允许某些重要的同步消息可以优先于异步消息来执行,提高应用的响应效率。
Handler 和 MessageQueue
在 Android 中,Handler
和 MessageQueue
配合使用来管理消息的发送和处理。消息队列是一个有序的数据结构,通常会根据消息的到达时间顺序来看待处理。
消息屏障的工作原理
消息类型:
- 同步消息:需要按照正常顺序处理的消息。
- 异步消息:相对较不重要,允许可以稍后再处理。
屏障机制:
- 消息屏障会被插入消息队列中,从它插入的位置开始,所有同步消息会被暂时停止处理,但异步消息可以继续被处理。
- 当消息屏障插入队列后,随后的同步消息将被延迟处理,直到消息屏障被移除。
用法场景:
- 消息屏障通常被系统内部使用,通过设置它,可以让像动画或布局测量这样的耗时处理异步消息先行执行,优化UI的流畅性。
- 不是所有用户应用开发中直接去使用消息屏障,但理解其机制在性能调优中非常有帮助。
实现方式
在 Android 源码中,MessageQueue
提供了 postSyncBarrier
方法用于插入一个消息屏障,其内部会生成一个特殊的 token 来标识屏障。
示例
假设我们希望将某些动画消息优先于普通消息执行,可以通过插入消息屏障后再插入动画消息,动画消息以异步方式(Android 4.4开始支持)发送,这些消息会立即执行,后面的同步消息则会在屏障被移除后处理。
1 | int token = messageQueue.postSyncBarrier(); |
总结
使用消息屏障是一种高级性能优化策略,主要是帮助系统内部对不同优先级的消息进行合理调度。开发者在理解这类机制知识的同时,可以更好地设计那些需要高效响应的应用模块。
Android 图形框架
参考:https://mp.weixin.qq.com/s/I0Hy03SkyYvNr-1v2oCTbA
Android 的图形渲染框架从底层硬件到顶层应用程序,是一套复杂而高效的架构。它涉及硬件抽象层(HAL)、图形驱动、图形引擎以及高级应用层 API。为了理解 Android 的图形渲染流程,可以将其划分为几个重要的层次,从最底层的硬件到最高层的应用绘制。以下是 Android 从底层到顶层的图形渲染框架的详细解析:
1. 硬件抽象层 (Hardware Abstraction Layer, HAL)
Android 的底层图形框架直接与硬件交互,依赖于硬件抽象层 (HAL) 来统一访问不同的硬件组件。这一层是 Android 系统中的基础层,屏蔽了不同硬件平台的差异,使上层框架可以通过统一的接口来操作底层硬件。
SurfaceFlinger:SurfaceFlinger 是 Android 系统的核心组件之一,它负责将多个应用程序窗口的渲染结果合成到屏幕上。它与硬件抽象层交互,控制帧缓冲区 (Framebuffer),并管理图形缓冲区的交换和显示。
**Gralloc (Graphics Allocation)**:Gralloc 是图形内存分配器,它为图形帧缓冲区分配内存,用于存储各个应用程序渲染的图像。不同硬件平台的实现可能不同,Gralloc 负责跨平台的内存管理。
**硬件加速 (GPU)**:Android 使用 GPU(图形处理单元)来加速图形渲染。GPU 通过 OpenGL ES 或 Vulkan 接口进行硬件加速渲染,从而大幅提升图形处理的效率。
2. 图形驱动和接口
在 HAL 之上,Android 通过 OpenGL ES、Vulkan 和 Skia 等图形 API 与底层的 GPU 交互。这些 API 使得应用程序可以高效地进行 2D 和 3D 图形的渲染。
OpenGL ES:OpenGL ES 是 OpenGL 的嵌入式版本,用于移动设备和嵌入式系统。它是 Android 系统中使用最广泛的 3D 图形 API,支持高效的 2D 和 3D 图形绘制。
Vulkan:Vulkan 是一种现代的低开销、高性能的图形 API,特别适合于需要大量并发处理的高性能图形应用。它比 OpenGL ES 提供了更精细的控制和更高的性能,但复杂度也更高。
Skia:Skia 是 Android 的 2D 图形库,它用于渲染所有的 2D 图形,包括文本、形状、位图等。Android 中的所有 View 组件最终都是通过 Skia 渲染的。
**EGL (Embedded-System Graphics Library)**:EGL 是一个连接 OpenGL ES 和图形底层(如 Framebuffer、窗口系统)的接口,负责上下文管理、表面创建以及图形的缓冲交换。通过 EGL,应用程序可以创建和管理 OpenGL ES 的渲染上下文,并交换渲染结果。
3. Surface 和 SurfaceFlinger
Surface:每个 Android 应用程序的窗口都与一个
Surface
对象相关联,Surface
是用于与系统共享图形缓冲区的抽象,它代表了一个可以绘制的画布。应用程序通过绘制到Surface
来渲染内容,最终这些内容会被发送到SurfaceFlinger
进行合成。SurfaceFlinger:作为 Android 的窗口合成器,
SurfaceFlinger
负责将多个应用程序窗口的Surface
进行合成,并最终显示在屏幕上。它接收来自不同应用的Surface
,并通过与硬件抽象层(HAL)和 Gralloc 的交互,将这些内容组合成一帧最终的图像,展示在屏幕上。
4. 硬件加速和 VSync
硬件加速:Android 系统在图形绘制过程中广泛使用了 GPU 硬件加速。所有
View
组件默认开启硬件加速,特别是在绘制复杂图形或动画时,硬件加速能极大提升性能。硬件加速通过 OpenGL ES 或 Vulkan 将图形渲染工作交给 GPU 执行,从而释放 CPU 资源。**VSync (Vertical Sync)**:VSync 是图形系统中的垂直同步机制,它控制图形帧的刷新频率。VSync 信号会同步图形缓冲区的交换与屏幕刷新,防止帧撕裂等问题。在 VSync 的控制下,系统以每秒 60 帧(或设备支持的其他刷新率)进行屏幕更新。
5. RenderThread 和 Choreographer
RenderThread:Android 中的
RenderThread
是一个独立的线程,用于处理界面渲染的任务。它负责管理硬件加速的绘制操作,将渲染任务提交给 GPU 以确保 UI 更新的高效性和响应性。Choreographer:
Choreographer
是 Android 用于同步图形帧的系统,它负责协调应用程序的绘制周期和 VSync 信号。Choreographer
确保绘制、布局和动画更新与 VSync 同步,从而避免视觉上的不一致性和卡顿现象。
6. 顶层的 Android UI 绘制系统
Android 的顶层图形渲染框架直接面向应用开发者,提供了丰富的绘制 API 和布局系统。主要包括以下几个核心部分:
View 系统和绘制流程:
View 和 ViewGroup:
View
是 Android 中的基本 UI 单元,负责处理用户输入和界面显示。ViewGroup
是View
的容器类,用来组织和管理子视图。- 每个
View
都有一个draw()
方法,通过它来实现自定义绘制逻辑,最终通过底层的 Skia 库进行实际绘制。
绘制流程:
- Android 中的 UI 绘制是一个复杂的流程,从布局的测量、位置计算到最终的绘制,都是通过遍历
View
树来完成的。 - measure()、layout() 和 draw() 是绘制流程的三大核心步骤,负责布局、定位和绘制视图。
- Android 中的 UI 绘制是一个复杂的流程,从布局的测量、位置计算到最终的绘制,都是通过遍历
Canvas 和 Paint:
Canvas
是 Android 的 2D 绘图类,代表了一个可绘制的表面。开发者可以在Canvas
上绘制图形、文本和位图。Paint
是用来定义绘制风格的类,指定了颜色、线条宽度、文本大小等属性。
硬件加速和 GPU 渲染:
- 在
View
系统中,当开启硬件加速时,所有绘制操作会通过 OpenGL 渲染,RenderThread
会将 UI 绘制任务交给 GPU 处理。 - DisplayList:为了提高性能,Android 会将
View
的绘制操作记录在一个 DisplayList 中,类似于一个绘图的指令集。当内容需要重绘时,Android 可以快速重放这些指令而不需要重新计算所有内容。
7. 动画系统
Android 的动画系统也是图形框架的重要组成部分,它提供了多种动画 API 用于实现复杂的 UI 交互效果。
Property Animations:
ObjectAnimator
和ValueAnimator
是 Android 的属性动画系统,允许开发者以流畅的方式改变View
的属性(如位置、透明度、缩放等),实现复杂的动画效果。硬件加速的动画:通过硬件加速,动画的每一帧可以在 GPU 上进行渲染,极大提升了动画的流畅度和性能。
8. 触摸事件和输入系统
Android 的触摸事件系统也是图形框架的一部分,负责处理用户输入,并将这些输入反馈到视图。
触摸事件分发:当用户在屏幕上触摸时,系统会生成触摸事件,并通过
View
的onTouchEvent()
方法传递到各个视图中。ViewGroup
负责将事件分发给其子视图,直到目标视图处理该事件。多点触控:Android 支持多点触控,系统会为每个触控点生成唯一的 ID,允许开发者处理复杂的多点交互场景。
总结
Android 的图形渲染框架从底层到顶层,是一个层次分明、结构复杂的体系。底层通过硬件抽象层 (HAL) 和 GPU 来高效管理图形资源和渲染操作,中间层提供了 OpenGL ES、V
ulkan、Skia 等 API 来进行图形渲染,顶层则通过 View
系统、动画、触摸事件等接口为应用开发者提供了高效的 UI 绘制和交互工具。整个体系通过 VSync 和硬件加速技术,保证了图形渲染的高效性和流畅性。
SurfaceView, GLSurfaceView 和 TextureSurfaceView
参考:https://mp.weixin.qq.com/s/tzIKbtxQoVuwP6RmLj06Aw
在 Android 中,SurfaceView
、GLSurfaceView
和 TextureView
是用于显示图形内容的三种常用控件,它们在实现方式、使用场景和性能特点上有所不同。以下是对它们的详细区别和应用场景的解析:
1. SurfaceView
SurfaceView
是 Android 中用于在独立线程上绘制内容的控件,通常用于处理高效的图形更新,比如视频播放、游戏渲染等场景。
特点:
独立的 Surface:
SurfaceView
的绘制是在一个独立的Surface
上进行的,该Surface
与主 UI 线程分离,意味着它可以在单独的渲染线程中进行绘制操作,不会影响主线程的性能。这样可以减少绘制时的卡顿。异步绘制:
SurfaceView
的内容可以通过后台线程进行异步绘制,适合需要频繁更新内容的场景,比如视频流或实时渲染。透明问题:
SurfaceView
的底层绘制是通过独立的Surface
实现的,所以它不支持 View 层的叠加或透明效果。SurfaceView
显示的内容始终是在窗口最前面的部分,因此它的View
层次较难与其他 UI 元素进行叠加。
使用场景:
- 视频播放:例如使用
MediaPlayer
或ExoPlayer
渲染视频内容。 - 游戏开发:使用单独的线程处理复杂的实时图形渲染。
示例:
1 | SurfaceView surfaceView = new SurfaceView(context); |
2. GLSurfaceView
GLSurfaceView
是一个专门用于 OpenGL 渲染的 SurfaceView
,封装了 OpenGL ES 的绘制流程,简化了开发者的 OpenGL 渲染工作。它专为 2D 和 3D 图形渲染设计,特别适用于游戏和需要高性能图形渲染的应用场景。
特点:
OpenGL ES 渲染支持:
GLSurfaceView
封装了 OpenGL 的初始化、上下文管理、绘制回调等细节。开发者只需实现GLSurfaceView.Renderer
接口来定义 OpenGL 渲染逻辑,而不需要手动管理 OpenGL 上下文和线程。独立线程渲染:
GLSurfaceView
也通过后台线程来进行 OpenGL 绘制,与主线程分离,避免 UI 卡顿。双缓冲机制:它默认支持双缓冲机制,可以避免图像撕裂问题,确保渲染帧的流畅显示。
EGL 管理:
GLSurfaceView
自动管理 OpenGL 的 EGL 环境,处理 OpenGL 上下文、配置选择和窗口关联等操作,简化了开发者的工作。
使用场景:
- 3D 图形渲染:如使用 OpenGL ES 绘制的 3D 游戏或数据可视化应用。
- 高性能 2D 图形渲染:如复杂的 2D 动画或自定义图形渲染。
示例:
1 | GLSurfaceView glSurfaceView = new GLSurfaceView(context); |
3. TextureView
TextureView
是一种更灵活的控件,允许应用程序直接控制渲染内容,并且可以像普通的 View
一样与其他 View
进行组合和叠加。它支持硬件加速,并且能够在应用层轻松管理。
特点:
可叠加:
TextureView
最大的优势是可以作为普通的View
在视图层次中使用,这意味着它可以与其他View
进行叠加、透明处理等复杂布局操作,克服了SurfaceView
不支持叠加的问题。硬件加速:
TextureView
支持硬件加速渲染,能够高效地处理复杂的图形内容。同时,它也支持在主线程上渲染,不像SurfaceView
那样强制需要后台线程。灵活性:
TextureView
通过SurfaceTexture
提供了灵活的绘制接口,允许开发者使用自定义的渲染逻辑,例如结合 OpenGL 或Canvas
绘制。更高的开销:虽然
TextureView
提供了更高的灵活性,但由于它在View
层级中与其他视图一起渲染,性能开销比SurfaceView
稍高,特别是在大规模或高频率的图形更新时。
使用场景:
- 视频播放:与
SurfaceView
类似的场景,但允许在 UI 层级中进行透明和叠加处理。 - 自定义视图:如自定义控件中需要灵活的图形渲染。
- 动画和视频效果:需要叠加效果的动画或视频处理。
示例:
1 | TextureView textureView = new TextureView(context); |
4. 区别总结
特性 | SurfaceView | GLSurfaceView | TextureView |
---|---|---|---|
绘制机制 | 使用独立的 Surface,后台线程渲染 | 基于 OpenGL ES 渲染,后台线程渲染 | 在主线程或后台线程渲染 |
渲染场景 | 常用于视频播放、2D 游戏等 | 用于 2D/3D 图形渲染,专为 OpenGL 设计 | 适用于需要叠加、透明效果的场景 |
是否支持叠加 | 不支持与其他 View 叠加 | 不支持与其他 View 叠加 | 支持与其他 View 叠加 |
OpenGL 支持 | 需要手动集成 OpenGL 支持 | 内置 OpenGL 支持,自动管理 EGL 环境 | 可以手动集成 OpenGL |
使用场景 | 视频播放、后台线程绘制 | 3D 游戏、复杂动画 | 视频播放、动画、需要叠加的场景 |
性能 | 高效,适合频繁更新的图形 | 高效,专为 OpenGL 优化 | 性能较 SurfaceView 略低 |
5. 选择建议
- 如果需要进行简单的视频播放或者通过后台线程渲染图像,**
SurfaceView
** 是一个不错的选择。 - 如果你的应用涉及到 2D 或 3D 的高性能图形渲染,且依赖于 OpenGL ES,**
GLSurfaceView
** 是最佳选择,因为它简化了 OpenGL 的管理和使用。 - 如果你需要在
View
层级中进行复杂的 UI 叠加(如视频播放的同时显示 UI 元素)或处理透明视图,**TextureView
** 提供了更灵活
的选择,但需要注意它可能带来更多的性能开销。
总结来说,SurfaceView
、GLSurfaceView
和 TextureView
各有其适用的场景和特点,选择合适的控件取决于你项目的需求、性能要求以及图形渲染的复杂程度。
MVC, MVP 和 MVVM
在软件开发中,MVC(Model-View-Controller)、MVP(Model-View-Presenter)和 MVVM(Model-View-ViewModel)是三种常见的架构模式,它们用于分离代码中的业务逻辑、数据层和用户界面层。这些架构模式的核心目的是提高代码的可维护性、可测试性和扩展性。下面是对 MVC、MVP 和 MVVM 这三种架构模式的详细介绍及其区别。
1. MVC(Model-View-Controller)架构模式
概述
MVC 是最早被提出的架构模式之一,用于分离应用程序的逻辑和用户界面。MVC 的核心思想是将应用程序分成三层:Model、View 和 Controller。在这种架构模式中,每一层都有明确的职责。
组件角色
- Model(模型):负责数据的获取和管理。它包括业务逻辑和数据操作,例如从数据库或网络获取数据,并处理这些数据。
- View(视图):负责展示数据。
View
直接与用户进行交互,显示Model
中的数据,并接收用户的输入。 - Controller(控制器):控制器是
Model
和View
之间的桥梁。它负责处理用户的输入,并调用Model
来获取或更新数据,然后将这些数据传递给View
进行显示。
数据流和交互
- 用户通过
View
与应用程序交互。 - 用户的输入被传递给
Controller
,由Controller
处理。 Controller
与Model
交互,获取或修改数据。Model
将处理后的数据返回给Controller
,然后Controller
将数据传递给View
来更新界面。
优点
- 分离了数据层和表现层,降低了代码的耦合度。
- 适用于简单的应用场景,快速开发 UI 界面。
缺点
Controller
在复杂应用中容易变得臃肿,因为它需要处理大量的逻辑。View
与Model
的解耦较差,View
可能会直接依赖Model
,不利于单元测试。
图示:
1 | User <--> View <--> Controller <--> Model |
2. MVP(Model-View-Presenter)架构模式
概述
MVP 是对 MVC 的一种改进,主要解决了 MVC 中 Controller
容易臃肿的问题,并增强了 View
和 Model
的解耦。MVP 将控制逻辑移到 Presenter 中,使得 View
只负责渲染界面,而 Presenter
完全控制应用逻辑。
组件角色
- Model(模型):与 MVC 中的
Model
类似,负责管理数据和业务逻辑。 - View(视图):
View
负责显示Presenter
提供的数据,并将用户的输入传递给Presenter
。在 Android 中,View
通常是 Activity、Fragment 或 XML 布局。 - Presenter(展示者):
Presenter
是View
和Model
之间的桥梁。它接收用户输入,处理业务逻辑,调用Model
获取数据,并将结果传递给View
来更新界面。Presenter
不直接依赖 UI 框架,因此可以单独进行单元测试。
数据流和交互
- 用户通过
View
与应用程序交互。 View
将用户的输入传递给Presenter
,由Presenter
处理业务逻辑。Presenter
通过Model
获取或更新数据。Presenter
将处理后的数据返回给View
,View
根据数据更新界面。
优点
- 更好的解耦:
View
和Model
之间完全解耦,View
只负责界面渲染,Presenter
负责业务逻辑。 Presenter
不依赖具体的 UI 组件,可以单独测试业务逻辑。- 更好的代码组织结构,适合中大型应用。
缺点
- 在复杂应用中,
Presenter
也可能变得臃肿,特别是当它负责过多的界面逻辑时。 - 对于频繁交互的场景,
Presenter
和View
之间的通信可能会增多,导致代码较为繁琐。
图示:
1 | User <--> View <--> Presenter <--> Model |
3. MVVM(Model-View-ViewModel)架构模式
概述
MVVM 是近年来流行的架构模式,特别是在数据绑定和响应式编程方面表现出色。MVVM 是对 MVP 的进一步演化,它引入了 ViewModel 来处理 View
的状态,并与 View
通过双向数据绑定(Data Binding)进行交互,减少了手动更新 UI 的操作。
组件角色
- Model(模型):与 MVC、MVP 中的
Model
一致,负责业务逻辑和数据处理。 - View(视图):
View
负责展示 UI,通常为 XML 布局文件或者 UI 组件。在 MVVM 中,View
通过数据绑定与ViewModel
进行交互。 - ViewModel(视图模型):
ViewModel
是Model
和View
之间的桥梁,负责处理逻辑和维护 UI 状态。它不直接与View
交互,而是通过Data Binding
或者LiveData
、Observable
等方式向View
提供数据。ViewModel
不依赖具体的View
,因此具有很好的可测试性。
数据流和交互
- 用户与
View
交互,View
将用户的操作通过数据绑定机制传递给ViewModel
。 ViewModel
处理业务逻辑,调用Model
获取数据。Model
将数据返回给ViewModel
,ViewModel
通过数据绑定自动更新View
。
优点
- 双向数据绑定:
View
和ViewModel
通过数据绑定机制进行交互,减少了手动更新 UI 的代码量。UI 可以自动响应数据变化。 - 更好的分离:
View
和ViewModel
之间完全解耦,ViewModel
不需要知道View
的具体实现,使得ViewModel
更容易测试。 - 响应式编程:通过
LiveData
或Observable
,View
可以实时监听数据的变化,适合需要频繁更新 UI 的场景。
缺点
- 学习曲线较高,特别是在没有数据绑定框架的情况下,手动实现数据绑定较为复杂。
- 数据绑定可能导致调试和追踪问题,因为 UI 和逻辑的交互变得不那么直观。
图示:
1 | User <--> View <--> ViewModel <--> Model |
MVC、MVP 和 MVVM 的区别
特性 | MVC | MVP | MVVM |
---|---|---|---|
View 和 Model 的关系 | View 和 Model 之间可以直接交互 | View 和 Model 之间完全解耦 | View 和 Model 之间完全解耦 |
业务逻辑的放置位置 | Controller | Presenter | ViewModel |
数据绑定 | 无 | 无 | 双向数据绑定(需要框架支持) |
可测试性 | 较差,Controller 中的逻辑难以测试 | 较好,Presenter 易于测试 | 最好,ViewModel 不依赖 UI,易测试 |
代码复杂度 | 适合简单应用,较低复杂度 | 中等,适合中型应用 | 较高,适合复杂应用 |
使用场景 | 小型应用或快速开发 | 中型应用或需要业务逻辑分离的场景 | 大型应用或需要响应式界面的场景 |
总结
- MVC:最基础的架构模式,适用于简单或较小的项目,但当项目变复杂时,
Controller
会变得臃肿,难以维护。 - MVP:相比 MVC,MVP 的
View
和Model
之间解耦更彻底,Presenter
负责所有的业务逻辑,适合中等复杂度的应用。Presenter
更容易测试和维护。 - MVVM:提供了更高级的双向数据绑定机制,使得
View
和ViewModel
之间的交互更加自动化,特别适合需要频繁更新 UI 的复杂应用,但引入了更高的复杂性。
四大组件
Android 的四大组件(Four Major Components)是 Android 应用程序的基础构建模块,它们分别是 Activity、Service、BroadcastReceiver 和 ContentProvider。每一个组件都有其独特的功能和用途,用于构建功能丰富且高度交互的应用程序。下面是对这四个组件的详细介绍:
1. Activity(活动)
概述
Activity 是 Android 应用程序的主要组件,用于展示用户界面并处理用户与应用程序的交互。每个 Activity 都代表应用的一个界面,是应用与用户之间交互的入口点。用户可以通过触摸屏幕、按键等操作与 Activity 进行交互,Activity 会响应这些操作并作出相应的反应。
主要特点:
- 生命周期:
Activity
的生命周期由 Android 系统管理。常见的生命周期方法包括onCreate()
、onStart()
、onResume()
、onPause()
、onStop()
和onDestroy()
。通过这些方法,开发者可以处理Activity
的创建、显示、暂停、销毁等过程中的事件。 - UI 界面:每个
Activity
都包含一个 UI 界面,通常由 XML 布局文件描述,开发者可以在其中添加按钮、文本框、图像等 UI 元素。
使用场景:
- 展示用户界面,如登录页面、主页、设置界面等。
- 处理用户交互,如点击按钮、输入数据等。
示例:
1 | public class MainActivity extends AppCompatActivity { |
2. Service(服务)
概述
Service 是一个运行在后台的 Android 组件,用于执行长时间运行的操作,例如下载文件、播放音乐、处理网络请求等。与 Activity
不同,Service
没有用户界面。Service
可以在应用程序关闭后继续运行,并且它可以与其他组件(如 Activity
)进行交互。
主要特点:
- 后台运行:
Service
可以在后台执行操作,而不会直接与用户交互。 - 生命周期:
Service
也有其自己的生命周期管理方法,包括onStartCommand()
、onBind()
、onCreate()
和onDestroy()
。服务可以是启动服务(通过startService()
启动)或绑定服务(通过bindService()
启动)。 - 前台服务:
Service
可以作为前台服务运行,这意味着它会持续运行并在状态栏中显示通知,用户可以知道服务正在运行。典型的例子是音乐播放器或 GPS 导航服务。
使用场景:
- 长时间的后台任务,如音乐播放、下载文件、数据同步等。
- 执行任务后不需要与用户直接交互的操作。
示例:
1 | public class MyService extends Service { |
3. BroadcastReceiver(广播接收器)
概述
BroadcastReceiver 是一种 Android 组件,用于监听和接收广播消息。广播是一种应用程序间的消息传递机制,系统或应用程序可以发送广播,其他应用程序或组件可以通过 BroadcastReceiver
监听并响应这些广播。广播可以是系统广播(如网络连接状态变化、电量变化等)或自定义广播。
主要特点:
- 广播消息:广播消息是一种全局通知,广播接收器可以监听这些通知并对其作出反应。系统广播如 “BOOT_COMPLETED” 或 “ACTION_BATTERY_LOW”,应用广播则是应用自定义的广播消息。
- 无界面组件:
BroadcastReceiver
没有用户界面,只是在接收到广播时执行相应的逻辑。 - 动态和静态注册:广播接收器可以在 AndroidManifest.xml 中进行静态注册,或者在代码中动态注册(通常使用
registerReceiver()
和unregisterReceiver()
方法)。
使用场景:
- 监听系统广播(如网络状态变化、低电量通知等)。
- 在应用程序之间进行消息传递。
- 在特定事件发生时触发操作,如日历提醒、下载完成通知等。
示例:
动态注册广播接收器:
1 | public class MyReceiver extends BroadcastReceiver { |
4. ContentProvider(内容提供者)
概述
ContentProvider 是 Android 中用于在应用程序之间共享数据的组件。ContentProvider
通过标准化的接口为应用程序提供数据访问,可以管理文件、数据库、网络数据等。通过 ContentProvider
,应用程序可以共享它们的数据,其他应用程序通过 ContentResolver
接口与 ContentProvider
进行通信。
主要特点:
- 数据共享:
ContentProvider
提供了跨应用的数据共享功能。它可以对 SQLite 数据库、文件或网络资源的数据进行操作。 - URI 访问数据:数据通过 URI(统一资源标识符)进行访问,
ContentProvider
通过提供 CRUD 操作(创建、读取、更新、删除)来管理数据。 - 安全性:
ContentProvider
允许对共享的数据进行权限控制,开发者可以设置权限来限制其他应用对数据的访问。
使用场景:
- 应用之间的数据共享,例如联系人、媒体、文件等。
- 实现应用与外部存储或其他应用的数据交互。
- 提供标准化的数据访问接口,例如 Android 系统的联系人、短信、媒体等数据访问。
示例:
1 | public class MyContentProvider extends ContentProvider { |
四大组件的总结
组件 | 作用 | 典型使用场景 | 是否有用户界面 |
---|---|---|---|
Activity | 展示用户界面,处理用户交互 | 显示应用程序的界面,如主页、设置页面 | 有 |
Service | 执行后台任务,长时间运行操作 | 播放音乐、后台下载文件、数据同步等 | 无 |
BroadcastReceiver | 监听并响应广播消息,进行全局事件处理 | 监听系统事件如网络变化、电量低等,或处理应用内的广播消息 | 无 |
ContentProvider | 在应用间共享数据,通过标准接口提供数据访问 | 共享联系人、文件、数据库等,或提供应用间的数据访问 | 无 |
四大组件的协作:
- 这些组件经常一起使用来构建复杂的 Android 应用程序。例如,一个应用的
Activity
可以启动Service
来在后台处理任务,而当任务完成时通过广播通知BroadcastReceiver
,同时ContentProvider
可以用于提供持久化的数据访问。
掌握 Android 的四大组件是开发 Android 应用的基础,理解它们的生命周期、作用和交互
方式,可以帮助开发者设计和实现功能丰富且高效的应用程序。
AIDL
AIDL(Android Interface Definition Language,Android 接口定义语言)是 Android 提供的一种用于实现 进程间通信(Inter-Process Communication,简称 IPC)的机制。通过 AIDL,应用程序可以与运行在不同进程中的服务或组件进行通信,允许跨进程访问对象并调用远程方法。AIDL 是 Android 中强大且灵活的进程间通信工具之一,尤其适合在多进程环境中使用。
为什么需要 AIDL?
在 Android 中,默认情况下应用程序中的所有组件(如 Activity
、Service
等)都是运行在同一个进程中的,可以直接通过引用对象来共享数据。但是,当组件运行在不同的进程时,它们之间是不能直接共享数据的,因为每个进程都有自己独立的内存空间。这时候就需要 AIDL 来进行进程间的数据传递和方法调用。
AIDL 的目标是允许你定义一个接口,让其他应用程序或服务可以调用你进程中的方法,就像调用本地方法一样。
AIDL 的工作原理
AIDL 的本质是利用 Binder 机制来实现不同进程之间的通信。在 Android 中,每个进程都有自己的内存空间,不能直接访问其他进程的数据。Binder 是 Android 的一种高效的 IPC 机制,AIDL 基于 Binder 来实现接口方法的跨进程调用。
AIDL 的工作流程如下:
- 定义 AIDL 接口:创建一个
.aidl
文件,定义接口中可以被远程调用的方法。 - 自动生成代码:Android SDK 工具会根据
.aidl
文件生成Stub
和Proxy
代码,Stub
负责接收远程调用,Proxy
负责在客户端执行远程调用。 - 服务端实现接口:服务端实现
Stub
类中的方法,这些方法会在远程调用时被触发。 - 客户端绑定服务:客户端通过
bindService()
绑定到远程服务,获取Proxy
对象,并通过这个对象调用远程服务的方法。
AIDL 的使用步骤
1. 定义 AIDL 接口
首先,你需要定义一个 .aidl
文件,描述客户端和服务端共享的接口。AIDL 支持的基本数据类型包括:int
、long
、boolean
、float
、double
、String
等,也支持数组、List
、Map
和自定义 Parcelable
对象。
1 | // IMyAidlInterface.aidl |
2. 实现 AIDL 接口
服务端需要实现这个接口。Android 会自动为这个接口生成一个 Stub
类,服务端需要继承这个 Stub
类并实现接口中的方法。
1 | // MyService.java |
3. 客户端绑定远程服务
客户端通过 bindService()
方法来绑定远程服务,并获取 Proxy
对象。通过这个 Proxy
对象,客户端可以像调用本地方法一样调用远程服务中的方法。
1 | // MainActivity.java |
4. 在 AndroidManifest.xml
中声明服务
服务端的服务组件需要在 AndroidManifest.xml
中声明,并指定 android:exported="true"
,以允许外部进程访问它。
1 | <service |
AIDL 的数据类型支持
AIDL 支持以下几种数据类型:
- 基本数据类型:如
int
、long
、boolean
、float
、double
、char
、String
等。 - 集合类型:如
List
和Map
(List
可以是泛型List<T>
,T 必须是 AIDL 支持的类型)。 - 自定义
Parcelable
对象:如果需要传递复杂的对象,可以实现Parcelable
接口,并在 AIDL 文件中使用该类型。Parcelable
是 Android 中用于序列化对象的机制,它比 Java 的Serializable
更高效。
AIDL 的优缺点
优点:
- 跨进程通信:AIDL 是 Android 官方提供的 IPC 机制,支持跨进程调用远程服务中的方法。
- 灵活性:通过 AIDL,可以灵活定义接口、数据类型以及不同进程间的调用方式。
- 高效:AIDL 基于 Binder 机制,而 Binder 是 Android 特有的、非常高效的进程间通信方式。
缺点:
- 复杂性:AIDL 增加了开发复杂度,尤其是涉及到传递复杂对象时,需要手动实现
Parcelable
。 - 性能开销:虽然 AIDL 基于 Binder 是高效的,但频繁的跨进程通信仍然会带来一定的性能开销。一般情况下,应尽量减少进程间通信的频率和数据量。
- 并发问题:AIDL 服务是多线程的,因此在实现 AIDL 接口时,开发者需要注意线程安全问题。
AIDL 的典型使用场景
- 音乐播放器:当音乐播放器的 UI 组件和后台播放服务运行在不同的进程中时,可以使用 AIDL 来让 UI 控制后台服务,比如播放、暂停、跳转等操作。
- 远程数据处理:有些应用会在后台服务中进行繁重的计算任务或网络操作,而主应用进程可以通过 AIDL 调用这些任务并获取结果。
- 跨应用通信:AIDL 可以用于不同应用间的通信。例如,应用 A 提供了一些服务,应用 B 可以通过 AIDL 访问这些服务。
总结
AIDL 是 Android 中用于实现进程间通信的强大工具,它基于 Binder 机制,允许不同进程的组件通过接口进行方法调用和数据传递。AIDL 主要用于需要在不同进程间进行复杂数据交互的场景,如后台服务、跨应用通信等。
开发者在使用 AIDL 时,需要了解进程间通信的特性,并注意线程安全和性能开销问题。虽然 AIDL 提供了极大的灵活性,但应在有必要的场景下使用,避免不必要的复杂度。
性能优化
在 Android 应用开发中,性能优化是确保应用流畅运行、节省资源并提升用户体验的关键环节。常见的 Android 性能优化方法可以分为多种类型,如内存优化、UI 优化、电量优化、网络优化等。以下是一些常用的 Android 性能优化方法:
1. 内存优化
参考:https://mp.weixin.qq.com/s/vuVZWsn9iGXTxvHQPfmKHQ
1.1 减少内存泄漏
- 内存泄漏会导致应用在长时间运行后变得缓慢,甚至崩溃。
- 使用工具:Android Studio 的 LeakCanary 或 Memory Profiler 来检测内存泄漏。
- 避免持有对
Context
的长时间引用,尤其是Activity
或Fragment
,避免使用静态变量持有Context
。 - 对于生命周期长的对象,如单例、线程等,应确保它们不会持有对短生命周期对象的引用。
1.2 使用合适的数据结构
- 使用轻量级的数据结构,比如在小数据量情况下使用
ArrayList
而不是HashMap
。 - 对于大数据或频繁操作的数据集,使用更高效的容器,比如
SparseArray
替代HashMap
来节省内存。
1.3 使用合适的 Bitmap 配置
- 加载图片时使用
BitmapFactory.Options
的inSampleSize
来缩放图片,防止加载过大的图片导致OutOfMemoryError
。 - 使用 LruCache 对图片进行缓存,避免重复加载。
- 在不需要图片时,及时调用
Bitmap.recycle()
来释放内存。
1.4 避免使用过多的 Service
Service
会常驻内存,占用资源。尽量使用 JobScheduler 或 WorkManager 来替代传统的Service
,尤其是后台任务的调度。
2. UI 优化
2.1 避免 UI 卡顿(ANR)
- 使用主线程(UI线程)处理复杂的逻辑或长时间的任务会导致 ANR(Application Not Responding)。
- 将耗时的操作(如网络请求、数据库查询、文件读写等)放在子线程中,使用 Handler 或 AsyncTask 进行异步处理。
- 对于频繁更新 UI 的场景,使用 RecyclerView 替代
ListView
,并对 View 进行缓存和复用,减少创建和销毁View
的次数。
2.2 优化布局
- 减少布局层级,避免深层嵌套。
- 使用 ConstraintLayout,替代复杂的
RelativeLayout
和LinearLayout
嵌套。 - 使用 Layout Inspector 工具分析布局层级,优化过于复杂的布局结构。
- 对于动态界面,使用 ViewStub 和 include 标签来优化布局加载,减少不必要的视图渲染。
2.3 避免频繁重绘
- 避免频繁调用
invalidate()
,尽量减少对 View 的重复绘制。 - 对于动画等频繁更新 UI 的场景,使用
View.invalidate()
时,只更新变化的区域而非整个视图。
3. 电量优化
3.1 减少不必要的后台任务
- 使用 JobScheduler 或 WorkManager 来调度任务,而不是使用
Service
或AlarmManager
来频繁唤醒设备。 - 合理使用 Doze 模式 和 App Standby,在应用进入后台时,尽量停止不必要的后台任务。
- 控制应用的唤醒频率,尽量减少
WakeLock
的使用,避免长时间占用 CPU。
3.2 优化传感器和 GPS 使用
- 尽量减少传感器(如加速度计、陀螺仪)和 GPS 的高频次使用。
- 使用更节能的定位方式,如 Fused Location Provider API,并根据需求调整定位精度,避免频繁的高精度 GPS 调用。
4. 网络优化
4.1 减少不必要的网络请求
- 合理使用网络请求,减少频繁的请求。
- 在需要重复请求的场景下,使用 缓存机制,例如 OkHttp 的缓存控制功能,避免每次都从服务器获取相同的数据。
4.2 压缩数据
- 尽量压缩网络请求的数据,使用 JSON 格式替代 XML 格式,减少数据包大小。
- 对图片等静态资源进行压缩,使用合适的图片格式和尺寸,减少带宽消耗。
4.3 批量处理网络请求
- 使用 批量请求,例如合并多个请求或使用多路复用(如 HTTP/2 支持的多路复用)来减少网络交互的次数和延迟。
- 合理设计 API 接口,减少请求次数和数据传输量。
4.4 使用高效的网络库
- 使用高效的网络库如 Retrofit、OkHttp,并配置合理的连接超时、读取超时等参数。
- 对于大文件下载,使用断点续传等技术减少网络资源浪费。
5. 启动速度优化
5.1 减少启动时的初始化工作
- 在应用启动时,避免进行过多的初始化操作。将非必要的初始化延迟到用户实际需要时再进行。
- 使用 Lazy Initialization 技术,按需加载资源和模块。
- 通过 Profile GPU Rendering 工具检查启动时的绘制性能。
5.2 优化冷启动
- 使用
SplashActivity
或空白的Theme
来显示一个过渡界面,确保应用的冷启动时间尽可能短。 - 减少或推迟启动时的耗时操作,如数据库查询、网络请求等。
6. 多线程优化
6.1 合理使用线程池
- 避免创建过多的线程,尽量使用 线程池 来管理并发任务。
- 使用 AsyncTask 或 Executors 来处理异步任务,减少线程的开销和资源消耗。
6.2 避免线程竞争
- 合理管理多线程操作,避免多线程竞争资源导致的性能问题,例如使用锁机制时要避免锁粒度过大,影响性能。
7. 数据库优化
7.1 使用高效的数据库操作
- 数据库操作应尽量在子线程中进行,避免阻塞主线程。
- 使用 批量插入/更新,减少频繁的数据库写入操作。
- 在查询时,尽量避免使用
SELECT *
,而是只查询需要的字段,减少数据读取量。
7.2 使用索引优化查询
- 对常用的查询字段建立索引,提高查询效率,但要避免在不必要的字段上建立过多的索引。
8. 其他优化方法
8.1 使用 ProGuard 进行代码混淆与优化
- 开启 ProGuard 或 R8 来混淆代码、移除未使用的代码和资源,减小 APK 大小,并提升应用的安全性。
8.2 资源优化
- 减少 APK 包大小,压缩图片、音频资源,删除不必要的资源文件。
- 使用 Android App Bundle,让 Google Play 根据用户设备生成最适合的 APK,减小下载包大小。
结论:
通过合理运用上述的性能优化策略,开发者可以显著提升 Android 应用的运行效率,减少内存消耗,避免 UI 卡顿和崩溃,提高电池续航,优化网络请求。性能优化是一个持续关注和调优的过程,借助 Android Studio 的工具(如 Profiler、Lint 等),可以更容易发现和解决潜在的性能瓶颈。
MediaCodec
MediaCodec
是 Android 平台上的一个多媒体编解码器 API,允许应用程序高效地编码和解码音视频数据。它的主要作用是提供一个硬件加速的接口来处理媒体数据,能够在支持的设备上显著提高媒体处理的效率和性能。MediaCodec
通常与 MediaExtractor
、MediaMuxer
等组件结合使用,以实现媒体的读取、解码、处理和重编码。下面是 MediaCodec
的工作原理及其基本流程:
MediaCodec
的工作原理:
MediaCodec
使用缓冲区队列模型,在内部通过输入和输出缓冲区与编码器或解码器硬件进行交互。它的基本工作流程可以分为以下几个步骤:
创建编码器或解码器:
- 应用程序需要根据需要使用的编解码器(如 H.264、AAC 等)来初始化一个
MediaCodec
实例。 - 通过
MediaCodec.createDecoderByType()
或MediaCodec.createEncoderByType()
指定编码或解码的媒体格式类型。
- 应用程序需要根据需要使用的编解码器(如 H.264、AAC 等)来初始化一个
**配置
MediaCodec
**:- 使用
configure()
方法来设置编解码器的格式参数(例如帧率、码率、分辨率、采样率等),并且可以指定输入输出的Surface
(如果是视频数据)。 - 可以配置解码器将视频输出到屏幕或者输出到内存中进一步处理。
- 使用
输入缓冲区处理(解码/编码):
- 填充输入缓冲区:
MediaCodec
会分配一组输入缓冲区(input buffers),应用程序需要获取这些缓冲区,然后将需要解码或编码的数据填充到缓冲区中。- 通过
dequeueInputBuffer()
方法获取空闲的输入缓冲区。 - 应用程序将音视频数据填充到缓冲区后,再通过
queueInputBuffer()
提交给MediaCodec
进行处理。
- 通过
- 对于视频解码,通常与
MediaExtractor
一起使用,先从媒体文件中提取帧数据,然后将数据送入输入缓冲区。
- 填充输入缓冲区:
解码或编码过程:
MediaCodec
内部硬件或软件会对输入缓冲区中的数据进行处理,解码或编码为指定的格式。- 在解码的场景中,
MediaCodec
将压缩的数据(如 H.264)解码为原始帧数据。在编码的场景中,MediaCodec
将原始数据(如 YUV)编码为压缩格式。
输出缓冲区处理:
MediaCodec
将解码或编码后的数据放入输出缓冲区,应用程序可以通过dequeueOutputBuffer()
获取这些缓冲区。- 获取输出缓冲区后,应用程序可以对解码后的原始帧进行显示、保存,或者对编码后的数据进行封装存储等操作。
- 对于视频解码,可以直接将数据呈现到
Surface
上,避免数据从 GPU 到 CPU 再到 GPU 的拷贝,提升效率。
释放和重置:
- 完成编解码操作后,应用程序可以调用
release()
方法释放MediaCodec
实例。 - 也可以通过
flush()
方法重置MediaCodec
,在不改变配置的前提下,清空所有缓冲区,适用于流式媒体解码场景。
- 完成编解码操作后,应用程序可以调用
基本工作流程图:
创建
MediaCodec
并配置:1
2
3MediaCodec codec = MediaCodec.createDecoderByType("video/avc");
codec.configure(format, surface, null, 0);
codec.start();解码数据:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27while (decoding) {
// 获取输入缓冲区并填充数据
int inputBufferIndex = codec.dequeueInputBuffer(TIMEOUT_US);
if (inputBufferIndex >= 0) {
ByteBuffer inputBuffer = codec.getInputBuffer(inputBufferIndex);
inputBuffer.clear();
// 从数据源读取数据并填充到缓冲区中
int sampleSize = extractor.readSampleData(inputBuffer, 0);
if (sampleSize < 0) {
// No more data
codec.queueInputBuffer(inputBufferIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
decoding = false;
} else {
long presentationTimeUs = extractor.getSampleTime();
codec.queueInputBuffer(inputBufferIndex, 0, sampleSize, presentationTimeUs, 0);
extractor.advance();
}
}
// 获取解码后的输出缓冲区
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
int outputBufferIndex = codec.dequeueOutputBuffer(bufferInfo, TIMEOUT_US);
if (outputBufferIndex >= 0) {
// 处理解码后的数据
codec.releaseOutputBuffer(outputBufferIndex, true); // true 表示渲染到 Surface
}
}**释放
MediaCodec
**:1
2codec.stop();
codec.release();
MediaCodec
的特点:
- 硬件加速:
MediaCodec
通过设备硬件(如 GPU、DSP 等)加速解码和编码操作,比纯软件编解码效率更高,特别适合高分辨率视频处理。 - 低延迟:由于直接操作缓冲区队列并结合硬件加速,能够提供低延迟的编解码,适合实时媒体处理,如视频会议、直播。
- 灵活性:支持视频和音频的多种格式(如 H.264、H.265、VP8、AAC、MP3 等),可适应多种应用场景。
总结来说,MediaCodec
提供了一个高效、灵活的多媒体编解码接口,通过硬件加速的方式让应用程序能够处理复杂的音视频数据,同时降低了对系统资源的消耗。
参考
Camera 和 Camera2 的区别
Android Camera
API 和 Camera2
API 是 Android 平台上用于开发摄像头功能的两个主要 API。Camera2
API 是为了替代早期的 Camera
API 而推出的,提供了更强大的功能和更灵活的控制方式。以下是这两个 API 的主要区别:
1. API 的推出时间
- Camera API:这是 Android 1.0 版本引入的较早的摄像头接口,它在 Android 5.0(Lollipop)之后逐渐被弃用。
- Camera2 API:在 Android 5.0(Lollipop)中引入,用于提供更精细的摄像头控制和硬件功能支持。
2. API 设计和复杂性
Camera API:
- 简单易用:
Camera API
比较简单易用,适合快速开发一些基本的摄像头功能。 - 功能有限:它的设计目标是提供基本的拍照和录制视频功能,无法直接访问摄像头硬件的高级特性。
- 同步模式:操作大多是同步的,容易出现卡顿或性能瓶颈的问题,尤其在高性能需求场景下。
- 简单易用:
Camera2 API:
- 高度灵活且复杂:
Camera2
采用了全新的设计模式,基于管道(pipeline)机制。开发者可以非常灵活地控制图像捕捉流程,包括对曝光、对焦、ISO 等参数进行精细的调整。 - 异步模式:
Camera2
使用异步的回调机制,通过Handler
来处理图像数据流,支持多线程和更好的性能优化。 - 更高的学习曲线:与
Camera API
相比,Camera2
的使用更为复杂,需要对相机特性有更深入的了解。
- 高度灵活且复杂:
3. 硬件能力的访问
Camera API:
- 只能进行非常有限的设置,例如分辨率和缩放。无法很好地访问和利用摄像头的高级功能。
- 不支持手动控制 ISO、快门速度、白平衡、对焦等参数。
- 无法使用 RAW 格式拍摄,只能获取压缩格式(如 JPEG)的图片。
Camera2 API:
- 提供对摄像头硬件的细粒度控制,允许开发者通过
CaptureRequest
自定义曝光时间、ISO、对焦距离、快门速度等参数。 - 支持高级模式,比如全手动模式、自动对焦/自动曝光/自动白平衡锁定等。
- 支持拍摄 RAW 格式图像,便于开发者在后期处理时有更大的灵活度。
- 允许开发者访问双摄像头功能,以及处理高帧率视频和慢动作视频。
- 提供对摄像头硬件的细粒度控制,允许开发者通过
4. 图像处理与数据流
Camera API:
- 图像处理和捕捉流程是比较固定的,开发者难以自定义数据流。
- 通过预览界面获取图像数据,无法获得预览与拍摄的分离控制。
Camera2 API:
Camera2
提供了一个基于 管道(Capture Pipeline) 的图像捕捉流程,可以同时处理多个数据流,比如同时预览、拍照、录像等。- 可以处理多个输出目标(如
Surface
、ImageReader
),支持多线程并发处理数据。 - 支持并行预览、拍照和录像,且能够更好地控制帧率和分辨率。
5. 性能与帧率控制
Camera API:
- 性能相对较低,主要适用于低帧率的预览和拍摄。
- 对高帧率视频录制和高性能图像处理支持有限。
Camera2 API:
- 提供更高效的图像处理流程,支持高帧率视频录制和慢动作视频。
- 可以通过手动调节帧率和分辨率来优化性能,适合高性能场景。
6. 开发者工具与兼容性
Camera API:
- 由于 Camera API 较为简单,适合快速开发入门级的摄像头应用。
- 但是它的功能有限,且在 Android 5.0 之后逐渐被 Camera2 取代,因此并不适合现代高需求的应用场景。
Camera2 API:
- 虽然复杂度较高,但适合开发需要高度自定义、专业级别的应用,如专业相机应用、AR/VR 应用等。
- Camera2 API 兼容 Android 5.0 及更高版本的设备,某些设备可能没有完全支持 Camera2 API 的所有特性(如完全手动控制),这取决于设备硬件支持。
7. 扩展和进化
Camera API:
- 基本没有进一步的发展,功能已经很有限,主要用于向后兼容性。
Camera2 API:
- 是 Android 未来摄像头开发的主流 API,不断得到改进和扩展。随着 Android 版本的升级,新特性不断加入(如多摄像头的同步控制、深度图等)。
8. 示例代码差异
Camera API 示例代码:
1 | Camera camera = Camera.open(); |
Camera2 API 示例代码:
1 | CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE); |
总结:
- Camera API 更简单,适合基础的拍照和视频录制应用,但功能和性能有限,已逐步被淘汰。
- Camera2 API 提供了更强大、更灵活的摄像头控制,适合对性能和高级功能有较高要求的应用开发,但其复杂性较高,需要开发者更深入的掌握相关概念和技术。
目前,开发复杂或高性能摄像头应用时,Camera2 API
是首选。
BufferQueue
BufferQueue
是 Android 图形子系统中一个非常核心的组件,它负责管理生产者和消费者之间的缓冲区交换,通常用于处理图像和视频流数据。BufferQueue
是构建图像渲染管道的基础部分,应用于窗口管理器(SurfaceFlinger)、相机、视频播放器等与图像显示相关的系统组件中。
BufferQueue
的工作机制
BufferQueue
是一个生产者/消费者模式的实现,包含两个主要部分:
- 生产者(Producer):向缓冲区队列提交图像数据。
- 消费者(Consumer):从缓冲区队列读取并处理图像数据。
生产者和消费者可以在不同的线程或进程中运行,BufferQueue
负责在它们之间传递缓冲区。
主要组件
**
GraphicBuffer
**:BufferQueue
中传递的实际缓冲区,是承载图像数据的内存块。生产者将数据填充到GraphicBuffer
中,消费者从中读取图像并进行处理或显示。**
BufferSlot
**:BufferQueue
中的每个缓冲区被称为一个BufferSlot
,通常BufferQueue
维护一个固定数量的BufferSlot
。生产者和消费者通过BufferSlot
来交换数据。**
Surface
和SurfaceTexture
**:- **
Surface
**:生产者通过Surface
向BufferQueue
提交数据,常用于图像渲染。例如,当一个应用想要在屏幕上绘制图像时,它会通过Surface
向BufferQueue
发送图像。 - **
SurfaceTexture
**:消费者通过SurfaceTexture
从BufferQueue
获取数据,通常用于纹理渲染(如 OpenGL 中的渲染)。
- **
流程
生产者提交数据:
- 生产者通过
Surface
或其他接口向BufferQueue
提交新的GraphicBuffer
。 - 生产者调用
dequeueBuffer()
从BufferQueue
请求一个空闲的缓冲区BufferSlot
,生产者可以向这个缓冲区中填充图像数据。 - 填充完数据后,生产者调用
queueBuffer()
将这个缓冲区返回给BufferQueue
,表示该缓冲区现在包含了新的一帧图像数据。
- 生产者通过
消费者读取数据:
- 消费者会调用
acquireBuffer()
来从BufferQueue
中获取已填充的BufferSlot
,并从中读取图像数据。 - 一旦消费者完成了对图像的处理或显示,它会调用
releaseBuffer()
,将缓冲区返回给BufferQueue
,以便生产者再次使用。
- 消费者会调用
BufferQueue
的主要作用
异步图像处理:
BufferQueue
实现了生产者和消费者的异步通信。生产者和消费者可以在不同的线程或进程中独立运行,BufferQueue
通过缓冲区管理确保数据交换的高效和流畅。生产者可以持续生成数据而不用等待消费者处理完成。双缓冲或多缓冲机制:
BufferQueue
支持多缓冲机制(通常是双缓冲或三缓冲),确保在高帧率的图像处理过程中避免撕裂现象。生产者可以生成下一帧数据,而消费者正在处理或显示上一帧的数据。图形显示和纹理渲染的桥梁:
在 Android 中,应用通过Surface
提交图像数据,这些数据被消费者(如SurfaceFlinger
或 GPU)从BufferQueue
中读取,用于最终的图形显示或作为 OpenGL 纹理使用。
BufferQueue
的工作示例
图形渲染流程:
在一个典型的 Android 图形渲染场景中,应用程序通过 SurfaceView
绘制图像,背后实际涉及 BufferQueue
的运作:
- 应用通过
Surface
提交绘制数据(例如,游戏中的帧)。 BufferQueue
在后台处理缓冲区队列。SurfaceFlinger
作为消费者,从BufferQueue
获取缓冲区并将其合成到最终的显示屏幕上。
示例流程:
- 应用程序向
Surface
请求一个缓冲区。 BufferQueue
分配一个空闲的缓冲区(BufferSlot
)并返回给应用程序。- 应用程序将图像数据写入缓冲区,并将其放回
BufferQueue
。 SurfaceFlinger
从BufferQueue
获取这个缓冲区,并将其显示在屏幕上。- 一旦显示完成,缓冲区会被标记为空闲状态,并且可供生产者再次使用。
BufferQueue 的实际应用场景
1. Camera API
在 Camera 应用程序中,BufferQueue
被用于管理相机的预览帧流。相机硬件作为生产者不断地将预览帧(图像数据)发送到 BufferQueue
中,而应用程序或 SurfaceFlinger
作为消费者,从 BufferQueue
中获取并处理或显示这些帧。
2. 视频播放
在视频播放过程中,视频解码器解码后的图像帧通过 BufferQueue
发送给显示器。解码器是生产者,播放器是消费者。视频播放器通过 SurfaceView
或 TextureView
处理解码后的图像,并呈现在用户界面上。
3. OpenGL 纹理渲染
SurfaceTexture
使用 BufferQueue
来管理将图像数据传递给 GPU 的过程。应用程序可以通过 SurfaceTexture
从摄像头或其他数据源获取图像,并将其作为 OpenGL 纹理进行渲染。
BufferQueue 的优化点
锁定与并发:
BufferQueue
使用了同步机制来确保生产者和消费者的并发访问安全。在高性能场景下,对锁的优化非常关键。Android 内部对BufferQueue
的锁机制进行了深度优化,避免了不必要的阻塞和性能开销。多缓冲区模式:通过支持双缓冲、三缓冲等机制,
BufferQueue
能够有效减少生产者和消费者之间的等待时间,提高图像处理的流畅度。硬件加速:
BufferQueue
直接与硬件加速进行协作,生产者和消费者可以使用GraphicBuffer
来减少数据拷贝的开销,直接在 GPU 中操作,提高效率。
总结
BufferQueue
是 Android 图形系统的核心机制,用于协调生产者(例如应用、相机、视频解码器等)和消费者(如 SurfaceFlinger
、OpenGL 渲染器等)之间的缓冲区传递。它通过异步处理和多缓冲区机制,确保图像数据高效传输,并避免卡顿、撕裂等现象。在实际的 Android 开发中,理解 BufferQueue
的工作原理对于优化图像显示性能非常重要。
组件化
Android 组件化是一种将应用程序的不同功能模块分离成独立组件的开发架构模式。这些组件可以单独开发、测试、调试和维护,并且通过某种机制进行集成,最终构成一个完整的应用程序。组件化的目的是提升代码的可维护性、复用性和灵活性,尤其在大型团队开发中显得尤为重要。
为什么需要组件化?
降低复杂度:随着应用功能越来越复杂,代码变得难以维护和扩展。通过将不同功能划分成模块,可以降低整体代码的复杂性。
并行开发:团队成员可以同时开发不同的模块,减少相互依赖,提高开发效率。
复用性:组件可以在不同项目中复用,降低代码冗余,提升开发效率。
独立性:组件可以独立开发、调试和测试,不依赖于整个应用程序的运行,减少开发调试的时间。
降低耦合:通过组件化,模块间的依赖性降低,模块之间的通信可以通过接口或依赖注入实现,从而降低耦合度。
组件化的基本概念
组件(Component):
- 每个组件是一个相对独立的功能模块,比如登录模块、用户模块、支付模块等。
- 组件可以是可执行的 Android 模块(如
Activity
、Service
),也可以是非可执行的业务逻辑模块(如数据处理、工具库等)。
模块化(Modularization):
- 组件化的实现通常是通过模块化来完成的,Android 中通过 Gradle 的多模块构建实现模块化开发。
- 每个模块可以有自己的独立的代码、资源和配置,模块之间可以通过接口进行交互。
公共库(Common Library):
- 在组件化架构中,公共库是存放通用代码(如工具类、网络请求、日志处理等)的模块,其他业务模块可以依赖它。
主应用(App Shell):
- 主应用是最终集成所有模块的入口。各个组件开发完成后,最终都会通过某种机制集成到主应用中,主应用通常只负责整体调度和框架集成,而不包含具体的业务逻辑。
组件通信(Component Communication):
- 不同组件之间通常通过接口(Interface)、事件总线(Event Bus)或依赖注入(Dependency Injection)等方式进行通信。
组件化的架构层次
组件化架构一般分为几个层次,每个层次都有其独立的职责和功能:
基础层(Base Layer):
- 包含一些通用的基础库和工具库,提供网络请求、数据库操作、日志管理等功能。
- 例如
common
模块,通常包含 Android 应用开发过程中用到的工具类、常用函数和常量等。
业务层(Business Layer):
- 包含具体的业务功能,每个功能模块封装在一个独立的组件中,比如登录模块、用户管理模块、支付模块等。
- 这些模块通常不直接相互依赖,而是通过公共接口来进行通信。
接口层(Interface Layer):
- 定义模块之间的交互接口,确保模块之间解耦。通过接口和服务的方式,其他模块能够调用业务逻辑而无需知道其具体实现。
- 例如,使用
Router
作为组件之间导航的工具,或者通过EventBus
发送事件来进行模块间通信。
主应用层(App Shell Layer):
- 主应用层负责集成各个业务模块,将所有模块组合成一个完整的应用程序。
- 在开发阶段,主应用可以是一个轻量的壳应用,主要用于调度和启动业务模块。
组件化的实现方式
Android 组件化有多种实现方式,常见的方式包括使用 Gradle
的多模块构建、使用路由(Router)进行模块间通信、通过服务接口定义模块交互、以及通过依赖注入框架实现模块的动态加载。
1. Gradle 多模块化构建
在 Android 中,使用 Gradle
可以轻松实现模块化构建。每个业务模块可以通过 Gradle
定义成一个独立的 Android Library 或 Android Module。通过配置 settings.gradle
和 build.gradle
,可以将这些模块组合到主应用中。
模块划分:一个应用被拆分成多个 Gradle 模块,每个模块可以是
Android Library
或Java Library
,也可以是可执行的 Android 模块。示例:
1 | // settings.gradle |
通过这种方式,模块之间的依赖是明确的,编译时 Gradle
能自动解析模块间的依赖关系。
2. 组件间通信方式
(1)接口通信(Interface Communication)
模块之间通过接口(Interface)来实现通信和解耦。接口可以定义在公共模块中,各个业务模块通过实现这些接口来完成具体的业务逻辑。
示例:
1 | // 公共模块中的接口 |
(2)路由通信(Router Communication)
路由器(Router)是一种常见的组件间通信方式,它允许模块通过 URI(统一资源标识符)进行导航或调用彼此的功能。常用的路由库如 ARouter。
示例:
1 | // 在登录模块中定义路由路径 |
(3)事件总线(Event Bus)
使用事件总线(如
EventBus
、RxBus
)在不同模块之间发送事件通知,实现模块之间的解耦和通信。示例:
1 | // 发送事件 |
(4)依赖注入(Dependency Injection)
- 使用依赖注入框架(如 Dagger 或 Hilt)可以实现动态加载组件,进一步解耦模块之间的依赖。
3. 动态化加载
在某些场景下,组件可能不需要随着应用一起打包,而是需要在运行时动态加载。例如在插件化框架(如 RePlugin、Small)中,组件可以作为插件单独打包和发布,主应用可以在运行时动态加载这些插件。
组件化的开发模式
单一组件模式(Single Component Mode):
- 在组件开发的早期阶段,每个组件通常独立开发和测试。开发者可以通过单一组件的模式,将某个模块设置为一个独立的应用进行调试。
集成模式(Integration Mode):
- 当所有组件开发完成后,它们会集成到主应用中,组成一个完整的应用程序。此时可以通过配置
Gradle
构建文件来控制每个模块的集成状态。
- 当所有组件开发完成后,它们会集成到主应用中,组成一个完整的应用程序。此时可以通过配置
组件化的优缺点
优点:
- 开发效率提升:模块化拆分后,不同功能模块可以并行开发、测试、调试,提高开发效率。
- 代码可维护性提升:代码拆分成小的模块后,每个模块都更加独立,维护和扩展变得更加容易。
- 模块复用:业务模块可以复用到其他项目中,减少重复开发的工作量。
- 灵活的应用架构:可以通过增加或减少模块来灵活调整应用的功能,而无需对整个应用进行大的改动。
缺点:
- 初期成本较高:组件化架构的搭建和维护需要一定的技术门槛,初期设计和实施的成本较高。
- 复杂的依赖管理:组件化会导致依赖管理变得复杂,特别是在有大量公共库和模块之间存在复杂依赖关系的情况下。
性能开销:如果模块之间的通信频繁,或者路由、依赖注入的使用不当,可能会带来一定的性能开销。
总结
Android 组件化是一种非常有效的架构模式,尤其适用于大型项目或多人协作开发的项目。通过合理的模块划分、解耦的通信方式和灵活的依赖管理,组件化能够提升项目的可维护性和扩展性,同时也能提高开发效率。不过,组件化的实施需要根据项目规模、团队需求和性能考虑权衡进行。
插件化
Android 插件化是一种将应用程序的功能模块化,并且在运行时动态加载模块的方法。与组件化不同,插件化的目标是使应用程序的某些功能可以在应用发布后动态添加、更新或移除,而无需重新打包整个应用。通过插件化,开发者可以灵活地更新或扩展应用程序的功能,提升应用的可扩展性和灵活性。
为什么需要插件化?
动态扩展:应用程序的功能可以在运行时动态加载,无需重新打包发布整个应用。例如,可以在应用中实现动态更新某些模块(如业务功能、界面等)。
减少 APK 大小:将一些次要或不常用的功能以插件的形式分离出去,用户可以在需要时才下载和加载这些功能,减少初始安装包的体积。
灵活更新:通过插件化,可以局部更新应用中的某些功能模块,而无需发布完整的更新包,从而减少用户的更新成本和开发者的发布压力。
多团队并行开发:不同功能模块可以作为插件独立开发和测试,团队之间的耦合降低,提升开发效率。
实现业务隔离:通过插件化架构,不同业务模块可以相对独立地实现和部署,避免功能模块之间的代码耦合,提升代码的可维护性。
Android 插件化的基本概念
宿主(Host App):
- 宿主是插件化系统的核心部分,它负责加载和运行插件,同时管理插件与宿主的交互。
- 宿主提供了基本的应用框架和资源,插件运行时依赖于宿主提供的环境。
插件(Plugin):
- 插件是一个独立的功能模块,可以被宿主动态加载和运行。插件通常包含自己的代码、资源和配置。
- 插件可以是一个完整的模块(如登录模块、支付模块),也可以是某个具体的功能(如一个新界面、一个新工具等)。
插件框架(Plugin Framework):
- 插件框架是插件化的基础,它负责管理插件的加载、卸载、资源访问和类的调用等功能。
- 常见的插件框架有 RePlugin、Small、DynamicLoadApk、DroidPlugin 等。
资源管理(Resource Management):
- 插件的资源(如图片、布局文件等)需要与宿主应用的资源隔离开来,插件框架负责解决插件和宿主之间的资源访问问题。
ClassLoader:
- Android 插件化的一个关键技术点是 ClassLoader,它用于动态加载插件的类文件,使插件能够在宿主环境中执行。
- 插件框架通常会通过自定义的
ClassLoader
来实现插件的动态加载和隔离。
插件生命周期管理:
- 插件中的
Activity
、Service
等组件的生命周期需要通过插件框架进行管理,插件框架会负责将插件中的组件映射到宿主的上下文环境中执行。
- 插件中的
插件化的工作原理
1. 动态加载插件
Android 插件化的核心原理是 动态加载,即在应用运行时,通过某种机制将插件的代码和资源加载到宿主的内存中,并运行这些代码。其基本流程如下:
插件打包:插件通常被打包成 APK 格式,但它不是一个独立运行的应用程序。插件的代码、资源和配置文件(如
AndroidManifest.xml
)打包在一起。插件的加载:宿主应用通过插件框架使用
DexClassLoader
或PathClassLoader
动态加载插件中的代码和资源。这个过程通常需要解决类的查找、资源加载和AndroidManifest.xml
文件的解析等问题。插件组件的运行:插件的
Activity
、Service
等组件不能直接注册到系统中,而是通过插件框架模拟这些组件的生命周期。宿主会将插件的组件映射到宿主环境中运行。
2. ClassLoader 动态加载机制
Android 插件化的一个关键点是通过 ClassLoader 实现插件的动态加载。插件框架通常使用 DexClassLoader
或自定义 ClassLoader
来加载插件的 .dex
文件(即插件的字节码),从而使插件的代码在宿主中运行。
**
DexClassLoader
**:Android 提供的一个类加载器,用于加载外部存储或网络下载的.dex
文件。插件框架会利用这个类加载器将插件的代码动态加载到内存中。类的加载顺序:插件加载时,ClassLoader 会首先从宿主的类路径中查找需要的类,如果没有找到,则会从插件的
.dex
文件中查找类。这样可以实现插件与宿主之间的类隔离。
3. 资源管理
插件化的一个重要挑战是 资源的管理,因为 Android 的资源(如图片、布局文件等)是通过 R
文件生成的静态引用,而插件的资源和宿主的资源需要分开管理,不能冲突。
- 插件框架通常会通过
AssetManager
动态加载插件的资源包,并将插件的资源加入宿主的资源管理系统中。 - 宿主和插件的资源 ID 可能会发生冲突,插件框架需要通过
Resources
动态解析插件资源,避免 ID 冲突。
4. Activity 的生命周期管理
插件化中,插件的 Activity
不能直接注册到系统的 AndroidManifest.xml
中,因此插件框架需要模拟 Activity
的生命周期,并将插件的 Activity
与宿主的 Activity
进行映射。
- 插件框架会在宿主的
AndroidManifest.xml
中注册一个占位的Activity
(称为代理Activity
),当插件的Activity
需要启动时,宿主会启动代理Activity
,然后通过反射将插件的Activity
的生命周期方法映射到代理Activity
上执行。
插件化框架的常见实现
1. RePlugin
RePlugin 是一个由 360 公司开发的开源 Android 插件化框架,目标是解决复杂的插件化场景,包括插件的动态加载、卸载、资源管理和生命周期管理。
特点:
- 支持插件的动态加载和卸载。
- 插件可以动态更新,且更新过程无需重启宿主应用。
- 插件和宿主的资源可以独立管理,避免冲突。
- 支持插件的独立调试,开发体验较好。
实现原理:
- 使用自定义的
ClassLoader
来实现插件的动态加载。 - 通过代理
Activity
来处理插件的组件生命周期问题。 - 使用
Resources
和AssetManager
动态管理插件的资源。
- 使用自定义的
2. Small
Small 是一个轻量级的插件化框架,适合那些只需要简单插件化场景的应用。它的设计目标是让开发者尽可能少地修改现有项目代码,同时实现应用的插件化。
特点:
- 插件轻量化,依赖少,集成简单。
- 支持资源管理、类加载、Activity 生命周期管理。
- 支持插件动态加载。
实现原理:
- Small 使用
PathClassLoader
来加载插件的代码。 - 插件的资源通过
AssetManager
动态加入宿主的资源系统中,保证宿主和插件之间的资源隔离。
- Small 使用
3. DroidPlugin
DroidPlugin 是 360 安全团队开发的另一个开源插件化框架,专注于通过插件化实现应用多进程、热更新和动态功能扩展。
特点:
- 支持插件的动态安装、卸载、升级。
- 支持插件的多进程运行。
- 插件可以访问宿主提供的服务,能够进行复杂的业务逻辑实现。
实现原理:
- DroidPlugin 使用代理机制来启动和管理插件的
Activity
、Service
和BroadcastReceiver
。 - 通过自定义的
ClassLoader
来动态加载插件的代码。
- DroidPlugin 使用代理机制来启动和管理插件的
4. DynamicLoadApk
DynamicLoadApk 是另一个简单的插件化框架,主要通过 DexClassLoader
动态加载插件的代码,并通过代理 Activity
来实现插件 Activity
的生命周期管理。
- 特点:
- 插件通过 APK 文件形式加载,插件的代码和宿主隔离。
- 支持插件
Activity
的生命周期管理。 - 框架简单易用,适合简单插件化场景。
插件化的实现步骤
- 创建宿主应用:宿主应用是插件化系统的核心,负责加载和管理插件。首先,需要在宿主应用
中配置插件框架,并为插件的加载提供接口。
创建插件应用:插件是宿主应用的功能扩展,插件通常打包为 APK 格式,并通过插件框架加载到宿主中运行。
使用插件框架:选择合适的插件框架(如 RePlugin、Small、DroidPlugin),并在宿主应用中集成框架代码,处理插件的加载、资源管理和生命周期管理。
处理组件和资源:插件中的组件(如 Activity、Service)和资源(如图片、布局文件)需要通过插件框架进行加载和管理,确保插件和宿主的资源和组件能够正确工作。
插件化的挑战
性能问题:插件化框架在加载和卸载插件时,会涉及到大量的反射操作、资源加载等,可能会影响性能。特别是在低端设备上,插件的加载速度可能较慢。
资源冲突:插件和宿主的资源可能会发生冲突,特别是资源 ID 重复的问题。插件框架需要解决资源的隔离和冲突问题。
兼容性问题:不同的 Android 版本和设备在处理插件加载和资源管理时,可能会表现出不同的行为,这需要插件框架进行兼容性处理。
调试难度:插件的动态加载和运行增加了调试的难度,开发者需要使用特定的工具和框架来调试插件中的问题。
总结
Android 插件化是一种强大的架构模式,它允许应用程序在运行时动态加载和卸载功能模块,提升应用的灵活性和扩展性。插件化的核心原理是通过 ClassLoader
动态加载插件的代码,并通过插件框架处理插件的资源和组件生命周期管理。常见的插件化框架如 RePlugin、Small、DroidPlugin 等,能够帮助开发者快速实现应用的插件化。
插件化的实施可以解决应用程序动态更新、功能扩展、模块化开发等问题,但同时也带来了性能、资源冲突和兼容性等挑战。开发者在使用插件化时需要根据具体需求权衡利弊,并选择合适的框架和实现方案。
热更新
Android 热更新(Hotfix)是一种在应用程序无需重新安装或从应用商店下载新版本的情况下,动态修复代码或资源错误的技术。通过热更新技术,开发者可以在不经过 Google Play 等应用商店重新发布应用的情况下修复应用中的 Bug,甚至在某些情况下动态更新应用中的某些业务逻辑。
为什么需要热更新?
快速修复 Bug:应用发布后,可能会出现一些紧急问题或 Bug,传统的方式需要重新打包、发布、等待用户更新,这个过程可能会导致修复延迟,影响用户体验。热更新可以立即修复这些问题。
减少发布成本:每次发布新版本的应用都需要经过打包、测试、上架等一系列复杂流程,热更新能够节省开发和发布的时间成本。
增强灵活性:热更新可以动态修改应用中的某些逻辑或资源,提升应用的灵活性。
减少应用重新发布的频率:通过热更新技术,可以减少应用频繁上架应用市场的次数,减少对用户造成的打扰。
热更新的基本原理
Android 热更新的基本原理是通过动态加载机制,替换应用程序中的部分代码或资源,使应用在运行时能够使用新的代码或资源,而无需重新安装或重启应用。其主要实现方式有以下几种:
类替换(Class Replacement):在应用运行时,通过修改类加载器(
ClassLoader
)的加载逻辑,将有问题的类替换为修复后的类。方法替换(Method Hooking):通过字节码修改技术,将应用中的某些方法替换为新的方法,这样可以在不改变整个类的情况下修复部分方法。
资源替换(Resource Replacement):通过动态加载外部资源包的方式,替换应用中的图片、布局、字符串等资源。
热更新的实现方式
目前,Android 热更新技术主要通过以下几种方式实现:
1. Dex 文件替换
Dex 文件是 Android 中的可执行文件格式,它包含了应用程序的字节码。通过热更新技术,可以在应用运行时动态加载新的 Dex 文件,替换掉原来的有问题的类。
关键技术:
- 使用
DexClassLoader
或PathClassLoader
动态加载新的 Dex 文件。 - 通过修改
ClassLoader
的父加载器,将新 Dex 中的类优先加载,覆盖掉原应用中的类。
- 使用
示例流程:
- 应用运行时,发现某个类存在问题。
- 从服务器下载修复后的 Dex 文件。
- 使用
DexClassLoader
动态加载这个 Dex 文件,并替换有问题的类。
优点:可以灵活地替换整个类,适用于修复逻辑错误。
缺点:修改的是整个类,粒度相对较大,且存在兼容性问题。
2. 方法替换(Hook 技术)
方法替换是一种更加精细的热更新技术。通过 Hook 技术,可以在不替换整个类的情况下,仅替换有问题的方法。这种方式使用了字节码操作技术,通过修改运行时方法的字节码来实现方法级别的替换。
关键技术:
- 使用字节码操作框架,如 ASM 或 JavaAssist,在运行时动态修改方法的字节码。
- Hook Android 的类加载器,拦截方法的调用,并替换为修复后的逻辑。
示例流程:
- 应用运行时发现某个方法存在 Bug。
- 从服务器下载包含新方法的字节码。
- 使用字节码修改工具,将旧方法替换为新方法。
优点:可以只替换有问题的方法,避免对整个类的替换,修复粒度更细。
缺点:实现复杂,尤其是在不同版本的 Android 系统上存在兼容性问题。
3. 资源替换
热更新不仅限于代码的修复,某些情况下,应用中的资源文件(如图片、布局、字符串等)也可能需要动态更新。通过资源替换技术,可以在不重启应用的情况下,动态更新应用的资源。
关键技术:
- 动态加载资源包(APK、AAR 或其他格式)。
- 使用反射或
AssetManager
将外部资源与应用的资源系统整合。
示例流程:
- 应用发现某个资源有问题。
- 从服务器下载新的资源文件(如 APK 包中的资源)。
- 使用
AssetManager
加载新资源,替换旧资源。
优点:可以动态更新应用的图片、布局等静态资源,不需要重新安装应用。
缺点:资源的替换相对简单,但可能需要与代码的热更新结合使用。
热更新的常用框架
1. Tinker(微信开源)
Tinker 是腾讯微信团队开源的一个热修复框架,支持类、So 库、资源等多种类型的修复。Tinker 是目前最流行的 Android 热修复框架之一,广泛应用于大多数 Android 应用中。
特点:
- 支持 Dex 修复:可以动态替换应用中的代码。
- 支持资源修复:可以动态加载新的图片、布局等资源。
- 支持 So 库修复:可以修复应用中的本地库文件。
原理:
Tinker 通过生成一个 Patch 文件(补丁包),这个补丁包包含需要修复的 Dex 文件、资源或 So 文件。应用运行时加载这个补丁包,并通过反射和ClassLoader
动态替换原有的代码和资源。Tinker 工作流程:
- 构建补丁包:开发者通过 Tinker 工具生成一个补丁包,包含需要修复的代码和资源。
- 应用加载补丁包:应用启动时,通过 Tinker 框架加载补丁包。
- 动态替换:Tinker 框架通过自定义的
ClassLoader
和AssetManager
将补丁包中的内容替换到原有的应用中。
优势:
- 支持多种修复类型,功能强大。
- 已在多个大型应用中验证,稳定性好。
劣势:
- 实现相对复杂,集成门槛较高。
- 对于大版本更新,热修复的效果有限。
2. AndFix(阿里巴巴开源)
AndFix 是阿里巴巴开源的一个轻量级的热修复框架,专注于方法级别的替换,适合快速修复线上 Bug。
特点:
- 修复粒度小:AndFix 通过修改方法字节码实现热修复,不需要替换整个类。
- 使用方便:无需重新打包应用,可以通过补丁文件直接修复 Bug。
原理:
AndFix 利用JNI
技术修改方法的字节码,从而在运行时替换掉有问题的方法。修复的补丁是基于方法级别的字节码修改,而不是替换整个 Dex 文件。优势:
- 修复粒度小,效率高。
- 实现简单,开发者容易上手。
劣势:
- 只支持方法级别的修复,适用场景有限。
- 由于底层依赖于
JNI
和ASM
,可能会存在一定的兼容性问题。
3. Robust(美团点评开源)
Robust 是美团点评开源的另一个热修复框架,专注于解决类、方法的修复问题。Robust 提供了不同的修复方案,适用于不同的业务场景。
特点:
- 支持类和方法的替换,修复粒度灵活。
- 提供了不同的模式(如全量模式和增量模式)来满足不同的业务需求。
原理:
通过代理机制,Robust 在运行时创建一个类的代理对象,并将代理对象的方法指向修复后的方法实现。通过这种方式,Robust 可以在不修改原始类的情况下,修复其中的 Bug。优势:
- 提供多种修复模式,适用场景广泛。
- 不依赖
JNI
,兼容性较好。
劣势:
- 修复效率相对较低。
- 实现相对复杂。
4. Nuwa
Nuwa 是另一个早期的 Android 热修复框架,Nuwa 采用了类似 Tinker 的实现方式,但更加轻
量,专注于 Dex 文件的修复。
特点:
- 支持 Dex 修复,重点是修复逻辑错误。
- 较为简单,适合快速集成和使用。
优势:
- 实现简单,轻量级。
- 不依赖过多的外部框架。
劣势:
- 不支持资源和 So 文件的修复。
- 功能相对较为单一。
热更新技术的挑战
尽管热更新可以极大地提高开发和发布效率,但实现热更新时仍然面临着一些技术挑战:
兼容性问题:不同版本的 Android 系统中,
ClassLoader
和资源管理机制有所不同,可能导致热更新在某些设备上不兼容或无法正常工作。尤其是在 Android 5.0 及以上,ART 虚拟机引入了新的机制,使得 Dex 文件的修改更加困难。安全性问题:热更新的补丁文件通常需要通过网络下载,这会带来一定的安全风险。如果补丁文件没有做好安全校验,可能被恶意攻击者利用,篡改补丁内容。热更新框架需要对补丁进行严格的签名和校验。
性能问题:由于热更新涉及到类加载器的修改、字节码的替换等操作,可能会带来一定的性能开销。过多或频繁的热更新可能导致应用启动变慢或运行时的性能下降。
系统限制:Google 从 Android 7.0 开始对动态加载的 APK 进行了限制,增加了对
Dex
文件的验证和优化,某些热更新框架在新的 Android 版本上可能失效。
总结
Android 热更新技术为开发者提供了一种在应用运行时动态修复 Bug 或更新代码的能力。通过 Dex 文件替换、方法级别修复、资源替换等方式,开发者可以在不重新打包应用的情况下快速修复问题或更新功能。
常见的热更新框架如 Tinker、AndFix、Robust 等,为开发者提供了不同的解决方案。每种框架都有其独特的实现方式和适用场景,开发者可以根据自己的需求选择合适的框架。
尽管热更新具有显著的优点,但在实际应用中也面临兼容性、安全性和性能方面的挑战。开发者在使用热更新技术时需要权衡利弊,确保应用的稳定性和用户体验。
线程间通信
在 Android 开发中,线程间通信是一项非常重要的任务,尤其是在处理多线程操作时,比如在后台线程中执行耗时任务,然后将结果返回到主线程更新 UI。由于 Android 的 UI 操作只能在主线程中执行,线程间的通信机制变得尤为关键。以下是几种常见的 Android 线程间通信方式:
1. Handler
Handler
是 Android 中最常用的线程间通信工具。它主要用于将消息从后台线程传递到主线程,从而更新 UI。Handler
工作机制是将消息放入一个线程的消息队列中,Looper 再循环处理这些消息。
工作原理:
Looper
负责轮询消息队列。Handler
用于发送消息。Message
是传递的数据载体。
示例:
1 | Handler handler = new Handler(Looper.getMainLooper()); |
2. AsyncTask(已废弃,使用替代方案)
AsyncTask
过去用于在后台线程执行任务并在主线程中返回结果。由于存在内存泄漏风险和并发控制不佳的缺陷,从 Android API 30 开始已经被废弃。
建议使用 ExecutorService
和 Handler
组合来替代 AsyncTask
。
3. Executor + Future + Callable
ExecutorService
是一种管理线程池的方式,可以用来替代 AsyncTask
执行异步任务。通过 Future
可以获取任务的执行结果,使用 Callable
来返回结果。
- 示例:
1 | ExecutorService executor = Executors.newSingleThreadExecutor(); |
4. HandlerThread
HandlerThread
是一个带有 Looper
的线程。通过 HandlerThread
可以轻松地创建后台线程,并且利用 Handler
来处理它的消息队列。
- 示例:
1 | HandlerThread handlerThread = new HandlerThread("MyHandlerThread"); |
5. BroadcastReceiver
BroadcastReceiver
主要用于不同组件(Activity、Service 等)之间的通信。它也可以用于线程间通信,尤其在多个线程之间需要广播事件时。
- 示例:
发送广播:
1
2
3Intent intent = new Intent("com.example.UPDATE_UI");
intent.putExtra("data", "Hello from background");
sendBroadcast(intent);接收广播:
1
2
3
4
5
6
7
8BroadcastReceiver receiver = new BroadcastReceiver() {
public void onReceive(Context context, Intent intent) {
String data = intent.getStringExtra("data");
textView.setText(data);
}
};
registerReceiver(receiver, new IntentFilter("com.example.UPDATE_UI"));
6. EventBus
EventBus
是一个第三方库,它简化了线程间的通信流程。通过发布-订阅模式,可以轻松地在后台线程发布事件,并在主线程订阅和处理这些事件。
- 基本步骤:
- 在后台线程发布事件。
- 在主线程订阅该事件并处理。
总结:
Android 提供了多种线程间通信的机制。最常用的方案是基于 Handler
的方法,因为它与 Android 的消息队列机制紧密集成。对于一些复杂的场景,诸如 ExecutorService
和 HandlerThread
也能提供更灵活的多线程处理能力。
Java
HashMap的实现原理
HashMap
是 Java 集合框架中的一个常用数据结构,基于哈希表(Hash Table)实现,用于存储键值对(key-value)数据。HashMap
的核心思想是通过哈希函数快速定位键的位置,进而实现高效的查找、插入和删除操作。以下是 HashMap
的实现原理及其主要特性。
1. 基本结构
HashMap
主要基于数组和链表(JDK 1.8 之前)或红黑树(JDK 1.8 及之后)的数据结构来实现。它的核心结构如下:
数组(Node[] table):哈希表的核心部分是一个
Node
数组,数组中的每个元素是一个链表的头节点(JDK 1.8 之前)或红黑树的根节点(JDK 1.8 之后)。链表或红黑树:当多个键的哈希值相同时,会发生哈希冲突。在 JDK 1.8 之前,冲突的键值对会以链表的形式存储在数组的同一个位置上;而在 JDK 1.8 之后,当冲突的链表长度超过一定阈值(默认是 8)时,链表会转化为红黑树以提高性能。
2. 存储单元:Node
每个键值对都封装在一个 Node
对象中,Node
是 HashMap
中的内部类。Node
的定义如下:
1 | static class Node<K,V> implements Map.Entry<K,V> { |
hash
:键的哈希值。key
:存储的键。value
:存储的值。next
:用于指向下一个节点(当发生哈希冲突时,形成链表)。
3. 哈希函数
HashMap
通过哈希函数将键映射到数组的索引位置。哈希函数的目的是通过键计算出一个整数(即哈希值),然后通过取模运算将哈希值映射到数组中的一个具体位置。
哈希值的计算:
HashMap
使用键的hashCode()
方法来计算哈希值。为了减少哈希冲突并提高分布的均匀性,HashMap
会进一步处理hashCode()
返回的哈希值。JDK 1.8 中的实现将高位的哈希值与低位异或(XOR)运算以减少冲突:1
2
3
4static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}数组索引计算:
HashMap
使用哈希值 % 数组长度
的方式将哈希值映射到数组的某个索引位置(实际上是通过hash & (table.length - 1)
位运算来计算索引,这比取模运算效率更高)。
4. 处理哈希冲突
哈希冲突发生在多个键具有相同的哈希值并映射到数组的同一位置时。HashMap
通过以下两种方式处理哈希冲突:
1. 链地址法(链表):
在 JDK 1.8 之前,HashMap
处理冲突时使用的是链地址法。多个哈希值相同的元素会被存储在同一个数组位置(即 Node
),形成一个链表。新的元素会插入到链表的末尾。
1 | // 插入新节点 |
2. 红黑树(树化):
在 JDK 1.8 之后,当链表长度超过 8 时,链表会转化为红黑树。红黑树的查找、插入和删除的时间复杂度为 O(log n),相比链表的 O(n) 效率更高。
- 当链表的长度超过阈值(8)时,
HashMap
会自动将链表转换为红黑树。 - 如果红黑树的节点数量减少到 6 以下,树会被重新转换回链表,以节省内存开销。
1 | if (binCount >= TREEIFY_THRESHOLD - 1) // TREEIFY_THRESHOLD is 8 |
5. 扩容机制
HashMap
的底层数组有容量限制,当数组的元素过多时,发生哈希冲突的概率增大,因此需要动态扩容来保持较低的冲突率。HashMap
的扩容机制如下:
触发条件:当哈希表中的元素数量超过数组容量与负载因子(
load factor
)的乘积时,会触发扩容操作。默认负载因子是 0.75。- 计算公式:
元素数量 > 数组容量 * 负载因子
- 计算公式:
扩容过程:扩容时,
HashMap
将数组的容量扩大为原来的两倍,并将旧数组中的所有元素重新哈希并放入新的数组中。这一过程称为rehash。rehash 的计算:重新计算所有键的哈希值并将它们放入新数组,键的位置可能会改变(因为数组容量变大了)。
1 | void resize() { |
6. 查找过程
查找操作通过键来查找相应的值,基本流程如下:
- 通过哈希函数计算出键的哈希值。
- 通过哈希值计算出数组中的索引位置。
- 在对应位置,如果是链表,遍历链表查找对应键;如果是红黑树,通过树的查找逻辑查找相应键。
1 | public V get(Object key) { |
7. 删除操作
删除操作的过程和查找类似,首先根据键计算哈希值,再通过索引找到对应的链表或树结构,找到后将其从链表中删除或从树中移除。
8. HashMap
的性能
时间复杂度:在理想情况下,
HashMap
的查找、插入和删除操作的时间复杂度是 O(1)。这是因为哈希表的设计使得每次操作都能快速通过哈希值定位到元素。- 如果发生哈希冲突并且链表很长,时间复杂度可能退化为 O(n),但在 JDK 1.8 之后,通过红黑树的引入,最坏情况下时间复杂度也只是 O(log n)。
空间复杂度:
HashMap
的空间复杂度主要取决于其底层数组和存储的元素数量。当哈希冲
突严重时,链表和树会占用额外的空间。
总结
HashMap
是一种基于哈希表的数据结构,使用了哈希函数来实现快速的键值对存取。- 它通过数组、链表和红黑树相结合的方式处理哈希冲突,提高查找和插入的效率。
- 它支持动态扩容,通过重新计算哈希值将元素分布到更大的数组中,以保持哈希表的性能。
JNI中Java如何调C++
在JNI(Java Native Interface)中,Java 调用 C++ 代码的过程需要通过定义 Native 方法并进行 JNI 函数调用。以下是Java调用C++代码的详细步骤和机制:
1. 声明 Native 方法
在 Java 中,使用 native
关键字声明一个原生方法,这个方法的实现会在 C/C++ 代码中。Java 代码并不实现这个方法,而是依赖 JNI 来调用 C/C++ 代码。
例如,Java类中可以这样声明一个 Native 方法:
1 | public class NativeExample { |
- **nativeMethod()**:这是在 Java 中声明的原生方法。
- **System.loadLibrary(“native-lib”)**:这个方法会加载名为
native-lib
的本地库,确保 C/C++ 代码可以被调用。
2. 生成 C/C++ 头文件
使用 javac
编译 Java 文件,然后使用 javah
工具生成对应的 C/C++ 头文件(.h
文件)。这个文件会为本地方法生成一个对应的函数声明。
例如:
1 | javac NativeExample.java # 编译 Java 文件 |
javah
工具会生成一个头文件 NativeExample.h
,其中包含对应的 C/C++ 函数声明,例如:
1 | /* Header for class NativeExample */ |
Java_NativeExample_nativeMethod
: 这是由javah
根据 Java 类名和方法名自动生成的 C 函数名。JNIEnv *
: 这是 JNI 环境指针,提供了大量可以在 C/C++ 中调用 Java API 的函数。jobject
: 代表的是 Java 中调用这个方法的实例对象。
3. 实现 Native 方法 (C/C++ 实现)
在生成的头文件基础上,开发者需要实现这个 C/C++ 函数。
例如,可以在 native-lib.cpp
中实现这个函数:
1 |
|
关键点:
- JNIEXPORT 和 JNICALL 是修饰符,确保函数可以被 JNI 机制正确调用。
- JNIEnv 提供了丰富的接口,可以在 C/C++ 中操作 Java 对象或调用 Java 方法。
- jobject 是对 Java 对象的引用,通过它可以访问调用该 Native 方法的 Java 对象。
4. 编译 C/C++ 代码并生成库
将 C/C++ 代码编译成动态库。这个库的名字需要与 Java 中通过 System.loadLibrary()
加载的库名一致。
在 Linux 中,可以这样编译 C++ 代码:
1 | g++ -shared -fPIC -o libnative-lib.so native-lib.cpp -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux |
关键参数:
-shared
: 编译为共享库(即.so
文件)。-fPIC
: 生成与位置无关的代码,以便可以用于动态加载。-o libnative-lib.so
: 指定输出文件为共享库libnative-lib.so
,Java 将加载此库。-I
: 指定包含 JNI 头文件的路径。
5. 运行 Java 代码
在 Java 中,通过 System.loadLibrary("native-lib")
加载编译后的共享库,然后可以直接调用定义的 nativeMethod()
方法。
例如,Java 类 NativeExample
可以这样调用:
1 | public class NativeExample { |
运行程序时,Java 将通过 JNI 调用 native-lib
中的 nativeMethod()
实现,最终执行 C/C++ 代码并输出结果。
6. JNI 调用机制的关键概念
JNIEnv:这是 JNI 环境的指针,JNI 提供了很多函数供 C/C++ 代码调用,例如,调用 Java 方法、访问 Java 对象、操作字符串等。通过
JNIEnv
,可以实现很多 Java 和 C/C++ 的交互。jobject:这是 Java 对象的引用,C/C++ 可以通过这个对象调用 Java 中的成员方法或属性。
类型映射:Java 和 C/C++ 之间的数据类型不完全一致,JNI 提供了一些特定的数据类型,例如:
jint
对应于 Java 的int
jboolean
对应于 Java 的boolean
jstring
对应于 Java 的String
7. 调用Java方法和访问字段
在 C/C++ 中,不仅可以实现 Native 方法,还可以通过 JNI 机制调用 Java 对象的方法,或访问 Java 对象的字段。常用的函数包括:
调用Java方法:
1
2
3jclass clazz = env->GetObjectClass(obj);
jmethodID methodID = env->GetMethodID(clazz, "methodName", "()V");
env->CallVoidMethod(obj, methodID);访问Java字段:
1
2jfieldID fieldID = env->GetFieldID(clazz, "fieldName", "I");
jint fieldValue = env->GetIntField(obj, fieldID);
总结:
Java 调用 C/C++ 代码的流程可以总结为以下步骤:
- 在 Java 中声明
native
方法。 - 通过
javah
工具生成 C/C++ 头文件。 - 在 C/C++ 中实现这些头文件声明的函数。
- 编译 C/C++ 代码为动态库,并在 Java 中加载这个库。
- 通过 JNI 环境(
JNIEnv
)在 C/C++ 中调用 Java 方法、访问字段或进行对象交互。
JNI中C++如何调Java
在JNI(Java Native Interface)中,C++ 调用 Java 方法的过程称为”从Native代码调用Java代码”。这种调用通常发生在C++代码需要通知Java端事件,或使用Java提供的某些功能时。以下是C++调用Java代码的详细步骤和机制:
1. 获取JNI环境指针 (JNIEnv
)
每个Native方法中,JNI环境指针JNIEnv *
是一个非常重要的参数,它提供了调用Java方法的所有接口。在任何一个Native方法中,JNIEnv *
都是自动提供的。可以通过它调用Java中的方法、访问类、对象等。
2. 获取Java类
在调用Java方法之前,C++代码首先需要通过JNIEnv
获取到相应的Java类。可以通过以下方式获取Java类的引用:
1 | jclass clazz = env->FindClass("com/example/YourJavaClass"); |
其中,com/example/YourJavaClass
是Java类的完全限定名(包名+类名)。此时,clazz
是对该Java类的引用。
3. 获取Java方法ID
为了调用Java方法,C++代码需要通过JNIEnv
获取方法的标识符(jmethodID
)。调用Java的方法可能是实例方法(对象方法)或静态方法。
获取实例方法ID:
1
jmethodID methodID = env->GetMethodID(clazz, "methodName", "(I)V");
methodName
: 要调用的Java方法的名称。"(I)V"
: 方法的签名,其中(I)
表示方法参数为一个int
类型,V
表示返回类型为void
。
获取静态方法ID:
1
jmethodID staticMethodID = env->GetStaticMethodID(clazz, "staticMethodName", "(Ljava/lang/String;)V");
GetStaticMethodID
用于获取静态方法的ID。"(Ljava/lang/String;)V"
是方法签名,表示参数为一个String
类型,返回类型为void
。
Java方法的签名格式:
- 基本类型:
Z
:boolean
B
:byte
C
:char
S
:short
I
:int
J
:long
F
:float
D
:double
V
:void
- 引用类型(对象)使用全路径的形式表示,例如
Ljava/lang/String;
表示java.lang.String
。
4. 调用Java方法
获取了Java类和方法ID后,C++代码可以调用该Java方法。具体方法取决于你调用的是实例方法还是静态方法。
调用实例方法:
- 首先需要一个Java对象实例(
jobject
),可以通过传入的参数或者构造方法创建一个对象。 - 然后调用实例方法:
1
2jobject obj = // 已有的Java对象或通过构造方法创建;
env->CallVoidMethod(obj, methodID, 42); // 传入参数42,调用Java方法其中,
CallVoidMethod
是用于调用返回类型为void
的方法。类似地,有:CallIntMethod()
:调用返回int
的方法CallObjectMethod()
:调用返回Java对象的方法CallBooleanMethod()
等其他类型的方法调用函数。
- 首先需要一个Java对象实例(
调用静态方法:
如果调用的是静态方法,就不需要jobject
,而是通过类的引用调用:1
env->CallStaticVoidMethod(clazz, staticMethodID, env->NewStringUTF("Hello from C++"));
5. 创建Java对象实例
如果需要在C++中创建一个Java对象,C++可以通过调用Java类的构造方法来实例化Java对象。过程如下:
获取构造方法ID:
1
jmethodID constructorID = env->GetMethodID(clazz, "<init>", "(Ljava/lang/String;)V");
这里
"<init>"
表示构造方法,(Ljava/lang/String;)V
表示构造方法的签名,即参数是String
,返回值为void
。创建对象:
1
jobject obj = env->NewObject(clazz, constructorID, env->NewStringUTF("Hello"));
上述代码会创建一个新实例,并调用Java类的构造函数。
6. 访问Java字段
除了调用Java方法,C++代码还可以访问Java对象的成员字段(包括静态字段和实例字段)。
获取实例字段ID:
1
jfieldID fieldID = env->GetFieldID(clazz, "fieldName", "I");
fieldName
是Java中的字段名,"I"
表示该字段是int
类型。读取字段值:
1
jint fieldValue = env->GetIntField(obj, fieldID);
使用
Get<Type>Field()
来获取字段值,这里的<Type>
是字段的类型,如Int
、Boolean
、Object
等。设置字段值:
1
env->SetIntField(obj, fieldID, 100);
使用
Set<Type>Field()
设置字段值。
7. 完整示例:C++ 调用 Java 方法
假设我们在 Java 类 JavaExample
中有如下代码:
1 | public class JavaExample { |
在C++中,可以如下调用这个Java类的方法:
1 |
|
8. 异常处理
在使用 JNI 调用 Java 方法时,C++ 代码应该处理可能出现的 Java 异常。可以通过以下方法检查是否有异常抛出:
1 | if (env->ExceptionCheck()) { |
总结:
C++ 调用 Java 方法的流程大致如下:
- 获取JNI环境指针:使用
JNIEnv
来调用Java代码。 - 获取Java类引用:通过
FindClass
获取Java类。 - 获取方法ID:使用
GetMethodID
或GetStaticMethodID
获取实例方法或静态方法的ID。 - 调用方法:使用
Call<Type>Method
或CallStatic<Type>Method
来调用实例方法或静态方法。 - 访问Java字段:使用
GetFieldID
获取字段ID,然后通过Get<Type>Field
或Set<Type>Field
访问或修改字段值。
通过这些步骤,C++代码可以灵活地与Java代码进行交互。
内存模型
JVM(Java Virtual Machine,Java 虚拟机)的内存模型定义了 Java 程序运行时内存的管理方式,包括内存区域的划分、数据的存储方式以及垃圾回收机制等。JVM 内存模型可以划分为多个区域,这些区域用于存储不同类型的数据,并且各区域的生命周期和作用也有所不同。
JVM 内存模型主要分为以下几个部分:
**程序计数器 (Program Counter Register)**:
- 这是一块很小的内存区域,用来存储每个线程当前执行的字节码指令的地址。因为 JVM 是多线程的,每个线程都有一个独立的程序计数器,用于记录当前线程所执行的字节码指令的地址。
- 如果线程正在执行的是一个本地方法(native method),程序计数器为空(undefined)。
**Java 虚拟机栈 (Java Virtual Machine Stack)**:
- 每个线程都有一个独立的 Java 虚拟机栈,栈中存放着每个方法调用的栈帧(Stack Frame),包括局部变量、操作数栈、动态链接和方法的返回地址。
- 局部变量表 存放方法的局部变量,包括基本数据类型(如
int
,long
,float
,double
)和对象的引用。 - 每个方法被调用时,都会在虚拟机栈中创建一个栈帧,方法执行完毕后,栈帧出栈。
- 栈的大小可以通过启动参数
-Xss
来设置。
**本地方法栈 (Native Method Stack)**:
- 与 Java 虚拟机栈类似,但本地方法栈用于存储本地方法调用时的信息,本地方法是指使用 JNI(Java Native Interface)调用的非 Java 方法,如 C 或 C++ 代码。
- 这一部分内存区域是与平台相关的,主要用于处理平台相关的原生代码。
**堆 (Heap)**:
- 堆是 JVM 内存中最大的一块区域,几乎所有对象都存储在堆中,垃圾回收器主要关注的也是这个区域。
- 堆在 JVM 启动时创建,所有线程共享这一块内存,任何线程都可以访问堆中的对象。
- 堆的结构:
- 新生代 (Young Generation): 新生代是对象最先创建的区域,分为三个部分:一个 “Eden” 区和两个 “Survivor” 区(S0 和 S1)。
- 当一个对象首次被创建时,会被分配到 Eden 区,当 Eden 区满时,存活下来的对象会被移到 Survivor 区。
- 在新生代经过几次垃圾回收依然存活的对象,会被晋升到老年代。
- 老年代 (Old Generation): 老年代存储生命周期较长的对象,即从新生代中晋升过来的对象。老年代垃圾回收频率较低,但回收时耗时较长。
- 新生代 (Young Generation): 新生代是对象最先创建的区域,分为三个部分:一个 “Eden” 区和两个 “Survivor” 区(S0 和 S1)。
- 堆内存的大小可以通过
-Xms
和-Xmx
参数来设置。
**方法区 (Method Area)**:
- 方法区是所有线程共享的,用于存储类的元数据(Class Metadata)、常量、静态变量、即时编译器编译后的代码(JIT 代码)等。
- 方法区可以看作是堆的逻辑部分,但它专门用于存储与类相关的结构数据。
- 在 HotSpot JVM 中,方法区是由 元空间 (Metaspace) 实现的。在 Java 8 之前,方法区也被称为永久代(PermGen),从 Java 8 开始,永久代被元空间取代。
- 元空间的内存空间是由系统的本地内存而非 JVM 堆来管理,因此相比之前版本的永久代,减少了 OutOfMemoryError 的问题。
**运行时常量池 (Runtime Constant Pool)**:
- 运行时常量池是方法区的一部分,用于存储编译期生成的各种字面量和符号引用。比如,字符串常量、数字常量、方法引用等。
- 它不仅包括 Java 源代码中的常量,还包含运行时才能确定的动态常量。
JVM 内存模型的内存区域图示:
1 | +------------------------------------+ |
垃圾回收 (Garbage Collection)
JVM 提供了自动的垃圾回收机制,用来释放不再使用的对象。垃圾回收的主要目标是堆内存,特别是新生代和老年代。常用的垃圾回收算法有:
**标记-清除算法 (Mark and Sweep)**:
- 垃圾回收器会首先遍历对象图,标记所有存活的对象。然后,它会清除未被标记的对象,回收相应的内存空间。
**复制算法 (Copying)**:
- 主要用于新生代。它将对象分配到 Eden 区和 Survivor 区之间,并在垃圾回收时将存活的对象复制到空的 Survivor 区,释放其他空间。
**标记-压缩算法 (Mark-Compact)**:
- 主要用于老年代。标记存活对象后,将存活对象压缩到堆的一端,释放连续的内存空间。
总结
JVM 的内存模型通过堆、方法区、栈、程序计数器等多个区域的划分和管理,来保证 Java 程序的高效运行,同时通过垃圾回收机制实现内存自动管理。理解这些内存区域的工作原理有助于优化 Java 程序的性能和内存使用。
垃圾回收
Java 的垃圾回收机制(Garbage Collection,简称 GC)是 Java 语言的一大优势之一,它自动管理内存分配和释放,程序员无需手动处理内存释放,减少了内存泄漏和程序崩溃的可能性。垃圾回收机制通过追踪对象的生命周期,回收不再被引用的对象所占用的内存。
垃圾回收的基本原理
垃圾回收的核心任务是找到并删除不再被程序使用的对象。JVM 使用根可达性算法(Reachability Analysis)来判断对象是否可以被回收。该算法通过判断对象是否可以从GC Root到达(即是否有引用链可达),如果一个对象从 GC Root 不可达,那么该对象就被认为是垃圾,可以被回收。
GC Roots 常见的有:
- 当前线程栈中的局部变量。
- 方法区中静态变量。
- 方法区中的常量。
- JNI 引用的对象(Native 方法中的引用)。
垃圾回收的主要算法
Java 的垃圾回收机制主要依赖以下几种算法:
**标记-清除算法 (Mark-Sweep)**:
- 过程:
- 标记阶段:从 GC Root 开始,遍历所有的引用对象,标记活跃对象。
- 清除阶段:遍历整个堆,回收未被标记的对象。
- 优点:不需要额外空间来存储对象。
- 缺点:容易产生内存碎片,导致较大的对象无法分配连续的内存空间。
- 过程:
**复制算法 (Copying)**:
- 过程:
- 将内存分为两块区域,每次只使用其中的一块。当一块内存空间用完时,将存活的对象复制到另一块空间,然后清空当前的内存空间。
- 优点:不产生内存碎片,分配内存效率较高。
- 缺点:需要额外的内存空间(通常是将新生代内存分为 Eden 和两个 Survivor 区)。
- 应用场景:主要用于 Java 堆中的新生代。
- 过程:
**标记-整理算法 (Mark-Compact)**:
- 过程:
- 首先标记所有的活跃对象,之后将所有的存活对象压缩到内存的一端,保持内存的连续性,最后清除端边界外的空间。
- 优点:避免了内存碎片问题。
- 缺点:整理过程需要移动对象,开销较大。
- 应用场景:主要用于老年代。
- 过程:
分代收集算法:
- 基本思想:Java 中的对象按其生命周期长短被分为不同的代,垃圾回收器采用不同的算法来管理不同区域的内存。
- 分代结构:
- **新生代 (Young Generation)**:新创建的对象会首先分配到新生代。新生代中的对象通常“朝生夕死”,垃圾回收频繁,回收速度较快。
- Eden 区:大多数新对象在这里分配。
- Survivor 区:包含两个区 (S0, S1),存活的对象会从 Eden 区复制到其中一个 Survivor 区,当该 Survivor 区满时,活跃对象会复制到另一个 Survivor 区或进入老年代。
- **老年代 (Old Generation)**:存活时间较长、生命周期较长的对象会被晋升到老年代,老年代的垃圾回收频率较低,但耗时较长。
- **永久代/元空间 (Permanent Generation/Metaspace)**:存储类的元数据(在 Java 8 之后,永久代被元空间取代)。
- **新生代 (Young Generation)**:新创建的对象会首先分配到新生代。新生代中的对象通常“朝生夕死”,垃圾回收频繁,回收速度较快。
Java 垃圾回收器
JVM 中有多种不同的垃圾回收器,适合不同的应用场景,常见的有:
Serial GC:
- 单线程的垃圾回收器,适用于小型应用程序。
- 优点:简单、低内存开销。
- 缺点:GC 时会暂停所有应用线程(即所谓的“Stop the World”),效率较低。
Parallel GC(也叫做 “吞吐量优先垃圾回收器”):
- 多线程垃圾回收器,适合高吞吐量的应用程序。
- 优点:并行执行 GC,能够处理大量的对象分配。
- 缺点:在高响应需求的应用中,可能不够快速。
**CMS GC (Concurrent Mark-Sweep Garbage Collector)**:
- 低延迟的垃圾回收器,适用于需要低停顿时间的应用程序。
- 优点:标记和清除过程是并发执行的,减少了长时间的停顿。
- 缺点:会产生内存碎片,并且在高并发下,垃圾回收线程与应用线程可能会争抢资源。
**G1 GC (Garbage First Garbage Collector)**:
- Java 7 引入的一种新的垃圾回收器,设计用于取代 CMS。
- 分区收集:将堆划分为多个相同大小的区域,根据区域内的垃圾回收优先级进行回收,优先清理垃圾最多的区域。
- 优点:能够提供更可预测的停顿时间,适合大内存、多处理器的系统,降低了大规模老年代 GC 的延迟。
- 缺点:比 CMS 更复杂。
**ZGC (Z Garbage Collector)**:
- 在 Java 11 引入的一种超低延迟垃圾回收器,设计用于处理超大堆内存。
- 优点:能处理 TB 级别的堆,并且 GC 停顿时间非常短(通常在 10 毫秒以下)。
- 缺点:实现复杂,并且相比传统 GC 可能会占用更多的内存。
垃圾回收的触发机制
垃圾回收不是随时都可以发生的,通常会在以下情况下触发:
- 当堆内存中的新生代(Eden 区)空间耗尽时,会触发Minor GC,它只清理新生代。
- 当堆内存中的老年代空间耗尽时,会触发Major GC 或 Full GC,它会清理整个堆,包括新生代和老年代。Full GC 的代价较高,通常伴随长时间的暂停。
垃圾回收的调优
为了提高应用的性能,开发者可以根据实际需求调整垃圾回收器的策略。调优时可以根据以下参数进行设置:
- -Xms 和 -Xmx:设置堆的初始大小和最大大小。
- -XX:NewSize 和 -XX:MaxNewSize:设置新生代的大小。
- -XX:SurvivorRatio:设置 Eden 区与 Survivor 区的比例。
- -XX:+UseG1GC:启用 G1 垃圾回收器。
- -XX:+UseZGC:启用 ZGC 垃圾回收器。
总结
Java 的垃圾回收机制通过自动回收不再使用的对象,简化了内存管理,同时不同的垃圾回收器和算法适应不同类型的应用场景。在高性能应用中,理解和调优垃圾回收机制是提升应用性能的重要手段。
常用设计模式
设计模式是软件开发中的一种通用解决方案,用于解决某类常见的设计问题。设计模式并不是具体的代码,而是经过总结的、可复用的解决方案,可以帮助开发者编写高效、可维护和可扩展的代码。设计模式分为三大类:创建型模式、结构型模式和行为型模式。以下是常用的设计模式及其解释和应用场景。
1. 创建型模式
创建型模式关注对象的创建方式,目的是将对象的创建过程与其使用分离,以提高系统的灵活性和可扩展性。
1.1 单例模式 (Singleton Pattern)
意图:保证一个类只有一个实例,并提供一个全局访问点。
- 应用场景:用于需要一个类有且仅有一个实例的情况,比如全局配置对象、日志系统、线程池等。
- 实现:通过私有化构造函数、提供一个静态方法来获取唯一的实例。
1 | public class Singleton { |
1.2 工厂方法模式 (Factory Method Pattern)
意图:定义一个创建对象的接口,但让子类决定实例化哪个类。工厂方法让类的实例化推迟到子类中进行。
- 应用场景:需要根据不同的条件创建不同类型的对象时,可以使用工厂方法模式。
- 实现:通过定义一个抽象工厂类,子类根据需要创建具体对象。
1 | public interface Product { |
1.3 抽象工厂模式 (Abstract Factory Pattern)
意图:提供一个接口,用于创建相关或依赖对象的家族,而不需要指定具体的类。
- 应用场景:当需要创建多个相关联的对象时,使用抽象工厂模式。
- 实现:定义多个工厂接口,分别用于创建相关的产品族。
1 | public interface GUIFactory { |
1.4 建造者模式 (Builder Pattern)
意图:将对象的创建与表示分离,使得相同的创建过程可以构建不同的对象。
- 应用场景:用于创建复杂对象,特别是当构建过程复杂时,如创建包含多个步骤的对象时。
- 实现:通过一个
Builder
类逐步构造复杂对象,最后返回完整对象。
1 | public class Product { |
2. 结构型模式
结构型模式关注类和对象的组合,帮助我们更好地组织代码结构,特别是在类继承和对象组合方面提供更灵活的解决方案。
2.1 适配器模式 (Adapter Pattern)
意图:将一个类的接口转换成客户希望的另一个接口,使得原本由于接口不兼容而不能一起工作的类可以协同工作。
- 应用场景:当你需要使用一个已有的类,但它的接口与其他代码不兼容时,使用适配器模式。
- 实现:通过创建一个适配器类,桥接客户类和需要适配的类。
1 | public interface Target { |
2.2 装饰器模式 (Decorator Pattern)
意图:动态地给对象添加额外的职责,而不是通过继承来扩展功能。
- 应用场景:用于需要动态地为对象添加功能的时候,例如对核心功能进行增强。
- 实现:通过装饰器类包装原始对象,并在装饰器类中添加新功能。
1 | public interface Component { |
2.3 代理模式 (Proxy Pattern)
意图:为其他对象提供一个代理,以控制对该对象的访问。
- 应用场景:用于延迟加载、控制访问权限、在访问对象时添加额外的逻辑等场景。
- 实现:代理类实现目标接口,并控制对目标对象的访问。
1 | public interface Service { |
3. 行为型模式
行为型模式关注对象之间的交互及职责的划分,定义了对象之间如何通信和协作。
3.1 观察者模式 (Observer Pattern)
意图:定义对象间的一对多依赖,当一个对象的状态发生改变时,所有依赖于它的对象都会自动收到通知。
- 应用场景:用于事件处理机制,发布-订阅系统,典型的应用是 GUI 事件处理和消息系统。
- 实现:通过维护观察者列表,在被观察者状态变化时通知所有观察者。
1 | public interface Observer { |
3.2 策略模式 (Strategy Pattern)
意图:定义一系列算法,将每个算法封装起来,并使它们可以相互替换。策略模式使得算法的变化不会影响使用算法的客户。
- 应用场景:用于需要动态选择算法或行为的场景,例如支付方式的选择、排序算法的切换等。
- 实现:将不同的算法实现封装到具体的策略类中,通过统一接口调用不同的策略。
1 | public interface Strategy { |
3.3 命令模式 (Command Pattern)
意图:将请求封装成一个对象,从而可以用不同的请求对客户进行参数化,以及对请求排队或记录请求日志。
- 应用场景:用于执行请求操作的场景,如按钮点击、撤销操作等。
- 实现:通过将请求封装为命令对象,然后在需要的时候调用这些命令对象。
1 | public interface Command { |
总结
- 创建型模式 解决对象创建问题,确保系统中的对象创建过程灵活可控(如单例、工厂、建造者等)。
- 结构型模式 解决类或对象的组合问题,确保系统的不同部分能灵活、有效地协作(如适配器、代理、装饰器等)。
- 行为型模式 解决对象之间的交互问题,确保对象之间的职责分配合理且合作顺畅(如观察者、策略、命令等)。
不同的设计模式有助于解决软件开发中的常见问题,通过合理选择合适的设计模式,可以提高代码的可维护性、可复用性和可扩展性。
并发编程
Java 并发编程是指在多线程环境下,协调多个线程并发执行任务的编程技术。并发编程是 Java 语言中的一个重要特性,它允许程序在多核处理器上充分利用计算资源,提高程序的运行效率。然而,并发编程也带来了线程同步、数据共享和竞态条件等挑战。Java 提供了一整套工具和类库来帮助开发者编写高效且安全的并发程序。
为什么需要并发编程?
- 提高性能:在多核处理器上,通过并发编程可以让多个任务同时执行,提高程序的效率。
- 优化资源使用:并发编程允许程序在 I/O 操作(如网络请求、文件读写)或其他阻塞操作期间执行其他任务,从而更好地利用系统资源。
- 响应性:在 GUI 程序中,使用并发编程可以让主线程负责界面渲染,而后台线程执行耗时任务,避免用户界面卡顿。
Java 并发编程的基本概念
1. 线程
线程是 Java 并发编程的基本单位。每个线程代表程序中的一个执行路径。Java 提供了 Thread
类和 Runnable
接口来创建和管理线程。
- 创建线程:
- 通过继承
Thread
类:1
2
3
4
5
6
7
8class MyThread extends Thread {
public void run() {
System.out.println("Thread is running");
}
}
MyThread thread = new MyThread();
thread.start(); // 启动线程 - 通过实现
Runnable
接口:1
2
3
4
5
6
7
8class MyRunnable implements Runnable {
public void run() {
System.out.println("Runnable is running");
}
}
Thread thread = new Thread(new MyRunnable());
thread.start();
- 通过继承
2. 线程的生命周期
Java 线程的生命周期通常包括以下几个阶段:
- 新建(New):线程被创建,但还没有启动。
- 就绪(Runnable):线程已经准备好,并等待 CPU 资源进行执行。
- 运行(Running):线程正在执行中。
- 阻塞(Blocked):线程在等待某个条件(如锁、I/O)时进入阻塞状态。
- 死亡(Terminated):线程的任务完成,或因异常退出,线程进入终止状态。
线程同步与共享资源
当多个线程共享同一个资源时,可能会发生竞态条件(Race Condition),导致程序出现错误。Java 提供了多种机制来实现线程同步,以确保多个线程能够安全地访问共享资源。
1. synchronized 关键字
synchronized
是 Java 中最基础的同步机制,它用于锁定某个对象,使得同一时间只有一个线程能够访问被锁定的代码块或方法。
- 同步方法:
1
2
3public synchronized void method() {
// 同步代码,确保同一时间只有一个线程执行此方法
} - 同步代码块:
1
2
3
4
5public void method() {
synchronized(this) {
// 同步代码块
}
}
当线程进入 synchronized
方法或代码块时,会自动获取对象的锁,一旦线程完成执行或抛出异常,锁会被自动释放。
2. volatile 关键字
volatile
关键字用于标记变量,使其在多个线程间可见。它确保了变量的修改会立即被写入主内存,并且每次读取时都从主内存读取。
- 使用场景:适用于简单的共享变量(如状态标志)的场景,但不适合复杂的同步逻辑。
1
private volatile boolean flag = true;
3. Lock 接口
Lock
是一种更灵活的锁机制,相比 synchronized
,它提供了更多的控制和功能。例如,Lock
可以在获取不到锁时等待,也可以尝试非阻塞地获取锁。
- ReentrantLock:常用的
Lock
实现类,支持可重入锁。1
2
3
4
5
6
7Lock lock = new ReentrantLock();
lock.lock(); // 获取锁
try {
// 临界区
} finally {
lock.unlock(); // 释放锁
}
线程间通信
线程之间需要通过某种方式进行通信,以协调它们的执行顺序。Java 提供了多种线程间通信的机制。
1. wait() / notify() / notifyAll()
wait()
、notify()
和 notifyAll()
是 Java 提供的原始线程通信方法,它们必须在同步块或同步方法中使用。
wait()
:使当前线程进入等待状态,直到另一个线程调用notify()
或notifyAll()
。notify()
:唤醒等待该对象锁的某一个线程。notifyAll()
:唤醒等待该对象锁的所有线程。
示例:
1 | class SharedResource { |
2. Condition 接口
Condition
是与 Lock
配合使用的线程通信机制,它提供了类似 wait()
和 notify()
的功能,但更为灵活。每个 Lock
对象可以创建多个 Condition
实例,实现更精细的线程通信控制。
- 使用
Condition
:1
2
3
4
5
6
7
8
9
10Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
lock.lock();
try {
condition.await(); // 当前线程等待
condition.signal(); // 唤醒等待的线程
} finally {
lock.unlock();
}
Java 并发工具类
Java 的 java.util.concurrent
包提供了许多用于并发编程的工具类,简化了线程的管理和调度。
1. Executor 框架
Executor
框架提供了一种更高级的管理线程池的方法,避免了直接操作线程。ExecutorService
是 Executor
的子接口,提供了更多的方法来管理线程池中的任务。
创建线程池:
1
2
3
4
5ExecutorService executor = Executors.newFixedThreadPool(5); // 创建固定大小的线程池
executor.submit(() -> {
System.out.println("Task is running");
});
executor.shutdown(); // 关闭线程池常见线程池类型:
newFixedThreadPool(int nThreads)
:固定大小的线程池。newCachedThreadPool()
:根据需要创建新线程的线程池,但会重用先前的线程。newSingleThreadExecutor()
:单线程池,确保所有任务按顺序执行。
2. Future 和 Callable
Callable
是 Runnable
的增强版本,允许任务有返回值。Future
是 Callable
的返回值,表示一个异步计算的结果。
- Callable 使用示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14Callable<Integer> task = () -> {
Thread.sleep(1000);
return 123;
};
ExecutorService executor = Executors.newFixedThreadPool(2);
Future<Integer> future = executor.submit(task);
try {
Integer result = future.get(); // 阻塞等待任务完成并获取结果
System.out.println(result); // 输出 123
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
3. CountDownLatch
CountDownLatch
是一种同步工具,允许一个或多个线程等待其他线程完成某些操作。它使用一个计数器,线程调用 countDown()
递减计数器,直到计数器归零,等待的线程才会被唤醒。
- 示例:
1
2
3
4
5
6
7
8
9
10
11CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
new Thread(() -> {
System.out.println("Thread " + Thread.currentThread().getName() + " finished.");
latch.countDown(); // 计数器减一
}).start();
}
latch.await(); // 等待计数器归零
System.out.println("All threads finished.");
4. CyclicBarrier
CyclicBarrier
是一种允许一组线程彼此等待到达某个公共屏障点的同步工具。
使用场景:适用于多线程任务需要在某个时间点共同执行的场景。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39CyclicBarrier barrier = new CyclicBarrier(3);
for (int i = 0; i < 3; i++) {
new Thread(() -> {
System.out.println("Thread " + Thread.currentThread().getName() + " is waiting.");
try {
barrier.await(); // 所有线程都等待,直到屏障被释放
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println("Thread " + Thread.currentThread().getName() + " is released.");
}).start();
}
### 总结
Java 并发编程是构建高效、多线程应用的核心技术。通过线程的创建、同步、通信和 `java.util.concurrent` 工具包,Java 提供了一个强大的并发编程框架。使用线程池、锁、并发集合、同步机制等工具,可以帮助开发者编写高效、安全的并发程序。开发者需要注意线程的生命周期、同步问题、死锁、并发性能等问题,以确保程序的稳定性和性能。
## 常用的锁
在并发编程中,锁是用于协调多个线程对共享资源的访问,防止线程间的竞态条件和数据不一致问题。Java 提供了多种锁的实现,每种锁适用于不同的场景,具有各自的特性和用法。下面是 Java 中常用的几种锁及其详细介绍。
---
### 1. **synchronized 关键字**
`synchronized` 是 Java 中最基本的内置锁(也叫**监视器锁**),它可以修饰方法或代码块,用于确保同一时间只有一个线程能够执行该方法或代码块,保证线程安全。
#### 特点:
- **可重入**:`synchronized` 是可重入锁,意味着同一个线程可以多次获得同一把锁,而不会发生死锁。
- **内置锁**:`synchronized` 是 JVM 层面提供的机制,不需要手动管理锁的获取和释放,JVM 会自动处理。
- **效率较低**:在高并发场景下,`synchronized` 的性能相对较低,尤其是在锁竞争激烈时,可能导致线程的频繁阻塞和唤醒。
#### 用法:
- 修饰实例方法:
```java
public synchronized void method() {
// 线程安全的代码
}修饰代码块:
1
2
3
4
5public void method() {
synchronized(this) {
// 线程安全的代码
}
}
2. ReentrantLock
ReentrantLock
是 Lock
接口的常用实现类,它是一个可重入锁,类似于 synchronized
,但它提供了更多的控制和功能。
特点:
- 手动加锁与解锁:与
synchronized
不同,ReentrantLock
需要手动调用lock()
获取锁,使用完后需要手动调用unlock()
释放锁。 - 可重入性:与
synchronized
类似,ReentrantLock
允许同一线程多次获取同一把锁。 - 可中断性:在获取锁时,线程可以选择响应中断,这意味着线程可以被中断,从而避免永久等待锁。
- 公平锁与非公平锁:
ReentrantLock
支持公平锁和非公平锁,公平锁按照线程请求锁的顺序来分配锁,非公平锁则允许插队。
用法:
非公平锁:
1
2
3
4
5
6
7Lock lock = new ReentrantLock();
lock.lock(); // 获取锁
try {
// 临界区代码
} finally {
lock.unlock(); // 释放锁
}公平锁:
1
Lock lock = new ReentrantLock(true); // 公平锁
3. ReentrantReadWriteLock
ReentrantReadWriteLock
是一种读写锁,允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。这种锁设计的目的是提高并发读的性能,因为读操作不会互相阻塞。
特点:
- 读锁和写锁分离:
ReentrantReadWriteLock
提供了两种锁:readLock()
和writeLock()
。多个线程可以同时获取读锁,但只有一个线程可以获取写锁,并且写锁与读锁互斥。 - 可重入性:读锁和写锁都是可重入的,意味着同一个线程可以多次获取读锁或写锁。
- 写锁优先:默认情况下,写锁的优先级高于读锁,意味着写锁会在等待时阻止其他线程获取读锁。
用法:
1 | ReadWriteLock rwLock = new ReentrantReadWriteLock(); |
4. StampedLock
StampedLock
是 Java 8 引入的一种改进的读写锁。它提供了类似 ReentrantReadWriteLock
的读写锁功能,但在某些场景下提供了更高效的并发控制。StampedLock
通过使用时间戳(stamp)来管理锁状态。
特点:
- 不可重入:与
ReentrantReadWriteLock
不同,StampedLock
是不可重入的,意味着线程在同一锁上不能重复加锁。 - 乐观读锁:
StampedLock
提供了一种乐观读锁机制,允许在没有加锁的情况下进行读取操作,但在执行完读取操作后需要确认锁的有效性。乐观读锁可以提高并发读的性能。 - 传统读写锁:
StampedLock
也提供传统的读锁和写锁。
用法:
乐观读锁:
1
2
3
4
5
6
7
8
9
10
11
12
13
14StampedLock stampedLock = new StampedLock();
long stamp = stampedLock.tryOptimisticRead();
// 进行读取操作
// 验证锁的有效性
if (!stampedLock.validate(stamp)) {
// 如果锁在读取过程中被修改,重新获取锁
stamp = stampedLock.readLock();
try {
// 读操作
} finally {
stampedLock.unlockRead(stamp);
}
}写锁:
1
2
3
4
5
6long stamp = stampedLock.writeLock();
try {
// 写操作
} finally {
stampedLock.unlockWrite(stamp);
}
5. CountDownLatch
CountDownLatch
是一种用于线程同步的工具类,它允许一个或多个线程等待其他线程完成某些操作。CountDownLatch
通过一个计数器来控制线程的执行,当计数器减为零时,所有等待的线程将继续执行。
特点:
- 一次性使用:
CountDownLatch
是一次性的,计数器减为零后无法重置。 - 线程等待:一个线程或多个线程可以调用
await()
方法等待,其他线程完成任务后调用countDown()
减少计数器。
用法:
1 | CountDownLatch latch = new CountDownLatch(3); |
6. CyclicBarrier
CyclicBarrier
是另一种用于线程同步的工具类,允许一组线程相互等待,直到所有线程都到达某个屏障点。与 CountDownLatch
不同的是,CyclicBarrier
可以多次使用。
特点:
- 可重复使用:
CyclicBarrier
可以在所有线程到达屏障后重复使用,适用于需要多轮同步操作的场景。 - 可执行回调:当所有线程到达屏障时,可以执行一个可选的回调操作。
用法:
1 | CyclicBarrier barrier = new CyclicBarrier(3, () -> { |
7. Semaphore
Semaphore
是一种计数信号量,用于控制同时访问某个资源的线程数。它可以用于实现限流,限制同时访问共享资源的线程数量。
特点:
- 控制访问数量:
Semaphore
通过一个计数器来控制可用的资源数量,线程调用acquire()
获取资源,调用release()
释放资源。 - 多线程访问控制:适用于限制多个线程同时访问共享资源的场景,如数据库连接池等。
用法:
1 | Semaphore semaphore = new Semaphore(3); // 允许同时有三个线程访问资源 |
8. ReadWriteLock
ReadWriteLock
是一种特殊类型的锁,允许
多个读线程同时执行,但只允许一个写线程执行。适用于读多写少的场景,提高并发性能。
特点:
- 读锁可共享:多个线程可以同时获取读锁。
- 写锁独占:写锁只能被一个线程持有,且与读锁互斥。
用法:
1 | ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); |
总结
Java 提供了多种并发编程中的锁和同步机制,以满足不同场景下的需求:
- **
synchronized
**:最简单的同步机制,用于基本的互斥访问。 - **
ReentrantLock
**:提供了更多控制和功能的可重入锁。 ReentrantReadWriteLock
和 **StampedLock
**:适用于读多写少的场景,提供了读写分离的锁机制。CountDownLatch
和 **CyclicBarrier
**:用于线程同步,前者是一次性使用,后者可重复使用。- **
Semaphore
**:用于控制同时访问某个资源的线程数。 - **
ReadWriteLock
**:允许多个线程同时读取,但只允许一个线程写入。
根据具体的应用场景,选择合适的锁机制可以有效提高并发程序的性能和可靠性。
四种引用
在Java中,有四种引用类型:强引用、软引用、弱引用和虚引用。
强引用:最常见的引用方式,Java垃圾回收器不会回收被强引用的对象。只要强引用存在,对象就会一直存在。
软引用:主要用于缓存,当系统内存不足时,垃圾回收器会回收这些对象。适合用来实现内存敏感的缓存。
弱引用:比软引用更弱,垃圾回收器在下一次回收时会回收所有只被弱引用的对象。这适用于描述一些非强依赖的对象。
虚引用:也称为幽灵引用,不会影响对象的生命周期,只能用来跟踪对象被垃圾回收器回收的状态。通常和
ReferenceQueue
一起使用。
线程间通信
在 Java 中,线程间通信是一项关键技术,用于在多线程环境中协同处理数据和任务。Java 提供了多种线程间通信的方式,常见的方式如下:
1. 使用 wait()
、notify()
和 notifyAll()
这是 Java 中经典的线程间通信方式,基于对象的监视器锁(Monitor)。wait()
、notify()
和 notifyAll()
是 Object
类中的方法,用于在线程间进行协调与通信。
**
wait()
**:当前线程进入等待状态,并释放持有的对象锁,直到另一个线程调用notify()
或notifyAll()
方法唤醒它。**
notify()
**:唤醒在该对象监视器上等待的某一个线程。**
notifyAll()
**:唤醒在该对象监视器上等待的所有线程。示例:
1 | class SharedResource { |
在上述例子中,ThreadA
将等待资源的释放,而 ThreadB
负责释放资源。
2. 使用 synchronized
关键字
sychronized
是 Java 中用于线程同步的关键字,它可以保证同一时间只有一个线程能访问被 synchronized
修饰的代码块或方法,从而避免线程间的竞争问题。
- 示例:
1 | class Counter { |
在这个例子中,increment()
和 getCount()
方法是同步的,确保同一时刻只有一个线程能修改 count
的值,从而避免数据竞争。
3. 使用 Lock
和 Condition
(从 java.util.concurrent
包)
Lock
和 Condition
提供了比 synchronized
更灵活的线程同步机制。Lock
类允许手动获取和释放锁,而 Condition
类则类似于 wait()
和 notify()
,但具有更大的灵活性。
Lock:提供显式的锁定和解锁操作,支持更复杂的同步控制。
Condition:可以让线程等待某个条件满足,再继续执行,类似于
Object.wait()
和Object.notify()
。示例:
1 | import java.util.concurrent.locks.Condition; |
4. 使用 BlockingQueue
BlockingQueue
是 Java 的并发包中的一种线程安全的队列,它提供了用于生产者-消费者模式的通信机制。多个线程可以安全地将元素放入或取出队列,BlockingQueue
会自动处理线程之间的同步问题。
- 示例:
1 | import java.util.concurrent.ArrayBlockingQueue; |
5. 使用 Semaphore
Semaphore
是一种计数信号量,用于控制同时访问特定资源的线程数量。它可以用于限制某些资源的访问,例如限制一个文件同时只能被最多 3 个线程访问。
- 示例:
1 | import java.util.concurrent.Semaphore; |
6. 使用 Exchanger
Exchanger
用于两个线程之间交换数据。一个线程调用 exchange()
方法将数据传给另一个线程,同时接收对方的数据。
- 示例:
1 | import java.util.concurrent.Exchanger; |
总结
Java 提供了多种线程间通信的机制,每种方式适合不同的场景:
wait()
/notify()
适合简单的生产者-消费者模型。Lock
和Condition
提供了更加灵活的同步控制。BlockingQueue
和Semaphore
适用于并发资源管理。Exchanger
适合两个线程之间的数据交换。
选择适合的线程通信方式需要根据实际的应用场景和需求。
JVM 中 Java 对象的创建过程
在 JVM 中,Java 对象的创建是一个复杂且精细的过程,涉及多个步骤。以下是 Java 对象创建的主要流程:
1. 类加载检查
- 在创建一个对象之前,JVM 会首先检查该对象所属的类是否已经被加载、解析和初始化。如果类还没有被加载,JVM 会触发类加载过程(包括加载、链接和初始化)。
- 类加载是通过类加载器(ClassLoader)完成的。这个过程确保了所有必要的类信息都已准备就绪,包含字段和方法等定义。
2. 内存分配
- 在类加载完成后,JVM 会为新对象分配内存。对象的内存分配通常是在堆中进行的。具体分配内存的位置取决于堆的结构和垃圾收集器的实现。
- JVM 会根据对象的大小,从堆中划分一块空间用于存储对象的实例变量、元数据等信息。一般来说,对象的内存大小由类中的字段(包括继承的字段)和对象头(Object Header)共同决定。
- JVM 中的内存分配方式有两种:
- 指针碰撞:如果堆中的内存是规整的,没有被垃圾回收器整理过,那么 JVM 可以直接通过移动指针来分配内存。
- 空闲列表:如果堆中内存不规整,有分散的空闲区域,JVM 会通过一个空闲列表来找到一块合适的内存区域分配给新对象。
3. 内存区域初始化为零值
- 为了保证对象的安全性,JVM 会将新分配的内存空间初始化为零值。这意味着对象实例变量(包括基本类型和引用类型)在分配后默认为零或
null
值。这一过程确保了对象的默认值在构造方法之前被初始化。
4. 设置对象头
- JVM 在对象内存中分配了一个对象头(Object Header),用于存储对象的元数据。
- 对象头通常包括以下信息:
- Mark Word:包含对象的哈希码、GC 信息、锁状态等。
- 类指针(Class Pointer):指向对象所属的类的元数据,表示该对象是哪个类的实例。
- 如果是数组对象,JVM 还会在对象头中存储数组的长度信息。
5. 执行构造方法( 方法)
- JVM 完成对象的内存分配和初始化后,会调用对象的构造方法(即
new
语句后自动调用的<init>
方法)。 - 构造方法用于进行对象的进一步初始化,可能包括显式赋值和其他逻辑操作。
- 构造方法调用结束后,JVM 完成了整个对象创建过程。
6. 返回对象的引用
- 构造方法执行完成后,对象的创建过程就结束了。JVM 返回对象的引用地址(或指针),并将其存储到变量中,从而可以在程序中访问和使用该对象。
补充:锁与同步
- 对象创建完成后,JVM 会确保对象头中的锁状态处于“无锁”状态。
- 当对象用于同步操作(例如
synchronized
关键字)时,对象头中的 Mark Word 将被更新,以存储锁的信息。
总结
Java 对象的创建过程涉及从类加载到内存分配,再到初始化和构造的多个步骤。这些步骤确保每个对象都是从其所属的类定义中产生的,并且初始值是安全的。这种流程体现了 JVM 对对象管理的精确控制和优化。
动态代理
动态代理是一种在程序运行时动态创建代理对象的技术,主要用于拦截方法调用,以在不修改原始代码的情况下对方法调用进行增强或修改。Java 提供了两种主要的动态代理实现方式:JDK 动态代理和 CGLIB 动态代理。
1. JDK 动态代理
JDK 动态代理是 Java 内置的一种动态代理实现方式。它基于接口来生成代理对象,因此只适用于代理实现了接口的类。核心类是 java.lang.reflect.Proxy
和 InvocationHandler
接口。
实现步骤
- 定义接口:目标对象需要实现一个或多个接口,以便 JDK 动态代理能够基于接口生成代理对象。
- 实现 InvocationHandler:创建一个类实现
InvocationHandler
接口,重写invoke
方法。这个方法会在代理对象的方法被调用时触发,在invoke
方法中可以添加增强逻辑。 - 生成代理对象:使用
Proxy.newProxyInstance
方法生成代理对象。此方法需要提供类加载器、接口列表和InvocationHandler
实例。
示例代码
1 | public interface Service { |
特点
- 基于接口:JDK 动态代理要求目标对象必须实现接口。
- 性能较高:由于代理是基于接口实现的,通常运行速度较快。
- 代理方法范围:只能代理实现了接口的方法,无法直接代理类中的具体方法。
2. CGLIB 动态代理
CGLIB(Code Generation Library)是一个第三方库,通过生成字节码的方式实现代理,它基于继承生成代理类,因此可以代理不实现接口的类。CGLIB 是通过继承目标类并重写其方法来实现代理的,这种方式使用了 ASM 字节码生成框架。
实现步骤
- 引入 CGLIB 库:CGLIB 不是 JDK 自带的库,需要手动添加依赖。
- 实现 MethodInterceptor:创建一个实现
MethodInterceptor
接口的类,重写intercept
方法。这个方法会在代理对象的方法被调用时执行,可以在此方法中添加增强逻辑。 - 生成代理对象:使用
Enhancer
类生成代理对象。Enhancer
可以创建任何类的代理,除非该类是final
类(因为final
类不能被继承)。
示例代码
1 | import org.springframework.cglib.proxy.Enhancer; |
特点
- 基于继承:CGLIB 是通过继承目标类来实现代理的,因此可以代理不实现接口的类。
- 性能相对较低:CGLIB 代理性能略低于 JDK 动态代理,但适用于无接口的情况。
- 限制性:不能代理
final
类,因为 CGLIB 需要通过继承来实现动态代理。
3. 动态代理的应用场景
动态代理在许多框架中广泛应用,尤其是 AOP(面向切面编程)和拦截器模式。常见的应用场景包括:
- 权限检查:在方法调用之前检查调用方是否具有相应权限。
- 日志记录:在方法调用之前或之后添加日志记录逻辑。
- 事务管理:在方法调用开始和结束时分别开启和关闭事务。
- 远程调用代理:在客户端调用服务端的远程方法时,通过代理封装调用细节。
- 缓存处理:在方法执行前后进行缓存查询或缓存更新,以提高效率。
4. JDK 动态代理和 CGLIB 代理的选择
- JDK 动态代理:适用于代理实现了接口的对象,通常比 CGLIB 代理速度更快,且不需要额外依赖。
- CGLIB 动态代理:适用于没有接口的类代理,但代理创建速度相对较慢,且生成的代理对象较重。CGLIB 是通过字节码生成实现的,因此比 JDK 动态代理更灵活。
在 Spring AOP 中,默认情况下会优先使用 JDK 动态代理,如果目标类没有实现接口,则会退而使用 CGLIB 动态代理。
总结
动态代理为程序提供了强大的灵活性和可扩展性,特别适合需要对方法调用进行拦截和增强的场景。通过动态代理,开发者可以在运行时创建代理对象,实现如权限校验、日志、事务等切面功能,极大地提高了代码的解耦性和可维护性。
C++
智能指针的类型
C++ 的智能指针是为了解决传统指针的内存管理问题而引入的,它们能够自动管理动态分配的内存,避免内存泄漏和悬挂指针问题。C++11 标准引入了三种主要的智能指针类型:std::unique_ptr
、std::shared_ptr
和 std::weak_ptr
,它们分别适用于不同的内存管理场景。
1. std::unique_ptr
概述:
std::unique_ptr
是一种独占所有权的智能指针,即一个对象只能被一个 unique_ptr
拥有。当 unique_ptr
被销毁时,它所管理的对象也会自动销毁。unique_ptr
不允许复制,但可以通过移动语义将所有权转移给另一个 unique_ptr
。
主要特点:
- 独占所有权:一个对象只能被一个
unique_ptr
拥有,不能共享。 - 移动语义支持:可以通过
std::move
将unique_ptr
转移到另一个unique_ptr
。 - 自动销毁:当
unique_ptr
退出作用域或被销毁时,自动调用delete
来释放内存。
示例:
1 |
|
使用场景:
- 独占资源的场景,比如文件句柄、网络连接、动态分配的内存等。
- 当不希望多个指针共享同一个对象时,使用
unique_ptr
来确保对象的唯一所有权。
2. std::shared_ptr
概述:
std::shared_ptr
是一种共享所有权的智能指针,多个 shared_ptr
可以同时指向同一个对象,并通过引用计数来管理对象的生命周期。只有当最后一个 shared_ptr
被销毁时,所管理的对象才会被释放。
主要特点:
- 共享所有权:多个
shared_ptr
可以共享同一个对象,每个shared_ptr
都增加引用计数。 - 引用计数:每次复制
shared_ptr
,引用计数会增加,销毁时引用计数会减少,当引用计数降为 0 时,对象会被释放。 - 线程安全:引用计数的增加和减少是线程安全的,但对象本身的操作并不是线程安全的。
示例:
1 |
|
使用场景:
- 当需要多个对象共享同一个资源时,比如在图结构、树结构中,多个节点可以共享相同的子节点。
- 适合动态分配的资源需要在多个对象之间共享,且不确定资源何时释放的场景。
3. std::weak_ptr
概述:
std::weak_ptr
是一种不参与引用计数的智能指针,用于解决 std::shared_ptr
循环引用的问题。weak_ptr
不会影响引用计数,它提供了一种弱引用的机制,允许访问对象但不会控制对象的生命周期。weak_ptr
必须通过 lock()
方法提升为 shared_ptr
才能访问对象。
主要特点:
- 不增加引用计数:
weak_ptr
只是对shared_ptr
的弱引用,不会增加对象的引用计数。 - 解决循环引用问题:在
shared_ptr
循环引用的场景中,使用weak_ptr
可以打破循环引用。 - 检查对象是否仍然存在:通过
expired()
方法可以判断对象是否已经被释放。
示例:
1 |
|
使用场景:
- 解决
shared_ptr
的循环引用问题,例如双向链表或父子对象之间的引用。 - 当需要观察一个对象的生命周期但不希望参与其管理时,可以使用
weak_ptr
。
4. std::auto_ptr
(已废弃)
std::auto_ptr
是 C++98 引入的一种早期智能指针类型,但由于它的所有权语义不清晰(复制时所有权会转移)和不支持 C++11 的现代功能(如移动语义),已在 C++11 中被废弃,并由 unique_ptr
取而代之。现代 C++ 应避免使用 auto_ptr
,转而使用 unique_ptr
。
各类智能指针对比
智能指针类型 | 所有权 | 引用计数 | 线程安全 | 使用场景 |
---|---|---|---|---|
unique_ptr |
独占所有权 | 不支持 | 不支持 | 适合独占的资源管理,不需要多个指针共享对象 |
shared_ptr |
共享所有权 | 支持 | 增减计数是线程安全的 | 适合需要多个指针共享同一个对象的场景 |
weak_ptr |
无所有权 | 不支持 | 不支持 | 解决 shared_ptr 的循环引用问题,弱引用对象 |
总结
C++ 提供了多种智能指针来管理动态内存,它们各有不同的应用场景和使用特点:
- **
std::unique_ptr
**:独占所有权,适用于不需要共享的资源管理。 - **
std::shared_ptr
**:共享所有权,适用于需要多个对象共享同一资源的场景。 - **
std::weak_ptr
**:弱引用,用于解决循环引用问题或需要观察对象但不参与管理的场景。
通过智能指针的合理使用,可以有效地避免手动内存管理中的常见错误,如内存泄漏、悬挂指针等问题。
Kotlin
音视频
视频播放器的实现原理
视频播放器的基本实现原理包括了音视频文件的解码、同步播放、显示和用户交互等过程。一个完整的视频播放器通常由多个模块协作完成,从读取媒体数据到音视频的同步输出。以下是视频播放器的基本实现原理和关键模块:
1. 媒体文件的读取和解封装
容器格式和封装格式:
视频文件通常包含视频流、音频流和其他数据,如字幕等,这些数据被封装在容器格式中(如 MP4、AVI、MKV、FLV 等)。播放器的第一步就是将这些封装格式打开,提取出音频、视频和其他流。
- **解封装 (Demuxing)**:解封装是指将多媒体容器文件中的不同流(视频流、音频流等)分离出来。这个过程依赖于容器的解析器(Demuxer)。常见的库有 FFmpeg,它可以处理多种封装格式。
- 流的分离:解封装后,视频和音频流会被分别提取,用于后续的解码操作。
2. 音视频的解码
编码格式:
音频和视频流通常是压缩的,以节省存储空间和带宽。视频通常使用 H.264、H.265(HEVC)等编码格式,音频则可能使用 AAC、MP3 等。播放器需要对这些压缩的音视频流进行解码。
- 视频解码器:解码器(Codec)将压缩的视频流解码为原始的帧数据(如 YUV 格式的图像帧)。
- 音频解码器:解码音频流,将压缩的音频数据解码为可播放的音频采样数据(如 PCM 格式)。
解码的复杂性取决于视频和音频的编码格式,现代播放器通常使用第三方库(如 FFmpeg)来支持多种解码方式。解码过程的性能需求较高,解码速度必须足够快才能保证实时播放。
3. 音视频同步
时间戳 (PTS/DTS):
为了确保音频和视频同步播放,每个音频帧和视频帧都带有时间戳。播放器根据这些时间戳进行音视频同步。
- **PTS (Presentation Time Stamp)**:表示该帧需要在什么时候进行播放。
- **DTS (Decoding Time Stamp)**:表示该帧需要在什么时候进行解码。对于一些编解码格式,DTS 和 PTS 可能不一样。
音视频同步算法:
通常,播放器会以音频播放为基准,通过音频输出的时间来控制视频帧的显示。如果发现音频和视频不同步,播放器会调整视频帧的播放时间,或丢弃/重复某些视频帧,以确保播放的流畅性和同步性。
4. 渲染和播放
视频渲染:
- 视频帧的显示:视频帧数据通常是 YUV 格式,需要转换为 RGB 格式,然后交由显示设备(如屏幕)渲染。转换和渲染过程依赖于图形处理单元 (GPU),现代视频播放器通常会使用硬件加速技术(如 OpenGL、DirectX 或 Vulkan)来提高渲染效率。
- 显示刷新率匹配:为了确保播放平滑,播放器需要将视频的帧率与显示器的刷新率进行匹配。例如,如果视频是 24fps,而显示器是 60Hz,播放器需要适当插入或重复帧,保证播放平滑。
音频播放:
- 音频输出:解码后的音频数据(通常是 PCM 格式)需要送到音频设备进行播放。常用的音频输出接口有 OpenAL、ALSA、DirectSound 等。
- 音频缓冲:音频播放通常需要使用缓冲区来存储待播放的音频数据。播放器会将解码好的音频数据填充到缓冲区,音频硬件则从缓冲区中取出数据进行播放。音频缓冲的大小需要合适,如果过大,会导致音画不同步,过小则可能导致音频播放卡顿。
5. 控制逻辑和用户交互
播放器还需要处理用户输入的各种控制指令,例如:
- 播放、暂停、停止:播放器需要在解码和渲染时对这些命令做出响应,控制音视频的解码和输出过程。
- 快进、快退:播放器需要快速跳过未播放的部分,重新读取和解码目标位置的音视频帧。
- 音量控制、静音:控制音频播放时的音量。
- 字幕显示:如果视频文件带有字幕,播放器需要将字幕文件解析出来,并在适当的时间同步显示在视频画面上。
6. 缓冲和网络播放
对于在线视频播放器,还需要处理网络延迟和不稳定性。网络播放器会预先从网络缓冲一定量的数据,以保证播放流畅性。
- 缓冲机制:在网络播放中,播放器需要下载并缓存音视频数据。缓冲的大小和策略可以根据网络带宽和视频编码的特性来调整。
- **自适应码率 (ABR)**:对于流媒体播放,播放器需要根据网络状况调整播放视频的码率,以便在网络状况恶化时能维持流畅的播放。
7. 硬件加速
现代视频播放器通常使用硬件加速来提升性能,特别是在处理高分辨率视频(如 4K、8K)时。硬件加速的常见形式包括:
- 硬件解码:通过 GPU 或专用解码芯片进行视频解码,减轻 CPU 负担。
- 硬件渲染:使用 GPU 渲染视频帧,提升渲染效率和显示效果。
常见的硬件解码接口包括:
- VDPAU:适用于 Linux 平台的硬件加速解码接口。
- DXVA:用于 Windows 的硬件解码接口。
- MediaCodec:Android 平台的硬件解码接口。
视频播放器实现流程总结
- 读取和解析文件:播放器打开媒体文件,解封装得到音视频流。
- 音视频解码:通过解码器解码音视频流,得到可用的音频采样和视频帧数据。
- 音视频同步:根据音频和视频的时间戳,保持音视频同步播放。
- 视频渲染:将解码的原始视频帧渲染到屏幕上,通常使用 GPU 进行加速渲染。
- 音频播放:将解码后的音频数据送到音频设备进行播放。
- 用户交互:响应用户的播放、暂停、快进、音量调节等操作。
- 流媒体支持:通过网络缓冲和自适应码率调整,确保在线播放的流畅性。
总结
一个视频播放器的基本实现原理主要包括媒体数据的解封装、解码、音视频同步、渲染和播放。为了保证播放的流畅性和性能,现代播放器通常依赖硬件加速、优化的解码算法和用户友好的交互设计。对于在线视频,播放器还需要处理网络延迟、码率调整等问题。
视频直播的实现原理
视频直播是一种实时将视频数据从一个或多个源传输到远程观众的技术。它的实现涉及多个关键步骤,包括视频采集、编码、传输、解码和播放,依赖于多个技术协议和优化手段来确保低延迟和高质量的实时视频体验。以下是视频直播的基本实现原理和关键技术。
视频直播的基本流程
视频采集
- 视频直播从采集视频和音频数据开始。数据可以来自摄像头、麦克风或其他音视频设备。
- 采集设备会将原始的视频和音频数据传送给直播服务器或直播软件进行处理。
- 视频采集通常以帧为单位进行,常见的帧率有 24fps、30fps 或 60fps。
视频编码
- 原始视频数据通常体积庞大,未经压缩的音视频数据直接传输会占用大量带宽。为了减少传输带宽,视频采集后需要进行压缩处理,这就是视频编码。
- 常见的视频编码格式包括 H.264、H.265(HEVC),而音频编码格式则常见的有 AAC、MP3 等。
- 编码的目的是:在保证视频质量的前提下,压缩视频数据大小,使其可以在网络上实时传输。
- 实时编码需要考虑延迟问题,因此通常选择具备快速编码能力的编解码器。
封装
- 编码后的音视频数据会被封装成特定格式的流文件,用于网络传输。
- 封装协议决定了音视频如何打包以及数据的传输顺序。常见的封装格式有 FLV、MP4 等。
- FLV 是视频直播中最常见的封装格式之一,因为它支持流媒体播放,适合直播应用。
推流
- 视频编码和封装后,数据通过网络传输到直播服务器,称为推流(Publishing Stream)。
- 常见的推流协议有:
- **RTMP (Real-Time Messaging Protocol)**:最常用的直播推流协议,低延迟,广泛支持。
- **HLS (HTTP Live Streaming)**:适用于跨平台播放,延迟较高,但兼容性好,适合点播和直播。
- **SRT (Secure Reliable Transport)**:低延迟,高可靠性,适合在不稳定网络下传输。
- **WebRTC (Web Real-Time Communication)**:用于实现浏览器端的实时音视频通信,延迟极低。
直播服务器和分发
- 直播服务器:接收推流并进行处理(如转码、多码率处理、流的分发等)。直播服务器还会维护与观众的连接,并将流数据分发到观众端。
- **内容分发网络 (CDN)**:为了解决大量用户同时观看时的带宽和负载问题,直播流通常通过 CDN 分发。CDN 在全球各地部署服务器,通过缓存和负载均衡来加速直播内容的分发,减少延迟并提高可用性。
- 转码:为了适应不同网络条件和设备,直播服务器通常会对原始视频流进行多码率转码。不同质量的流可以满足用户的不同带宽和设备条件。
拉流和播放
- 拉流(Pulling Stream):观众端的播放器通过特定的协议向直播服务器请求视频流,称为拉流。
- 播放器根据接收到的视频流数据进行解码和播放。
- 播放器需要支持直播使用的协议,如 RTMP、HLS 或 WebRTC。通常播放器内部实现了缓冲机制,确保即使网络有波动,也能提供连续的播放体验。
关键技术和优化
1. 编码优化
- 在直播中,编码器的选择至关重要,既要确保较高的压缩效率,又要控制延迟。
- 硬件加速:为了减少编码的延迟,现代直播系统往往使用硬件加速编码器(如 GPU 加速)来提高编码效率,减少编码延迟。
- 自适应码率:视频直播系统通常支持自适应码率(ABR),根据用户当前的网络状况,自动调整视频流的质量,以保证播放的流畅性和低延迟。
2. 传输协议的选择
- RTMP:低延迟的实时传输协议,支持推流到服务器并在观众端实时播放,延迟通常在 1-3 秒范围内。
- HLS:基于 HTTP 的直播协议,兼容性好但延迟较高(通常在 6-30 秒)。HLS 通过将视频分割成若干小的 TS 文件进行传输。
- SRT:提供更好的传输可靠性和抗网络抖动能力,适合复杂网络条件下的低延迟传输。
- WebRTC:浏览器端实时音视频通信协议,延迟通常在毫秒级,非常适合对延迟敏感的场景(如互动直播、视频会议等)。
3. CDN 和边缘节点优化
- CDN 通过在全球多个节点部署缓存服务器,将直播内容分发到离观众最近的服务器节点,减少网络延迟和带宽压力。
- 边缘计算:某些直播服务在 CDN 的边缘节点上进行实时处理和转码,减少直播服务器的负载,并提高分发效率。
4. 延迟控制
- 低延迟是直播技术的一个重要目标,尤其是在互动性较强的场景中(如在线游戏直播、体育赛事、在线教育等)。通过以下手段可以减少直播的延迟:
- 减少编码延迟:使用快速编码器或硬件加速。
- 减少传输延迟:使用低延迟传输协议,如 WebRTC、SRT 或低延迟 RTMP。
- 减少播放器的缓冲:尽量减少播放器端的缓冲时间,虽然这可能会增加播放过程中的卡顿风险,但可以显著降低延迟。
5. 网络抖动与丢包处理
- 在网络不稳定的环境下(如移动网络),直播系统需要应对网络抖动和丢包问题。
- **FEC (Forward Error Correction)**:通过在传输数据中增加冗余信息来修复丢失的数据包。
- **ARQ (Automatic Repeat reQuest)**:当检测到数据包丢失时,接收端请求发送端重新发送丢失的数据包。
- 自适应传输机制:根据网络带宽的变化,自动调整视频流的码率,确保流畅性。
实现原理的示例流程
假设有一个主播使用 RTMP 协议推流至直播平台,观众通过 HLS 协议观看的典型流程:
- 主播通过摄像头和麦克风采集音视频数据。
- 音视频数据经过编码(H.264 视频编码,AAC 音频编码),并封装成 FLV 格式,通过 RTMP 协议推送到直播平台服务器。
- 直播平台服务器接收流后,可能对流进行转码,生成不同清晰度的版本,以适应不同网络状况的观众。
- 转码后的流通过 CDN 分发到各个边缘节点,离观众最近的节点提供服务。
- 观众使用支持 HLS 的播放器向 CDN 请求视频流,并按顺序播放收到的 HLS 切片文件(.ts 文件)。
- 播放器对视频流进行解码,将解码后的画面和音频同步输出,实现直播观看。
总结
视频直播的实现涉及多个环节,从视频采集到推流、服务器处理、内容分发和最终播放,每个环节都影响着直播的质量和延迟。选择合适的编码方式、传输协议以及内容分发策略对于实现高质量、低延迟的直播体验至关重要。
网速差如何保证流畅
在网络状况不理想时,直播画面可能会卡顿或画质下降。为了保证在网络条件差的情况下仍然能够提供流畅的观看体验,可以采取多种技术手段进行优化。以下是一些常用的策略:
1. 自适应码率流 (Adaptive Bitrate Streaming, ABR)
自适应码率技术是目前直播系统最常用的优化手段。它根据用户的网络状况动态调整视频流的码率,以保证观看流畅性。
- 原理:直播平台通常会将同一个直播视频流生成多种不同清晰度的版本(不同的分辨率和码率),如 480p、720p、1080p 等。当网络带宽变差时,播放器会自动切换到低清晰度、低码率的流,从而减少带宽占用,确保视频播放流畅。
- 技术实现:
- 对于 HLS 或 DASH 等协议,播放器端可以自动选择适合当前带宽的流进行播放。
- CDN 或服务器端在推送视频流时,会根据带宽监控情况为用户推送合适码率的视频。
2. 降低视频分辨率和帧率
当网络条件变差时,适当降低视频的分辨率和帧率可以减少需要传输的数据量,从而提高流畅性。
- 降低分辨率:比如从 1080p 降低到 720p 或 480p。低分辨率视频所需的带宽更少,可以在较差的网络环境中流畅播放。
- 降低帧率:通过减少每秒传输的帧数(例如从 60fps 降低到 30fps 或 15fps),可以大幅减少需要传输的数据量。虽然帧率降低会影响视频的细节流畅度,但在网络条件极差时,降低帧率是非常有效的手段。
3. 视频编码器优化
选择合适的编码器和优化编码参数,能够在较低码率下保持较高的视频质量。
- H.265 (HEVC) 或 VP9:相比 H.264,H.265 和 VP9 编解码器在同等画质下具有更高的压缩效率,适合在低带宽环境下使用。
- 动态码率调整:实时编码时,编码器可以根据当前场景复杂度动态调整码率。例如,视频中变化较少的静态场景可以使用较低的码率,而运动较多的场景可以使用较高码率。
- 编码参数调整:减少关键帧频率(如 I 帧间隔)等编码设置,能够减少需要传输的关键帧数量,从而减少带宽占用。
4. 提高缓冲策略
适当增加播放器的缓冲区大小,使其在网络波动时仍能继续播放缓存中的视频数据,减少卡顿。
- 增大缓冲区:在网络抖动较大时,适当增加缓冲区的大小(如将几秒的缓冲时间增加到 10 秒),可以让播放器预加载更多的视频数据,从而应对网络的暂时中断或抖动问题。
- 逐步加载策略:在缓冲区不足时,播放器可以动态调整加载策略,首先下载关键帧和低质量数据,确保视频不会卡顿。
5. 使用低延迟传输协议
选择合适的低延迟传输协议来减少视频传输时的延迟,尤其是在网络条件不稳定时,可以使用以下协议:
- **SRT (Secure Reliable Transport)**:SRT 协议具有抗网络抖动和丢包的功能,能够在低带宽和高延迟的环境下提供更稳定的流传输效果。
- WebRTC:WebRTC 是一种超低延迟传输协议,适合需要极低延迟的直播场景。WebRTC 能够在不稳定的网络环境下通过自适应码率、拥塞控制等机制保持较流畅的视频体验。
- 低延迟 HLS:低延迟 HLS 通过减少切片的大小和缓存时间,使直播延迟缩短,同时保持一定流畅性。
6. 网络丢包与错误修正
在差网络环境下,网络丢包是常见问题,通过一些错误修正和丢包重传机制可以提升传输效果。
- **FEC (Forward Error Correction)**:前向纠错技术会在数据传输中加入冗余数据,以便在一定程度上修复丢失的包,而无需重传,减少由于网络丢包导致的卡顿。
- **ARQ (Automatic Repeat Request)**:当传输过程中数据包丢失时,通过 ARQ 机制请求重新发送丢失的数据包,确保数据的完整性。
7. 优化直播的网络环境
虽然技术手段可以在一定程度上提高流畅性,但优化网络环境也是至关重要的手段。
- 选择合适的 CDN:通过全球分布的内容分发网络 (CDN),直播流可以通过就近的服务器节点传输到用户端,减少网络延迟。
- 网络带宽管理:对于移动设备,建议使用 4G 或 5G 网络,避免使用 Wi-Fi 网络不稳定的环境。在宽带不高的网络条件下,可以通过限制其他应用程序的网络使用,确保直播有足够的带宽。
8. 使用预加载和智能缓存
- 智能缓存机制:通过智能分析用户的带宽情况和当前的视频播放进度,提前加载未来几秒的视频流数据。即使网络突然波动,用户仍能利用缓存的数据,继续播放视频,避免卡顿。
- 渐进式加载:在加载视频时,优先加载低分辨率的内容并逐渐切换到高分辨率。这种方式能确保即使网络不稳定,用户也能快速开始观看直播。
9. 降低非必要开销
- 去除非关键数据流:在网络条件差时,可以暂停或去掉非关键的数据流,例如关闭不必要的字幕、统计信息等,减少带宽占用。
- 优化交互逻辑:在一些互动性较强的直播场景中,降低实时互动频率,减少数据的往返传输,可以保证视频和音频的流畅性。
总结
为了在网络条件较差的情况下保持视频直播的流畅性,可以通过自适应码率流、降低分辨率和帧率、优化编码器、提高缓冲、使用低延迟协议、网络丢包处理等技术手段来优化直播体验。同时,选择合适的网络传输架构(如 CDN)和改善本地网络环境也非常重要。
常用流媒体协议
流媒体协议是指在网络上传输音频、视频等多媒体内容的协议,它们定义了如何在不同设备和网络条件下高效地传输数据。常见的流媒体协议有多个,每个协议都有其独特的特点和应用场景,主要用于视频点播、直播、实时通信等领域。下面是常用的几种流媒体协议的详细介绍:
1. RTMP(Real-Time Messaging Protocol)
概述
RTMP 是 Adobe 公司开发的一种用于音视频数据传输的流媒体协议,最初用于 Flash 播放器。虽然 Flash 已经逐渐被淘汰,但 RTMP 仍然在视频直播中得到广泛应用,特别是在推流和传输中。
特点:
- 低延迟:RTMP 提供了较低的传输延迟,通常用于实时直播场景。
- TCP 协议:RTMP 基于 TCP 协议,提供可靠的数据传输,保证数据的完整性。
- 适用于推流:RTMP 常用于从客户端推送视频流到服务器,或者从服务器向播放器分发直播视频。
- 逐渐被 HLS 替代:随着 HLS 等协议的普及,RTMP 的应用正在逐渐减少,特别是在终端播放器上,但它仍广泛应用于服务器端的推流过程。
使用场景:
- 视频直播推流:RTMP 常用于客户端(如 OBS)向服务器推送直播流。
- 视频点播:一些旧的 Flash 视频点播系统仍然使用 RTMP。
架构示例:
- 推流:客户端(如摄像头或编码器)通过 RTMP 协议将音视频数据推送到流媒体服务器(如 Wowza、NGINX-RTMP)。
- 拉流:观众设备通过 RTMP 拉取音视频流,实现低延迟观看。
2. HLS(HTTP Live Streaming)
概述
HLS 是由苹果公司开发的一种基于 HTTP 的流媒体传输协议。它通过将媒体流分割成若干小的文件(TS 切片),并通过标准 HTTP 协议进行传输。这种方式使得 HLS 可以在任意支持 HTTP 的平台上工作,且具备良好的兼容性。
特点:
- 广泛支持:HLS 是移动设备上最常用的流媒体协议,iOS、macOS 原生支持 HLS。
- 基于 HTTP 传输:HLS 通过 HTTP 进行传输,具备很好的兼容性,可以利用现有的 HTTP 服务器和 CDN(内容分发网络)进行分发。
- 适应性流媒体:HLS 支持自适应比特率流(ABR),可以根据网络条件动态切换视频质量,保证在不同带宽下的流畅播放。
- 高延迟:HLS 的一个主要缺点是延迟较高,通常在 10-30 秒,主要是由于每个切片的时长(通常为 2-6 秒)和缓冲机制所致。
使用场景:
- 视频点播(VOD):HLS 常用于视频点播服务,如 Netflix 和 YouTube。
- 直播流媒体:HLS 也用于实时直播,但在一些对低延迟要求较高的场景(如在线教育、游戏直播)中效果不佳。
工作原理:
- 切片:将视频文件切分为多个 TS 小文件,并生成一个
.m3u8
文件来索引这些切片。 - 传输:客户端根据
.m3u8
文件,通过 HTTP 协议逐个下载 TS 切片并进行播放。 - 自适应:客户端根据网络带宽和播放情况动态选择合适的切片质量。
3. DASH(Dynamic Adaptive Streaming over HTTP)
概述
DASH 是一种与 HLS 类似的流媒体传输协议,也基于 HTTP 传输。它是由 MPEG 组织开发的标准协议,旨在提供自适应的流媒体传输。
特点:
- 跨平台支持:DASH 是开放标准,相较于 HLS 在苹果平台的主导地位,DASH 在 Android 和 Windows 等平台上也有广泛支持。
- 自适应比特率流:与 HLS 类似,DASH 支持自适应比特率流,可以根据网络带宽切换不同清晰度的流。
- 基于 HTTP:DASH 通过 HTTP 进行传输,兼容 HTTP 服务器和 CDN。
- 更灵活的编码支持:DASH 支持多种编码格式,如 H.264、H.265、VP9 等,具有更高的灵活性。
使用场景:
- 视频点播:DASH 常用于高清视频点播服务,支持 4K、HDR 等高质量视频内容。
- 实时流媒体:DASH 支持实时流媒体,但与 HLS 一样,延迟通常较高。
工作原理:
- 切片与索引文件:DASH 将视频切片为若干小段,并通过 MPD(Media Presentation Description)文件进行索引,类似于 HLS 的
.m3u8
文件。 - 动态适应:客户端根据网络状况选择不同质量的切片,进行动态适应,提供最佳播放体验。
4. RTSP(Real-Time Streaming Protocol)
概述
RTSP 是一种用于控制多媒体流的网络协议,它与 RTP 协议配合使用,用于流媒体播放控制。RTSP 定义了如何在客户端和服务器之间建立和管理流媒体会话,但实际的数据传输通常通过 RTP 进行。
特点:
- 实时流媒体传输:RTSP 常用于实时视频传输场景,尤其是在视频监控、IP 摄像机中广泛应用。
- 基于 RTP:RTSP 本身不传输数据,数据传输由 RTP(Real-Time Transport Protocol)完成。RTP 使用 UDP 进行传输,具备低延迟的优势。
- 复杂的控制功能:RTSP 提供了丰富的控制功能,如播放、暂停、快进、倒退等操作。
- 灵活性:RTSP 非常灵活,适用于实时媒体流的传输和控制。
使用场景:
- 视频监控:RTSP 是许多 IP 摄像机、监控系统中传输视频的主要协议。
- 视频会议:RTSP 被广泛应用于视频会议系统中,提供实时音视频传输。
工作原理:
- 会话管理:RTSP 定义了会话控制命令,如
PLAY
、PAUSE
、SETUP
等,用于控制媒体流的播放。 - 数据传输:RTSP 通过 RTP 协议传输音视频数据。RTP 通常基于 UDP 传输,能够实现低延迟的视频播放。
5. WebRTC(Web Real-Time Communication)
概述
WebRTC 是一种用于实时音视频通信的开源协议,支持点对点的低延迟音视频传输。它主要用于实时通信场景,例如视频聊天、视频会议和直播互动。
特点:
- 超低延迟:WebRTC 的延迟通常在毫秒级,非常适合实时通信场景。
- 点对点通信:WebRTC 支持通过 P2P(点对点)进行音视频传输,可以减少服务器的压力,并提供更快的传输速度。
- 跨平台支持:WebRTC 在现代浏览器中得到了广泛支持,如 Chrome、Firefox 等。
- 安全性:WebRTC 默认使用加密传输(DTLS 和 SRTP),确保数据的安全性。
使用场景:
- 视频聊天和会议:例如 Google Meet、Zoom 等视频会议工具使用 WebRTC 实现点对点通信。
- 实时直播:WebRTC 也用于超低延迟的互动直播场景,特别是在需要观众和主播实时互动的场景中。
- 游戏直播与多人互动:如需要极低延迟的场景,例如多人游戏直播和在线协作应用。
工作原理:
- 信令交换:在建立 WebRTC 连接之前,客户端通过信令交换(如通过 WebSocket)传递会话描述信息(SDP),确定媒体格式和传输方式。
- 点对点通信:一旦连接建立,音视频数据通过 P2P 进行传输,减少中转服务器的负载和延迟。
6. SRT(Secure Reliable Transport)
概述
SRT 是一种开源的传输协议,专为在不稳定网络条件下进行安全和低延迟的音视频传输设计。SRT 具有抗丢包、抗抖动的特性,适合在复杂网络环境下的直播流媒体传输。
特点:
- 低延迟:SRT 专为低延迟传输设计,适合高质量
直播。
- 安全性:SRT 提供了端到端的加密,确保数据传输的安全性。
- 抗丢包与抖动:SRT 内置了丢包重传机制和网络抖动缓冲,能够在网络状况不佳的情况下保证音视频流的完整性和质量。
- 基于 UDP:SRT 基于 UDP 协议,但通过丢包重传等机制,实现了可靠的数据传输。
使用场景:
- 远程直播:SRT 适合用于跨国直播、远程摄像机传输等需要高质量和低延迟的视频传输场景。
- 复杂网络环境下的直播:在丢包率较高的网络中,SRT 能够提供较好的传输性能。
工作原理:
- 丢包重传:SRT 使用 NACK 反馈机制检测丢包,并重新传输丢失的数据包。
- 加密传输:SRT 提供 AES 加密,确保传输过程中的数据安全。
总结
协议 | 传输方式 | 延迟 | 安全性 | 使用场景 |
---|---|---|---|---|
RTMP | 基于 TCP | 低 | 无 | 直播推流、低延迟视频传输 |
HLS | 基于 HTTP | 高 | 支持 | 视频点播、直播(延迟容忍度较高) |
DASH | 基于 HTTP | 中等 | 支持 | 高清视频点播、自适应流传输 |
RTSP | 基于 UDP(RTP) | 低 | 无 | 视频监控、视频会议 |
WebRTC | 基于 P2P/UDP | 超低 | 支持 | 视频聊天、视频会议、互动直播 |
SRT | 基于 UDP | 低 | 支持 | 跨国直播、远程视频传输 |
不同的流媒体协议有各自的优缺点,开发者需要根据具体的应用场景(如延迟需求、网络环境、传输稳定性等)选择合适的协议。
DTS 和 PTS 有什么区别
DTS(Decoding Time Stamp,解码时间戳)和 PTS(Presentation Time Stamp,显示时间戳)是视频和音频流中两个重要的时间戳概念,通常用于多媒体容器格式如 MPEG、MKV 等,目的是同步音视频的播放。它们的区别主要体现在解码和播放的时序上:
DTS(解码时间戳):
- 作用:告诉解码器什么时候开始解码某一帧的数据。
- 含义:当视频流中的某一帧可能需要提前解码(例如在有 B 帧的情况下),解码器会根据 DTS 值进行解码,但不一定立刻显示。
- 应用:在包含帧重排序的视频编码(如 H.264)中,DTS 代表该帧应当被解码的时刻。
PTS(显示时间戳):
- 作用:告诉播放器什么时候把解码后的帧显示到屏幕上。
- 含义:PTS 决定了解码后的帧何时被呈现给观众或播放出来。PTS 通常和播放的时间轴直接相关。
- 应用:PTS 用于确保帧按照正确的顺序和时间展示,尤其是对于具有复杂帧类型(如 I 帧、P 帧和 B 帧)的流。
总结:
- DTS 控制解码时间,PTS 控制显示时间。
- DTS 不一定总是存在,特别是在没有帧重排序的流中。
- PTS 是多媒体流中最常见的时间戳,用于保持音视频同步。
对于大多数视频播放场景,播放器会优先使用 PTS 确保音视频的同步播放,而 DTS 则更多地在视频解码过程中起作用。
数据结构和算法
数据结构和算法是计算机科学的核心,它们帮助我们以有效的方式存储、组织和处理数据。掌握常用的数据结构和算法对编写高效程序至关重要。下面将介绍一些常用的数据结构和算法,并提供相应的示例代码。
1. 常用数据结构
1.1 数组 (Array)
数组是一种固定大小的线性数据结构,存储相同类型的元素。它支持通过索引快速访问元素,但由于大小固定,插入和删除操作相对复杂。
- 时间复杂度:
- 访问元素:O(1)
- 插入/删除元素:O(n) (最坏情况下,需要移动元素)
示例(Java):
1 | int[] arr = {1, 2, 3, 4, 5}; |
1.2 链表 (Linked List)
链表是一种动态的数据结构,由节点组成,每个节点包含数据部分和指向下一个节点的指针。链表分为单链表和双链表。与数组不同,链表支持动态扩展,插入和删除操作高效,但随机访问性能较差。
- 时间复杂度:
- 访问元素:O(n)
- 插入/删除元素:O(1)(当给定节点位置时)
示例(Java 单链表):
1 | class Node { |
1.3 栈 (Stack)
栈是一种后进先出(LIFO)的数据结构。栈支持在栈顶进行插入和删除操作,常用于递归、括号匹配等问题。
- 时间复杂度:
- 压栈/出栈:O(1)
- 访问元素:O(n)
示例(Java 使用栈):
1 | import java.util.Stack; |
1.4 队列 (Queue)
队列是一种先进先出(FIFO)的数据结构,常用于排队处理任务。队列支持在队尾插入数据,并在队首删除数据。
- 时间复杂度:
- 入队/出队:O(1)
- 访问元素:O(n)
示例(Java 使用队列):
1 | import java.util.LinkedList; |
1.5 哈希表 (Hash Table)
哈希表是一种基于哈希函数的数据结构,用于快速查找、插入和删除数据。哈希表通过将键映射到特定的存储位置,能够实现接近 O(1) 时间复杂度的查找。
- 时间复杂度:
- 查找/插入/删除:平均 O(1),最坏 O(n)
示例(Java 使用哈希表):
1 | import java.util.HashMap; |
1.6 树 (Tree)
树是一种层次结构的非线性数据结构,包含节点,每个节点有一个父节点和若干子节点。二叉树是树的特殊形式,每个节点最多有两个子节点。二叉搜索树(BST)是一种特殊的二叉树,其中每个节点的左子节点都小于该节点,右子节点都大于该节点。
- 时间复杂度:
- 查找/插入/删除:O(log n)(在平衡二叉树中)
示例(二叉树的遍历):
1 | class TreeNode { |
2. 常用算法
2.1 排序算法
2.1.1 快速排序 (Quick Sort)
思想:快速排序是基于分治思想的排序算法,通过选择一个基准元素,将数组分成两部分,使得基准元素左边的元素都小于它,右边的元素都大于它,然后递归地对两部分进行排序。
- 时间复杂度:
- 最优/平均情况:O(n log n)
- 最坏情况:O(n²)
示例(Java 快速排序):
1 | public class QuickSort { |
2.2 查找算法
2.2.1 二分查找 (Binary Search)
思想:二分查找用于在有序数组中查找元素,通过不断将查找范围减半,直到找到目标元素或范围为空。
- 时间复杂度:O(log n)
示例(Java 二分查找):
1 | public class BinarySearch { |
2.3 贪心算法 (Greedy Algorithm)
思想:贪心算法通过每一步选择当前最优解来构建全局最优解,常用于解决优化问题,如背包问题、最小生成树等。
- 时间复杂度:取决于具体问题
示例(Java 找零问题):
1 | public class GreedyChange { |
2.4 动态规划 (Dynamic Programming)
思想:动态规划通过将复杂问题分解为更小的子问题来解决,每个子问题的结果都会被保存,以便后续重用,避免重复计算。
- 时间复杂度:取决于问题的规模和状态数
示例(Java 求解斐波那契数列):
1 | public class Fibonacci { |
总结
- 数据结构提供了组织和存储数据的方式,如数组、链表、栈、队列、树、哈希表等。
- 算法提供了解决问题的步骤或过程,如排序(快速排序)、查找(二分查找)、贪心算法和动态规划等。
通过选择合适的数据结构和算法,可以显著提高程序的性能和可扩展性。
网络
Http 和 Https 的区别
HTTP(超文本传输协议)和 HTTPS(安全超文本传输协议)是用于在网络上传输数据的协议。两者之间的主要区别在于安全性、加密以及使用的端口等方面。以下是它们的主要区别:
1. 安全性
HTTP:
- HTTP 在传输数据时不使用任何加密,因此数据在客户端和服务器之间传输时是明文的。这意味着任何人都可以在传输过程中截获和查看数据,从而产生安全风险。
HTTPS:
- HTTPS 在 HTTP 的基础上增加了安全层,即 SSL/TLS 层。它对数据进行加密,确保数据在客户端和服务器之间传输时是安全的。这使得数据不易被截获或篡改,提供了更高的安全性。
2. 端口号
HTTP:
- 默认使用端口 80。
HTTPS:
- 默认使用端口 443。
3. 证书
HTTP:
- 无需安全证书。
HTTPS:
- 需要 SSL/TLS 证书。这些证书由受信任的证书颁发机构(CA)签发,确保用户连接到的是合法的网站。SSL/TLS 证书可以通过加密来验证网站的身份。
4. 性能
HTTP:
- 由于没有加密,HTTP 协议在数据传输方面的性能较高,延迟较低。
HTTPS:
- HTTPS 在建立连接时需要进行证书握手和加密解密的过程,相比之下会消耗更多的计算资源和时间,但现代的优化技术(如 HTTP/2、快取等)已经大幅降低了这些性能损失。
5. SEO 和浏览器支持
HTTP:
- 相较于 HTTPS,HTTP 被搜索引擎视为不安全的连接,可能影响网站的搜索排名。
HTTPS:
- 搜索引擎(如 Google)倾向于优先考虑 HTTPS 网站,因为它们提供更高的安全性。此外,现代浏览器会标记 HTTP 连接为“不安全”,提高了用户对 HTTP 网站的警惕。
6. 数据完整性
HTTP:
- 数据在传输过程中可能被篡改,并且没有保障。
HTTPS:
- 数据的完整性得到了保障,以防数据在传输过程中被攻击者篡改。
总结
- HTTP 和 HTTPS 的主要区别在于安全性:HTTP 是不安全的,而 HTTPS 使用 SSL/TLS 进行加密,因此提供了更高的安全性和信任度。
- 在当今互联网环境下,建议尽可能使用 HTTPS,以保障用户数据的安全和隐私。
Https 进行连接的过程
HTTPS(安全超文本传输协议)是通过 SSL/TLS 协议在 HTTP 的基础上实现加密连接的。其连接过程相对复杂,涉及多个步骤,以确保数据传输的安全性。下面是 HTTPS 连接的详细过程:
1. 客户端发送请求
- 发起请求:用户在浏览器中输入 HTTPS URL(以 https:// 开头)并按下 Enter。浏览器会首先解析域名并找到相应的 IP 地址。
2. 建立 TCP 连接
- 三次握手:与 HTTP 相同,HTTPS 也先通过 TCP(三次握手)建立连接。该过程包括:
- 客户端发送 SYN:客户端向服务器发送一个同步(SYN)请求包。
- 服务器回应 SYN-ACK:服务器响应,发送一个同步-确认(SYN-ACK)包。
- 客户端确认 ACK:客户端再发送一个确认(ACK)包,完成三次握手,此时 TCP 连接已建立。
3. TLS 握手
一旦 TCP 连接建立,接下来是 TLS 握手过程,这一步骤是确保加密连接的关键,具体步骤如下:
客户端Hello:
- 客户端发送一个”Client Hello”消息,包含客户端支持的 SSL/TLS 版本、随机数、加密套件(cipher suites)列表以及浏览器信息等。
服务器Hello:
- 服务器根据客户端的请求发送”Server Hello”消息,确认使用的 SSL/TLS 版本、选择的加密套件,以及另一个随机数。
服务器证书:
- 服务器向客户端发送数字证书,该证书包含公钥(由受信任的证书颁发机构签发),以便客户端验证服务器的身份。
证书验证:
- 客户端验证服务器的证书是否由受信任的 CA 签发,以及证书是否有效(未过期、未被撤销等)。
生成会话密钥:
- 客户端生成一个预主密钥(pre-master secret),用服务器上提供的数据和随机数进行加密,并发送给服务器。
生成会话密钥:
- 服务器使用私钥解密接收到的预主密钥,双方以此生成对称会话密钥,用于后续的加密通信。
完成握手:
- 客户端发送一个”Finished”消息,表示客户端所有的握手消息都发送完毕。服务器也发送一个”Finished”消息,表示握手完成。
4. 数据加密传输
- 使用会话密钥:客户端和服务器之间的后续数据传输使用会话密钥进行对称加密,确保数据的机密性和完整性。
5. 关闭连接
- 结束连接:数据传输完成后,客户端和服务器可以通过发送关闭连接的消息来结束连接。
总结
HTTPS 连接的全过程包括建立 TCP 连接、进行 TLS 握手以及使用加密传输数据,确保数据在传输过程中的安全性和完整性。这一过程涉及许多技术细节,包括证书的验证、加密和解密等,确保用户与网站之间的对话是安全的。通过 HTTPS,用户和服务器之间的通信得到保护,避免数据被窃取或篆改。
TCP 三次握手
过程
TCP 三次握手(Three-Way Handshake)是建立 TCP 连接的过程,用于确保双方可以可靠地进行通信。这一过程分为三个步骤:
第一次握手:客户端发送 SYN 包
客户端向服务器发送一个 SYN(synchronize)数据包,表示请求建立连接。这个数据包中包含了初始的序列号(Sequence Number),用于后续数据传输的顺序控制。此时客户端进入 SYN-SENT 状态,等待服务器的响应。第二次握手:服务器发送 SYN-ACK 包
服务器收到客户端的 SYN 包后,同样发送一个 SYN 数据包以表示同意连接请求,并附带一个 ACK(acknowledgment)确认包来确认客户端的序列号。这个 SYN-ACK 包表明服务器已准备好接收数据。此时服务器进入 SYN-RECEIVED 状态。第三次握手:客户端发送 ACK 包
客户端收到服务器的 SYN-ACK 包后,再发送一个 ACK 包来确认服务器的序列号。此时连接建立成功,客户端和服务器都进入 ESTABLISHED(已建立)状态,可以正式开始数据传输。
目的和重要性
三次握手的主要目的是确保客户端和服务器之间的通信通道畅通,双方都能接收到对方的消息。三次握手可以有效防止重复的连接请求引发错误,确保可靠的连接。
三次握手也能帮助防止一些常见的网络攻击(例如 SYN Flood 攻击),因为它要求服务器在发送 SYN-ACK 之后等待客户端的最后确认,不会轻易耗尽资源。