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

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

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 适配」等方面入手,并通过系统工具全面测试。遵循这些实践,可打造对所有用户包容、可用的应用。

相关推荐
Tiramisu20231 小时前
【Android】Android开发
android
是三好1 小时前
SQL 性能分析及优化
android·数据库·sql
我命由我123452 小时前
Android Jetpack Compose - enableEdgeToEdge 函数、MaterialTheme 函数、remember 函数
android·java·java-ee·kotlin·android studio·android jetpack·android-studio
2501_915921433 小时前
没有 iOS 源码的前提下如何进行应用混淆,源码混淆失效后的替代
android·ios·小程序·https·uni-app·iphone·webview
林栩link3 小时前
【车载Android】多媒体开发入门(上) - MediaSession
android·android jetpack
GoldenPlayer3 小时前
OKHTTP连接保持
android
我有与与症3 小时前
用 KuiklyUI Canvas 打造天气预测图表
android
冬奇Lab3 小时前
稳定性性能系列之八——系统性能分析基础:Systrace与Perfetto入门
android·性能优化
程序员码歌3 小时前
短思考第268天,自媒体路上的4大坑点,很多人都踩过
android·前端·ai编程
消失的旧时光-19435 小时前
从 Android 组件化到 Flutter 组件化
android·flutter·架构