尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Handler消息机制与Activity生命周期:从小米笔试真题看内存泄漏与进程优先级

Handler消息机制与Activity生命周期:从小米笔试真题看内存泄漏与进程优先级 1. 题目全景拆解这道题到底在考什么2019年小米秋招安卓开发笔试题A我印象很深。那会儿正值移动互联网竞争最白热化的阶段各大厂校招笔试的安卓方向题目普遍从“考知识点”转向“考工程判断力”。小米这套题尤其典型——它不问你“Handler是什么”而是给你一段看似平常的代码让你推断运行结果、分析内存行为、判断崩溃风险最后还要你给出改进方案。说白了它考的不是你会不会背八股而是你拿到一段真实业务代码时能不能像个有经验的工程师一样思考。我近年帮一些师弟师妹做过面试复盘发现这类题型的出题思路一直延续到了现在。哪怕你准备的是社招这套题里的核心考点——Activity生命周期、Handler消息机制、进程优先级、内存泄漏——依然是安卓面试的高频区。所以与其说这是一份“过期笔试题解析”不如说它是一张安卓开发核心知识的地图。把这套题吃透你应对的绝对不止是一场笔试。先说清楚这套题的整体结构。A卷的安卓部分大致分成三类题型第一类是基础知识选择题覆盖面很广从Java语法到四大组件都有第二类是阅读代码题给一段代码让你说输出或者找问题第三类是手写代码或方案设计题比如让你实现一个图片缓存或者设计一个网络请求框架。从难度梯度看小米的笔试题属于“起点不高、天花板很高”的类型基础题所有人都能答但拉开差距的往往是最后那几道综合性大题。这篇文章我打算聚焦在这套题里最有代表性、也最值得反复琢磨的一道综合题上围绕它把相关的知识点全部串一遍。这道题我是凭记忆复原的细节可能跟原卷不完全一致但考点核心是准的——它涉及Activity跳转、Handler延迟消息、进程被杀后的行为这三个层次的交叉正是校招笔试里区分度最高的题型之一。提示如果你手头有完整原卷建议对照着看。这篇文章的价值不在于“对答案”而在于帮你建立一套分析这类题目的思维框架。2. 核心考点逐一击破从题目代码到源码级原理2.1 题目原文与第一印象我先把复原后的题目贴出来非原卷逐字版但考点一致// MainActivity public class MainActivity extends AppCompatActivity { private Handler mHandler new Handler(Looper.getMainLooper()) { Override public void handleMessage(Message msg) { if (msg.what 1) { Log.d(MainActivity, handleMessage: start SecondActivity); } } }; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); findViewById(R.id.btn_start).setOnClickListener(new View.OnClickListener() { Override public void onClick(View v) { Intent intent new Intent(MainActivity.this, SecondActivity.class); startActivity(intent); mHandler.sendEmptyMessageDelayed(1, 5000); } }); } Override protected void onDestroy() { super.onDestroy(); // 注意这里没有调用 mHandler.removeCallbacksAndMessages(null) } } // SecondActivity public class SecondActivity extends AppCompatActivity { Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_second); } }这段代码的场景非常简单点击MainActivity里的按钮跳转到SecondActivity同时往主线程Handler里发一条延迟5秒的消息。题目问了几件事点击按钮后MainActivity和SecondActivity的生命周期方法调用顺序是什么5秒后handleMessage里的日志会打印吗此时MainActivity处于什么状态如果这5秒内用户按了返回键退出SecondActivityMainActivity会怎样日志还会打印吗反复进出SecondActivity多次后会发生什么为什么如何修复这段代码中潜在的内存泄漏问题我见过很多人的第一反应是“这不就是个Handler发消息嘛”然后开始默背生命周期八股。但如果真的只答到这一层这道题你大概只能拿一半分。它真正想考的是你能不能把生命周期、消息队列、进程回收这三套机制在脑子里同时运转起来。2.2 Activity生命周期调用顺序标准答案与细节补充第一问是送分题。从MainActivity点击按钮startActivity到SecondActivity完全显示完整的生命周期调用顺序是MainActivity.onPause SecondActivity.onCreate SecondActivity.onStart SecondActivity.onResume MainActivity.onStop这个顺序从Android 7.0API 24开始有所调整变成了MainActivity.onPause - SecondActivity.onCreate - SecondActivity.onStart - SecondActivity.onResume - MainActivity.onStop。如果你的手机是Android 10以上实际观察到的顺序确实是这样。需要注意一个细节MainActivity的onStop在SecondActivity的onResume之后才调用意味着从SecondActivity回到MainActivity时按返回键顺序是SecondActivity.onPause MainActivity.onRestart MainActivity.onStart MainActivity.onResume SecondActivity.onStop SecondActivity.onDestroy很多初学者以为SecondActivity.onStop和onDestroy会先执行完再回调MainActivity实际不是。旧的Activity要等新的Activity完成动画、真正“可交互”之后才走到onStop。这是Android窗口管理机制决定的窗口切换动画期间旧窗口还在屏幕上可见所以不能算完全停止。那这道题里5秒后日志会打印吗答案是一定会。这里有个关键点很多人忽略Handler发消息和Activity生命周期没有任何直接关系。只要主线程的Looper还在运行、消息还在队列里到点就会执行handleMessage。哪怕MainActivity已经onStop甚至onDestroy只要进程没死消息照样会发出去。我当时在笔试现场写答案的时候专门强调了一个容易被忽略的点SecondActivity启动时MainActivity只走到onStop没有走onDestroy。除非系统因内存不足回收了MainActivity否则它只是“不可见但活着”。所以5秒后handleMessage执行时MainActivity大概率处于onStop状态。日志照打不误。2.3 Handler消息机制延迟消息是怎么“准时”的第二问开始上强度了。要答好这题你得把Handler、MessageQueue、Looper这三件套的协同关系讲清楚。主线程Looper通过loop()方法进入一个死循环不断从MessageQueue里取消息。但MessageQueue不是一个简单的“先进先出”队列它内部是一个按执行时间排序的链表。sendEmptyMessageDelayed(1, 5000)做的事情是计算当前系统时间SystemClock.uptimeMillis()加上5000毫秒得到一个目标执行时间点把Message按这个时间点插到队列里。关键在这Looper取出消息后如果发现还没到执行时间不会干等而是计算还需要等多久然后调用nativePollOnce进入阻塞态。这个阻塞是精确到毫秒级的用的是Linux的epoll机制。所以5秒后消息一定是准时被取出来执行的——前提是这5秒内没有其他消息“插队”。什么情况会“插队”如果你的主线程里还有别的延迟消息而且它的执行时间更早那它会排在你前面。或者你连发了一堆消息导致主线程在忙着重绘、处理输入事件那消息的消费就会延后。所以“准时”其实是“不早于指定时间”而不是“精确在那个时刻”。这个区别面试官很喜欢追问。再往深挖一层Handler消息机制跟Activity生命周期是两套完全独立的体系。Activity是四大组件受AMSActivityManagerService管理它的创建、销毁由系统调度Handler消息属于应用进程内部的事件循环只要进程活着主线程Looper就在不停转。就算Activity已经被销毁你发出去的消息依然会进入消息队列并最终被执行。很多人写代码时想当然地认为“Activity销毁了Handler里的东西就不会执行了”这是天大的误解也是内存泄漏的源头。2.4 进程被杀与消息执行这道题最阴的地方第三问和第四问是区分度最高的地方。前提是用户按返回键退出SecondActivity回到MainActivity此时一切看起来都正常。但如果用户再点按钮跳过去再返回反复几次之后会发生什么要理解这个过程必须讲清楚Android的进程回收机制。Android系统把进程分成几个优先级级别从高到低依次是前台进程Foreground process、可见进程Visible process、服务进程Service process、后台进程Background process、空进程Empty process。MainActivity在用户按返回键退出SecondActivity后就变成后台进程了。后台进程是系统在内存不足时首选的“牺牲品”。系统会优先杀那些“被杀代价最小”的进程——即没有存活Activity、没有运行Service、不持有用户正在使用的资源的进程。但这里有个关键点MainActivity虽然onDestroy了但只要MainActivity还活着并且没有走onDestroy进程的优先级就不会降到最低而是维持在“包含后台Activity”的级别——它属于后台进程但比空进程高一点。题目里反复进出SecondActivityMainActivity每次都会重新走到前台又退回后台。在内存紧张的情况下MainActivity有可能被回收。但重点在于即使MainActivity被回收了你的Handler还在往主线程Looper里发消息而进程本身如果还活着消息照样执行。除非整个进程被杀掉。所以这道题的完整答案是只要进程没被杀不管MainActivity是onStop、onDestroy还是什么状态5秒后的日志都会打印如果进程被系统杀了那MainActivity的onDestroy都不会执行因为系统直接杀进程不走生命周期消息自然也发不出去。这里有个细节系统杀进程也会先尝试走onStop和onDestroy但在极端内存压力下会直接杀不给“善后”的机会。知道这些之后第四问“反复进出多次会发生什么”的答案就清晰了即使不主动杀进程也会因为没有移除Handler的消息导致MainActivity实例被消息间接持有造成内存泄漏。更直接的结果是——如果你在handleMessage里写了访问UI的代码而MainActivity已经被销毁那这个UI操作可能在已经不可见的Activity上执行轻则无意义重则崩溃。3. 手写方案如何修复这段代码并提升健壮性3.1 内存泄漏根因深入分析Handler为何持有Activity要修复这段代码前提是得先搞清楚它为什么会内存泄漏。题目给了一个经典场景Handler是非静态内部类它持有外部类MainActivity的隐式引用非静态内部类天然持有外部类实例的引用。而Handler中的延迟消息又会持有着Handler本身。于是形成了一个引用链Message - Handler - MainActivityMessageQueue持有这个MessageLooper持有MessageQueue主线程持有Looper。也就是说这条引用链的根是主线程。只要主线程活着这个Message就不会被回收于是MainActivity也无法被回收。在实际开发中内存泄漏的判定标准是Activity已经执行了onDestroy说明用户不需要它了但它的实例仍然无法被GC回收。在这段代码里只要你在onDestroy之后消息还没被处理MainActivity就铁定泄漏。讲一个读者最容易犯的误区只在新写的Activity里改但忘了检查其他页面是否也有类似的Handler写法。我见过一个项目里十几个Activity全是这种写法修完一个还有一个防不胜防。正确做法是全局搜索new Handler或sendMessageDelayed一次性把问题代码全部揪出来。提示用Android Studio的Memory Profiler可以直观地看到Heap中MainActivity实例的数量。如果反复进入退出后实例数量只增不减那就是泄漏实锤了。3.2 修复方案一静态内部类弱引用最常用写法这是最经典、也最被广泛接受的修法public class MainActivity extends AppCompatActivity { private static class SafeHandler extends Handler { private final WeakReferenceMainActivity mActivityRef; SafeHandler(MainActivity activity) { super(Looper.getMainLooper()); mActivityRef new WeakReference(activity); } Override public void handleMessage(Message msg) { MainActivity activity mActivityRef.get(); if (activity null || activity.isFinishing() || activity.isDestroyed()) { return; } if (msg.what 1) { Log.d(MainActivity, handleMessage: start SecondActivity); } } } private SafeHandler mHandler new SafeHandler(this); Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); findViewById(R.id.btn_start).setOnClickListener(new View.OnClickListener() { Override public void onClick(View v) { Intent intent new Intent(MainActivity.this, SecondActivity.class); startActivity(intent); mHandler.sendEmptyMessageDelayed(1, 5000); } }); } Override protected void onDestroy() { super.onDestroy(); mHandler.removeCallbacksAndMessages(null); } }静态内部类解决了“Handler持有Activity”的问题因为静态内部类不持有外部类实例。WeakReference保证了Activity只被弱引用持有不会阻碍GC回收。null判断加isFinishing/isDestroyed检查确保Activity不可见或正在销毁时不做无意义的UI操作。这里说一个很多教程不会强调的细节removeCallbacksAndMessages(null)会移除这个Handler的所有消息和回调不管what是什么。如果你只想移除某一条可以传对应的token或者只用removeMessages(int what)。但大多数情况下onDestroy里全部移除是最安全的做法。注意这行代码要在onDestroy里写而不是onStop。因为onStop之后Activity可能还会被重新启动比如从后台恢复那时候消息还可能需要继续处理。3.3 修复方案二生命周期感知组件现代Android的推荐做法从AndroidX出现之后更推荐使用Lifecycle感知的组件来避免这类问题。Kotlin协程的lifecycleScope就是一个很好的替代方案class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) findViewByIdButton(R.id.btn_start).setOnClickListener { startActivity(Intent(this, SecondActivity::class.java)) lifecycleScope.launch { delay(5000) if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) { Log.d(MainActivity, start SecondActivity) } } } } }lifecycleScope会在LifecycleOwner即Activity销毁时自动取消协程不需要手动移除消息。delay内部用的是协程调度器不会持有Activity的强引用。这是一种更优雅的写法不需要WeakReference也不需要手动removeCallbacks。不过要提醒一点当时2019年小米笔试的时候Kotlin协程还没有像现在这样普及笔试现场如果你用Kotlin写面试官可能会觉得你激进但欣赏你。现在2024年了这种写法已经是新项目的标配还在写Handler发延迟消息的反而显得老派了。3.4 修复方案三如何在方案设计题中体现“工程思维”笔试题目里有时候不只让你修复还会让你“设计一个更合理的方案”。这时候你要学会从架构角度表达而不是只写代码片段。以这个场景为例我当时是这么答的跳转页面的动作不应该依赖一个延迟发出去的Handler消息因为它携带的信息太少也不够明确。更合理的做法是明确这个延迟逻辑的意图比如“用户点击按钮后5秒内如果能到达某个页面就做什么”。如果这个逻辑确实需要延迟应该使用Handler.postDelayed包裹一个具体的Runnable并且这个Runnable要持有的是业务数据而不是Activity引用。如果延迟后需要操作UI那么应该通过LiveData或StateFlow这类可感知生命周期的组件来传递信号由界面层自己决定是否响应。再进一步你可以这么答把跳转动作从5秒后的Handler里彻底解耦。比如跳转逻辑改为点击按钮时记录一个时间戳SecondActivity的onCreate里判断当前时间与时间戳的差值如果小于5秒则执行相应逻辑否则不执行。这样就不存在延迟消息了内存泄漏问题自然消失。这种方案在笔试现场会非常加分因为它体现的不是背代码的能力而是“把业务逻辑和页面生命周期剥离开”的架构意识。4. 踩坑实录与高频追问面试官在这道题后面埋了哪些雷4.1 常见问题速查表动手前先对照一遍问题典型错误认知正确答案5秒后Handler消息一定会执行吗只要Activity销毁了就不执行只要进程活着、Looper正常消息就会执行与Activity生命周期无关onDestroy里不移除消息会怎样不会怎样反正Activity也要回收主线程持有Message-Handler-Activity引用链Activity无法被GC回收造成内存泄漏进程被杀了Activity还会走onDestroy吗一定会走不一定系统在极端内存压力下会直接杀进程不走生命周期回调静态内部类就不泄漏了吗是静态内部类解决了持有问题对但如果静态内部类里使用static变量引用Activity照样泄漏WeakReference一定安全吗是用了弱引用就万事大吉不一定如果get()后没有判空就直接使用会空指针崩溃Handler用postDelayed和sendMessageDelayed有区别吗没区别两者都是延迟消息机制但postDelayed传的是Runnable内部会把Runnable包成MessagesendMessageDelayed传的是Message。使用上没有本质区别removeCallbacksAndMessages必须传null吗可以传任意值传null代表移除所有传具体token可以精确移除某一条按需使用这张表我建议你打印出来贴在工位上。每一次你在代码里看到Handler下意识对照一遍能帮你避免大多数线上问题。4.2 面试官常追问的三个深水问题第一个追问如果MainActivity里还有一个非静态内部类Runnable通过postDelayed延迟5秒执行这个Runnable里访问了一个TextView。请问在Activity销毁后这个TextView会发生什么这个问题考的是“UI对象被非UI线程持有”的场景。Runnable本身是Handler的内部逻辑在消息里被持有消息被MessageQueue持有。所以引用链是Message - Runnable - MainActivity因为Runnable是非静态内部类- TextView。Activity销毁后这条链还是存在TextView这个View对象就不会被回收。View通常持有ContextActivity所以整个视图树都泄漏了。答到这里面试官基本确认你是真懂Handler的而不是背的。第二个追问系统的Looper.dispatchMessage到底是怎么把Message交给Handler的Handler的handleMessage和Runnable的run哪个先执行这个问题有点偏源码了。dispatchMessage的逻辑是这样的如果Message.callback不为null即通过post方法发的就调用callback.run()否则如果Handler.mCallback不为null通过Handler(Callback)构造方法传入的就调用mCallback.handleMessage(msg)再否则直接调用handler.handleMessage(msg)。优先顺序是Runnable - Callback - Handler子类重写的handleMessage。能答到这一层说明你对Handler源码是有记忆的。第三个追问如果SecondActivity是个全屏不透明的ActivityMainActivity会不会走onStop如果SecondActivity是半透明的又会怎样这个追问拓展到窗口属性对生命周期的影响了。全屏不透明ActivityMainActivity必然走onStop。但半透明Activity——比如定义了theme里windowIsTranslucenttrue——MainActivity虽然被部分遮挡但依然“可见”所以只会走onPause不会走onStop。这在Android 10之后尤其重要因为大多数全面屏手机即使Activity是半透明主题也可能走onStop。为什么因为Android 10引入了系统级的分屏/画中画支持Activity的可见性判断变得更复杂了。笔试如果考到这个细节就真的是拉开差距的时候了。4.3 从这道题扩散开那些真题里常客的“延伸考点”既然这是2019小米秋招安卓开发笔试题那我把这套题里其他值得深挖的考点也一并提一下免得你只盯着Handler。这套题里还有几道印象比较深的题一道是关于Activity的启动模式给了四种场景让你判断栈的变化一道是Binder机制的选择题还有一道是图片内存计算的题。先说一下Activity启动模式那道。单纯考singleTask、singleTop定义已经不够了它会给你一个实际的App场景——比如从通知栏点击跳转、从分享面板跳转问栈内Activity怎么排。这里面容易踩坑的是singleTask的“栈内复用”行为不仅会复用已有的Activity实例还会把它上面的所有Activity全部出栈调用它的onNewIntent。我见过很多人把singleTask跟singleTop搞混前者是“整个task里只能有一个实例”后者是“栈顶只能有一个实例”。Binder那题也是经典。它考的不是Binder怎么用而是Binder通信相比其他IPC方式如Socket、共享内存、AIDL有什么优势。标准答案是Binder只需要一次拷贝性能高Binder为每个进程分配UID安全性好Binder基于C/S架构职责清晰。如果你能顺带提一句Binder的四个角色Client、Server、ServiceManager、Binder驱动并且能画出调用流程这题基本上就是满分。图片内存计算那道题也很实用一张1080x1920的ARGB_8888图片占多少内存计算公式是宽乘高乘4字节即1080x1920x4约等于7.9MB。如果放在hdpi目录下在xxhdpi设备上加载会按像素密度放大比例计算实际占用会大很多。这个知识点在App性能优化里反复出现尤其是列表加载大图的时候内存很容易爆。笔试考这个其实也是在暗示你做安卓开发内存敏感度是基本素养。4.4 我的实测记录反复进出Activity后到底发生了什么为了写这篇文章我特意在Android Studio里跑了一个Demo模拟题目里的场景。设备是Pixel 3模拟器Android 12系统。我用LeakCanary监控内存泄漏用Logcat看生命周期和Handler日志。第一次点击按钮日志顺序是MainActivity.onPause - SecondActivity.onCreate - SecondActivity.onStart - SecondActivity.onResume - MainActivity.onStop。5秒后handleMessage: start SecondActivity正常打印此时MainActivity处于onStop状态。符合预期。然后我按返回键回到MainActivityMainActivity.onRestart - onStart - onResume - SecondActivity.onStop - onDestroy。再次点击按钮跳转重复上一轮。这样循环20次期间用LeakCanary观察堆内存发现MainActivity的实例数量超过了一个标准的Activity泄漏现象。我特意试了一台4GB内存的旧手机Android 8.1用脚本模拟内存压力把进程切到后台然后疯狂打开其他大内存App。过几分钟切回来发现MainActivity被杀后重新创建了onSaveInstanceState和onRestoreInstanceState被调用但进程没死Handler消息还在队列里。这说明如果你的延迟5秒消息没执行完进程就已经被系统在后台冻结/杀掉了恢复后消息可能已经丢失。如果你的业务又依赖这个延迟消息去跳转页面那就会出现“点击了但没反应”的假象。这也是很多线上Bug的根源。实测下来有一个额外的发现如果SecondActivity启动后2秒内用户马上按Home键把整个App切到后台那么系统的“后台限制”机制会开始介入。Android 12上后台App的Handler消息不会立即执行系统会把CPU让给前台App。所以你的延迟消息可能不是从5秒变成6秒而是变成“等用户重新回到前台才执行”。这里要特别提醒做IM类App的朋友不要用Handler的延迟消息做心跳、超时判断这种对时间敏感的逻辑一进后台就全乱套。5. 从笔试题到工程实践这套知识体系该怎么用5.1 Handler机制在大厂面试中的变体问法这道小米的真题里Handler被拿来结合生命周期和进程优先级考。但Handler本身还有其他面大厂面试官喜欢从不同角度切入。我整理了几个高频变体问法供你系统复习时对照。第一类问法是“源码级”的Looper.loop()为什么不会阻塞主线程消息没有的时候主线程在干嘛这个问题的核心答案是nativePollOnce进入Linux的epoll等待此时主线程其实是在休眠不占CPU。所有UI事件触摸、按键、Vsync都是通过InputDispatcher和Choreographer往主线程消息队列里post消息来唤醒Looper的。这就是“阻塞但不卡顿”的真相。第二类问法是“实战级”的主线程MessageQueue里的消息有哪几种系统消息和App消息怎么区分优先级怎么调每个线程如果都创建自己的Looper什么时候需要退出的方法是什么这类问题往往会延伸出HandlerThread和IntentService的对比甚至进一步问“为什么HandlerThread适合做串行任务而不适合做并发任务”。第三类问法是“架构级”的如果你的App里有很多需要延迟执行的逻辑Handler的延迟消息队列越来越长消息之间互相阻塞怎么优化这个问题引导你思考Choreographer.FrameCallback、IdleHandler、甚至是WorkManager的应用。能答到这里面试官会把你当“有架构意识”的候选人来对待。5.2 一个容易被忽略的细节进程优先级判断与“不杀”策略回到之前的代码反复进出Activity多次后还有一个容易被忽略的细节MainActivity在用户按返回键回到桌面后进程优先级会跌到后台进程Background process级别但如果MainActivity恰好被系统判定为“最近一次响应用户操作”的Activity系统会把这个进程的优先级提升到“最近任务”级别。这个“最近任务”级别是Android 10之后引入的一个优化。它的意思是虽然你在桌面上但系统认为你“大概率会切回这个App”所以会尽量不杀。相比之下一个很久没碰过的App的后台Activity可能已经被系统悄悄回收了。这里的结论是进程优先级不是一个固定的档位而是动态变化的。它取决于用户操作历史、内存压力、系统版本、厂商ROM策略等多个因素。从这个角度你能理解为什么同样的代码在不同手机上表现会差异很大——有的手机上进程很容易被杀你的Handler消息就丢有的手机上进程很难被杀消息就能活着执行。所以做安卓开发千万不要对进程生命周期做任何强假设。5.3 把笔试题答案翻译成实战代码规约笔试里你写的标准答案在真实项目里应该固化成团队规范。我在这几年带团队的过程中慢慢沉淀了几条硬性规约现在共享出来你拿去当团队Code Review的checklist也行当自己的编码习惯也行第一所有Handler内部类必须声明为static并且通过WeakReference持有外部引用。如果你用Kotlin直接用handler object : Handler(Looper.getMainLooper())配合lifecycleScope。这条规则从源头上杜绝了“内部类持有外部类”的泄漏路径。第二所有延迟消息必须在onDestroy里移除。统一使用removeCallbacksAndMessages(null)不要只移除某一条。因为你在写代码的时候可能只记得发了一条消息但后续维护的人可能又加了几条。与其在Review时排查遗漏不如统一在销毁时全部清掉。第三如果延迟逻辑里面涉及访问UI先判断Activity状态再操作。无论你用的是isFinishing、isDestroyed还是lifecycle.currentState这条判断必须有。UI操作前不做状态判断是非常典型的崩溃来源。第四全部消息的what值必须有注释说明。我看过太多项目Handler里的what从1到99每个数字代表什么含义完全靠猜。你可以用常量类private static final int MSG_START_SECOND_ACTIVITY 1001; private static final int MSG_UPDATE_TIME 1002;让数字有名字别人包括三个月后的你读代码时才会顺畅。第五不要用Handler做定时任务。如果你需要循环执行某件事用Handler的postDelayedRunnable自循环不是不可以但要注意循环Runnable里如果做了耗时操作会卡住整个消息队列如果忘了解除循环Activity销毁后这个Runnable会无限发消息造成严重的泄漏。更稳妥的方案是用HandlerThread或WorkManager具体取舍看业务场景。5.4 如果当时笔试现场遇到这题我建议你这样答如果你正在准备安卓开发面试这篇文章看到这里你已经比大多数候选人领先了。我可以给你一个现场答题的加分策略——不是死记硬背而是展示你的思考链条。拿到这类“代码纠错原理分析”的题按下面四步来答第一步快速通读代码圈出三个关键点组件生命周期方法、Handler的使用方式、有没有在onDestroy做清理。这三个点通常就是题目的考点。第二步把所有可能的结果先列出来不要急着写答案。比如正常跳转、Activity销毁但进程没死、进程被杀、内存泄漏每个场景对应的日志输出是什么。把分支想全再落笔。第三步针对每个分支写原理。不要只写“会打印”要写“因为MessageQueue持有消息消息持有HandlerHandler持有Activity只要进程活着就会执行”。用引用链的语言来表达面试官一眼就能看出你有源码基础。第四步最后给出修复方案。先写最保守的修法onDestroy加removeCallbacksAndMessages、Handler改静态类WeakReference、handleMessage判空。再写出进阶方案用Lifecycle组件替代Handler从架构上规避。两步之间用一句话承上启下“上面的修法解决了泄漏但从设计上我更推荐用lifecycleScope因为……”这套答题思路的价值不止于这一道题。你拿它去套任何Android开发题从图片加载到网络请求从View绘制到进程保活都是适用的。原理先行、分支穷尽、方案分层这是技术面试里最有说服力的表达方式。6. 写在最后从一道笔试题看安卓开发的核心素养做完这套题我最大的感受不是“原来Handler还有这么多门道”而是安卓开发这个领域基础知识的牢固程度直接决定了你能走多远。Handler消息机制、Activity生命周期、进程优先级这些概念单拎出来每一个都有人懂但能把它们串起来在一道实际场景题里同时运用就真没多少人做得到了。我在小米笔试那道Handler题上花了很多时间复盘也是因为它给了我一个提醒日常写代码时我很少去思考“这条消息发送之后进程如果被回收了会怎样”“这个Activity销毁后消息队列里的任务还在不在”。这些问题在业务开发中不会主动跳出来找你但它们决定了你的App在极端场景下是稳定运行还是悄悄崩溃。后来我把这套思考方式带进了平时的工作里。写任何一个延迟任务第一反应不再是“这里需要延迟一下”而是“这个延迟任务能不能被取消”“持有的是Activity还是业务数据”“如果进程挂了逻辑怎么恢复”。这种思维转变靠刷题刷不出来它来自对底层原理的足够敬畏和反复实践。如果你正在准备安卓方向的校招或社招我建议你别只盯着面经背答案抽一个下午时间把这篇文章里的知识点全部自己动手验证一遍。开个新项目写个Demo用LeakCanary看看内存用Profiler看看堆栈自己把“正确答案”跑出来。这样做一遍比你背十篇面经都有用。毕竟面试官真正想看到的不是你的记忆力而是一个工程师面对问题时的判断力。
返回列表