到顶啦
1、ios面经,页面view生命周期
2、鸿蒙面经
3、flutter面经,KMP面经
4、android 插件化,组件化,热更新
5、android okhttp, retrofit, https, kotlin, rxjava
6、算法
7、性能优化,android,flutter
一、IOS面经
高频考点权重排名
-
**UI 开发(25.4%)**:自定义控件、动画、数据存储
-
**内存管理(19.5%)**:ARC/MRC 机制、weak 引用打破循环
-
**多线程(15.3%)**:GCD 队列类型、@synchronized 原理
-
**网络通信(11.9%)**:HTTP/HTTPS、WebSocket/MQTT
-
**数据结构与算法(10.2%)**:数组操作、树结构
-
**系统设计(9.3%)**:架构思维、性能优化
-
**开发基础(8.5%)**:设计模式、语言特性
必掌握知识点
-
**UI 交互**:CoreAnimation 动画流程、Keychain 安全存储
-
**内存管理**:ARC 机制、循环引用打破
-
**多线程**:GCD 队列类型、锁原理、死锁避免
-
**底层原理**:RunLoop、Runtime、消息转发
-
**性能优化**:卡顿检测、启动优化、内存优化
差异化准备
-
结合业务说明性能优化经验(如图片加载三级缓存)
-
准备跨平台开发方案(Flutter/React Native 性能对比)
-
展示 SwiftUI/Combine 实战项目
-
了解 iOS 26 新特性
1、strong、weak 的区别和使用场景
| 关键字 | 引用计数 | 对象释放后 | 类型要求 | 典型场景 |
|--------|----------|------------|----------|----------|
| **strong** | +1 | 指针成为野指针(危险) | 任意 | 默认引用关系 |
| **weak** | +0 | 自动置为 nil | 必须是可选类型 | 打破循环引用 |
strong
表示"拥有"这个对象。只要存在至少一个 strong 引用指向该对象,该对象就不会被释放。
```swift
class Person {
var name: String
init(name: String) {
self.name = name
print("\(name) 已创建")
}
deinit {
print("\(name) 已销毁")
}
}
// 示例 1:单个 strong 引用
var person1: Person? = Person(name: "张三")
person1 = nil // 引用计数归零,对象销毁
// 输出:张三 已创建
// 张三 已销毁
// 示例 2:多个 strong 引用
var personA: Person? = Person(name: "李四")
var personB: Person? = personA // 引用计数 +1,现在为 2
personA = nil // 引用计数 -1,但仍为 1,对象不销毁
personB = nil // 引用计数归零,对象销毁
// 输出:李四 已创建
// 李四 已销毁
```
weak
表示"不拥有"这个对象。它不会增加对象的引用计数。
案例(循环引用)
```swift
class Person {
var name: String
var friend: Person? // strong 引用
init(name: String) {
self.name = name
print("\(name) 已创建")
}
deinit {
print("\(name) 已销毁")
}
}
// ❌ 错误示范:循环引用导致内存泄漏
func createCycle() {
var xiaoMing: Person? = Person(name: "小明")
var xiaoHong: Person? = Person(name: "小红")
xiaoMing?.friend = xiaoHong // 小明引用小红
xiaoHong?.friend = xiaoMing // 小红引用小明 → 循环引用!
xiaoMing = nil // 无法销毁,引用计数仍为 1
xiaoHong = nil // 无法销毁,引用计数仍为 1
// 输出:两人已创建,但永远不会输出"已销毁"
}
// ✅ 正确示范:使用 weak 打破循环
class PersonFixed {
var name: String
weak var friend: PersonFixed? // 改为 weak
init(name: String) {
self.name = name
print("\(name) 已创建")
}
deinit {
print("\(name) 已销毁")
}
}
func noCycle() {
var ming: PersonFixed? = PersonFixed(name: "小明")
var hong: PersonFixed? = PersonFixed(name: "小红")
ming?.friend = hong // 小明 weak 引用小红
hong?.friend = ming // 小红 weak 引用小明
ming = nil // 正常销毁
hong = nil // 正常销毁
// 输出:两人已创建,随后依次销毁
}
// weak 自动置 nil 的特性
func weakAutoNil() {
var teacher: Person? = Person(name: "王老师")
var student: Person? = Person(name: "学生")
// 假设有个属性是 weak 的
class StudentCard {
weak var owner: Person?
init(owner: Person) {
self.owner = owner
}
}
var card = StudentCard(owner: teacher!)
print("老师还在:\(card.owner != nil ? "是" : "否")") // 是
teacher = nil // 老师对象销毁
print("老师还在:\(card.owner != nil ? "是" : "否")") // 否(自动变 nil)
// card.owner 自动变为 nil,不会成为野指针
}
```
案例(Bloc)
objectivec
@interface ViewController : UIViewController
@property (nonatomic, strong) void (^completionBlock)(void); // strong 引用 Block
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 错误示例:可能导致循环引用
// self -> completionBlock (strong) -> Block -> self (strong, 隐式捕获)
// self 持有 Block,Block 也持有 self,形成循环,两者都无法释放
/*
self.completionBlock = ^{
NSLog(@"%@", self.title); // Block 内部 strong 引用了 self
};
*/
// 正确示例:使用 weak 引用打破循环
__weak typeof(self) weakSelf = self; // 创建 self 的 weak 引用
self.completionBlock = ^{
// 在 Block 内部使用 weakSelf
// 如果 self 在 Block 执行前已被释放,weakSelf 会是 nil,不会崩溃
if (weakSelf) {
NSLog(@"%@", weakSelf.title);
}
};
// self -> completionBlock (strong) -> Block -> weakSelf (weak) -> self
// weakSelf 不增加 self 的引用计数,循环被打破
}
@end
案例(代理)
objectivec
@protocol DataSourceDelegate <NSObject>
- (void)didUpdateData;
@end
@interface DataSource : NSObject
@property (nonatomic, weak) id<DataSourceDelegate> delegate; // 代理通常用 weak
@end
@implementation DataSource
- (void)updateData {
// ... 数据更新逻辑 ...
[self.delegate didUpdateData]; // 调用代理方法
}
@end
@interface ViewController : UIViewController <DataSourceDelegate>
@property (nonatomic, strong) DataSource *dataSource; // ViewController strong 引用 DataSource
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.dataSource = [[DataSource alloc] init];
self.dataSource.delegate = self; // DataSource weak 引用 ViewController
}
- (void)didUpdateData {
// 处理数据更新
}
@end
assign
表示简单的赋值操作,不涉及引用计数的变化
对于基本数据类型 (int, float, BOOL, NSInteger, CGRect, struct 等): 这是默认且唯一正确的选择。它直接进行值的拷贝
copy
创建一个新的副本并持有这个副本的 strong 引用
确保属性的值不会被外部修改。当外部对象改变时,属性持有的副本不受影响
2、ARC,MRC
ARC(Automatic Reference Counting,自动引用计数)** 是 Swift 的内存管理机制,在**编译期**自动插入内存管理代码,跟踪每个对象的引用计数,当引用计数降为 0 时自动释放对象
MRC(Manual Reference Counting,手动引用计数)时代,开发者需要手动调用 `retain`、`release`、`autorelease`
循环引用
```swift
// ===== 错误示范:循环引用 =====
print("\n=== 错误示范:循环引用 ===")
class Tenant {
let name: String
var apartment: Apartment? // ❌ strong 引用
init(name: String) {
self.name = name
print("🟢 [租户:\(name)] 被创建")
}
deinit {
print("🔴 [租户:\(name)] 被销毁")
}
}
class Apartment {
let number: String
var tenant: Tenant? // ❌ strong 引用
init(number: String) {
self.number = number
print("🟢 [公寓:\(number)] 被创建")
}
deinit {
print("🔴 [公寓:\(number)] 被销毁")
}
}
var zhangSan: Tenant? = Tenant(name: "张三")
var room101: Apartment? = Apartment(number: "101")
// 建立互相引用
zhangSan?.apartment = room101
room101?.tenant = zhangSan
// 尝试释放
zhangSan = nil
room101 = nil
// ❌ 问题:双方引用计数都不为 0,都无法释放!
// 输出:
// 🟢 [租户:张三] 被创建
// 🟢 [公寓:101] 被创建
// (没有销毁日志 → 内存泄漏!)
```
解决循环引用方案1:weak弱引用
```swift
// ===== 正确示范:使用 weak 打破循环 =====
print("\n=== 正确示范:weak 弱引用 ===")
class TenantWeak {
let name: String
var apartment: ApartmentWeak? // ✅ strong 引用
init(name: String) {
self.name = name
print("🟢 [租户:\(name)] 被创建")
}
deinit {
print("🔴 [租户:\(name)] 被销毁")
}
}
class ApartmentWeak {
let number: String
weak var tenant: TenantWeak? // ✅ weak 引用(不增加引用计数)
init(number: String) {
self.number = number
print("🟢 [公寓:\(number)] 被创建")
}
deinit {
print("🔴 [公寓:\(number)] 被销毁")
}
}
var zhangSanW: TenantWeak? = TenantWeak(name: "张三")
var room101W: ApartmentWeak? = ApartmentWeak(number: "101")
zhangSanW?.apartment = room101W
room101W?.tenant = zhangSanW
// 释放外部引用
zhangSanW = nil
// 租户引用计数 = 0 → 销毁 → apartment 属性释放 → 公寓引用计数 -1
// 公寓引用计数 = 0 → 销毁
room101W = nil
// 已经销毁了,无操作
// ✅ 输出:
// 🟢 [租户:张三] 被创建
// 🟢 [公寓:101] 被创建
// 🔴 [租户:张三] 被销毁
// 🔴 [公寓:101] 被销毁
```
3、dispatch_once 实现原理
dispatch_once 使用原子操作和标记位保证代码块只执行一次:
-
内部使用 `dispatch_once_token_t` 标记
-
通过原子比较交换(CAS)确保线程安全
-
第一次执行后设置标记,后续调用直接跳过
objectivec
dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 这里的代码只会执行一次,无论被多少个线程调用
});
经典场景
单例模式
objectivec
@interface SingletonManager : NSObject
@property (nonatomic, strong) NSString *data;
+ (instancetype)sharedInstance;
@end
@implementation SingletonManager
+ (instancetype)sharedInstance {
static SingletonManager *instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[SingletonManager alloc] init];
// 可以在这里进行复杂的初始化逻辑
instance.data = @"Initial Data";
});
return instance;
}
@end
一次性配置初始化
objectivec
- (void)setupConfiguration {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 注册通知观察者
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleNotification:)
name:@"ConfigUpdated"
object:nil];
// 加载配置文件
[self loadDefaultSettings];
// 初始化第三方 SDK
[ThirdPartySDK setupWithKey:@"YOUR_API_KEY"];
NSLog(@"配置初始化完成");
});
}
static 关键字必须使用
不要重置 onceToken
避免在 block 中使用 self 导致循环引用
4、SwiftUI 和 Combine 分别是什么
| 框架 | 定位 | 作用 | 关系 |
|------|------|------|------|
| **SwiftUI** | 声明式 UI 框架 | 构建用户界面 | View = f(State) |
| **Combine** | 响应式编程框架 | 处理数据流和事件 | 为 SwiftUI 提供数据支持 |
**核心思想:** 由状态驱动 UI,数据变化自动触发视图更新
swift - 常用属性包装器
```swift
import SwiftUI
class User: ObservableObject {
@Published var name: String
@Published var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
struct DetailView: View {
// @State: 视图本地状态
@State private var isShowing = false
// @Binding: 双向数据绑定
@Binding var isActive: Bool
// @ObservedObject: 观察外部对象
@ObservedObject var user: User
// @EnvironmentObject: 环境对象(全局共享)
@EnvironmentObject var settings: AppSettings
// @StateObject: iOS 14+, 拥有对象生命周期
@StateObject private var viewModel = DetailViewModel()
var body: some View {
Text(user.name)
}
}
```
Combine
- **Publisher**:发布者,产生数据流
- **Subscriber**:订阅者,接收数据
- **Operator**:操作符,转换和处理数据流
- **Subscription**:订阅关系
5、SwiftUI 状态管理详解
@State 管小我,值类型内部用;
@Binding 传父子,双向绑定$符号;
@StateObject 自己创,视图独享它来管;
@ObservedObject 外来客,共享引用不创造;
@EnvironmentObject 全局注,跨层访问真方便。
class CounterModel: ObservableObject {
@Published var count: Int = 0
func increment() {
count += 1
}
}
struct StateObjectExample: View {
// ✅ 视图内部创建并管理对象
@StateObject private var counter = CounterModel()
var body: some View {
VStack {
Text("计数:\(counter.count)")
Button("增加") {
counter.increment()
}
}
}
}
6、HTTPS通信过程
HTTP(明文协议):
Client → Server: GET /index.html HTTP/1.1
Server → Client: HTTP/1.1 200 OK + 数据
✅ 直接通信,无握手
HTTPS(加密协议):
Client ←─── TCP 三次握手 ───→ Server (建立连接)
Client ←─── TLS 握手 ───────→ Server (协商加密参数)
Client ←─── HTTP 通信 ──────→ Server (加密数据传输)
|---------|---------------------------------------------------------------------------------|
| TCP三次握手 |
|
| TSL1.3 |
|
7、页面生命周期
oc

SwiftUI

8、页面切换

