Administrator
发布于 2026-08-08 / 20 阅读
1
0

Android开发面试准备 -- 安卓基础必考题

系统控件:自定义 View 绘制流程、事件分发机制;

动画:属性动画、帧动画,动画卡顿问题;

多线程:线程池、Handler 消息机制、ANR 产生原因;

网络协议:HTTP/HTTPS,TCP,了解 RTMP 简单原理(直播会用到);

数据库:SQLite,事务,大数据量优化;

性能 & 内存优化高频:内存泄漏工具 LeakCanary,OOM,卡顿检测,启动优化,UI 掉帧

三张图覆盖了 View 绘制、Handler 机制和性能优化全景

一、系统控件

1.1 自定义 View 绘制流程

三个阶段:measure() → layout() → draw(),由 ViewRootImpl 的 performTraversals() 触发。

measure 核心机制

MeasureSpec = mode + size,32 位 int,高 2 位是 mode,低 30 位是 size。三种 mode:

Mode

含义

对应 LayoutParams

EXACTLY

精确值

match_parent / 具体 dp

AT_MOST

最大值

wrap_content

UNSPECIFIED

不限制

ScrollView 子 View

子 View 的 MeasureSpec 由父 View 的 Spec + 子 View 的 LayoutParams 共同决定,这是 getChildMeasureSpec() 方法的核心逻辑。

面试加分话术:

"在直播项目中,我自定义了一个礼物连击 View,需要根据礼物数量动态计算尺寸。wrap_content 场景下 measure 是 AT_MOST,我在 onMeasure 里根据内容宽高和 padding 计算实际尺寸,然后 setMeasuredDimension()。关键是不能忘了 resolveSize() 做边界约束,否则父容器给了最大值但你填了更大的值会被裁剪。"

draw 内部顺序(面试常问):

  1. drawBackground — 画背景

  2. saveLayer — 如果有 alpha/clip 层

  3. onDraw — 画自己的内容

  4. dispatchDraw — 画子 View

  5. drawForeground — 滚动条等前景

关键细节:

  • onDraw 里绝对不要 new 对象(Paint、Path 等),因为 draw 每帧执行,GC 压力巨大

  • Canvas 状态 save() / restore() 必须配对,否则影响后续绘制

  • invalidate() 触发重绘走 onDraw;requestLayout() 触发 measure+layout+draw 全流程

1.2 事件分发机制

三个核心方法,形成 U 型链路:

plaintext

dispatchTouchEvent()  →  onInterceptTouchEvent()  →  onTouchEvent()
   (分发)                  (拦截, 仅 ViewGroup)          (消费)

分发规则:

  1. Activity.dispatchTouchEvent → Window → DecorView → ViewGroup

  2. ViewGroup 先调 onInterceptTouchEvent,返回 true 则自己消费,不往下传

  3. 返回 false 则遍历子 View,调用 child.dispatchTouchEvent

  4. 子 View 的 onTouchEvent 返回 false,事件回传给父 View 的 onTouchEvent

  5. 所有 View 都不消费,最终回到 Activity.onTouchEvent

关键规则:

  • ACTION_DOWN 决定消费者:谁在 DOWN 时返回 true,后续 MOVE/UP 都直接发给它

  • 一旦消费就不能反悔:后续事件不再走 onInterceptTouchEvent

  • requestDisallowInterceptTouchEvent:子 View 可以禁止父 View 拦截(用于嵌套滑动)

滑动冲突解决(面试必问):

冲突类型

典型场景

解决方案

同方向

ScrollView 嵌套 ListView

外部拦截法:父 View onInterceptTouchEvent 根据滑动方向判断

不同方向

ViewPager 嵌套 ListView

内部拦截法:子 View 调 requestDisallowInterceptTouchEvent

多层嵌套

直播间弹幕+礼物+列表

组合使用,分层处理

面试加分话术:

"直播间的交互层很复杂——底层是视频画面 SurfaceView,中间层是弹幕 RecyclerView,上层有礼物动画和连击按钮。礼物连击需要长按连发,但上层 ViewGroup 默认会拦截 MOVE 事件。我用内部拦截法,在 DOWN 时调用 parent.requestDisallowInterceptTouchEvent(true) 让父布局不要拦截,UP 时释放。同时弹幕列表和外层 ViewPager 的水平滑动冲突用外部拦截法处理,在 onInterceptTouchEvent 里比较 X/Y 方向位移差判断滑动方向。"


