Android第六次面试总结之Java设计模式篇(一)
一、单例模式在 Android 面试中的核心考点
1. Android 中如何安全实现单例?需注意哪些坑?(字节跳动、美团面试真题)
解答:
Android 中实现单例需重点关注 Context 泄漏、线程安全 和 反射 / 序列化攻击。
- 推荐实现:静态内部类(线程安全 + 避免内存泄漏)
public class AppManager { private Context context; // 静态内部类持有实例(类加载时初始化,线程安全) private static class Holder { static final AppManager INSTANCE = new AppManager(); } private AppManager() { // 使用 Application Context,避免持有 Activity Context 导致泄漏 context = MyApplication.getAppContext(); } public static AppManager getInstance() { return Holder.INSTANCE; } }
- 注意事项:
- Context 选择:单例若需持有 Context,必须使用 Application Context(通过
getApplicationContext()
或自定义 Application 类获取),避免传入 Activity Context 导致内存泄漏(Activity 销毁后单例仍持有其引用)。 - 反序列化安全:若单例需实现
Serializable
,需添加readResolve()
方法返回已有实例;或直接使用 枚举单例(Android 中枚举序列化安全,且代码简洁)。
- Context 选择:单例若需持有 Context,必须使用 Application Context(通过
2. 单例在 Android 中的典型应用场景(阿里、腾讯面试真题)
解答:
- 全局管理器:如网络请求管理器(Retrofit 单例)、图片加载器(Glide 内部单例)、数据库管理类(Room Database 单例)。
- 状态管理:全局用户信息、配置参数(如 App 主题、语言设置)。
- 系统服务代理:封装
SensorManager
、NotificationManager
等系统服务,提供统一访问入口。
3. 反模式:为什么不推荐在 Android 中使用双重检查锁(DCL)?
解答:
- DCL 需配合
volatile
关键字防止指令重排序,但 Android 早期版本(如 Java 1.4 前)的 JVM 对volatile
支持不完整,可能导致实例未完全初始化就被访问。 - 静态内部类或枚举单例实现更简单且线程安全,无需手动处理同步,是 Android 中的首选方案。
二、工厂模式在 Android 开发中的实战场景
1. LayoutInflater 如何体现工厂模式?如何自定义 View 工厂?(字节跳动面试真题)
解答:
- 系统级应用:Android 的
LayoutInflater
是典型的 工厂方法模式,通过inflate()
方法根据布局文件创建 View 实例,子类(如AppCompatDelegate
)可自定义 View 创建逻辑(如兼容旧版控件)。 - 自定义 View 工厂(示例:根据类型创建不同的 ViewHolder):
public interface ViewHolderFactory { ViewHolder createViewHolder(View itemView, int viewType); } // 实现类 public class DefaultViewHolderFactory implements ViewHolderFactory { @Override public ViewHolder createViewHolder(View itemView, int viewType) { if (viewType == TYPE_TEXT) { return new TextViewHolder(itemView); } else if (viewType == TYPE_IMAGE) { return new ImageViewHolder(itemView); } throw new IllegalArgumentException("Unknown view type"); } }
- 优势:解耦 View 创建逻辑,方便扩展(如新增 ViewType 时无需修改适配器核心代码)。
2. 对比简单工厂 vs 工厂方法:何时选择哪种?
Android 场景举例:
- 简单工厂:适合轻量化场景,如根据类型创建不同的动画对象(AnimationFactory.createAnimation (type)),新增类型需修改工厂类。
- 工厂方法:适合复杂场景或需遵循开闭原则,如 RecyclerView 的
ViewHolder
创建(通过onCreateViewHolder
由子类实现)。
3. ThreadFactory 为什么是线程池的 “灵魂组件”?(字节跳动、美团面试真题)
源码级解析:
线程池(如ThreadPoolExecutor
)通过ThreadFactory
创建线程,核心作用:
- 解耦线程创建逻辑:将 “如何创建线程”(如命名、优先级、守护线程)与 “如何管理线程”(如任务队列、拒绝策略)分离。
- 统一线程属性:确保线程池内所有线程具备相同的基础配置(如业务线程设置
setName("Biz-Thread-%d")
,便于日志追踪)。
自定义 ThreadFactory 实战(面试必考代码):
public class NamedThreadFactory implements ThreadFactory { private final String namePrefix; private final boolean daemon; private final int priority; public NamedThreadFactory(String namePrefix, boolean daemon, int priority) { this.namePrefix = namePrefix; this.daemon = daemon; this.priority = Math.min(Math.max(priority, Thread.MIN_PRIORITY), Thread.MAX_PRIORITY); } @Override public Thread newThread(Runnable r) { Thread thread = new Thread(r, namePrefix + "-" + counter.incrementAndGet()); thread.setDaemon(daemon); // 设置守护线程(如后台日志线程) thread.setPriority(priority); // 关键:为线程设置未捕获异常处理器,避免静默崩溃 thread.setUncaughtExceptionHandler((t, e) -> { Log.e("ThreadFactory", "Thread " + t.getName() + " crashed: " + e.getMessage()); }); return thread; } private final AtomicInteger counter = new AtomicInteger(1);
}
4. 线程池工厂模式的 5 大面试考点
① 为什么线程池不直接使用 new Thread (),而是通过工厂?
- 答案:
- 统一管控线程属性(命名规则、优先级),避免 “野线程”(无意义的 Thread-0/1/2),提升调试效率(通过线程名快速定位问题线程)。
- 支持扩展(如创建守护线程、设置安全上下文
AccessControlContext
)。
② FixedThreadPool 为什么被弃用?与工厂模式的关系?
- 源码对比(JDK 8):
// 旧版FixedThreadPool(硬编码工厂,无自定义能力) public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), new DefaultThreadFactory()); // 匿名工厂,线程名无业务含义 } // 推荐做法:自定义工厂 ExecutorService pool = new ThreadPoolExecutor( 4, 8, 30, SECONDS, new ArrayBlockingQueue<>(100), new NamedThreadFactory("Order-Processor", false, Thread.NORM_PRIORITY) );
- 弃用原因:默认工厂创建的线程名无业务意义,且
LinkedBlockingQueue
可能导致 OOM(队列无界),自定义工厂 + 合理参数是现代开发标配。
③ 守护线程在工厂中的应用场景
- 场景:后台日志收集线程、心跳检测线程,设置
thread.setDaemon(true)
,确保程序退出时随主线程终止,避免资源泄漏。
④ 工厂模式如何配合线程池拒绝策略?
- 当线程池拒绝任务(如
AbortPolicy
抛出异常),可通过工厂为线程添加钩子函数,监控拒绝事件:// 在newThread中设置钩子 thread.setUncaughtExceptionHandler((t, e) -> { if (e instanceof RejectedExecutionException) { metrics.trackRejectedTask(t.getName()); // 统计拒绝次数 } });
三、建造者模式在 Android 中的高频考点
1. OkHttpClient.Builder:网络配置的 “瑞士军刀”(字节跳动、腾讯面试真题)
核心配置项源码解析:
public final class OkHttpClient.Builder { // 必选参数(无默认值,构建时校验) private int connectTimeout = 10_000; // 10秒 private int readTimeout = 10_000; private int writeTimeout = 10_000; // 可选参数(有默认实现,支持链式覆盖) private List<Interceptor> interceptors = new ArrayList<>(); // 应用拦截器(用户自定义) private List<Interceptor> networkInterceptors = new ArrayList<>(); // 网络拦截器(OkHttp内部) private ConnectionPool connectionPool = new ConnectionPool(); // 连接池(默认保持5分钟) // 链式方法本质:返回this,支持连续调用 public Builder connectTimeout(int timeout, TimeUnit unit) { this.connectTimeout = unit.toMillis(timeout); return this; } // 构建核心:校验参数 + 不可变对象创建 public OkHttpClient build() { return new OkHttpClient(this); // 将Builder状态复制到OkHttpClient实例 }
}
面试高频问题:
① 应用拦截器 vs 网络拦截器(顺序决定行为)
- 执行顺序:应用拦截器(用户自定义)→ 重试 & 重定向拦截器 → 桥接拦截器 → 网络拦截器 → 连接池拦截器 → 最后是实际网络请求。
- 典型场景:
- 应用拦截器:添加公共 Header(如 Token)、日志打印(不关心重定向)。
- 网络拦截器:处理响应体压缩(Gzip)、监控真实网络请求耗时(排除重试逻辑)。
② 连接池为什么默认保持 5 分钟?建造者如何自定义
// 自定义连接池(长连接场景)
OkHttpClient client = new OkHttpClient.Builder() .connectionPool(new ConnectionPool(10, 30, TimeUnit.MINUTES)) // 保持10个空闲连接,30分钟 .build();
- 原理:通过建造者设置
ConnectionPool
,避免频繁创建 TCP 连接(三次握手开销),提升 HTTPS 请求性能。
2. Retrofit.Builder:从接口到网络请求的 “转换器工厂”(阿里、美团面试真题)
核心扩展点源码解析:
public final class Retrofit { private final String baseUrl; private final List<Converter.Factory> converterFactories; // 数据转换器(如Gson/Jackson) private final List<CallAdapter.Factory> callAdapterFactories; // 回调适配器(如RxJava、Kotlin Coroutines) private final OkHttpClient client; // 依赖OkHttp的建造者配置 // 建造者核心:链式添加工厂 public Builder addConverterFactory(Converter.Factory factory) { converterFactories.add(factory); return this; } public Builder client(OkHttpClient client) { this.client = client; // 可复用外部配置好的OkHttpClient(如已添加日志拦截器) return this; }
}
面试高频问题:
① 为什么 Retrofit 需要 Converter.Factory?自定义转换器如何实现?
- 答案:
Retrofit 通过工厂模式解耦 “网络字节流” 与 “业务对象” 的转换逻辑,例如:// 自定义Gson转换器(支持LocalDateTime序列化) public class CustomGsonConverterFactory extends Converter.Factory { private final Gson gson; public static CustomGsonConverterFactory create() { return create(new GsonBuilder() .registerTypeAdapter(LocalDateTime.class, new LocalDateTimeAdapter()) .create()); } // 重写requestBodyConverter和responseBodyConverter方法 } // 使用时通过建造者添加 Retrofit retrofit = new Retrofit.Builder() .addConverterFactory(CustomGsonConverterFactory.create()) .build();
② Retrofit 为什么推荐复用 OkHttpClient 实例?与建造者模式的关系?
- 性能原因:OkHttpClient 内部的连接池、DNS 缓存等是重量级资源,重复创建会导致性能下降。
- 建造者实践:
// 全局单例OkHttpClient(通过建造者配置) private static final OkHttpClient OK_HTTP_CLIENT = new OkHttpClient.Builder() .connectTimeout(15, SECONDS) .addInterceptor(new LoggingInterceptor()) .build(); // Retrofit复用该实例 private static final Retrofit RETROFIT = new Retrofit.Builder() .baseUrl("https://api.example.com") .client(OK_HTTP_CLIENT) .addConverterFactory(GsonConverterFactory.create()) .build();
3. OkHttp vs Retrofit 建造者模式对比(面试必考点)
特性 | OkHttpClient.Builder | Retrofit.Builder |
---|---|---|
核心职责 | 配置网络底层(连接、拦截器、证书、连接池) | 配置接口转换(BaseUrl、数据转换器、回调适配) |
不可变对象 | 构建后OkHttpClient 所有属性不可变 | 同理,Retrofit 实例构建后不可变 |
依赖关系 | 独立配置,可被 Retrofit.Builder 引用 | 必须依赖 OkHttpClient(或使用默认实例) |
链式调用核心 | 网络参数的 “物理层” 配置(如超时、代理) | 业务层抽象(如将接口方法转为 HTTP 请求) |
面试陷阱 | 忘记设置readTimeout 导致 Socket 永久阻塞 | 未添加数据转换器导致ClassCastException |
Android 场景如何区分?
解答:
- 工厂模式:适合 “一键创建” 简单对象,如
LayoutInflater.inflate()
直接生成 View。 - 建造者模式:适合 “分步配置” 复杂对象,如配置一个带有拦截器、超时时间、缓存策略的 OkHttpClient(需多个可选参数组合)。
四、Android 面试高频综合题:设计模式与性能 / 内存优化结合
1. 单例持有 Activity Context 为什么会导致内存泄漏?如何避免?(阿里、美团面试真题)
解答:
- 原理:单例是全局静态实例,若持有非静态的 Activity Context,当 Activity 销毁后,单例的强引用会阻止 Activity 被回收,导致内存泄漏。
- 解决方案:
- 单例中使用 Application Context(生命周期与 App 一致)。
- 若必须持有 Activity Context,可使用 弱引用(但需注意空指针问题,非推荐方案)。
2. 线程池工厂模式如何避免 “幽灵线程”?(字节跳动面试真题)
- 问题场景:线程异常终止后,线程池通过工厂创建新线程,但未记录历史,导致调试困难。
- 解决方案:
// 在ThreadFactory中添加线程创建序号和业务标签 public Thread newThread(Runnable r) { Thread thread = new Thread(r, String.format("%s-%d", namePrefix, counter.getAndIncrement())); // 关键:设置线程上下文(如MDC,用于日志关联) MDC.put("thread_id", thread.getName()); return thread; }
3. OkHttp 建造者如何优化 HTTPS 性能?(阿里面试真题)
- 实战配置:
OkHttpClient client = new OkHttpClient.Builder() // 启用TLS 1.3(比1.2快50%握手时间) .sslSocketFactory(sslContext.getSocketFactory(), trustManager) .protocols(Arrays.asList(Protocol.TLS_1_3, Protocol.TLS_1_2)) // 配置连接池(长连接场景) .connectionPool(new ConnectionPool(50, 5, TimeUnit.MINUTES)) // 启用HTTP/2(需服务端支持) .addInterceptor(new OkHttp3.Http2CleartextInterceptor()) .build();
4. Retrofit 建造者如何处理多环境配置(开发 / 测试 / 生产)?
- 工厂方法 + 建造者组合模式:
public class RetrofitFactory { private static final Map<String, Retrofit> retrofitMap = new HashMap<>(); public static Retrofit getRetrofit(String env) { if (!retrofitMap.containsKey(env)) { OkHttpClient client = new OkHttpClient.Builder() .addInterceptor(new EnvInterceptor(env)) // 根据环境切换BaseUrl .build(); Retrofit retrofit = new Retrofit.Builder() .client(client) .baseUrl(getBaseUrlByEnv(env)) .addConverterFactory(GsonConverterFactory.create()) .build(); retrofitMap.put(env, retrofit); } return retrofitMap.get(env); } }