Push / Pop(导航栈层级导航)
Swift
// 压入新页面
navigationController?.pushViewController(detailVC, animated: true)
// 弹出当前页面
navigationController?.popViewController(animated: true)
- 适用于层级递进的导航结构
- 页面保存在导航栈中,可逐层返回
- 动画:新页面从右侧滑入,旧页面同时向左滑出。
- 导航栏:顶部自动出现 NavigationBar,左侧有
< 上一页按钮。 - 手势:支持屏幕边缘右滑返回(iOS 原生手势)。
Swift
Push B: [A] -> [A, B]
Push C: [A, B] -> [A, B, C]
Pop : [A, B, C] -> [A, B] (C 被销毁)
Present / Dismiss(模态临时页面)
Swift
// 呈现模态页面
present(alertVC, animated: true)
// 关闭模态页面
dismiss(animated: true)
- 适用于临时操作(如弹窗、表单、登录页)
- 可指定
.fullScreen、.pageSheet等呈现样式
- 可指定
- 动画:新页面从底部向上覆盖 (Cover Vertical),或者淡入 (Fade)。旧页面原地不动,被盖住。
- 导航栏:默认没有顶部栏。如果需要返回按钮,必须自己画一个,或者把 Present 的页面包在一个新的 NavigationController 里。
- 手势:默认支持从顶部向下滑动关闭 (iOS 13+)。
Swift
Present B: A 覆盖上 B (A 还在,但不可见)
Present C: B 覆盖上 C
Dismiss : C 移除,露出 B (注意:不是露出 A,除非 B 也 Dismiss)
9、GCD(Grand Central Dispatch)并发编程
GCD(Grand Central Dispatch)是 iOS/macOS 系统的核心并发编程框架,类似于 Java 的 ExecutorService + Handler 机制,但设计哲学更简洁高效
1. 什么是 GCD?
GCD 是 Apple 提供的底层 C 语言 API,用于管理任务队列和线程调度。开发者只需关注"要执行什么任务",无需手动管理线程生命周期(由系统自动优化)。
核心思想:将任务(Block)提交到队列(Dispatch Queue),由系统决定何时何线程执行。
2. 两大核心组件
(1)任务(Task / Block)
Swift
// Objective-C
dispatch_block_t task = ^{
NSLog(@"子线程执行任务");
};
// Swift
let task = {
print("子线程执行任务")
}
- 本质是一个闭包(Block),包含要执行的代码。
- 类似 Java 的
Runnable或Callable。
(2)队列(Dispatch Queue)
队列负责存储任务,并按特定规则调度执行。GCD 提供两种队列类型:

3.iOS 如何开启子线程
使用全局并发队列(最常用)
Swift
// Objective-C
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 子线程执行耗时操作
NSData *data = [NSData dataWithContentsOfURL:url];
// 回到主线程更新 UI
dispatch_async(dispatch_get_main_queue(), ^{
self.imageView.image = [UIImage imageWithData:data];
});
});
Swift
// Swift
DispatchQueue.global(qos: .default).async {
// 子线程执行耗时操作
let data = try? Data(contentsOf: url)
// 回到主线程更新 UI
DispatchQueue.main.async {
self.imageView.image = UIImage(data: data!)
}
}
Android 类比:
Swift
// Java - 线程池执行
Executors.newCachedThreadPool().execute(() -> {
// 子线程
runOnUiThread(() -> {
// 主线程更新 UI
});
});
自定义串行队列(隔离任务)
Swift
// 创建私有串行队列
let serialQueue = DispatchQueue(label: "com.example.serialQueue")
serialQueue.async {
// 此队列中的任务按顺序执行,不会与其他队列干扰
// 适合处理有序的数据写入、数据库操作等
}
延时执行
Swift
// 2 秒后执行
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
print("延迟任务")
}
4.线程池
全局并发队列 = 线程池
dispatch_get_global_queue() 返回的是系统维护的全局并发队列,底层由线程池支持。
- 自动伸缩:系统根据 CPU 核心数和负载动态调整线程数量。
- 优先级控制:通过
qos(Quality of Service)区分任务优先级。 - 无需手动管理:开发者不需要像 Java 那样配置
corePoolSize、maxPoolSize。
5.子线程能否更新 UI
绝对不能!
与 Android 一样,iOS 也禁止在子线程中更新 UI。违反此规则会导致:
- App 崩溃(抛出异常)
- UI 渲染异常(画面撕裂、卡顿)
- 线程安全问题(数据竞争)
Swift
// ✅ 正确:子线程处理数据,主线程更新 UI
DispatchQueue.global().async {
let data = fetchData() // 耗时操作
DispatchQueue.main.async {
self.label.text = data // 安全更新
}
}

二、鸿蒙面经
**三大核心重灾区**:
- **状态管理**(占比约 30%)
-
@State、@Prop、@Link 区别
-
跨组件通信方案
-
深层观测问题
- **ArkUI 渲染**(占比约 25%)
-
组件生命周期
-
ForEach vs LazyForEach
-
渲染流程原理
- **Stage 模型**(占比约 20%)
-
UIAbility 生命周期
-
应用启动过程
-
Context 关系
1、为什么 ArkTS 禁止使用 `any` 类型
为了支持 AOT 预编译。确定类型后,编译器可以直接生成机器码,无需运行时进行类型推导,大幅提升启动和运行速度
2、ArkTs V1 V2

@Builder 与 @BuilderParam
**作用**:
-
`@Builder`:用于提取轻量级 UI 复用逻辑
-
`@BuilderParam`:类似 Vue 的 slot 或 React 的 render props,允许父组件向子组件传递一段 UI 结构
3、组件生命周期
**自定义组件生命周期**:
| 方法 | 触发时机 | 典型用途 |
|------|----------|----------|
| aboutToAppear | 组件实例创建完成,build 前执行 | 初始化状态变量、发起网络请求(只执行一次) |
| build | 定义 UI 结构 | 不能写 console.log、局部变量 |
| onDidBuild | build 函数执行完毕 | 做一些不影响布局的后续操作 |
| aboutToDisappear | 组件即将销毁 | 销毁定时器、取消订阅、释放资源 |
**@Entry 页面级组件额外生命周期**:
| 方法 | 触发时机 | 典型用途 |
|------|----------|----------|
| onPageShow | 页面可见时(切前台、路由入栈) | 刷新数据、恢复动画 |
| onPageHide | 页面不可见时(切后台、路由出栈) | 暂停动画、释放资源 |
| onBackPress | 物理返回键触发 | 自定义返回逻辑、弹窗确认 |
**高频考点**:`aboutToAppear` 和 `onPageShow` 的区别?
> **答案**:
> - `aboutToAppear` 是组件级的,在创建后、build 前执行,只执行一次
> - `onPageShow` 是页面级的(@Entry),每次页面进入前台都会执行(包括切回前台、路由返回)

4、常见布局技巧
#### Blank 组件
**作用**:在 Row 或 Column 中自动填充剩余空间,常用于"左边文字 - 右边图标"的布局
#### 获取组件宽高
**正确方式**:使用 `onAreaChange` 回调
```typescript
Text('Hello')
.onAreaChange((oldValue: Area, newValue: Area) => {
console.log(`宽度:{newValue.width}, 高度:{newValue.height}`)
})
```
5、@State、@Prop、@Link

