2026年面试5

17 阅读15分钟

1、Android 每次启动Activity时,创建一个 static String b,Activity关闭后static b销毁了吗?每次启动的时创建的static String b是同一个吗

  1. Activity关闭后,static String b 销毁了吗?
    不会销毁。 它会一直存在于内存中,直到你的整个App进程被系统杀死
  2. 每次启动时创建的 static String b 是同一个吗?
    是同一个引用。 严格来说,不存在“每次创建”,它只在类首次被加载时创建一次。你之后的操作只是在修改这个唯一的、全局的 String 对象。

记住:static 变量属于类,生于类加载,死于进程结束。它是全局唯一的“单例”,修改它就是修改全局状态,用它直接引用 Activity 是常见的内存泄漏陷阱。

2、核心原理

核心原理static 变量是 “类变量” ,不属于任何对象实例,而属于定义它的 Activity 类本身。它的生命周期和类加载绑定。

class MyActivity : AppCompatActivity() {
    // 这行代码在 MyActivity 类首次被系统加载时执行(通常是你第一次打开这个Activity时)
    companion object {
        var b: String = "初始值"
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        b = "Activity ${System.currentTimeMillis()}" // 这只是修改,不是创建!
        println("当前b的值是: $b, 内存地址: ${System.identityHashCode(b)}")
    }
}
  • 第一次启动 MyActivity:类加载,静态变量 b 被创建并初始化为 “初始值”,随后在 onCreate 中被修改。
  • 关闭该 MyActivity 界面:b 纹丝不动,保留着最后被赋予的值。
  • 第二次、第三次…启动 MyActivity:不会再创建新的 b,你访问和修改的,始终是内存中的那同一个 String 变量。每次打印的内存地址(identityHashCode)将是相同的(除非你赋值了一个全新的 String 对象,但变量引用本身 b 还是那一个)。

3、我之前遇到的问题是,static b读取到老的值,没有读取到新的Activity赋值的新值,是怎么回事?

你遇到的这个问题非常典型,根本原因通常不是 static 变量“失效”了,而是由于 多线程环境下的“内存可见性”问题,或者代码执行的时序问题导致的。

情况一:多线程与内存可见性(最可能)

这是最核心、最常见的原因。在Java/Android中,为了性能,线程可能会将变量缓存到自己的本地内存(寄存器、工作内存) 中。如果一个线程(如UI主线程)更新了 static b,而另一个线程(如网络回调线程、IO线程)去读取,读取线程可能看不到最新的值,因为它读的是自己缓存里的旧副本。

关键概念:这违反了 “happens-before” 原则。简单的赋值操作不能保证对其他线程立即可见。

复现场景

class ProblemActivity : AppCompatActivity() {
    companion object {
        var b: String = "初始值" // ❌ 没有同步,多线程下可见性无法保证
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 主线程赋值
        b = "主线程设置的新值"

        // 在后台线程中读取
        thread {
            Thread.sleep(10) // 稍微延迟,确保赋值操作完成
            println("后台线程读取到的 b: $b") // 可能输出:“初始值” 或 “主线程设置的新值”
        }
    }
}

解决方案:确保变量的修改对所有线程立即可见

  1. 使用 @Volatile 注解(推荐)
companion object {
    @Volatile // 👈 保证可见性和部分有序性,读/写操作直接针对主内存
    var b: String = "初始值"
}

2.使用 AtomicReference (适合需要原子性操作时):

companion object {
    val b = AtomicReference<String>("初始值")
    // 赋值: b.set("新值")
    // 读取: b.get()
}
  1. 在同步块 (synchronized) 中读写

要彻底消除这种不确定性,你必须主动添加同步机制,向JVM和CPU声明“这个变量的访问需要特殊对待”:

工具解决的核心问题典型代码
@Volatile保证可见性,禁止指令重排序。一写多读场景的首选。@Volatile var flag = false
synchronized保证可见性原子性(互斥访问)。synchronized(lock) { counter++ }
Atomic使用CAS保证原子性,内部通常也处理了可见性。val atomicInt = AtomicInteger(0)

