标签

Android

Android 是一个基于 Linux 的开源操作系统,主要被设计用来操作触屏移动设备如智能手机和平板电脑。Android 最初由安迪·鲁宾等人开发,并于2007年由Google收购。随后,Google带领开放手机联盟(Open Handset Alliance)继续开发Android,并在2008年推出了第一款商用Android设备。

Android
服务端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(); } ```
服务端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完成协调