二、动画

2.1 属性动画 vs 帧动画 vs 补间动画

类型

原理

适用场景

缺点

帧动画

逐帧播放 Drawable 序列

简单序列动画

内存占用高,每帧一张图

补间动画

改变绘制位置/矩阵

View 位移/缩放/旋转

只改绘制不改真实属性,点击位置不变

属性动画

通过 setter 动态修改属性值

任意对象属性动画

需要属性有 setter 方法

补间动画的最大坑:动画后 View 看起来移动了,但点击事件还在原位置。因为只改了渲染矩阵,没改 left/top/right/bottom。解决方案:用属性动画替代,或动画结束后调 view.clearAnimation() + 手动 layout。

属性动画核心:

  • ValueAnimator:计算器,每帧算出一个值,你需要手动 invalidate 刷新

  • ObjectAnimator:自动调用 setter 更新属性(如 translationX、alpha)

  • AnimatorSet:组合动画,支持同时/顺序/延迟播放

  • PropertyValuesHolder:一个 View 上多个属性同时动画

  • TypeEvaluator:自定义值计算(如颜色渐变用 ArgbEvaluator)

  • Interpolator:插值器控制速度曲线(加速/减速/弹跳)

属性动画底层原理:

ObjectAnimator 内部通过 Choreographer 的 FrameCallback 监听 VSYNC 信号,每 16ms(60fps)计算一次当前帧的属性值,然后通过反射调用 setter 方法更新 View,最终触发 invalidate → onDraw。

2.2 动画卡顿问题

卡顿原因:

  1. 帧动画图太大:每帧 PNG 加载到内存,OOM + GC 卡顿。1080p PNG 单帧 ≈ 8MB,30 帧就是 240MB

  2. 属性动画触发 layout:动画 width/height 属性会触发 requestLayout,走完整 measure+layout+draw

  3. 动画回调里做重计算:onAnimationUpdate 里做布局/网络/文件操作

  4. 多动画叠加 CPU 过载:同时跑 5+ 个 ObjectAnimator,每帧计算量大

  5. 没有开启硬件加速:Canvas 操作走 CPU 软渲染

优化方案:

// 1. 用 translationX/Y 代替 layout 属性动画
// 不好:触发 requestLayout
ObjectAnimator.ofInt(view, "width", 0, 500);
// 好:只触发 invalidate
view.setTranslationX(deltaX);

// 2. 用硬件层加速复杂动画
view.setLayerType(View.LAYER_TYPE_HARDWARE, null);
animator.start();
// 动画结束后关闭,避免内存占用
animator.addListener(new AnimatorListenerAdapter() {
    @Override public void onAnimationEnd(Animator a) {
        view.setLayerType(View.LAYER_TYPE_NONE, null);
    }
});

// 3. 帧动画用 Lottie/_svga 替代纯图片序列

面试加分话术:

"直播间礼物特效最初用帧动画,30 帧 1080p PNG,内存占用超过 200MB,低端机直接 OOM。后来改用 SVGA 格式(矢量动画),同一个礼物效果从 8MB 降到 500KB,而且支持动态替换文字。对于简单的飞行动画用 ObjectAnimator + translationX,开启硬件层加速,动画结束后及时 setLayerType(LAYER_TYPE_NONE) 释放 GPU 纹理内存。同时用 Choreographer.FrameCallback 监测掉帧,超过 3 帧丢弃时自动降级礼物特效帧率。"


三、多线程

3.1 线程池

七大参数(必须背熟):

new ThreadPoolExecutor(
    int corePoolSize,        // 核心线程数
    int maximumPoolSize,     // 最大线程数
    long keepAliveTime,      // 非核心线程空闲存活时间
    TimeUnit unit,           // 时间单位
    BlockingQueue<Runnable> workQueue,  // 任务队列
    ThreadFactory threadFactory,        // 线程工厂
    RejectedExecutionHandler handler    // 拒绝策略
);

