本文来自音视频技术社群关键帧的音视频开发圈的分享,该社群会定期整理一些音视频相关的面试题,本期分享 Android 音视频方向面试题实录。
下面是我们技术社群的一位群友最近面试 Java 音视频岗位遇到的一些面试题:
- 1、多线程同步,怎么保证多线程访问不会出问题?都有哪些线程同步的方法?
- 2、Java 堆和 Native 堆有什么区别?系统会为每一个应用分配 Java 堆和 Native 堆吗?
- 3、分配的内存模型是什么样子的,既有 Java 堆,又有 Native 堆,还有别的吗?
- 4、播放卡顿是由于什么造成的?
- 5、描述一下 AAC,AAC 跟 PCM 有什么关系?AAC 怎么进行压缩的 PCM?
- 6、AAC 音频流过来怎么去解析的?既然是流,肯定要分割的,一帧的大小是怎么定的?
- 7、播放器遇到音频与视频不同步时,是什么问题?怎么解决的?
- 8、播放遇到过卡顿是什么原因?怎么解决?
- 9、播放器遇到黑屏、花屏,是怎么分析的?怎么解决?
- 10、有没有遇到过内存问题导致播放器卡顿,怎么优化的内存?
- 11、介绍一下 WebRTC,音视频数据是从 Camera 拿到的数据还是下载好的数据?遇到过黑屏或者花屏一些问题吗?
1、多线程同步,怎么保证多线程访问不会出问题?都有哪些线程同步的方法?
多线程同步是为了防止多个线程同时访问共享资源时出现数据竞争和不一致的问题。保证多线程访问安全的方法包括:
- 互斥锁(Mutex):最常用,保证同一时间只有一个线程可以访问共享资源。
- 读写锁(Read-Write Lock):允许多个读线程同时访问,但写线程独占。
- 条件变量(Condition Variable):用于线程间的通信,当某个条件成立时,通知等待的线程。
- 信号量(Semaphore):控制同时访问共享资源的线程数量。
- 自旋锁(Spinlock):线程会循环等待锁,适用于等待时间短的场景。
- 原子操作(Atomic Operations):对于简单的数据类型,使用原子操作来保证线程安全。
- 使用线程安全的数据结构,如 ConcurrentHashMap。
在音视频开发中,多线程同步常用于解码、渲染等环节,确保数据读写安全。
2、Java 堆和 Native 堆有什么区别?系统会为每一个应用分配 Java 堆和 Native 堆吗?
- Java 堆:是 Java 虚拟机管理的内存,用于存放 Java 对象实例,由垃圾回收器(GC)自动回收。
- Native 堆:也称为本地堆,是通过 C/C++ 代码直接分配的内存(如 malloc、new),不受 Java 虚拟机管理,需要手动释放。
系统会为每个应用分配一个 Java 堆,而 Native 堆是进程级别的,每个进程都有一个 Native 堆,所以应用中的 Native 代码和 Java 代码通过 JNI 都可以访问到同一个 Native 堆。
我们可以通过下面这个表格来清晰地对比它们的核心区别:
| 特性 | Java 堆 | Native 堆 |
|---|---|---|
| 管理方式 | 由 Java 虚拟机(JVM/ART) 自动管理。 | 由 开发者手动管理(如C++的malloc/free或new/delete),或由本地代码的运行时库管理。 |
| 垃圾回收 | 受 GC 管理。JVM/ART会通过垃圾回收器自动回收不再使用的对象。 | 不受 GC 管理。必须手动释放内存,否则会导致内存泄漏。 |
| 存放内容 | 存放 Java 对象实例(只要是new关键字创建的对象)。 | 存放 JNI 中创建的对象、C/C++代码分配的内存、Bitmap的像素数据等。 |
| 分配限制 | 大小受 -Xmx 等JVM参数或Android系统对单个App的堆大小限制。 | 理论上只受进程可用的系统总内存限制,通常比Java堆大得多。 |
| 访问方式 | 只能被 Java 线程 访问。 | 可以被 任何线程(Java线程或Native线程)访问。 |
| 性能和开销 | 分配和回收(尤其是GC发生时)有一定开销,可能导致应用卡顿。 | 分配和释放速度通常很快,但管理不当的后果更严重(泄漏、野指针)。 |
| 诊断工具 | Android Studio Profiler, MAT等。 | Android Studio Profiler (Native Memory), adb shell dumpsys meminfo等。 |
3、分配的内存模型是什么样子的,既有 Java 堆,又有 Native 堆,还有别的吗?
进程内存模型概览:
当一个应用进程启动后,操作系统会为它分配一个独立的、连续的虚拟内存地址空间。这个地址空间被划分成不同的区域(Segment),每个区域有特定的用途。下图是一个高度简化的内存布局示意图:
高地址 +----------------------+ <-- 栈顶 (例如 0x7fffffff0000)
| 栈 | <-- 每个线程有自己的栈,存储局部变量、函数调用信息
| (Stack) |
| | |
| v | (栈向下增长)
+----------------------+
| 内存映射区域 | <-- 包括 .so 库、映射文件、Android Art Heap 等
| (Memory Mapping) |
+----------------------+
| ^ |
| | |
| 堆 | <-- 用于动态内存分配 (malloc/new)
| (Heap) | (堆向上增长)
+----------------------+
| BSS 段 | <-- 未初始化的静态变量和全局变量
+----------------------+
| 数据段 (Data) | <-- 已初始化的静态变量和全局变量
+----------------------+
| 代码段 (Text) | <-- 存放可执行代码 (机器指令)
低地址 +----------------------+ <-- 0x0 (或一个很小的地址,如 0x400000)
- 代码段:
- 内容:存放编译后的、可执行的机器指令。包括你应用的 Dex 代码经过 AOT 或 JIT 编译后的本地代码、所有加载的 .so 库(如 libc.so, libart.so)的代码。
- 特性:通常是只读和可执行的,以防止程序意外修改自身的指令。在多个进程加载同一个库时(如 libc.so),物理内存中通常只有一份代码副本,被多个进程只读共享,以节省内存。
- 数据段:
- 内容:已初始化的全局变量和静态变量(包括 static 变量)。
- 特性:在程序启动时就被赋予了初始值,具有明确的生命周期(与程序共存亡)。
- BSS 段:
- 内容:未初始化的全局变量和静态变量。
- 特性:在程序加载时,操作系统会将这一片区域全部初始化为零。这样做可以减小可执行文件的大小,因为不需要在文件中存储一堆零值。
- Native 堆:
- 内容:JNI 中 NewByteArray 等函数创建的、不受 GC 管理的对象。C/C++ 代码中直接分配的对象和缓冲区。一些系统组件(如 Bitmap 在 Android 8.0 之前)的像素数据。
- 特性:就是传统 C/C++ 意义上的“堆”。通过 malloc, calloc, new 等函数进行分配。由系统的 Libc 库(如 Android 的 Bionic)中的分配器(如 scudo)或开发者自己管理。
- Java 堆:
- 内容:几乎所有通过 new 关键字创建的 Java 对象实例。
- 特性:它实际上是 Native 堆内部 由 ART 虚拟机划出并管理的一块特殊区域。当你在 Java 代码中执行 new Object() 时,ART 会在这个区域内为你分配内存。由 ART 的垃圾回收器 全权管理。GC 会在这里面移动对象、标记和回收垃圾。
- 栈:
- 内容:每个线程都有自己独立的栈。它用于存储局部变量、方法参数、函数调用的返回地址等。
- 特性:分配和释放速度极快(只是移动栈指针)。生命周期与线程绑定。当方法被调用时,其栈帧被压入栈;当方法返回时,栈帧被弹出。通常大小是有限的(例如 1MB 或 8MB,可配置),如果递归调用过深或局部变量过大,会导致 StackOverflowError。
- 内存映射区域:
- 内容:动态链接库:所有加载的 .so 文件都被映射到这个区域。内存映射文件:通过 mmap 系统调用将文件直接映射到内存,例如 ZipFile 读取 APK 中的资源。匿名内存映射:不关联任何文件,用于分配大块内存。在 Android 中,Art Heap(即 Java 堆)就是通过匿名内存映射创建的!显式分配的堆:有些系统或库可能会使用 mmap 直接分配一大块内存作为自己的“堆”来使用。
- 特性:这是一个非常灵活和重要的区域,位于堆和栈之间。
Android 应用内存的独特之处:
在 Android 中,我们通过 adb shell dumpsys meminfo <package_name> 看到的内存分类,正是对这个内存模型的另一种视角的呈现:
Applications Memory Usage (in Kilobytes):
Uptime: 1234567890 Realtime: 1234567890
** MEMINFO in pid 12345 [com.example.app] **
Pss Private Private SwapPss Heap Heap Heap
Total Dirty Clean Dirty Size Alloc Free
------ ------ ------ ------ ------ ------ ------
Native Heap 25680 25600 0 200 30000 25000 5000
Dalvik Heap 5120 5100 0 100 10000 5200 4800 <-- 对于ART,这里也叫 Dalvik/Art Heap
Stack 16 16 0 0
Cursor 0 0 0 0
Ashmem 100 100 0 0
Gfx dev ......
Other dev ......
.so mmap ......
.jar mmap ......
.apk mmap ...... <-- 这些都属于内存映射区域
.ttf mmap ......
.dex mmap ......
.oat mmap ......
.art mmap ......
Other mmap ......
Unknown ......
TOTAL XXXXX YYYYY ZZZZZ WWWW 40000 30200 9800
从这份报告可以看出,除了 Native Heap 和 Dalvik/Art Heap,一个应用的内存还包括:
- Stack:栈内存。
- .so mmap, .apk mmap, .dex mmap 等:各种内存映射文件。
- Ashmem:Android 特有的共享内存。
- Gfx dev:图形缓冲区内存(如 EGL、GL)。
- Other dev:其他设备驱动占用的内存。
总结:
一个应用进程的内存模型是一个复杂的、结构化的虚拟地址空间,它远不止 Java 堆和 Native 堆:
- 代码段:存放可执行指令。
- 数据段 & BSS 段:存放全局和静态变量。
- 堆:动态分配区,内部包含:
- Native 堆:C/C++/JNI 手动管理的内存。
- Java 堆:ART 在 Native 堆中划出并由 GC 管理的区域,用于 Java 对象。
- 栈:每个线程私有,用于函数调用和局部变量。
- 内存映射区域:一个灵活的区域,用于加载库、映射文件,并且 Java 堆本身也建立在这个区域之上。
所有这些部分加起来,构成了你在 Android Profiler 或 dumpsys meminfo 中看到的应用总内存占用。理解这个模型,对于深度优化内存性能和诊断复杂的内存问题(如泄漏、OOM)至关重要。
4、播放卡顿是由于什么造成的?
播放卡顿可能由以下原因造成:
- 网络问题:带宽不足、网络波动导致数据加载慢。
- 解码性能不足:硬件解码器不支持或软件解码器效率低。
- 渲染不及时:视频帧渲染速度跟不上。
- 线程阻塞:主线程或渲染线程被其他任务阻塞。
- 内存不足:导致 GC 频繁或内存交换。
- 文件读取慢:本地文件读取速度慢(如磁盘 IO 问题)。
5、描述一下 AAC,AAC 跟 PCM 有什么关系?AAC 怎么进行压缩的 PCM?
AAC(Advanced Audio Coding)是一种有损音频压缩格式,是 MP3 的后续版本,提供更好的音质和更高的压缩比。
PCM(Pulse Code Modulation)是一种未压缩的音频数据格式,是音频的原始表示形式。
关系:AAC 是通过对 PCM 音频数据进行压缩编码得到的。播放时,AAC 需要解码成 PCM 才能被声卡播放。
压缩原理:AAC 利用心理声学模型,去除人耳不敏感的频率成分,并且利用量化、霍夫曼编码等技术减少冗余,从而实现压缩。
AAC 压缩 PCM 的过程(简要):
- 心理声学分析:分析 PCM 信号的频率,利用“人耳掩蔽效应”(强信号会掩盖临近频率的弱信号),去除人耳不敏感或听不到的音频信息。
- 变换与量化:将时域的 PCM 信号通过 MDCT 变换到频域,然后对频域数据进行量化(即降低数据精度),这是信息损失的主要步骤。
- 编码与打包:最后使用霍夫曼编码等无损压缩技术对量化后的数据进行二次压缩,并打包成 AAC 格式文件。
PCM 是原始的“音频原料”,而 AAC 是经过“精加工和压缩”后的成品,体积更小,便于存储和传输。
6、AAC 音频流过来怎么去解析的?既然是流,肯定要分割的,一帧的大小是怎么定的?
- AAC 流解析:AAC 流是由一帧一帧的数据组成的。解析时需要找到帧头(通常为 0xFFF 或 0xFFE),然后根据帧头信息解析出当前帧的长度和其他信息。
- 分割:通过搜索帧同步字(0xFFF)来分割每一帧。
- 一帧的大小:AAC 帧的大小由帧头中的信息决定。在 ADTS(Audio Data Transport Stream)格式中,帧头包含帧长度信息。具体来说,ADTS 帧头有 13 位表示帧长度,所以一帧的大小最大为 2^13-1=8191 字节。
7、播放器遇到音频与视频不同步时,是什么问题?怎么解决的?
问题原因:
- 解码性能不足:设备处理能力不够,解码速度跟不上,解码速度不一致。
- 文件问题:音视频流时间戳错误或封装格式有缺陷。
- 播放器设置:音频/视频缓存设置不当或解码器兼容性问题。
- 渲染延迟。
解决方法:
- 以音频为基准,调整视频播放速度,因为人耳对音频变化更敏感。
- 使用时间戳同步:在解码时,根据音视频帧的时间戳进行同步,如果视频落后则跳帧,如果视频超前则等待。
- 调整时钟基准:使用一个共同的时钟基准(如系统时钟)来比较音视频帧的播放时间。
- 调整同步设置:在播放器中手动调节音视频同步偏移值。
- 更新解码器:切换软/硬解码模式,或更新显卡声卡驱动。
- 转码文件:用格式工厂等工具重新封装,修复时间戳问题。
- 降低负载:关闭其他占用资源的程序,或降低视频分辨率。
8、播放遇到过卡顿是什么原因?怎么解决?
播放可能原因:网络缓冲不足、解码器性能问题、渲染延迟、线程调度问题、内存不足等。
解决:
- 增加网络缓冲:提前缓冲更多数据。
- 优化解码:使用硬件解码,降低视频分辨率或码率。
- 优化渲染:使用 SurfaceView 或 TextureView,确保在独立的渲染线程。
- 线程优化:将解码、网络 IO 等操作放在后台线程,避免阻塞主线程。
- 内存优化:避免内存泄漏,及时释放不再使用的资源。
9、播放器遇到黑屏、花屏,是怎么分析的?怎么解决?
- 黑屏:可能没有视频数据输出,或者渲染设置错误。
- 分析:检查解码器输出是否正常,渲染 Surface 是否有效,视频数据是否为空。
- 解决:确保解码器正确初始化,检查视频数据流,确保渲染 Surface 有效。
- 花屏:通常是由于解码错误或数据丢失造成的。
- 分析:检查视频帧是否完整,解码过程中是否发生错误,是否有关键帧丢失。
- 解决:确保视频流的关键帧完整,检查解码器是否支持该视频格式,尝试重置解码器。
10、有没有遇到过内存问题导致播放器卡顿,怎么优化的内存?
内存问题:内存泄漏、内存碎片、频繁 GC 等。
优化:
- 使用对象池:复用解码后的帧对象,避免频繁创建和销毁。
- 及时释放资源:解码后的帧及时释放,Native 内存手动管理。
- 使用内存映射:对于大文件,使用内存映射(如 MappedByteBuffer)来减少内存拷贝。
- 监控内存使用:使用工具(如 Android Profiler)检测内存泄漏和内存使用情况。
11、介绍一下 WebRTC,音视频数据是从 Camera 拿到的数据还是下载好的数据?遇到过黑屏或者花屏一些问题吗?
- WebRTC(Web Real-Time Communication)是一个支持网页浏览器进行实时语音、视频通话的开源项目。
- 音视频数据来源:可以是 Camera 采集的数据,也可以是屏幕共享、文件数据(但通常 WebRTC 用于实时通信,所以主要是 Camera 和麦克风)。
- 黑屏/花屏问题:在 WebRTC 中也可能遇到,原因可能包括:
- 视频采集失败:Camera 没有正确打开或没有权限。
- 编码问题:编码器设置错误或不支持。
- 网络问题:数据包丢失导致花屏。
- 解决:检查 Camera 采集、编码器配置,优化网络传输(如使用前向纠错、重传等)。
版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。