一、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 {
}
问题:
- 不应该直接实例化
ViewModel
,ViewModel
是 Jetpack 架构组件成员之一,意味着它可以在配置更改时存活,如设备旋转时,它比Activity
有更长的生命周期。 - 在代码中直接实例化
ViewModel
,尽管它可以工作,但它将会变成一个普通的ViewModel
,失去原本拥有的特性。
修改:
- 要实例化
ViewModel
,建议使用ViewModel KTX
,代码如下所示:
class TestActivity: AppCompatActivity() {
private val viewMode:TestViewModel by viewModels()
}
-
by viewModels ()
会在重新启动或从已杀死的进程中恢复时,实例化一个新的ViewModel
。如果有配置更改,例如设备被旋转时,它将检查ViewModel
是否已经创建,而不重新创建它 -
ViewModels()
会根据需要自动注入SavedInstancestate
(如 Activity 中的SavedInstanceState
和Intent
),如果我们有其他依赖可通过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()
)。
- 配置变化时保留数据:当屏幕旋转或系统配置变更导致 Activity/Fragment 重建时,
-
2. 关注点分离(Separation of Concerns)
- 解耦 UI 与业务逻辑:将数据操作和业务逻辑从 Activity/Fragment 剥离到
ViewModel
中,使 UI 控制器专注于处理视图和用户交互。 - 代码可维护性:减少 UI 控制器的臃肿,提升代码可读性和可维护性。
- 解耦 UI 与业务逻辑:将数据操作和业务逻辑从 Activity/Fragment 剥离到
-
3. 数据共享与通信
- 跨 Fragment 共享数据:通过 Activity 作用域的
ViewModel
,多个 Fragment 可共享同一数据源,避免冗余请求或复杂回调。 - 结合 LiveData 实现响应式 UI:
ViewModel
通常与LiveData
配合,实现数据变化自动驱动 UI 更新,减少手动同步的样板代码。
- 跨 Fragment 共享数据:通过 Activity 作用域的
-
4. 避免内存泄漏
- 不持有 UI 引用:
ViewModel
不直接持有 Activity/Fragment 的引用,避免因生命周期不一致导致的内存泄漏。 - 安全使用 Context:如需 Context,应使用
AndroidViewModel
获取Application
上下文,而非关联 Activity。
- 不持有 UI 引用:
-
5. 提升可测试性
- 独立于 UI 测试:业务逻辑集中在
ViewModel
中,可通过单元测试直接验证,无需依赖 Android 框架或 UI 组件。
- 独立于 UI 测试:业务逻辑集中在
1.5、Jetpack 架构组件提供的 LiveData
的作用是什么。
LiveData
的核心价值在于:
- 安全:通过生命周期感知避免内存泄漏和无效更新。
- 高效:简化数据到 UI 的同步逻辑,减少样板代码。
- 灵活:支持数据转换、组合,并与 Jetpack 组件(如
ViewModel
、Room
)无缝协作。
它是构建响应式、健壮且可维护的 Android 应用的关键工具,尤其适合处理与 UI 直接相关的数据流。
以下回答可供参考:
-
1. 生命周期感知的数据更新
-
自动管理观察者的生命周期:
LiveData
仅在观察者(如 Activity/Fragment)处于活跃状态(STARTED
或RESUMED
)时通知数据变化,避免在后台更新 UI 导致的崩溃或资源浪费。 -
自动解除绑定:
当观察者的生命周期结束(如 Activity 销毁),LiveData
自动移除观察者,避免内存泄漏。
-
-
2. 响应式 UI 更新
- 数据驱动 UI:
当数据源(如ViewModel
中的数据)发生变化时,LiveData
自动通知所有活跃的观察者,触发 UI 更新,无需手动调用findViewById
或notifyDataSetChanged
。 - 与
ViewModel
配合:
结合ViewModel
实现业务逻辑与 UI 的解耦,确保数据在配置变更(如屏幕旋转)时持久化,并在 UI 重建后立即推送最新数据。
- 数据驱动 UI:
-
3. 数据共享与一致性
- 单数据源:
多个观察者(如多个 Fragment)可共享同一LiveData
实例,确保所有 UI 组件显示的数据始终一致。 - 粘性事件支持:
新注册的观察者会立即收到最后一次更新的数据,避免因延迟导致的空白界面。
- 单数据源:
-
4. 简化异步操作
- 配合协程或
Room
使用:
例如,Room
数据库可直接返回LiveData
对象,当数据库变化时,自动触发 UI 更新,无需手动查询或回调。 - 避免回调地狱:
通过观察LiveData
替代传统回调,使异步数据流的处理更直观。
- 配合协程或
-
5. 数据转换与组合
- 使用
Transformations
操作数据流:
通过map
、switchMap
等方法对LiveData
进行转换,实现链式数据处理(如将数据库实体映射为 UI 模型)。 - 合并多个数据源:
例如,通过MediatorLiveData
监听多个LiveData
的变化,并组合成统一的数据流。
- 使用
-
6. 提升可测试性
- 隔离 UI 与数据逻辑:
在测试中可单独验证LiveData
的行为,无需依赖 Android 组件。 - 支持
TestObserver
:
通过工具类(如observeForTesting
)直接获取LiveData
的值,简化单元测试。
- 隔离 UI 与数据逻辑:
-
与其他技术的对比
特性 | LiveData | RxJava /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层管道阻塞:
nativePollOnce
的epoll_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/8DiskCache
: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(200, 200);
// 结合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优化读写锁