Android第六次面试总结之Java设计模式(二)
一、适配器模式(Adapter Pattern)
1. ListView vs RecyclerView 的 Adapter 核心区别?为什么 RecyclerView 需要 ViewHolder?
解答:
-
核心区别:
特性 ListView.Adapter(如 ArrayAdapter) RecyclerView.Adapter ViewHolder 机制 无,直接通过 getView
重复创建 View(性能差)强制使用 ViewHolder 缓存 View,避免重复 inflate 数据更新 只能全局刷新( notifyDataSetChanged
)支持局部刷新( notifyItemChanged
等细粒度方法)布局管理器 内置固定布局(垂直 / 水平) 可自定义(LinearLayoutManager/GridLayoutManager) -
ViewHolder 必要性:
RecyclerView 通过 ViewHolder 缓存列表项的视图组件,避免每次滑动都调用LayoutInflater.inflate
,将列表滑动性能从 O(n) 提升至接近 O(1)。// RecyclerView 适配器必须实现 ViewHolder class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> { public static class ViewHolder extends RecyclerView.ViewHolder { TextView textView; ViewHolder(View itemView) { super(itemView); textView = itemView.findViewById(R.id.tv); } } }
2. RecyclerView 适配器如何实现多类型布局?对比 ListView 有何优势?
解答:
- 多类型布局实现:
通过重写getItemType
返回不同类型标识,在onCreateViewHolder
中创建对应 ViewHolder。@Override public int getItemType(int position) { return dataList.get(position).getType(); // 根据数据类型返回不同标识 } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { if (viewType == TYPE_ITEM1) { return new Item1ViewHolder(inflate(R.layout.item1, parent)); } else { return new Item2ViewHolder(inflate(R.layout.item2, parent)); } }
- 对比 ListView 优势:
- ViewHolder 复用机制:避免重复创建 View,性能提升显著。
- 局部刷新:支持
notifyItemChanged
等细粒度更新,减少 UI 线程负担。 - 布局灵活性:通过
LayoutManager
实现线性、网格、瀑布流等多种布局。
真题陷阱:
若getItemType
返回固定值,RecyclerView 会如何处理?
- 答案:所有 Item 使用同一 ViewHolder,无法实现多类型布局。
3. 如何优化 RecyclerView 适配器的滑动性能?
解答:
- ViewHolder 复用:避免在
onCreateViewHolder
频繁创建 View,RecyclerView 已内置此机制。 - 减少布局层级:使用
merge
标签或约束布局,避免过度嵌套。 - 预加载与缓存:
- 设置
setItemViewCacheSize
增加缓存空间。 - 开启
setHasFixedSize(true)
(若 Item 高度固定)。
- 设置
- 异步加载:在
onBindViewHolder
中避免耗时操作,如网络请求或复杂计算。
真题陷阱:
为什么setHasFixedSize(true)
能提升性能?
- 答案:告知 RecyclerView 无需重新测量 Item 尺寸,减少
requestLayout
调用。
二、观察者模式(Observer Pattern)
1. LiveData 如何实现生命周期感知?粘性事件的原理是什么?
解答:
- 生命周期感知原理:
LiveData 通过LifecycleOwner
获取生命周期状态,仅在STARTED
或RESUMED
时通知观察者。关键源码:public void observe(LifecycleOwner owner, Observer<? super T> observer) { owner.getLifecycle().addObserver(wrapper); // 注册生命周期观察者 } class LifecycleBoundObserver extends ObserverWrapper implements LifecycleEventObserver { @Override public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) { if (event == Lifecycle.Event.ON_DESTROY) { removeObserver(observer); // 自动移除观察者 } } }
- 粘性事件原理:
LiveData 缓存最新数据,新注册的观察者会立即收到当前数据。可通过MediatorLiveData
或自定义LiveData
去除粘性。
真题陷阱:
若在onStop
后数据更新,Activity 恢复后是否会收到通知?
- 答案:会。LiveData 在 Activity 重新回到
STARTED
状态时重新通知最新数据。
2. LiveData 的setValue
与postValue
有何区别?为何前者必须在主线程?(字节跳动 / 腾讯 2024 面试真题)
解答:
方法 | 线程安全 | 调用时机 | 原理 |
---|---|---|---|
setValue | 非线程安全 | 必须在主线程 | 直接调用dispatchingValue 触发通知,未通过队列保证顺序。 |
postValue | 线程安全 | 任意线程 | 将数据封装为Runnable 通过Handler 发送到主线程,保证顺序性。 |
- 主线程限制原因:
setValue
未通过队列调度,若在子线程调用可能导致数据竞争或通知顺序混乱。
真题陷阱:
在子线程中调用setValue
会发生什么?
- 答案:抛出
IllegalStateException
,提示必须在主线程调用。
3. 对比 LiveData 与 EventBus,各自的优缺点?
解答:
特性 | LiveData | EventBus |
---|---|---|
生命周期感知 | 支持(自动移除观察者) | 不支持(需手动移除,否则内存泄漏) |
线程调度 | 仅主线程更新(setValue)/ 支持子线程(postValue) | 支持自定义线程(@Subscribe (threadMode = ...)) |
数据类型 | 强类型(泛型约束) | 弱类型(任意对象) |
适用场景 | 页面内数据共享、组件间通信(同作用域) | 跨组件复杂通信(如跨 Activity/Fragment) |
- 最佳实践:
页面内数据绑定优先用 LiveData,跨组件通信可用 MutableLiveData + ViewModel,复杂场景(如事件总线)再考虑 EventBus。
三、责任链模式(Chain of Responsibility Pattern)
1. OkHttp 拦截器链如何实现责任链模式?自定义拦截器要注意什么?
解答:
-
核心原理:
OkHttp 的拦截器(Interceptor)按顺序组成链条,每个拦截器可处理请求 / 响应,或调用chain.proceed(request)
将请求传递给下一个拦截器。// 自定义日志拦截器 public class LoggingInterceptor implements Interceptor { @Override public Response intercept(Chain chain) throws IOException { Request request = chain.request(); Log.d("OkHttp", "Request: " + request.url()); Response response = chain.proceed(request); // 传递给下一个拦截器 Log.d("OkHttp", "Response: " + response.code()); return response; } }
-
拦截器顺序影响:
- 应用拦截器(
addInterceptor
):先于网络拦截器执行,且不关心重定向(适合添加公共 Header)。 - 网络拦截器(
addNetworkInterceptor
):在重试 / 重定向之后执行,可获取真实网络请求结果(适合处理 Gzip 响应)。
- 应用拦截器(
-
面试陷阱:
若拦截器未调用chain.proceed(request)
,会导致链条中断,后续拦截器无法执行(类似 Android 事件分发的onTouchEvent
返回 true)。
2. Android 事件分发是否用到责任链模式?举例说明。
解答:
-
事件分发链条:
触摸事件从Activity.dispatchTouchEvent
开始,依次经过ViewGroup
(如 LinearLayout)的dispatchTouchEvent
→onInterceptTouchEvent
,最后到View
的dispatchTouchEvent
→onTouchEvent
。- 每个节点(Activity/ViewGroup/View)可决定自己处理事件或传递给子节点(责任链核心:节点处理或转发请求)。
-
示例代码(ViewGroup 拦截事件):
public class CustomViewGroup extends ViewGroup { @Override public boolean onInterceptTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { return true; // 拦截事件,不再传递给子 View } return super.onInterceptTouchEvent(ev); } }
3. 责任链模式的优缺点?适用场景有哪些?
解答:
- 优点:
- 解耦请求发送者与处理者,处理逻辑可动态添加 / 删除(如 OkHttp 灵活配置拦截器)。
- 符合开闭原则,新增处理逻辑无需修改原有代码(只需添加新拦截器)。
- 缺点:
- 调试困难:事件传递路径不明确,需逐层打印日志定位问题。
- 可能导致处理链过长,影响性能(需控制拦截器数量)。
- Android 适用场景:
- 网络请求处理(OkHttp 拦截器)。
- 事件分发(触摸事件、按键事件处理)。
- 复杂业务流程(如订单状态校验链:库存检查→价格校验→支付权限校验)。
四、综合面试题:设计模式对比与选型
1. 适配器模式与装饰器模式的区别?(阿里 / 华为 2024 面试真题)
解答:
特性 | 适配器模式 | 装饰器模式 |
---|---|---|
目的 | 转换接口,使不兼容的类协同工作 | 动态扩展对象功能,不改变原有接口 |
类关系 | 适配器持有目标类或继承目标类 | 装饰器持有被装饰对象,实现相同接口 |
Android 案例 | RecyclerView.Adapter 适配数据到视图 | TextAppearance 为 TextView 添加样式 |
真题陷阱:
如何区分两者?
- 答案:适配器改变接口,装饰器增强功能。例如,将 List 适配为 ListView 是适配器,给 Button 添加点击动画是装饰器。
2. 观察者模式中,如何避免 “内存泄漏” 和 “重复通知”?
解答:
- 内存泄漏:
- LiveData 依赖 LifecycleOwner,自动移除观察者;传统观察者需在宿主销毁时调用
subject.removeObserver(observer)
。 - 真题陷阱:若观察者被 Activity 持有,而被观察者是单例,会导致内存泄漏吗?
- LiveData 依赖 LifecycleOwner,自动移除观察者;传统观察者需在宿主销毁时调用
答案:会。单例持有观察者的强引用,Activity 无法被回收。
- 重复通知:
- LiveData 通过
mVersion
版本号控制,相同版本数据不会重复通知(onChanged
仅在数据变化时调用)。 - 自定义观察者可添加去重逻辑(如比较新旧数据)。
- LiveData 通过