字节跳动 (Android) 一面

1.java的三个特性

  1. 封装(Encapsulation)

    • 我在日常项目开发中,会把一个类的内部数据和实现细节对外隐藏,只暴露必要的访问接口(通常是 public 的 getter/setter 或业务方法)。
    • 这样做的好处是:当我需要修改类内部实现或添加新字段时,不会影响使用这个类的其他模块,降低了耦合度。
    • 在 Android 上,举例来说,我会把网络请求结果解析、缓存策略等逻辑都封装到一个 Repository 或 Manager 类中,Activity 或 Fragment 只需调用它提供的方法,不关心内部怎么实现。
    • 同时,封装还能防止外部直接修改关键字段,保证对象状态的一致性和合法性。
  2. 继承(Inheritance)

    • 继承允许我创建一个新类时复用已有类的属性和方法,只需要关注增量逻辑就行,避免大量重复代码。
    • 在 Android 中,最常见的是自定义组件或基类 Activity。例如我会定义一个 BaseActivity,把通用的权限请求、生命周期埋点、主题切换等代码放在基类里,后续所有具体页面都继承它,只写页面特有的逻辑。
    • 这样做不仅提升了开发效率,也保证了团队的页面风格和行为一致性。
    • 另外,通过继承我还能对公共行为做统一控制,比如统一的错误处理、日志打印等,只要在基类里实现一次,所有子类都能受益。
  3. 多态(Polymorphism)

    • 多态体现为同一个方法调用在不同对象上会产生不同的行为,常见的形式有方法重写(Override)和接口回调。
    • 在 Android 开发里,我经常会把逻辑定义到接口或父类中,然后在具体实现类中重写,比如把网络请求回调定义成一个接口 Callback<T>,Activity/Fragment 实现它后,统一传给数据层,数据层只负责调用回调方法。
    • 这样,无论是使用 Retrofit、OkHttp 还是自研网络库,接口层和业务层都能保持一致,切换实现库时对业务层影响极小。
    • 同时,多态也方便我用集合存储不同的子类型实例、统一调度,比如把一批自定义 ViewHandler 放到 List 中,循环调用同一个接口方法,而具体行为各不相同。

总结来说,封装 保证模块边界清晰、易维护;继承 提供代码复用和统一管理;多态 则让系统更灵活,可插拔,可扩展。

创建字符串的时候的区别

str1 = "abc"   str2=new String ("abc")

