Android 互联网大厂,高频重点面试题集分享(四)

74 阅读13分钟

一、Android Lifecycle 知识点实战

1.1、说说下面的代码有什么问题,如何修改它?

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        supportFragmentManager
           .beginTransaction()
           .replace(R.id.container, MainFragment())
           .commit()
   }
}

问题:

  • 如果Activity被意外杀死并恢复,会再次执行 onCreate() 方法创建新的 Fragment ,会导致栈中存在2个Fragment 。即在Fragment 上的任何操作都有可能被执行两次,这会导致出现奇怪的问题。
graph LR
    Activity -->|To Be Restored| Fragment1
    Activity -->|Created| Fragment2

修改:

防止 Activity 因意外被杀死而恢复,导致添加新的 Fragment,可以使用 stateInstanceState == null 作为判断条件,防止添加新的 Fragment,因此可以将上面的代码简单修改如下:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        if (savedInstanceState == null) {
            supportFragmentManager
                .beginTransaction()
                .replace(R.id.container, MainFragment())
                .commit()
        }
   }
}

1.2、如何为Fragment构造函数中添加参数,说说下面的代码有什么问题?

supportFragmentManager
    .beginTransaction()
    .replace(R.id.container, FindFragment())
    .commit()
    
class FindFragment(private val repository: Repository): Fragment() {
    
}    

问题:

  • 在某些情况下系统需要重新初始化它,比如应用配置更改、设备被旋转等,会导致Fragment被销毁,如果没有默认空参构造函数,系统就会不知道如何重新初始化Fragment实例。
graph LR
    Activity -->|Creation Fragment| FindFragment
    Activity -->|State Restoration Fragment| FindFragment
  • 因此,我们总是需要实体化Fragment的时候有一个空的构造函数。

以下回答可供参考:

不应该直接用带参数的构造函数实例化任何 Fragment(),如果想使用带参数的构造函数实例化 Fragment(),可以使用 FragmentFactory 解决这个问题,这是在 AndriodX Fragment 1.2.0 中新增加的 API,详情可参考以下提供的文章:

如果不使用 AndriodX Fragment 库,默认情况下系统是不支持的,上面的代码虽然可以正常编译运行,但是在运行过程当中,因为配置更改,导致在销毁恢复的过程中会崩溃,错误信息如下所示:

Caused by: java.lang.NoSuchMethodException: FindFragment.<init>

1.3、直接实例化 ViewModel,下面的代码会有什么问题?

class TestActivity: AppCompatActivity() {
    private val viewModel = TestViewModel()
}

class  TestViewModel(): ViewModel {

}

问题:

  • 不应该直接实例化 ViewModelViewModel 是 Jetpack 架构组件成员之一,意味着它可以在配置更改时存活,如设备旋转时,它比 Activity 有更长的生命周期。
  • 在代码中直接实例化 ViewModel,尽管它可以工作,但它将会变成一个普通的 ViewModel,失去原本拥有的特性。

修改:

  • 要实例化 ViewModel,建议使用 ViewModel KTX,代码如下所示:
class TestActivity: AppCompatActivity() {
    private val viewMode:TestViewModel by viewModels()
}
  • by viewModels () 会在重新启动或从已杀死的进程中恢复时,实例化一个新的 ViewModel。如果有配置更改,例如设备被旋转时,它将检查 ViewModel 是否已经创建,而不重新创建它

  • ViewModels() 会根据需要自动注入 SavedInstancestate (如 Activity 中的 SavedInstanceStateIntent),如果我们有其他依赖可通过 Dagger Hilt 注入,它将与 ViewModel 一起使用下面的参数:

@HiltViewModel
class CustomViewModel @Inject constructor(
    private val repository: Repository,
    savedStateHandle: SavedStateHandle
) : ViewModel {
    
}

1.4、Jetpack 架构组件提供的 ViewModel 的作用是什么?

ViewModel 是 Android 架构的核心组件,通过生命周期感知、数据管理解耦和响应式设计,显著提升应用健壮性、可维护性和用户体验。