给你的直接建议:对于会被多线程访问的可变静态变量,无脑加上 @Volatile 是最简单有效的起步方案。这相当于给你的代码上了一道最基础的保险。

4、子线程中静态变量b做了哪些操作?

class ProblemActivity : AppCompatActivity() {
    companion object {
        var b: String = "初始值" // ❌ 没有同步,多线程下可见性无法保证
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 主线程赋值
        b = "主线程设置的新值"

        // 在后台线程中读取
        thread {
            Thread.sleep(10) // 稍微延迟,确保赋值操作完成
            println("后台线程读取到的 b: $b") // 可能输出:“初始值” 或 “主线程设置的新值”
        }
    }
}
操作层级具体操作说明与潜在问题
Java代码层面1. 读取变量 b 的值 2. 进行字符串拼接这是你直接看到的。println 需要获取 b 的当前值来构造输出字符串。
JVM内存模型层面 (关键)1. 从子线程的“工作内存”中读取缓存值 2. 工作内存与主内存同步 (非即时)如果没有 volatile 修饰,JVM不保证子线程的工作内存何时从主内存刷新数据。因此,读取到的可能是过期的缓存值
并发时序层面与主线程的“写操作”存在竞争关系子线程的 read 和主线程的 write 执行顺序不确定(尽管有sleep(10)),导致结果不可预测。
指令执行层面可能发生“指令重排序”为提高性能,编译器或CPU可能对读写指令进行重排,这可能加剧多线程下观察到的顺序混乱。

这行代码执行时,具体会发生以下步骤:

  1. 尝试读取:子线程需要执行 println,它必须先获取静态变量 b 的值。
  2. 查找缓存:线程会首先检查自己的本地工作内存(如CPU缓存) 中是否有 b 的副本。
  3. 决定读取源
    • 如果工作内存中有副本,JVM可能会直接返回这个缓存值(即使主内存中的值已被主线程更新)。这就是“内存不可见”问题。
    • 如果工作内存中没有,或由于某些屏障触发了同步,线程会去主内存读取最新的值。
  4. 执行后续操作:将读取到的值与字符串 “后台线程读取到的 b: ” 拼接,然后输出。

5、核心播放器对比类

Android中常用的视频播放内核主要包括MediaPlayer(系统原生)、ExoPlayer(Google开源)、IjkPlayer(基于FFmpeg)、VLC播放器等。

请对比MediaPlayer、ExoPlayer、IjkPlayer的主要区别和适用场景

参考答案

播放器核心特点优点缺点适用场景
MediaPlayerAndroid系统原生API系统级支持,兼容性好,无需额外依赖功能有限,扩展性差,不支持自定义解码简单播放场景,对包体积敏感
ExoPlayerGoogle开源,模块化设计功能丰富,扩展性强,支持HLS/DASH,更新活跃需要集成,包体积增加复杂播放需求,直播、自定义解码
IjkPlayer基于FFmpeg封装格式兼容性好,软硬解灵活切换集成复杂,维护成本较高特殊格式支持,跨平台需求
VLC跨平台播放器格式支持最全,功能强大包体积较大,性能开销大

6、ExoPlayer的架构设计是怎样的?各核心组件的作用是什么?

ExoPlayer采用组件化架构,核心组件包括:

  • Loader:负责数据加载(网络、本地文件),通过DataSource和DataSink抽象数据源
  • Renderer:负责音视频解码和渲染,包括VideoRenderer、AudioRenderer、TextRenderer等
  • TrackSelector:选择音视频轨道,支持自适应码率切换
  • MediaSource:封装媒体数据源,如HlsMediaSource、DashMediaSource、ProgressiveMediaSource
  • Player:对外暴露播放控制接口,协调各组件工作

工作流程:Player通过MediaSource获取数据,TrackSelector选择轨道,Loader加载数据,Renderer进行解码渲染。

