可能是最贴近实际开发的面试问题

本系列为小说《逆袭西二旗》的技术讲解,用于详细说明剧情里涉及的开发细节。

SparseArray

SparseArray 是 Android 中一种常用数据结构,它将整数键映射到对象值,类似于 HashMap。不过,它针对整数键进行了优化,因此在处理基于整数的键时,比普通的 MapHashMap 更节省内存。

核心特性如下:

  1. 内存高效 :不同于 HashMap 依赖哈希表实现键值映射,SparseArray 直接使用基本类型 int 作为键,避免了自动装箱(如 intInteger),也无需额外数据结构,内存占用显著更低。
  2. 性能表现 :虽然大数据集下速度不及 HashMap,但在中等规模数据场景中,SparseArray 因内存优化带来的性能更优。
  3. 无空键限制 :由于仅支持基本类型 int 作为键,SparseArray 天然不允许空键。

SparseArray 的用法与其他 Map 类结构类似,示例代码:

kotlin 复制代码
import android.util.SparseArray

val sparseArray = SparseArray<String>()
sparseArray.put(1, "Tom")
sparseArray.put(2, "Sam")
sparseArray.put(3, "Bob")

// 获取元素
val value = sparseArray.get(2) // 结果为 "Sam"

// 移除元素
sparseArray.remove(3)

// 遍历元素
for (i in 0 until sparseArray.size()) {
    val key = sparseArray.keyAt(i)
    val value = sparseArray.valueAt(i)
    println("Key: $key, Value: $value")
}

面试问题

你会在哪些场景优先选择 SparseArray 而非 HashMap

二者在性能和适用性上的权衡是什么?

对比 Array/HashMap

  1. 避免自动装箱HashMap<Integer, Object> 会将键存储为 Integer 对象,频繁装箱/拆箱会带来额外开销;而 SparseArray 直接使用 int 键,既省内存又提升效率。
  2. 内存占用更少SparseArray 基于数组实现,无需像 HashMap 那样创建大量 Entry 对象,内存消耗更低。
  3. 紧凑存储:适用于「键值对数量少」或「键在整数范围内分布稀疏」的场景。
  4. Android 原生适配 :是 Android 专门设计的结构,适配移动端资源受限的场景(例如 UI 组件中 View ID 与对象的映射)。

局限性

尽管 SparseArray 内存高效,但并非适用于所有场景:

  1. 大数据集性能不足SparseArray 通过二分查找实现键查询,在超大规模数据下速度会慢于 HashMap
  2. 仅支持整数键 :限制了 int 类型以外的键场景。

总结

SparseArray 是「整数键-对象值」映射的专用数据结构,核心优势是内存高效------通过避免自动装箱、减少内存占用,在移动端资源受限场景(如 Android 应用)中表现突出。


运行时权限处理

在 Android 上处理运行时权限,是访问用户敏感数据的同时保证流畅用户体验的关键。自 Android 6.0(API 级别 23)起,应用必须在运行时明确请求危险权限,而不能在安装时自动获得。这种机制提升了用户隐私保护,让他们只在必要时才授权。

面试问题

Android 运行时权限系统如何提升用户隐私?

申请敏感权限前应做哪些准备?

声明与检查

在请求权限之前,应用必须在 AndroidManifest.xml 文件中声明该权限。运行时,应在用户与需要该权限的功能交互时才发起请求。在提示用户前,最好先用 ContextCompat.checkSelfPermission() 检查权限是否已授予。如果已授权,功能可直接执行;否则,应用需主动请求。

kotlin 复制代码
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
    != PackageManager.PERMISSION_GRANTED
) {
    // 权限未授予,需要申请
    if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA)) {
        // 向用户说明权限的必要性
        showPermissionRationale()
    } else {
        // 直接请求权限
        requestPermission()
    }
} else {
    // 权限已授予,执行功能逻辑
}

请求权限

推荐使用 ActivityResultLauncher API 简化权限请求流程,系统会向用户弹出授权对话框:

kotlin 复制代码
val requestPermissionLauncher = registerForActivityResult(
    ActivityResultContracts.RequestPermission()
) { granted ->
    if (granted) {
        // 权限已授予,执行功能
    } else {
        // 权限被拒绝,优雅处理(如提示功能受限)
    }
}

// 触发权限请求
requestPermissionLauncher.launch(Manifest.permission.CAMERA)

必要性说明

在某些情况下,系统建议在请求权限前先显示一个说明理由,可通过 shouldShowRequestPermissionRationale() 判断。如果返回 true,界面应解释为何需要该权限。这样做能提升用户体验,也更有可能获得用户的授权:

kotlin 复制代码
fun showPermissionRationale() {
    AlertDialog.Builder(this)
        .setTitle("Permission Required")
        .setMessage("This app needs access to your camera to function properly.")
        .setPositiveButton("OK") { _, _ ->
            requestPermissionLauncher.launch(Manifest.permission.CAMERA)
        }
        .setNegativeButton("Cancel", null)
        .show()
}

被拒绝的处理

若用户多次拒绝同一权限,Android 会视为「永久拒绝」,此时应用无法再次请求该权限,需引导用户到系统设置中手动开启:

kotlin 复制代码
if (!ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA)) {
    // 权限被永久拒绝,引导到设置
    showSettingsDialog()
}

fun showSettingsDialog() {
    val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
        data = Uri.parse("package:${packageName}")
    }
    startActivity(intent)
}

位置权限的特殊处理

位置权限分为 前台后台 两类。前台位置访问需要 ACCESS_FINE_LOCATIONACCESS_COARSE_LOCATION,而后台访问则需要 ACCESS_BACKGROUND_LOCATION,且需额外说明理由。

xml 复制代码
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />

从 Android 10(API 级别 29)开始,应用若要请求后台位置权限,必须先获取前台位置权限,然后再单独申请后台权限。

一次性权限

Android 11(API 30)新增「一次性权限」(适用于位置、相机、麦克风):用户授予的权限会在应用关闭后自动失效。

总结

合理处理运行时权限是保障安全、合规和用户信任的关键。通过「检查权限状态、说明权限用途、上下文内请求、优雅处理拒绝」等最佳实践,可打造兼顾隐私与体验的应用。


Looper、Handler、HandlerThread

LooperHandlerHandlerThread 是协同工作的组件,用于管理线程和处理异步通信。这些类对于在后台线程执行任务,同时与主线程交互以更新 UI 至关重要。

面试问题

Handler 如何配合 Looper 实现线程间通信?

Handler 的常见使用场景有哪些?

HandlerThread 是什么?与手动通过 Looper.prepare() 创建线程相比,它如何简化后台线程管理?

Looper

Looper 是 Android 线程模型的核心,负责维持线程的存活状态 ,并按顺序处理消息队列中的任务。它在主线程(UI 线程)和工作线程中都扮演着关键角色:

  • 作用 :持续监听消息队列,将消息/任务分发到对应的 Handler 处理。
  • 线程依赖 :需处理消息的线程必须绑定 Looper(主线程默认自带 Looper,工作线程需手动创建)。
  • 初始化方式 :通过 Looper.prepare() 为线程绑定 Looper,再通过 Looper.loop() 启动消息循环。

示例代码:

kotlin 复制代码
val thread = Thread {
    Looper.prepare() // 为线程绑定 Looper
    val handler = Handler(Looper.myLooper()!!) // 关联 Looper 创建 Handler
    Looper.loop() // 启动消息循环
}
thread.start()

Handler

Handler 用于在线程的消息队列中发送/处理消息/任务 ,需与 Looper 配合使用:

  • 作用:实现线程间通信(例如从后台线程更新 UI)。
  • 工作原理 :创建 Handler 时会绑定当前线程的 Looper,发送的任务会在该 Looper 对应的线程中执行。

示例代码(主线程中更新 UI):

kotlin 复制代码
val handler = Handler(Looper.getMainLooper()) // 绑定主线程 Looper
handler.post {
    // 在主线程执行 UI 更新
    textView.text = "Updated from background thread"
}

HandlerThread