以下回答可供参考:

  • 1. 生命周期感知的数据管理

    • 配置变化时保留数据:当屏幕旋转或系统配置变更导致 Activity/Fragment 重建时,ViewModel 不会随之销毁,从而避免重新加载数据,提升用户体验。
    • 生命周期绑定ViewModel 自动绑定到 UI 控制器的生命周期,确保数据在正确时机释放(如 Activity 完全销毁时调用 onCleared())。
  • 2. 关注点分离(Separation of Concerns)

    • 解耦 UI 与业务逻辑:将数据操作和业务逻辑从 Activity/Fragment 剥离到 ViewModel 中,使 UI 控制器专注于处理视图和用户交互。
    • 代码可维护性:减少 UI 控制器的臃肿,提升代码可读性和可维护性。
  • 3. 数据共享与通信

    • 跨 Fragment 共享数据:通过 Activity 作用域的 ViewModel,多个 Fragment 可共享同一数据源,避免冗余请求或复杂回调。
    • 结合 LiveData 实现响应式 UIViewModel 通常与 LiveData 配合,实现数据变化自动驱动 UI 更新,减少手动同步的样板代码。
  • 4. 避免内存泄漏

    • 不持有 UI 引用ViewModel 不直接持有 Activity/Fragment 的引用,避免因生命周期不一致导致的内存泄漏。
    • 安全使用 Context:如需 Context,应使用 AndroidViewModel 获取 Application 上下文,而非关联 Activity。
  • 5. 提升可测试性

    • 独立于 UI 测试:业务逻辑集中在 ViewModel 中,可通过单元测试直接验证,无需依赖 Android 框架或 UI 组件。

1.5、Jetpack 架构组件提供的 LiveData 的作用是什么。

LiveData 的核心价值在于:

  • 安全:通过生命周期感知避免内存泄漏和无效更新。
  • 高效:简化数据到 UI 的同步逻辑,减少样板代码。
  • 灵活:支持数据转换、组合,并与 Jetpack 组件(如 ViewModelRoom)无缝协作。

它是构建响应式、健壮且可维护的 Android 应用的关键工具,尤其适合处理与 UI 直接相关的数据流。

以下回答可供参考:

  • 1. 生命周期感知的数据更新

    • 自动管理观察者的生命周期
      LiveData 仅在观察者(如 Activity/Fragment)处于活跃状态STARTED 或 RESUMED)时通知数据变化,避免在后台更新 UI 导致的崩溃或资源浪费。

    • 自动解除绑定
      当观察者的生命周期结束(如 Activity 销毁),LiveData 自动移除观察者,避免内存泄漏

  • 2. 响应式 UI 更新

    • 数据驱动 UI
      当数据源(如 ViewModel 中的数据)发生变化时,LiveData 自动通知所有活跃的观察者,触发 UI 更新,无需手动调用 findViewById 或 notifyDataSetChanged
    • 与 ViewModel 配合
      结合 ViewModel 实现业务逻辑与 UI 的解耦,确保数据在配置变更(如屏幕旋转)时持久化,并在 UI 重建后立即推送最新数据。
  • 3. 数据共享与一致性

    • 单数据源
      多个观察者(如多个 Fragment)可共享同一 LiveData 实例,确保所有 UI 组件显示的数据始终一致。
    • 粘性事件支持
      新注册的观察者会立即收到最后一次更新的数据,避免因延迟导致的空白界面。
  • 4. 简化异步操作

    • 配合协程或 Room 使用
      例如,Room 数据库可直接返回 LiveData 对象,当数据库变化时,自动触发 UI 更新,无需手动查询或回调。
    • 避免回调地狱
      通过观察 LiveData 替代传统回调,使异步数据流的处理更直观。
  • 5. 数据转换与组合

    • 使用 Transformations 操作数据流
      通过 mapswitchMap 等方法对 LiveData 进行转换,实现链式数据处理(如将数据库实体映射为 UI 模型)。
    • 合并多个数据源
      例如,通过 MediatorLiveData 监听多个 LiveData 的变化,并组合成统一的数据流。
  • 6. 提升可测试性

    • 隔离 UI 与数据逻辑
      在测试中可单独验证 LiveData 的行为,无需依赖 Android 组件。
    • 支持 TestObserver
      通过工具类(如 observeForTesting)直接获取 LiveData 的值,简化单元测试。
  • 与其他技术的对比