-------

  1. 字符串常量池 vs 堆上对象

    • 当写
      str1 = "abc"
      时,Java 会把这个字面量 “abc” 放到 字符串常量池(String Pool)里,JVM 启动或类加载阶段只会创建一次这个对象,后面再遇到相同的字面量都会直接复用池里的实例。
    • 而写
      str2 = new String("abc")
      则是显式地在  上开辟一个新的 String 对象,它内部也会引用常量池里的那个 “abc” 字符数组,但无论池里有没有,都强制再创建一次堆对象。
  2. 引用相等(==)与内容相等(equals)

    • 对于 str1,与常量池中同样的 “abc” 比较,用 == 判断会返回 true,因为它们是同一个引用。
    • 而 str2 一定是新对象,用 == 比较常量池中的 “abc” 就不相等;要判断字符内容是否一致,就必须调用 equals。
    • 在面试中经常被问到的 “为什么 s1==s2 和 s1.equals(s2 结果不同” 就是基于这个常量池 vs new 的差异。
  3. 性能和垃圾回收

    • 因为 new String("abc") 每次都会在堆上创建新的实例,如果在循环或高频场景中反复这么写,就会产生大量短命对象,给 GC 带来压力。
    • 而直接使用字面量能让多个地方复用同一个实例,既省内存也减轻 GC 负担。
    • 所以在绝大多数情况下,只要不需要修改底层字符数组(String 是不可变的),我们都会优先用字面量方式。
  4. 调用 intern() 的权衡

    • 如果确实因为某种原因拿到了一堆 new String 对象,却又想回到常量池——可以手动调用 str2.intern(),让它返回池里的那个实例引用。
    • 但 intern() 本身也要查表并维护池,过度使用也会带来性能开销,通常只有在需要大量重复字符串对比或缓存 key 的场景才会用。
  5. 安全性和不可变性的一致保障

    • 无论常量池还是 new String,String 本身都是不可变类,不会因为拿到同一份字符数组就允许你修改内容。但 new 出来的对象在堆上,引用更分散,不容易被意外共享,某些场合反而更安全。
    • 举个例子,如果我们把方法参数设计为 String,并在内部做了某种缓存,new String 可以确保调用者的原始字面量不被污染。
  6. 实战中的指导原则

    • 默认用字面量:这是最常见、最省资源的方式。
    • 避免无意义的 new:新对象要有它的理由,比如你真要操作不同的引用或做 intern 之前的临时处理。
    • 性能敏感时注意 GC:在 Adapter、列表渲染、循环拼接里,千万不要在循环里 new String,每个新对象都是一次 GC 的潜在痕迹。
    • 理解底层机制再权衡:只有对常量池、堆、引用比较都了然于胸,才能在面试里清晰、准确地回答,说明你不仅能背概念,还真正懂原理和应用。

总结:

  • str1 = "abc":使用常量池,只会创建一次,== 相等,节省资源。
  • str2 = new String("abc"):每次都在堆上开新实例,== 不等于池中实例,增加 GC 压力。

讲讲Android四大组件

  1. Activity

    • 职责:代表一个界面(Screen),负责和用户交互。每个 Activity 都是一块可见的 UI 区域。
    • 生命周期:从 onCreateonStartonResume 到 onPauseonStoponDestroy,它描述了界面从创建、可见、前台交互,到后台不可见、销毁的完整流程。
    • 关键点
      • 在 onCreate 中进行布局加载、数据初始化;
      • 在 onPause/onStop 中保存临时状态(如表单输入),防止进程被回收导致数据丢失;
      • 在 onResume 恢复界面交互。
    • 场景举例
      • 登录页、列表页、详情页都由不同 Activity 承担。
      • 在页面切换时,通过显式或隐式 Intent 启动下一步 Activity,并可通过 startActivityForResult 获取返回结果。
  2. Service

    • 职责:在后台执行长时或无界面的任务,如播放音乐、定时上传日志、持续定位。
    • 类型
      • Started Service(以 startService 启动,自己运行直到调用 stopService 或 stopSelf
      • Bound Service(以 bindService 启动,供客户端组件绑定并通过 Binder 进行交互)
    • 生命周期
      • Started Service 的生命周期在 onCreate → onStartCommand →(多次 onStartCommand)→ onDestroy
      • Bound Service 则在第一次 bindService 时调用 onCreateonBind,最后一次解绑或调用 stopService 时走 onUnbindonDestroy
    • 注意事项
      • Android 8.0+ 对后台 Service 有严格限制,推荐使用前台 Service(startForeground)或者借助 JobScheduler、WorkManager 进行调度。
    • 场景举例
      • 音乐播放器的播放控制、下载管理;
      • 与 Activity 绑定进行数据传输(比如实时进度回调)。
  3. BroadcastReceiver

    • 职责:接收并响应全局或应用内广播消息,用于各组件间或系统事件的通知。
    • 注册方式
      • 静态注册:在 AndroidManifest 中声明,能够接收系统级广播(如设备启动、网络变化)。
      • 动态注册:在代码中通过 registerReceiver,生命周期受注册者所在组件(Activity/Service)控制。
    • 生命周期
      • 收到广播后执行 onReceive,时长必须很短(不适合做耗时操作),否则会发生 ANR。
      • 如果要做耗时任务,需要在 onReceive 中启动 Service。
    • 场景举例
      • 网络状态变化时重新加载数据;
      • 封装应用内事件分发(动态注册本地广播或使用 Jetpack 的 LiveData/Flow 更推荐)。
  4. ContentProvider

    • 职责:负责不同应用或同一应用各模块之间的数据共享与访问控制。
    • 核心概念
      • 以统一的 URI(content://)作为数据标识符;
      • 提供 CRUD 接口(queryinsertupdatedeletegetType);
      • 通过 ContentResolver 在客户端进行跨进程访问。
    • 生命周期
      • 程序第一次访问该 Provider 时由系统调度创建,调用 onCreate
      • 以后每次访问直接在内存中复用。
    • 安全与权限
      • 可通过 android:exportedandroid:permission 来控制谁能访问;
      • 在 Android 7.0+ 强制 URI 权限校验,避免安全泄露。
    • 场景举例
      • 系统的联系人、短信、媒体库都通过 ContentProvider 暴露接口;
      • 自定义小组件间的本地数据共享,或给第三方应用提供数据访问。

———

四大组件如何协作?

  • Intent 是它们之间的主要通信方式:
    • Activity 与 Activity 之间通过隐式/显式 Intent 跳转;
    • Activity 启动 Service 或绑定 Service;
    • 各组件之间通过广播 Intent 分发消息。
  • ContentResolver + Uri 实现组件与 ContentProvider 的数据交互,支持跨应用。

总结

  • Activity 承担「视图 + 交互」;
  • Service 承担「后台任务」;
  • BroadcastReceiver 承担「事件分发」;
  • ContentProvider 承担「数据共享」。

讲讲MVVM,然后知道MVP吗

  1. MVVM(Model‑View‑ViewModel)

    • 我是怎么理解的
      MVVM 最大的心智模型就是 “视图没有直接去操作业务逻辑,所有要展示的数据和交互都通过 ViewModel 来驱动”。
    • 职责划分
      • Model:纯粹的数据层,比如网络请求、数据库读写,跟 UI 完全无关。
      • View:Activity、Fragment 或自定义 View,负责把界面渲染出来、响应用户点击、手势等,然后把用户动作“汇报”给 ViewModel。
      • ViewModel:位于中间,拿到用户的操作之后去触发 Model 层接口(像发起一次网络请求、查询本地数据),然后把结果通过可观察的数据(LiveData、StateFlow 等)流回给 View。View 只需要订阅这些数据变化,拿到更新就刷新 UI。
    • 好处
      1. 弱耦合:ViewModel 完全不依赖 Android UI API(它甚至可以用 Unit Test 直接跑),业务逻辑独立于 Activity/Fragment 的生命周期。
      2. 数据驱动:借助 Data Binding 或 LiveData/Flow,所有 UI 更新都自然跟数据变动挂钩,不用在各个回调里再去找 findViewById、手动 setText。
      3. 生命周期安全:LiveData 会自己感知 Activity/Fragment 的状态,避免常见的 “View 已销毁还回调” 导致的崩溃。
    • 我在项目里怎么用
      • 把所有网络、缓存逻辑封装到 Repository 层;
      • ViewModel 只暴露几个 LiveData 或 StateFlow 给 UI,UI 只订阅,不关心数据怎么来的;
      • ViewModel 里也没任何 Android Context 的操作,只在最顶层用 Hilt/Dagger 给它注入 Repository。
  2. MVP(Model‑View‑Presenter)

    • 我是怎么接触到的
      早期我做过一个老项目,用的是 MVP,Activity/Fragment 实现一个 IView 接口,Presenter 持有这个接口引用,每次有业务动作就调用 Presenter 方法,Presenter 拿到结果后再反过来通过 IView 的 callback 更新 UI。
    • 职责划分
      • Model:同样是数据层。
      • View:只负责渲染和用户交互,提供接口给 Presenter。
      • Presenter:有点像 ViewModel,但它直接拿到 View 的引用,耦合更紧密,所有 UI 调用都是方法回调。
    • 优劣势
      • 优点:逻辑和 UI 分离也比较清晰,比传统把所有网络、UI 放一起在 Activity 强胶囊性好多了;
      • 缺点:
        1. 内存风险:Presenter 持有 View 引用,如果不手动断开绑定,就可能导致 Activity 泄漏;
        2. 回调臃肿:接口里一堆方法,网络成功、网络失败、Loading 展示、Loading 隐藏……随着功能增多,Presenter / View 接口很快就挂满了方法;
        3. 测试难度:虽然也能 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是一个数据容器)

  1. LiveData 在 MVVM 里的位置

    • 我们把 LiveData 放到 ViewModel 里,View(Activity/Fragment)去观察它。
    • ViewModel 负责从 Repository 拉取数据(网络请求、数据库查询等),把拿到的结果通过 LiveData 发布出去。
    • View 那边只做订阅、收到变化后刷新 UI,不用关心数据是从哪来、怎么来的。
  2. 为什么要用 LiveData

    • 生命周期感知:只有在 Activity/Fragment 处于活跃状态(STARTED/RESUMED)时才会收到更新,避免崩溃和内存泄漏。
    • 自动解绑:系统帮我们把观察者和宿主生命周期绑一起,不用手动 unregisterReceiver 那样麻烦,也不会出现“忘解绑导致泄漏”这类问题。
    • 清晰的单向数据流:UI 只负责展示和传用户操作到 ViewModel,数据变化只从 ViewModel 流向 View,思路简单,不会在回调里左右跳。
  3. 子线程更新 LiveData 的做法
    我们都知道,LiveData 有两个方法可以改值:

    • setValue:只能在主线程调用,直接把数据推给观察者。
    • postValue:可以在任何线程调用,内部会把值丢到主线程的消息队列,等主线程空闲时再执行一次 setValue。

    在我实际项目里,场景通常是这样的:

    • 发起一个 Retrofit 或者 Room 的异步请求,这些回调大多是在工作线程里返回结果。
    • 拿到结果后,不能直接在这个线程里刷新 UI,一定要回主线程。
    • 我就直接在回调里调用 LiveData.postValue(data)。它会把 data 安全地搬到主线程,然后通知 View 更新。
  4. 注意事项和坑

    • 连续多次 postValue:如果在非常短时间内多次调用 postValue,只有最后一次会真正被 dispatch(因为它内部只保留最新的值),所以如果要做累积或增量更新,要自己在 ViewModel 里做合并,而不是盲目多次 post。
    • UI 线程调试:有时候在主线程也会看不到立刻更新,因为 LiveData 通知是异步的(它会等到主线程的下一次调度),如果你写了 setValue 之后马上去测,就可能发现数据还没到,切记。
    • 从协程更新:我常用 Kotlin 和协程,协程里拿到数据之后如果在 IO Dispatcher,就用 postValue;如果切回到 Main Dispatcher,就直接用 setValue。或者更直接,用 androidx.lifecycle:lifecycle-livedata-ktx 提供的 asLiveData/ liveData 构建器,一行代码就把协程结果变成 LiveData,内部自动帮你切线程。
  5. 总结我的实践经验

    • 绝大多数场景:后台线程回调里用 postValue,把数据安全送到主线程。
    • 主线程逻辑:像用户交互触发的简单本地计算,用 setValue。
    • 更高级用法:结合 MediatorLiveData 或 Transformations,把多个数据源合并再发布,或者用 LiveData.switchMap 实现依赖链,都让我少写回调、少管生命周期。

讲讲RecycleVIew

  1. RecyclerView 的定位和优势

    • RecyclerView 其实是对旧版 ListView 和 GridView 的一次重构升级,不只是简单地替换控件。
    • 它把「数据和视图解耦」、把「布局管理」和「滚动动画」都模块化了,给我们足够的自由去定制各种列表效果,比如瀑布流、轮播图、九宫格、瀑布流……基本上只要写个 LayoutManager 就能搞定。
    • 而且它把之前 ListView 的屏幕外复用机制抽象成 Recycler(回收池),性能更高,写起来更灵活。
  2. 三大组件: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 的摆放和偏移。
  3. 回收与复用机制

    • RecyclerView 核心在于「缓存池」,它维护三种缓存:
      1. Scrap 缓存:刚退出屏幕,还没 detach,就保留在这里,最优先复用;
      2. RecycledViewPool:完全 detach 后进入全局池,不同 RecyclerView 之间也能共享;
      3. CachedViews:大小可配置,用来缓存临近屏幕外的 view,滑动时如果滚回去能瞬间复用。
    • 这一套回收机制让我在滚动超长列表、上百张图片时不卡顿,也不频繁触发 GC。
  4. 触发回调与动画

    • RecyclerView 通过 Adapter 的 notifyItemChanged/Inserted/Removed 系列方法来驱动局部动画,它会自动根据差异给出默认的淡入淡出、滑入滑出动画。
    • 如果要更炫一点,比如瀑布流项目里瀑布块翻转、折叠卡片等,我会自定义 ItemAnimator 或者直接在 onBindViewHolder 时给 item 加 Animator,结合 LayoutManager 的预布局回调实现入场动画。
  5. 常见优化手段

    • DiffUtil:避免整表刷新,减少 bind 和动画开销;
    • Payloads:在 notifyItemChanged(position, payload) 时只更新局部视图,比如只刷新点赞数,不重置整个 item;
    • 预取机制:在 LinearLayoutManager/RecyclerView 上启用预取和子线程预加载布局量,滑动更加流畅;
    • ViewType 合理拆分:同一布局不同状态(加载中、空视图、item)拆成不同 ViewType,减少 if/else 逻辑;
    • RecyclerPool 管理:如果有多个 RecyclerView 共用相同 item 类型,我会创建公用的 RecycledViewPool,让它们共享缓存,内存更平稳。

内存优化,内存泄漏延伸到了java四大引用,以及每个引用的特点

  1. 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)代替。
  2. 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 里,我自己没在业务层用得太多,但底层大厂的缓存框架或垃圾回收监控里会用。
  3. 每种引用的权衡和落地

    • 默认首选强引用,简单直接,用完置 null 或让作用域结束。
    • 缓存策略
      1. 一级缓存——强引用哈希表,常驻内存;
      2. 二级缓存——软引用哈希表,内存紧张时自动清;
      3. 三级缓存——磁盘或数据库;
    • 监听/回调安全
      • 用弱引用包装 Context 或 View,GC 后 weakRef.get() 为 null,就代表宿主销毁,可安全停止回调。
    • 微调与监控
      • 如果需要做额外清理(日志、文件句柄、连接等),我会配合虚引用和 ReferenceQueue,在后台线程里监听回收事件再做后续处理。
  4. 实战经验和注意点

    • 不要滥用软引用:在 Android 上,软引用并不会像想象那样“很久都留着再回收”,有时一 GC 就清掉,缓存命中率反而低。
    • 优先用现成框架:Glide/Coil、LruCache 已经帮我们做好了强/弱/软引用的分层缓存,除非有特别需求,一般直接用它们。
    • 借助工具检查泄漏:LeakCanary 这类工具就利用弱引用或虚引用来检测 Activity、Fragment 泄漏,一旦 LeakCanary 的弱引用没被 GC,就提醒我哪里没解绑。
    • 结合 Android Profiler:看堆快照,找持有链,确认是哪条引用链把对象“拽”住不放,再决定改成软/弱引用,还是手动置 null。

如何判断内存优化的40%

  1. 明确测试场景和指标

    • 先选一个典型场景:比如 App 启动后滑动首页列表 20 次,或者打开某个模块并切后台再切前台。
    • 确定要对比的指标:我常拿 Android Studio 的 Memory Profiler 里的「Allocated」值(实时分配内存)、“PSS”(常驻集内存),以及 dalvik/native heap 的峰值。
    • 记录三次以上的多次试跑,取平均数,避免某次 GC 时机差异导致数据波动太大。
  2. 抓基线数据(Before)

    • 在完全未做任何优化的分支或版本上,启动 Memory Profiler,重放刚才的测试场景。
    • 看记录:比如峰值 Dalvik Heap 刚好到 120 MB,PSS 峰值到 100 MB,Allocated 总量大概 150 MB。
    • 把这几个数字写到表格里,作为「优化前」的对比基准。
  3. 做具体优化

    • 比如:
      • 把大图用 downsample + LruCache 缓存;
      • 用 LeakCanary 定位并修掉一处 Activity 泄漏;
      • 避免在循环里 new String/Bitmap,改成复用对象或 Flyweight 模式;
      • 把内存缓存层做成弱引用或软引用分层。
    • 每做一步或者做完一批,重新跑一次完全相同的测试场景。
  4. 抓优化后数据(After)

    • 同样在 Memory Profiler 上跑三遍,取平均:
      • Dalvik Heap 峰值 72 MB;
      • PSS 峰值 60 MB;
      • Allocated 总量 90 MB。
    • 把这些「优化后」数据也记下来。
  5. 计算优化比例

    • 我会选择最能代表整体内存压力的那个指标。假设我用 Dalvik Heap 峰值做对比:
      (120 MB – 72 MB) / 120 MB ≈ 0.40,也就是降低了 40%。
    • 如果你用 PSS 或 Allocated,总量差 / 优化前总量,也都能算出百分比。
  6. 验证和复盘

    • 不要只看一次结果,还要在不同手机、不同系统版本上跑,同样的脚本场景,确认在主流设备上都能稳定降到差不多的数值。
    • 再用 MAT(Memory Analyzer)或 heap dump 对比,看看对象数量是不是真的少了,缓存池命中率有没有上来。
    • 最后把这些「Before/After 的对比图」截下来,给团队一个清晰的优化报告:
      • 优化前 Dalvik Heap 峰值 120 MB → 优化后 72 MB,降幅 40%。
      • PSS 从 100 MB → 60 MB,降幅 40%。
      • GC 触发次数从平均 8 次 → 4 次,减少一半,流畅度明显提升。
  7. 面试回答要点

    • 强调量化:指定场景、固定动作、跑 Memory Profiler。
    • 强调可重复性:多跑几次取平均,多机型对比,避免偶发。
    • 强调对比指标:Dalvik Heap、PSS、Allocated,任选其一,统一口径。
    • 强调工具链:Android Studio Profiler + LeakCanary + MAT + heap dump 对比。
    • 最后给出一个百分比公式:
      (优化前 – 优化后) ÷ 优化前 × 100% = 优化比例(比如 40%)

讲讲Handler机制

  1. 核心三脚架:Looper、MessageQueue、Handler

    • Looper:每个线程如果想用消息队列,得先调用 Looper.prepare(),创建一个 Looper 和它挂钩的 MessageQueue。主线程系统帮我们做了这步。
    • MessageQueue:就是一个链表或小根堆,按时间戳、优先级存放 Message 或 Runnable
    • Looper.loop():启动一个无限循环,不停地从 MessageQueue 里取消息/任务,拿到后交给它对应的 Handler 去处理。
  2. 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),也就是你的那段重写逻辑。
  3. 主线程 vs 子线程

    • 主线程 Handler:无论你在 Fragment、Activity 中 new Handler(),背后都是拿到主 Looper,保证你任何时候发消息都跑到 UI 线程。最常见用法就是在后台线程里 handler.post { … } 更新 UI。
    • HandlerThread:如果你想给子线程也配一个消息循环,就用 HandlerThread。它内部自己调用了 prepare()/loop(),你拿它的 looper new 一个 Handler,就能往这个子线程发消息,做串行异步任务或定时重试。
  4. 为什么不用直接 Thread + sleep

    • Handler 带队列、能定时(delay)、能分优先级,能在不同线程间天然切换,而且消息时间比 Timer 精准,不会被直接抖动打断。
    • 而且一条消息一个 Message 对象,你也可以复用 Message.obtain()…sendToTarget(),减少分配压力。
  5. 常见坑和优化建议

    • 内存泄漏
      – 匿名或非静态内部 Handler 会隐式持有外部 Activity/Fragment,导致只要队列里没消费完,它就一直“拽”着 Activity 不回收。
      – 我们项目里都改成静态 Handler + WeakReference<Activity>,或者干脆用 Handler(Looper.getMainLooper(), callback)、把 Callback 里逻辑扔进去,解耦引用。
    • 滥用 postDelayed
      – 如果页面销毁前忘 removeCallbacksAndMessages(null),定时任务还在队列里,不仅会继续回调,也会泄漏。
      – 我会在 onDestroy() 里统一清空,或者结合 LifecycleObserver,在销毁时自动解绑。
    • 消息合并
      – 短时间内连续发相同 Message,很可能让 UI 一顿抖动、重复绑定。可以用 sendEmptyMessageAtTime 指定同一个 what,或者先 removeMessages(what) 再发,保证同一时刻队列里只剩最新一条。
  6. 小结

    • 启动:Looper + MessageQueue 在某个线程里无限循环。
    • 发消息:Handler 把 Message/Runnable 放队列。
    • 取消息:Looper.loop 取出,分发给 Handler.dispatchMessage,最后到你的 handleMessage/Runnable。
    • 场景:UI 更新、定时任务、子线程串行、资源清理都能靠它;注意生命周期、内存、消息合并这些细节。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值