HandlerThread 是一个内置了 Looper 的专用线程,它简化了创建能处理任务或消息队列的后台线程的过程。

  • 作用 :创建自带 Looper 的工作线程,支持在该线程上顺序处理任务。
  • 生命周期 :通过 start() 启动线程,通过 getLooper() 获取 Looper;使用完成后需调用 quit()quitSafely() 释放资源。

示例代码:

kotlin 复制代码
val handlerThread = HandlerThread("WorkerThread")
handlerThread.start() // 启动线程

// 关联 HandlerThread 的 Looper 创建 Handler
val workerHandler = Handler(handlerThread.looper)

workerHandler.post {
    // 在后台线程执行任务
    performBackgroundTask()
    handler.post {
        Log.d("WorkerThread", "Task completed")
    }
}

// 任务完成后停止线程
handlerThread.quitSafely()

核心区别与关联

  1. Looper:消息处理的「引擎」,维持线程存活并处理消息队列。
  2. Handler:与 Looper 交互的「接口」,负责消息的入队与处理。
  3. HandlerThread:简化后台线程创建的「工具」,自动完成 Looper 的初始化。

适用场景

  • Looper:用于主线程或工作线程中,管理持续的消息队列。
  • Handler:适用于线程间通信(例如后台线程更新 UI)。
  • HandlerThread:适用于需要独立线程处理任务的场景(例如数据处理、网络请求)。

总结

LooperHandlerHandlerThread 共同构成了 Android 中管理线程和消息队列的坚实框架。Looper 确保线程能持续处理任务,Handler 提供了任务通信的接口,而 HandlerThread 则以内置消息循环的方式,为管理工作线程提供了便捷途径。


异常追踪

追踪异常是定位、解决问题的关键,Android 提供了多种工具和技术来辅助调试。

面试问题

在开发环境中用 Logcat 调试异常,与在生产环境中用 Firebase Crashlytics 处理异常,二者的核心区别是什么?

如何在对应场景中解决异常?

使用 Logcat

Logcat 是 Android Studio 中查看日志、追踪异常的核心工具。当异常发生时,系统会在 Logcat 中输出完整的堆栈信息(包括异常类型、消息、抛出位置)。可通过 E/AndroidRuntime: FATAL 等关键词过滤异常日志。

使用 try-catch

在代码关键位置使用 try-catch 块,可控制异常处理流程、避免应用崩溃,并记录异常信息:

kotlin 复制代码
try {
    val result = performRiskyOperation()
} catch (e: Exception) {
    Log.e("Error", "Exception occurred: ${e.message}")
}

上述代码确保了异常会被记录下来,便于追踪和排查问题。

全局异常处理

通过 Thread.setDefaultUncaughtExceptionHandler 设置全局异常处理器,可捕获应用中未处理的异常,适用于集中式错误上报或日志记录:

kotlin 复制代码
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        Thread.setDefaultUncaughtExceptionHandler { thread, exception ->
            Log.e("UncaughtException", "Thread: ${thread.name}, Error: ${exception.message}")
            // 保存或上报异常详情
        }
    }
}

这种方法在调试和监控应用中的运行时问题方面非常有效。此外,你可以在 debugtest 版本中单独实现全局异常处理器,让测试人员能高效地追踪异常,并将详细报告发送给开发团队,从而简化调试和问题解决流程。

Firebase Crashlytics

Firebase Crashlytics 是生产环境中追踪异常的优秀工具,可自动记录未捕获的异常,并提供包含堆栈信息、设备状态、用户信息的详细崩溃报告。也可手动记录非关键异常:

kotlin 复制代码
try {
    val data = fetchData()
} catch (e: Exception) {
    Crashlytics.logException(e)
}

CrashlyticsFirebase 集成,便于分析崩溃并追踪修复进度。

Bugly

Firebase Crashlytics 类似,Bugly 是腾讯推出的一款面向移动应用的崩溃分析与 Bug 管理工具,广泛应用于 Android 和 iOS 平台。它能够实时监控应用的崩溃、卡顿、ANR(应用无响应)等问题,并提供详细的堆栈信息、设备环境和用户行为路径,帮助开发者快速定位和修复问题。