工作流程(面试必答,四步):

  1. 任务来了,核心线程没满 → 创建核心线程执行

  2. 核心线程满了 → 放入任务队列

  3. 队列满了 → 创建非核心线程(直到 maximumPoolSize)

  4. 最大线程也满了 → 执行拒绝策略

四种拒绝策略:

  • AbortPolicy:抛异常(默认)

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

  • DiscardPolicy:静默丢弃

  • DiscardOldestPolicy:丢掉队列最老的任务

Executors 四种线程池为什么不推荐(高频追问):

方法

问题

newFixedThreadPool

队列 LinkedBlockingQueue 无界 → OOM

newSingleThreadExecutor

同上

newCachedThreadPool

最大线程 Integer.MAX_VALUE → OOM

newScheduledThreadPool

最大线程 Integer.MAX_VALUE → OOM

正确做法:手动 ThreadPoolExecutor,根据任务类型配置参数。

线程数配置经验:

  • CPU 密集型:N + 1(N = CPU 核心数)

  • IO 密集型:2N 或更多

  • 混合型:拆分成两个线程池

面试加分话术:

"直播推流 SDK 里我用了两个线程池:编码线程池 CPU 密集型,核心线程数 = CPU 核心数 + 1,保证编码不被抢占;网络发送线程池 IO 密集型,核心线程数 = 2N。队列用有界的 ArrayBlockingQueue(容量 30),满了用 CallerRunsPolicy 背压——让调用线程自己执行编码,自动降速,避免队列无限堆积导致 OOM。"

3.2 Handler 消息机制

四大核心组件:

组件

职责

Handler

发送消息 + 处理消息

Message

消息载体,what/obj/arg1/arg2

MessageQueue

消息队列,按 when 时间排序的单链表

Looper

循环取消息分发,每个线程一个

工作流程:

  1. 子线程 handler.sendMessage() → MessageQueue.enqueueMessage() 按 when 插入链表

  2. Looper.loop() 死循环 → MessageQueue.next() 取出消息

  3. 没有到期消息 → nativePollOnce() 通过 epoll 阻塞(不耗 CPU)

  4. 有消息到期 → msg.target.dispatchMessage() → Handler.handleMessage()

  5. 消息处理完 → Message 回收到消息池(sPool),下次 obtain 复用

关键面试题:

Q: Looper.loop() 是死循环,为什么不会 ANR?

"loop() 确实是 for(;;) 死循环,但没有消息时它调用 MessageQueue.next() → nativePollOnce(),底层是 epoll_wait 将线程挂起,不消耗 CPU。有新消息时通过 nativeWake() 写管道唤醒线程。所以 loop() 本身不卡,ANR 是因为某一条消息处理时间太长导致后续消息排队等不到处理。"

Q: 为什么主线程能直接用 Handler?

"ActivityThread.main() 里调了 Looper.prepareMainLooper() 和 Looper.loop(),为主线程创建了 Looper。子线程用 Handler 必须先 Looper.prepare() + Looper.loop()。"

Q: Handler 内存泄漏怎么处理?

"Handler 持有 Activity 的隐式引用(内部类),Message 持有 Handler,MessageQueue 持有 Message。Activity 销毁时如果队列里还有未处理的消息,Activity 就泄漏了。解决方案:静态内部类 + WeakReference,或在 onDestroy 里 handler.removeCallbacksAndMessages(null) 清空队列。"

面试加分话术:

"推流 SDK 中编码在子线程,编码完成后需要回主线程更新 UI。我用 Handler 的 obtainMessage() 复用 Message 对象避免频繁创建,编码线程通过 Handler 发送帧统计数据到主线程。退出直播时在 onDestroy 里调 removeCallbacksAndMessages(null) 清空消息队列,防止编码回调持有已销毁的 Activity。"

3.3 ANR 产生原因

四种 ANR 类型及阈值(参考图中的表格):

类型

超时时间

触发条件

InputEvent

5 秒

触摸/按键事件未处理完

Broadcast (前台)

10 秒

onReceive 未执行完

Service (前台)

20 秒

onCreate/onStartCommand 未执行完

ContentProvider

10 秒

publish 未完成

常见原因:

  1. 主线程做 IO 操作(文件读写、SharedPreferences)

  2. 主线程做网络请求

  3. 主线程做密集计算(JSON 解析、Bitmap 解码)

  4. BroadcastReceiver onReceive 里做耗时操作

  5. 主线程等待锁(子线程持有锁,主线程等待)

