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

  1. operator fun String.invoke(x: () -> String):
    • 这是一个扩展函数,允许字符串像函数一样被调用
    • 它将原字符串与 lambda 返回的字符串连接起来
    • "x"{"y"} 等价于 "x".invoke { "y" },结果是 "x" + "y" = "xy"
  2. fun String.z():
    • 扩展函数,在字符串前加上 !
    • "xy".z() 返回 "!xy"
  3. fun String.toString():
    • 这个扩展函数实际上不会被调用
    • 因为成员函数 toString() 的优先级高于扩展函数
    • 当 Kotlin 需要字符串表示时,会调用原始的 toString() 成员函数
  4. 完整执行过程:
    • "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

  1. A.onPause()
  2. B.onCreate() (B 是 singleTask,但此时没有实例,所以创建新实例)
  3. B.onStart()
  4. B.onResume()
  5. A.onStop() (如果 A 被完全覆盖)

当前栈状态: A → B (B 在栈顶)

第二步:B → C

  1. B.onPause()
  2. C.onCreate() (C 是 standard,创建新实例)
  3. C.onStart()
  4. C.onResume()
  5. B.onStop() (B 被完全覆盖)

当前栈状态: A → B → C (C 在栈顶)

第三步:C → B (关键步骤)
由于 B 的 launchMode 是 singleTask,当 C 尝试启动 B 时:

  1. 系统会查找 B 的实例:发现在当前任务栈中已存在 B 的实例(在 A 上面,C 下面)
  2. 清除 B 上方的所有 Activity:将 C 从栈中弹出并销毁
  3. 将 B 带到前台:重用已有的 B 实例

生命周期调用顺序:

  1. C.onPause()
  2. B.onNewIntent() (因为是重用已有实例,所以会收到新的 Intent)
  3. B.onRestart() (因为 B 之前处于 onStop 状态)
  4. B.onStart()
  5. B.onResume()
  6. C.onStop()
  7. C.onDestroy() (C 被从栈中移除)

最终栈状态: A → B (B 在栈顶)

重要注意事项:

  1. B 不会调用 onCreate():因为 singleTask 会重用已有实例
  2. B 会调用 onNewIntent():这是 singleTask/launchMode 的重要特性
  3. C 被完全销毁:从栈中移除并调用 onDestroy()
  4. 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);

在面试中,可以按以下结构简要回答:

  1. 一句话概括:“Retrofit 是一个基于动态代理和注解解析的 RESTful HTTP 客户端框架。”
  2. 核心机制:“它通过动态代理技术拦截接口方法调用,解析方法上的注解和参数,构建 HTTP 请求。”
  3. 关键组件:“主要包含 ServiceMethod 缓存方法元数据,CallAdapter 适配返回类型,Converter 处理数据转换。”
  4. 底层依赖:“底层使用 OkHttp 执行网络请求,可以灵活配置各种拦截器和转换器。”
  5. 设计优势:“这种设计实现了高度解耦、类型安全,并且通过注解让代码更加简洁易读。”

高级特性原理(如果追问)
1. 适配器模式

  • 支持 RxJava、协程等不同编程范式
  • 通过 CallAdapterCall<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. 升级过程中生成的文件

主要生成的文件:

  1. 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": []
  }
}
  1. 生成的实现类
    • 路径:app/build/generated/source/kapt/debug/your.package.name/
    • 命名:AppDatabase_Impl.java
    • 包含迁移逻辑和数据库创建代码
  2. 临时文件(升级过程中)
    • 临时数据库文件: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"
                ]
            }
        }
    }
}

面试回答结构建议

  1. 先说核心方法
    • “Room 数据库升级主要通过创建 Migration 对象实现,指定起始版本和目标版本,在 migrate 方法中编写 SQL 语句修改表结构。”
  2. 提及生成的关键文件
    • “升级过程中最重要的生成文件是 Schema JSON 文件,它记录了每个版本的数据表结构,用于验证迁移的正确性。”
    • “Schema 文件通常保存在 app/schemas/ 目录下,命名格式为 版本号.json。”
  3. 补充其他生成文件
    • “还会生成 Room 的实现类 AppDatabase_Impl,其中包含了数据库创建和迁移的逻辑。”
  4. 介绍高级特性
    • “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
        )
    }
}

