Administrator
发布于 2026-08-08 / 24 阅读
0
0

Android开发面试准备 -- Java、Kotlin、C++语言考点

面试准备

1)Java:集合、多线程、内存、泛型、JNI 基础;

2)Kotlin:协程、Flow、空安全;

3)C++ 加分项准备:讲 JNI 调用、内存管理、指针、 JNI 调用 C++ 音视频库的经验

一、Java

1. 集合

HashMap(面试必问,准备到源码级别)​

  • 1.7 vs 1.8 区别:1.7 是数组+链表,头插法(并发死循环);1.8 是数组+链表+红黑树,尾插法。链表长度 ≥8 且数组长度 ≥64 时转红黑树,≤6 时退化为链表

  • 扩容机制:初始容量 16,负载因子 0.75,扩容翻倍(*2)。1.8 扩容时元素位置要么不变要么 原位置+旧容量,用 hash & oldCap 判断

  • hash 计算:(h = key.hashCode()) ^ (h >>> 16),高 16 位异或低 16 位,减少碰撞

  • 线程不安全:put 可能覆盖,扩容可能丢数据。并发场景用 ConcurrentHashMap

ConcurrentHashMap

  • 1.7:Segment 分段锁,默认 16 段,每段独立加锁

  • 1.8:去掉 Segment,用 CAS + synchronized 锁单个桶节点。put 时先 CAS 写入空桶,非空则 synchronized 锁链表头节点。size 用 CounterCell 并发计数

ArrayList

  • 初始容量 10,扩容 1.5 倍(oldCapacity + oldCapacity >> 1)

  • System.arraycopy 做数组拷贝

  • fail-fast:迭代时 modCount 检查,并发修改抛 ConcurrentModificationException

2. 多线程

线程池(7 大参数 + 工作流程 + 4 种拒绝策略,必问)​

工作流程:核心线程 → 队列 → 非核心线程 → 拒绝策略

4 种拒绝策略:

  • AbortPolicy:抛 RejectedExecutionException(默认)

  • CallerRunsPolicy:调用者线程执行(降级常用)

  • DiscardPolicy:静默丢弃

  • DiscardOldestPolicy:丢弃队列最老任务

加分:Executors 提供的四种线程池为什么不推荐用?因为 newFixedThreadPool 和 newSingleThreadExecutor 队列是 LinkedBlockingQueue(无界),可能 OOM;newCachedThreadPool 最大线程 Integer.MAX_VALUE,也可能 OOM。生产环境必须用 ThreadPoolExecutor 手动构造。

synchronized vs ReentrantLock

维度

synchronized

ReentrantLock

实现

JVM 层 monitorenter/monitorexit

API 层 AQS

公平性

非公平

可选公平/非公平

中断

不可中断

可 lockInterruptibly

超时

不支持

tryLock(timeout)

条件变量

一个 wait/notify

多个 Condition

volatile

  • 保证可见性(MESI 缓存一致性协议 + 内存屏障)

  • 禁止指令重排序(Store-Store / Load-Load 屏障)

  • 不保证原子性(i++ 仍不安全)

  • 典型应用:DCL 单例的 volatile 修饰

ThreadLocal

  • 每个线程有独立 ThreadLocalMap,key 是 ThreadLocal 弱引用,value 是强引用

  • 内存泄漏:value 不会被回收(key 回收后 value 仍在),需要手动 remove()

  • 加分:InheritableThreadLocal 可以父子线程传递,但线程池场景要用 TransmittableThreadLocal

3. 内存模型

JVM 内存区域

  • 堆:对象实例 + 数组,GC 主战场。分新生代(Eden + 2 Survivor,8:1:1)和老年代

  • 方法区/元空间:类信息、常量池、静态变量。1.8 从永久代改为本地内存元空间

  • 虚拟机栈:方法栈帧(局部变量表、操作数栈、动态链接)

  • 本地方法栈:native 方法

  • 程序计数器:当前线程执行位置

GC 算法

  • 判断存活:可达性分析,GC Roots(栈引用、静态变量、JNI 引用)

  • 回收算法:标记-清除(碎片)、复制(浪费空间)、标记-整理

  • 分代回收:新生代用复制算法,老年代用标记-整理

  • GC 收集器:CMS(低延迟,标记-清除)、G1(Region 化,可预测停顿)、ZGC(着色指针,<10ms)

加分:G1 将堆分为 2048 个 Region,每个 1-32MB,混合回收(新生代 + 老年代部分 Region)。-XX:MaxGCPauseMillis 设定目标停顿时间。

