1.java的三个特性
-
封装(Encapsulation)
- 我在日常项目开发中,会把一个类的内部数据和实现细节对外隐藏,只暴露必要的访问接口(通常是
public
的 getter/setter 或业务方法)。 - 这样做的好处是:当我需要修改类内部实现或添加新字段时,不会影响使用这个类的其他模块,降低了耦合度。
- 在 Android 上,举例来说,我会把网络请求结果解析、缓存策略等逻辑都封装到一个
Repository
或Manager
类中,Activity 或 Fragment 只需调用它提供的方法,不关心内部怎么实现。 - 同时,封装还能防止外部直接修改关键字段,保证对象状态的一致性和合法性。
- 我在日常项目开发中,会把一个类的内部数据和实现细节对外隐藏,只暴露必要的访问接口(通常是
-
继承(Inheritance)
- 继承允许我创建一个新类时复用已有类的属性和方法,只需要关注增量逻辑就行,避免大量重复代码。
- 在 Android 中,最常见的是自定义组件或基类 Activity。例如我会定义一个
BaseActivity
,把通用的权限请求、生命周期埋点、主题切换等代码放在基类里,后续所有具体页面都继承它,只写页面特有的逻辑。 - 这样做不仅提升了开发效率,也保证了团队的页面风格和行为一致性。
- 另外,通过继承我还能对公共行为做统一控制,比如统一的错误处理、日志打印等,只要在基类里实现一次,所有子类都能受益。
-
多态(Polymorphism)
- 多态体现为同一个方法调用在不同对象上会产生不同的行为,常见的形式有方法重写(Override)和接口回调。
- 在 Android 开发里,我经常会把逻辑定义到接口或父类中,然后在具体实现类中重写,比如把网络请求回调定义成一个接口
Callback<T>
,Activity/Fragment 实现它后,统一传给数据层,数据层只负责调用回调方法。 - 这样,无论是使用 Retrofit、OkHttp 还是自研网络库,接口层和业务层都能保持一致,切换实现库时对业务层影响极小。
- 同时,多态也方便我用集合存储不同的子类型实例、统一调度,比如把一批自定义 ViewHandler 放到 List 中,循环调用同一个接口方法,而具体行为各不相同。
总结来说,封装 保证模块边界清晰、易维护;继承 提供代码复用和统一管理;多态 则让系统更灵活,可插拔,可扩展。
创建字符串的时候的区别
str1 = "abc" str2=new String ("abc")
-------
-
字符串常量池 vs 堆上对象
- 当写
str1 = "abc"
时,Java 会把这个字面量 “abc” 放到 字符串常量池(String Pool)里,JVM 启动或类加载阶段只会创建一次这个对象,后面再遇到相同的字面量都会直接复用池里的实例。 - 而写
str2 = new String("abc")
则是显式地在 堆 上开辟一个新的 String 对象,它内部也会引用常量池里的那个 “abc” 字符数组,但无论池里有没有,都强制再创建一次堆对象。
- 当写
-
引用相等(==)与内容相等(equals)
- 对于 str1,与常量池中同样的 “abc” 比较,用 == 判断会返回 true,因为它们是同一个引用。
- 而 str2 一定是新对象,用 == 比较常量池中的 “abc” 就不相等;要判断字符内容是否一致,就必须调用 equals。
- 在面试中经常被问到的 “为什么 s1==s2 和 s1.equals(s2 结果不同” 就是基于这个常量池 vs new 的差异。
-
性能和垃圾回收
- 因为 new String("abc") 每次都会在堆上创建新的实例,如果在循环或高频场景中反复这么写,就会产生大量短命对象,给 GC 带来压力。
- 而直接使用字面量能让多个地方复用同一个实例,既省内存也减轻 GC 负担。
- 所以在绝大多数情况下,只要不需要修改底层字符数组(String 是不可变的),我们都会优先用字面量方式。
-
调用 intern() 的权衡
- 如果确实因为某种原因拿到了一堆 new String 对象,却又想回到常量池——可以手动调用 str2.intern(),让它返回池里的那个实例引用。
- 但 intern() 本身也要查表并维护池,过度使用也会带来性能开销,通常只有在需要大量重复字符串对比或缓存 key 的场景才会用。
-
安全性和不可变性的一致保障
- 无论常量池还是 new String,String 本身都是不可变类,不会因为拿到同一份字符数组就允许你修改内容。但 new 出来的对象在堆上,引用更分散,不容易被意外共享,某些场合反而更安全。
- 举个例子,如果我们把方法参数设计为 String,并在内部做了某种缓存,new String 可以确保调用者的原始字面量不被污染。
-
实战中的指导原则
- 默认用字面量:这是最常见、最省资源的方式。
- 避免无意义的 new:新对象要有它的理由,比如你真要操作不同的引用或做 intern 之前的临时处理。
- 性能敏感时注意 GC:在 Adapter、列表渲染、循环拼接里,千万不要在循环里 new String,每个新对象都是一次 GC 的潜在痕迹。
- 理解底层机制再权衡:只有对常量池、堆、引用比较都了然于胸,才能在面试里清晰、准确地回答,说明你不仅能背概念,还真正懂原理和应用。
总结:
- str1 = "abc":使用常量池,只会创建一次,== 相等,节省资源。
- str2 = new String("abc"):每次都在堆上开新实例,== 不等于池中实例,增加 GC 压力。
讲讲Android四大组件
-
Activity
- 职责:代表一个界面(Screen),负责和用户交互。每个 Activity 都是一块可见的 UI 区域。
- 生命周期:从
onCreate
、onStart
、onResume
到onPause
、onStop
、onDestroy
,它描述了界面从创建、可见、前台交互,到后台不可见、销毁的完整流程。 - 关键点:
- 在
onCreate
中进行布局加载、数据初始化; - 在
onPause
/onStop
中保存临时状态(如表单输入),防止进程被回收导致数据丢失; - 在
onResume
恢复界面交互。
- 在
- 场景举例:
- 登录页、列表页、详情页都由不同 Activity 承担。
- 在页面切换时,通过显式或隐式 Intent 启动下一步 Activity,并可通过
startActivityForResult
获取返回结果。
-
Service
- 职责:在后台执行长时或无界面的任务,如播放音乐、定时上传日志、持续定位。
- 类型:
- Started Service(以
startService
启动,自己运行直到调用stopService
或stopSelf
) - Bound Service(以
bindService
启动,供客户端组件绑定并通过 Binder 进行交互)
- Started Service(以
- 生命周期:
- Started Service 的生命周期在
onCreate
→onStartCommand
→(多次onStartCommand
)→onDestroy
。 - Bound Service 则在第一次
bindService
时调用onCreate
、onBind
,最后一次解绑或调用stopService
时走onUnbind
、onDestroy
。
- Started Service 的生命周期在
- 注意事项:
- Android 8.0+ 对后台 Service 有严格限制,推荐使用前台 Service(
startForeground
)或者借助 JobScheduler、WorkManager 进行调度。
- Android 8.0+ 对后台 Service 有严格限制,推荐使用前台 Service(
- 场景举例:
- 音乐播放器的播放控制、下载管理;
- 与 Activity 绑定进行数据传输(比如实时进度回调)。
-
BroadcastReceiver
- 职责:接收并响应全局或应用内广播消息,用于各组件间或系统事件的通知。
- 注册方式:
- 静态注册:在 AndroidManifest 中声明,能够接收系统级广播(如设备启动、网络变化)。
- 动态注册:在代码中通过
registerReceiver
,生命周期受注册者所在组件(Activity/Service)控制。
- 生命周期:
- 收到广播后执行
onReceive
,时长必须很短(不适合做耗时操作),否则会发生 ANR。 - 如果要做耗时任务,需要在
onReceive
中启动 Service。
- 收到广播后执行
- 场景举例:
- 网络状态变化时重新加载数据;
- 封装应用内事件分发(动态注册本地广播或使用 Jetpack 的 LiveData/Flow 更推荐)。
-
ContentProvider
- 职责:负责不同应用或同一应用各模块之间的数据共享与访问控制。
- 核心概念:
- 以统一的 URI(
content://
)作为数据标识符; - 提供 CRUD 接口(
query
、insert
、update
、delete
、getType
); - 通过
ContentResolver
在客户端进行跨进程访问。
- 以统一的 URI(
- 生命周期:
- 程序第一次访问该 Provider 时由系统调度创建,调用
onCreate
; - 以后每次访问直接在内存中复用。
- 程序第一次访问该 Provider 时由系统调度创建,调用
- 安全与权限:
- 可通过
android:exported
、android:permission
来控制谁能访问; - 在 Android 7.0+ 强制 URI 权限校验,避免安全泄露。
- 可通过
- 场景举例:
- 系统的联系人、短信、媒体库都通过 ContentProvider 暴露接口;
- 自定义小组件间的本地数据共享,或给第三方应用提供数据访问。
———
四大组件如何协作?
- Intent 是它们之间的主要通信方式:
- Activity 与 Activity 之间通过隐式/显式 Intent 跳转;
- Activity 启动 Service 或绑定 Service;
- 各组件之间通过广播 Intent 分发消息。
- ContentResolver + Uri 实现组件与 ContentProvider 的数据交互,支持跨应用。
总结:
- Activity 承担「视图 + 交互」;
- Service 承担「后台任务」;
- BroadcastReceiver 承担「事件分发」;
- ContentProvider 承担「数据共享」。
讲讲MVVM,然后知道MVP吗
-
MVVM(Model‑View‑ViewModel)
- 我是怎么理解的
MVVM 最大的心智模型就是 “视图没有直接去操作业务逻辑,所有要展示的数据和交互都通过 ViewModel 来驱动”。 - 职责划分
- Model:纯粹的数据层,比如网络请求、数据库读写,跟 UI 完全无关。
- View:Activity、Fragment 或自定义 View,负责把界面渲染出来、响应用户点击、手势等,然后把用户动作“汇报”给 ViewModel。
- ViewModel:位于中间,拿到用户的操作之后去触发 Model 层接口(像发起一次网络请求、查询本地数据),然后把结果通过可观察的数据(LiveData、StateFlow 等)流回给 View。View 只需要订阅这些数据变化,拿到更新就刷新 UI。
- 好处
- 弱耦合:ViewModel 完全不依赖 Android UI API(它甚至可以用 Unit Test 直接跑),业务逻辑独立于 Activity/Fragment 的生命周期。
- 数据驱动:借助 Data Binding 或 LiveData/Flow,所有 UI 更新都自然跟数据变动挂钩,不用在各个回调里再去找 findViewById、手动 setText。
- 生命周期安全:LiveData 会自己感知 Activity/Fragment 的状态,避免常见的 “View 已销毁还回调” 导致的崩溃。
- 我在项目里怎么用
- 把所有网络、缓存逻辑封装到 Repository 层;
- ViewModel 只暴露几个 LiveData 或 StateFlow 给 UI,UI 只订阅,不关心数据怎么来的;
- ViewModel 里也没任何 Android Context 的操作,只在最顶层用 Hilt/Dagger 给它注入 Repository。
- 我是怎么理解的
-
MVP(Model‑View‑Presenter)
- 我是怎么接触到的
早期我做过一个老项目,用的是 MVP,Activity/Fragment 实现一个 IView 接口,Presenter 持有这个接口引用,每次有业务动作就调用 Presenter 方法,Presenter 拿到结果后再反过来通过 IView 的 callback 更新 UI。 - 职责划分
- Model:同样是数据层。
- View:只负责渲染和用户交互,提供接口给 Presenter。
- Presenter:有点像 ViewModel,但它直接拿到 View 的引用,耦合更紧密,所有 UI 调用都是方法回调。
- 优劣势
- 优点:逻辑和 UI 分离也比较清晰,比传统把所有网络、UI 放一起在 Activity 强胶囊性好多了;
- 缺点:
- 内存风险:Presenter 持有 View 引用,如果不手动断开绑定,就可能导致 Activity 泄漏;
- 回调臃肿:接口里一堆方法,网络成功、网络失败、Loading 展示、Loading 隐藏……随着功能增多,Presenter / View 接口很快就挂满了方法;
- 测试难度:虽然也能 Mock View 接口做单测,但所有 UI 相关 stub 都要自己写,繁琐一些。
- 为什么后来切 MVVM
因为项目越来越大,我们想要一个更自动化的方式把状态变化推到界面上,又能少写接口回调,看了 Android Jetpack 推的实战案例,就切成 MVVM,结合 LiveData / Flow 和 ViewBinding,开发效率和可维护性都上了一个台阶。
- 我是怎么接触到的
总结对比
- 耦合度:MVP 的 Presenter 持有 View 接口,两者有显式引用;MVVM 把 ViewModel 与 View 通过可观察数据解耦,不直接调用 View 方法。
- 回调方式:MVP 要你手写一堆回调接口;MVVM 里用 LiveData/Flow 自动通知,写法更简洁。
- 生命周期管理:MVVM 自带生命周期感知,MVP 要手动解绑。
- 测试友好度:MVVM 里只测 ViewModel,Mock Data 流就行;MVP 里还要 Mock IView,维护测试桩更麻烦。
livedata应用,然后子线程更新数据涉及过吗?
(livedata是一个数据容器)
-
LiveData 在 MVVM 里的位置
- 我们把 LiveData 放到 ViewModel 里,View(Activity/Fragment)去观察它。
- ViewModel 负责从 Repository 拉取数据(网络请求、数据库查询等),把拿到的结果通过 LiveData 发布出去。
- View 那边只做订阅、收到变化后刷新 UI,不用关心数据是从哪来、怎么来的。
-
为什么要用 LiveData
- 生命周期感知:只有在 Activity/Fragment 处于活跃状态(STARTED/RESUMED)时才会收到更新,避免崩溃和内存泄漏。
- 自动解绑:系统帮我们把观察者和宿主生命周期绑一起,不用手动 unregisterReceiver 那样麻烦,也不会出现“忘解绑导致泄漏”这类问题。
- 清晰的单向数据流:UI 只负责展示和传用户操作到 ViewModel,数据变化只从 ViewModel 流向 View,思路简单,不会在回调里左右跳。
-
子线程更新 LiveData 的做法
我们都知道,LiveData 有两个方法可以改值:- setValue:只能在主线程调用,直接把数据推给观察者。
- postValue:可以在任何线程调用,内部会把值丢到主线程的消息队列,等主线程空闲时再执行一次 setValue。
在我实际项目里,场景通常是这样的:
- 发起一个 Retrofit 或者 Room 的异步请求,这些回调大多是在工作线程里返回结果。
- 拿到结果后,不能直接在这个线程里刷新 UI,一定要回主线程。
- 我就直接在回调里调用 LiveData.postValue(data)。它会把 data 安全地搬到主线程,然后通知 View 更新。
-
注意事项和坑
- 连续多次 postValue:如果在非常短时间内多次调用 postValue,只有最后一次会真正被 dispatch(因为它内部只保留最新的值),所以如果要做累积或增量更新,要自己在 ViewModel 里做合并,而不是盲目多次 post。
- UI 线程调试:有时候在主线程也会看不到立刻更新,因为 LiveData 通知是异步的(它会等到主线程的下一次调度),如果你写了 setValue 之后马上去测,就可能发现数据还没到,切记。
- 从协程更新:我常用 Kotlin 和协程,协程里拿到数据之后如果在 IO Dispatcher,就用 postValue;如果切回到 Main Dispatcher,就直接用 setValue。或者更直接,用 androidx.lifecycle:lifecycle-livedata-ktx 提供的 asLiveData/ liveData 构建器,一行代码就把协程结果变成 LiveData,内部自动帮你切线程。
-
总结我的实践经验
- 绝大多数场景:后台线程回调里用 postValue,把数据安全送到主线程。
- 主线程逻辑:像用户交互触发的简单本地计算,用 setValue。
- 更高级用法:结合 MediatorLiveData 或 Transformations,把多个数据源合并再发布,或者用 LiveData.switchMap 实现依赖链,都让我少写回调、少管生命周期。
讲讲RecycleVIew
-
RecyclerView 的定位和优势
- RecyclerView 其实是对旧版 ListView 和 GridView 的一次重构升级,不只是简单地替换控件。
- 它把「数据和视图解耦」、把「布局管理」和「滚动动画」都模块化了,给我们足够的自由去定制各种列表效果,比如瀑布流、轮播图、九宫格、瀑布流……基本上只要写个 LayoutManager 就能搞定。
- 而且它把之前 ListView 的屏幕外复用机制抽象成 Recycler(回收池),性能更高,写起来更灵活。
-
三大组件:Adapter、ViewHolder、LayoutManager
① Adapter- 相当于「数据到视图」的桥梁,负责告诉 RecyclerView 一共有多少条数据、每个位置应该用哪个布局、数据怎么绑定到控件上。
- 我平时会用 ListAdapter 或者加上 DiffUtil,它能智能比较新旧数据集的差异,只更新变化的那几行,避免整表闪烁和多余的 bind 事件。
- 如果是分段加载或分页,我会配合 Paging 库,把加载状态、空数据、错误页也当作额外的 ViewType 一并处理。
② ViewHolder
- 你可以把它看成一个「视图缓存容器」,将每个 item 的子控件引用缓存起来,避免反复 findViewById。
- RecyclerView 在滑出屏幕后,并不会销毁这些 ViewHolder,而是放到 RecyclerPool 里,下次复用时直接取出来 bind 新数据。
- 我常在 ViewHolder 构造里做一次性 findViewById,然后写一个 bind(data) 方法专门赋值、加载图片、处理点击,保持职责清晰。
③ LayoutManager
- 这是最核心但也最容易忽略的部分,它决定了 item 怎么布局、如何滚动、大小测量、回收时机。
- 默认有 LinearLayoutManager(列表/横滑)、GridLayoutManager(宫格)、StaggeredGridLayoutManager(瀑布流)三种,也可以自己继承 LayoutManager 完全自定义,比如实现瀑布流的瀑布对齐、横向日历滚动等效果。
- 我在项目里如果要做一些复杂布局(类似多列不等高 + 脚布局 + 悬浮标题),都会重写 LayoutManager,让它在 onLayoutChildren 和 scrollVerticallyBy 里精细控制 child view 的摆放和偏移。
-
回收与复用机制
- RecyclerView 核心在于「缓存池」,它维护三种缓存:
- Scrap 缓存:刚退出屏幕,还没 detach,就保留在这里,最优先复用;
- RecycledViewPool:完全 detach 后进入全局池,不同 RecyclerView 之间也能共享;
- CachedViews:大小可配置,用来缓存临近屏幕外的 view,滑动时如果滚回去能瞬间复用。
- 这一套回收机制让我在滚动超长列表、上百张图片时不卡顿,也不频繁触发 GC。
- RecyclerView 核心在于「缓存池」,它维护三种缓存:
-
触发回调与动画
- RecyclerView 通过 Adapter 的
notifyItemChanged/Inserted/Removed
系列方法来驱动局部动画,它会自动根据差异给出默认的淡入淡出、滑入滑出动画。 - 如果要更炫一点,比如瀑布流项目里瀑布块翻转、折叠卡片等,我会自定义 ItemAnimator 或者直接在 onBindViewHolder 时给 item 加 Animator,结合 LayoutManager 的预布局回调实现入场动画。
- RecyclerView 通过 Adapter 的
-
常见优化手段
- DiffUtil:避免整表刷新,减少 bind 和动画开销;
- Payloads:在
notifyItemChanged(position, payload)
时只更新局部视图,比如只刷新点赞数,不重置整个 item; - 预取机制:在 LinearLayoutManager/RecyclerView 上启用预取和子线程预加载布局量,滑动更加流畅;
- ViewType 合理拆分:同一布局不同状态(加载中、空视图、item)拆成不同 ViewType,减少 if/else 逻辑;
- RecyclerPool 管理:如果有多个 RecyclerView 共用相同 item 类型,我会创建公用的 RecycledViewPool,让它们共享缓存,内存更平稳。
内存优化,内存泄漏延伸到了java四大引用,以及每个引用的特点
-
Android 层面的内存优化和常见泄漏
- 大对象与适时回收
比如 Bitmap、Drawable、Cursor、InputStream 这类资源,如果一直持有不释放,就会占用大量堆外或堆内内存。我会:
• 在 Activity/Fragment 对应的生命周期里及时调用bitmap.recycle()
或者在onDestroyView
/onDestroy
释放引用;
• 对 Cursor 用完记得close()
,对流记得close()
; - Context 泄漏
最大的坑就是把 Activity Context 误当成 Application Context:
• 比如把 Activity 的 Context 赋给静态变量、单例、Handler、TimerTask,这些长期持有,Activity 无法回收;
• 我的做法是:能够用 Application Context 时就用 Application Context,不能用就千万别用静态结构持有;Handler、TimerTask 这种带回调的,用静态内部类+弱引用持有 Context。 - Adapter 和 ViewHolder 泄漏
列表里图文并茂,用 Glide 等异步加载图片时,如果 Adapter、ViewHolder 引用了外部 Activity,就要注意在滑出屏幕后取消加载,或者让外部不被强引用。 - 匿名内部类/Runnable
匿名内部类自动持有外部类引用,如果 Runnable、Callback、Listener 一直没解绑,就会泄漏。我会:
• 如果要在子线程回调到主线程,用静态 Handler + 弱引用,或者用 Lifecycle-aware 的组件(LiveData、ViewModel、Coroutine)代替。
- 大对象与适时回收
-
Java 四大引用及特点
在上面那些场景里,往往需要对对象引用强度做区分,于是就有了 Java 的四种引用:① 强引用(Strong Reference)
- 默认的引用类型:
Object obj = new Object();
- 只要还有强引用指向对象,GC 就绝不回收它。
- 优势:使用最简单;
- 风险:如果不手动置 null,或者在大对象、长生命周期类里滥用,就会造成 OOM。
② 软引用(Soft Reference)
- 用来做内存敏感的缓存:
SoftReference<Bitmap> cache = new SoftReference<>(bitmap);
- GC 在内存不足时才会回收软引用指向的对象,平时软引用对象是不会轻易被回收的。
- 特点:能最大程度利用内存做缓存;当内存紧张、要抛 OOM 时才清;
- 场景:我会用它做图片缓存层(第二级缓存),内存足够就缓存,内存压力来时自动清掉,避免 OOM。
③ 弱引用(Weak Reference)
- 用于监听或地图缓存:
WeakReference<Activity> weakAct = new WeakReference<>(this);
- 一旦 GC 运行,不管内存是否紧张,弱引用对象只要没有强引用就会被回收;
- 特点:比软引用更容易被回收,生命周期很短,只用于跟踪对象存在与否;
- 场景:
• 在自定义缓存中,如果我想要对象一旦没人用就马上释放,就用弱引用;
• 在 Handler、Runnable、Listener 持有外部引用时,我会把 Activity、View 用弱引用包一层,防止它自己活得太久。
④ 虚引用(Phantom Reference)
- 最弱的一种引用:对象只要被虚引用关联,随时可能被回收,但它主要配合
ReferenceQueue
做更精细的清理工作; - 特点:无法通过虚引用拿到对象实例,只能用于在对象“虚拟准备回收”时收到一个通知;
- 场景:
• 在底层缓存或者自定义资源池中,我会用它注册到ReferenceQueue
,当对象要被回收时执行一些清理逻辑(比如关闭文件、Socket 连接);
• 在 Android 里,我自己没在业务层用得太多,但底层大厂的缓存框架或垃圾回收监控里会用。
- 默认的引用类型:
-
每种引用的权衡和落地
- 默认首选强引用,简单直接,用完置 null 或让作用域结束。
- 缓存策略:
- 一级缓存——强引用哈希表,常驻内存;
- 二级缓存——软引用哈希表,内存紧张时自动清;
- 三级缓存——磁盘或数据库;
- 监听/回调安全:
- 用弱引用包装 Context 或 View,GC 后 weakRef.get() 为 null,就代表宿主销毁,可安全停止回调。
- 微调与监控:
- 如果需要做额外清理(日志、文件句柄、连接等),我会配合虚引用和 ReferenceQueue,在后台线程里监听回收事件再做后续处理。
-
实战经验和注意点
- 不要滥用软引用:在 Android 上,软引用并不会像想象那样“很久都留着再回收”,有时一 GC 就清掉,缓存命中率反而低。
- 优先用现成框架:Glide/Coil、LruCache 已经帮我们做好了强/弱/软引用的分层缓存,除非有特别需求,一般直接用它们。
- 借助工具检查泄漏:LeakCanary 这类工具就利用弱引用或虚引用来检测 Activity、Fragment 泄漏,一旦 LeakCanary 的弱引用没被 GC,就提醒我哪里没解绑。
- 结合 Android Profiler:看堆快照,找持有链,确认是哪条引用链把对象“拽”住不放,再决定改成软/弱引用,还是手动置 null。
如何判断内存优化的40%
-
明确测试场景和指标
- 先选一个典型场景:比如 App 启动后滑动首页列表 20 次,或者打开某个模块并切后台再切前台。
- 确定要对比的指标:我常拿 Android Studio 的 Memory Profiler 里的「Allocated」值(实时分配内存)、“PSS”(常驻集内存),以及 dalvik/native heap 的峰值。
- 记录三次以上的多次试跑,取平均数,避免某次 GC 时机差异导致数据波动太大。
-
抓基线数据(Before)
- 在完全未做任何优化的分支或版本上,启动 Memory Profiler,重放刚才的测试场景。
- 看记录:比如峰值 Dalvik Heap 刚好到 120 MB,PSS 峰值到 100 MB,Allocated 总量大概 150 MB。
- 把这几个数字写到表格里,作为「优化前」的对比基准。
-
做具体优化
- 比如:
• 把大图用 downsample + LruCache 缓存;
• 用 LeakCanary 定位并修掉一处 Activity 泄漏;
• 避免在循环里 new String/Bitmap,改成复用对象或 Flyweight 模式;
• 把内存缓存层做成弱引用或软引用分层。 - 每做一步或者做完一批,重新跑一次完全相同的测试场景。
- 比如:
-
抓优化后数据(After)
- 同样在 Memory Profiler 上跑三遍,取平均:
• Dalvik Heap 峰值 72 MB;
• PSS 峰值 60 MB;
• Allocated 总量 90 MB。 - 把这些「优化后」数据也记下来。
- 同样在 Memory Profiler 上跑三遍,取平均:
-
计算优化比例
- 我会选择最能代表整体内存压力的那个指标。假设我用 Dalvik Heap 峰值做对比:
(120 MB – 72 MB) / 120 MB ≈ 0.40,也就是降低了 40%。 - 如果你用 PSS 或 Allocated,总量差 / 优化前总量,也都能算出百分比。
- 我会选择最能代表整体内存压力的那个指标。假设我用 Dalvik Heap 峰值做对比:
-
验证和复盘
- 不要只看一次结果,还要在不同手机、不同系统版本上跑,同样的脚本场景,确认在主流设备上都能稳定降到差不多的数值。
- 再用 MAT(Memory Analyzer)或 heap dump 对比,看看对象数量是不是真的少了,缓存池命中率有没有上来。
- 最后把这些「Before/After 的对比图」截下来,给团队一个清晰的优化报告:
• 优化前 Dalvik Heap 峰值 120 MB → 优化后 72 MB,降幅 40%。
• PSS 从 100 MB → 60 MB,降幅 40%。
• GC 触发次数从平均 8 次 → 4 次,减少一半,流畅度明显提升。
-
面试回答要点
- 强调量化:指定场景、固定动作、跑 Memory Profiler。
- 强调可重复性:多跑几次取平均,多机型对比,避免偶发。
- 强调对比指标:Dalvik Heap、PSS、Allocated,任选其一,统一口径。
- 强调工具链:Android Studio Profiler + LeakCanary + MAT + heap dump 对比。
- 最后给出一个百分比公式:
(优化前 – 优化后) ÷ 优化前 × 100% = 优化比例(比如 40%)
讲讲Handler机制
-
核心三脚架:Looper、MessageQueue、Handler
- Looper:每个线程如果想用消息队列,得先调用
Looper.prepare()
,创建一个 Looper 和它挂钩的MessageQueue
。主线程系统帮我们做了这步。 - MessageQueue:就是一个链表或小根堆,按时间戳、优先级存放
Message
或Runnable
。 - Looper.loop():启动一个无限循环,不停地从 MessageQueue 里取消息/任务,拿到后交给它对应的 Handler 去处理。
- Looper:每个线程如果想用消息队列,得先调用
-
Handler 究竟干了什么
- 构造时:你 new 一个 Handler(也可以传一个
Callback
),它会把自己和当前线程的 Looper 绑上:mLooper = Looper.myLooper()
、mQueue = mLooper.queue
。 - 发消息:你可以调用
sendMessage()
、sendEmptyMessageAtTime()
、post()
、postDelayed()
等接口。底层就是把一个Message
(或封装了Runnable
的 Message)塞到MessageQueue.enqueueMessage()
。 - 分发消息:当消息队列到时间,Looper.loop() 把 Message 拿出来,调用
handler.dispatchMessage(msg)
:
– 如果你给 Handler 传了Callback
,就先执行callback.handleMessage(msg)
;
– 否则就掉handler.handleMessage(msg)
,也就是你的那段重写逻辑。
- 构造时:你 new 一个 Handler(也可以传一个
-
主线程 vs 子线程
- 主线程 Handler:无论你在 Fragment、Activity 中
new Handler()
,背后都是拿到主 Looper,保证你任何时候发消息都跑到 UI 线程。最常见用法就是在后台线程里handler.post { … }
更新 UI。 - HandlerThread:如果你想给子线程也配一个消息循环,就用
HandlerThread
。它内部自己调用了prepare()/loop()
,你拿它的 looper new 一个 Handler,就能往这个子线程发消息,做串行异步任务或定时重试。
- 主线程 Handler:无论你在 Fragment、Activity 中
-
为什么不用直接 Thread + sleep
- Handler 带队列、能定时(delay)、能分优先级,能在不同线程间天然切换,而且消息时间比 Timer 精准,不会被直接抖动打断。
- 而且一条消息一个 Message 对象,你也可以复用
Message.obtain()…sendToTarget()
,减少分配压力。
-
常见坑和优化建议
- 内存泄漏
– 匿名或非静态内部 Handler 会隐式持有外部 Activity/Fragment,导致只要队列里没消费完,它就一直“拽”着 Activity 不回收。
– 我们项目里都改成静态 Handler +WeakReference<Activity>
,或者干脆用Handler(Looper.getMainLooper(), callback)
、把 Callback 里逻辑扔进去,解耦引用。 - 滥用 postDelayed
– 如果页面销毁前忘removeCallbacksAndMessages(null)
,定时任务还在队列里,不仅会继续回调,也会泄漏。
– 我会在onDestroy()
里统一清空,或者结合 LifecycleObserver,在销毁时自动解绑。 - 消息合并
– 短时间内连续发相同 Message,很可能让 UI 一顿抖动、重复绑定。可以用sendEmptyMessageAtTime
指定同一个 what,或者先removeMessages(what)
再发,保证同一时刻队列里只剩最新一条。
- 内存泄漏
-
小结
- 启动:Looper + MessageQueue 在某个线程里无限循环。
- 发消息:Handler 把 Message/Runnable 放队列。
- 取消息:Looper.loop 取出,分发给 Handler.dispatchMessage,最后到你的 handleMessage/Runnable。
- 场景:UI 更新、定时任务、子线程串行、资源清理都能靠它;注意生命周期、内存、消息合并这些细节。