特性LiveDataRxJava/Flow
生命周期感知原生支持需手动绑定(如 LifecycleScope
学习成本高(需掌握操作符、线程调度等)
适用场景简单 UI 数据流复杂异步流、多线程操作
数据转换能力基础(Transformations强大(丰富操作符)

1.6、例举一个场景,实例的视图被破坏了,但实例(Activity/Fragment)还存在。

以下回答可供参考:

  • Activity 总是与其视图一起被销毁。因此,在 Activity 中没有 onDestroyView () 生命周期方法。
  • 只有在 Fragment 中有 onDestroyView () 生命周期方法。在大多数情况下 Fragment 和它的视图一起被销毁。
  • 通过 Fragment transaction 用一个 Fragment 替换另一个 Fragment 时,栈下面的 Fragment 仍然存在,但是它的视图被破坏了。
  • 当一个 Fragment 被另一个 Fragment 替换时,会调用 onDestroyView ()  方法,但不会调用 onDestroy ()  或 onDetect ()  方法。正因为这种复杂性,在使用 Fragment 时,会遇到许多奇怪的问题。

1.7、在应用中使用协程,如何确保它们的生命周期可感知?

以下回答可供参考:

使用repeatOnLifecycle提供额外的作用域。

  • 因为对于普通视图,即使 lifecycleScope 是可用的,它在 Activity 或 Fragment 的整个生命周期中都处于活动状态。但有时我们希望某些特定场景只在 onStart() 或 onResume() 之后处于活动状态。

示例代码如下:

lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.stateFlowValue.collect {
            // Do something
        }
    }
}

使用 collectAsStateWithLifecycle () 和 stateFlow 中的 WhileSubscribed (...),确保只在 Activity 或 Fragment 处于正确的生命周期。

  • 因为对于组合视图 collectAsState() 不会确保在组合函数处于活动状态时安全使用数据,它也不会停止继续发送 StateFlow,这会导致资源浪费。

  • 可参考collectAsStateWithLifecycle()源码,你会发现它也在使用 repeatOnLifecycle(…)

二、使用Handler常见的内存泄漏场景和解决方案

2.1、Handler延迟消息的“定时炸弹”

泄漏过程:

// 发送10秒延迟消息后立即关闭Activity    
mHandler.sendEmptyMessageDelayed(0, 10000);    
finish();
  • Message持有Handler引用 → Handler持有Activity引用  
  • nativePollOnce阻塞机制:消息未处理完成时,MessageQueue通过epoll机制阻塞主线程

预防策略:

@Override    
protected void onDestroy() {    
    // 清空所有消息(包括延迟消息)    
    mHandler.removeCallbacksAndMessages(null);    
    super.onDestroy();    
}

2.2、匿名内部类Handler(新手坟场)

泄漏原理:

// 致命写法!直接导致Activity泄漏    
private Handler mHandler = new Handler() {    
    @Override    
    public void handleMessage(Message msg) {    
        updateUI(); // 隐式持有外部Activity引用    
    }    
};
  • 非静态内部类自动持有外部类引用(编译器自动注入this指针)  
  • Looper生命周期=应用生命周期,导致Handler持续存活

解决方案:

// 静态内部类+弱引用双保险    
private static class SafeHandler extends Handler {    
    private WeakReferencemActivityRef;    
  
    SafeHandler(Activity activity) {    
        mActivityRef = new WeakReference<>(activity);    
    }    
  
    @Override    
    public void handleMessage(Message msg) {    
        Activity activity = mActivityRef.get();    
        if (activity == null || activity.isDestroyed()) return;    
        // 安全操作UI    
    }    
}

2.3、静态Handler的“伪装者”

示例代码:

// 错误!静态Handler仍可能持有Activity    
private static Handler sHandler;    
void init() {    
    sHandler = new Handler(Looper.getMainLooper()) {    
        @Override    
        public void handleMessage(Message msg) {    
            // 隐式持有Activity的View    
            mTextView.setText("Hello");    
        }    
    };    
}
  • 静态变量间接持有UI组件 → 组件持有Activity → 泄漏链依然存在

解决方案:

