Android高级面试题
Android高级面试题。
博主博客
一、Kotlin println 输出什么内容?
operator fun String.invoke(x:() -> String) = this + x()
fun String.z() = "!$this"
fun String.toString() = "$this!"
println("x"{"y"}.z())
这段 Kotlin 代码的输出是:!xy
operator fun String.invoke(x: () -> String):- 这是一个扩展函数,允许字符串像函数一样被调用
- 它将原字符串与 lambda 返回的字符串连接起来
"x"{"y"}等价于"x".invoke { "y" },结果是"x" + "y" = "xy"
fun String.z():- 扩展函数,在字符串前加上
! "xy".z()返回"!xy"
- 扩展函数,在字符串前加上
fun String.toString():- 这个扩展函数实际上不会被调用
- 因为成员函数
toString()的优先级高于扩展函数 - 当 Kotlin 需要字符串表示时,会调用原始的
toString()成员函数
- 完整执行过程:
"x"{"y"}→"xy""xy".z()→"!xy"println("!xy")输出:!xy
二、Activity 生命周期
Android Activity 中 A->B->C->B 其中 B 的 launchMode=“singleTask”, A 和 C 的 launchMode=“standard”, 经历的生命周期有哪些?
这个导航场景中 Activity 的生命周期调用顺序如下:
第一步:A → B
A.onPause()B.onCreate()(B 是 singleTask,但此时没有实例,所以创建新实例)B.onStart()B.onResume()A.onStop()(如果 A 被完全覆盖)
当前栈状态: A → B (B 在栈顶)
第二步:B → C
B.onPause()C.onCreate()(C 是 standard,创建新实例)C.onStart()C.onResume()B.onStop()(B 被完全覆盖)
当前栈状态: A → B → C (C 在栈顶)
第三步:C → B (关键步骤)
由于 B 的 launchMode 是 singleTask,当 C 尝试启动 B 时:
- 系统会查找 B 的实例:发现在当前任务栈中已存在 B 的实例(在 A 上面,C 下面)
- 清除 B 上方的所有 Activity:将 C 从栈中弹出并销毁
- 将 B 带到前台:重用已有的 B 实例
生命周期调用顺序:
C.onPause()B.onNewIntent()(因为是重用已有实例,所以会收到新的 Intent)B.onRestart()(因为 B 之前处于 onStop 状态)B.onStart()B.onResume()C.onStop()C.onDestroy()(C 被从栈中移除)
最终栈状态: A → B (B 在栈顶)
重要注意事项:
- B 不会调用 onCreate():因为 singleTask 会重用已有实例
- B 会调用 onNewIntent():这是 singleTask/launchMode 的重要特性
- C 被完全销毁:从栈中移除并调用 onDestroy()
- A 仍然在栈底:没有被销毁,但处于 onStop 状态
三、简述 Retrofit 的原理?
Retrofit 的核心原理是通过动态代理 + 注解解析,将 Java 接口声明转换为 HTTP 请求,底层使用 OkHttp 执行网络请求。
1. 动态代理模式
- 使用
Proxy.newProxyInstance()创建接口的动态代理对象 - 当调用接口方法时,调用被转发到
InvocationHandler.invoke() - 这样可以在运行时解析方法注解并构建请求
2. 注解解析
- 解析方法上的注解(
@GET,@POST,@Path,@Query等) - 通过反射获取方法参数信息
- 根据注解信息构建 HTTP 请求的各个部分(URL、请求头、请求体等)
3. 请求构建过程
接口方法调用 → 动态代理拦截 → 解析注解和参数 → 构建 Request → OkHttp 执行
4. 核心组件协同工作
- Retrofit 类:配置和创建 API 实例
- ServiceMethod:缓存解析后的方法元数据(HTTP 方法、路径、参数处理器等)
- CallAdapter:适配返回类型(如 RxJava 的 Observable、Kotlin 协程的 suspend 函数)
- Converter:数据转换(请求体转换、响应体解析)
- OkHttpCall:包装 OkHttp 的 Call,执行实际网络请求
5. 工作流程
// 1. 定义接口
interface ApiService {
@GET("user/{id}")
Call<User> getUser(@Path("id") int id);
}
// 2. 创建 Retrofit 实例
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addConverterFactory(GsonConverterFactory.create())
.build();
// 3. 创建代理对象
ApiService service = retrofit.create(ApiService.class);
// 4. 调用方法(触发动态代理)
Call<User> call = service.getUser(123);
在面试中,可以按以下结构简要回答:
- 一句话概括:“Retrofit 是一个基于动态代理和注解解析的 RESTful HTTP 客户端框架。”
- 核心机制:“它通过动态代理技术拦截接口方法调用,解析方法上的注解和参数,构建 HTTP 请求。”
- 关键组件:“主要包含 ServiceMethod 缓存方法元数据,CallAdapter 适配返回类型,Converter 处理数据转换。”
- 底层依赖:“底层使用 OkHttp 执行网络请求,可以灵活配置各种拦截器和转换器。”
- 设计优势:“这种设计实现了高度解耦、类型安全,并且通过注解让代码更加简洁易读。”
高级特性原理(如果追问)
1. 适配器模式
- 支持 RxJava、协程等不同编程范式
- 通过
CallAdapter将Call<T>转换为其他类型
2. 数据转换器
- 支持 Gson、Moshi、Jackson 等序列化库
- 通过
Converter实现请求/响应体的序列化和反序列化
3. 拦截器机制
- 利用 OkHttp 的拦截器链实现统一处理(日志、认证、缓存等)
- 支持自定义应用拦截器和网络拦截器
4. 协程支持原理
- 使用
suspend函数时,Retrofit 生成一个Call适配器 - 在协程上下文中执行网络请求,自动处理线程切换和取消
与其他网络库对比
- Volley:Retrofit 更轻量,配置更灵活,类型安全
- OkHttp 直接使用:Retrofit 提供了更高层次的抽象,减少了样板代码
- Ktor Client:Retrofit 在 Java/Android 生态更成熟,Ktor 更适合 Kotlin 多平台
四、动态代理和静态代理的区别?
一句话概括区别
“静态代理在编译时就已经确定代理关系,需要手动为每个被代理类编写代理类;而动态代理在运行时动态生成代理类,一个代理类可以代理多个不同的接口。”
核心区别对比表
| 维度 | 静态代理 | 动态代理 |
|---|---|---|
| 创建时机 | 编译时创建 | 运行时动态生成 |
| 代理类数量 | 每个被代理类需要一个代理类 | 一个代理类可以代理多个接口 |
| 代码量 | 代码冗余,需要为每个方法编写代理逻辑 | 代码简洁,通用代理逻辑 |
| 灵活性 | 低,修改接口需要修改代理类 | 高,接口变更不影响代理逻辑 |
| 性能 | 稍高,直接方法调用 | 稍低,涉及反射调用(但可优化) |
| 实现方式 | 手动编写代理类,实现相同接口 | 使用 Proxy.newProxyInstance() 和 InvocationHandler |
1. 静态代理
// 1. 定义接口
interface UserService {
void addUser();
}
// 2. 实现类(被代理类)
class UserServiceImpl implements UserService {
public void addUser() { System.out.println("添加用户"); }
}
// 3. 静态代理类(需要手动编写)
class UserServiceProxy implements UserService {
private UserService target;
public UserServiceProxy(UserService target) {
this.target = target;
}
public void addUser() {
System.out.println("前置处理");
target.addUser(); // 调用真实对象
System.out.println("后置处理");
}
}
// 使用
UserService proxy = new UserServiceProxy(new UserServiceImpl());
proxy.addUser();
特点:
- 代理类和被代理类实现相同的接口
- 编译时就已经确定代理关系
- 需要为每个被代理类编写对应的代理类
2. 动态代理
// 动态代理处理器
class LogHandler implements InvocationHandler {
private Object target;
public LogHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("调用方法: " + method.getName());
Object result = method.invoke(target, args); // 反射调用
System.out.println("方法调用完成");
return result;
}
}
// 创建动态代理
UserService realService = new UserServiceImpl();
UserService proxy = (UserService) Proxy.newProxyInstance(
UserService.class.getClassLoader(),
new Class[]{UserService.class}, // 可以代理多个接口
new LogHandler(realService)
);
proxy.addUser();
特点:
- 在运行时动态生成代理类
- 通过
InvocationHandler统一处理所有方法调用 - 一个代理处理器可以代理多个不同的接口
五、Room 数据库如何升级?中间会生成什么文件?
核心回答
“Room 数据库升级主要通过 Migration 类实现,指定起始版本和目标版本,在 migrate() 方法中执行 SQL 语句修改表结构。升级过程中会生成 Schema JSON 文件,用于验证迁移的正确性。”
1. 升级方法
方法一:手动 Migration(最常用)
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
// 添加新列
database.execSQL("ALTER TABLE user ADD COLUMN age INTEGER NOT NULL DEFAULT 0")
// 创建新表
database.execSQL("CREATE TABLE address (id INTEGER PRIMARY KEY, street TEXT)")
}
}
val MIGRATION_2_3 = object : Migration(2, 3) {
override fun migrate(database: SupportSQLiteDatabase) {
// 修改表结构
database.execSQL("ALTER TABLE user RENAME TO user_old")
database.execSQL("CREATE TABLE user (...)")
database.execSQL("INSERT INTO user SELECT * FROM user_old")
database.execSQL("DROP TABLE user_old")
}
}
// 构建数据库时添加迁移
Room.databaseBuilder(context, AppDatabase::class.java, "app.db")
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
.build()
方法二:自动迁移(Room 2.4.0+)
适用于简单的架构更改:
@Database(
version = 2,
entities = [User::class],
autoMigrations = [
AutoMigration(from = 1, to = 2)
]
)
abstract class AppDatabase : RoomDatabase()
2. 升级过程中生成的文件
主要生成的文件:
- Schema JSON 文件(最重要)
- 路径:
app/schemas/your.package.name.AppDatabase/ - 命名:
版本号.json(如:1.json,2.json,3.json) - 内容:包含数据库的完整结构信息
- 路径:
{
"formatVersion": 1,
"database": {
"version": 2,
"identityHash": "xxx",
"entities": [
{
"tableName": "user",
"createSql": "CREATE TABLE `user` (`id` INTEGER, `name` TEXT, PRIMARY KEY(`id`))",
"fields": [...],
"primaryKey": {...}
}
],
"views": [],
"setupQueries": []
}
}
- 生成的实现类
- 路径:
app/build/generated/source/kapt/debug/your.package.name/ - 命名:
AppDatabase_Impl.java - 包含迁移逻辑和数据库创建代码
- 路径:
- 临时文件(升级过程中)
- 临时数据库文件:
app.db-journal(WAL 模式) - 备份文件:Room 在执行复杂迁移时可能创建临时备份
- 临时数据库文件:
3. 如何启用 Schema 导出
@Database(version = 2, entities = [User::class])
abstract class AppDatabase : RoomDatabase() {
companion object {
fun build(context: Context): AppDatabase {
return Room.databaseBuilder(context, AppDatabase::class.java, "app.db")
.addMigrations(MIGRATION_1_2)
// 启用 Schema 导出到 JSON 文件
.setJournalMode(JournalMode.TRUNCATE)
.fallbackToDestructiveMigration() // 开发时可添加,生产环境谨慎使用
.build()
}
}
}
// 在 build.gradle 中配置
android {
defaultConfig {
javaCompileOptions {
annotationProcessorOptions {
arguments += [
"room.schemaLocation": "$projectDir/schemas".toString(),
"room.incremental": "true",
"room.expandProjection": "true"
]
}
}
}
}
面试回答结构建议
- 先说核心方法:
- “Room 数据库升级主要通过创建 Migration 对象实现,指定起始版本和目标版本,在 migrate 方法中编写 SQL 语句修改表结构。”
- 提及生成的关键文件:
- “升级过程中最重要的生成文件是 Schema JSON 文件,它记录了每个版本的数据表结构,用于验证迁移的正确性。”
- “Schema 文件通常保存在
app/schemas/目录下,命名格式为版本号.json。”
- 补充其他生成文件:
- “还会生成 Room 的实现类
AppDatabase_Impl,其中包含了数据库创建和迁移的逻辑。”
- “还会生成 Room 的实现类
- 介绍高级特性:
- “Room 2.4.0 之后支持自动迁移,适用于简单的表结构变更。”
- “可以通过
fallbackToDestructiveMigration()设置降级策略。”
六、线上的 ANR 如何获取?
核心回答
“线上 ANR 主要通过两种方式获取:1. 监控上报 - 在应用内集成 ANR 监控组件,主动捕获并上报;2. 平台收集 - 利用第三方 APM 平台或 Google Play Console 等系统工具自动收集。”
1. ANR 监控原理
ANR 触发条件:
- 主线程阻塞超过 5 秒(前台服务)
- BroadcastReceiver 执行超过 10 秒
- ContentProvider 执行超过 10 秒
- Service 执行超过 20 秒(前台服务)
2. 主动监控方案
方案一:使用 ANR-WatchDog 开源库
// 添加依赖
implementation 'com.github.anrwatchdog:anrwatchdog:1.4.0'
// 初始化监控
ANRWatchDog().setANRListener { error ->
// 收集 ANR 信息
val stackTrace = error.stackTrace
val threadDump = getAllThreadsStackTraces()
val logcat = collectLogcat()
// 上报到服务器
uploadANRInfo(stackTrace, threadDump, logcat)
}.start()
方案二:自定义监控线程
class ANRMonitor : Thread() {
private var tick = 0
private var notified = false
override fun run() {
while (!isInterrupted) {
tick = (tick + 1) % 10
ticked = tick
Thread.sleep(5000) // 5秒检查一次
if (ticked == tick && !notified) {
// 5秒内主线程未更新,可能发生 ANR
notified = true
val stackTrace = Looper.getMainLooper().thread.stackTrace
collectAndReportANR(stackTrace)
} else {
notified = false
}
}
}
}
3. 信息收集内容
关键数据需要收集:
data class ANRInfo(
// 1. 堆栈信息
val mainThreadStackTrace: String,
val allThreadsDump: String,
// 2. 系统信息
val anrReason: String, // ANR 原因
val cpuUsage: String, // CPU 使用率
val memoryInfo: String, // 内存信息
val batteryLevel: Int, // 电量
// 3. 应用状态
val foregroundActivity: String,
val fragmentStack: String,
val viewHierarchy: String, // 当前视图层级
// 4. 设备信息
val deviceModel: String,
val osVersion: String,
val appVersion: String,
// 5. 日志信息
val logcatOutput: String, // 最近日志
val traceFile: String? // /data/anr/traces.txt 内容
)
4. 第三方平台方案
各大平台对比:
| 平台 | ANR 收集方式 | 特点 |
|---|---|---|
| Google Play Console | 自动收集 | 系统级收集,数据全面,有聚合分析 |
| Firebase Crashlytics | 自动+手动 | 实时报警,支持自定义日志 |
| Bugly | 自动收集 | 腾讯出品,国内网络优化好 |
| Sentry | SDK 集成 | 开源可自部署,支持自定义上报 |
| 听云/OneAPM | SDK 集成 | 性能监控全面,商业方案 |
Firebase 示例配置:
// build.gradle
implementation 'com.google.firebase:firebase-crashlytics-ktx:18.3.2'
// 初始化
FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(true)
// 自定义 ANR 上报
FirebaseCrashlytics.getInstance().recordException(anrException)
FirebaseCrashlytics.getInstance().setCustomKey("anr_time", System.currentTimeMillis())
FirebaseCrashlytics.getInstance().log("ANR StackTrace: $stackTrace")
5. 系统文件获取
手动获取 ANR 文件(需要 root):
fun collectSystemANRFiles(): List<File> {
val anrFiles = mutableListOf<File>()
// 1. traces.txt 文件
val tracesFile = File("/data/anr/traces.txt")
if (tracesFile.exists()) {
anrFiles.add(tracesFile)
}
// 2. dropbox 目录下的 ANR 报告
val dropboxDir = File("/data/system/dropbox/")
dropboxDir.listFiles { file ->
file.name.contains("anr_") || file.name.contains("data_app_anr")
}?.forEach { anrFiles.add(it) }
return anrFiles
}
6. 完整监控方案示例
class ANRMonitorManager {
fun setupANRMonitoring(context: Context) {
// 1. 初始化 WatchDog
ANRWatchDog().apply {
setReportThreadNamePrefix("ANR-")
setANRListener { anrError ->
handleANR(context, anrError)
}
setIgnoreDebugger(true) // 调试时也监控
start()
}
// 2. 设置 UncaughtExceptionHandler 兜底
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
if (thread == Looper.getMainLooper().thread && isANR(throwable)) {
collectANRInfo(context)
}
}
}
private fun handleANR(context: Context, anrError: ANRError) {
// 收集信息
val anrInfo = collectANRInfo(context, anrError)
// 保存到本地(用于下次启动上报)
saveANRInfoLocally(anrInfo)
// 尝试立即上报(如果网络可用)
if (isNetworkAvailable(context)) {
uploadANRInfo(anrInfo)
}
}
private fun collectANRInfo(context: Context, anrError: ANRError): ANRInfo {
return ANRInfo(
mainThreadStackTrace = anrError.stackTrace.joinToString("\n"),
allThreadsDump = getAllThreadsStackTraces(),
cpuUsage = getCpuUsage(),
memoryInfo = getMemoryInfo(context),
foregroundActivity = getCurrentActivityName(),
deviceModel = Build.MODEL,
osVersion = Build.VERSION.RELEASE,
appVersion = context.packageManager.getPackageInfo(
context.packageName, 0
).versionName,
logcatOutput = captureLogcat(100), // 最近100行日志
traceFile = tryReadTracesFile() // 尝试读取系统 traces
)
}
}
面试回答结构建议
- 先说总体方案:
- “线上 ANR 获取主要依靠主动监控上报,结合第三方平台辅助收集。”
- “核心是在应用中集成 ANR 检测机制,捕获发生时的主线程堆栈和系统状态。”
- 分点说明实现方式:
- “第一,使用 ANR-WatchDog 等开源库进行监控,在检测到 ANR 时收集堆栈信息。”
- “第二,上报到服务器,信息包括主线程堆栈、所有线程状态、CPU/内存使用情况等。”
- “第三,可以结合第三方 APM 平台如 Firebase、Bugly 的自动收集功能。”
- 补充进阶方案:
- “对于需要深度分析的场景,可以尝试获取系统的
/data/anr/traces.txt文件。” - “在 Android 8.0+ 上,可以通过
ActivityManager.getHistoricalProcessExitReasons()获取 ANR 原因。”
- “对于需要深度分析的场景,可以尝试获取系统的
- 提及注意事项:
- “ANR 上报要注意用户隐私,避免收集敏感信息。”
- “上报策略要考虑网络状况,失败时要有本地存储和重试机制。”
高级监控技巧
Android 8.0+ 官方 API:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val activityManager = context.getSystemService(ACTIVITY_SERVICE) as ActivityManager
val reasons = activityManager.getHistoricalProcessExitReasons(
context.packageName, 0, 0
)
reasons.forEach { reason ->
if (reason.reason == ActivityManager.ProcessExitReason.REASON_ANR) {
val description = reason.description // ANR 描述
val timestamp = reason.timestamp // 发生时间
// 上报处理
}
}
}
七、简述 ANRWatchDog 原理?
“ANRWatchDog 通过监控主线程是否在规定时间内响应一个定时发送的"探针信号"来检测 ANR。基本原理是:在独立监控线程中定期向主线程发送一个任务,如果主线程在规定时间(通常是5秒)内没有执行这个任务,就认为发生了 ANR。”
1. 整体架构
class ANRWatchDog : Thread() {
// 核心字段
private var tick: Int = 0 // 递增计数器,作为"探针"
private var reported: Boolean = false // 是否已报告 ANR
private var interval: Long = 5000 // 监控间隔(5秒)
// 主线程 Handler
private val handler = Handler(Looper.getMainLooper())
}
2. 核心监控机制
override fun run() {
while (!isInterrupted) {
val previousTick = tick
reported = false
// 向主线程发送"探针"任务
handler.post {
// 主线程执行:更新 tick,表示主线程还活着
tick = tick + 1
}
// 等待 interval 时间(如5秒)
try {
sleep(interval)
} catch (e: InterruptedException) {
return
}
// 检查主线程是否响应
if (tick == previousTick && !reported) {
// 如果 tick 没有变化,说明主线程在 interval 时间内没有响应
reported = true
// 获取主线程堆栈
val mainThread = Looper.getMainLooper().thread
val stackTrace = mainThread.stackTrace
// 触发 ANR 回调
anrListener?.onAppNotResponding(ANRError(...))
}
}
}
3. 工作流程
1. 监控线程启动循环
↓
2. 记录当前 tick 值,假设 tick = 0
↓
3. 向主线程 Handler 发送任务:tick = tick + 1
↓
4. 监控线程 sleep(5000) 等待5秒
↓
5. 检查 tick 是否变为 1:
- 如果 tick == 1:主线程正常执行了任务,继续循环
- 如果 tick == 0:主线程在5秒内没执行任务 → 检测到 ANR
↓
6. 收集主线程堆栈,触发回调
4. 关键优化细节
避免误判的机制:
// 1. 忽略调试模式
setIgnoreDebugger(true)
// 2. 设置超时容差值(比系统 ANR 时间稍短)
private val timeoutInterval = 4000 // 4秒,比系统5秒短,提前预警
// 3. 多次检测确认机制
if (tick == previousTick) {
// 第一次检测到可能 ANR,再给一次机会
sleep(1000) // 再等待1秒
if (tick == previousTick) { // 确认 ANR
reportANR()
}
}
// 4. 避免重复报告同一 ANR
private var lastReportedStackTrace: String? = null
if (currentStackTrace != lastReportedStackTrace) {
reportANR()
lastReportedStackTrace = currentStackTrace
}
5. 与系统 ANR 检测的区别
| 维度 | 系统 ANR 检测 | ANRWatchDog |
|---|---|---|
| 触发时机 | 主线程5秒无响应 | 可自定义(默认5秒) |
| 检测方式 | 系统信号量机制 | 探针+计数器机制 |
| 信息收集 | 系统自动生成 traces.txt | 自定义收集堆栈、日志 |
| 灵活性 | 固定,不可配置 | 高度可配置 |
| 调试模式 | 调试时不触发 | 可选择忽略调试模式 |
6. 实际源码关键部分
简化版源码解析:
class ANRWatchDog @JvmOverloads constructor(
private val timeoutInterval: Long = DEFAULT_ANR_TIMEOUT
) : Thread("ANR-WatchDog") {
private val uiHandler = Handler(Looper.getMainLooper())
private var tick = 0
private var reported = false
override fun run() {
setName("|ANR-WatchDog|")
while (!isInterrupted) {
val previousTick = tick
val previouslyReported = reported
reported = false
// 向主线程发送探针
uiHandler.post { tick = tick + 1 }
// 等待超时
try {
sleep(timeoutInterval)
} catch (e: InterruptedException) {
return
}
// 如果 tick 没有增加,说明主线程阻塞
if (tick == previousTick && !previouslyReported) {
// 确保不是调试模式
if (!ignoreDebugger && Debug.isDebuggerConnected()) {
continue
}
val error = ANRError("Application Not Responding", thread)
anrListener?.onAppNotResponding(error)
reported = true
}
}
}
}
面试回答结构建议
- 先说核心思想:
- “ANRWatchDog 通过独立的监控线程定期向主线程发送’心跳’任务,检查主线程是否在规定时间内响应。”
- “它本质上是一个定时器+探针机制,模拟了系统 ANR 检测的原理。”
- 解释具体实现:
- “监控线程每5秒向主线程的 Handler 发送一个递增计数器的任务。”
- “如果5秒后计数器没有变化,说明主线程没有执行这个任务,判断为 ANR。”
- “然后收集主线程堆栈信息,通过回调通知开发者。”
- 提及关键优化:
- “为了减少误报,它考虑了调试模式、设置了合理的超时阈值。”
- “还做了重复报告过滤、容错机制等优化。”
- 对比系统机制:
- “与系统 ANR 检测相比,ANRWatchDog 更轻量、可配置,但原理类似。”
实际应用示例
// 初始化配置
val watchDog = ANRWatchDog(4000) // 4秒超时
.setReportThreadNamePrefix("ANR-")
.setANRListener { error ->
// 收集详细信息
val stackTrace = error.stackTrace
val threadDump = getAllThreadsStackTraces()
// 上报到监控平台
uploadANRInfo(
stackTrace = stackTrace,
threadDump = threadDump,
timestamp = System.currentTimeMillis(),
appVersion = BuildConfig.VERSION_NAME
)
// 本地记录
saveToLocal(stripPersonalInfo(stackTrace))
}
.setIgnoreDebugger(true) // 调试时不触发
// 开始监控
watchDog.start()
// 停止监控
watchDog.interrupt()
优缺点分析
优点:
- 轻量级:只增加一个监控线程,开销小
- 及时性:比系统 ANR 日志更早获取信息
- 灵活性:可自定义超时时间、回调处理
- 兼容性:无需系统权限,适用于所有 Android 版本
缺点:
- 无法完全替代系统检测:某些系统级 ANR 可能检测不到
- 性能影响:虽然小,但仍有额外线程开销
- 误报可能:极端情况下可能误判(已有很多优化)
与其他方案对比
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| ANR-WatchDog | 探针+计数器 | 简单可靠,开源 | 需要集成第三方库 |
| FileObserver | 监控 traces.txt 文件变化 | 直接获取系统 ANR 日志 | 需要读取系统文件权限 |
| Sentry/Firebase | SDK 集成上报 | 功能全面,有分析平台 | 依赖第三方服务 |
| 自定义 Handler | 类似 WatchDog 原理 | 可高度定制 | 需要自己实现和维护 |
八、如何检测内存泄漏?可以使用什么工具?
“检测内存泄漏主要通过手动代码审查 + 自动化工具监控。常用工具有 LeakCanary、Android Profiler、MAT 等,从静态分析到动态监控形成完整检测体系。”
1. 常见内存泄漏场景
// 场景1:静态引用持有 Activity
class Singleton {
companion object {
var activity: Activity? = null // 错误!静态变量持有Activity
}
}
// 场景2:Handler 未及时移除消息
class MyActivity : Activity() {
private val handler = Handler(Looper.getMainLooper())
override fun onCreate(savedInstanceState: Bundle?) {
handler.postDelayed({
// 延迟任务,如果Activity销毁时未移除,会持有引用
updateUI()
}, 10000)
}
}
// 场景3:匿名内部类隐式持有外部类
class MyActivity : Activity() {
private lateinit var listener: SomeListener
override fun onCreate(savedInstanceState: Bundle?) {
listener = object : SomeListener {
override fun onEvent() {
// 此匿名内部类隐式持有MyActivity引用
doSomething()
}
}
}
}
2. 检测工具链
| 工具 | 使用阶段 | 特点 |
|---|---|---|
| LeakCanary | 开发/测试 | 自动检测,友好提示,集成简单 |
| Android Profiler | 开发 | Android Studio 内置,实时监控 |
| MAT (Memory Analyzer) | 深度分析 | 功能强大,适合复杂泄漏分析 |
| Android Studio Inspector | 开发 | 可视化内存分配 |
| adb shell dumpsys | 命令行 | 系统级内存信息 |
3. LeakCanary 深度使用
基本集成:
// build.gradle
dependencies {
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12'
}
// Application 中初始化(自动初始化已支持)
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
// 手动初始化(如果需要配置)
LeakCanary.config = LeakCanary.config.copy(
retainedVisibleThreshold = 3, // 泄漏阈值
dumpHeapWhenDebugging = false // 调试时不dump
)
}
}
高级配置:
// 1. 监听特定对象泄漏
class MyActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate()
val watcher = ObjectWatcher(
clock = Clock { SystemClock.uptimeMillis() },
checkRetainedExecutor = {
check(isFinishing) {
// 检测到泄漏时自定义处理
LeakCanary.showLeakDisplayActivity(this)
}
}
)
// 手动观察对象
watcher.watch(
watchedObject = this,
description = "MyActivity destroyed"
)
}
}
// 2. 自定义泄漏分析
class CustomLeakListener : OnObjectRetainedListener {
override fun onObjectRetained() {
// 发生泄漏时的自定义逻辑
trackLeakEvent()
}
}
4. Android Profiler 实战流程
步骤:
- 启动 Profiler:View → Tool Windows → Profiler
- 录制内存分配:
- 点击 Memory 时间线
- 选择 “Record object allocations”
- 操作应用复现场景
- 停止录制
- 分析堆转储:
- 点击 “Dump Java heap”
- 在 Heap Dump 视图中分析
关键检查点:
- Activities 实例数:同一 Activity 不应有多个实例
- Fragment 实例数:检查是否意外保留
- 大对象数组:检查 Bitmap、数组等
- 静态引用:筛选 static 字段
5. MAT (Memory Analyzer) 深度分析
分析流程:
# 1. 获取堆转储文件
adb shell am dumpheap <package-name> /data/local/tmp/heapdump.hprof
adb pull /data/local/tmp/heapdump.hprof .
# 2. 转换格式(MAT 需要)
hprof-conv heapdump.hprof heapdump-converted.hprof
MAT 关键功能:
- Histogram:按类统计对象数
- Dominator Tree:支配树,找出持有大量内存的对象
- Path to GC Roots:查看泄漏对象的引用链
- OQL:类似 SQL 的对象查询语言
典型分析步骤:
- 打开 Heap Dump 文件
- 执行 “Histogram” 查询
- 搜索 Activity 类名,查看实例数
- 右键 → Merge Shortest Paths to GC Roots → exclude weak/soft references
- 分析引用链,找出持有者
6. 自动化检测策略
单元测试集成:
class MemoryLeakTest {
@Test
fun testActivityLeak() {
// 使用 Espresso 启动 Activity
val scenario = ActivityScenario.launch(MainActivity::class.java)
scenario.onActivity { activity ->
// 模拟用户操作
onView(withId(R.id.button)).perform(click())
}
// 关闭 Activity
scenario.close()
// 等待 GC
Runtime.getRuntime().gc()
Thread.sleep(1000)
// 检查是否泄漏
val leakDetector = LeakDetector()
assertFalse(leakDetector.hasLeak(MainActivity::class.java))
}
}
// 自定义泄漏检测器
class LeakDetector {
fun hasLeak(activityClass: Class<*>): Boolean {
val weakRef = WeakReference(activityClass)
System.gc()
return weakRef.get() == null
}
}
7. 线上监控方案
上报关键指标:
data class MemoryMetrics(
// 内存指标
val heapSize: Long, // 堆大小
val usedMemory: Long, // 已用内存
val memoryClass: Int, // 内存等级
val lowMemory: Boolean, // 是否低内存
// 泄漏指标
val activityCount: Int, // Activity 实例数
val fragmentCount: Int, // Fragment 实例数
val bitmapCount: Int, // Bitmap 数量
val viewCount: Int, // View 数量
// 设备信息
val deviceModel: String,
val osVersion: String,
val appVersion: String
)
// 定期收集上报
class MemoryMonitor {
fun collectAndReport() {
val metrics = MemoryMetrics(
heapSize = Runtime.getRuntime().maxMemory(),
usedMemory = Runtime.getRuntime().totalMemory() -
Runtime.getRuntime().freeMemory(),
memoryClass = (getSystemService(ACTIVITY_SERVICE) as ActivityManager)
.memoryClass,
lowMemory = (getSystemService(ACTIVITY_SERVICE) as ActivityManager)
.isLowRamDevice,
activityCount = getActivityInstanceCount(),
fragmentCount = getFragmentInstanceCount()
)
// 异常检测
if (metrics.activityCount > 5) { // 同一Activity超过5个实例
reportMemoryLeakSuspected(metrics)
}
}
}
面试回答结构建议
- 先说检测思路:
- “检测内存泄漏我会采用多维度方法:开发时用 LeakCanary 自动检测,性能分析时用 Android Profiler,深度分析用 MAT。”
- “同时结合代码审查,重点关注常见泄漏场景。”
- 分工具说明:
- LeakCanary:“开发阶段首选,能自动检测并提供清晰引用链,集成简单。”
- Android Profiler:“官方工具,适合实时监控内存分配和对象创建。”
- MAT:“功能强大,适合复杂泄漏分析,可以从堆转储中找到具体引用关系。”
- 补充高级技巧:
- “线上可以通过监控 Activity/Fragment 实例数来发现泄漏。”
- “编写单元测试模拟场景,自动化检测泄漏。”
- “使用 WeakReference 辅助测试对象是否被回收。”
- 结合实际案例:
- “比如检测 Activity 泄漏,我会先用 LeakCanary 快速定位,再用 MAT 分析具体引用链,最后通过代码修复。”
完整检测流程示例
// 1. 集成自动检测
class DebugApplication : Application() {
override fun onCreate() {
super.onCreate()
if (LeakCanary.isInAnalyzerProcess(this)) {
return
}
LeakCanary.config = LeakCanary.config.copy(
retainedVisibleThreshold = 3,
dumpHeap = true
)
}
}
// 2. 关键点手动检测
object MemoryLeakChecker {
fun checkActivityLeak(activity: Activity) {
val ref = WeakReference(activity)
// 延迟检测
Handler(Looper.getMainLooper()).postDelayed({
val activity = ref.get()
if (activity != null && !activity.isFinishing) {
// 可能泄漏,记录堆栈
Log.e("MemoryLeak", "Possible leak: ${activity::class.simpleName}")
Log.e("MemoryLeak", "Stack trace:", Throwable())
}
}, 5000) // 5秒后检查
}
}
// 3. 生命周期监控
class LifecycleMonitor : Application.ActivityLifecycleCallbacks {
private val activityStack = mutableMapOf<String, Int>()
override fun onActivityDestroyed(activity: Activity) {
val name = activity::class.java.name
activityStack[name] = (activityStack[name] ?: 0) + 1
if (activityStack[name]!! > 3) {
// 同一Activity销毁超过3次,可能存在泄漏
reportPotentialLeak(name)
}
}
}
最佳实践建议
- 开发阶段:
- 所有调试版本集成 LeakCanary
- 关键页面添加手动检测点
- 定期使用 Profiler 检查
- 测试阶段:
- 编写内存泄漏测试用例
- 使用 Monkey 测试后检查内存
- 关键路径进行压力测试
- 线上阶段:
- 抽样收集内存指标
- 监控 OOM 率变化
- 建立异常报警机制
- 代码规范:
// 正确示例
class SafeActivity : Activity() {
private val handler = Handler(Looper.getMainLooper())
private val disposable = CompositeDisposable()
override fun onDestroy() {
// 1. 移除所有Handler消息
handler.removeCallbacksAndMessages(null)
// 2. 取消所有Rx订阅
disposable.clear()
// 3. 解除绑定
binding?.unbind()
super.onDestroy()
}
}
九、简述 LeakCanary 的原理
LeakCanary 是一个自动检测 Android 内存泄漏的开源库。它的核心原理是监控生命周期对象的销毁过程,通过弱引用和引用队列判断对象是否被回收,并在怀疑泄漏时分析堆转储找出引用链。
工作原理分步解析
1. 自动安装与初始化
- 通过
ContentProvider自动初始化,无需手动在 Application 中调用 - 监听
Application的ActivityLifecycleCallbacks和FragmentLifecycleCallbacks
2. 监控对象生命周期
// 监控 Activity
class ActivityWatcher {
fun watch(activity: Activity) {
val weakRef = WeakReference(activity, referenceQueue)
// 在 onDestroy 后开始检测
}
}
3. 检测泄漏的核心机制
引用队列(ReferenceQueue)+ 手动 GC
// 1. 创建弱引用并关联引用队列
val referenceQueue = ReferenceQueue<Any>()
val weakRef = WeakReference(watchedObject, referenceQueue)
// 2. 对象销毁后,延迟5秒(默认)检测
Handler().postDelayed({
// 3. 手动触发 GC
Runtime.getRuntime().gc()
System.runFinalization()
// 4. 检查引用队列
if (weakRef.isEnqueued) {
// 对象已进入引用队列 → 已被回收 → 无泄漏
} else {
// 对象未进入引用队列 → 可能泄漏 → 触发堆转储
dumpHeapAndAnalyze()
}
}, 5000)
4. 堆转储与分析
- 使用
Debug.dumpHprofData()生成堆转储文件 - 通过 Shark 库(替代旧版 HAHA)解析堆转储
- 查找从 GC Roots 到泄漏对象的引用链
- 排除弱引用、软引用等不影响垃圾回收的引用
5. 智能判断与展示
- 同一泄漏路径只报告一次,避免重复通知
- 提供清晰的可视化引用链,帮助定位问题
- 在 Logcat 输出详细信息,并显示系统通知
关键优化点
避免误判的机制
// 1. 多次检测确认
if (suspectLeak) {
// 等待更长时间,再次触发GC检测
postDelayed({ recheckLeak() }, 10000)
}
// 2. 忽略已知的不可回收对象(如MainActivity)
val ignoredTypes = setOf("MainActivity")
// 3. 阈值控制:同一泄漏路径出现多次才报告
val retainedThreshold = 5 // 默认5次
性能优化
- 延迟分析:堆转储和分析在独立进程进行,避免阻塞主进程
- 抽样检测:在高频场景下可配置抽样率,减少性能影响
- 智能触发:仅在怀疑泄漏时进行堆转储,避免频繁操作
与其他工具的区别
| 特性 | LeakCanary | Android Profiler | MAT |
|---|---|---|---|
| 检测方式 | 自动监控,主动报警 | 手动录制,被动分析 | 手动分析堆转储 |
| 实时性 | 实时检测,及时反馈 | 需要手动触发 | 离线分析 |
| 易用性 | 简单集成,自动报告 | 需要专业知识 | 学习曲线陡峭 |
| 性能影响 | 较小(独立进程分析) | 较大(实时监控) | 无运行时影响 |
实际应用示例
// 1. 基础集成(最新版自动初始化)
dependencies {
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12'
}
// 2. 自定义配置
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
LeakCanary.config = LeakCanary.config.copy(
// 只监控Activity
watchActivities = true,
watchFragments = true,
watchFragmentViews = false,
// 检测阈值
retainedVisibleThreshold = 3,
// 堆转储配置
dumpHeap = true,
dumpHeapWhenDebugging = false
)
}
}
回答结构建议
-
一句话概括:“LeakCanary 通过监控生命周期对象,利用弱引用和引用队列检测对象是否被回收,在怀疑泄漏时分析堆转储找出引用链。”
-
分步说明:
- “第一步:自动监听 Activity/Fragment 的销毁”
- “第二步:使用弱引用和引用队列,延迟检查对象是否被回收”
- “第三步:手动触发 GC,确认泄漏后生成堆转储”
- “第四步:使用 Shark 库分析堆转储,找出泄漏引用链”
-
提及关键优势:
- “自动化程度高,无需手动触发”
- “在独立进程分析,减少对主应用的影响”
- “提供直观的泄漏路径,便于快速定位问题”
-
补充适用场景:
- “适合开发调试阶段快速发现内存泄漏”
- “可通过配置减少对性能的影响”
- “不建议在生产环境全量使用,但可抽样收集”
十、简述 Glide 的原理?
“Glide 的核心原理是通过三级缓存 + 生命周期感知 + 高效解码实现图片的快速加载和内存优化。它将图片加载分解为请求管理、资源获取、解码转换和缓存复用四个关键阶段。”
详细原理分析
1. 整体架构
Glide.with(context) // 1. 绑定生命周期
.load(url) // 2. 设置数据源
.into(imageView) // 3. 执行加载并显示
2. 三级缓存机制
缓存层级:
// 1. 活动资源缓存(Active Resources)- 弱引用缓存
Map<Key, ResourceWeakReference> activeResources;
// 2. 内存缓存(Memory Cache)- LruCache
LruCache<Key, Resource> memoryCache;
// 3. 磁盘缓存(Disk Cache)- 多级策略
DiskLruCache diskCache;
// 4. 资源复用池(Bitmap Pool)- 复用 Bitmap 内存
BitmapPool bitmapPool;
缓存查找顺序:
1. 活动资源缓存(弱引用,正在使用的资源)
↓ 未命中
2. 内存缓存(LRU,最近使用过的资源)
↓ 未命中
3. 磁盘缓存(原始数据或转换后的图片)
↓ 未命中
4. 原始源(网络、文件等)
3. 生命周期管理
自动绑定机制:
// Glide 自动检测 Context 类型
public static RequestManager with(Activity activity) {
return getRetriever(activity).get(activity);
}
// 通过 Fragment 监听生命周期
SupportRequestManagerFragment fragment = new SupportRequestManagerFragment();
fragment.setRequestManager(requestManager);
activity.getSupportFragmentManager()
.beginTransaction()
.add(fragment, TAG)
.commitAllowingStateLoss();
生命周期状态:
- onStart():恢复请求
- onStop():暂停请求
- onDestroy():清理请求和资源
4. 图片加载流程
// 简化版加载流程
class RequestBuilder<R> {
void into(ImageView view) {
// 1. 构建请求
Request request = buildRequest(view);
// 2. 检查内存缓存
EngineResource<?> cached = loadFromCache(key);
if (cached != null) {
// 命中缓存,直接使用
view.setImageBitmap(cached.getBitmap());
return;
}
// 3. 启动加载任务
EngineJob job = engine.load(...);
// 4. 异步获取数据
DataFetcher fetcher = new HttpUrlFetcher(...);
byte[] data = fetcher.loadData();
// 5. 解码和转换
Bitmap bitmap = decodeStream(data);
Bitmap transformed = transformation.transform(bitmap);
// 6. 缓存结果
cacheResource(key, transformed);
// 7. 更新 UI
mainHandler.post(() -> view.setImageBitmap(transformed));
}
}
5. Bitmap 复用机制
BitmapPool 原理:
class BitmapPool {
private final LruPoolStrategy strategy;
// 获取可复用的 Bitmap
Bitmap get(int width, int height, Bitmap.Config config) {
Bitmap bitmap = strategy.get(width, height, config);
if (bitmap != null) {
// 重用 Bitmap 内存,避免重新分配
bitmap.eraseColor(Color.TRANSPARENT);
return bitmap;
}
return Bitmap.createBitmap(width, height, config);
}
// 释放 Bitmap 到池中
void put(Bitmap bitmap) {
if (bitmap.isMutable() && bitmapPool.canPut(bitmap)) {
strategy.put(bitmap);
}
}
}
6. 线程池管理
Glide 使用多个线程池:
class GlideBuilder {
// 1. 磁盘缓存读取线程池(1个线程)
ExecutorService diskCacheExecutor;
// 2. 网络请求线程池(默认4个线程)
ExecutorService sourceExecutor;
// 3. 动画解码线程池(2个线程,用于 GIF)
ExecutorService animationExecutor;
}
7. 高效解码策略
解码流程优化:
- 采样率计算:根据 ImageView 尺寸计算合适采样率
- 内存占用优化:使用 RGB_565 或 ARGB_8888 配置
- 大图优化:通过
Downsampler处理大图加载
面试回答结构建议
- 先说核心设计:
- “Glide 的设计核心是三级缓存和生命周期管理,确保图片加载高效且内存安全。”
- “通过活动资源缓存、内存缓存、磁盘缓存的三级机制,最大化复用图片资源。”
- 分步说明流程:
- “当调用
into()时,Glide 会先检查活动资源缓存(弱引用),然后是内存缓存(LRU),接着是磁盘缓存,最后才从网络加载。” - “加载过程中会自动绑定生命周期,在页面停止时暂停请求,销毁时清理资源。”
- “当调用
- 强调关键优化:
- “Glide 的 Bitmap 复用池(BitmapPool)能重用 Bitmap 内存,大幅减少 GC 频率。”
- “采用多个线程池分工协作,分别处理磁盘缓存、网络请求和 GIF 解码。”
- 对比其他库:
- “相比 Picasso,Glide 更注重内存优化和生命周期管理;相比 Fresco,Glide 更轻量且 API 更简洁。”
实际使用示例
// Glide 的智能特性示例
Glide.with(context)
.load(imageUrl)
.placeholder(R.drawable.placeholder) // 占位符
.error(R.drawable.error) // 错误图
.override(800, 600) // 指定尺寸
.centerCrop() // 裁剪方式
.circleCrop() // 圆形裁剪
.diskCacheStrategy(DiskCacheStrategy.AUTOMATIC) // 缓存策略
.transition(DrawableTransitionOptions.withCrossFade()) // 渐变动画
.priority(Priority.HIGH) // 加载优先级
.thumbnail(0.25f) // 缩略图先加载
.into(imageView)
缓存策略详解
enum DiskCacheStrategy {
ALL, // 缓存原始数据和转换后数据
NONE, // 不缓存
DATA, // 只缓存原始数据
RESOURCE, // 只缓存转换后数据
AUTOMATIC // 智能选择(默认)
}
与 Picasso 的核心区别
| 特性 | Glide | Picasso |
|---|---|---|
| 缓存机制 | 活动缓存 + 内存 + 磁盘 | 内存 + 磁盘 |
| 内存优化 | Bitmap 复用池,内存占用更小 | 相对较大 |
| GIF 支持 | 原生支持 | 需要额外库 |
| 生命周期 | 自动绑定 | 手动管理 |
| 默认格式 | RGB_565(内存小) | ARGB_8888(质量高) |
设计模式应用
- 建造者模式:
RequestBuilder构建加载请求 - 工厂模式:
ModelLoader根据不同数据源创建加载器 - 策略模式:
DiskCacheStrategy定义缓存策略 - 观察者模式:
Request监听加载状态
性能优化技巧
// 1. 预加载
Glide.with(context).load(url).preload();
// 2. 清理缓存
Glide.get(context).clearMemory(); // 清理内存缓存(UI线程)
Glide.get(context).clearDiskCache(); // 清理磁盘缓存(后台线程)
// 3. 暂停/恢复请求
Glide.with(context).pauseRequests();
Glide.with(context).resumeRequests();
// 4. 获取缓存的 Bitmap(用于其他用途)
Glide.with(context)
.asBitmap()
.load(url)
.into(object : CustomTarget<Bitmap>() {
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
// 直接获取 Bitmap 对象
}
});
十一、Webp,PNG,JPEG 与 RGBA_8888 什么关系?
“WebP、PNG、JPEG 是图像文件格式,定义了图像数据的压缩存储方式;RGBA_8888 是位图内存配置,决定了像素数据在内存中的存储格式。它们的关系是:图像文件在解码成 Bitmap 时,需要根据文件特性选择合适的 Bitmap.Config(如 RGBA_8888)来存储像素数据。”
详细解析
1. 概念区分
| 类型 | 说明 | 特点 |
|---|---|---|
| WebP/PNG/JPEG | 图像文件格式(编码格式) | 定义图像数据如何压缩存储于文件中 |
| RGBA_8888 | Bitmap.Config(位图配置) | 定义像素数据在内存中如何存储 |
2. Bitmap.Config 详解
Android 支持的位图配置:
enum Config {
ALPHA_8, // 每个像素8位,只有透明度,无颜色
RGB_565, // 每个像素16位:R(5位)G(6位)B(5位),无透明度
ARGB_4444, // 每个像素16位:ARGB各4位(已废弃)
ARGB_8888, // 每个像素32位:ARGB各8位(默认)
RGBA_F16, // 每个像素64位:高精度,用于广色域
HARDWARE // 特殊配置,纹理存储在GPU内存
}
RGBA_8888 的具体结构:
一个像素占32位(4字节):
- R(红色):8位(0-255)
- G(绿色):8位(0-255)
- B(蓝色):8位(0-255)
- A(透明度):8位(0-255)
内存计算:
// RGBA_8888 的内存占用
val width = 1000
val height = 1000
val memory = width * height * 4 // 4,000,000 字节 ≈ 3.81 MB
// 对比 RGB_565
val memory565 = width * height * 2 // 2,000,000 字节 ≈ 1.91 MB
3. 图像格式与位图配置的对应关系
| 图像格式 | 特性 | 推荐位图配置 | 说明 |
|---|---|---|---|
| JPEG | 有损压缩,不支持透明度 | RGB_565 或 ARGB_8888 | 无Alpha通道,解码时可节省内存 |
| PNG | 无损压缩,支持透明度 | ARGB_8888 | 需要Alpha通道支持透明度 |
| WebP | 有损/无损,支持透明度 | ARGB_8888 | 类似PNG,支持透明时用ARGB_8888 |
| GIF | 支持动画,有限颜色 | ARGB_8888 | 转换为ARGB显示 |
代码示例:
// 加载时指定配置
val options = BitmapFactory.Options().apply {
inPreferredConfig = Bitmap.Config.RGB_565 // 节省内存
}
// 对于JPEG(无透明度),使用RGB_565可节省一半内存
val jpegBitmap = BitmapFactory.decodeFile("image.jpg", options)
// 对于PNG/WebP(有透明度),必须使用ARGB_8888
options.inPreferredConfig = Bitmap.Config.ARGB_8888
val pngBitmap = BitmapFactory.decodeFile("image.png", options)
4. 实际应用中的选择策略
根据场景选择配置:
fun loadOptimalBitmap(context: Context, resId: Int): Bitmap {
val options = BitmapFactory.Options().apply {
// 1. 先只获取尺寸
inJustDecodeBounds = true
}
BitmapFactory.decodeResource(context.resources, resId, options)
// 2. 根据图像特性选择配置
val mimeType = options.outMimeType ?: ""
val config = when {
mimeType.contains("jpeg") -> Bitmap.Config.RGB_565 // JPEG用RGB_565
options.outWidth * options.outHeight > 1024 * 1024 ->
Bitmap.Config.RGB_565 // 大图用RGB_565节省内存
else -> Bitmap.Config.ARGB_8888 // 其他用ARGB_8888
}
// 3. 实际解码
options.inJustDecodeBounds = false
options.inPreferredConfig = config
return BitmapFactory.decodeResource(context.resources, resId, options)
}
5. 内存优化实践
Glide 中的智能配置选择:
// Glide 根据图像特性自动选择配置
class Downsampler {
Bitmap decode() {
// 检查图像是否有Alpha通道
if (hasAlpha(channel)) {
return decodeWithAlpha(config); // 使用ARGB_8888
} else {
return decodeWithoutAlpha(config); // 可能使用RGB_565
}
}
}
查看图像的配置信息:
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.image)
Log.d("Bitmap", "Config: ${bitmap.config}") // 输出:ARGB_8888
Log.d("Bitmap", "Has alpha: ${bitmap.hasAlpha()}") // 是否支持透明度
Log.d("Bitmap", "Byte count: ${bitmap.byteCount}") // 内存占用
面试回答结构建议
- 明确概念区别:
- “WebP、PNG、JPEG 是图像的文件格式,用于存储和传输;RGBA_8888 是 Bitmap 在内存中的存储格式。”
- “它们属于图像处理的不同阶段:文件格式决定磁盘存储效率,位图配置决定内存使用效率。”
- 解释具体关系:
- “当 Android 系统解码图像文件时,需要将压缩数据转换为像素数组,RGBA_8888 定义了这些像素在内存中的排列方式。”
- “不同图像格式特性不同:JPEG 无透明度,可用 RGB_565 节省内存;PNG/WebP 有透明度,通常需要 ARGB_8888。”
- 补充实践意义:
- “选择正确的配置能显著影响内存占用:ARGB_8888 质量最好但内存大,RGB_565 内存小但颜色较少。”
- “开发中应根据图像特性、显示需求、设备内存来平衡选择。”
- 举例说明:
- “比如加载一个 1000×1000 的 JPEG,用 RGB_565 只需 2MB 内存,用 ARGB_8888 则需要 4MB。”
高级知识点
1. 色彩空间支持
- sRGB:标准色彩空间,ARGB_8888 足够
- 广色域:需要 RGBA_F16(每个通道16位浮点数)
2. Android 8.0+ 的硬件位图
// Android O 引入的硬件位图,纹理存储在 GPU
val options = BitmapFactory.Options().apply {
inPreferredConfig = Bitmap.Config.HARDWARE
}
// 优点:GPU 渲染更快,不占用应用堆内存
// 限制:无法直接读取像素数据,需要先复制到软件位图
3. WebP 的特殊性
// WebP 支持有损、无损、透明、动画
fun isWebPAnimated(data: ByteArray): Boolean {
return if (data.size > 12) {
val header = String(data, 0, 12, Charsets.US_ASCII)
header == "RIFF" && data[15] == 'X'.toByte() // VP8X 扩展
} else false
}
4. 格式转换的代价
// 转换配置会产生新 Bitmap,消耗 CPU 和内存
Bitmap rgb565Bitmap = Bitmap.createBitmap(
argb8888Bitmap,
0, 0,
argb8888Bitmap.getWidth(),
argb8888Bitmap.getHeight(),
Bitmap.Config.RGB_565, // 转换配置
false
);
总结建议
- 优先使用 WebP:压缩率更高,支持透明,Android 4.0+ 原生支持
- 大图用 JPEG:无透明需求时,JPEG 文件更小
- 图标用 PNG/WebP:需要透明或简单图形
- 内存敏感用 RGB_565:列表图片、背景图等
- 质量要求高用 ARGB_8888:头像、高清图等
十二、简述 EventBus 的原理?
“EventBus 的核心原理是发布-订阅模式 + 注解解析 + 类型映射。它通过注册时扫描注解方法建立事件类型与处理方法的映射关系,发布事件时根据事件类型查找并调用对应的订阅者方法,实现了组件间的解耦通信。”
详细原理分析
1. 核心架构
三大核心组件:
// 1. 事件(Event):任意 Java 对象
class MessageEvent {
String message;
}
// 2. 订阅者(Subscriber):包含 @Subscribe 注解的方法
class SubscriberClass {
@Subscribe(threadMode = ThreadMode.MAIN)
public void onMessageEvent(MessageEvent event) {
// 处理事件
}
}
// 3. 事件总线(EventBus):管理中心
EventBus bus = EventBus.getDefault();
2. 注册过程原理
注册流程:
public void register(Object subscriber) {
// 1. 获取订阅者类
Class<?> subscriberClass = subscriber.getClass();
// 2. 查找所有 @Subscribe 注解的方法
List<SubscriberMethod> subscriberMethods =
findSubscriberMethods(subscriberClass);
// 3. 按事件类型分组存储
for (SubscriberMethod method : subscriberMethods) {
subscribe(subscriber, method);
}
}
// 核心数据结构:事件类型 → 订阅者列表
Map<Class<?>, CopyOnWriteArrayList<Subscription>> subscriptionsByEventType;
private void subscribe(Object subscriber, SubscriberMethod method) {
Class<?> eventType = method.eventType;
// 创建订阅关系
Subscription subscription = new Subscription(subscriber, method);
// 添加到对应事件类型的订阅列表
subscriptionsByEventType.putIfAbsent(eventType,
new CopyOnWriteArrayList<>());
subscriptionsByEventType.get(eventType).add(subscription);
// 同时维护订阅者 → 订阅事件类型的反向映射(用于快速注销)
List<Class<?>> subscribedEvents = typesBySubscriber.get(subscriber);
// ...
}
3. 事件发布原理
发布流程:
public void post(Object event) {
// 1. 获取当前线程的 posting 状态
PostingThreadState postingState = currentPostingThreadState.get();
List<Object> eventQueue = postingState.eventQueue;
// 2. 事件加入队列
eventQueue.add(event);
if (!postingState.isPosting) {
postingState.isPosting = true;
// 3. 处理队列中所有事件
while (!eventQueue.isEmpty()) {
Object currentEvent = eventQueue.remove(0);
postSingleEvent(currentEvent, postingState);
}
}
}
private void postSingleEvent(Object event, PostingThreadState postingState) {
Class<?> eventClass = event.getClass();
// 4. 查找事件的所有订阅者
List<Subscription> subscriptions;
synchronized (this) {
subscriptions = subscriptionsByEventType.get(eventClass);
}
if (subscriptions != null && !subscriptions.isEmpty()) {
// 5. 遍历订阅者,根据线程模式分发事件
for (Subscription subscription : subscriptions) {
postingState.event = event;
postingState.subscription = subscription;
// 根据线程模式调用订阅者方法
postToSubscription(subscription, event,
subscription.subscriberMethod.threadMode);
}
}
}
4. 线程模式处理
ThreadMode 的实现:
private void postToSubscription(Subscription subscription,
Object event,
ThreadMode threadMode) {
switch (threadMode) {
case POSTING:
// 在发布线程直接调用
invokeSubscriber(subscription, event);
break;
case MAIN:
// 在主线程调用
if (isMainThread()) {
invokeSubscriber(subscription, event);
} else {
mainThreadPoster.enqueue(subscription, event);
}
break;
case BACKGROUND:
// 在后台线程调用
if (isMainThread()) {
backgroundPoster.enqueue(subscription, event);
} else {
invokeSubscriber(subscription, event);
}
break;
case ASYNC:
// 在独立异步线程调用
asyncPoster.enqueue(subscription, event);
break;
}
}
// 实际调用订阅者方法
void invokeSubscriber(Subscription subscription, Object event) {
try {
subscription.subscriberMethod.method.invoke(
subscription.subscriber, event
);
} catch (IllegalAccessException | InvocationTargetException e) {
handleSubscriberException(e, event, subscription);
}
}
5. 粘性事件原理
粘性事件实现:
// 存储已发布的粘性事件
Map<Class<?>, Object> stickyEvents;
public void postSticky(Object event) {
synchronized (stickyEvents) {
// 1. 存入粘性事件缓存
stickyEvents.put(event.getClass(), event);
}
// 2. 正常发布
post(event);
}
public void register(Object subscriber) {
// ... 正常注册逻辑
// 3. 注册后检查粘性事件
if (subscriberMethod.sticky) {
Object stickyEvent = stickyEvents.get(eventType);
if (stickyEvent != null) {
// 立即发送缓存的粘性事件给新注册的订阅者
postToSubscription(subscription, stickyEvent,
subscription.subscriberMethod.threadMode);
}
}
}
6. 注解处理器优化(EventBus 3.0+)
索引生成原理:
// 编译时生成索引类
@EventBusIndex
public class MyEventBusIndex implements SubscriberInfoIndex {
private static final Map<Class<?>, SubscriberInfo> SUBSCRIBER_INDEX;
static {
SUBSCRIBER_INDEX = new HashMap<>();
// 注册时直接使用预先生成的信息,避免反射扫描
putIndex(new SimpleSubscriberInfo(MainActivity.class, true,
new SubscriberMethodInfo[] {
new SubscriberMethodInfo("onMessageEvent",
MessageEvent.class, ThreadMode.MAIN),
new SubscriberMethodInfo("onOtherEvent",
OtherEvent.class, ThreadMode.BACKGROUND),
}));
}
}
// 配置 EventBus 使用索引
EventBus.builder()
.addIndex(new MyEventBusIndex())
.installDefaultEventBus();
7. 优先级和事件取消
优先级处理:
// 订阅时按优先级排序
private void subscribe(Object subscriber, SubscriberMethod method) {
// ...
// 按优先级插入订阅列表
int size = subscriptions.size();
for (int i = 0; i <= size; i++) {
if (i == size || method.priority > subscriptions.get(i)
.subscriberMethod.priority) {
subscriptions.add(i, newSubscription);
break;
}
}
// 事件取消
public void cancelEventDelivery(Object event) {
PostingThreadState postingState = currentPostingThreadState.get();
if (!postingState.isPosting) {
throw new EventBusException("只能在事件处理期间取消");
}
postingState.canceled = true;
}
}
面试回答结构建议
- 先说核心模式:
- “EventBus 基于发布-订阅模式,核心是通过事件类型映射订阅者方法,实现组件间解耦通信。”
- 分步说明流程:
- “注册时扫描
@Subscribe注解方法,建立事件类型到处理方法的映射。” - “发布事件时根据事件类型查找订阅者,按照线程模式分发调用。”
- “支持粘性事件缓存,新注册的订阅者也能收到之前发布的事件。”
- “注册时扫描
- 强调关键特性:
- “支持四种线程模式,自动处理线程切换。”
- “通过注解处理器生成索引,避免运行时反射扫描,提升性能。”
- “支持优先级和事件取消机制。”
- 对比其他方案:
- “相比接口回调,EventBus 更解耦;相比 RxJava,EventBus 更轻量简单。”
设计模式应用
- 观察者模式:核心是发布-订阅
- 单例模式:
EventBus.getDefault() - 策略模式:不同线程模式对应不同分发策略
- 建造者模式:
EventBus.builder()创建配置
性能优化点
// 1. 使用 CopyOnWriteArrayList 保证线程安全
Map<Class<?>, CopyOnWriteArrayList<Subscription>> subscriptionsByEventType;
// 2. 线程局部变量减少对象创建
private final ThreadLocal<PostingThreadState> currentPostingThreadState =
new ThreadLocal<PostingThreadState>() {
@Override
protected PostingThreadState initialValue() {
return new PostingThreadState();
}
};
// 3. 事件继承支持
private List<Class<?>> lookupAllEventTypes(Class<?> eventClass) {
synchronized (eventTypesCache) {
List<Class<?>> eventTypes = eventTypesCache.get(eventClass);
if (eventTypes == null) {
eventTypes = new ArrayList<>();
Class<?> clazz = eventClass;
while (clazz != null) {
eventTypes.add(clazz);
addInterfaces(eventTypes, clazz.getInterfaces());
clazz = clazz.getSuperclass();
}
eventTypesCache.put(eventClass, eventTypes);
}
return eventTypes;
}
}
注意事项
- 内存泄漏风险:注册后必须及时注销,特别是在 Activity/Fragment 中
- 事件类型混淆:避免使用过于通用的事件类型(如
Object、String) - 过度使用问题:简单通信可用接口回调,复杂数据流考虑 RxJava
实际应用示例
// 1. 定义事件
data class LoginEvent(val userId: String, val success: Boolean)
// 2. 订阅事件
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
EventBus.getDefault().register(this)
}
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true, priority = 1)
fun onLoginEvent(event: LoginEvent) {
// 处理登录事件
}
override fun onDestroy() {
super.onDestroy()
EventBus.getDefault().unregister(this) // 必须注销!
}
}
// 3. 发布事件
EventBus.getDefault().post(LoginEvent("user123", true))
EventBus.getDefault().postSticky(LoginEvent("user456", false))
十三、写过什么自定义 View?简述自定义 View 如何实现?
“自定义 View 的实现主要分为继承现有控件扩展功能和继承 View/ViewGroup 完全自定义两种方式。核心步骤包括:定义自定义属性、重写构造方法、测量布局、绘制内容、处理交互事件。我曾实现过多种自定义 View,比如…”
详细实现步骤
1. 我实现过的自定义 View 示例
示例 1:圆形进度条(继承 View)
class CircleProgressView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
// 实现细节...
}
示例 2:组合控件 - 带删除按钮的输入框(继承 ViewGroup)
class ClearableEditText : LinearLayout {
private lateinit var editText: EditText
private lateinit var clearButton: ImageButton
// 实现细节...
}
示例 3:自定义下拉刷新控件(继承 ViewGroup)
class PullToRefreshLayout : ViewGroup {
// 实现下拉刷新动画和手势处理
}
2. 自定义 View 实现步骤
步骤一:定义自定义属性
<!-- res/values/attrs.xml -->
<declare-styleable name="CircleProgressView">
<attr name="progressColor" format="color|reference" />
<attr name="progressWidth" format="dimension|reference" />
<attr name="maxProgress" format="integer" />
<attr name="currentProgress" format="integer" />
<attr name="backgroundColor" format="color|reference" />
<attr name="textColor" format="color|reference" />
<attr name="textSize" format="dimension|reference" />
</declare-styleable>
步骤二:解析自定义属性
class CircleProgressView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private var progressColor = Color.BLUE
private var progressWidth = 20f
private var maxProgress = 100
private var currentProgress = 0
init {
// 解析自定义属性
val typedArray = context.obtainStyledAttributes(
attrs, R.styleable.CircleProgressView, defStyleAttr, 0
)
progressColor = typedArray.getColor(
R.styleable.CircleProgressView_progressColor, Color.BLUE
)
progressWidth = typedArray.getDimension(
R.styleable.CircleProgressView_progressWidth, 20f
)
maxProgress = typedArray.getInteger(
R.styleable.CircleProgressView_maxProgress, 100
)
currentProgress = typedArray.getInteger(
R.styleable.CircleProgressView_currentProgress, 0
)
typedArray.recycle()
}
}
步骤三:重写 onMeasure() - 测量尺寸
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
// 自定义 View 的默认大小
val defaultSize = 200.dpToPx(context) // 转换为像素
val measuredWidth = when (widthMode) {
MeasureSpec.EXACTLY -> widthSize // 精确模式,使用给定的尺寸
MeasureSpec.AT_MOST -> min(defaultSize, widthSize) // 最大模式,不超过给定尺寸
else -> defaultSize // 未指定,使用默认尺寸
}
val measuredHeight = when (heightMode) {
MeasureSpec.EXACTLY -> heightSize
MeasureSpec.AT_MOST -> min(defaultSize, heightSize)
else -> defaultSize
}
// 确保是正方形
val finalSize = min(measuredWidth, measuredHeight)
setMeasuredDimension(finalSize, finalSize)
}
// dp 转 px 扩展函数
fun Int.dpToPx(context: Context): Int {
return (this * context.resources.displayMetrics.density).toInt()
}
步骤四:重写 onDraw() - 绘制内容
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val centerX = width / 2f
val centerY = height / 2f
val radius = min(width, height) / 2f - progressWidth / 2
// 1. 绘制背景圆
paint.color = backgroundColor
paint.style = Paint.Style.STROKE
paint.strokeWidth = progressWidth
canvas.drawCircle(centerX, centerY, radius, paint)
// 2. 绘制进度圆弧
paint.color = progressColor
paint.style = Paint.Style.STROKE
paint.strokeCap = Paint.Cap.ROUND // 圆角端点
val sweepAngle = 360f * currentProgress / maxProgress
val rectF = RectF(
centerX - radius, centerY - radius,
centerX + radius, centerY + radius
)
canvas.drawArc(rectF, -90f, sweepAngle, false, paint)
// 3. 绘制进度文本
paint.color = textColor
paint.style = Paint.Style.FILL
paint.textSize = textSize
paint.textAlign = Paint.Align.CENTER
val progressText = "$currentProgress%"
val textY = centerY - (paint.descent() + paint.ascent()) / 2
canvas.drawText(progressText, centerX, textY, paint)
}
步骤五:处理触摸事件(如果需要)
// 如果需要点击或滑动交互
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
// 处理按下事件
return true
}
MotionEvent.ACTION_MOVE -> {
// 处理移动事件,更新进度
updateProgressFromTouch(event.x, event.y)
invalidate() // 请求重绘
return true
}
MotionEvent.ACTION_UP -> {
// 处理抬起事件
performClick() // 触发点击事件
return true
}
}
return super.onTouchEvent(event)
}
// 处理无障碍点击
override fun performClick(): Boolean {
super.performClick()
// 自定义点击逻辑
return true
}
步骤六:提供属性设置方法
// 提供外部设置进度的方法
fun setProgress(progress: Int) {
currentProgress = progress.coerceIn(0, maxProgress)
invalidate() // 更新UI
// 可选:触发监听器
progressChangeListener?.onProgressChanged(currentProgress)
}
// 进度改变监听器
interface OnProgressChangeListener {
fun onProgressChanged(progress: Int)
}
var progressChangeListener: OnProgressChangeListener? = null
3. 自定义 ViewGroup 实现
重写 onLayout():
class CustomLayout : ViewGroup {
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
var currentLeft = paddingLeft
var currentTop = paddingTop
for (i in 0 until childCount) {
val child = getChildAt(i)
if (child.visibility != GONE) {
// 测量子 View
val childWidth = child.measuredWidth
val childHeight = child.measuredHeight
// 布局子 View(这里示例为水平排列)
child.layout(
currentLeft,
currentTop,
currentLeft + childWidth,
currentTop + childHeight
)
// 更新下一个位置
currentLeft += childWidth + horizontalSpacing
}
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
// 测量所有子 View
measureChildren(widthMeasureSpec, heightMeasureSpec)
// 计算总尺寸
var totalWidth = paddingLeft + paddingRight
var maxHeight = 0
for (i in 0 until childCount) {
val child = getChildAt(i)
if (child.visibility != GONE) {
totalWidth += child.measuredWidth
maxHeight = max(maxHeight, child.measuredHeight)
}
}
// 添加子 View 之间的间距
totalWidth += max(0, childCount - 1) * horizontalSpacing
// 考虑父容器的限制
val width = resolveSize(totalWidth, widthMeasureSpec)
val height = resolveSize(maxHeight + paddingTop + paddingBottom, heightMeasureSpec)
setMeasuredDimension(width, height)
}
}
面试回答结构建议
- 先说自己的经验:
- “我实现过几种自定义 View,比如圆形进度条、带删除按钮的输入框组合控件、下拉刷新控件等。”
- “以圆形进度条为例,我通过继承 View 类,重写 onDraw 方法用 Canvas 绘制圆弧和文本。”
- 分步说明实现过程:
- “第一步:定义自定义属性,在 XML 中配置样式参数。”
- “第二步:在构造方法中解析这些属性。”
- “第三步:重写 onMeasure 确定 View 的大小。”
- “第四步:重写 onDraw 进行绘制,使用 Paint 和 Canvas。”
- “第五步:根据需要处理触摸事件,实现交互功能。”
- 补充关键知识点:
- “要注意 View 的三种测量模式(EXACTLY、AT_MOST、UNSPECIFIED)。”
- “绘制时要考虑性能,避免在 onDraw 中创建对象。”
- “自定义 ViewGroup 需要重写 onMeasure 和 onLayout 来管理子 View。”
- 提及优化和注意事项:
- “使用
invalidate()请求重绘,postInvalidate()在非 UI 线程调用。” - “考虑屏幕适配,使用 dp 或根据屏幕密度转换。”
- “实现
Parcelable接口保存状态,处理配置变化。”
- “使用
性能优化技巧
// 1. 避免在 onDraw 中创建对象
class OptimizedView : View {
private val paint = Paint() // 提前创建,复用对象
private val rectF = RectF() // 复用 RectF
override fun onDraw(canvas: Canvas) {
// 重用 paint 对象,而不是每次创建新的
paint.color = Color.RED
canvas.drawCircle(centerX, centerY, radius, paint)
}
}
// 2. 使用缓存
private var cachedBitmap: Bitmap? = null
fun drawToCache() {
if (cachedBitmap == null ||
cachedBitmap?.width != width ||
cachedBitmap?.height != height) {
cachedBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val cacheCanvas = Canvas(cachedBitmap!!)
// 在缓存 Canvas 上绘制
drawContent(cacheCanvas)
}
}
override fun onDraw(canvas: Canvas) {
cachedBitmap?.let {
canvas.drawBitmap(it, 0f, 0f, null)
}
}
// 3. 使用硬件加速
class HardwareAcceleratedView : View {
init {
// 开启硬件加速(默认开启,但可以显式设置)
setLayerType(LAYER_TYPE_HARDWARE, null)
}
}
高级特性实现
属性动画支持:
class AnimatableProgressView : View {
private var animatedProgress = 0f
set(value) {
field = value
invalidate()
}
fun setProgressWithAnimation(progress: Int, duration: Long = 1000) {
ValueAnimator.ofFloat(animatedProgress, progress.toFloat()).apply {
this.duration = duration
interpolator = AccelerateDecelerateInterpolator()
addUpdateListener { animator ->
animatedProgress = animator.animatedValue as Float
}
start()
}
}
}
保存和恢复状态:
class StatefulView : View {
private var currentState = 0
// 保存状态
override fun onSaveInstanceState(): Parcelable? {
val superState = super.onSaveInstanceState()
val savedState = SavedState(superState)
savedState.progressState = currentState
return savedState
}
// 恢复状态
override fun onRestoreInstanceState(state: Parcelable?) {
val savedState = state as? SavedState
if (savedState != null) {
super.onRestoreInstanceState(savedState.superState)
currentState = savedState.progressState
invalidate()
} else {
super.onRestoreInstanceState(state)
}
}
// 自定义 SavedState 类
private class SavedState : BaseSavedState {
var progressState = 0
constructor(superState: Parcelable?) : super(superState)
constructor(source: Parcel) : super(source) {
progressState = source.readInt()
}
override fun writeToParcel(out: Parcel, flags: Int) {
super.writeToParcel(out, flags)
out.writeInt(progressState)
}
}
}
常见问题与解决方案
- 测量不准确:确保理解父容器传递的 MeasureSpec
- 绘制超出边界:使用
canvas.clipRect()限制绘制区域 - 内存泄漏:避免在 View 中持有 Activity 的引用
- 卡顿问题:减少 onDraw 中的计算量,使用缓存
十四、模块化、组件化、插件化有什么区别?
“这三个概念是软件架构演进的三个不同层级:模块化是代码组织方式,组件化是业务解耦架构,插件化是运行时动态加载技术。模块化解决代码复用问题,组件化解决团队协作问题,插件化解决动态更新和包大小问题。”
详细对比分析
1. 概念定义对比
| 维度 | 模块化 | 组件化 | 插件化 |
|---|---|---|---|
| 定义 | 代码组织的拆分方式 | 业务模块的独立开发架构 | 动态加载和更新的技术 |
| 目标 | 代码复用、逻辑清晰 | 解耦、独立开发、测试、发布 | 动态扩展、热修复、包体积优化 |
| 粒度 | 代码/功能层面 | 业务/功能模块层面 | 完整功能/业务包层面 |
| 通信方式 | 直接依赖、接口调用 | 路由/AIDL/EventBus | 宿主-插件通信协议 |
| 构建方式 | 静态库、源码依赖 | 动态aar、独立构建 | 独立APK/Dex/So文件 |
| 运行时 | 编译期确定,整体运行 | 编译期确定,整体运行 | 运行时动态加载 |
2. 技术实现对比
模块化示例:
// 传统模块化 - 按功能分包
app/
├── common/ # 公共模块
├── network/ # 网络模块
├── database/ # 数据库模块
├── utils/ # 工具模块
└── main/ # 主模块
// build.gradle
dependencies {
implementation project(':common')
implementation project(':network')
}
组件化示例:
// 现代组件化架构
project/
├── app/ # 壳工程
├── component-home/ # 首页组件(可独立运行)
├── component-user/ # 用户组件
├── component-shop/ # 商城组件
├── component-pay/ # 支付组件
└── base/ # 基础库
// 组件独立运行配置
// component-home/build.gradle
if (isModule.toBoolean()) {
apply plugin: 'com.android.application' // 独立应用
} else {
apply plugin: 'com.android.library' // 组件库
}
// 路由通信
ARouter.getInstance().build("/home/main").navigation()
插件化示例:
// 插件化动态加载
class PluginManager {
fun loadPlugin(pluginPath: String) {
// 1. 创建插件 ClassLoader
val dexClassLoader = DexClassLoader(
pluginPath,
context.getDir("plugin", 0).absolutePath,
null,
context.classLoader
)
// 2. 加载插件资源
val pluginAssetManager = AssetManager::class.java.newInstance()
pluginAssetManager.javaClass.getMethod("addAssetPath", String::class.java)
.invoke(pluginAssetManager, pluginPath)
// 3. 创建插件上下文
val pluginContext = context.createPackageContext(
packageName, Context.CONTEXT_INCLUDE_CODE
)
// 4. 反射调用插件方法
val pluginClass = dexClassLoader.loadClass("com.plugin.MainActivity")
val pluginInstance = pluginClass.newInstance()
pluginClass.getMethod("onCreate").invoke(pluginInstance)
}
}
3. 具体特征对比
模块化特征:
- 代码层面的拆分,按功能或层级划分
- 模块间有依赖关系,编译时链接
- 适用于中小型项目,提升代码可维护性
组件化特征:
- 业务层面的独立,每个组件可独立开发、测试、发布
- 组件间解耦,通过路由/协议通信
- 适用于大型项目,支持多团队并行开发
- 双工程结构:组件可独立运行或集成到主工程
插件化特征:
- 动态加载,无需安装完整APK
- 支持热更新、热修复、功能动态下发
- 需要处理资源冲突、四大组件代理等问题
- 技术门槛较高,需兼容不同Android版本
4. 通信机制对比
模块化通信:
// 直接依赖,接口调用
interface UserService {
fun getUserInfo(): User
}
// 模块A提供实现
class UserServiceImpl : UserService {
override fun getUserInfo() = User()
}
// 模块B直接使用
val userService = UserServiceImpl()
val user = userService.getUserInfo()
组件化通信:
// 通过路由解耦
@Route(path = "/user/service")
class UserServiceImpl : UserService {
override fun getUserInfo() = User()
}
// 其他组件通过路由调用
val userService = ARouter.getInstance().build("/user/service")
.navigation() as? UserService
val user = userService?.getUserInfo()
// 或者通过 EventBus
EventBus.getDefault().post(UserEvent("get_user"))
插件化通信:
// 宿主-插件双向通信
interface IPluginCallback {
fun onPluginEvent(event: PluginEvent)
}
// 宿主定义接口
class HostApp : IPluginCallback {
override fun onPluginEvent(event: PluginEvent) {
// 处理插件事件
}
}
// 插件通过Binder调用宿主
val hostBinder = serviceConnection?.asInterface(proxy)
hostBinder?.callHostMethod("plugin_event", data)
// 资源隔离下的通信
val pluginContext = PluginContext(hostContext, pluginAssetManager)
val resources = pluginContext.resources
5. 实际应用场景
模块化适用场景:
- 中小型应用
- 团队规模较小(5人以下)
- 需要代码复用但不需要动态更新
组件化适用场景:
- 大型应用(如淘宝、微信)
- 多个团队并行开发
- 需要灰度发布、A/B测试
- 按需编译,提升编译速度
插件化适用场景:
- 需要动态功能扩展
- 频繁热修复需求
- 包体积优化(功能按需下载)
- 多业务线独立开发部署
6. 技术选型对比
| 技术 | 模块化 | 组件化 | 插件化 |
|---|---|---|---|
| 常用框架 | 无特定框架 | ARouter, WMRouter | VirtualAPK, RePlugin, Shadow |
| Gradle配置 | implementation project(':module') |
动态切换application/library | 独立构建插件包 |
| 代码隔离 | 包名隔离 | 组件隔离,编译时检查 | 完全的ClassLoader隔离 |
| 资源冲突 | 无 | 资源前缀规范 | 资源ID动态分配 |
| 四大组件 | 正常注册 | 通过路由跳转 | 代理/占位方式 |
| 打包方式 | 整体APK | 整体APK | 宿主APK + 插件包 |
面试回答结构建议
- 先定义三者的核心目标:
- “模块化关注代码组织,目标是复用和解耦;组件化关注业务独立,目标是团队协作和独立发布;插件化关注动态加载,目标是热更新和包体积优化。”
- 从技术角度对比:
- “模块化通过代码分包实现,组件化通过工程拆分和路由实现,插件化通过ClassLoader和资源隔离实现。”
- “组件化可以看作模块化的升级版,插件化则是更激进的动态化方案。”
- 结合实际经验:
- “在我们项目中,我们采用组件化架构:每个业务组件可以独立运行,通过ARouter通信,大大提升了并行开发效率。”
- “对于需要动态更新的功能,我们考虑过插件化方案,但由于兼容性和维护成本,最终选择了其他方案。”
- 提及演进关系:
- “这三个概念实际上是架构演进的三个阶段:先模块化解耦代码,再组件化解耦团队,最后插件化实现动态能力。”
具体实践案例
组件化实施步骤:
// 1. 配置开关
gradle.properties:
isModule=false
// 2. 组件独立运行配置
android {
defaultConfig {
if (isModule.toBoolean()) {
applicationId "com.example.home"
}
}
sourceSets {
main {
if (isModule.toBoolean()) {
manifest.srcFile 'src/main/module/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
}
}
}
}
// 3. 路由配置
@Route(path = "/home/main")
class HomeActivity : AppCompatActivity()
// 4. 集成模式
if (!isModule.toBoolean()) {
dependencies {
implementation project(':component-home')
implementation project(':component-user')
}
}
插件化关键技术点:
- ClassLoader隔离:每个插件有自己的ClassLoader
- 资源隔离:通过AssetManager添加插件路径
- 组件代理:Activity、Service等通过代理方式启动
- so库加载:动态加载native库
- 版本兼容:不同Android版本适配
优缺点分析
模块化:
- ✅ 优点:简单易实现,代码结构清晰
- ❌ 缺点:编译速度慢,难以独立开发测试
组件化:
- ✅ 优点:独立开发测试,编译速度快,便于团队协作
- ❌ 缺点:架构复杂,学习成本高,需要统一规范
插件化:
- ✅ 优点:动态更新,包体积小,功能热插拔
- ❌ 缺点:技术门槛高,兼容性问题,安全问题
发展趋势
- 模块化 → 组件化:几乎所有大型App的必经之路
- 插件化 → 小程序/快应用:更轻量、更安全的动态化方案
- Flutter/React Native:跨平台动态化方案的新选择
- App Bundle:Google官方动态分发方案
十五、OkHttp 原理 - 面试回答要点
核心回答
“OkHttp 的核心原理是拦截器责任链模式 + 连接池复用机制 + HTTP/2 多路复用。它通过拦截器链处理请求的各个阶段,利用连接池复用 TCP 连接,并支持 HTTP/2 提高网络性能。”
详细原理分析
1. 整体架构
核心流程图:
Request → 拦截器链 (责任链模式) → Response
↓
拦截器链:重试、桥接、缓存、连接、网络、回调
↓
连接池复用 (ConnectionPool)
2. 拦截器链机制
OkHttp 的核心设计模式:
// 拦截器接口
interface Interceptor {
fun intercept(chain: Chain): Response
}
// 内置拦截器链(按顺序执行)
val interceptors = listOf(
RetryAndFollowUpInterceptor(), // 重试和重定向
BridgeInterceptor(), // 添加请求头、处理 Cookie
CacheInterceptor(), // 缓存处理
ConnectInterceptor(), // 建立连接
CallServerInterceptor() // 发送请求和接收响应
)
// 拦截器链执行过程
class RealInterceptorChain(
val interceptors: List<Interceptor>,
val index: Int
) : Interceptor.Chain {
override fun proceed(request: Request): Response {
// 获取下一个拦截器
val next = RealInterceptorChain(interceptors, index + 1)
val interceptor = interceptors[index]
// 执行当前拦截器
return interceptor.intercept(next)
}
}
3. 各拦截器功能详解
① RetryAndFollowUpInterceptor - 重试和重定向
class RetryAndFollowUpInterceptor : Interceptor {
override fun intercept(chain: Chain): Response {
var request = chain.request()
var response: Response
var retryCount = 0
while (true) {
try {
response = chain.proceed(request)
} catch (e: IOException) {
// 检查是否应该重试
if (!canRetry(e, retryCount)) throw e
retryCount++
continue
}
// 检查是否需要重定向(状态码 3xx)
val redirectRequest = followUpRequest(response)
if (redirectRequest == null) return response
// 执行重定向
request = redirectRequest
}
}
}
② BridgeInterceptor - 请求桥接
- 添加默认请求头(User-Agent, Host, Connection, Accept-Encoding)
- 处理 Cookie
- 自动解压 GZIP 响应
- 处理请求体编码
③ CacheInterceptor - 缓存处理
class CacheInterceptor : Interceptor {
override fun intercept(chain: Chain): Response {
// 1. 尝试从缓存获取响应
val cacheCandidate = cache?.get(chain.request())
// 2. 缓存策略(根据请求头决定)
val strategy = CacheStrategy.Factory(
chain.request(), cacheCandidate
).compute()
// 3. 如果缓存有效,直接返回
if (strategy.networkRequest == null && strategy.cacheResponse != null) {
return strategy.cacheResponse!!
}
// 4. 否则发起网络请求
val networkResponse = chain.proceed(strategy.networkRequest!!)
// 5. 如果响应可缓存,存入缓存
if (cache != null && CacheStrategy.isCacheable(networkResponse)) {
cache.put(networkResponse)
}
return networkResponse
}
}
④ ConnectInterceptor - 建立连接
class ConnectInterceptor : Interceptor {
override fun intercept(chain: Chain): Response {
val realChain = chain as RealInterceptorChain
// 获取或创建连接
val exchange = realChain.call.initExchange(chain)
// 传递给下一个拦截器(CallServerInterceptor)
return realChain.copy(exchange = exchange).proceed(realChain.request)
}
}
⑤ CallServerInterceptor - 网络读写
- 写入请求头和请求体
- 读取响应头和响应体
- 处理流结束
- 关闭连接或放入连接池复用
4. 连接池复用机制
连接池核心实现:
class ConnectionPool(
val maxIdleConnections: Int = 5,
val keepAliveDuration: Long = 5 * 60 * 1000L // 5分钟
) {
private val connections = ArrayDeque<RealConnection>()
// 获取可用连接
fun get(address: Address, call: RealCall): RealConnection? {
synchronized(this) {
// 遍历连接池,寻找匹配的连接
for (connection in connections) {
if (connection.isEligible(address)) {
connections.remove(connection)
return connection
}
}
}
return null
}
// 清理空闲连接
fun cleanup() {
val now = System.nanoTime()
var longestIdleConnection: RealConnection? = null
var longestIdleDurationNs = Long.MIN_VALUE
synchronized(this) {
val iterator = connections.iterator()
while (iterator.hasNext()) {
val connection = iterator.next()
val idleDurationNs = now - connection.idleAtNanos
if (idleDurationNs > keepAliveDuration) {
// 超过保活时间,移除连接
iterator.remove()
connection.socket().closeQuietly()
} else if (idleDurationNs > longestIdleDurationNs) {
longestIdleDurationNs = idleDurationNs
longestIdleConnection = connection
}
}
}
}
}
5. HTTP/2 多路复用
HTTP/2 特性支持:
class RealConnection {
private var http2Connection: Http2Connection? = null
// 支持多路复用:多个请求共享一个连接
fun newCodec(client: OkHttpClient, chain: RealInterceptorChain): ExchangeCodec {
val socket = this.socket()!!
return if (http2Connection != null) {
// HTTP/2 连接
Http2ExchangeCodec(client, this, chain.request, http2Connection!!)
} else {
// HTTP/1.1 连接
Http1ExchangeCodec(client, this, source, sink)
}
}
}
6. 异步请求实现
Dispatcher 调度器:
class Dispatcher {
// 三种队列
private val readyAsyncCalls = ArrayDeque<AsyncCall>() // 等待队列
private val runningAsyncCalls = ArrayDeque<AsyncCall>() // 运行队列
private val runningSyncCalls = ArrayDeque<RealCall>() // 同步调用队列
// 最大并发请求数
var maxRequests = 64
// 单主机最大并发数
var maxRequestsPerHost = 5
fun enqueue(call: AsyncCall) {
synchronized(this) {
readyAsyncCalls.add(call)
}
promoteAndExecute()
}
private fun promoteAndExecute(): Boolean {
val executableCalls = mutableListOf<AsyncCall>()
synchronized(this) {
val i = readyAsyncCalls.iterator()
while (i.hasNext()) {
val call = i.next()
// 检查是否超过并发限制
if (runningAsyncCalls.size >= maxRequests) break
if (call.callsPerHost().get() >= maxRequestsPerHost) continue
i.remove()
call.callsPerHost().incrementAndGet()
executableCalls.add(call)
runningAsyncCalls.add(call)
}
}
// 执行请求
for (call in executableCalls) {
call.executeOn(executorService)
}
return executableCalls.isNotEmpty()
}
}
面试回答结构建议
- 先说核心设计:
- “OkHttp 的核心是拦截器责任链,将网络请求的各个处理阶段分解为独立的拦截器,每个拦截器负责特定功能。”
- “通过连接池复用 TCP 连接,减少握手开销,支持 HTTP/2 多路复用提高性能。”
- 分拦截器说明:
- “重试拦截器处理失败重试和重定向;桥接拦截器添加必要请求头;缓存拦截器实现 HTTP 缓存;连接拦截器管理连接;网络拦截器处理实际 I/O。”
- 强调关键特性:
- “连接池自动清理空闲连接,避免连接泄漏。”
- “支持透明的 GZIP 压缩,自动处理请求体压缩。”
- “Dispatcher 调度器管理请求并发和队列。”
- 结合实际使用:
- “在实际使用中,可以通过添加自定义拦截器实现日志、认证、监控等功能。”
高级特性
自定义拦截器示例:
// 日志拦截器
class LoggingInterceptor : Interceptor {
override fun intercept(chain: Chain): Response {
val request = chain.request()
val startTime = System.nanoTime()
// 记录请求信息
log("Sending request ${request.url}")
log("Headers: ${request.headers}")
val response = chain.proceed(request)
// 记录响应信息
val endTime = System.nanoTime()
log("Received response for ${response.request.url} in ${(endTime - startTime) / 1e6}ms")
log("Response code: ${response.code}")
log("Response headers: ${response.headers}")
return response
}
}
// 认证拦截器
class AuthInterceptor : Interceptor {
override fun intercept(chain: Chain): Response {
val originalRequest = chain.request()
// 添加认证头
val authRequest = originalRequest.newBuilder()
.header("Authorization", "Bearer $token")
.build()
return chain.proceed(authRequest)
}
}
性能优化设计
- 连接复用:相同的 Host 和端口重用连接
- 请求合并:HTTP/2 多路复用,一个连接并发多个请求
- 响应缓存:遵循 HTTP 缓存规范,减少重复请求
- 数据压缩:自动处理 GZIP,减少传输数据量
- 超时控制:连接、读取、写入超时分别配置
与其他库对比
| 特性 | OkHttp | HttpURLConnection | Volley |
|---|---|---|---|
| 连接池 | 支持,自动管理 | 简单连接池 | 有限支持 |
| HTTP/2 | 完整支持 | Android 5.0+ 支持 | 不支持 |
| 拦截器 | 强大灵活 | 不支持 | 有限支持 |
| 缓存 | 遵循 HTTP 规范 | 基本缓存 | 自定义缓存 |
| 异步 | Call + Callback | AsyncTask | RequestQueue |
这样的回答既清晰地解释了 OkHttp 的核心原理,又涵盖了实际使用和高级特性,适合面试场景。
评论