@Prop ------ 单向父子传参
// 子组件
@Component
struct UserInfoCard {
@Prop name: string
@Prop age: number
@Prop tags: string[]
@Prop config: { color: string, fontSize: number }
build() {
Column() {
Text(`${this.name} - ${this.age}岁`)
.fontSize(this.config.fontSize)
.fontColor(this.config.color)
Row() {
ForEach(this.tags, (tag: string) => {
Text(tag)
.padding(5)
.backgroundColor('#e0e0e0')
})
}
// ✅ 可以修改复杂对象的内部属性
Button('修改颜色')
.onClick(() => {
this.config.color = 'red' // 子组件本地生效
// 但父组件的 config 不会同步变化
})
// ❌ 禁止修改简单类型
// this.name = 'new name' // 编译错误
}
}
}
// 父组件
@Entry
@Component
struct ParentPage {
@State userName: string = '张三'
@State userAge: number = 25
@State userTags: string[] = ['工程师', '武汉']
@State cardConfig: { color: string, fontSize: number } = {
color: 'blue',
fontSize: 18
}
build() {
Column() {
UserInfoCard({
name: this.userName,
age: this.userAge,
tags: this.userTags,
config: this.cardConfig
})
Button('修改父组件数据')
.onClick(() => {
this.userName = '李四' // 子组件会同步更新
this.cardConfig.color = 'green' // 子组件会同步更新
})
}
}
}
@Link ------ 双向绑定
// 子组件
@Component
struct CounterChild {
@Link count: number
build() {
Column() {
Text(`计数:${this.count}`)
.fontSize(24)
Button('加 1')
.onClick(() => this.count++) // ✅ 修改会同步到父组件
Button('减 1')
.onClick(() => this.count--) // ✅ 修改会同步到父组件
}
}
}
// 父组件
@Entry
@Component
struct ParentPage {
@State parentCount: number = 0
@State displayMessage: string = ''
build() {
Column() {
Text(`父组件计数:${this.parentCount}`)
.fontSize(20)
.fontColor('blue')
CounterChild({
count: $parentCount // $ 符号表示双向绑定
})
Button('父组件直接修改')
.onClick(() => {
this.parentCount += 5
this.displayMessage = `当前计数:${this.parentCount}`
})
Text(this.displayMessage)
}
}
}
6、@Provide 与 @Consume
@Provide:在祖先组件"广播"数据
@Consume:在后代组件"接收"广播
就近原则:
如果一个组件树下有多个同名的 @Provide(例如祖父和父亲都提供了 themeColor),@Consume 只会绑定到最近的一个祖先组件提供的变量上。
双向绑定:
@Provide 和 @Consume 之间是双向绑定的。
父组件改值 -> 子组件刷新。
子组件改值 -> 父组件刷新(以及其他兄弟组件刷新)。
性能优化:
虽然方便,但不要滥用。如果在深层组件树中频繁修改高频状态(如动画帧、滚动位置),可能会导致大范围的组件重绘,影响性能。对于仅需要单向传递且不需要响应式更新的简单数据,直接通过构造函数参数传递可能更高效。
生命周期:
当提供数据的组件(@Provide 所在组件)销毁时,如果它是数据的唯一来源,消费者将失去数据源(通常会回退到 @Consume 定义的默认值)。
与 @State 的区别:
@State 仅作用于当前组件内部的状态驱动。
@Provide / @Consume 作用于组件树层级之间的状态共享。
通常 @Provide 修饰的变量内部也会结合 @State 使用(或者它本身就是状态),但在 ArkTS 最新规范中,@Provide 本身已具备状态触发能力,无需同时加 @State(具体视 HarmonyOS API 版本而定,API 9+ 推荐单独使用 @Provide)。
7、AppStorage vs LocalStorage
AppStorage
AppStorage 是应用进程内的全局单例存储,整个应用程序的所有组件、所有 Ability 都可以访问其中的数据。它类似于一个全局的"状态总线",适合存储跨页面、跨组件共享的全局状态。
关键特点:
作用域:整个应用生命周期内有效
唯一性:单例模式,所有地方访问的是同一份数据
持久化:数据存储在内存中,应用退出后丢失(如需持久化需配合 PersistentStorage)
线程安全:主线程访问,不支持多线程并发修改
// 1. 初始化(通常在 EntryAbility 中)
@Entry
@Component
struct Index {
aboutToAppear() {
// 初始化全局变量
AppStorage.SetOrCreate('userId', '12345');
AppStorage.SetOrCreate('isLoggedIn', false);
AppStorage.SetOrCreate('theme', 'light');
}
build() {
Column() {
Text('应用首页')
}
}
}
// 2. 在任何组件中读取
@Component
struct ProfileComponent {
// 使用 @StorageLink 双向绑定
@StorageLink('userId') userId: string = '';
// 使用 @StorageProp 单向读取
@StorageProp('isLoggedIn') isLoggedIn: boolean = false;
build() {
Column() {
Text(`用户 ID: ${this.userId}`)
Text(`登录状态:${this.isLoggedIn ? '已登录' : '未登录'}`)
Button('退出登录')
.onClick(() => {
// 3. 修改全局状态
AppStorage.Set('isLoggedIn', false);
// 或使用 @StorageLink 直接修改
this.isLoggedIn = false;
})
}
}
}
// 3. 在另一个页面组件中访问
@Component
struct SettingsPage {
@StorageLink('theme') theme: string = 'light';
build() {
Column() {
Text(`当前主题:${this.theme}`)
Button('切换深色模式')
.onClick(() => {
this.theme = this.theme === 'light' ? 'dark' : 'light';
// 所有使用 theme 的组件都会自动刷新
})
}
}
}
LocalStorage
LocalStorage 是页面或组件树级别的局部存储,通常用于单个 Ability 或特定组件树内部的状态管理。每个 LocalStorage 实例是独立的,不同实例之间的数据不共享。
关键特点:
作用域:限定在创建它的组件树或 Ability 内
隔离性:多个实例互不干扰,适合多窗口、多标签页场景
灵活性:可以动态创建和销毁
性能:相比 AppStorage 更轻量,适合局部高频更新
// 1. 创建 LocalStorage 实例(通常在 Ability 或根组件中)
@Entry
@Component
struct HomePage {
// 创建页面级存储
private storage: LocalStorage = new LocalStorage();
aboutToAppear() {
// 初始化局部变量
this.storage.SetOrCreate('pageCounter', 0);
this.storage.SetOrCreate('selectedTab', 'home');
}
build() {
Column() {
// 将 storage 传递给子组件
TabContent() {
TabContent() {
HomeTab()
}
TabContent() {
ProfileTab()
}
}
}
}
}
// 2. 子组件中使用 LocalStorage
@Component
struct HomeTab {
// 通过 @LocalStorageLink 双向绑定
@LocalStorageLink('pageCounter') counter: number = 0;
// 通过 @LocalStorageProp 单向读取
@LocalStorageProp('selectedTab') selectedTab: string = '';
build() {
Column() {
Text(`点击次数:${this.counter}`)
Button('增加计数')
.onClick(() => {
this.counter++; // 直接修改,自动同步到 LocalStorage
})
Text(`当前标签:${this.selectedTab}`)
}
}
}
// 3. 在另一个独立组件树中使用不同的 LocalStorage
@Component
struct IndependentModule {
private localStore: LocalStorage = new LocalStorage();
aboutToAppear() {
this.localStore.SetOrCreate('moduleData', '初始值');
}
build() {
Column() {
ModuleChild({ storage: this.localStore })
}
}
}
@Component
struct ModuleChild {
@LocalStorageLink('moduleData') data: string = '';
build() {
Text(`模块数据:${this.data}`)
}
}
8、数组/对象深层观测问题
**常见问题**:数组中的对象属性改变了,UI 没刷新
**原因分析**:
-
`@State` 只能观察到数组本身的增删(push/pop)
-
`@State` 只能观察对象第一层属性的变化
-
嵌套对象的深层属性变化无法自动触发 UI 刷新
**解决方案(V1)**:使用 `@Observed` + `@ObjectLink`
java
@Observed
class UserV1 {
public name: string;
public age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
java
@Entry
@Component
struct ParentComponentV1 {
@State user: UserV1 = new UserV1('张三', 25);
build() {
Column({ space: 10 }) {
Text(`父组件显示: ${this.user.name}`)
.fontSize(20)
.fontWeight(FontWeight.Bold)
// 传递对象给子组件
UserItemV1({ user: this.user })
}
.padding(20)
}
}
java
@Component
struct UserItemV1 {
// @ObjectLink 接收父组件传递的 @State/@Prop 对象
@ObjectLink user: UserV1;
build() {
Row() {
Text(`姓名: ${this.user.name}`)
.fontSize(16)
Button('修改姓名 (V1)')
.onClick(() => {
// 直接修改属性,UI 会自动刷新
this.user.name = `NewName_${Math.random().toFixed(2)}`;
})
}
.padding(10)
}
}
**解决方案(V2 推荐)**:使用 `@ObservedV2` + `@Trace`
java
@ObservedV2
class UserV2 {
// 只有标记了 @Trace 的属性变化才会触发 UI 刷新
@Trace name: string;
// 如果没有标记 @Trace,修改此属性不会触发 UI 更新(适合纯数据字段)
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
java
@Entry
@Component
struct ParentComponentV2 {
// 使用 @Local 或 @State 均可,建议在新项目中统一使用 @Local 管理内部状态
@Local user: UserV2 = new UserV2('李四', 30);
build() {
Column({ space: 10 }) {
Text(`父组件显示: ${this.user.name} (年龄: ${this.user.age})`)
.fontSize(20)
.fontWeight(FontWeight.Bold)
UserItemV2({ user: this.user })
Button('父组件修改年龄')
.onClick(() => {
this.user.age += 1;
// 因为 age 没有 @Trace,所以这里的修改不会触发任何组件的 UI 刷新
// 但数据确实变了,可以通过日志验证
})
}
.padding(20)
}
}
java
@Component
struct UserItemV2 {
// 使用 @Param 接收父组件传递的对象
// 在 V2 中,只要对象是 @ObservedV2 且属性有 @Trace,@Param 也能实现双向同步效果
@Param user: UserV2;
build() {
Row() {
Text(`姓名: ${this.user.name}`)
.fontSize(16)
Button('修改姓名 (V2)')
.onClick(() => {
// 直接修改带 @Trace 的属性,UI 自动刷新
this.user.name = `V2_New_${Math.random().toFixed(2)}`;
// 尝试修改未标记 @Trace 的属性,UI 不会刷新
this.user.age += 1;
console.info(`Age changed to ${this.user.age}, but UI won't update`);
})
}
.padding(10)
}
}
9、@Watch 装饰器
**作用**:监听状态变量的变化,并在变化时执行特定的逻辑函数
**典型场景**:
-
数据变化后自动请求网络
-
状态变化后执行副作用操作
-
表单验证
10、UIAbility 间数据传递
-
**startAbility 接口**(优先推荐):通过 `wantInfo` 添加启动参数
-
**应用级状态管理**:AppStorage、LocalStorage、PersistentStorage
-
**Emitter 和 Worker**:线程间通信
-
**CES(公共事件服务)**:进程间通信
-
**Call 调用**:UIAbility 交互

以下是一个完整的电商应用场景,整合了上述五种机制
// 1. 主入口:启动应用并初始化全局状态
@Entry
@Component
struct AppEntry {
private worker: Worker = new Worker('workers/dataSyncWorker.ts');
aboutToAppear() {
// 初始化持久化状态
const persistStorage = new PersistentStorage();
persistStorage.PersistPersist('cartCount');
persistStorage.PersistPersist('userToken');
// 从本地读取历史数据
const savedToken = AppStorage.Get('userToken') || '';
AppStorage.SetOrCreate('isLoggedIn', !!savedToken);
// Worker 后台同步购物车数据
this.worker.postMessage({ action: 'syncCart', token: savedToken });
this.worker.onmessage = (event) => {
AppStorage.Set('cartCount', event.data.count);
};
}
}
// 2. 商品列表页:启动详情页并传递参数
@Component
struct ProductList {
@StorageProp('cartCount') cartCount: number = 0;
build() {
Column() {
ForEach(productList, (item) => {
ProductItem({
product: item,
onClick: () => {
// 使用 startAbility 启动详情页
const wantInfo = {
bundleName: 'com.example.shop',
abilityName: 'DetailAbility',
parameters: { productId: item.id }
};
getContext(this).startAbility(wantInfo);
}
})
})
// 显示购物车数量(全局状态)
Badge({ value: this.cartCount }) {
Button('购物车')
}
}
}
}
// 3. 详情页:接收参数并处理
export default class DetailAbility extends UIAbility {
onCreate(want: Want) {
const productId = want.parameters?.['productId'];
AppStorage.Set('currentProductId', productId);
}
}
// 4. 订单页:使用 Call 调用库存服务
async function checkStock(productId: string) {
const want = {
bundleName: 'com.example.shop',
abilityName: 'StockServiceAbility'
};
const abilityController = getContext(this).getApplicationContext().getAbilityController();
abilityController.call(want, 'checkInventory', [productId], (err, result) => {
if (!err) {
console.info(`库存数量:${result.stock}`);
}
});
}
// 5. 监听系统网络事件
import commonEvent from '@ohos.commonEvent';
commonEvent.subscribe(commonEvent.CommonEventStatus.NETWORK_STATE, (err, data) => {
if (!err && data.data.networkState === 'DISCONNECTED') {
// 网络断开,提示用户
AppStorage.Set('networkAvailable', false);
}
});
11、并发编程
| 模式 | 适用场景 | 特点 | 典型用途 |
|------|----------|------|----------|
| TaskPool | CPU 密集型任务 | 并行执行,自动调度 | 图像处理、数据计算 |
| Worker | 长时间运行的后台任务 | 独立线程,持续运行 | 下载、轮询、实时数据处理 |
| Actor 模型 | 基于消息传递的场景 | 异步消息通信,无锁 | 事件驱动架构 |
12、网络请求
-
**http 模块**:基础 HTTP 请求
-
**Fetch**:现代 API,支持 Promise
**权限配置**:需在 `module.json5` 中声明 `ohos.permission.INTERNET`
13、数据存储方案
| 存储类型 | 数据结构 | 适用场景 | 容量限制 | API 模块 |
|----------|----------|----------|----------|----------|
| Preferences | 键值对 | 轻量级配置、用户偏好 | 小数据量 | `@ohos.data.preferences` |
| RelationalStore | 关系型(SQLite) | 结构化数据、复杂查询 | 较大数据量 | `@ohos.data.relationalStore` |
| DataShare | 跨应用共享 | 多应用数据共享 | 中等数据量 | `@ohos.data.dataShare` |
| File IO | 文件 | 大文件、二进制数据 | 无明确限制 | `@ohos.fileio` |
14、分布式能力
### 核心特性
-
**分布式软总线**:设备发现、连接、通信
-
**分布式数据管理**:多设备数据同步
-
**分布式任务调度**:跨设备任务迁移
### 常用场景及 API
| 场景 | API | 说明 |
|------|-----|------|
| 分布式数据共享 | `dataShareManager.createDataShareHelper` | 多设备共享同一份数据 |
| 任务迁移 | `abilityManager.startAbilityForResult` | 将应用从一台设备迁移到另一台设备 |
| 设备管理 | `deviceManager.getDeviceList` | 获取当前超级终端中的所有设备信息 |
8.1 重点倾斜方向
-
**ArkTS V2 特性**:@Local/@Param/@Event、@Computed、深层观测
-
**HarmonyOS NEXT 新能力**:纯原生开发,移除 Android 兼容层
-
**跨设备开发**:分布式能力、多端适配
-
**性能优化**:LazyForEach、渲染流程、内存管理
8.2 备考建议
-
**避开过时知识点**:LiteOS、旧版 JS/Java 开发模式
-
**重点掌握 V2 版本核心差异与实战应用**
-
**准备实际项目案例**:分布式场景、性能优化实践
-
**理解底层原理**:渲染三棵树、AOT 编译、状态管理机制
三、Flutter面经
flutter通过自绘引擎(Skia)直接操作 GPU 渲染像素,不依赖原生平台的 UI 组件
1、flutter缺点
-
**包体积较大**:需要打包 Skia 引擎和 Dart 运行时,初始安装包比原生应用大
-
**平台特定功能受限**:依赖 Platform Channel 与原生通信,部分深度平台集成需额外开发
-
**生态系统相对年轻**:相比原生开发,第三方库和社区资源仍在发展中
-
**Dart 语言学习成本**:开发者需额外学习 Dart 语言及其异步编程模型
2、Flutter 中的 Widget
-
**StatelessWidget**:无状态组件,创建后不会改变,仅依赖父节点传递的配置信息(如 Text、Row、Column、Container)
-
**StatefulWidget**:有状态组件,持有可变的 State 对象,在生命周期内数据可能发生变化(如复选框、按钮)
注:当组件不需要响应变化时,优先使用 StatelessWidget 避免多余的状态管理开销
3、StatefulWidget 的生命周期
-
**`initState()`**:State 初始化调用,此时不能访问 Context(可通过 Future.delayed() 间接获取)
-
**`didChangeDependencies()`**:在 initState() 后立即调用,当 State 对象的依赖关系变化时也会触发
-
**`build()`**:构建 UI 界面,每次状态更新都会重新调用
-
**`didUpdateWidget()`**:当父组件传递的新配置导致 Widget 更新时调用
-
**`deactivate()`**:当 State 暂时从视图树移除时调用(类似 Android 的 onPause)
-
**`dispose()`**:Widget 销毁时调用,用于清理定时器、流订阅、动画控制器等资源
4、Flutter 采用三层架构设计
| 层级 | 核心作用 | 可变性 | 生命周期 | 示例 |
|------|---------|--------|---------|------|
| **Widget** | 描述 UI 结构和配置(不可变蓝图) | 不可变 | 频繁创建/销毁 | Text、Container、StatefulWidget |
| **Element** | Widget 的实例化对象(UI 树骨架) | 可变 | 与 UI 树节点一一对应,生命周期长 | StatelessElement、StatefulElement |
| **RenderObject** | 承载布局、绘制、触摸检测逻辑 | 可变 | 仅在布局/绘制需求变化时更新 | RenderBox、RenderFlex |
**创建流程:**
-
调用 `runApp(Widget)` 创建根 Element
-
Element 调用 `widget.createElement()` 生成子 Element,形成 Element 树
-
Element 调用 `createRenderObject()` 生成 RenderObject,形成 RenderObject 树
-
Widget 重建时,Element 对比新旧 Widget 的 key 和 runtimeType 决定是否复用
5、Flutter 渲染管线的核心流程是什么
-
**布局(Layout)**:Widget 树通过 `performLayout()` 计算每个节点的位置和尺寸(基于 Constraints 约束传递),生成 RenderObject 树
-
**绘制(Painting)**:RenderObject 调用 `paint()` 方法生成 Layer 树(分层绘制结构,如 ClipLayer、TransformLayer)
-
**合成(Compositing)**:Engine 将 Layer 树转换为 GPU 可识别的纹理(Texture),确定 Layer 的合成顺序(避免过度绘制)
-
**提交(Commit)**:将合成指令和纹理信息提交给 Skia 引擎
-
**GPU 渲染**:Skia 调用 OpenGL/Metal/Vulkan 接口,将纹理渲染到屏幕帧缓冲区
-
**显示(Display)**:系统通过 VSync(垂直同步)信号,将帧缓冲区内容显示到屏幕
6、什么是帧丢失(Jank)?如何避免
当 UI 线程生成一帧的时间超过 16.67ms(对应 60fps),或 GPU 线程处理一帧的时间超过 16.67ms,就会导致当前帧无法赶上 VSync 信号,屏幕只能显示上一帧,出现卡顿
7、Flutter 引擎的 UI 线程与 GPU 线程如何协作
Flutter 引擎默认有 4 个核心线程,其中 UI 线程和 GPU 线程是渲染的核心:
| 线程名称 | 别名/俗称 | 核心职责 | 通信方式 | 典型任务 |
|---------|----------|---------|---------|---------|
| **Platform Thread** | UI Thread / Main Thread | Dart 代码执行、UI 构建 | Event Loop、Task Runner | Widget 构建、Layout 计算、Layer 树生成、事件处理 |
| **GPU Thread** | Raster Thread / Render Thread | 渲染管线、Skia 绘制 | Task Runner、VSync 信号 | 光栅化、纹理管理、GPU 命令提交、合成图层 |
| **IO Thread** | I/O Worker Thread | 异步I/O操作 | Task Runner、Future | 图片解码、文件读写、网络请求、数据序列化 |
| **Platform-Specific Threads** | Native Threads | 平台原生 API 调用 | Platform Channel | 原生插件调用、系统 API 调用、平台事件处理 |
-
**UI 线程(Dart 线程)**:执行 Dart 代码(Widget 构建、状态更新、布局计算),生成 Layer 树,将其打包为"UI 帧任务"发送给 GPU 线程
-
**GPU 线程(C++ 线程)**:接收 UI 线程的 Layer 树,调用 Skia 转换为 GPU 指令,最终提交给硬件渲染
-
**协作规则**:UI 线程和 GPU 线程通过"管道(Pipeline)"异步通信,UI 线程生成帧后,需等待 GPU 线程完成上一帧渲染,避免"帧堆积"
8、flutter引擎三种channel
| 特性维度 | MethodChannel | EventChannel | BasicMessageChannel |
|---------|--------------|--------------|---------------------|
| **通信模式** | 请求 - 响应 | 订阅 - 发布 | 双向消息传递 |
| **返回值** | 有(Future) | 无(Stream) | 有(Future) |
| **数据方向** | 双向 | 单向(原生→Flutter) | 双向 |
| **编解码器** | StandardMessageCodec | StandardMessageCodec | 可自定义 |
| **典型场景** | 一次性方法调用 | 连续事件流 | 结构化数据传输 |
| **性能开销** | 中等 | 低(批量传输) | 低 |
| **使用频率** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ |
Flutter 通过 Platform Channel 机制与原生平台通信,主要有三种类型:
MethodChannel 用于方法调用(请求 - 响应模式),
EventChannel 用于事件流(订阅 - 发布模式),
BasicMessageChannel 用于双向消息传递。通信是异步的,数据以 ByteBuffer 为载体通过 BinaryMessenger 传输。实际使用中要注意线程切换
MethodChannel ------ 方法调用(请求 - 响应模式)
核心定位:跨端 RPC,用于一次性方法调用并获取返回值
Dart
class MethodChannelManager {
static final shared = MethodChannelManager._();
MethodChannelManager._() {
setupMethodChannel();
}
final methodChannel = MethodChannel("FlutterNativeBridge");
// 设置方法处理器,接收原生调用
void setupMethodChannel() {
methodChannel.setMethodCallHandler((call) async {
switch (call.method) {
case 'getData':
return 'getData result from Flutter';
default:
throw MissingPluginException();
}
});
}
// 主动调用原生方法
Future invokeMethod(String method, [dynamic arguments]) async {
final result = await methodChannel.invokeMethod(method, arguments);
return result;
}
}
// 使用示例
final batteryLevel = await MethodChannelManager.shared.invokeMethod('getBatteryLevel');
Dart
public class MainActivity extends FlutterActivity {
@Override
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
super.configureFlutterEngine(flutterEngine);
new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), "FlutterNativeBridge")
.setMethodCallHandler((call, result) -> {
switch (call.method) {
case "getBatteryLevel":
int batteryLevel = getBatteryLevel();
if (batteryLevel != -1) {
result.success(batteryLevel);
} else {
result.error("UNAVAILABLE", "Battery level not available.", null);
}
break;
case "nativeMethod":
result.success("Native Response");
break;
default:
result.notImplemented();
}
});
}
private int getBatteryLevel() {
int batteryLevel = -1;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
BatteryManager batteryManager = (BatteryManager) getSystemService(BATTERY_SERVICE);
batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY);
}
return batteryLevel;
}
}
EventChannel ------ 事件流(订阅 - 发布模式)
核心定位:原生持续向 Flutter 推送数据流,单向通信
Dart
class EventMessageChannelManager {
static final shared = EventMessageChannelManager._();
EventMessageChannelManager._() {
setupEventMessageChannel();
}
final eventMessageChannel = EventChannel("FlutterNativeEventChannelBridge");
StreamSubscription? _subscription;
void setupEventMessageChannel() {
// 接收广播流
_subscription = eventMessageChannel.receiveBroadcastStream().listen(
(event) {
print("收到原生事件:$event");
},
onError: (error) {
print("事件错误:$error");
},
);
}
void dispose() {
_subscription?.cancel();
}
}
// 使用示例
EventMessageChannelManager.shared;
Dart
public class MainActivity extends FlutterActivity {
private EventChannel.EventSink eventSink;
@Override
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
super.configureFlutterEngine(flutterEngine);
new EventChannel(flutterEngine.getDartExecutor().getBinaryMessenger(),
"FlutterNativeEventChannelBridge")
.setStreamHandler(new EventChannel.StreamHandler() {
@Override
public void onListen(Object args, EventChannel.EventSink events) {
eventSink = events;
// 开始发送事件(例如每秒发送一次)
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
@Override
public void run() {
if (eventSink != null) {
Map<String, Object> eventData = new HashMap<>();
eventData.put("timestamp", System.currentTimeMillis());
eventData.put("data", "Sensor Data");
eventSink.success(eventData);
}
}
}, 1000);
}
@Override
public void onCancel(Object args) {
eventSink = null;
}
});
}
// 主动推送数据
private void sendEventToFlutter(Map<String, Object> data) {
if (eventSink != null) {
eventSink.success(data);
}
}
}
BasicMessageChannel ------ 消息传递(双向自由通信)
核心定位:最底层、最灵活的消息通道,支持自定义协议和编解码器
Dart
class BasicMessageChannelManager {
static final shared = BasicMessageChannelManager._();
BasicMessageChannelManager._() {
setupBasicMessageChannel();
}
// 使用 StandardMessageCodec 编解码器
final basicMessageChannel = BasicMessageChannel(
"FlutterNativeBasicMessageChannelBridge",
StandardMessageCodec(),
);
void setupBasicMessageChannel() {
// 设置消息处理器
basicMessageChannel.setMessageHandler((message) async {
print("来自原生:$message");
return "BasicMessageChannelManager receive message: $message";
});
}
// 发送消息到原生
void sendMessage(String message) {
basicMessageChannel.send(message);
}
// 发送复杂数据结构
void sendComplexData() {
basicMessageChannel.send({
"cmd": "ping",
"timestamp": DateTime.now().millisecondsSinceEpoch,
"data": {"key": "value"},
});
}
}
// 使用示例
BasicMessageChannelManager.shared.sendMessage("Hello Native");
BasicMessageChannelManager.shared.sendComplexData();
Dart
public class MainActivity extends FlutterActivity {
@Override
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
super.configureFlutterEngine(flutterEngine);
new BasicMessageChannel<>(flutterEngine.getDartExecutor().getBinaryMessenger(),
"FlutterNativeBasicMessageChannelBridge",
StandardMessageCodec.INSTANCE)
.setMessageHandler((message, reply) -> {
Log.e("msg", message.toString());
// 回复消息
reply.reply("pong");
// 也可以主动发送消息给 Flutter
// BasicMessageChannel channel = ...;
// channel.send("Hello Flutter");
});
}
// 主动发送消息到 Flutter
private void sendMessageToFlutter(Map<String, Object> message) {
BasicMessageChannel<Object> channel = new BasicMessageChannel<>(
flutterEngine.getDartExecutor().getBinaryMessenger(),
"FlutterNativeBasicMessageChannelBridge",
StandardMessageCodec.INSTANCE
);
channel.send(message);
}
}
9、MethodChannel 和 EventChannel 的区别是什么
MethodChannel 是请求 - 响应模式,有返回值,适合一次性方法调用;
MethodChannel 通过 invokeMethod 调用,原生端用 setMethodCallHandler 处理;
EventChannel 是订阅 - 发布模式,无返回值但返回 Stream,适合连续事件流。
EventChannel 通过 receiveBroadcastStream 监听,原生端用 setStreamHandler 的 onListen/onCancel 管理生命周期
10、Flutter 中的状态管理是什么
状态管理是指对应用运行时所有影响外观和行为的数据进行有效组织和更新。App State(应用状态)包括用户输入、界面状态、网络请求结果、应用设置等
**App State 的类型:**
-
**UI State**:与界面相关的状态(当前选中的标签、输入字段内容、加载状态)
-
**Business Logic State**:应用逻辑相关状态(用户登录信息、购物车内容、表单数据)
-
**Network State**:与网络请求相关的状态(数据是否已加载、请求结果、错误信息)
11、Flutter 中有哪些常用的状态管理方案
| 方案 | 特点 | 适用场景 |
|------|------|---------|
| **setState** | 最基础的内置方案,适用于简单局部状态更新 | 小组件内部状态管理 |
| **InheritedWidget** | 从上到下的数据传递机制,适合跨多个小部件共享状态 | 中等规模应用的状态共享 |
| **Provider** | 基于 InheritedWidget 的高级封装,更灵活可扩展 | 大多数中小型应用 |
| **Riverpod** | Provider 的改进版,编译时安全,支持依赖注入 | 中大型应用 |
| **Bloc** | 基于 Stream 的状态管理,强调业务逻辑与 UI 分离 | 复杂业务逻辑的大型应用 |
| **GetX** | 轻量级方案,集成路由、状态管理、依赖注入 | 快速开发的中小型应用 |
12、Bloc
Bloc(Business Logic Component)是一种基于 Stream 的状态管理模式,核心思想是将业务逻辑与 UI 完全分离。Bloc 通过 Event(事件)和 State(状态)的流转来管理应用状态,配合 BLoC 模式实现单向数据流,便于测试和维护