// 结合Lifecycle组件自动解绑    
class LifecycleHandler(    
    lifecycle: Lifecycle,    
    looper: Looper    
) : Handler(looper), LifecycleObserver {    
  
    init {    
        lifecycle.addObserver(this)    
    }    
  
    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)    
    fun onDestroy() {    
        removeCallbacksAndMessages(null)    
    }    
}

2.4、Handler同步屏障的“幽灵锁”

底层机制:

  • 同步屏障消息SyncBarrier):临时屏蔽同步消息,优先处理异步消息(如VSYNC信号)  
  • 危险操作:未及时调用removeSyncBarrier()导致主线程永久阻塞

泄漏表现:

// 系统API添加同步屏障(开发者无法直接调用)    
ViewRootImpl.postSyncBarrier();    
// 若异步消息处理完毕后未移除屏障→主线程无限阻塞
  • MessageQueue持续阻塞 → Handler引用链无法断开Activity泄漏

检测工具:

# 使用Systrace观察主线程状态    
python systrace.py looper -t 10

2.5、Handler跨线程的“死亡握手”

链路示例:

子线程Handler发送消息 → 主线程MessageQueue未处理 → 子线程强引用Activity
  • 跨线程引用导致GC Roots链异常  
  • Native层管道阻塞:nativePollOnceepoll_wait未唤醒时,消息可能跨线程持有对象

解决方案:

// 使用WeakReference包装跨线程对象    
Message msg = Message.obtain(mHandler, () -> {    
    WeakReferenceref = new WeakReference<>(activity);    
    // ...    
});

2.6、如何设计永不泄漏的Handler?

示例代码:

public class SafeHandler extends Handler {    
    private final WeakReferencemContextRef;    
    private final WeakReferencemCallbackRef;    
  
    public SafeHandler(Context context, Callback callback) {    
        super(Looper.getMainLooper());    
        mContextRef = new WeakReference<>(context);    
        mCallbackRef = new WeakReference<>(callback);    
    }    
  
    @Override    
    public void handleMessage(Message msg) {    
        if (mContextRef.get() == null || mCallbackRef.get() == null) return;    
        mCallbackRef.get().handleMessage(msg);    
    }    
}    
  
// 配合Lifecycle自动清理    
lifecycle.addObserver(new LifecycleObserver() {    
    @OnLifecycleEvent(Lifecycle.Event.ON_STOP)    
    void onStop() {    
        safeHandler.removeCallbacksAndMessages(null);    
    }    
});

2.7、nativePollOnce的阻塞机制如何导致泄漏?

问题剖析:

  • 1. epoll_wait系统调用:主线程通过Linux的epoll机制进入休眠状态

  • 2. 唤醒条件

    • 新消息入队时写入管道(write(mWakeWritePipeFd, "W", 1))
    • 同步屏障未移除时,即使无消息也会持续阻塞
  • 3. 阻塞期间Handler存活 → 若此时关闭Activity,Message→Handler→Activity的引用链无法断开

三、Glide的缓存机制深度解析和性能优化

3.1、Glide三级缓存架构核心原理

Glide默认采用内存缓存(ActiveResources+MemoryCache) + 磁盘缓存(DiskCache) + 网络加载的三级架构:  

  • ActiveResources:强引用缓存,存储正在展示的图片(防GC回收)  
  • MemoryCache:LRU内存缓存,默认占App可用内存的1/8  
  • DiskCache:LRU磁盘缓存,支持DATA(原始数据)和RESOURCE(解码后数据)两种策略

3.2、Glide默认配置的致命缺陷

  • 内存缓存僵化:固定比例分配,无法适配不同机型(如6GB与12GB内存手机)
  • 磁盘缓存混存:原始数据和转换后数据混杂,空间利用率低30%
  • 网络加载粗暴:无优先级管理,快速滑动时仍加载不可见图
  • 资源回收滞后SoftReference导致GC不及时,引发OOM

3.3、如何进行内存缓存动态扩容(LruCachen改造)?

痛点:低端机内存吃紧时频繁GC,高端机内存浪费

解决方案(示例代码):

  • 引入Bitmap尺寸权重系数  
  • 结合DisplayMetrics动态调整maxSize