排查方法:

  • ANR 发生后系统生成 /data/anr/traces.txt,里面有所有线程的调用栈

  • 线上用 ANR-WatchDog 监控主线程卡顿

  • adb bugreport 导出完整 ANR 日志

面试加分话术:

"直播项目中遇到过一个 ANR:推流 SDK 的初始化在 Application.onCreate 里同步执行,包括加载 so 库和创建编码器,低端机耗时超过 5 秒。排查发现是初始化做了太多串行操作,后来改成在子线程异步初始化推流 SDK,主线程只做 UI 初始化,用 CountDownLatch 保证首帧推流前 SDK 就绪。线上 ANR 率从 0.8% 降到 0.05%。"


四、网络协议

4.1 HTTP/HTTPS

HTTP 特点:

  • 无状态(靠 Cookie/Session 维持状态)

  • 基于请求-响应模型

  • HTTP/1.1 默认 keep-alive 长连接

  • HTTP/2 支持多路复用、头部压缩(HPACK)、服务端推送

  • HTTP/3 基于 QUIC(UDP),解决队头阻塞

HTTPS = HTTP + TLS/SSL:

plaintext

客户端                           服务端
  | --- ClientHello (支持的TLS版本、加密套件、随机数) ---> |
  | <-- ServerHello (选定套件、随机数) + 证书 + 公钥 ---- |
  | --- 验证证书 + 生成 pre-master-secret,用公钥加密 --> |
  | <-- 双方用三个随机数生成 session key,后续对称加密 --- |
  | ============ 对称加密通信 ========================== |

TLS 握手关键点:

  • 非对称加密(RSA/ECDHE)只在握手阶段用,用于交换 session key

  • 通信阶段用对称加密(AES),性能好

  • 证书链验证:服务端证书 → 中间 CA → 根 CA(系统内置)

4.2 TCP

三次握手:

plaintext

客户端 --- SYN, seq=x ---> 服务端
客户端 <-- SYN+ACK, seq=y, ack=x+1 --- 服务端
客户端 --- ACK, ack=y+1 ---> 服务端

为什么三次不是两次? 防止历史连接请求导致服务端白创建资源。如果两次握手,客户端发的旧 SYN 延迟到服务端,服务端直接建连但客户端不理会,资源浪费。

四次挥手:

plaintext

主动方 --- FIN ---> 被动方
被动方 --- ACK ---> 主动方 (被动方可能还有数据要发)
被动方 --- FIN ---> 主动方
主动方 --- ACK ---> 被动方 (TIME_WAIT, 2MSL 后关闭)

TIME_WAIT 为什么要等 2MSL?

  1. 确保最后一个 ACK 能到达被动方(如果丢了,被动方会重发 FIN)

  2. 确保本次连接的旧报文在网络中消亡,不会影响新连接

TCP 可靠传输机制:

  • 序列号 + 确认号

  • 超时重传(RTO 动态计算)

  • 滑动窗口(流量控制)

  • 拥塞控制(慢启动 → 拥塞避免 → 快重传 → 快恢复)

4.3 RTMP 简单原理(直播会用到)

RTMP 协议本质:基于 TCP 的流媒体协议,Adobe 设计。

推流流程:

plaintext

TCP 握手 → RTMP 握手 (C0/C1 → S0/S1/S2 → C2)
→ Connect (建立应用连接)
→ CreateStream (创建流)
→ Publish (声明推流)
→ 发送 FLV 音视频数据

FLV 封装:RTMP 传的不是裸 H.264,而是 FLV tag。H.264 NAL 需要从 Annex-B 格式(00 00 00 01 起始码)转成 AVCC 格式(4 字节长度前缀)。

为什么直播选 RTMP?

  • 基于 TCP,丢包重传保证可靠性

  • 延迟 1-3 秒,可接受

  • 生态成熟,CDN 厂商普遍支持

RTMP 的缺点:

  • TCP 队头阻塞:一个包丢了,后续包都等着

  • 延迟下限受 TCP 重传限制,做不到亚秒级

  • 趋势是 SRT / WebRTC 替代 RTMP 实现超低延迟

