服务端5月29日 23:47
Android 内存泄漏有哪些常见场景?如何检测和避免?Android 内存泄漏本质是对象生命周期结束了,却还被 GC Roots 间接引用,导致无法回收。高频场景有:静态变量持有 Activity、非静态 Handler 或匿名内部类持有外部类、监听器/广播未注销、线程或网络请求未取消、Cursor/IO 流未关闭、集合缓存长期保存 View 或 Context。检测优先用 LeakCanary 看引用链,复杂问题再用 Android Studio Memory Profiler 抓 Heap Dump。避免原则很简单:谁注册谁注销,谁启动谁取消,长生命周期对象不要持有短生命周期 Context。
## 追问
### 为什么静态变量持有 Activity 会泄漏?
静态变量生命周期接近进程,Activity 销毁后如果仍被它引用,GC 无法回收整个 Activity 以及关联 View 树。需要 Context 时优先传 applicationContext。
### Handler 为什么容易泄漏?
非静态内部 Handler 会隐式持有 Activity,消息队列里的 Message 又持有 Handler。Activity 销毁后消息没执行完,就会延长 Activity 生命周期。
### LeakCanary 主要看什么?
重点看泄漏对象到 GC Root 的引用链,找到第一个不该长期持有它的对象。不要只看类名,要结合页面生命周期判断是不是误报。
### 项目里怎么避免监听器泄漏?
在 onStart/onStop 或 onCreate/onDestroy 成对注册和注销。使用 LifecycleObserver、协程 lifecycleScope、Flow repeatOnLifecycle 可以减少手动清理遗漏。
## 写段代码
```java
static class SafeHandler extends Handler {
private final WeakReference<Activity> ref;
SafeHandler(Activity activity) {
ref = new WeakReference<>(activity);
}
@Override public void handleMessage(Message msg) {
Activity a = ref.get();
if (a == null || a.isFinishing()) return;
}
}
@Override protected void onDestroy() {
handler.removeCallbacksAndMessages(null);
super.onDestroy();
}
```标签
Android
Android 是一个基于 Linux 的开源操作系统,主要被设计用来操作触屏移动设备如智能手机和平板电脑。Android 最初由安迪·鲁宾等人开发,并于2007年由Google收购。随后,Google带领开放手机联盟(Open Handset Alliance)继续开发Android,并在2008年推出了第一款商用Android设备。