class DynamicLruCache(context: Context) : LruCache<Key, Bitmap>(    
    // 根据设备内存动态计算    
    (Runtime.getRuntime().maxMemory() / 1024 / 8).toInt()    
) {    
    // 增加权重计算(大图占用更多缓存份额)    
    overridefunsizeOf(key: Key, value: Bitmap): Int {    
        return value.byteCount / 1024 * when {    
            value.width > 2000 -> 2  
            value.height > 1000 -> 1.5  
            else -> 1  
        }    
    }    
}    
  
// 配置到GlideModule    
builder.setMemoryCache(DynamicLruCache(context))

3.4、磁盘缓存分区优化(DiskLruCache改造)

痛点:用户头像与高清大图混合存储,缓存命中率低

使用分层存储方案:

  • 按业务场景划分存储池  
  • 采用AES-256加密敏感缩略图
// 创建不同存储池    
DiskCachesmallImageCache= DiskLruCacheWrapper.create(    
    newFile(context.getCacheDir(), "small"),    
    20 * 1024 * 1024// 20MB    
);    
  
DiskCachelargeImageCache= DiskLruCacheWrapper.create(    
    newFile(context.getCacheDir(), "large"),    
    100 * 1024 * 1024// 100MB    
);    
  
// 根据URL特征路由    
if (url.contains("/avatar/")) {    
    return smallImageCache;    
} elseif (url.contains("/wallpaper/")) {    
    return largeImageCache;    
}

3.5、网络预加载智能降级

痛点:快速滑动时仍加载不可见图,浪费流量

加载策略:

  • 基于RecyclerView滑动速度动态调整优先级  
  • 高速滑动时加载缩略图,停止后替换高清图
Glide.with(context)    
    .load(url)    
    .apply(    
        RequestOptions()    
            // 根据滑动速度动态调整优先级    
            .priority(    
                when (scrollSpeed) {    
                    in0..2000 -> Priority.HIGH    
                    in2001..5000 -> Priority.NORMAL    
                    else -> Priority.LOW    
                }    
            )    
            // 开启智能降级    
            .override(    
                if (scrollSpeed > 3000) 100else Target.SIZE_ORIGINAL    
            )    
    )

3.6、Glide性能优化篇:混合预加载--内存预热

  • 可提升首屏加载速度40%  
  • 利用系统空闲时段更新缓存
/ 在Application初始化时预加载关键资源    
Glide.with(context)    
    .load(Urls.CRITICAL_IMAGES)    
    .preload(200200);    
  
// 结合JobScheduler在充电时预热    
JobSchedulerscheduler= (JobScheduler) context.getSystemService(JOB_SCHEDULER_SERVICE);    
JobInfojobInfo=newJobInfo.Builder(1,    
    newComponentName(context, PreloadService.class))    
    .setRequiresCharging(true)    
    .build();    
scheduler.schedule(jobInfo);

3.7、Glide性能优化篇:混合预加载--磁盘缓存冷热分离

  • 热数据区:保留最近3天访问记录(SSD加速)  
  • 冷数据区:存储历史数据(HDD大容量)  
  • 淘汰策略:热区用LRU,冷区用LFU

3.8、Glide如何防止加载大图导致OOM?

常规答案+优化方案:

  • 1、常规方案:

    • 根据ImageView尺寸自动计算采样率 
    • 采用BitmapPool复用内存  
  • 2. 优化方案:

// 强制限制解码尺寸    
.override(deviceWidth, deviceHeight)    
// 开启硬件加速解码    
.format(DecodeFormat.PREFER_RGB_565)    
// 大图分块加载    
.set(Downsampler.ALLOW_HARDWARE_DECODE_CONFIG, true)

3.9、LruCache和DiskLruCache如何实现线程安全?

LruCache:

  • 使用LinkedHashMap+同步锁 
  • trimToSize()时计算权重  

DiskLruCache:

  • 通过Journal日志文件保证原子性 
  • 采用Double-check Locking优化读写锁

更多分享

  1. Android 互联网大厂,高频重点面试题集分享(一)
  2. Android 互联网大厂,高频重点面试题集分享(二)
  3. Android 互联网大厂,高频重点面试题集分享(三)
  4. Android 架构以及优化相关面试题分享
  5. Android Kotlin协程相关面试题分享