定义事件 (Events)
Dart
// counter_event.dart
import 'package:equatable/equatable.dart';
// 基类
abstract class CounterEvent extends Equatable {
const CounterEvent();
@override
List<Object?> get props => [];
}
// 递增事件
class CounterIncrementPressed extends CounterEvent {
const CounterIncrementPressed();
}
// 递减事件
class CounterDecrementPressed extends CounterEvent {
const CounterDecrementPressed();
}
定义状态 (States)
Dart
// counter_state.dart
import 'package:equatable/equatable.dart';
class CounterState extends Equatable {
final int value;
const CounterState({required this.value});
@override
List<Object?> get props => [value];
// 方便创建新状态(不可变)
CounterState copyWith({int? value}) {
return CounterState(value: value ?? this.value);
}
}
// 初始状态
const CounterInitial = CounterState(value: 0);
创建 Bloc 实例
Dart
// counter_bloc.dart
import 'package:flutter_bloc/flutter_bloc.dart';
import 'counter_event.dart';
import 'counter_state.dart';
class CounterBloc extends Bloc<CounterEvent, CounterState> {
// 1. 初始化 Bloc,设置初始状态
CounterBloc() : super(CounterInitial) {
// 2. 注册事件处理器
on<CounterIncrementPressed>(_onIncrement);
on<CounterDecrementPressed>(_onDecrement);
}
// 处理递增事件
Future<void> _onIncrement(
CounterIncrementPressed event,
Emitter<CounterState> emit,
) async {
// emit 新状态,UI 会自动更新
emit(state.copyWith(value: state.value + 1));
}
// 处理递减事件
Future<void> _onDecrement(
CounterDecrementPressed event,
Emitter<CounterState> emit,
) async {
emit(state.copyWith(value: state.value - 1));
}
// 可选:重写 onClose 进行资源清理
@override
Future<void> close() {
// 取消订阅、关闭流等
return super.close();
}
}
在 UI 中使用 Bloc 实例
Dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'counter_bloc.dart';
import 'counter_event.dart';
import 'counter_state.dart';
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Bloc 示例')),
body: BlocBuilder<CounterBloc, CounterState>(
// 3. 构建 UI,监听状态变化
builder: (context, state) {
return Center(
child: Text(
'${state.value}',
style: Theme.of(context).textTheme.headlineMedium,
),
);
},
),
floatingActionButton: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
// 4. 发送事件
context.read<CounterBloc>().add(const CounterIncrementPressed());
},
),
SizedBox(height: 4),
FloatingActionButton(
child: Icon(Icons.remove),
onPressed: () {
context.read<CounterBloc>().add(const CounterDecrementPressed());
},
),
],
),
// 5. 提供 Bloc 实例
// create 回调中实例化 Bloc
// lazy: false 表示立即初始化(可选)
);
}
}
// 在实际使用中,你需要用 BlocProvider 包裹 CounterPage
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: BlocProvider(
create: (context) => CounterBloc(), // 这里创建了 Bloc 实例
child: CounterPage(),
),
);
}
}
13、Dart 异步编程:谈谈 Flutter 中的 Future、async 和 await
-
**Future**:表示异步操作的结果,通常通过 `then()` 来处理返回的结果
-
**async**:用于标明函数是一个异步函数,其返回值类型是 Future 类型
-
**await**:用来等待耗时操作的返回结果,这个操作会阻塞到后面的代码
-
**Isolate**:异步并行多个任务,Future 是异步串行多个任务
14、Dart 的事件循环(Event Loop)机制是什么
Dart 是单线程模型,通过事件循环处理所有任务,核心有两个队列:
| 队列类型 | 任务类型 | 优先级 | 执行时机 |
|---------|---------|--------|---------|
| **微任务队列** | `scheduleMicrotask()`、Future 回调 | 高 | 当前事件循环"空歇期"立即执行 |
| **事件队列** | 网络请求、I/O、定时器、UI 事件 | 低 | 微任务队列清空后执行 |
**与 UI 渲染的关联:** Flutter 的 UI 渲染帧(每 16.67ms 一次)是事件队列中的一个"渲染事件",其执行需等待当前微任务队列和事件队列中更早的任务完成。若微任务队列中堆积大量耗时任务,会阻塞后续事件队列的"渲染事件",导致帧丢失
15、提升 Flutter 性能的方法
**1. 减少不必要的 Widget 重建**
-
使用 `const` 关键字:对于不会改变的 Widget,用 const 修饰(如 `const Text('固定文本')`),Flutter 会视为编译时常量,多处复用也不产生额外开销
-
恰当使用 StatefulWidget:无状态改变需求时使用 StatelessWidget,避免多余的状态管理开销
**2. 优化布局**
- 避免嵌套过深:多层嵌套的 Column、Row、Padding 会拖慢渲染,可用 Flex 组件灵活排版减少层级
**3. 图片处理**
- 压缩图片资源:上传前在本地压缩,或选择合适尺寸的网络图片,防止加载超大图拖慢速度与内存占用
**4. 异步操作优化**
- 高效处理网络请求:使用 dio 等网络库时批量发起请求、合理设置超时时间,利用异步编程避免阻塞主线程
**5. 内存管理**
-
及时释放资源:监听 State 的 dispose 生命周期方法,清理定时器、流订阅、动画控制器等,防止内存泄漏
-
优化数据存储:大量数据存储在内存时考虑分页加载,长列表场景只加载屏幕内及附近少量 item,滚动时动态
16、Flutter 中如何实现路由和导航
**1. 基础路由(Navigator 1.0)**
-
使用 `Navigator.push()` 和 `Navigator.pop()` 进行页面跳转
-
通过 `MaterialPageRoute` 或 `CupertinoPageRoute` 定义路由过渡动画
-
适合简单应用的页面导航
**2. 声明式路由(Navigator 2.0)**
-
使用 `Router`、`RouterDelegate`、`RouteInformationParser` 实现声明式路由
-
支持 URL 解析、浏览器后退按钮、深层链接
-
适合 Web 应用和需要 URL 管理的场景
**3. 第三方路由库**
-
**go_router**:基于 Navigator 2.0 的简化封装,支持类型安全的路由定义
-
**auto_route**:通过代码生成自动管理路由,减少样板代码
-
**get**:GetX 框架内置的路由管理,支持命名路由和参数传递
17、Flutter 的热重载(Hot Reload)
Flutter 的热重载是基于 JIT(Just-In-Time)编译模式的代码增量同步机制。由于 JIT 属于动态编译,能够在运行时将修改后的 Dart 代码注入到正在运行的应用中,无需重新启动应用即可看到 UI 变化
18、Flutter 项目中 pubspec.yaml 文件的用途
pubspec.yaml 是 Flutter/Dart 项目的配置文件,核心作用包括:
-
**项目元数据**:定义项目名称、描述、版本、作者等信息
-
**依赖管理**:声明项目所需的 Dart 包和 Flutter 插件(dependencies)
-
**资源声明**:注册图片、字体、JSON 等静态资源(assets)
-
**平台配置**:设置 Android/iOS 的权限、图标、启动页等
-
**开发依赖**:指定测试、代码生成等开发工具(dev_dependencies)
19、main.dart 的用途是什么
main.dart 是 Flutter 应用的入口文件,包含 `main()` 函数。其核心职责是:
-
调用 `runApp()` 启动应用
-
传入根 Widget(通常是 MaterialApp 或 CupertinoApp)
-
配置主题、路由、本地化等全局设置
-
初始化必要的服务(如状态管理、网络客户端)
20、开发 Flutter 项目时一般用什么版本
这个问题考察候选人对 Flutter 版本迭代的了解。建议回答:
-
关注 Flutter 的稳定版(Stable Channel),生产环境优先使用稳定版
-
了解当前最新稳定版的版本号(如 Flutter 3.x)
-
说明版本选择策略:平衡新特性和稳定性,避免使用 Beta/Dev 版本上线
-
提及定期升级版本的计划,跟进官方安全补丁和性能优化
四、android面经
1、ContentProvider 有什么作用
ContentProvider 主要用于不同应用之间共享数据。它对外提供统一的数据访问接口,常通过 Uri 标识数据
2、什么是内存泄漏OOM
内存泄漏指对象已经不再使用,但仍然被引用,导致 GC 无法回收
应用程序消耗的内存超过系统分配给该应用的最大堆内存限制时,系统会抛出 java.lang.OutOfMemoryError 异常,导致应用崩溃
**常见原因:**
-
大图加载未压缩
-
Bitmap 未及时回收
-
集合类持有大量对象
-
单例、静态变量、非静态内部类导致内存泄漏
-
页面销毁后对象仍被引用
**常见场景:**
-
单例持有 Activity Context
-
Handler 持有外部 Activity 引用
-
匿名内部类或非静态内部类持有外部类引用
-
注册监听后未反注册
-
静态集合持有页面对象
**优化方式:**
-
图片按需压缩和采样
-
使用图片加载框架,如 Glide、Coil
-
避免内存泄漏
-
及时释放不再使用的资源
3、为什么不能在子线程直接更新 UI
Android 的 UI 控件不是线程安全的。如果多个线程同时修改 UI,容易导致状态不一致和异常。
因此 Android 规定只有主线程可以更新 UI。子线程如果要更新界面,通常需要通过:
-
Handler
-
runOnUiThread()
-
View.post()
-
LiveData
-
Kotlin 协程切回主线程(withContext(Dispatchers.Main))
4、Thread、HandlerThread、AsyncTask、线程池有什么区别
-
**Thread**:最基础的线程,适合简单异步任务
-
**HandlerThread**:带有消息循环的线程,适合串行处理任务
-
**AsyncTask**:早期用于异步任务封装,但现在已被废弃,不推荐使用
-
**ThreadPoolExecutor**:线程池,可复用线程,适合统一管理并发任务
**现代建议:**
-
现代 Android 更推荐使用 Kotlin 协程处理异步任务
-
对于可调度后台任务,推荐 WorkManager
5、Serializable 和 Parcelable 的区别是什么
-
**Serializable** 是 Java 提供的序列化接口,使用简单(只需实现接口),但性能较差,依赖反射和 I/O 操作
-
**Parcelable** 是 Android 提供的序列化方式,性能更好,更适合组件间传递数据,但实现较复杂
**使用场景:**
-
组件间传递数据(Intent 传值)→ 优先使用 Parcelable
-
持久化存储到磁盘 → 可使用 Serializable
-
跨进程通信(AIDL)→ 必须使用 Parcelable
6、Kotlin 2.0 的核心变化是什么?K2 编译器带来了哪些改进
**主要改进:**
-
**性能提升**:编译速度提升 2 倍以上,尤其是增量编译
-
**统一前端**:不再区分 JS/JVM/Native 前端,扩展性更强
-
**新语言特性**:context parameters、when 强化、let 改进等
-
**跨平台能力**:KMP(Kotlin Multiplatform)稳定,编译产物优化
统一的前端架构(FIR)
这是 K2 编译器最核心的架构变革。在旧版编译器中,Kotlin 针对不同的目标平台(JVM、JS、Native)有各自独立的前端实现,这导致:
代码复用率低
不同平台的语言特性不一致
维护成本高,新特性需要分别实现
K2 引入了统一的 FIR(Frontend Intermediate Representation)前端,所有平台共享同一套前端实现,然后针对不同后端生成相应的代码。这种架构带来的优势包括:
语言特性在所有平台上保持一致
新特性只需实现一次即可覆盖所有平台
扩展性更强,便于未来支持新平台
面试回答策略
当面试官问到这个问题时,建议按以下结构回答:
第一层:基础认知
先说明 Kotlin 2.0 的核心是 K2 编译器,这是一次架构级别的重构,而不是简单的版本迭代。
第二层:技术细节
解释 K2 的关键改进点:
编译性能提升 2 倍以上
FIR 统一前端架构的意义
对跨平台开发(KMP)的促进作用
第三层:实践经验(加分项)
如果实际项目中已经使用过 Kotlin 2.0,可以分享:
升级过程中是否遇到问题
编译速度的实际改善程度
团队对新增特性的使用情况
第四层:深度理解(高阶加分)
可以进一步讨论:
K2 编译器对 Compose 编译的影响
FIR 架构如何支持未来语言演进
K2 与 Kotlin Multiplatform 稳定化的关系
7、kotlin协程
通过编译器层面的 CPS 转换将异步代码重构为状态机,运行时通过 Continuation 保存和恢复执行状态,从而实现"以同步方式编写异步代码"
编译器转换机制:CPS 与状态机
第一步:CPS(续体传递风格)转换
编译器会给每个 suspend 函数添加一个隐藏的 Continuation 参数。这个参数就像是一个"恢复包",里面包含了:
当前执行到哪个位置(label 状态标签)
局部变量的值(需要跨挂起点保存的变量)
恢复后要执行的逻辑
第二步:生成状态机类
编译器将整个 suspend 函数重构成一个包含 when(label) 的状态机。代码为例:
// 原始代码
suspend fun fetchData(): String {
val user = getUser() // 挂起点 1
val order = getOrder(user) // 挂起点 2
return order.toString()
}
// 编译后伪代码
class FetchDataStateMachine : Continuation<String> {
var label = 0 // 状态标签
var user: User? = null // 保存局部变量
var order: Order? = null
override fun invokeSuspend(result: Result<String>) {
when (label) {
0 -> {
// 第一次执行,调用 getUser()
label = 1
result = getUser(this) // 传入 Continuation
if (result == COROUTINE_SUSPENDED) return
}
1 -> {
// 从第一个挂起点恢复
user = getResult(result)
label = 2
result = getOrder(user, this)
if (result == COROUTINE_SUSPENDED) return
}
2 -> {
// 从第二个挂起点恢复
order = getResult(result)
resumeWith(Result.success(order.toString()))
}
}
}
}
关键理解点:
label = 0 表示函数刚开始执行
label = 1 表示刚从 getUser() 恢复,继续往下执行
label = 2 表示刚从 getOrder() 恢复
label = -1 表示函数执行完成
挂起与恢复的完整流程
这个流程图展示了协程从启动到结束的完整生命周期:
挂起的触发过程:
协程开始执行,遇到第一个挂起点(如 delay(1000) 或网络请求)
挂起函数返回特殊标记 COROUTINE_SUSPENDED
关键点:此时线程被释放,可以去执行其他任务,而不是阻塞等待
状态机的当前 label 和局部变量被保存在 Continuation 对象中
恢复的触发过程:
异步操作完成(如 1 秒后 timer 触发,或网络回调返回)
系统调用 Continuation.resumeWith(result) 方法
状态机的 invokeSuspend() 被重新调用
根据保存的 label 值,when 语句跳转到对应的状态分支
从挂起点的下一行代码继续执行,就像从未中断过
这就是为什么协程被称为"轻量级线程"------它不需要操作系统级别的线程切换开销,而是通过编译器生成的状态机在用户态完成协作式调度。
调度器的工作原理
调度器(Dispatcher)决定了协程在哪个线程执行。常见的调度器有:
Dispatchers.Main:Android 主线程,用于 UI 操作
Dispatchers.IO:专为网络/文件 IO 优化的线程池
Dispatchers.Default:CPU 密集型计算的默认线程池
Dispatchers.Unconfined:不受限,挂起后在哪个线程恢复取决于挂起函数本身
withContext 切换线程的原理:
当你调用 withContext(Dispatchers.IO) { ... } 时,实际发生了以下步骤:
当前协程被挂起,保存当前状态
创建一个新的协程,使用目标 Dispatcher 启动
新协程执行传入的代码块
代码块完成后,结果通过 Continuation 传回原协程
原协程在原来的 Dispatcher 上恢复执行
这个过程中,两次线程切换都是通过挂起 - 恢复机制完成的,没有阻塞任何线程。
8、Jetpack Compose
**状态管理:**
- 使用 `remember` / `mutableStateOf` 管理局部状态
@Composable
fun CounterCorrect() {
// ✅ 正确:使用 remember 保存状态,重组后状态保留
var count by remember { mutableStateOf(0) }
Column {
Text("计数:$count")
Button(onClick = { count++ }) {
Text("增加")
}
}
}
remember 的作用是将对象存储在 Composition 中,只有首次组合时执行初始化
重组发生时,remember 返回之前存储的值,而不是重新创建
mutableStateOf 创建一个可观察的状态 holder,任何读取它的 Composable 都会在状态变化时重组
- 与 ViewModel 配合,避免过度重组
class LoginViewModel : ViewModel() {
// ✅ 使用 StateFlow 暴露状态给 Compose
private val _uiState = MutableStateFlow(LoginUiState())
val uiState: StateFlow<LoginUiState> = _uiState.asStateFlow()
fun updateUsername(username: String) {
_uiState.update { it.copy(username = username) }
}
fun login() {
viewModelScope.launch {
// 执行登录逻辑
}
}
}
data class LoginUiState(
val username: String = "",
val password: String = "",
val isLoading: Boolean = false,
val errorMessage: String? = null
)
@Composable
fun LoginScreen(viewModel: LoginViewModel = hiltViewModel()) {
// ✅ 使用 collectAsStateWithLifecycle 自动管理生命周期
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
Column {
TextField(
value = uiState.username,
onValueChange = { viewModel.updateUsername(it) }
)
if (uiState.isLoading) {
CircularProgressIndicator()
}
uiState.errorMessage?.let { error ->
Text(error, color = Color.Red)
}
Button(
onClick = { viewModel.login() },
enabled = !uiState.isLoading
) {
Text("登录")
}
}
}
- 状态提升原则:将状态移到共同的父级
反例(状态封闭在子组件内)
@Composable
fun LoginForm() {
var username by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
Column {
UsernameField(username, onValueChange = { username = it })
PasswordField(password, onValueChange = { password = it })
Button(
onClick = { submit(username, password) },
enabled = username.isNotEmpty() && password.isNotEmpty()
) {
Text("登录")
}
}
}
// ❌ 问题:UsernameField 内部不知道外部需要验证逻辑
@Composable
fun UsernameField(value: String, onValueChange: (String) -> Unit) {
TextField(value = value, onValueChange = onValueChange)
}
正例(状态提升到父组件)
@Composable
fun LoginForm() {
// ✅ 状态集中在父组件,便于统一管理和测试
var username by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
val isValid = username.length >= 6 && password.length >= 8
Column {
UsernameField(
username = username,
onValueChange = { username = it },
errorMessage = if (username.isNotEmpty() && username.length < 6)
"用户名至少 6 位" else null
)
PasswordField(
password = password,
onValueChange = { password = it },
errorMessage = if (password.isNotEmpty() && password.length < 8)
"密码至少 8 位" else null
)
Button(
onClick = { submit(username, password) },
enabled = isValid
) {
Text("登录")
}
}
}
@Composable
fun UsernameField(
value: String,
onValueChange: (String) -> Unit,
errorMessage: String? = null
) {
Column {
TextField(
value = value,
onValueChange = onValueChange,
label = { Text("用户名") }
)
errorMessage?.let {
Text(it, color = Color.Red, style = MaterialTheme.typography.bodySmall)
}
}
}
状态提升的三大原则:
单向数据流:状态向下传递,事件向上传递
无状态 Composable 更易测试:子组件只负责 UI 渲染
状态集中管理:避免状态分散在多个地方导致不一致
**重组优化:**
- 使用 `@Stable`、`@Immutable` 注解标记稳定的数据类
data class User(val name: String, val age: Int)
@Composable
fun UserProfile(user: User) {
// ❌ 问题:即使 name 和 age 没变,每次重组都会触发
// 因为 Compose 默认认为 data class 是不稳定的
Text(user.name)
Text(user.age.toString())
}
// ✅ 方案 1:使用 @Stable 标记(推荐)
@Stable
data class User(val name: String, val age: Int)
// ✅ 方案 2:使用 @Immutable(适用于完全不可变类型)
@Immutable
data class Config(val theme: String, val fontSize: Int)
// ✅ 方案 3:自定义类实现稳定性
@Stable
class ProductList {
private val _items = mutableListOf<Product>()
val items: List<Product> get() = _items.toList()
fun add(product: Product) {
_items.add(product)
// 通知 Compose 状态变化
}
}
稳定性判断规则(Compose 编译器自动推断):
- ✅ 稳定的:所有属性都是基本类型、String、或稳定的类
- ✅ 不可变的:所有属性都是 val 且类型不可变
- ❌ 不稳定的:包含可变集合、var 属性、或不稳定类型的引用
- 合理拆分 `@Composable` 函数,缩小重组范围
反例:headerText 变化 → 整个 SettingsScreen 重组(包括所有 Switch)
@Composable
fun SettingsScreen(settings: Settings) {
Column {
// ❌ 问题:headerText 变化会导致整个 Column 重组,包括下面的 Switch
Text(settings.headerText, style = MaterialTheme.typography.headlineMedium)
Row {
Text("开启通知")
Switch(
checked = settings.notificationsEnabled,
onCheckedChange = { /* ... */ }
)
}
Row {
Text("深色模式")
Switch(
checked = settings.darkModeEnabled,
onCheckedChange = { /* ... */ }
)
}
// ... 更多设置项
}
}
正例:headerText 变化 → 仅 HeaderSection 重组,其他行不受影响
@Composable
fun SettingsScreen(settings: Settings) {
Column {
HeaderSection(headerText = settings.headerText)
NotificationSettingRow(
enabled = settings.notificationsEnabled,
onToggle = { /* ... */ }
)
DarkModeSettingRow(
enabled = settings.darkModeEnabled,
onToggle = { /* ... */ }
)
}
}
// ✅ 每个小组件独立重组,互不影响
@Composable
fun HeaderSection(headerText: String) {
Text(headerText, style = MaterialTheme.typography.headlineMedium)
}
@Composable
fun NotificationSettingRow(enabled: Boolean, onToggle: (Boolean) -> Unit) {
Row {
Text("开启通知")
Switch(checked = enabled, onCheckedChange = onToggle)
}
}
@Composable
fun DarkModeSettingRow(enabled: Boolean, onToggle: (Boolean) -> Unit) {
Row {
Text("深色模式")
Switch(checked = enabled, onCheckedChange = onToggle)
}
}
- 派生状态使用 `derivedStateOf` 避免不必要的重组
@Composable
fun MessageList(messages: List<Message>) {
var scrollPosition by remember { mutableStateOf(0) }
// ❌ 问题:每次 messages 变化(即使 unreadCount 不变)都会重组
val unreadCount = messages.count { !it.isRead }
LazyColumn(state = rememberLazyListState(initialFirstVisibleItemIndex = scrollPosition)) {
item {
Text("未读消息:$unreadCount")
}
items(messages) { message ->
MessageItem(message)
}
}
}
@Composable
fun MessageList(messages: List<Message>) {
var scrollPosition by remember { mutableStateOf(0) }
// ✅ 正确:只有当 unreadCount 实际变化时才重组
val unreadCount by derivedStateOf {
messages.count { !it.isRead }
}
LazyColumn(state = rememberLazyListState(initialFirstVisibleItemIndex = scrollPosition)) {
item {
Text("未读消息:$unreadCount")
}
items(messages) { message ->
MessageItem(message)
}
}
}
derivedStateOf 创建一个派生状态,只有当计算结果真正变化时才通知观察者
上面的例子中,如果新消息是已读的,messages 列表变化但 unreadCount 不变,不会触发重组
- 列表使用 `LazyColumn` / `LazyRow` 替代传统滚动布局
@Composable
fun LongList(items: List<String>) {
// ❌ 严重问题:1000 个 item 会全部创建和布局,即使只显示 10 个
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
items.forEach { item ->
ListItem(item) // 创建 1000 个 Composable
}
}
}
@Composable
fun LongList(items: List<String>) {
// ✅ 懒加载:只创建可见区域的 item(约 10-15 个)
LazyColumn {
items(items) { item ->
ListItem(item)
}
}
}
@Composable
fun ListItem(item: String) {
Row(modifier = Modifier.fillMaxWidth().padding(16.dp)) {
Icon(Icons.Default.Person, contentDescription = null)
Spacer(modifier = Modifier.width(16.dp))
Text(item)
}
}
**常见陷阱:**
-
在 Composable 中直接创建对象(每次重组都会新建)
-
忘记使用 remember 导致状态丢失
-
过度使用 mutableStateOf 导致全量重组
9、Compose 与传统 View 系统的区别
| 对比项 | Compose | View 系统 |
|--------|---------|----------|
| 编程范式 | 声明式 UI | 命令式 UI |
| 代码量 | 少(约 40% 更少) | 多 |
| 预览功能 | @Preview 实时预览 | 需要运行应用 |
| 动画 | 简化的动画 API | 复杂的 Animation 类 |
| 互操作性 | 可通过 AndroidView 嵌入 View | 可通过 ViewCompositionStrategy 使用 Compose |
| 学习曲线 | 需适应声明式思维 | 传统方式,资料多 |
10、MVC、MVP、MVVM 、MVI 架构的区别
**MVC(Model-View-Controller):**
-
Model:数据和业务逻辑
-
View:界面展示
-
Controller:处理用户输入,协调 Model 和 View
-
缺点:View 和 Model 有直接耦合
**MVP(Model-View-Presenter):**
-
Model:数据层
-
View:被动接口,由 Presenter 调用
-
Presenter:处理业务逻辑,连接 Model 和 View
-
优点:View 和 Model 完全解耦,便于单元测试
-
缺点:Presenter 容易臃肿
**MVVM(Model-View-ViewModel):**
-
Model:数据层
-
View:界面,观察 ViewModel
-
ViewModel:持有 UI 状态,通过 LiveData/Flow 通知 View
-
优点:双向绑定,代码简洁,符合 Jetpack 理念
-
缺点:需要理解数据绑定机制
**MVI(Model-View-Intent):**
-
Model:不可变的 UI 状态快照(State)
-
View:纯渲染层,根据 State 确定性展示界面
-
Intent:用户意图的封装,触发状态变更的唯一入口
-
优点:单向数据流,状态可预测,易于调试和测试
-
缺点:学习曲线陡峭,样板代码较多,需理解响应式编程