服务端5月29日 23:47
Android View 的绘制流程是怎样的?View 的绘制流程一句话就是:从 ViewRootImpl 发起,依次执行 measure、layout、draw。measure 负责算宽高,核心是父 View 传下来的 MeasureSpec;layout 负责确定 left、top、right、bottom;draw 负责真正画到 Canvas 上。自定义 View 里最常见的问题是只重写 onDraw,却忘了在 onMeasure 处理 wrap_content。尺寸或位置变化用 requestLayout,内容变化用 invalidate,别混着用。
## 追问
### MeasureSpec 有哪几种模式?
EXACTLY 表示确定尺寸,常见于固定 dp 或 match_parent;AT_MOST 表示最大不能超过某个值,常见于 wrap_content;UNSPECIFIED 表示父容器不限制,ScrollView 测子 View 时可能出现。
### ViewGroup 和普通 View 的绘制有什么区别?
ViewGroup 在 measure 阶段要测量子 View,在 layout 阶段摆放子 View,在 draw 阶段通过 dispatchDraw 绘制子 View。普通 View 通常只关心自己的测量和 onDraw。
### requestLayout 和 invalidate 有什么区别?
requestLayout 会重新走 measure、layout,必要时再 draw;invalidate 只触发重绘。改文字大小、宽高、位置用 requestLayout,改颜色、进度、选中态通常用 invalidate。
### 实际项目里容易踩什么坑?
自定义 View 如果 wrap_content 不设置默认尺寸,可能测出来是 0 或不符合预期。另一个坑是在 onDraw 里频繁 new Paint、Path、Rect,会造成 GC 抖动和掉帧。
## 写段代码
```java
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
int defaultW = dp(120);
int defaultH = dp(48);
int w = resolveSize(defaultW, widthSpec);
int h = resolveSize(defaultH, heightSpec);
setMeasuredDimension(w, h);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawCircle(getWidth() / 2f, getHeight() / 2f, radius, paint);
}
```服务端5月29日 23:47
Android 热修复原理是什么?主流方案怎么选?Android 热修复的本质是在不发新版 APK 的情况下,让应用优先执行修复后的代码。常见路线有三类:类加载方案把补丁 dex 插到 `dexElements` 前面,重启后优先生效;底层替换方案改 ArtMethod 入口,可即时生效但兼容性压力大;插桩方案在编译期埋跳转逻辑,运行时分发到补丁实现。
## 追问
### Tinker 为什么通常需要重启?
Tinker 走类加载和差分合成路线,把补丁 dex、资源或 so 合并后,在下次启动时让 ClassLoader 加载新内容。它稳定、覆盖面广,但不能保证所有改动立即生效。
### Sophix、AndFix 这类方案为什么兼容性难?
它们涉及 ART 虚拟机内部结构,比如方法入口、ArtMethod 布局。不同 Android 版本和厂商 ROM 实现差异大,越底层越容易踩兼容坑。
### 热修复不能修什么?
通常不适合修 Manifest 变更、新增四大组件、资源 ID 大变动、启动早期就崩的代码。补丁也要做签名校验、版本校验和灰度发布,否则有安全风险。
### 线上怎么选方案?
如果追求稳定和覆盖范围,选 Tinker 类方案;如果业务强依赖即时修复,才考虑商业方案或插桩方案。无论哪种,都要有回滚、灰度和补丁监控。
## 写段代码
```java
// 关键思路:patchElements 放到原 dexElements 前面
Object[] combined = concat(patchElements, dexElements);
setField(pathList, "dexElements", combined);
```服务端5月29日 23:47
Android 性能优化怎么做?常用工具有哪些?Android 性能优化先看指标,再动代码。常见方向是启动、卡顿、内存、网络、电量和包体积;常用工具是 Android Profiler、Perfetto/Systrace、Layout Inspector、GPU Overdraw、LeakCanary、StrictMode、Battery Historian。不要凭感觉优化,先抓 trace、heap dump 或线上监控数据,定位瓶颈后再改。
## 追问
### 卡顿一般怎么排查?
先看主线程是否超过 16ms,抓 Perfetto 或 System Trace,重点看 UI Thread、RenderThread、Choreographer、GC 和锁等待。常见原因是主线程 IO、布局太深、频繁创建对象、RecyclerView 绑定太重。
### 内存优化主要看什么?
看泄漏和峰值。LeakCanary 适合开发期发现 Activity、Fragment、View 泄漏;Android Profiler 和 heap dump 用来查大对象、Bitmap、集合缓存是否失控。图片要按需采样,缓存要有上限。
### 启动优化怎么做?
区分冷启动、温启动、热启动。Application 里只放必要初始化,非关键 SDK 延迟或异步;首屏数据和布局尽量轻,启动耗时用 Startup Timing、Perfetto 或线上 APM 看。
### 网络和电量怎么优化?
网络上减少请求次数、启用缓存和压缩、合并接口;电量上少用常驻后台服务,周期任务交给 WorkManager,并设置网络、电量等约束。
## 写段代码
```kotlin
val request = PeriodicWorkRequestBuilder<SyncWorker>(1, TimeUnit.HOURS)
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.build()
).build()
```服务端5月29日 22:54
Android四大组件分别是什么?各自承担什么职责?Android四大组件是 **Activity、Service、BroadcastReceiver、ContentProvider**,它们是构成 Android 应用的基石,各自负责不同的职责边界。
**Activity** 负责用户界面的展示与交互,一个屏幕对应一个 Activity,通过 Intent 实现页面跳转和数据传递,生命周期由系统回调管理(onCreate → onResume → onPause → onDestroy)。
**Service** 在后台执行长时间运行的操作,不提供界面。分为 Started Service(startService 启动,与调用者无关)和 Bound Service(bindService 绑定,允许通信)。注意 Service 仍运行在主线程,耗时操作需开子线程。
**BroadcastReceiver** 监听并响应系统或应用发出的广播消息,如开机启动、网络变化、电量改变。分静态注册(AndroidManifest 声明,应用未启动也可接收)和动态注册(代码注册,跟随组件生命周期)。
**ContentProvider** 管理应用间数据共享,提供标准 CRUD 接口,通过 URI 定位数据。系统联系人、短信等均通过 ContentProvider 暴露。
> **追问:**
> 1. Activity 的四种启动模式分别适用于什么场景?
> 2. Service 和 IntentService 的区别是什么?IntentService 为什么已废弃?
> 3. 静态注册广播在 Android 8.0 后有哪些限制?如何适配?
> 4. ContentProvider 的 URI 格式是怎样的?如何自定义 Authority?
> 5. 四大组件中哪些可以在 Manifest 中声明多个实例?服务端5月28日 02:42
Android Binder 的原理是什么?为什么用它替代其他 IPC?Binder 是 Android 进程间通信的核心机制,系统四大组件的跨进程调用全靠它。Android 不用 Linux 原生的管道、Socket 或共享内存,核心原因三个:**只拷贝一次、内核级安全校验、自带服务发现**。
管道和 Socket 至少两次数据拷贝(用户态→内核态→用户态),Binder 通过 mmap 只拷贝一次。共享内存虽然零拷贝,但进程间没有任何身份验证机制,任何进程都能读写,Android 不敢用。Binder 每次通信都由内核自动附加调用方的 UID/PID,身份无法伪造,这是它最核心的安全优势。再加上 ServiceManager 充当"服务目录",Client 不需要硬编码 Server 地址,查一下就行。
Binder 驱动运行在内核态,暴露 `/dev/binder` 设备节点。通信流程:Server 向 ServiceManager 注册服务 → Client 查询 ServiceManager 拿到 Binder 代理对象 → Client 通过代理调方法 → Binder 驱动负责数据搬移和线程调度。ServiceManager 本身也是个 Binder 服务,handle 固定为 0。
mmap 的具体过程:binder_open 时调用 mmap,在接收方进程的用户空间和内核空间之间建立一块共享映射区(上限 4MB)。发送方通过 copy_from_user 把数据拷进这块内核映射区,接收方已经映射了同一块物理内存,直接读取——这就是"一次拷贝"的来由。发送方不做映射,因为一次通信只有接收端需要零拷贝读取。
## 追问
### Binder 线程池默认多大?满了怎么办?
默认最大 16 个线程(主线程 + 15 个工作线程)。客户端发起同步 Binder 调用,驱动在服务端线程池取线程执行。16 个都忙,新请求排队。所以主线程上不要做耗时 Binder 调用,否则 ANR——这不是建议,是血泪教训。
### Binder 通信数据大小限制是多少?
异步(oneway)事务约 64KB,同步事务整个缓冲区约 1MB(不同 Android 版本略有差异)。Intent 底层走 Binder 传输,塞大数据会炸 TransactionTooLargeException。传大文件走 ContentProvider 或 SharedMemory,别往 Intent 里硬塞。
### oneway 和同步调用有什么区别?
oneway 是 AIDL 方法修饰符,客户端调用后不阻塞,直接往下走,底层走异步事务。但注意:同一个 Binder 对象的 oneway 调用串行执行,不是并发。踩坑点——oneway 方法里抛异常,客户端完全无感知,线上排查这种问题特别痛苦。
### 为什么发送方不做 mmap 映射?
因为一次通信中,数据流是单向的:发送方只需要"写",接收方只需要"读"。给接收方做映射就能省掉第二次拷贝,给发送方做映射没有收益,还浪费内存。如果双向都需要高效传输,那就建立两个 Binder 通道,各自映射各自的。
## 写段代码
```java
// AIDL 定义
interface IBookManager {
List<Book> getBookList();
void addBook(in Book book);
oneway void notifyChange(); // 异步,不阻塞调用方
}
```服务端5月28日 02:28
Android中Handler机制的工作原理是什么?Handler是Android线程间通信的核心机制,主线程正是依靠Handler的消息循环来驱动整个应用的事件分发与UI刷新。理解Handler,关键在于搞清楚Handler、Looper、MessageQueue、Message四者如何协作,以及底层阻塞唤醒的原理。
### 核心组件与关系
**Handler**:消息的发送者与处理者。创建时绑定当前线程的Looper,通过`sendMessage()`或`post()`将消息投递到MessageQueue。
**Looper**:消息循环引擎。每个线程最多一个Looper,通过`Looper.loop()`开启死循环,不断从MessageQueue取消息分发。主线程的Looper在`ActivityThread.main()`中由系统自动创建,子线程需手动调用`Looper.prepare()`和`Looper.loop()`。
**MessageQueue**:消息队列,按`when`字段(延迟时间)升序排列的单链表,并非严格FIFO。`enqueueMessage()`按时间插入,`next()`取队头消息。
**Message**:消息载体。`what`标识类型,`arg1/arg2`传整型,`obj`传对象,`callback`可携带Runnable。通过`Message.obtain()`从消息池复用,避免频繁GC。
关系总结:一个Looper对应一个MessageQueue,一个Looper可被多个Handler共享,每个Handler只能绑定一个Looper。
### 消息发送与分发流程
```
发送:Handler.sendMessage/post()
→ Handler.enqueueMessage() 设置msg.target = this
→ MessageQueue.enqueueMessage() 按when插入链表
分发:Looper.loop() 死循环
→ MessageQueue.next() 取消息(无消息时nativePollOnce阻塞)
→ msg.target.dispatchMessage(msg) 回到发送该消息的Handler
→ Handler.dispatchMessage() 优先级:
1. msg.callback != null → handleCallback(msg)
2. mCallback != null → mCallback.handleMessage(msg)
3. handleMessage(msg) 子类重写的方法
```
**dispatchMessage的优先级是面试高频点**:post发送的Runnable优先级最高,其次是Callback接口,最后才是handleMessage。理解这个优先级有助于排查"handleMessage不执行"的问题——很可能是post的Runnable拦截了消息。
### MessageQueue的阻塞与唤醒
MessageQueue的`next()`方法中,当没有消息或队头消息的执行时间未到时,调用`nativePollOnce()`使线程进入休眠。底层实现基于Linux的epoll机制:Looper初始化时通过`eventfd`创建一个文件描述符,`nativePollOnce()`调用`epoll_wait()`阻塞在该fd上。当`enqueueMessage()`入队新消息或延迟消息到期时,`nativeWake()`向eventfd写入数据唤醒epoll。
这就是主线程`Looper.loop()`虽然是死循环却不会ANR的原因——无消息时线程休眠,有消息时才唤醒处理。ANR发生在单条消息处理超时(输入事件5秒、BroadcastReceiver 10秒、Service 20秒),而非loop()本身。
### 同步屏障(Sync Barrier)
MessageQueue支持同步屏障消息(target为null的Message)。当插入同步屏障后,`next()`会跳过所有同步消息,优先取出异步消息。系统用这个机制保证Choreographer的VSYNC信号优先处理,从而保障UI流畅绘制。具体流程:Choreographer在请求VSYNC前post一个同步屏障,VSYNC回调到来时作为异步消息插入,处理完后移除屏障。
```java
// 发送同步屏障(系统API,应用层需反射调用)
int token = MessageQueue.postSyncBarrier();
// 移除同步屏障
MessageQueue.removeSyncBarrier(token);
// Handler构造时指定async=true可发送异步消息
Handler asyncHandler = new Handler(Looper.getMainLooper(), null, true);
```
面试中若能讲出同步屏障配合Choreographer保障UI绘制的完整链路,属于加分项。
### IdleHandler
当MessageQueue中没有消息可处理时,会遍历执行注册的IdleHandler。适合做低优先级的初始化、预加载或资源回收。注意IdleHandler在`next()`中执行,若返回true则每次空闲都会重复调用。
```java
Looper.myQueue().addIdleHandler(() -> {
// 队列空闲时执行
return false; // false=只执行一次,true=每次空闲都执行
});
```
### 内存泄漏与修复
**原因**:非静态内部类Handler持有Activity引用,延迟消息在MessageQueue中存活,导致Activity无法回收。
```java
// 修复方案:静态内部类 + WeakReference
private static class SafeHandler extends Handler {
private final WeakReference<Activity> ref;
SafeHandler(Activity activity) {
ref = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
Activity activity = ref.get();
if (activity == null || activity.isFinishing()) return;
// 处理消息
}
}
// Activity销毁时清除消息
@Override
protected void onDestroy() {
handler.removeCallbacksAndMessages(null);
super.onDestroy();
}
```
### 面试追问
**Q:Handler.post()和sendMessage()的区别?**
post()底层也是sendMessage,只是将Runnable包装成Message的callback字段。dispatchMessage时callback优先级高于handleMessage。所以如果同时post了一个Runnable又send了一个Message,Runnable会先执行。
**Q:为什么主线程不会因为Looper.loop()的死循环而ANR?**
loop()在无消息时通过epoll休眠,不占CPU。ANR是单条消息处理超时,与循环本身无关。可以把loop()理解为"没活干就歇着,有活干才起来",ANR是"活干了太久"。
**Q:一个线程可以有几个Handler?几个Looper?**
多个Handler,但只有一个Looper。Handler构造时从ThreadLocal获取当前线程的Looper,重复调用`Looper.prepare()`会抛"Only one Looper may be created per thread"异常。
**Q:Message.obtain()为什么比new Message()好?**
obtain()从消息池(sPool链表,最大50个)复用Message对象,避免重复创建和GC。recycleUnchecked()将用完的Message清空数据后回收到池中。高频场景下差异明显。
**Q:Handler与Kotlin协程的关系?**
协程的`Dispatchers.Main`底层通过Handler将续体(Continuation)分发到主线程。协程是更高层的并发抽象,但主线程调度仍依赖Handler机制。可以说Handler是Android线程通信的基石,协程是在此之上构建的便利API。
服务端5月28日 02:26
Android中Jetpack组件有哪些,它们的作用是什么?## Jetpack核心组件及其作用
Jetpack是Google推出的Android组件库,按架构、UI、基础、行为四类组织,解决三个核心问题:生命周期管理、样板代码、跨版本一致性。
### 架构组件
**ViewModel** — 管理UI数据,配置变更时保留。实现原理:ComponentActivity通过onRetainNonConfigurationInstance保存ViewModelStore,重建时恢复。作用域与Lifecycle绑定,Activity finish时自动清理。
```kotlin
class MyViewModel : ViewModel() {
private val _data = MutableLiveData<String>()
val data: LiveData<String> = _data
}
```
> 追问:ViewModel和onSaveInstanceState的区别?——ViewModel存内存中的大对象,onSaveInstanceState存少量可序列化数据到磁盘;ViewModel在进程杀死时丢失,onSaveInstanceState可恢复。
**LiveData** — 生命周期感知的可观察数据容器。LifecycleBoundObserver在ON_START激活、ON_STOP暂停、ON_DESTROY自动移除,避免泄漏。粘性事件问题:新Observer注册时收到最后一次数据,解法是SingleLiveEvent或迁移到Kotlin Flow。
> 追问:map和switchMap区别?——map同步转换一対一场景;switchMap异步转换,数据源切换时取消旧观察(如根据输入ID切换查询)。
**Room** — SQLite抽象层,编译时校验SQL。三要素:Entity映射表、Dao定义操作、Database作入口。支持返回Flow实现实时更新。
```kotlin
@Entity
data class User(@PrimaryKey val id: Int, val name: String)
@Dao
interface UserDao {
@Query("SELECT * FROM user")
fun getAll(): Flow<List<User>>
}
```
> 追问:Room迁移怎么做?——Migration定义版本间ALTER语句,addMigrations注册;未覆盖的版本差走fallbackToDestructiveMigration清库。多表查询用@Transaction防数据不一致。
**Navigation** — 导航图管理Fragment跳转。SafeArgs提供编译时类型安全的参数传递,支持Deep Link。页面重叠用clearBackStack清理无效栈。
**WorkManager** — 保证执行的后台调度器,API 23+走JobScheduler,以下走AlarmManager+BroadcastReceiver。支持约束条件(网络、电量、存储)和任务链(WorkContinuation串联OneTimeWorkRequest)。
**DataBinding** — 布局与数据双向绑定。防泄漏:onDestroy中unbind()。优化:复杂逻辑抽到@BindingAdapter,不要在XML写计算。
**Hilt** — Dagger2的简化封装。@AndroidEntryPoint替代@Component,@HiltViewModel注入ViewModel,预定义Component减少90%模板代码。
**Paging 3** — 分页加载库。PagingConfig.prefetchDistance控制预加载距离,PagingDataAdapter自动处理DiffUtil和占位符。
**DataStore** — 替代SharedPreferences的异步存储。Preferences DataStore存简单键值对,Proto DataStore存类型化对象。基于协程和Flow,避免ANR。
### UI组件
**Fragment** — 模块化UI,与Activity解耦。关键:Fragment生命周期受宿主Activity驱动,onDestroyView和onDestroy在不同场景触发时机不同。
**RecyclerView** — 四级缓存(Scrap → Cache → ViewCacheExtension → RecycledViewPool),强制ViewHolder模式。与ListView区别:差异化更新、布局管理器解耦、支持多种ItemViewType。
**ViewPager2** — 基于RecyclerView,支持垂直和RTL布局,彻底替代ViewPager。
**Jetpack Compose** — 声明式UI框架,官方推荐方案。核心机制:
- 重组(Recomposition):智能跳过未变化的组合节点,粒度比View的invalidate更细
- 状态提升(State Hoisting):无状态Composable + 状态上移,提升复用性
- 副作用(LaunchedEffect/DisposableEffect):处理非组合逻辑
性能优化:@Stable减少重组范围、LazyColumn延迟加载、remember缓存计算、derivedStateOf避免频繁重组。
> 追问:remember和rememberSaveable区别?——remember配置变更时丢失,rememberSaveable通过Bundle持久化可恢复。
### 基础组件
**AppCompat** — 向后兼容层,Activity需继承AppCompatActivity才能使用Material主题和新属性。
**Android KTX** — Kotlin扩展集。典型:SharedPreferences.edit { putString() } 替代 commit/apply 样板。
**Multidex** — 突破65536方法数限制。minSdk 21+由ART原生支持,无需配置。
### 行为组件
**Notifications** — Android 13+需POST_NOTIFICATIONS权限。渠道(Channel)机制从Android 8.0开始强制。
**Permissions** — registerForActivityResult替代onRequestPermissionsResult,类型安全。
**Media3** — 统一媒体API,整合ExoPlayer和MediaCompat,推荐迁移。
### 组件协作:MVVM架构
```
View (Activity/Fragment/Compose)
↓ 观察状态
ViewModel (StateFlow / LiveData)
↓ 调用
Repository (单一数据源)
↓ 获取
Data Source (Room / Retrofit / DataStore)
```
ViewModel持有UiState,通过StateFlow推送;Repository协调本地和远程数据源;Hilt注入各层依赖;Navigation管理页面流转;DataStore处理偏好设置。这才是Jetpack组件协同工作的完整图景。
### 面试高频追问
- ViewModelStore何时创建何时销毁?——ComponentActivity首次getViewModelStore时创建,onDestroy且非配置变更时清空
- LiveData粘性事件除SingleLiveEvent外还有什么方案?——Flow的SharedFlow(SharingStarted.WhileSubscribed)天然非粘性
- WorkManager任务链如何处理中间失败?——通过observeForever监听WorkInfo,FAILED状态可链式指定fallback
- Compose重组触发条件?——读取的State变化时触发,未读取该State的部分不重组(智能跳过)
- Hilt的@Singleton作用域绑定在哪?——绑定在ApplicationComponent上,进程级单例;ActivityRetainedComponent对应ViewModel级服务端5月28日 01:53
Android中ANR是什么,如何定位和解决ANR问题?## ANR是什么?
ANR(Application Not Responding)是Android系统的一种保护机制。当应用主线程在规定时间内无法响应用户操作或系统事件时,系统会弹出"应用无响应"对话框,让用户选择继续等待或强制关闭。
ANR不是崩溃(Crash),二者本质不同:Crash是程序异常导致的进程终止,ANR是主线程阻塞导致的超时告警。一个Crash的进程也可能同时触发ANR——如果主线程在异常处理过程中阻塞了输入事件分发。
## ANR的触发条件
| 类型 | 超时阈值 | 触发场景 |
|------|---------|---------|
| 输入事件ANR | 5秒 | 按键/触摸事件未在窗口内完成分发 |
| 前台广播ANR | 10秒 | onReceive()执行超时 |
| 后台广播ANR | 60秒 | 后台广播接收器超时 |
| 前台Service ANR | 20秒 | Service生命周期方法执行超时 |
| 后台Service ANR | 200秒 | 后台Service超时 |
| ContentProvider ANR | 10秒 | ContentProvider操作未及时返回 |
## ANR的常见原因
### 主线程执行耗时操作
这是ANR最常见的原因。网络请求、数据库查询、文件IO、复杂计算等操作如果在主线程执行,会阻塞事件分发,触发ANR。
```kotlin
// 错误示例:主线程网络请求
fun onClick(v: View) {
val result = httpClient.execute(request) // 直接在主线程网络请求,5秒超时必ANR
}
// 正确示例:协程切换到IO线程
lifecycleScope.launch {
val result = withContext(Dispatchers.IO) {
httpClient.execute(request)
}
// 回到主线程更新UI
textView.text = result
}
```
### 死锁
主线程等待子线程持有的锁,子线程又等待主线程持有的锁,形成循环等待。
```kotlin
private val lock1 = Object()
private val lock2 = Object()
// 主线程
synchronized(lock1) {
Thread.sleep(100)
synchronized(lock2) { /* 死锁 */ }
}
// 子线程
synchronized(lock2) {
Thread.sleep(100)
synchronized(lock1) { /* 死锁 */ }
}
```
### Binder通信超时
跨进程调用时,如果服务端进程无响应或响应过慢,客户端主线程会因等待Binder回调而阻塞。
### 内存不足导致频繁GC
内存紧张时,系统频繁触发GC,GC过程会暂停所有线程(Stop-The-World),主线程也被挂起,累积后可能触发ANR。
## ANR的定位方法
### 分析traces文件
ANR发生时,系统会将所有线程的堆栈快照写入traces文件:
```bash
# 旧版系统
adb pull /data/anr/traces.txt
# Android 10+,traces文件按时间命名
adb pull /data/anr/anr_2026-05-28-14-30-00-000
```
分析步骤:
1. 定位到自己的进程PID(搜索包名)
2. 查看"main"线程状态,关注 Sleeping、Waiting、Monitor 等阻塞状态
3. 沿堆栈从上往下找,定位到业务代码位置
4. 如果主线程状态是 Monitor,说明在等锁,搜索持有该锁的线程
```text
"main" prio=5 tid=1 Monitor
| group="main" sCount=1 dsCount=0 obj=0x73c12000 self=0xb8e2e800
at com.example.app.MyActivity.loadData(MyActivity.java:42)
- waiting to lock <0x0d123456> (a java.lang.Object) held by thread "Worker-1"
```
### 使用Logcat过滤ANR信息
```bash
adb logcat | grep -E "am_anr|ANR in"
```
日志中会输出ANR的进程、原因、CPU使用情况。CPU使用率接近100%说明是计算密集型阻塞,CPU使用率很低说明是等锁或IO等待。
### 使用StrictMode检测主线程违规操作
```kotlin
if (BuildConfig.DEBUG) {
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()
.penaltyLog()
.build()
)
}
```
StrictMode只在Debug模式下启用,可以在开发阶段提前发现主线程的磁盘读写和网络请求。
### 使用性能分析工具
- **Systrace**:系统级性能追踪,能展示主线程每一帧的耗时分布,定位ANR前的卡顿点
- **Android Studio CPU Profiler**:方法级别的耗时分析,找出主线程最耗时的调用链
- **Perfetto**:Systrace的升级版,支持更长时间的性能追踪
## ANR的解决方案
### 将耗时操作移到子线程
```kotlin
// Kotlin协程(推荐)
lifecycleScope.launch(Dispatchers.IO) {
val data = database.query()
withContext(Dispatchers.Main) {
updateUI(data)
}
}
// 线程池
val executor = Executors.newFixedThreadPool(4)
executor.execute {
val data = database.query()
runOnUiThread { updateUI(data) }
}
```
### 避免死锁
- 统一锁的获取顺序,避免循环等待
- 使用 `tryLock(timeout)` 替代 `lock()`,设置超时避免永久阻塞
- 缩小锁的粒度,减少持锁时间
- 优先使用无锁方案,如 `ConcurrentHashMap`、`AtomicInteger`
### 优化广播接收器
```kotlin
// onReceive中不要执行耗时操作
override fun onReceive(context: Context, intent: Intent) {
// 耗时任务交给WorkManager
val request = OneTimeWorkRequestBuilder<DataProcessWorker>()
.setInputData(workDataOf("key" to intent.getStringExtra("key")))
.build()
WorkManager.getInstance(context).enqueue(request)
}
```
### 线上ANR监控
```kotlin
// 使用FileObserver监听traces文件写入
class ANRMonitor(private val anrDir: String = "/data/anr") {
private val observer = object : FileObserver(anrDir, FileObserver.CREATE) {
override fun onEvent(event: Int, path: String?) {
// 检测到新traces文件,上报ANR信息
reportANR(path)
}
}
fun start() { observer.startWatching() }
}
// 使用Watchdog定时检测主线程是否阻塞
class ANRWatchdog(private val timeoutMs: Long = 5000) : Thread("ANR-Watchdog") {
private val handler = Handler(Looper.getMainLooper())
@Volatile private var tick = 0L
override fun run() {
while (true) {
val expectedTick = SystemClock.uptimeMillis()
handler.post { tick = expectedTick }
sleep(timeoutMs)
if (tick != expectedTick) {
// 主线程阻塞超过5秒,收集堆栈上报
reportANR(Looper.getMainLooper().thread.stackTrace)
}
}
}
}
```
## 面试追问
**问:ANR和Crash有什么区别?**
ANR是主线程超时触发的系统对话框,进程仍在运行;Crash是未捕获异常导致的进程终止。关键区别:ANR时进程存活,Crash时进程死亡。但Crash可能引发ANR——异常处理过程中若阻塞了主线程,会先ANR再Crash。
**问:traces.txt找不到业务代码怎么办?**
说明ANR不是业务代码直接阻塞导致的。常见情况:系统GC暂停(主线程状态为 NATIVE 或 SUSPENDED)、Binder对端进程阻塞(看 Binder 线程堆栈)、系统资源竞争(看是否有系统锁持有者)。此时需要结合 Systrace 分析系统级行为。
**问:线上ANR率怎么治理?**
分三步:一是接入监控,使用 Watchdog 或 FileObserver 实时采集 ANR 堆栈;二是归因分类,将 ANR 按原因分为 IO 阻塞、锁等待、GC 频繁、Binder 超时等类型;三是逐类治理,IO 异步化、减少锁竞争、优化内存减少 GC、拆分跨进程调用。治理是持续过程,需要建立 ANR 率的看板和告警。
服务端5月28日 01:53
Android中如何优化应用启动速度?## Android应用启动优化详解
应用启动速度直接影响用户的第一印象。Google 的调研数据显示,启动时间每增加 100ms,转化率下降约 1.5%。在面试中,启动优化是 Android 性能优化板块的高频考点,需要从原理到实战都有清晰的理解。
### 启动类型
#### 冷启动(Cold Start)
应用进程不存在,系统从零开始创建。完整链路:
**点击图标 → Zygote fork 进程 → ActivityThread.main() → Application.attachBaseContext() → Application.onCreate() → Activity 生命周期 → ViewRootImpl.performTraversals() → 首帧绘制**
冷启动是优化的核心目标,耗时最长,用户感知最明显。
#### 热启动(Hot Start)
应用仍在后台,Activity 实例未销毁,直接从后台恢复。几乎无额外开销。
#### 温启动(Warm Start)
进程已被系统回收,但 Activity 记录仍保留在任务栈中。需要重建进程和 Application,但可跳过部分 Activity 初始化。
### 冷启动流程与时序
```
Click Launcher Icon
↓
Zygote Fork App Process (100-200ms)
↓
ActivityThread.main()
↓
Application.attachBaseContext() ← 最早可插桩的时机
↓
Application.onCreate() ← 优化主战场
↓
ContentProvider.onCreate() ← 容易被忽视的耗时点
↓
Activity.onCreate()
↓
onStart() → onResume()
↓
ViewRootImpl.performTraversals()
↓
First Frame Drawn ← 启动完成标志
```
重点关注两个阶段:**Application.onCreate()** 和 **ContentProvider.onCreate()**。很多第三方 SDK 通过 ContentProvider 静默初始化,这是隐性的启动耗时来源。
### 核心优化策略
#### 1. Application 延迟与异步初始化
**原则:主线程只做必要初始化,其余全部延迟或异步处理。**
```kotlin
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
// 必须在主线程同步初始化的(如 CrashSDK)
CrashReport.init(this)
// 异步初始化:不依赖主线程的 SDK
val ioScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
ioScope.launch {
AnalyticsSDK.init(this@MyApplication)
PushService.init(this@MyApplication)
}
// 延迟初始化:首帧之后才需要的
mainHandler.postDelayed({
ImageLoader.init(this@MyApplication)
}, 1000)
}
}
```
**注意异步初始化的线程安全:** 如果多个异步任务之间有依赖关系,不要简单用线程池,应使用 Jetpack App Startup 或协程的 `join()` 管理时序。
#### 2. Jetpack App Startup
App Startup 统一管理 ContentProvider 初始化,减少 ContentProvider 数量,并支持声明依赖关系:
```kotlin
// 定义初始化器
class AnalyticsInitializer : Initializer<Unit> {
override fun create(context: Context) {
AnalyticsSDK.init(context)
}
override fun dependencies(): List<Class<out Initializer<*>>> {
return emptyList() // 声明依赖的其他 Initializer
}
}
// 在 AndroidManifest 中移除 SDK 自带的 ContentProvider
// 改为统一通过 App Startup 管理
// <provider android:name="androidx.startup.InitializationProvider"
// android:authorities="${applicationId}.androidx-startup"
// android:exported="false"
// tools:node="merge">
// <meta-data android:name="com.example.AnalyticsInitializer"
// android:value="androidx.startup" />
// </provider>
```
**优势:** 将多个 ContentProvider 合并为一个,减少启动时 ContentProvider 的遍历开销。
#### 3. Baseline Profiles(关键优化,提升 20-30%)
Baseline Profiles 是 Android 7.0+ (ART) 的提前编译机制。通过在开发阶段生成关键代码路径的编译配置,让 ART 在安装时直接编译这些热点代码为机器码,跳过 JIT 解释执行:
```kotlin
// 1. 添加依赖
// implementation("androidx.profileinstaller:profileinstaller:1.3.1")
// implementation("androidx.benchmark:benchmark-macro-junit4:1.2.3")
// 2. 生成 Baseline Profile(在 androidTest 中运行)
@RunWith(AndroidJUnit4::class)
class BaselineProfileGenerator {
@get:Rule
val rule = BaselineProfileRule()
@Test
fun generate() {
rule.collect(
packageName = "com.example.app",
includeInStartupProfile = true
) {
startActivityAndWait()
// 模拟用户关键操作路径
}
}
}
// 3. 生成的 baseline-prof.txt 会随 APK 发布
// ART 安装时预编译这些类和方法
```
**实测数据:** 根据Google官方基准测试,配合 Baseline Profiles 可使冷启动时间减少 20-30%。在 AGP 8.0+ 中,新建项目默认集成。
#### 4. 布局优化
**减少布局层级:**
```xml
<!-- 优先使用 ConstraintLayout,减少嵌套层级 -->
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- 扁平化布局,避免 LinearLayout 嵌套 -->
</androidx.constraintlayout.widget.ConstraintLayout>
```
**ViewStub 延迟加载非首屏布局:**
```xml
<ViewStub
android:id="@+id/stub_detail_panel"
android:layout="@layout/detail_panel"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
```
```kotlin
// 需要时才 inflate
binding.stubDetailPanel.inflate()
```
**AsyncLayoutInflater 异步加载布局:**
```kotlin
AsyncLayoutInflater(this).inflate(R.layout.activity_main, null) { view, _, _ ->
setContentView(view)
}
```
注意:AsyncLayoutInflater 不支持设置 Factory,且 inflate 完成前不能操作 View。
#### 5. 黑白屏优化
冷启动时,系统先创建空白 Window 显示背景色(黑或白),直到首帧绘制完成。
**传统方案:设置启动页背景**
```xml
<style name="LaunchTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
```
**Android 12+ SplashScreen API(必须适配):**
Android 12 强制所有应用使用 SplashScreen,传统透明主题方案失效:
```kotlin
// 在 Activity 中定制退出动画
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
val splashScreen = installSplashScreen()
super.onCreate(savedInstanceState)
// 控制 SplashScreen 保持到数据加载完成
splashScreen.setKeepOnScreenCondition {
!viewModel.isDataReady.value!!
}
// 定制退出动画
splashScreen.setOnExitAnimationListener { splashScreenView ->
// 自定义退出动画
splashScreenView.remove()
}
}
}
```
#### 6. 启动耗时测量
**adb 命令(快速查看):**
```bash
adb shell am start -W com.example/.MainActivity
# 输出:WaitTime, TotalTime 等指标
# TotalTime = 应用自身启动耗时
adb logcat -s ActivityManager | grep "Displayed"
# 输出:Displayed com.example/.MainActivity: +1s234ms
```
**代码打点(精确分段):**
```kotlin
class MyApplication : Application() {
override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base)
StartTrace.begin("attachBaseContext")
}
override fun onCreate() {
StartTrace.begin("Application.onCreate")
super.onCreate()
// ...初始化逻辑
StartTrace.end("Application.onCreate")
}
}
```
**Systrace / Perfetto(系统级分析):**
```bash
# 使用 Perfetto(Systrace 的替代)
adb shell perfetto -c - --txt <<EOF
buffers: { size_kb: 65536 }
data_sources: { config { name: "linux.ftrace" } }
duration_ms: 10000
EOF
```
**Macrobenchmark(自动化性能测试):**
```kotlin
@RunWith(AndroidJUnit4::class)
class StartupBenchmark {
@get:Rule
val rule = MacrobenchmarkRule()
@Test
fun startupNoCompilation() = benchmarkStartup(CompilationMode.None())
@Test
fun startupWithBaselineProfiles() = benchmarkStartup(
CompilationMode.Partial(BaselineProfileMode.Require)
)
private fun benchmarkStartup(compilationMode: CompilationMode) {
rule.measureRepeated(
packageName = "com.example.app",
metrics = listOf(StartupTimingMetric()),
compilationMode = compilationMode,
iterations = 10
) {
startActivityAndWait()
}
}
}
```
### 面试核心回答
**问:Android 冷启动太慢怎么优化?**
核心思路是减少主线程在首帧绘制前的阻塞时间,从三个方向入手:
1. **削减主线程工作量**:Application.onCreate() 中只保留必须同步初始化的 SDK,其余全部异步或延迟。重点排查 ContentProvider 隐式初始化,用 App Startup 统一管理。
2. **加速代码执行**:使用 Baseline Profiles 让 ART 提前编译启动路径上的热点代码,实测可减少 20-30% 冷启动时间。
3. **优化用户感知**:通过 SplashScreen API(Android 12+ 必须适配)展示品牌启动画面,消除黑白屏等待。
测量工具链:`adb shell am start -W` 快速查看 TotalTime,Perfetto/Macrobenchmark 做精细化分析。
**追问:多个异步初始化任务有依赖关系怎么处理?**
用 App Startup 声明 `dependencies()` 管理时序,或用协程的 `async + await` 控制依赖顺序。不要用 CountDownLatch 等阻塞方式,会拖慢主线程。
**追问:Baseline Profiles 的原理是什么?**
ART 运行时默认使用 JIT 解释执行字节码,热点代码才编译为机器码。Baseline Profiles 在安装阶段就告诉 ART 哪些类和方法是启动路径上的热点,直接 AOT 编译,避免运行时 JIT 的开销。这与 AGP 8.0+ 的 baseline-prof.txt 集成,安装时自动应用。服务端5月28日 01:52
Android Activity生命周期有哪些回调方法?各自什么时机触发?## 7个核心回调方法
Activity生命周期是Android面试的高频考点,理解每个回调的触发时机和正确用法,是写出稳定应用的基础。
### onCreate()
Activity首次创建时调用。这是生命周期的入口,执行一次性的初始化工作:加载布局(setContentView)、初始化变量、恢复保存的状态。
```kotlin
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 恢复保存的状态
if (savedInstanceState != null) {
val value = savedInstanceState.getString("key")
}
}
```
注意:此时Activity尚未可见,不要在这里启动动画或执行需要可见UI的操作。
### onStart()
Activity即将变为可见时调用。此时Activity已经出现在屏幕上,但用户还无法与之交互。适合做轻量级的准备工作,比如注册广播接收器、初始化UI动画。
Activity从停止状态回到前台时也会经过此方法,因此onStart()中的逻辑每次可见都会执行,不要放只应执行一次的初始化代码。
### onResume()
Activity获得焦点、可以与用户交互时调用。此时Activity位于前台,处于活跃状态。需要独占资源的操作——比如打开相机、开始GPS定位、注册传感器监听——都在这里启动。
```kotlin
override fun onResume() {
super.onResume()
cameraManager.openCamera(cameraId, stateCallback, backgroundHandler)
sensorManager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_NORMAL)
}
```
### onPause()
Activity失去焦点时调用。此时Activity可能仍然部分可见(比如被透明或浮层Activity遮挡),但用户无法与之交互。这是保存未提交数据和释放非关键资源的安全位置。
**关键约束**:onPause()必须快速执行。系统在前一个Activity的onPause()返回之前,不会启动新的Activity。如果在这里执行耗时操作(网络请求、数据库写入),会直接卡住界面切换。
```kotlin
override fun onPause() {
super.onPause()
// 停止相机预览,释放独占资源
camera?.stopPreview()
sensorManager.unregisterListener(this)
}
```
### onStop()
Activity完全不可见时调用。与onPause()的区别:onPause()时Activity可能还部分可见,onStop()时已完全被遮挡。适合释放不再需要的资源:注销广播、停止动画、持久化数据。
如果系统内存紧张,onStop()之后Activity可能被直接回收,不会再走到onDestroy()。因此关键数据的持久化不要拖到onDestroy()。
### onDestroy()
Activity被销毁前的最后回调。执行最终的资源清理:关闭数据库连接、释放文件句柄、取消网络请求。
可以通过isFinishing()判断是正常结束(用户按返回键)还是系统配置变更导致的重建:
```kotlin
override fun onDestroy() {
super.onDestroy()
if (isFinishing) {
// 用户主动退出,彻底清理
releaseAllResources()
}
// 配置变更导致的销毁,ViewModel中的数据不需要清理
}
```
### onRestart()
Activity从停止状态重新启动时调用,之后会走onStart() → onResume()。这个回调使用频率不高,主要用于在Activity重新可见前做一些恢复工作,比如刷新可能过时的UI数据。
## 典型场景下的生命周期流程
| 场景 | 回调顺序 |
|------|----------|
| 首次打开 | onCreate → onStart → onResume |
| 跳转到其他Activity | 当前Activity: onPause → onStop |
| 从其他Activity返回 | onRestart → onStart → onResume |
| 按Home键切到后台 | onPause → onStop |
| 从后台回到前台 | onRestart → onStart → onResume |
| 按返回键退出 | onPause → onStop → onDestroy |
| 弹出Dialog | 不触发生命周期回调(Activity仍有焦点) |
| 弹出全屏Dialog | onPause →(关闭后)→ onResume |
| 横竖屏切换(默认) | onPause → onStop → onDestroy → onCreate → onStart → onResume |
| 横竖屏切换(configChanges) | onConfigurationChanged,不重建 |
## 面试高频追问
### 两个Activity跳转时,生命周期回调的先后顺序是什么?
从Activity A跳转到Activity B,执行顺序是:
A.onPause → B.onCreate → B.onStart → B.onResume → A.onStop
注意:是A的onPause先执行完,B才开始创建。这再次说明onPause不能做耗时操作。
### onSaveInstanceState和onRestoreInstanceState在什么时候调用?
onSaveInstanceState在Activity可能被系统回收之前调用,确保在onStop之前。典型场景:按Home键、切换到其他Activity、屏幕旋转。
onRestoreInstanceState在Activity被重建后、onStart之后调用。只有当系统确实回收并重建了Activity时才会触发,正常创建不会调用。
```kotlin
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString("edit_text_content", editText.text.toString())
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
val content = savedInstanceState.getString("edit_text_content")
editText.setText(content)
}
```
也可以在onCreate中通过判断savedInstanceState是否为null来恢复数据,但onRestoreInstanceState更安全——它保证了Bundle一定非空。
### 如何避免横竖屏切换时Activity重建?
在AndroidManifest中为Activity添加configChanges属性:
```xml
<activity
android:name=".MainActivity"
android:configChanges="orientation|screenSize|keyboardHidden" />
```
这样屏幕旋转时只回调onConfigurationChanged,不会走销毁重建流程。但更推荐的做法是配合ViewModel,让数据在配置变更时自动保留,而不是禁止重建。
### ViewModel与生命周期的关系是什么?
ViewModel的生命周期独立于Activity的配置变更。当Activity因横竖屏切换而销毁重建时,ViewModel不会被清除,新创建的Activity实例会拿到同一个ViewModel对象。只有当Activity真正结束(isFinishing为true)时,ViewModel的onCleared()才会被调用。
这就是为什么推荐用ViewModel保存UI数据,而不是用onSaveInstanceState——前者能保留任意大小的对象,后者只能存少量可序列化数据。
服务端5月28日 01:42
Android中RecyclerView和ListView的区别是什么,为什么推荐使用RecyclerView?## 核心区别一览
RecyclerView从Android 5.0(API 21)引入,设计目标就是替代ListView。两者最本质的差异在于架构理念:ListView是一个大而全的控件,把布局、复用、点击都包在自己身上;RecyclerView则把职责拆散,布局交给LayoutManager,复用交给Recycler,动画交给ItemAnimator,装饰交给ItemDecoration——每个环节都可替换。
| 对比维度 | ListView | RecyclerView |
|------|----------|--------------|
| ViewHolder | 可选,需手动实现 | 强制,内置在Adapter中 |
| 布局方式 | 仅垂直列表 | Linear/Grid/Staggered,支持水平 |
| 缓存层级 | 2级(mActiveViews + mScrapViews) | 4级(Scrap → CachedViews → Extension → Pool) |
| 局部刷新 | 仅notifyDataSetChanged() | notifyItemChanged/Inserted/Removed + DiffUtil |
| Item动画 | 无内置支持 | 内置ItemAnimator |
| 分割线 | divider属性 | ItemDecoration自定义 |
| 点击事件 | setOnItemClickListener | ViewHolder中回调 |
| 嵌套滚动 | 不支持 | 实现NestedScrollingChild |
| Header/Footer | addHeaderView/addFooterView | 无直接API,需多ViewType实现 |
## 为什么推荐RecyclerView
### 强制ViewHolder模式
ListView时代,ViewHolder是一个最佳实践但不是强制要求。忘记写ViewHolder的代码依然能跑,只是滑动时反复调用findViewById导致卡顿。RecyclerView直接把ViewHolder变成Adapter的内部类,你不写就无法编译通过,从根本上杜绝了性能隐患。
```java
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {
static class ViewHolder extends RecyclerView.ViewHolder {
TextView textView;
ViewHolder(View view) {
super(view);
textView = view.findViewById(R.id.text);
}
}
}
```
### 四级缓存机制
这是面试最爱追问的细节。ListView只有两级缓存:屏幕内的mActiveViews和屏幕外的mScrapViews,且mScrapViews缓存的是裸View,取出后必须重新bindView。RecyclerView有四级缓存,缓存的是ViewHolder(含View和绑定状态),数据未变时连bindView都省了:
1. **mAttachedScrap**:屏幕内缓存。Item短暂移出屏幕(如滑动后弹回)直接从这里取,不需要重新bind,复用速度最快
2. **mCachedViews**:屏幕外缓存,默认容量2个。刚滑出屏幕的Item暂存于此,也是不需要重新bind的
3. **ViewCacheExtension**:自定义缓存层,开发者可按业务逻辑实现,比如按ViewType做LRU缓存
4. **RecycledViewPool**:缓存池,按ViewType分类存储,默认每种ViewType最多5个。从这里取出的ViewHolder需要重新bind,但省去了inflate的开销
关键区别在于:ListView的缓存取出后一定重新bindView,RecyclerView的Scrap和CachedViews层取出后可以跳过bindView,这就是性能优势的核心来源。
### 灵活的布局管理
RecyclerView通过LayoutManager解耦了布局逻辑,一种Adapter可以配合不同LayoutManager展示不同效果:
```java
// 垂直列表
recyclerView.setLayoutManager(new LinearLayoutManager(context));
// 网格
recyclerView.setLayoutManager(new GridLayoutManager(context, 2));
// 瀑布流
recyclerView.setLayoutManager(
new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL));
```
ListView只能做垂直列表,想做横向或网格得换控件。
### 内置动画与局部刷新
RecyclerView默认支持Item增删动画,一行代码启用:
```java
recyclerView.setItemAnimator(new DefaultItemAnimator());
```
更实用的是局部刷新能力。ListView只能notifyDataSetChanged()全量刷新,RecyclerView可以精确到单个Item:
```java
adapter.notifyItemInserted(position); // 插入
adapter.notifyItemRemoved(position); // 删除
adapter.notifyItemChanged(position); // 更新
```
DiffUtil则更进一步,自动计算新旧数据集的差异并派发最小范围的刷新事件,避免不必要的重绘:
```java
DiffUtil.DiffResult result = DiffUtil.calculateDiff(new MyDiffCallback(oldList, newList));
result.dispatchUpdatesTo(adapter);
```
### 嵌套滚动支持
RecyclerView实现了NestedScrollingChild接口,可以与CoordinatorLayout、AppBarLayout等协同工作,实现折叠Toolbar、吸顶等交互效果。ListView不支持嵌套滚动,放在CoordinatorLayout中会出现滑动冲突。
## RecyclerView的优化实践
### setHasFixedSize
如果Item高度固定,设置此标记可以避免每次数据变化都重新测量RecyclerView自身尺寸:
```java
recyclerView.setHasFixedSize(true);
```
### 共享RecycledViewPool
多个RecyclerView使用相同ViewType时,共享缓存池可以减少inflate次数,常见于ViewPager+Tab场景:
```java
RecyclerView.RecycledViewPool pool = new RecyclerView.RecycledViewPool();
pool.setMaxRecycledViews(TYPE_ITEM, 10);
recyclerView1.setRecycledViewPool(pool);
recyclerView2.setRecycledViewPool(pool);
```
### 预加载
在嵌套滑动的父RecyclerView中,子RecyclerView可以提前预加载Item,减少滑动卡顿:
```java
linearLayoutManager.setInitialPrefetchItemCount(4);
```
### DiffUtil替代notifyDataSetChanged
全量刷新是最常见的性能浪费。DiffUtil在后台线程计算差异,主线程只刷新变化的Item,对于列表数据频繁变化的场景性能提升显著。
## ListView还有用武之地吗
RecyclerView功能强大但代码量大,一个最简单的列表RecyclerView需要写Adapter、ViewHolder、LayoutManager三部分。如果需求就是展示一个固定数据的小列表,不需要动画、不需要多布局,ListView写起来更快。另外ListView的addHeaderView/addFooterView用起来确实比RecyclerView的多ViewType方案简单。但新项目原则上应全部使用RecyclerView,ListView只在维护老代码时才会遇到。
## 面试追问方向
- RecyclerView四级缓存各层的作用和复用条件是什么?重点区分哪些层需要重新bind,哪些不需要
- ListView两级缓存 vs RecyclerView四级缓存,性能差距到底体现在哪?答案在bindView的调用次数
- DiffUtil的内部实现原理?基于Eugene W. Myers差分算法,时间复杂度O(N),空间复杂度O(N²),数据量大时建议用AsyncListDiffer
- RecyclerView嵌套滚动的原理?通过NestedScrollingChild向父View分发滚动事件,配合NestedScrollingParent完成协调