面试回答结构建议

  1. 先说总体方案
    • “线上 ANR 获取主要依靠主动监控上报,结合第三方平台辅助收集。”
    • “核心是在应用中集成 ANR 检测机制,捕获发生时的主线程堆栈和系统状态。”
  2. 分点说明实现方式
    • “第一,使用 ANR-WatchDog 等开源库进行监控,在检测到 ANR 时收集堆栈信息。”
    • “第二,上报到服务器,信息包括主线程堆栈、所有线程状态、CPU/内存使用情况等。”
    • “第三,可以结合第三方 APM 平台如 Firebase、Bugly 的自动收集功能。”
  3. 补充进阶方案
    • “对于需要深度分析的场景,可以尝试获取系统的 /data/anr/traces.txt 文件。”
    • “在 Android 8.0+ 上,可以通过 ActivityManager.getHistoricalProcessExitReasons() 获取 ANR 原因。”
  4. 提及注意事项
    • “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
            }
        }
    }
}

面试回答结构建议

  1. 先说核心思想
    • “ANRWatchDog 通过独立的监控线程定期向主线程发送’心跳’任务,检查主线程是否在规定时间内响应。”
    • “它本质上是一个定时器+探针机制,模拟了系统 ANR 检测的原理。”
  2. 解释具体实现
    • “监控线程每5秒向主线程的 Handler 发送一个递增计数器的任务。”
    • “如果5秒后计数器没有变化,说明主线程没有执行这个任务,判断为 ANR。”
    • “然后收集主线程堆栈信息,通过回调通知开发者。”
  3. 提及关键优化
    • “为了减少误报,它考虑了调试模式、设置了合理的超时阈值。”
    • “还做了重复报告过滤、容错机制等优化。”
  4. 对比系统机制
    • “与系统 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()

优缺点分析
优点:

  1. 轻量级:只增加一个监控线程,开销小
  2. 及时性:比系统 ANR 日志更早获取信息
  3. 灵活性:可自定义超时时间、回调处理
  4. 兼容性:无需系统权限,适用于所有 Android 版本

缺点:

  1. 无法完全替代系统检测:某些系统级 ANR 可能检测不到
  2. 性能影响:虽然小,但仍有额外线程开销
  3. 误报可能:极端情况下可能误判(已有很多优化)

与其他方案对比

方案 原理 优点 缺点
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 实战流程
步骤:

  1. 启动 Profiler:View → Tool Windows → Profiler
  2. 录制内存分配
    • 点击 Memory 时间线
    • 选择 “Record object allocations”
    • 操作应用复现场景
    • 停止录制
  3. 分析堆转储
    • 点击 “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 的对象查询语言

典型分析步骤:

  1. 打开 Heap Dump 文件
  2. 执行 “Histogram” 查询
  3. 搜索 Activity 类名,查看实例数
  4. 右键 → Merge Shortest Paths to GC Roots → exclude weak/soft references
  5. 分析引用链,找出持有者

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)
        }
    }
}

面试回答结构建议

  1. 先说检测思路
    • “检测内存泄漏我会采用多维度方法:开发时用 LeakCanary 自动检测,性能分析时用 Android Profiler,深度分析用 MAT。”
    • “同时结合代码审查,重点关注常见泄漏场景。”
  2. 分工具说明
    • LeakCanary:“开发阶段首选,能自动检测并提供清晰引用链,集成简单。”
    • Android Profiler:“官方工具,适合实时监控内存分配和对象创建。”
    • MAT:“功能强大,适合复杂泄漏分析,可以从堆转储中找到具体引用关系。”
  3. 补充高级技巧
    • “线上可以通过监控 Activity/Fragment 实例数来发现泄漏。”
    • “编写单元测试模拟场景,自动化检测泄漏。”
    • “使用 WeakReference 辅助测试对象是否被回收。”
  4. 结合实际案例
    • “比如检测 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)
        }
    }
}

最佳实践建议

  1. 开发阶段
    • 所有调试版本集成 LeakCanary
    • 关键页面添加手动检测点
    • 定期使用 Profiler 检查
  2. 测试阶段
    • 编写内存泄漏测试用例
    • 使用 Monkey 测试后检查内存
    • 关键路径进行压力测试
  3. 线上阶段
    • 抽样收集内存指标
    • 监控 OOM 率变化
    • 建立异常报警机制
  4. 代码规范
// 正确示例
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 中调用
  • 监听 ApplicationActivityLifecycleCallbacksFragmentLifecycleCallbacks

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
        )
    }
}

回答结构建议

  1. 一句话概括:“LeakCanary 通过监控生命周期对象,利用弱引用和引用队列检测对象是否被回收,在怀疑泄漏时分析堆转储找出引用链。”

  2. 分步说明

    • “第一步:自动监听 Activity/Fragment 的销毁”
    • “第二步:使用弱引用和引用队列,延迟检查对象是否被回收”
    • “第三步:手动触发 GC,确认泄漏后生成堆转储”
    • “第四步:使用 Shark 库分析堆转储,找出泄漏引用链”
  3. 提及关键优势

    • “自动化程度高,无需手动触发”
    • “在独立进程分析,减少对主应用的影响”
    • “提供直观的泄漏路径,便于快速定位问题”
  4. 补充适用场景

    • “适合开发调试阶段快速发现内存泄漏”
    • “可通过配置减少对性能的影响”
    • “不建议在生产环境全量使用,但可抽样收集”