Bugly 还支持热更新功能(如 Tinker 集成),可在不重新发版的情况下修复线上 Bug,大幅提升运维效率。

作为腾讯自研并内部广泛使用的工具,Bugly 以稳定、轻量、易集成著称,是国内众多移动开发团队首选的质量监控解决方案之一。

断点调试

在 Android Studio 中设置断点,可暂停代码执行并交互式地检查应用状态(变量、方法调用、异常堆栈),是开发阶段定位异常根源的高效方式。

捕获 Bug 报告

通过 Android 捕获 Bug 报告,可收集设备日志、堆栈信息、系统状态等数据,辅助诊断问题。ADB 是获取 Bug 报告的常用工具,步骤如下:

  1. 开发者选项:开启「开发者选项」→ 进入「开发者选项」→ 选择「Take bug report」并分享报告。
  2. Android 模拟器:打开「Extended Controls」→ 选择「Bug report」并保存。
  3. ADB 命令:在终端执行 adb bugreport /path/to/save/bugreport

生成的 ZIP 文件包含 dumpsysdumpstatelogcat 等日志,是调试性能和崩溃问题的关键数据。

总结

异常追踪需结合本地工具与生产环境监控:Logcat 提供运行时日志,try-catch 和全局异常处理器保障异常的记录与管理,Firebase Crashlytics / Bugly 是生产环境中崩溃上报的强大工具,断点调试则在开发阶段提供交互式体验。这些方法结合使用,可实现全面的异常管理与故障排查。


构建变体与产品风味

构建变体(Build Variants)和产品风味(Product Flavors)是 Android 中从单一代码库生成不同应用版本的灵活机制,可高效管理「开发/生产版本」「免费/付费版本」等多配置场景。

面试问题

构建类型与产品风味的区别是什么?

二者如何配合生成构建变体?

构建变体(Build Variants)

构建变体(build variant)是将特定的构建类型(build type)和产品风味(product flavor,如果定义了的话)组合后的结果。Android Gradle 插件会为每种组合生成对应的构建变体,从而让你能针对不同使用场景生成定制化的 APK 或 bundle。

构建类型表示应用的构建方式,通常包括:

  • Debug:开发过程中使用的构建配置。通常包含调试工具、日志输出以及用于测试的调试证书。
  • Release:为发布优化的配置,常包含代码压缩、混淆,并使用发布密钥签名以上传至应用商店。

默认情况下,每个 Android 项目都包含 debugrelease 两种构建类型。开发者可根据具体需求添加自定义的构建类型。

产品风味(Product Flavors)

产品风味用于定义应用的不同变体(例如免费版/付费版、不同地区版本),每种风味可配置独立的应用 ID、版本号、资源等,无需重复代码即可生成定制化构建包。

示例(build.gradle 中的风味配置):

groovy 复制代码
android {
    flavorDimensions "version"

    productFlavors {
        free {
            applicationId "com.example.app.free"
            versionName "1.0-free"
        }
        paid {
            applicationId "com.example.app.paid"
            versionName "1.0-paid"
        }
    }
}

上述配置会生成 freeDebugfreeReleasepaidDebugpaidRelease 等构建变体。

构建类型与产品风味的组合

构建变体系统会将「构建类型」与「产品风味」组合成构建矩阵,例如:

  • freeDebug:免费版的调试构建。
  • paidRelease:付费版的发布构建。

freepaid 意味着产品风味,DebugRelease 表示构建类型

每种组合都可以拥有独立的配置、资源或代码。例如,你可能希望在免费版本中显示广告,而在付费版本中禁用广告。你可以使用特定于风味的资源目录,或 Java/Kotlin 代码来实现这一点。

优势

  1. 高效的配置管理:减少代码重复,从单一代码库管理多版本构建。
  2. 定制化行为:可针对不同变体调整功能(如付费版开启高级功能)、API(如调试版使用测试接口)。
  3. 自动化流程Gradle 会根据变体自动完成 APK 签名、代码压缩、混淆等任务。

总结

