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); } }