这四种架构的核心区别在于数据流方向和状态管理方式。MVC 是最基础的模式但耦合度高;MVP 通过接口隔离提升了可测试性;MVVM 利用 DataBinding 减少了样板代码,是目前 Android 的主流选择;而 MVI 强调单向数据流和不可变状态,在复杂场景下提供了最强的可预测性
面试准备清单
**基础知识(必考):**
-
✅ 四大组件及生命周期
-
✅ Handler 消息机制
-
✅ 内存泄漏与优化
-
✅ 多线程与协程
-
✅ 常见数据结构与算法
**进阶内容(加分项):**
-
✅ Jetpack Compose 实战经验
-
✅ Clean Architecture 理解
-
✅ 性能优化案例(启动速度、内存、卡顿)
-
✅ 跨平台开发经验(KMP)
-
✅ 开源项目贡献
五、KMP面经
1、KMP与 Flutter、React Native 的区别
-
**KMP**:允许你写一套 Kotlin 代码处理网络请求、数据库操作、数据校验、业务规则和状态管理等业务逻辑,然后在 Android 和 iOS 平台分别使用原生 UI(Android 用 Jetpack Compose,iOS 用 SwiftUI)
-
**Flutter**:自带渲染引擎(Impeller),UI 和业务逻辑全部共享,在两个平台上绘制完全一致的像素
-
**React Native**:通过桥接调用原生组件,UI 和业务逻辑都共享,但存在性能损耗
KMP 的设计巧妙之处在于:它只跨平台共享了"最该共享"的代码(业务逻辑),保留了"最不该统一"的代码(平台 UI),各平台可以使用各自的最佳实践
UI 层性能:直接使用平台原生组件,没有桥接开销或自绘引擎的额外负担,动画流畅度、手势响应、无障碍支持等都符合平台规范
业务逻辑性能:Kotlin 代码被编译为平台原生代码(Android 上是字节码,iOS 上是 Swift 兼容的二进制),没有解释器或桥接的性能损耗,逻辑执行效率接近原生
对比 Flutter:Flutter 虽然性能也很高,但需要嵌入整个 Dart 运行时和渲染引擎,初始包体积较大(约 5-10MB)
对比 RN:RN 依赖 JavaScript 桥接,早期性能瓶颈明显,虽然新架构(Fabric/TurboModules)大幅改善,但仍存在桥接延迟
2、KMP 的核心优势是什么?适合哪些场景?
**核心优势:**
-
**原生性能**:编译为各平台原生代码,无桥接损耗
-
**类型安全**:Kotlin 的强类型系统贯穿整个业务逻辑层
-
**渐进式采用**:可以逐步迁移现有项目,无需一次性重写
-
**生态复用**:可复用 Kotlin 生态系统(协程、序列化库等)
-
**团队友好**:已有 Kotlin/Android 团队学习成本低
**适用场景:**
-
已有成熟 Android 团队,希望扩展 iOS 端
-
业务逻辑复杂但 UI 需要高度定制化的应用
-
对性能要求较高的场景(如数据处理、算法密集型应用)
-
需要长期维护的中大型项目
3、expect/actual 机制的工作原理
`expect/actual` 是 KMP 实现平台适配的核心机制:
-
**expect**:在 commonMain 中声明平台相关功能的接口或抽象
-
**actual**:在各平台-specific 模块中提供具体实现
**面试考点:**
-
expect/actual 必须保持签名一致(函数名、参数、返回类型)
-
可以用于函数、类、属性、注解等多种场景
-
编译器会在编译期检查 actual 是否正确实现了 expect
4、KMP 的性能表现如何?与原生开发相比有差距吗?
KMP 的性能损耗主要来自 UI 层(如果使用 Compose Multiplatform),业务逻辑层与原生持平。对于大多数应用,性能差异用户无法感知
**性能对比:**
-
**业务逻辑**:与原生几乎无差异(编译为原生代码)
-
**UI 渲染**:Compose Multiplatform 略低于纯原生,但优于 React Native
-
**启动速度**:首次启动增加约 100-200ms(KMP 运行时初始化)
-
**内存占用**:增加约 5-10MB(KMP 运行时库)
5、KMP 开发中遇到的最大挑战是什么?如何解决的?
| 挑战 | 解决方案 |
|------|----------|
| iOS 团队不熟悉 Kotlin | 组织培训,编写 iOS 端调用文档,提供 Swift 示例代码 |
| 调试困难 | Android 用 Android Studio,iOS 用 Xcode 断点调试;日志统一输出到平台原生日志系统 |
| 第三方库缺失 | 优先选择官方支持 KMP 的库(如 Ktor、SQLDelight);必要时自己封装 actual 实现 |
| 构建速度慢 | 启用 Gradle 构建缓存,配置增量编译;CI/CD 分离 Android/iOS 构建任务 |
| 测试覆盖不全 | commonMain 编写单元测试,各平台补充集成测试;使用 Kotest 多平台测试框架 |
6、KMP 的技术趋势是什么
-
**Compose Multiplatform 成熟**:UI 跨平台能力增强,更多大厂采用
-
**K2 编译器优化**:编译速度进一步提升,增量编译体验改善
-
**AI 辅助开发**:GitHub Copilot 等工具对 KMP 代码生成支持更好
-
**生态完善**:更多第三方库支持多平台,降低开发门槛
5.**KMP + 鸿蒙整合:**
7、对比 Flutter,为什么选择 KMP?什么时候应该选 Flutter 而不是 KMP?
**选择 KMP 的理由:**
-
**已有原生团队**:Android 团队可快速上手,iOS 团队继续用 Swift
-
**UI 定制化要求高**:需要遵循各平台设计规范(Material Design vs Human Interface Guidelines)
-
**性能敏感**:业务逻辑复杂,需要原生性能
-
**长期维护**:大项目,希望代码可控、可测试、可演进
**选择 Flutter 的理由:**
-
**快速原型**:小团队、创业公司,追求极致开发效率
-
**UI 一致性**:设计要求两个平台完全一致的视觉效果
-
**Web/桌面端需求**:需要同时覆盖移动端、Web、Windows/macOS
-
**热更新需求**:需要 CodePush 等动态下发能力
六、算法
PriorityQueue
offer 添加元素
poll 获取栈顶并删除
peek 获取栈顶不删除
Stack
push 入栈
pop 获取栈顶并删除
peek 获取栈顶不删除
1、数组和与哈希表
1)两数之和
**题目描述**
给定一个整数数组 `nums` 和一个目标值 `target`,找出数组中和为目标值的两个整数,返回它们的数组下标。每种输入只会对应一个答案,同一个元素不能重复使用 [1][2][4]。
**示例** `
`` 输入:nums = [2,7,11,15], target = 9 输出:[0,1](因为 nums[0] + nums[1] == 9) ```
java
private static void twoSum(int[] arrays, int sum) {
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < arrays.length; i++) {
int temp = sum - arrays[i];
if (map.containsKey(temp)) {
System.out.println("索引: " + map.get(temp) + " 值:" + temp);
System.out.println("索引: " + i + " 值:" + arrays[i]);
int[] result = {map.get(temp), i};
System.out.println(Arrays.toString(result));
break;
}
map.put(arrays[i], i);
}
}
2)字母异位词分组
**题目描述**
给定一个字符串数组,将字母异位词组合在一起。字母异位词是指字母相同但排列不同的字符串 [2][4]。
**示例**
```
输入:["eat", "tea", "tan", "ate", "nat", "bat"]
输出:[["bat"],["nat","tan"],["ate","eat","tea"]]
```
java
private static void solution(String[] strings) {
Map<String, List<String>> map = new HashMap<>();
for (String str : strings) {
char[] ch = str.toCharArray();
Arrays.sort(ch);
String after = new String(ch);
//如果键不存在,则计算并放入一个新值;如果键已存在,则直接返回旧值
map.computeIfAbsent(after.toLowerCase(), k -> new ArrayList<>()).add(str);
}
System.out.println(new ArrayList<>(map.values()));
}
3)数组中的第 K 个最大元素
**题目描述**
给定整数数组 `nums` 和整数 `k`,返回数组中第 `k` 个最大的元素 [1][4]。
**示例**
```
输入:nums = [3,2,1,5,6,4], k = 2
输出:5
```
java
/**
* 在数组中找第K大的元素,使用小顶堆
*/
private static void findKMax(int[] nums, int k) {
PriorityQueue<Integer> smallHeap = new PriorityQueue<>();
for (int n : nums) {
//添加元素
smallHeap.offer(n);
if (smallHeap.size() > k) {
//删除并返回堆顶
smallHeap.poll();
}
}
//获取堆顶(最小值),不删除
System.out.println("第 " + k + " 大的元素是 " + smallHeap.peek());
}
java
/**
* 在数组中找第K小的元素,用大顶堆
*/
private static void findKMin(int[] nums, int k) {
// PriorityQueue<Integer> bigHeap = new PriorityQueue<>((a, b) -> b - a);
PriorityQueue<Integer> bigHeap = new PriorityQueue<>(Collections.reverseOrder());
for (int n : nums) {
bigHeap.offer(n);
if (bigHeap.size() > k) {
bigHeap.poll();
}
}
System.out.println("第 " + k + " 小的元素是 " + bigHeap.peek());
}
2、链表
链表节点
java
public class LinkNode {
int value;
LinkNode next;
public LinkNode() {
}
public LinkNode(int value) {
this.value = value;
}
public LinkNode(LinkNode next, int value) {
this.next = next;
this.value = value;
}
}
生成链表
java
//生成链表
public static void generateLinkList(LinkNode head) {
LinkNode p = head;
for (int i = 0; i < 10; i++) {
LinkNode node = new LinkNode(i + 1);
node.next = null;
p.next = node;
p = node;
}
}
打印链表
java
//输出链表
public static void printLinkList(LinkNode head) {
List<Integer> result = new ArrayList<>();
LinkNode p = head.next;
while (p != null) {
result.add(p.value);
p = p.next;
}
System.out.println(result);
}
4)反转链表
**题目描述**
给定整数数组 `nums` 和整数 `k`,返回数组中第 `k` 个最大的元素 [1][4]。
**示例**
```
输入:nums = [3,2,1,5,6,4], k = 2
输出:5
```
java
//反转链表
private static void reverseLinkList(LinkNode head) {
LinkNode current = head.next;
LinkNode pre = null;
while (current != null) {
LinkNode temp = current.next;
current.next = pre;
pre = current;
current = temp;
}
head.next = pre;
}
5)合并两个有序链表
**题目描述**
将两个升序链表合并为一个新的升序链表并返回 [2][4]。
**示例**
```
输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]
```
java
private static LinkNode mergeLinkList(LinkNode head1, LinkNode head2) {
LinkNode mergeHead = new LinkNode();
LinkNode current = mergeHead;
LinkNode p1 = head1.next;
LinkNode p2 = head2.next;
while (p1 != null && p2 != null) {
if (p1.value < p2.value) {
current.next = p1;
p1 = p1.next;
} else {
current.next = p2;
p2 = p2.next;
}
current = current.next;
}
current.next = p1 == null ? p2 : p1;
return mergeHead;
}
6)判断链表是否有环
**题目描述**
给定一个链表,判断链表中是否存在环 [2][4]。
**示例**
```
输入:head = [3,2,0,-4](-4 指向 2,形成环)
输出:true
```
java
private static boolean isLoopLinkList(LinkNode node) {
LinkNode slow = node;
LinkNode fast = node.next;
while (slow != fast) {
if (fast == null || fast.next == null) {
return false;
}
slow = slow.next;
fast = fast.next.next;
}
return true;
}
7)链表的中间节点
**题目描述**
给定一个非空单链表,返回链表的中间节点。如果有两个中间节点,返回第二个 [4]。
java
* 快指针的速度是慢指针的2倍
* 所以相同起点,快指针走完全程时,慢指针位于中间
*/
private static LinkNode findMiddleNode(LinkNode head) {
LinkNode slow = head.next;
LinkNode fast = head.next;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
3、字符串与栈
8)最长无重复子串
**题目描述**
给定一个字符串 `s`,找出其中不含有重复字符的**最长子串**的长度 [2][4]。
**示例**
```
输入:s = "abcabcbb"
输出:3(最长子串为 "abc")
```
滑动窗口
java
private static void findNoRepeatMaxStr(String originalStr) {
Set<Character> characterSet = new HashSet<>();
int left = 0;
int maxLength = 0;
int maxStart = 0;
for (int right = 0; right < originalStr.length(); right++) {
char currentChar = originalStr.charAt(right);
while (characterSet.contains(currentChar)) {
characterSet.remove(currentChar);
left++;
}
characterSet.add(currentChar);
if (right - left + 1 > maxLength) {
maxLength = right - left + 1;
maxStart = left;
}
}
System.out.println("最大字符串: " + originalStr.substring(maxStart, maxStart + maxLength));
System.out.println("最大字符串长度: " + maxLength);
}
9)有效括号
**题目描述**
给定一个只包括 `'('`, `')'`, `'{'`, `'}'`, `'['`, `']'` 的字符串 `s`,判断字符串是否有效 [2][4]。
**有效字符串需满足**:
1. 左括号必须用相同类型的右括号闭合
2. 左括号必须以正确的顺序闭合
3. 每个右括号都有一个对应的相同类型的左括号
**示例**
```
输入:s = "()[]{}"
输出:true
输入:s = "(]"
输出:false
```
java
private static boolean isBracketValid(String originalString) {
if (originalString.length() % 2 != 0) {
return false;
}
Map<Character, Character> bracketMap = new HashMap<>();
bracketMap.put('(', ')');
bracketMap.put('[', ']');
bracketMap.put('{', '}');
Stack<Character> characterStack = new Stack<>();
for (Character c : originalString.toCharArray()) {
if (bracketMap.containsKey(c)) {
//遇到左括号,把有括号压入栈
characterStack.push(bracketMap.get(c));
} else {
//遇到右括号
if (characterStack.isEmpty() || characterStack.peek() != c) {
return false;
}
characterStack.pop();
}
}
return true;
}
10)最长回文子串
**题目描述**
给定一个字符串 `s`,找到 `s` 中最长的回文子串 [2][4]。
**示例**
```
输入:s = "babad"
输出:"bab" 或 "aba"
```
中心扩散
java
private static void findPalindromeString(String originalStr) {
int start = 0;
int end = 0;
for (int i = 0; i < originalStr.length(); i++) {
int len1 = palindromeLength(i, i, originalStr); //奇数长度
int len2 = palindromeLength(i, i + 1, originalStr); //偶数长度
int len = Math.max(len1, len2);
if (len > end - start) {
//(len-1)/2 是"左半径",len/2 是"右半径"。
//奇数时两半径相等,偶数时右半径比左半径多 1(因为偶数中心偏右半边)
start = i - (len - 1) / 2;
end = i + len / 2;
}
}
System.out.println("最长回文字符串:" + originalStr.substring(start, end + 1));
}
private static int palindromeLength(int left, int right, String originalStr) {
while (left >= 0 && right < originalStr.length() && originalStr.charAt(left) == originalStr.charAt(right)) {
left--;
right++;
}
//while 循环退出时,left 和 right 已经"多走了一步",指向的是不匹配的位置(或越界位置),所以真正的回文区间是 [left+1, right-1]
return right - left - 1;
}
4、二叉树
二叉树节点
java
public class BinaryTreeNode {
public int value;
public BinaryTreeNode left;
public BinaryTreeNode right;
public BinaryTreeNode() {
}
public BinaryTreeNode(int value) {
this.value = value;
}
public BinaryTreeNode(int value, BinaryTreeNode left, BinaryTreeNode right) {
this.value = value;
this.left = left;
this.right = right;
}
}
生成二叉树
java
//[3,9,20,null,null,15,7]
public static BinaryTreeNode generateBinaryTree(Integer[] values) {
if (values == null || values.length == 0 || values[0] == null) {
return null;
}
BinaryTreeNode root = new BinaryTreeNode(values[0]);
Queue<BinaryTreeNode> treeNodeQueue = new LinkedList<>();
treeNodeQueue.offer(root);
int i = 1;
while (!treeNodeQueue.isEmpty() && i < values.length) {
BinaryTreeNode curNode = treeNodeQueue.poll();
//左叶子
if (i < values.length && values[i] != null) {
BinaryTreeNode leftNode = new BinaryTreeNode(values[i]);
curNode.left = leftNode;
treeNodeQueue.offer(leftNode);
}
i++;
//右叶子
if (i < values.length && values[i] != null) {
BinaryTreeNode rightNode = new BinaryTreeNode(values[i]);
curNode.right = rightNode;
treeNodeQueue.offer(rightNode);
}
i++;
}
return root;
}
11)二叉树的最大深度
**题目描述**
给定一个二叉树,找出其最大深度。二叉树的深度为根节点到最远叶子节点的最长路径上的节点数 [2][4]。
**示例**
```
输入:root = [3,9,20,null,null,15,7]
输出:3
```
java
private static int maxDeep(BinaryTreeNode root) {
if (root == null) {
return 0;
}
return 1 + Math.max(maxDeep(root.left), maxDeep(root.right));
}
12 )二叉树的层序遍历
遍历二叉树-不展示空节点
java
public static void traverseBinaryTree(BinaryTreeNode root) {
Queue<BinaryTreeNode> nodeQueue = new LinkedList<>();
nodeQueue.offer(root);
List<Integer> result = new ArrayList<>();
while (!nodeQueue.isEmpty()) {
BinaryTreeNode curNode = nodeQueue.poll();
result.add(curNode.value);
if (curNode.left != null) {
nodeQueue.offer(curNode.left);
}
if (curNode.right != null) {
nodeQueue.offer(curNode.right);
}
}
System.out.println("二叉树遍历:" + result);
}
遍历二叉树-展示中间空节点
java
/**
* 遍历二叉树(层序遍历,输出 LeetCode 风格序列:null 表示空节点)
* 通过维护「队列中剩余非 null 节点数」来判断终止条件,避免产生尾部多余的 null
*/
public static void printBinaryTree(BinaryTreeNode root) {
List<Integer> result = new ArrayList<>();
Queue<BinaryTreeNode> treeNodeQueue = new LinkedList<>();
treeNodeQueue.offer(root);
int remainingNonNullCount = 1;
// 当队列里还有非 null 节点时才继续遍历,否则后面全是 null 占位,没必要再加
while (remainingNonNullCount > 0) {
BinaryTreeNode curNode = treeNodeQueue.poll();
// 情况 1:poll 出来的是 null(说明它是某个真实节点的空子节点占位)
if (curNode == null) {
result.add(null); // 占位写进结果
continue; // 注意:null 不需要扩展子节点,也不影响计数器
}
// 情况 2:poll 出来的是真实节点
result.add(curNode.value);
remainingNonNullCount--; // ✅ 真实节点处理完了,计数 -1
// 把左右子节点都 offer 进队(即使是 null 也 offer,用于占位)
treeNodeQueue.offer(curNode.left);
if (curNode.left != null) {
remainingNonNullCount++; // ✅ 新增了一个真实节点,计数 +1
}
treeNodeQueue.offer(curNode.right);
if (curNode.right != null) {
remainingNonNullCount++;
}
}
System.out.println("二叉树序列:" + result);
}
13)翻转二叉树
**题目描述**
给定一棵二叉树的根节点 `root`,翻转这棵二叉树并返回其根节点 [2][4]。
**示例**
```
输入:root = [4,2,7,1,3,6,9]
输出:[4,7,2,9,6,3,1]
```
java
private static void reverseBinaryTree(BinaryTreeNode node) {
if (node == null) {
return;
}
BinaryTreeNode temp = node.left;
node.left = node.right;
node.right = temp;
reverseBinaryTree(node.left);
reverseBinaryTree(node.right);
}
14)平衡二叉树
**题目描述**
给定一个二叉树,判断它是否是高度平衡的二叉树。一棵高度平衡二叉树定义为:每个节点的左右子树高度差不超过 1 [1][2]。
**示例**
```
输入:root = [3,9,20,null,null,15,7]
输出:true
```
java
private static int check(BinaryTreeNode node) {
// 1. 递归终止条件:空节点的高度是 0,且天然平衡
if (node == null) {
return 0;
}
// 2. 递归求左子树高度(如果左子树已经不平衡,直接返回 -1)
int leftHeight = check(node.left);
// 3. 递归求右子树高度(同理)
int rightHeight = check(node.right);
// 4. 三种"不平衡"情况,任一成立就返回 -1(向上传播"不平衡"信号)
if (leftHeight == -1 // 左子树内部就已经不平衡了
|| rightHeight == -1 // 右子树内部就已经不平衡了
|| Math.abs(leftHeight - rightHeight) > 1 // 当前节点的左右高度差 > 1
) {
return -1;
}
// 5. 都没问题,返回当前节点的高度 = max(左, 右) + 1
return Math.max(leftHeight, rightHeight) + 1;
}
5、动态规划
15)爬楼梯
**题目描述**
假设你正在爬楼梯。需要 `n` 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶 [2][4]?
**示例**
```
输入:n = 3
输出:3
解释:有三种方法可以爬到楼顶
1. 1 阶 + 1 阶 + 1 阶
2. 1 阶 + 2 阶
3. 2 阶 + 1 阶
```
斐波那契数列
java
private static int calculateStep(int n) {
if (n == 0) {
return 0;
}
if (n == 1) {
return 1;
}
if (n == 2) {
return 2;
}
int[] dp = new int[n + 1];
dp[1] = 1;
dp[2] = 2;
for (int i = 3; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
七、android 插件化,组件化,热更新
1、组件化
ARouter原理





gradletest01 与 ARouter


2、插件化






3、热更新
ClassLoader 在加载类时,按 dexElements 数组的顺序依次查找,找到即返回。 把修复后的类所在的 dex 插入到数组头部,就能抢先于原始 dex 被加载,从而实现热修复------这就是经典的 "dex 插桩" 方案

八、性能优化
1、android性能优化
2026 年性能优化面试的新趋势是什么?
**新兴考点:**
- **Jetpack Compose 性能**
-
重组机制深度理解
-
稳定性注解使用
-
LazyColumn 与传统 RecyclerView 对比
- **Kotlin 协程优化**
-
结构化并发避免泄漏
-
Flow 背压处理
-
调度器选择(Default/IO/Main)
- **跨端性能**
-
KMP 共享逻辑的性能表现
-
Flutter 与原生性能对比
- **AI 辅助优化**
-
使用 AI 分析性能瓶颈
-
自动化优化工具集成
- **低端机适配**
-
多后台场景下的资源竞争
-
弱网环境的降级策略
1)什么是 OOM?Android 中 OOM 的触发机制是什么?
OOM(Out Of Memory)是指应用程序申请的内存超过了系统分配的最大限制,导致虚拟机无法分配更多内存而抛出 `OutOfMemoryError`。
**触发机制:**
-
**Heap 分配流程**:当应用请求分配内存时,VM 会在 Heap 中寻找连续可用空间。若找不到足够空间且 GC 后仍无法满足,则触发 OOM
-
**Binder 缓冲区限制**:Intent 传输数据通过 Binder 机制,默认缓冲区约 1MB(500KB-1MB)。超过限制会抛出 `TransactionTooLargeException`
-
**Native 内存耗尽**:Bitmap 等大对象直接分配 Native 内存,不受 Dalvik/ART Heap 限制管理
**常见原因:**
-
内存泄漏(静态变量持 Context、未注销广播、Handler 非静态内部类)
-
一次性加载大量数据
-
Bitmap 加载不当(未缩放、未回收)
-
缓存过多(LruCache 容量设置过大)
**解决方案:**
-
使用 LeakCanary 检测内存泄漏
-
大数据分批加载,避免一次性加载
-
图片使用采样缩放(`inSampleSize`)或专业库(Glide/Fresco/Coil)
-
LruCache 根据内存级别合理设置容量(通常为可用内存的 1/8)
**高频泄漏场景:**
-
静态变量持有 Activity/Context 引用
-
匿名内部类 Handler 未使用弱引用
-
广播接收器未注销
-
异步任务(线程/协程)未在生命周期结束时取消
-
单例模式持有 Context(应使用 Application Context)
2)什么是内存抖动?如何避免?
**回答要点:**
**内存抖动**指短时间内频繁创建临时对象,导致 GC 频繁触发,引起 CPU 占用升高和 UI 卡顿。
**典型场景:**
-
循环内创建对象(`new String()`、`new StringBuilder()`)
-
自动装箱(`int` → `Integer`)
-
字符串拼接(`+` 操作在循环中)
-
自定义 View 的 `onDraw()` 中创建 `Paint`/`Path` 对象
**避免方案:**
-
循环外预创建对象复用
-
使用 `StringBuilder` 替代字符串拼接
-
基本类型优先,避免不必要的装箱
-
自定义 View 中将 `Paint` 等对象声明为成员变量,在构造函数初始化
-
使用对象池技术(如 RecyclerView 的 ViewHolder 复用机制)
3)Android 冷启动耗时的主要组成是什么?如何优化?
**冷启动耗时组成:**
-
**Process 创建**:系统 fork 进程(约 100-200ms)
-
**Application onCreate()**:第三方 SDK 初始化
-
**Activity 生命周期**:`onCreate()` → `onStart()` → `onResume()`
-
**首帧绘制**:布局解析、测量、绘制
**优化方案:**
**1. 异步初始化**
```kotlin
// 线程池并行初始化
val initExecutor = Executors.newFixedThreadPool(4)
initExecutor.execute { initBugly() }
initExecutor.execute { initIOT() }
```
**2. 延迟初始化**
使用 `IdleHandler` 在主线程空闲时执行非关键初始化:
```kotlin
Looper.myQueue().addIdleHandler {
initNonCriticalSDK()
false // 只执行一次
}
```
**3. 必要同步 + CountDownLatch**
对必须启动时初始化的 SDK,使用 `CountDownLatch` 等待完成后进入主界面:
```kotlin
val latch = CountDownLatch(2)
thread { initA(); latch.countDown() }
thread { initB(); latch.countDown() }
latch.await() // 主线程等待
```
**4. 主题优化**
设置启动页背景图,避免白屏:
```xml
<style name="LaunchTheme" parent="Theme.AppCompat">
<item name="android:windowBackground">@drawable/launch_bg</item>
</style>
4)Jetpack Compose 的重组机制是什么?如何优化重组性能?
**重组(Recomposition)**是 Compose 在状态变化时重新执行 Composable 函数的过程
**重组三阶段:** 组合(Composition)→ 布局(Layout)→ 绘制(Drawing)
**智能跳过机制:**
-
Compose 分析状态读取关系,自动跳过未变化的部分
-
作用域隔离:重组范围精确控制在最小 Composable 函数内
-
稳定性推断:编译器通过类型稳定性分析决定是否跳过
**类型稳定性分类:**
| 类型 | 特点 | 重组行为 | 示例 |
|------|------|----------|------|
| 稳定类型 | 所有属性为 val 且类型稳定 | 值未变时可跳过 | Int, String, `@Immutable` 类 |
| 不稳定类型 | 包含 var 或类型不稳定 | 总是触发重组 | List, 接口类型 |
| 运行时稳定 | 编译期不确定 | 依赖具体实现 | 泛型参数 |
**优化技巧:**
**1. 使用 `@Stable` / `@Immutable` 注解**
@Immutable= "我永远不会变,别检查我了,直接用吧。"(性能最好)@Stable= "我会变,但我会告诉你什么时候变,没变的时候别打扰我。"(性能良好)- Unstable = "我可能会偷偷变,你每次都得重新检查一遍。"(性能最差,需避免)



**2. 拆分 Composable 函数**
缩小重组范围,避免单个组件过大:
```kotlin
// ❌ 差:整个卡片都会重组
@Composable
fun Card(title: String, content: String) { ... }
// ✅ 好:只有变化部分重组
@Composable
fun Card(title: String, content: String) {
Title(text = title) // 独立重组
Content(text = content) // 独立重组
}
```
**3. 使用 `derivedStateOf` 避免派生状态的不必要重组**
```kotlin
// ❌ 每次 count 变化都重组
val filteredList = list.filter { it > threshold }
// ✅ 仅当过滤结果真正变化时才重组
val filteredList by derivedStateOf { list.filter { it > threshold } }
```
**4. 列表必须使用 LazyColumn**
禁止使用 `Column + verticalScroll` 加载长列表(性能差异 10 倍以上)
5)Compose 状态管理的最佳实践是什么
**黄金法则:**
- **remember 保存局部状态**
```kotlin
var count by remember { mutableStateOf(0) }
```
- **状态提升(State Hoisting)**
将状态移到父组件,实现单向数据流:
```kotlin
// 子组件无状态
@Composable
fun Counter(count: Int, onIncrement: () -> Unit) {
Button(onClick = onIncrement) {
Text("Clicked $count times")
}
}
// 父组件管理状态
@Composable
fun Parent() {
var count by remember { mutableStateOf(0) }
Counter(count = count, onIncrement = { count++ })
}
```
- **与 ViewModel 配合**
使用 `StateFlow + collectAsStateWithLifecycle` 自动管理生命周期:
```kotlin
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
```
**常见陷阱:**
-
忘记 `remember` 导致状态丢失
-
在 Composable 中直接创建对象(浪费内存)
-
过度使用 `mutableStateOf` 导致全量重组(应封装成数据类并拆分组件)
6)APK 瘦身有哪些常用方案?各能减少多少体积?
**1. 代码压缩与混淆(R8 / ProGuard)**
```gradle
android {
buildTypes {
release {
minifyEnabled true // 启用代码压缩
shrinkResources true // 启用资源压缩
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
```
**效果:** 减少 30%-60% 体积 [1](https://m.blog.csdn.net/wstever/article/details/147506487)
**2. 移除未使用资源**
-
启用 `shrinkResources true`
-
使用 lint 工具检查:`./gradlew lint`
-
限制资源语言:`resConfigs "zh", "en"`
**3. 图片优化**
-
使用 WebP 替代 PNG/JPEG(减少 50%-80%)
-
压缩工具:TinyPNG、ImageOptim
-
使用矢量图(Vector Drawable)替代多分辨率位图
**4. ABI 拆分**
```gradle
android {
splits {
abi {
enable true
include 'armeabi-v7a', 'arm64-v8a'
universalApk false
}
}
}
```
**5. 使用 AAB(Android App Bundle)**
Google Play 根据设备生成优化 APK,支持动态下发
**6. 移除调试信息**
```gradle
buildTypes {
release {
debuggable false
jniDebuggable false
}
}
```
**7. 动态功能模块**
将非核心功能拆分为按需下载的 Dynamic Feature Module
7)Android UI 卡顿的常见原因有哪些?如何优化?
**卡顿原因:**
- **主线程耗时操作**
-
网络请求、数据库读写
-
复杂计算、大图片解码
- **布局问题**
-
过度嵌套(超过 3 层)
-
深层 LinearLayout 权重计算
-
自定义 View 测量/绘制耗时
- **内存抖动**
- 频繁 GC 导致主线程暂停
- **RecyclerView 优化不足**
-
未使用 ViewHolder
-
`onBindViewHolder` 中耗时操作
**优化方案:**
**1. 布局优化**
-
使用 ConstraintLayout 替代嵌套 LinearLayout
-
使用 `<merge>` 标签减少层级
-
使用 `ViewStub` 延迟加载
**2. RecyclerView 优化**
```kotlin
// 启用预加载
recyclerView.setItemViewCacheSize(20)
// 固定大小(已知 item 高度相同)
recyclerView.setHasFixedSize(true)
// 使用 DiffUtil 局部刷新
diffUtil.submitList(newList)
```
**3. 硬件加速**
```kotlin
// 开启硬件加速
window.setFlags(
WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
)
```
**4. 使用 Systrace / Perfetto 定位卡顿**
生成火焰图,找出耗时方法