7、 MediaPlayer的播放流程是怎样的?

// 典型流程
MediaPlayer mp = new MediaPlayer();
mp.setDataSource(url);          // 设置数据源
mp.prepareAsync();              // 异步准备
mp.setOnPreparedListener(() -> {
    mp.start();                 // 开始播放
});
// 控制:pause()、stop()、seekTo()、release()

关键状态:Idle→Initialized→Preparing→Prepared→Started→Paused→Stopped→End

8、硬解码与软解码的区别?如何选择?

  • 硬解码:通过MediaCodec调用硬件解码器(GPU/专用芯片),功耗低、效率高,但兼容性受限(需设备支持)
  • 软解码:使用CPU进行解码,兼容性好但功耗高、性能差
  • 选择策略
    • 优先尝试硬解码(性能好)
    • 硬解码失败时降级到软解码
    • 特殊格式(如AV1)可能只有软解码支持
    • 可根据设备性能动态选择(高性能设备用硬解)

9、如何优化视频播放的启动速度?

  • 预加载:使用ExoPlayer的preload()提前加载部分数据
  • 边下边播:HLS/DASH分片加载,无需等待完整文件
  • 缓冲区优化:设置合适的缓冲区大小(minBufferMs、maxBufferMs)
  • 首帧优化:使用thumbnail()快速显示缩略图
  • 硬件加速:使用SurfaceView/TextureView的硬件加速渲染
  • 网络优化:使用CDN加速,开启HTTP缓存

10、如何减少视频播放的内存占用?

  • 及时释放:页面销毁时调用release()释放MediaPlayer资源
  • 复用实例:避免频繁创建播放器实例
  • 尺寸适配:根据显示区域设置合适的分辨率,避免解码过大Bitmap
  • SurfaceView优势:SurfaceView使用独立图层,不占用应用内存
  • 解码格式:使用RGB_565格式(2字节/像素)替代ARGB_8888(4字节/像素)

11、如何监控和优化播放卡顿?

参考答案

  • 监控指标:卡顿次数、缓冲时间、首帧时间、丢帧率
  • 卡顿原因:网络带宽不足、解码性能瓶颈、缓冲区设置不合理
  • 优化措施
    • 网络:使用CDN加速、开启HTTP缓存、启用GZIP压缩
    • 解码:优先硬解码,降码率播放
    • 缓冲区:动态调整缓冲区大小,网络差时增大缓冲
    • 列表优化:RecyclerView滑动时暂停加载

12、如何支持不常见的视频格式?

参考答案

  • 方案一:集成FFmpeg库,通过JNI调用进行软解码
  • 方案二:使用IjkPlayer或VLC等第三方播放器
  • 方案三:自定义MediaCodec解码器(需要了解编解码原理)
  • 方案四:服务器端转码为标准格式(如H.264)

13、如何实现视频列表的预加载和缓存?

参考答案

  • 预加载策略

    • RecyclerView滑动时,预加载相邻item的视频
    • 使用ExoPlayer的MediaSource预加载机制
    • 控制并发播放器数量(通常1-2个)
  • 缓存机制

    • 内存缓存:使用LruCache缓存解码后的Bitmap
    • 磁盘缓存:缓存原始视频文件或分片
    • ExoPlayer内置SimpleCache组件

14、常见的视频封装格式和编码格式有哪些?

参考答案

  • 封装格式:MP4(最常用)、MKV、AVI、TS、HLS(.m3u8)、DASH(.mpd)
  • 视频编码:H.264(AVC,最普及)、H.265(HEVC,压缩率高)、VP9、AV1(新兴编码)
  • 音频编码:AAC(最常用)、MP3、Opus、FLAC

15、请说明Android屏幕适配的主要挑战和解决方案

主要挑战

  • 屏幕尺寸多样:从4寸到7寸以上,显示区域差异大
  • 分辨率碎片化:从480×800到4K,像素密度(dpi)跨度大
  • 宽高比不同:16:9、18:9、19.5:9、21:9等比例
  • 异形屏/刘海屏:状态栏、导航栏、挖孔屏等特殊区域

