Android面试 —— 八股文(一)

一、Scrollview 嵌套 Button 当按下Button(出现按下效果), 此时滑动ScrollviewButton点击事件是否执行? 并说明事件处理流程?

结论: 当用户在 Button 上按下并滑动 ScrollView 时,Button 的点击事件不会执行

虽然按下瞬间会显示按下效果(pressed 状态),但一旦滑动距离超过系统阈值(TouchSlop),ScrollView 会拦截后续触摸事件,Button 收到 ACTION_CANCEL 并取消按下状态,最终 onClick 不会被触发。

事件处理流程详解

  1. ACTION_DOWN

    • 触摸点落在 Button 区域。
    • ScrollViewdispatchTouchEvent() 将事件分发给子 View:调用 ButtondispatchTouchEvent()
    • Button 处理 ACTION_DOWN,返回 true(表示消费事件),同时进入 pressed 状态(显示按下效果)。
    • 此时 ScrollViewonInterceptTouchEvent() 尚未拦截,事件已被子 View 消费。
  2. ACTION_MOVE(用户开始滑动)

    • ScrollView 先收到 ACTION_MOVE,其 onInterceptTouchEvent() 被调用。
    • 检测到垂直滑动距离超过 ViewConfiguration.get(context).getScaledTouchSlop(),判定为滚动操作。
    • ScrollView 返回 true 拦截事件,并通知父容器(如果存在)停止向子 View 派发后续事件。
    • 同时,系统会向 Button 发送一个 ACTION_CANCEL 事件(通过 ButtondispatchTouchEvent())。
    • Button 收到 ACTION_CANCEL 后,清除 pressed 状态,不再等待 ACTION_UP 触发点击
  3. 后续 ACTION_MOVE / ACTION_UP

    • 事件不再传递给 Button,完全由 ScrollView 处理,执行滚动操作。
    • 最终没有 ACTION_UP 到达 ButtononClick 不会被调用。

特殊情况

  • 如果滑动距离很小 (未超过 TouchSlop),ScrollView 不会拦截,ACTION_UP 最终会传递给 Button,此时点击事件会正常执行
  • 若在 Button 上按下后立即水平滑动 (且 ScrollView 为垂直滚动),垂直方向未超阈值,也可能不拦截,点击仍可能触发。

总结流程图

text 复制代码
用户触摸 Button (DOWN)
    ↓
Button 消费 DOWN,显示按下效果
    ↓
用户开始滑动 (MOVE)
    ↓
ScrollView 检测滑动距离 > TouchSlop?
    ├── 是 → ScrollView 拦截事件
    │        向 Button 发送 CANCEL
    │        Button 取消按下状态
    │        ScrollView 处理滚动
    │        → 点击事件不执行
    └── 否 → 不拦截
              MOVE/UP 继续给 Button
              UP 时 Button 执行 onClick

二、 如何检测链表中是否存在环?

方法一:哈希表法(简单直观)

  • 思路:遍历链表,把每个节点的地址(或引用)存入哈希表。如果当前节点已经在表中,说明有环。
  • 复杂度:时间 O(n),空间 O(n)。
  • 适用场景:不限制空间时,代码简单,容易理解。

方法二:快慢指针(Floyd 判圈算法,最优解)

  • 思路 :使用两个指针 slowfastslow 每次走一步,fast 每次走两步。如果链表有环,它们最终会在环内相遇;如果 fast 走到 null,则无环。
  • 复杂度:时间 O(n),空间 O(1)。
python 复制代码
def hasCycle(head):
    slow = fast = head
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
        if slow == fast:
            return True
    return False

2. 如果存在环,如何找到环的入口点?

在快慢指针相遇后,再使用一个指针从头开始,另一个指针从相遇点开始,每次各走一步,它们再次相遇的地方就是环的入口。

python 复制代码
def detectCycle(head):
    slow = fast = head
    # 1. 判断是否有环,并找到相遇点
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
        if slow == fast:
            # 2. 找入口
            p1 = head
            p2 = slow
            while p1 != p2:
                p1 = p1.next
                p2 = p2.next
            return p1
    return None

原理 :设链表头到环入口距离为 a,环入口到相遇点距离为 b,相遇点到环入口距离为 c(即环长 L = b + c)。快慢指针相遇时,slow 走了 a+bfast 走了 a+b + kL。因为 fast 速度是 slow 的两倍,所以 2(a+b) = a+b + kLa+b = kLa = kL - b = (k-1)L + c。因此从头和相遇点同步走,一定会在入口相遇。

3. 如何"排查" -- 实际调试步骤