Android 中的构建变体由「构建类型」和「产品风味」组合而成:构建类型定义应用的构建方式(如 Debug / Release),产品风味定义应用的变体(如免费/付费)。二者结合可高效管理多配置场景,保障开发与发布的效率和可扩展性。


无障碍

无障碍(Accessibility)确保应用对所有用户(包括视障、听障、肢体障碍者)可用,既提升用户体验,也需符合 WCAG(Web Content Accessibility Guidelines,网页内容无障碍指南)等全球标准。

面试问题

支持动态字体大小的最佳实践有哪些?为什么文本大小优先使用 sp 而非 dp

开发者如何保障辅助技术用户的焦点管理与导航体验?有哪些工具可辅助测试无障碍问题?

内容描述(Content Descriptions)

为按钮、图片、图标等 UI 元素添加文本描述,便于 TalkBack 等屏幕阅读器向视障用户播报内容。若元素是装饰性的(无需被屏幕阅读器识别),可将 android:contentDescription 设置为 null,或标记为 View.IMPORTANT_FOR_ACCESSIBILITY_NO

示例:

xml 复制代码
<ImageView
    android:contentDescription="Profile Picture"
    android:src="@drawable/profile_image" />

支持动态字体大小

确保应用文本大小随设备设置的字体偏好自动缩放,需使用 sp 作为文本大小的单位:

xml 复制代码
<TextView
    android:textSize="16sp"
    android:text="Sample Text" />

焦点管理与导航

为自定义 View、对话框、表单等组件合理管理焦点行为,使用 android:nextFocusDownandroid:nextFocusUp 等属性定义键盘/手柄用户的逻辑导航路径。同时需配合屏幕阅读器测试,确保焦点移动符合自然交互逻辑。

颜色对比度与视觉无障碍

保证文本与背景的颜色对比度足够高,以提升视障或色弱用户的可读性。可使用 Android Studio 的「无障碍扫描器」评估并优化应用的颜色对比度。

自定义 View 的无障碍支持

创建自定义 View 时,需实现 AccessibilityDelegate 定义屏幕阅读器与组件的交互逻辑。重写 onInitializeAccessibilityNodeInfo() 方法,提供有意义的组件描述和状态:

kotlin 复制代码
class CustomView(context: Context) : View(context) {
    init {
        importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_YES
        accessibilityDelegate = object : AccessibilityDelegate() {
            override fun onInitializeAccessibilityNodeInfo(
                host: View,
                info: AccessibilityNodeInfo
            ) {
                super.onInitializeAccessibilityNodeInfo(host, info)
                info.text = "Custom component description"
            }
        }
    }
}

无障碍测试

使用 Android Studio 中的「无障碍扫描器」「布局检查器」等工具,识别并修复无障碍问题,确保应用对辅助技术依赖用户友好。

总结

保障 Android 应用的无障碍性,需从「内容描述、动态字体、焦点管理、颜色对比度、自定义 View 适配」等方面入手,并通过系统工具全面测试。遵循这些实践,可打造对所有用户包容、可用的应用。

相关推荐
BoomHe1 天前
Android AOSP13 原生 Launcher3 壁纸获取方式
android
Digitally1 天前
如何将联系人从 Android 转移到 Android
android
一直在想名1 天前
Flutter 框架跨平台鸿蒙开发 - 黑白屏
flutter·华为·kotlin·harmonyos
李小枫1 天前
webflux接收application/x-www-form-urlencoded参数
android·java·开发语言
爱丽_1 天前
MySQL `EXPLAIN`:看懂执行计划、判断索引是否生效与排错套路
android·数据库·mysql
NPE~1 天前
[App逆向]环境搭建下篇 — — 逆向源码+hook实战
android·javascript·python·教程·逆向·hook·逆向分析
yewq-cn1 天前
AOSP 下载
android
cch89181 天前
Laravel vs ThinkPHP:PHP框架终极对决
android·php·laravel
米码收割机1 天前
【Android】基于安卓app的汽车租赁管理系统(源码+部署方式+论文)[独一无二]
android·汽车
流星雨在线1 天前
安卓使用 Startup 管理三方 SDK 初始化
android·startup