主流解决方案

  • 尺寸单位:dp(密度无关像素)用于控件尺寸,sp用于文字
  • 布局适配:ConstraintLayout(约束布局)、百分比布局、LinearLayout权重
  • 资源限定符:使用values-sw360dpvalues-hdpi等文件夹提供不同资源
  • 今日头条方案:修改density动态适配(但需谨慎使用)
  • 刘海屏适配:使用WindowInsets处理安全区域

16、dp、sp、px的区别和使用场景

参考答案

  • px(像素) :绝对单位,不同屏幕显示大小不同,不推荐直接使用
  • dp(密度无关像素) :基于160dpi屏幕的虚拟单位,1dp在160dpi屏幕上=1px,在320dpi=2px,用于控件尺寸、边距等
  • sp(缩放像素) :与dp类似,但会跟随系统字体大小设置变化,用于文字大小

17、如何实现不同屏幕尺寸的布局适配?

多维度适配方案

  • 布局文件:使用layout-sw360dplayout-land等限定符文件夹
  • 尺寸文件dimens.xml在不同values-swXXXdp文件夹中定义不同值
  • 代码动态适配:根据屏幕宽高动态计算控件尺寸(需谨慎)
  • 布局选择
    • ConstraintLayout:通过约束关系实现复杂布局,适配性好
    • LinearLayout权重:简单等分场景
    • 百分比布局:已废弃,可用ConstraintLayout替代

示例:在values-sw360dp/dimens.xml中定义小屏尺寸,在values-sw480dp中定义大屏尺寸

18、 如何适配多语言(国际化)?

实现方案

  • 资源文件:创建values-zhvalues-en等文件夹,提供不同语言的strings.xml
  • 系统语言切换:系统切换语言后,Activity会重建,自动加载对应资源
  • 代码中获取资源:使用getString(R.string.xxx)而非硬编码
  • 特殊语言处理:阿拉伯语(RTL布局)、繁体中文等需特殊处理

注意事项

  • 避免在代码中拼接字符串(如"错误:" + errorCode)
  • 使用%s%d等占位符,在strings.xml中定义格式
  • 测试不同语言下的布局显示(如德语单词较长)

19、Android toast如果短时间内频繁显示,怎么优化

优化的核心思路是复用 Toast 对象,避免重复创建

  1. 单例模式管理 Toast 通过一个全局的 Toast 实例,在需要显示新内容时,更新其文本而非创建新对象。这能有效防止消息队列堆积
public class ToastUtil {
    private static Toast mToast;

    public static void showToast(Context context, String message) {
        if (mToast == null) {
            // 如果Toast实例不存在,就创建一个
            mToast = Toast.makeText(context.getApplicationContext(), message, Toast.LENGTH_SHORT);
        } else {
            // 如果已存在,则只更新文本
            mToast.setText(message);
        }
        mToast.show();
    }
}

2.引入状态标志位

在单例基础上,增加一个标志位(如 isShowing)来判断当前是否有 Toast 正在显示。这样可以确保在上一个 Toast 消失前,新的显示请求不会立即中断当前显示,而是直接更新内容,实现“无缝切换”。

private boolean isShowingToast = false;
private Handler mHandler = new Handler(Looper.getMainLooper());

private void showToast(String msg) {
    if (isShowingToast) {
        return; // 如果正在显示,直接返回,不重复触发
    }
    // ... 设置Toast文本 ...
    mToast.show();
    isShowingToast = true;
    // 根据Toast.LENGTH_SHORT或LENGTH_LONG设置延时重置标志位
    mHandler.postDelayed(() -> isShowingToast = false, 2000); // LENGTH_SHORT约2秒
}

20、处理快速连续点击

对于按钮的快速连续点击,可以在点击事件中加入时间戳判断,过滤掉过快的请求.

