安卓鸿蒙面试
一、说一下Handler的机制原理
问:说一下Handler的机制原理?
答: Handler是Android中用于跨线程通信的核心机制,主要解决子线程不能直接操作UI的问题。其核心组件包括:
Handler:发送和处理消息。通过sendMessage()或post()将消息发送到MessageQueue,重写handleMessage()处理消息。
MessageQueue:消息队列,单链表结构,按时间顺序存储待处理的消息。
Looper:消息循环器,不断从MessageQueue中取出消息,分发给对应的Handler处理。每个线程只有一个Looper,通过Looper.prepare()初始化,Looper.loop()开启循环。
Message:消息载体,携带数据和回调。
工作原理流程:
主线程启动时自动创建Looper并开启循环。
Handler在主线程中创建,默认绑定主线程的Looper和MessageQueue。
子线程通过Handler发送消息(sendMessage())→ 消息入队MessageQueue。
Looper循环从队列中取消息 → 调用dispatchMessage() → 回调到主线程的handleMessage()中执行。
整个过程实现了子线程发消息,主线程处理消息的异步通信。
核心要点:
一个线程可以有多个Handler,但只能有一个Looper。
主线程的Looper在ActivityThread.main()中已自动初始化。
子线程需手动调用Looper.prepare()和Looper.loop()才能使用Handler。
需在onDestroy()中调用removeCallbacksAndMessages(null)避免内存泄漏。
二、安卓四种启动模式
问:Android的四种启动模式分别是什么?各有什么应用场景?
答: 启动模式决定了Activity在任务栈(Task Stack)中的实例化和管理方式,具体如下:
1. standard(标准模式/默认)
行为:每次启动都创建新的实例,放入启动它的任务栈顶。
场景:普通页面,如新闻详情、商品详情,允许用户多次打开不同内容的同一页面。
2. singleTop(栈顶复用模式)
行为:若目标Activity已在栈顶,则复用实例,调用onNewIntent(),不创建新实例;否则创建新实例。
场景:通知栏跳转、搜索输入联想页,避免用户频繁点击生成多个相同页面。
3. singleTask(栈内复用模式)
行为:检查栈中是否已存在目标Activity实例。若存在,则将其上方的所有Activity全部出栈(清空),使其位于栈顶,调用onNewIntent();若不存在则创建新实例。
场景:应用首页、主业务流程入口(如微信"发现"页),确保全局只有一个实例。
4. singleInstance(单实例模式)
行为:目标Activity独享一个独立任务栈,且该栈中只有它自己。再次启动直接复用。
场景:系统级界面,如来电接听、闹钟提醒,需独立于应用主流程之外。
补充:onNewIntent()在singleTop(栈顶复用)和singleTask(栈内复用)复用实例时回调,需在此方法中处理新Intent数据。
三、自定义View,详细解释onDraw
问:自定义View中onDraw的作用是什么?怎么使用?
答: onDraw(Canvas canvas)是自定义View中绘制内容的核心方法,当View需要显示或重绘时被系统调用。开发者在此方法中使用Canvas画布和Paint画笔绘制图形、文本、图片等。
关键概念:
Canvas(画布):提供绘制API,如drawCircle()、drawText()、drawBitmap()、drawPath()等,配合坐标和变换(平移、旋转)实现复杂图形。
Paint(画笔):控制绘制样式,包括颜色(setColor())、粗细(setStrokeWidth())、抗锯齿(setAntiAlias())、填充样式(setStyle())、字体(setTextSize())等。
触发重绘的两种方式:
invalidate():触发onDraw()重新绘制,必须在主线程调用。
postInvalidate():在子线程中调用,最终通过Handler切回主线程触发重绘。
性能优化要点:
避免在onDraw中创建对象:Canvas、Paint、Path等应在构造函数中提前创建并复用,防止频繁GC导致卡顿。
减少过度绘制:通过canvas.clipRect()裁剪不需要绘制的区域。
使用硬件加速(默认开启,可setLayerType()关闭):提升绘制性能。
复杂图形使用onDraw + 缓存:使用setDrawingCacheEnabled()或将绘制结果缓存为Bitmap。
示例:
java
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 绘制一个圆形
Paint paint = new Paint(); // 应提前在构造函数中创建
paint.setColor(Color.RED);
paint.setStyle(Paint.Style.FILL);
canvas.drawCircle(100, 100, 50, paint);
}
四、内存泄漏及优化方法
问:什么是内存泄漏?常见的泄漏场景及优化方法有哪些?
答: 内存泄漏是指不再使用的对象仍被GC Root引用,导致无法被垃圾回收器回收,持续占用堆内存,最终可能引发OOM(OutOfMemoryError)。
常见泄漏场景及解决方案:
泄漏场景 原因 解决方案
非静态内部类持有外部类引用 Handler、Thread、AsyncTask等匿名内部类隐式持有Activity引用 使用静态内部类 + WeakReference弱引用持有Activity
单例持有Context 单例模式长期持有Activity的Context 使用ApplicationContext替代Activity Context
未取消注册监听器 注册了广播、EventBus、LocationListener等未在销毁时反注册 在onDestroy()中调用unregisterReceiver()、EventBus.getDefault().unregister()
静态变量持有关联对象 静态集合或静态View持有了Activity实例 避免静态变量持有Activity,及时置空(= null)
WebView未销毁 WebView持有Activity引用且生命周期未正确释放 独立进程运行WebView,或在onDestroy()中调用webView.destroy()
资源未关闭 数据库Cursor、文件流、网络连接未及时关闭 使用try-catch-finally或Kotlin的use{}自动关闭
优化手段:
LeakCanary:自动检测内存泄漏并输出堆栈,开发阶段必备。
Android Profiler + MAT(Memory Analyzer Tool):分析Heap Dump文件,定位泄漏对象。
使用弱引用:缓存场景使用WeakHashMap或SoftReference。
生命周期管理:在onStop/onDestroy中释放资源、取消监听、停止动画。
五、App启动优化
问:App启动优化的核心思路和具体方法有哪些?
答: App启动分为冷启动(进程未创建)、温启动(进程存在但Activity需重建)和热启动(进程+Activity均在内存)。优化主要针对冷启动,核心目标是减少从点击图标到首帧显示的时间(TTI,Time To First Frame)。
启动流程优化点(按阶段):
阶段 优化手段
Application 初始化 1. 非必要库懒加载/延迟初始化(如推送SDK、图片库)。
2. 耗时操作放入子线程(如数据库预建、日志初始化)。
3. 使用ContentProvider初始化(Google官方方案)替代Application中同步初始化。
Activity 加载 1. 布局层级扁平化(减少嵌套),使用ConstraintLayout。
2. 使用ViewStub实现按需加载(非首屏组件)。
3. 减少主题中的windowBackground导致的白色/黑色闪屏,可用SplashScreen API替代。
数据预加载 1. 在Application中提前异步加载首页核心数据(如用户信息、配置)。
2. 使用缓存(本地数据库/SharedPreferences)减少首次网络请求等待。
3. 使用IdleHandler在CPU空闲时执行非紧急任务。
监控工具:
adb shell am start -W [packageName]/[ActivityName]:测量冷启动耗时(ThisTime/TotalTime)。
Systrace / Perfetto:分析CPU调度、主线程阻塞点。
BlockCanary:检测主线程耗时操作。
核心原则: 首屏只加载最必要的内容,非核心功能(如广告、动态推送)异步或延迟加载。
六、什么情况下造成卡顿,怎么解决
问:Android应用中什么情况会造成卡顿?如何定位和解决?
答: 卡顿的本质是主线程(UI线程)被阻塞或负载过高,导致无法在16ms内完成一帧绘制(60fps要求),造成掉帧。主要场景及解决方案如下:
卡顿原因 具体场景 解决方案
UI布局复杂 布局嵌套层级过深、过多控件 使用ConstraintLayout扁平化布局;使用ViewStub延迟加载非首屏组件;用merge标签减少冗余
主线程执行耗时操作 网络请求、数据库查询、大文件IO、JSON解析 所有耗时操作迁移到子线程(Thread/AsyncTask/RxJava/协程)
频繁GC 在onDraw中创建对象、循环中频繁分配内存 复用对象(提前创建);使用对象池;避免在循环中创建临时变量
过度绘制 重叠的背景、透明层过多 移除不必要的背景色;使用android:background=“@null”;用ViewOverlay替代多层叠加
列表滑动卡顿 RecyclerView中onBindViewHolder做耗时操作、加载大图 使用ViewHolder复用;图片加载使用三级缓存(Glide/Picasso);分页加载
动画卡顿 动画在布局属性(如layout_weight)上频繁触发测量 使用属性动画(PropertyAnimation);用View.setLayerType(View.LAYER_TYPE_HARDWARE)开启硬件加速
内存抖动 高频分配大量临时对象,触发GC 使用SparseArray替代HashMap;使用池化技术(如Message.obtain())
定位工具:
Profile GPU Rendering(开发者选项):直观看到每帧耗时。
Systrace / Perfetto:分析系统级耗时(CPU调度、Binder调用)。
Layout Inspector:查看布局层级和测量耗时。
Android Studio Profiler:监测CPU、Memory、Network实时数据。
解决思路: 定位 → 分析 → 优化 → 验证。核心是保证主线程16ms内完成所有任务,将非UI工作全部移到子线程。
-
JavaScript 和 TypeScript 的区别**
问:JavaScript 和 TypeScript 的区别是什么?答: TypeScript 是 JavaScript 的超集(Superset),核心区别在于 TS 增加了静态类型系统和ES6+ 新特性的提前支持。 类型系统:TS 支持变量、函数、返回值类型声明,可在编译阶段发现类型错误(如 string 赋值给 number),而 JS 是动态类型,错误只能运行时暴露。 编译机制:TS 代码需要编译(Compile)成 JS 才能在浏览器/设备运行,JS 是解释型脚本语言直接运行。 开发体验:TS 提供了更强大的 IDE 智能提示(IntelliSense) 和代码重构能力,适合大型工程化项目;JS 更适合小型脚本或快速原型开发。 鸿蒙关系:鸿蒙 ArkUI 使用的是 ArkTS,是 TypeScript 的超集(在 TS 基础上扩展了声明式 UI 语法),因此 TS 是鸿蒙应用开发的基础语言。
怎么设计一个好的架构
问:怎么设计一个好的架构?
答: 一个好的架构应遵循以下核心原则(SOLID + 分层):
分层清晰(Separation of Concerns):至少划分为 UI 层(View)、业务逻辑层(ViewModel/Presenter)、数据层(Repository/Model)。各层职责单一,互不越界。
单向数据流(Unidirectional Data Flow):界面事件 → 更新状态 → 驱动 UI 刷新(如 MVI 模式),使状态变化可追踪、可预测。
依赖倒置(Dependency Inversion):高层模块(业务逻辑)不应依赖低层模块(数据库、网络),应依赖抽象接口。通过依赖注入(DI)实现解耦,便于单元测试和模块替换。
模块化/组件化:按业务功能(如登录、购物车)而非技术分层(如 utils、network)拆分模块,支持独立编译、独立运行。
可测试性(Testability):业务逻辑应不依赖 Android 框架类,可脱离 UI 层进行纯 JUnit 单元测试(如 ViewModel 的测试)。
数据一致性:统一的数据源管理(Local + Remote),通过 Repository 模式决定优先从缓存还是网络获取,并自动同步。
本质:好的架构不是为了炫技,而是为了应对变化(产品需求变更)、降低 Bug 率、提高团队协作效率。
4. A逐步跳转到D页面(ABCD四个页面),从D直接回到A页面怎么实现(Android和HarmonyOS)
问:A→B→C→D 逐级跳转后,从 D 直接回到 A 页面怎么实现?
答: 核心思路是清空中间页面(B、C),只保留 A 和 D 之间的直达关系。
Android 实现方式:
方式一:FLAG_ACTIVITY_CLEAR_TOP(最常用)
java
Intent intent = new Intent(D.this, A.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
startActivity(intent);
如果 A 已在栈中,则将其之上的 B、C 全部出栈(销毁),A 复用并收到 onNewIntent()。
方式二:FLAG_ACTIVITY_NEW_TASK + CLEAR_TASK
重新创建 A 的新实例并清空整个任务栈,适用于重新登录等场景。
方式三:使用 Navigation Component 的 popBackStack()
通过 NavController.popBackStack(R.id.aFragment, false) 直接回退到 A 页面,并自动清空中间栈。
HarmonyOS 实现方式:
方式一:使用 Router 的 clearStack() 参数
typescript
router.pushUrl({ url: "pages/A" }, router.RouterMode.Single, (err) => {});
// 或在跳转时配合 router.RouterMode.Clear 模式清空中间栈
方式二:使用 Navigation 组件 + NavDestination 的 onPop 拦截
通过 NavPathStack.popToName("A") 直接弹出到指定页面,中间页面自动出栈。
方式三:使用 Ability 的 terminateSelf() + 重新启动 A
在 D 中直接 finish() 销毁自身,然后通过 Intent 重新拉起 A(但不推荐,会闪烁)。
关键点:关键在于利用系统提供的栈清理机制(Android 的 FLAG_ACTIVITY_CLEAR_TOP,鸿蒙的 popToName 或 RouterMode.Clear),避免手动管理中间页面的生命周期。
5. HarmonyOS 的声明式和 Android 的 XML 两种页面布局有什么区别
问:HarmonyOS 的声明式 UI 和 Android 的 XML 布局有什么区别?
答: 本质区别在于UI 构建范式完全不同:
6. 首页为多个动态Tab标签,什么时候获取这些数据最好,性能优化的方法是什么
问:首页多个动态Tab标签,什么时候获取数据最好?性能优化方法?
答:数据获取时机(最佳实践):
首次启动时异步预加载:在 onCreate() 或 onPageShow() 中尽早发起网络请求,不阻塞 UI 渲染。使用协程/Worker/异步任务在后台获取,获取期间展示骨架屏(Skeleton Screen) 或 Loading 状态。
避免在 Tab 切换时才加载:这样会导致每次切换都有延迟,体验差。应设计为一次性获取所有 Tab 的元数据(如 Tab 标题、图标、是否启用),再根据用户点击按需加载具体内容。
使用缓存策略:数据写入本地数据库或 SharedPreferences,下次启动时先展示缓存,再在后台静默更新(Cache-First 策略)。
性能优化方法:
懒加载(LazyLoad):仅当前选中的 Tab 加载完整内容,非可见 Tab 使用 LazyForEach(鸿蒙)或 RecyclerView 的预加载机制(Android),禁止一次性渲染所有 Tab 的内容。
视图复用:多个 Tab 内容结构相似时,使用视图池(ViewPool) 复用已创建的组件,避免频繁创建销毁。
数据分页:如果单个 Tab 内容是列表,采用分页加载(每页 20 条),配合上拉加载更多,减少首屏数据量。
图片优化:使用三级缓存(内存→磁盘→网络) + 缩略图,避免加载大图。
预加载相邻 Tab:在用户滑动或点击时,提前加载相邻 Tab 的前几页数据(利用网络空闲时),提升切换流畅度。
减少重组/重绘:在声明式 UI 中,使用 @ObjectLink 或 remember 避免不必要的状态更新触发全局重绘。
7. Emitter 是否支持跨线程通信,有没有遇到什么问题
问:Emitter 是否支持跨线程通信,有没有遇到什么问题?
答:
支持性:支持。Emitter(事件发射器)是鸿蒙系统中用于跨线程、跨进程(同一应用内) 的事件通信机制,允许主线程和 Worker 线程之间互发事件。
具体使用方式:
typescript
// 主线程发送事件
emitter.emit({
eventId: 1001,
priority: emitter.EventPriority.HIGH
}, { data: { message: "Hello from main" } });
// Worker 线程监听事件
emitter.on({ eventId: 1001 }, (event) => {
console.log("Worker received: " + event.data.message);
});
常见问题与挑战:
数据序列化开销:跨线程传递需要序列化(结构化克隆),传递大数据(如 Bitmap、大 JSON)时会有性能损耗和 GC 压力。建议:传递轻量级数据(如 string、number),大文件通过共享内存或文件传递。
生命周期管理混乱:事件监听器注册后,如果在页面销毁时未及时 off() 取消监听,会导致内存泄漏或"事件发送给已销毁的页面"导致的崩溃。解决方案:在 onPageHide() 或 aboutToDisappear() 中显式解注册。
事件时序性问题:跨线程事件无法保证发送和接收的顺序,可能出现"事件先发后到"的竞态条件。应对:使用事件 ID + 版本号校验,或采用 SharedFlow 替代(鸿蒙未原生支持)。
过度使用导致代码混乱:大量使用 Emitter 会导致事件泛滥,难以追踪数据来源。建议:仅用于跨模块通信(如模块间通知),页面内通信优先使用 @State 或 ViewModel。
8. Compose 的声明式和 HarmonyOS 的声明式有什么区别
问:Jetpack Compose 的声明式 UI 和 HarmonyOS 的声明式 UI 有什么区别?

9. HarmonyOS A跳转到B页面后,需要及时刷新A页面数据,怎么实现
问:HarmonyOS A 页面跳转到 B 页面后,B 页面操作需要及时刷新 A 页面数据,怎么实现?
答: 核心思路是数据共享 + 生命周期回调/事件通知。根据业务场景推荐以下方案:
方案一:全局 ViewModel(推荐)
将 A 和 B 的共享数据存储在 AppStorage(应用级全局状态)或单例 ViewModel 中。
B 页面修改数据后,A 页面通过 @StorageLink 或 @ObjectLink 自动监听到变化并刷新 UI。
适用场景:数据源唯一,多个页面共享相同数据(如用户信息、购物车)。
方案二:EventHub / Emitter 事件通知
B 页面在 onPageHide() 或提交操作时,通过 emitter.emit() 发送"数据已更新"事件。
A 页面在 onPageShow() 中注册监听,收到事件后主动拉取最新数据并刷新 UI。
适用场景:A 和 B 无直接继承关系,需松耦合通信。
方案三:Router 回调机制(鸿蒙特有)
使用 router.pushUrl() 时,通过 router.RouterOptions 的 onComplete 或 params 传递回调函数。
B 页面操作完成后,通过 router.backWithParam() 携带数据返回,A 在 onPageShow() 中接收参数并刷新。
适用场景:简单数据回传(如选择结果、表单提交)。
方案四:通过 Ability 生命周期回调
A 页面重写 onNewWant() 或 onPageShow(),每次从 B 返回时自动触发(前提是 B 执行了 finish() 或返回)。
在此方法中主动查询数据源(如数据库、网络)并更新 UI。
适用场景:数据可能被其他入口修改,需每次都重新获取。
最佳实践建议:优先使用 AppStorage 全局状态(无需手动监听,自动刷新),其次是 EventHub/Emitter(灵活但需注意生命周期管理)。避免在 A 的 onPageShow() 中执行耗时操作(如网络请求),可结合缓存和 Loading 状态优化体验。
更多推荐


所有评论(0)