4. 泛型

  • 类型擦除:编译后泛型类型被擦除为 Object 或上界,运行时无泛型信息。List<String> 和 List<Integer> 运行时都是 List

  • 通配符:<? extends T> 上界只读(生产者),<? super T> 下界只写(消费者)—— PECS 原则

  • 类型安全:编译期检查,运行时 ClassCastException 提前到编译期

  • 桥接方法:泛型擦除后为了保持多态,编译器生成桥接方法

5. JNI 基础

  • 数据类型映射:int→jint,byte[]→jbyteArray,Object→jobject,String→jstring

  • 静态 vs 实例方法:静态方法第二个参数是 jclass,实例方法是 jobject

  • 签名规则:I = int,[B = byte[],Ljava/lang/String; = String,(I[BI)I = 参数(int, byte[], int) 返回 int

  • JNIEnv:线程相关,每个线程有自己的 JNIEnv,不能跨线程传递


二、Kotlin

1. 协程

协程 vs 线程

  • 协程是用户态轻量级线程,挂起不阻塞线程,由 Kotlin 运行时调度

  • 一个线程可以跑多个协程,协程切换成本远低于线程切换

  • 协程核心是挂起函数(suspend),编译后生成 CPS(Continuation Passing Style)状态机

核心组件

  • CoroutineScope:管理协程生命周期,取消时自动取消子协程

  • CoroutineDispatcher:Dispatchers.IO(线程池)、Dispatchers.Main(主线程)、Dispatchers.Default(CPU 密集)

  • Job:协程句柄,可 cancel/join

  • Deferred<T>:带返回值的 Job,await() 获取结果

launch vs async

  • launch:启动协程不返回结果,返回 Job。用于 fire-and-forget

  • async:启动协程返回 Deferred<T>,await() 拿结果。用于并发请求

结构化并发

  • 父协程取消 → 所有子协程自动取消

  • 父协程等待所有子协程完成才完成

  • 异常会向上传播,除非用 SupervisorJob 隔离

加分:协程本质是编译器变换 + 线程池调度。suspend 函数编译后会多一个 Continuation 参数,通过状态机实现挂起恢复。挂起时保存局部变量到 Continuation,恢复时从 Continuation 恢复。

2. Flow

  • 冷流:类似 Sequence,每次 collect 才开始发射数据。不消费不生产

  • 背压:buffer()(缓冲)、conflate()(只保留最新)、collectLatest()(取消上一个慢的)

  • 异常处理:catch { } 操作符(只能 catch 上游异常)

  • 完成处理:onCompletion { },无论正常结束还是取消都会调用

  • 热流 StateFlow/SharedFlow:StateFlow 替代 LiveData,必须有初始值,conflation 去重;SharedFlow 支持多播,可配置 replay 缓存

面试加分:

kotlin

// Flow 是冷流,不 collect 不执行
val flow = flow { emit(1) }  // 定义时不执行
flow.collect { println(it) } // collect 时才执行

// StateFlow vs SharedFlow
// StateFlow = SharedFlow with replay=1, conflate=true
// StateFlow 总是有值,适合 UI 状态
// SharedFlow 更灵活,适合事件总线

3. 空安全

  • 编译期检查:String 不可空,String? 可空。编译器在调用点强制检查

  • 安全调用:?.(空则不执行)、?:(elvis,空则默认值)、!!(非空断言,空则 NPE)

  • let 配合:nullable?.let { ... } 替代 if-null 检查

  • 平台类型:Java 互操作时返回类型是平台类型(String!),需要手动确认可空性


三、C++ 加分项

1. 指针与内存管理

智能指针(现代 C++ 核心)​

  • unique_ptr:独占所有权,不可拷贝可 move,开销为零

  • shared_ptr:共享所有权,引用计数,线程安全的计数操作但对象访问不安全

  • weak_ptr:弱引用,不增加引用计数,解决 shared_ptr 循环引用

加分:

  • shared_ptr 控制块是堆分配的,用 make_shared 可以将对象和控制块分配在同一块内存,减少一次内存分配

  • shared_ptr 的引用计数是原子操作(性能有开销),不要在热路径大量使用

  • RAII:Resource Acquisition Is Initialization,资源获取即初始化。构造函数获取资源,析构函数释放资源,是 C++ 内存管理的核心思想

裸指针的坑

  • 野指针:delete 后没有置 null,继续访问

  • 悬空指针:指向的内存已被释放

  • 内存泄漏:new 了没有 delete

  • 重复释放:同一块内存 delete 两次 → 堆损坏

2. JNI 调用要点

静态注册 vs 动态注册

  • 静态注册:Java_com_pkg_Class_method 命名规则,JNI 自动查找。简单但每次调用有名字查找开销

  • 动态注册:JNI_OnLoad 中用 RegisterNatives 注册,性能好,混淆友好。生产环境推荐

引用类型

  • Local Reference:函数内自动创建,函数返回时自动释放。但在循环中创建不释放会撑爆 Local Ref 表(默认 512 个),必须手动 DeleteLocalRef

  • Global Reference:跨函数/跨线程使用,NewGlobalRef 创建,DeleteGlobalRef 释放。注意泄漏

  • Weak Global Reference:不阻止 GC 回收,使用前要 IsSameObject 检查是否存活

3. JNI 调用 C++ 音视频库经验(重点加分)

典型架构:

plaintext

Java (Kotlin) 推流 SDK
    → JNI 层 (C++ wrapper)
        → FFmpeg libavcodec / libavformat (编码封装)
        → 自研 C++ 编码器 (MediaCodec 降级方案)
    → 回调 Java 层 (编码后的 H.264 NAL)

关键经验点:

  1. DirectByteBuffer 零拷贝

    • 频繁传递大帧数据(如 1080p YUV ≈ 3MB),用 byte[] 每次都要 JNI 拷贝

    • 改用 DirectByteBuffer,C++ 层 GetDirectBufferAddress 直接拿指针,零拷贝

    • Java 侧分配:ByteBuffer.allocateDirect(size),C++ 侧 env->GetDirectBufferAddress(buf)

  2. 线程回调 Java

    • FFmpeg 编码是异步的,编码完成后在 C++ 子线程回调

    • 子线程没有 JNIEnv,必须 JavaVM->AttachCurrentThread(&env, nullptr) 获取

    • 回调完后 DetachCurrentThread,否则线程退出时崩溃

    • 最佳实践:用 pthread_key_t 线程局部存储管理 JNIEnv 生命周期

  3. 异常边界处理

    • C++ 异常不能跨 JNI 传播到 Java 层(直接 crash)

    • JNI 函数必须 try-catch 包裹所有 C++ 调用

    • catch 后用 env->ThrowNew(env->FindClass("java/lang/RuntimeException"), msg) 抛 Java 异常

    • 或者返回错误码,Java 层根据错误码处理

  4. 编码器生命周期管理

    • JNI_OnLoad 中初始化 JavaVM 指针和 Java 方法 ID(缓存,避免每次查找)

    • Java 层 init 时创建 C++ Encoder 对象,返回 jlong handle 给 Java 层持有

    • Java 层 release 时通过 handle 找到 C++ 对象 delete,然后置 0

    • 用 GlobalRef 持有 Java 回调对象,防止 GC 回收

cpp

// 典型 handle 模式
static jlong nativeInit(JNIEnv* env, jobject thiz) {
    auto* encoder = new VideoEncoder();
    return reinterpret_cast<jlong>(encoder);
}

static jint nativeEncode(JNIEnv* env, jobject thiz, jlong handle, jobject input) {
    auto* encoder = reinterpret_cast<VideoEncoder*>(handle);
    uint8_t* data = (uint8_t*)env->GetDirectBufferAddress(input);
    return encoder->encode(data, env->GetDirectBufferCapacity(input));
}

static void nativeRelease(JNIEnv* env, jobject thiz, jlong handle) {
    auto* encoder = reinterpret_cast<VideoEncoder*>(handle);
    delete encoder;
}

面试话术建议

自我介绍时:重点突出"Android 音视频方向,熟悉 MediaCodec 硬编 + RTMP 推流,有 JNI 调 C++ 音视频库经验"。

被问到 JNI 时:主动说"我在项目中用 JNI 封装了 FFmpeg 编码层,Java 层通过 DirectByteBuffer 传 YUV 数据实现零拷贝,C++ 编码完成后在子线程回调 Java,用 AttachCurrentThread 管理 JNIEnv 生命周期"。

被问到协程时:可以结合直播场景说"推流 SDK 用协程管理采集→编码→推流的 pipeline,用 Flow 做帧数据的流式处理,背压用 buffer + conflate 控制"。

被问到性能优化时:引到前面的弱网策略,展示工程深度。


评论