十、简述 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. 高效解码策略
解码流程优化:

  1. 采样率计算:根据 ImageView 尺寸计算合适采样率
  2. 内存占用优化:使用 RGB_565 或 ARGB_8888 配置
  3. 大图优化:通过 Downsampler 处理大图加载

面试回答结构建议

  1. 先说核心设计
    • “Glide 的设计核心是三级缓存和生命周期管理,确保图片加载高效且内存安全。”
    • “通过活动资源缓存、内存缓存、磁盘缓存的三级机制,最大化复用图片资源。”
  2. 分步说明流程
    • “当调用 into() 时,Glide 会先检查活动资源缓存(弱引用),然后是内存缓存(LRU),接着是磁盘缓存,最后才从网络加载。”
    • “加载过程中会自动绑定生命周期,在页面停止时暂停请求,销毁时清理资源。”
  3. 强调关键优化
    • “Glide 的 Bitmap 复用池(BitmapPool)能重用 Bitmap 内存,大幅减少 GC 频率。”
    • “采用多个线程池分工协作,分别处理磁盘缓存、网络请求和 GIF 解码。”
  4. 对比其他库
    • “相比 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(质量高)

设计模式应用

  1. 建造者模式RequestBuilder 构建加载请求
  2. 工厂模式ModelLoader 根据不同数据源创建加载器
  3. 策略模式DiskCacheStrategy 定义缓存策略
  4. 观察者模式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}")  // 内存占用

面试回答结构建议

  1. 明确概念区别
    • “WebP、PNG、JPEG 是图像的文件格式,用于存储和传输;RGBA_8888 是 Bitmap 在内存中的存储格式。”
    • “它们属于图像处理的不同阶段:文件格式决定磁盘存储效率,位图配置决定内存使用效率。”
  2. 解释具体关系
    • “当 Android 系统解码图像文件时,需要将压缩数据转换为像素数组,RGBA_8888 定义了这些像素在内存中的排列方式。”
    • “不同图像格式特性不同:JPEG 无透明度,可用 RGB_565 节省内存;PNG/WebP 有透明度,通常需要 ARGB_8888。”
  3. 补充实践意义
    • “选择正确的配置能显著影响内存占用:ARGB_8888 质量最好但内存大,RGB_565 内存小但颜色较少。”
    • “开发中应根据图像特性、显示需求、设备内存来平衡选择。”
  4. 举例说明
    • “比如加载一个 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;
    }
}

面试回答结构建议

  1. 先说核心模式
    • “EventBus 基于发布-订阅模式,核心是通过事件类型映射订阅者方法,实现组件间解耦通信。”
  2. 分步说明流程
    • “注册时扫描 @Subscribe 注解方法,建立事件类型到处理方法的映射。”
    • “发布事件时根据事件类型查找订阅者,按照线程模式分发调用。”
    • “支持粘性事件缓存,新注册的订阅者也能收到之前发布的事件。”
  3. 强调关键特性
    • “支持四种线程模式,自动处理线程切换。”
    • “通过注解处理器生成索引,避免运行时反射扫描,提升性能。”
    • “支持优先级和事件取消机制。”
  4. 对比其他方案
    • “相比接口回调,EventBus 更解耦;相比 RxJava,EventBus 更轻量简单。”

设计模式应用

  1. 观察者模式:核心是发布-订阅
  2. 单例模式EventBus.getDefault()
  3. 策略模式:不同线程模式对应不同分发策略
  4. 建造者模式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;
    }
}

注意事项

  1. 内存泄漏风险:注册后必须及时注销,特别是在 Activity/Fragment 中
  2. 事件类型混淆:避免使用过于通用的事件类型(如 ObjectString
  3. 过度使用问题:简单通信可用接口回调,复杂数据流考虑 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)
    }
}

面试回答结构建议

  1. 先说自己的经验
    • “我实现过几种自定义 View,比如圆形进度条、带删除按钮的输入框组合控件、下拉刷新控件等。”
    • “以圆形进度条为例,我通过继承 View 类,重写 onDraw 方法用 Canvas 绘制圆弧和文本。”
  2. 分步说明实现过程
    • “第一步:定义自定义属性,在 XML 中配置样式参数。”
    • “第二步:在构造方法中解析这些属性。”
    • “第三步:重写 onMeasure 确定 View 的大小。”
    • “第四步:重写 onDraw 进行绘制,使用 Paint 和 Canvas。”
    • “第五步:根据需要处理触摸事件,实现交互功能。”
  3. 补充关键知识点
    • “要注意 View 的三种测量模式(EXACTLY、AT_MOST、UNSPECIFIED)。”
    • “绘制时要考虑性能,避免在 onDraw 中创建对象。”
    • “自定义 ViewGroup 需要重写 onMeasure 和 onLayout 来管理子 View。”
  4. 提及优化和注意事项
    • “使用 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)
        }
    }
}