面试加分话术:

"项目中用 RTMP 推流,编码后的 H.264 NAL 需要转成 AVCC 格式封装成 FLV tag。遇到过一个坑:MediaCodec 输出的第一个 buffer 是 SPS/PPS(codec config),必须单独提取并拼成 AVC Sequence Header 在推流开始时发送,否则播放端解码器初始化失败导致花屏。弱网下 RTMP 的 TCP 队头阻塞会导致延迟累积,我们的方案是检测发送队列堆积超过阈值时主动降码率,避免队列无限增长。"


五、数据库

5.1 SQLite 基础

Android 中的 SQLite:

  • SQLiteOpenHelper 管理数据库创建和版本升级

  • SQLiteDatabase 提供增删改查 API

  • 推荐用 Room(Jetpack)封装,编译期检查 SQL,支持 LiveData/Flow

增删改查:

// insert
db.insert("table", nullColumnHack, contentValues);
// query
Cursor cursor = db.query("table", columns, selection, args, groupBy, having, orderBy);
// update
db.update("table", values, "whereClause", args);
// delete
db.delete("table", "whereClause", args);

5.2 事务

db.beginTransaction();
try {
    // 批量操作
    db.insert(...);
    db.insert(...);
    db.setTransactionSuccessful(); // 标记成功
} finally {
    db.endTransaction(); // 没标记成功就回滚
}

为什么要用事务?

  1. 原子性:要么全成功要么全回滚

  2. 性能:不用事务每条 SQL 都触发一次磁盘写入(fsync),1000 条插入从 30 秒降到 300ms

面试加分话术:

"直播间的弹幕消息需要本地缓存做历史记录查看。批量插入弹幕时用事务包裹,1000 条消息从 25 秒降到 200ms,因为不用事务每条 insert 都会触发 fsync 刷盘,事务模式下只在 endTransaction 时刷一次。"

5.3 大数据量优化

优化手段

说明

事务批量操作

insert/update 包在事务里,减少磁盘 IO

索引优化

WHERE 条件字段建索引,但索引太多影响写入速度

分页查询

LIMIT offset, count,避免一次查全表

预编译语句

SQLiteDatabase.compileStatement() 复用 SQL,避免重复解析

WAL 模式

enableWriteAheadLogging(),读写不互斥,并发性能提升

避免 SELECT *

只查需要的列,减少内存占用

异步操作

用 Room 的 suspend / Flow 自动切线程,避免主线程 IO

Room 的优势(面试加分):

  • 编译期检查 SQL 语法

  • 自动映射 Java/Kotlin 对象

  • 支持 RxJava/Coroutines/Flow

  • 自动处理迁移(Migration)

kotlin

@Dao
interface DanmuDao {
    @Query("SELECT * FROM danmu WHERE roomId = :roomId ORDER BY time DESC LIMIT :limit OFFSET :offset")
    suspend fun getDanmuList(roomId: String, limit: Int, offset: Int): List<Danmu>

    @Insert
    suspend fun insertBatch(danmus: List<Danmu>)

    @Transaction  // Room 自动用事务包裹
    suspend fun insertAll(danmus: List<Danmu>) {
        danmus.forEach { insert(it) }
    }
}

六、性能 & 内存优化

6.1 LeakCanary 与内存泄漏

LeakCanary 检测原理:

  1. ObjectWatcher.watch() 对目标对象创建 KeyedWeakReference(弱引用 + ReferenceQueue)

  2. GC 回收弱引用对象时,弱引用会被放入 ReferenceQueue

  3. 如果一段时间后(默认 5 秒 + 两次 GC)弱引用没出现在 Queue 中 → 疑似泄漏

  4. dumpHeap() 生成 hprof 文件

  5. 用 Shark(C++ 实现的 hprof 解析器,替代 HAHA)分析引用链

  6. 找到 GC Root 到泄漏对象的最短路径,展示给开发者

常见泄漏场景:

场景

原因

解决方案

静态变量持有 Activity

静态变量生命周期 = 进程生命周期

避免或用弱引用

内部类 Handler

隐式持有外部 Activity

静态内部类 + WeakReference

单例持有 Context

单例生命周期长于 Activity

用 ApplicationContext

动画未取消

动画持有一个 View 的引用