如果你在实际编程中遇到了链表环导致的问题(例如死循环、输出异常),可以按以下步骤排查:

  1. 打印节点地址/唯一标识

    遍历链表,打印每个节点的 id(Python 中用 id(node),Java 中用 System.identityHashCode(node))。如果某个 id 重复出现,说明有环。

  2. 使用快慢指针检测

    写一个简单的 hasCycle 函数,验证是否有环。

  3. 找到环的入口

    使用上面的 detectCycle 定位到环开始的位置,然后检查那个节点为什么被错误地指回了前面的节点。

  4. 检查链表构造代码

    回溯是哪个操作导致了环,常见原因:

    • 尾节点的 next 错误地指向了前面的节点。
    • 合并两个链表时,没有处理好复制与引用的关系。
    • 节点复用(对象池)导致意外形成环。

4. 扩展:求环的长度

在快慢指针相遇后,让 slow 不动,fast 继续每次走一步,再次相遇时走的步数就是环的长度。

总结

需求 方法 时间复杂度 空间复杂度
判断是否有环 快慢指针 O(n) O(1)
找到环的入口 快慢指针 + 同步指针 O(n) O(1)
打印环上所有节点 从入口开始遍历一圈 O(环长) O(1)

面试中推荐使用快慢指针,既能检测环,又能找到入口,且空间效率高。

三、View渲染流程:Measure → Layout → Draw

Android 的 View 渲染流程由 ViewRootImpl 驱动,通过 performTraversals() 方法依次执行 measurelayoutdraw 三大操作。这个过程决定了每个 View 的大小、位置和最终显示的内容。

总览

  • Measure(测量) :确定 View 的宽度和高度。
  • Layout(布局) :确定 View 在父容器中的位置(四个顶点坐标)。
  • Draw(绘制) :将 View 的内容绘制到屏幕上。

每个阶段都是递归的:父 View 会调用子 View 的对应方法,直到整棵树完成。

流程图

text 复制代码
ViewRootImpl.performTraversals()
    │
    ├── performMeasure()
    │       │
    │       └── DecorView.measure() → onMeasure()
    │               └── (递归) 子 View.measure() → onMeasure()
    │                       └── setMeasuredDimension()
    │
    ├── performLayout()
    │       │
    │       └── DecorView.layout() → onLayout()
    │               └── (递归) 子 View.layout() → onLayout()
    │                       └── setFrame()
    │
    └── performDraw()
            │
            └── DecorView.draw() → drawBackground() → onDraw() → dispatchDraw() → onDrawForeground()
                    └── (递归) 子 View.draw()

四、View渲染相关常见面试问题

  1. getMeasuredWidth()getWidth() 有什么区别?

    • getMeasuredWidth() 是测量阶段的结果,在 onMeasure 之后有效。
    • getWidth() 是布局阶段实际分配的宽度,在 onLayout 之后有效。两者一般相等,但在某些容器(如 ScrollView)中可能不同(测量尺寸可能大于实际屏幕,实际通过滚动显示)。
  2. requestLayout()invalidate() 的区别?

    • requestLayout() 会向上递归标记父 View 需要重新布局,最终触发 measure + layout(以及可能的重绘)。
    • invalidate() 只标记视图内容已改变,触发重绘(draw),不会重新测量和布局。
  3. 自定义 View 为什么一定要重写 onMeasure() 处理 wrap_content

    • 因为 View 的默认 onMeasure()AT_MOST 模式返回的是父容器允许的最大尺寸(等同于 match_parent)。为了正确支持 wrap_content,需要在 onMeasure 中计算内容实际需要的尺寸,并调用 setMeasuredDimension()
  4. dispatchDraw() 的作用是什么?

    • draw() 流程中负责绘制子 View。自定义 ViewGroup 可以重写它来改变子 View 的绘制顺序或添加额外绘制。
  5. 硬件加速下 Canvas 有何限制?

    • 某些操作不支持(如 drawPicture()、复杂的 clipPath() 等),或性能较差。可通过 View.setLayerType() 临时切换到软件渲染。
相关推荐
带娃的IT创业者2 小时前
围墙花园的隐形锁:当 reCAPTCHA 拒绝了“去谷歌化”的 Android 用户
android·隐私安全·人机验证·recaptcha·去谷歌化·grapheneos
awu的Android笔记2 小时前
Android 用户态实现 TCP 代理:从 SYN 到 FIN 的完整生命周期
android·tcp/ip
Geek_Vison2 小时前
技术实践:保险健康APP引入第三方小程序实战,如何构建一个安全可控的沙箱环境~
android·安全·小程序·uni-app·mpaas
卷帘依旧2 小时前
TypeScript高级应用能力举例
面试
2501_915918413 小时前
Python如何抓取HTTPS请求包的完整教程与代码示例
android·ios·小程序·https·uni-app·iphone·webview
. . . . .3 小时前
android开发
android
李剑一3 小时前
面试第一关!面试官:讲一下事件循环机制,宏&微任务,还有渲染时机
前端·面试
linweidong3 小时前
iOS 开发面试 50 个高频易混淆知识点详解
ios·设计模式·面试·cocoa·uikit·uiview·uistackview
程序员看世界3 小时前
Kotlin协程是如何实现优先级机制的
android·kotlin