private long mLastClickTime = 0;
public boolean isFastDoubleClick() {
    long time = System.currentTimeMillis();
    if (time - mLastClickTime < 500) { // 500毫秒内视为快速连续点击
        return true;
    }
    mLastClickTime = time;
    return false;
}

21、注意生命周期和内存泄漏

  • 在 Activity 的 onDestroy()onPause()方法中,及时调用 Toast 的 cancel()方法取消显示,并清理 Handler 中的消息,避免内存泄漏。
  • 尽量使用 Application Context来创建 Toast,避免因持有 Activity 上下文导致的内存泄漏。

22、为什么webView加载会慢呢?

WebView加载慢的主要原因包括网络请求优化不足、渲染流程复杂、资源加载策略不当、以及WebView自身初始化开销等几个核心方面。

网络层面因素

1. DNS解析耗时

WebView加载网页时,需要先解析域名到IP地址,如果DNS服务器响应慢或存在DNS污染,会显著延长首屏时间。

2. 连接建立开销

每个HTTP请求都需要经过TCP三次握手、TLS握手(HTTPS)等过程,如果页面资源多且分散,握手开销累积明显。

3. 资源请求阻塞

  • 主文档阻塞:HTML主文档未加载完成前,无法解析后续资源请求
  • 并发限制:浏览器对同一域名有并发请求限制(通常6个),资源多时需排队
  • 资源依赖:CSS/JS文件可能阻塞渲染,导致页面白屏时间长

4. 资源体积过大

未压缩的图片、未精简的JS/CSS、未启用GZIP压缩等,都会增加传输时间。

23、Java中有内存泄漏吗

静态集合类滥用​ 静态集合(如static Map)的生命周期与应用程序一致。如果不断向其中添加对象(例如缓存用户数据)却从不移除,这些对象会一直占用内存。

未关闭的资源​ 如数据库连接、文件流、网络连接等,如果只用不关,它们不仅占用的Java对象不会被回收,其背后的 native 资源也无法释放。务必使用 try-with-resources语句来管理。

监听器和回调未注销​ 在GUI应用或事件驱动架构中,向中心管理器注册了监听器后,如果在对象销毁时没有注销,管理器会一直持有该对象的引用,导致其无法被回收。

ThreadLocal使用不当ThreadLocal变量与特定线程绑定。在使用线程池时,线程会被复用。如果使用完ThreadLocal后没有调用remove()方法,那么之前设置的值会一直驻留在内存中,可能造成信息交叉污染和内存泄漏。

内部类持有外部类引用​ 非静态内部类(包括匿名内部类)会隐式持有其外部类对象的引用。如果这个内部类的生命周期长于外部类(例如,被一个静态集合引用),就会导致外部类实例无法被回收,即使它已不再需要。

哈希表键的哈希值改变​ 如果一个对象被用作HashSetHashMap的键之后,其参与计算哈希值的字段被修改,你将无法再通过这个修改后的键或原来的键找到并删除该对象。这个“孤儿”对象会一直留在哈希表中。

24、应用怎么判断自己处于前台还是后台?

方法原理优势劣势适用场景
ActivityLifecycleCallbacksApplication中注册全局回调,统计活跃 Activity数量。当计数为 0 时,应用进入后台。官方支持,准确性高,无需额外权限。需手动管理计数逻辑,需处理配置变更(如屏幕旋转)等边界情况。最通用、最推荐的方案,适用于绝大多数应用。
ProcessLifecycleOwner使用 Jetpack Lifecycle组件,直接监听整个应用进程的生命周期。代码简洁,免于手动计数,专为应用前后台监听设计。依赖 Android Jetpack 库,监听粒度较粗,无法感知单个 Activity的变化。只需简单监听应用整体前后台状态,且项目已使用 Jetpack。
RunningAppProcessInfo通过 ActivityManager获取进程列表,判断自身进程的 importance属性是否为前台级别。无需依赖 ApplicationActivity上下文。在 Android 5.0 (API 21) 及以上系统已不可靠,系统限制使其无法准确判断。不推荐在新项目中使用。