onDestroy 里 cancel

BroadcastReceiver 未注销

注册的 Receiver 持有 Activity

onDestroy 里 unregister

匿名内部类线程

线程持有外部类引用

静态内部类或及时中断

面试加分话术:

"直播项目中 LeakCanary 报过一个泄漏:Activity 持有推流编码器的回调对象,编码器在子线程异步运行,Activity 销毁时编码器还没结束,导致 Activity 无法回收。修复方案是将回调改为弱引用,并在 Activity onDestroy 时主动调 encoder.release() 停止编码线程。还有一个泄漏是礼物动画的 AnimatorListener 没移除,在 onDestroy 里 animator.cancel() + animator.removeAllListeners() 解决。"

6.2 OOM

常见 OOM 场景:

  1. Bitmap 过大:1080×1920 ARGB_8888 = 1080×1920×4 = 8.3MB,加载多张就 OOM

  2. 内存泄漏累积:多次进出页面,泄漏叠加到堆耗尽

  3. 帧缓存无上限:直播预览帧缓冲队列不限制大小

  4. 大文件一次读入内存:JSON/视频文件全量加载

Bitmap 优化:

// 采样率加载
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inJustDecodeBounds = true;  // 先只量尺寸
BitmapFactory.decodeFile(path, opts);
// 计算采样率
opts.inSampleSize = calculateSampleSize(opts, targetW, targetH);
opts.inJustDecodeBounds = false;  // 真正加载
Bitmap bmp = BitmapFactory.decodeFile(path, opts);

// Bitmap 配置优化
opts.inPreferredConfig = Bitmap.Config.RGB_565;  // 2字节/像素,省50%内存
// (但不支持透明通道,适合照片不适合UI图标)

// Android 8.0+ Bitmap 直接在 native 层分配,不会计入 Java 堆

onTrimMemory 回调:

@Override
public void onTrimMemory(int level) {
    if (level >= TRIM_MEMORY_BACKGROUND) {
        // 后台时清空图片缓存
        Glide.get(this).clearMemory();
    }
}

面试加分话术:

"直播间的观众头像列表是 OOM 重灾区,几百个头像 Bitmap 同时在内存中。用 Glide 做图片加载,配置 diskCacheStrategy(ALL) + 内存缓存限制。直播间退出时在 onTrimMemory 回调里清空 Glide 内存缓存。另外推流 SDK 的预览帧缓冲队列最初无上限,弱网时堆积几百帧 YUV(每帧 3MB),直接 OOM。后来加了队列上限 5 帧,超出时丢弃最旧帧。"

6.3 卡顿检测

三种检测手段:

1. Choreographer.FrameCallback

Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
    long lastTime = 0;
    @Override
    public void doFrame(long frameTimeNanos) {
        if (lastTime > 0) {
            long diff = (frameTimeNanos - lastTime) / 1_000_000; // ms
            if (diff > 16) {  // 超过一帧
                int dropped = (int) (diff / 16) - 1;
                Log.w("Jank", "掉帧 " + dropped + " 帧, 耗时 " + diff + "ms");
            }
        }
        lastTime = frameTimeNanos;
        Choreographer.getInstance().postFrameCallback(this);
    }
});

2. BlockCanary(原理)​:替换 Looper 的 mLogging(Printer),在 dispatchMessage 前后打印时间差,超过阈值认为卡顿。缺点是 Printer 本身有性能开销。

3. SysTrace / Perfetto:在关键方法插桩 Trace.beginSection() / Trace.endSection(),抓 trace 看每帧耗时分布。

面试加分话术:

"直播项目中用 Choreographer.FrameCallback 做线上卡顿监控,发现掉帧严重时自动上报堆栈。有一次发现礼物连击 View 的 onDraw 里每次都 new Paint 对象,60fps 下每秒创建 60 个 Paint,导致频繁 GC。改为成员变量复用后掉帧率从 12% 降到 1%。还遇到过一次是弹幕 RecyclerView 的 ViewHolder 复用失效,因为 itemViewType 写错了,每个 item 都新建 ViewHolder,布局 inflation 大量卡顿。"

6.4 启动优化

冷启动流程:

用户点击图标 → Zygote fork 进程 → ActivityThread.main()
→ Application.onCreate() → Activity.onCreate() → 首帧绘制