常见问题与解决方案

  1. 测量不准确:确保理解父容器传递的 MeasureSpec
  2. 绘制超出边界:使用 canvas.clipRect() 限制绘制区域
  3. 内存泄漏:避免在 View 中持有 Activity 的引用
  4. 卡顿问题:减少 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 + 插件包

面试回答结构建议

  1. 先定义三者的核心目标
    • “模块化关注代码组织,目标是复用和解耦;组件化关注业务独立,目标是团队协作和独立发布;插件化关注动态加载,目标是热更新和包体积优化。”
  2. 从技术角度对比
    • “模块化通过代码分包实现,组件化通过工程拆分和路由实现,插件化通过ClassLoader和资源隔离实现。”
    • “组件化可以看作模块化的升级版,插件化则是更激进的动态化方案。”
  3. 结合实际经验
    • “在我们项目中,我们采用组件化架构:每个业务组件可以独立运行,通过ARouter通信,大大提升了并行开发效率。”
    • “对于需要动态更新的功能,我们考虑过插件化方案,但由于兼容性和维护成本,最终选择了其他方案。”
  4. 提及演进关系
    • “这三个概念实际上是架构演进的三个阶段:先模块化解耦代码,再组件化解耦团队,最后插件化实现动态能力。”

具体实践案例

组件化实施步骤:

// 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')
    }
}

插件化关键技术点:

  1. ClassLoader隔离:每个插件有自己的ClassLoader
  2. 资源隔离:通过AssetManager添加插件路径
  3. 组件代理:Activity、Service等通过代理方式启动
  4. so库加载:动态加载native库
  5. 版本兼容:不同Android版本适配

优缺点分析

模块化:

  • ✅ 优点:简单易实现,代码结构清晰
  • ❌ 缺点:编译速度慢,难以独立开发测试

组件化:

  • ✅ 优点:独立开发测试,编译速度快,便于团队协作
  • ❌ 缺点:架构复杂,学习成本高,需要统一规范

插件化:

  • ✅ 优点:动态更新,包体积小,功能热插拔
  • ❌ 缺点:技术门槛高,兼容性问题,安全问题

发展趋势

  1. 模块化组件化:几乎所有大型App的必经之路
  2. 插件化小程序/快应用:更轻量、更安全的动态化方案
  3. Flutter/React Native:跨平台动态化方案的新选择
  4. 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()
    }
}

面试回答结构建议

  1. 先说核心设计
    • “OkHttp 的核心是拦截器责任链,将网络请求的各个处理阶段分解为独立的拦截器,每个拦截器负责特定功能。”
    • “通过连接池复用 TCP 连接,减少握手开销,支持 HTTP/2 多路复用提高性能。”
  2. 分拦截器说明
    • “重试拦截器处理失败重试和重定向;桥接拦截器添加必要请求头;缓存拦截器实现 HTTP 缓存;连接拦截器管理连接;网络拦截器处理实际 I/O。”
  3. 强调关键特性
    • “连接池自动清理空闲连接,避免连接泄漏。”
    • “支持透明的 GZIP 压缩,自动处理请求体压缩。”
    • “Dispatcher 调度器管理请求并发和队列。”
  4. 结合实际使用
    • “在实际使用中,可以通过添加自定义拦截器实现日志、认证、监控等功能。”

高级特性

自定义拦截器示例:

// 日志拦截器
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)
    }
}

性能优化设计

  1. 连接复用:相同的 Host 和端口重用连接
  2. 请求合并:HTTP/2 多路复用,一个连接并发多个请求
  3. 响应缓存:遵循 HTTP 缓存规范,减少重复请求
  4. 数据压缩:自动处理 GZIP,减少传输数据量
  5. 超时控制:连接、读取、写入超时分别配置

与其他库对比

特性 OkHttp HttpURLConnection Volley
连接池 支持,自动管理 简单连接池 有限支持
HTTP/2 完整支持 Android 5.0+ 支持 不支持
拦截器 强大灵活 不支持 有限支持
缓存 遵循 HTTP 规范 基本缓存 自定义缓存
异步 Call + Callback AsyncTask RequestQueue

这样的回答既清晰地解释了 OkHttp 的核心原理,又涵盖了实际使用和高级特性,适合面试场景。

参考文献