优化手段:

阶段

优化方案

Application.onCreate

异步初始化非核心 SDK;用启动框架(如 App Startup)管理依赖关系

Activity.onCreate

延迟加载非首屏可见的 View;用 ViewStub 按需 inflate

主题优化

windowBackground 设为启动图,避免白屏

闪屏页优化

闪屏 Activity 用透明主题 + 无布局,只做路由跳转

预加载

在闪屏页停留期间预加载首页数据

异步初始化框架思路:

// 把初始化任务按依赖关系拓扑排序,有依赖的串行,无依赖的并行
// 核心链路: 推流SDK → 美颜SDK → IM SDK (串行)
// 非核心: 统计SDK、推送SDK、日志SDK (并行,子线程)

面试加分话术:

"直播 App 冷启动从 3.2 秒优化到 1.5 秒。分析 SysTrace 发现 Application.onCreate 耗时 1.8 秒,里面有 8 个 SDK 串行初始化。改造方案:核心链路(推流 SDK + IM SDK)在主线程串行初始化,非核心(统计、推送、日志、热更新)全部放到子线程并行初始化。同时首屏直播间用 ViewStub 延迟加载互动区,先让视频画面快速展示。推流 SDK 的 so 库加载用 System.loadLibrary 放到异步线程,首次推流时用 CountDownLatch 确保已加载完毕。"

6.5 UI 掉帧

掉帧根因:每帧 16ms(60fps)内没完成 measure + layout + draw。

常见原因和优化:

原因

检测工具

优化方案

布局层级太深

Layout Inspector

用 ConstraintLayout 扁平化,减少嵌套

过度绘制

GPU 呈现模式分析

移除多余背景,@null 透明背景

主线程耗时

SysTrace / Perfetto

耗时操作移到子线程

频繁 requestLayout

代码审查

只改 size 用 requestLayout,只改外观用 invalidate

RecyclerView 滑动卡顿

RecyclerView Prefetch

复用 ViewHolder、设置 setItemViewCacheSize、避免在 onBindViewHolder 做重计算

内存抖动导致 GC

Memory Profiler

复用对象、避免循环内 new

过度绘制检测:开发者选项 → 调试 GPU 过度绘制。颜色含义:

  • 蓝色:多绘制 1 次(可接受)

  • 绿色:2 次(可接受)

  • 浅红:3 次(需优化)

  • 深红:4+ 次(必须优化)

面试加分话术:

"直播间互动区域掉帧严重,用 GPU 呈现模式分析发现大量深红区域。根因是弹幕层、礼物层、点赞层三个 FrameLayout 叠加,每层都有不透明背景。优化方案:中间层背景改为 @null,只在最底层保留背景;弹幕列表用 setHasFixedSize(true) 避免 RecyclerView 每次都 requestLayout;礼物特效的 SurfaceView 改为独立 Window 层(WindowManager.addView),避免影响 View 树的绘制。优化后从每帧 32ms 降到 12ms,掉帧率从 15% 降到 2%。"


面试回答建议

自我介绍时突出三个关键词:Android 音视频、直播推流、性能优化。

被问到自定义 View 时:引到直播间礼物连击 View 的实现,讲 measure + 事件分发 + 滑动冲突处理。

被问到多线程时:引到推流 pipeline 的线程模型——采集线程、编码线程池、网络发送线程,用 Handler 做跨线程通信。

被问到性能优化时:用"Profile 驱动"的思路串联——先用 Choreographer/SysTrace 量化问题,再针对性优化,最后用数据验证效果。强调每个优化都有前后对比数据,这是面试官最看重的。

被问到网络协议时:结合 RTMP 推流讲 TCP 的实际应用,展示你不只是背概念而是理解了协议在项目中的作用。

以上覆盖了六大模块的全部面试要点,每个知识点都配了直播项目实战案例和面试话术。三张图分别帮你建立了 View 绘制+事件分发、Handler 消息机制+ANR、性能优化工具链的视觉记忆框架。

核心面试策略:每个技术点都落到"我在直播项目中怎么用的、遇到什么问题、怎么解决的、效果数据是多少"​,用数据和案例说话,面试官会觉得你是真正做过而不是背过。


评论