ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Android点击图标启动应用:从触摸事件到SurfaceFlinger全链路解析

Android点击图标启动应用:从触摸事件到SurfaceFlinger全链路解析 指尖按住桌面图标松手图标放大又回弹下一秒应用内容铺满屏幕。整个过程不到一秒但躲在背后的是一次跨进程接力触摸事件要层层转交启动请求要去活动管理服务挂号新的进程要在Zygote里孵化窗口要等待WMS分配画布渲染好的帧还要等SurfaceFlinger点卯上屏。只要你做Android开发迟早会被这条链路里的某个环节卡住——要么看着桌面用力戳没反应要么启动白屏半天要么明明进程活着页面就是不出来。搞懂这条完整链路不是为了看懂内核是为了在问题冒头那一刻知道该去翻哪个日志、查哪层数据。这篇文章适合两类人一是刚接触Android Framework、想弄明白四大组件之外系统到底在干什么的开发者二是已经写过几年业务代码、每次排查启动问题都要靠猜的Android工程师。我会按一次点击从屏幕到图标的完整路径走一遍把ActivityManagerServiceAMS、ActivityTaskManagerServiceATMS、WindowManagerServiceWMS、Zygote、SurfaceFlinger这几个核心服务的职责和交接点讲清楚并给出可复现的排查手段。1. 触摸事件的链路里Launcher窗口是怎么被命中的1.1 从物理屏幕到InputDispatcher内核事件要过哪几道门很多人以为点击图标是从Android系统某个Service开始的实际上第一棒完全在系统最底层。手指触碰屏幕触摸屏控制器产生硬件中断内核的Input子系统把原始信号包装成input_event写入设备节点。随后系统进程里的InputManagerServiceIMS通过EventHub读取这些事件交给InputReader线程做加工——把坐标、按下时间、压力值整理成MotionEvent再交给InputDispatcher线程去分发。这里值得强调一个容易忽视的点Android的触摸事件不是发给所有窗口广播的而是精确地只发给一个窗口的InputChannel。InputDispatcher手里有一份窗口注册表里面记录着每个可触摸窗口的信息包括窗口区域、焦点状态、可见性。它根据点击坐标做一次命中测试把事件丢给命中的窗口。这个机制决定了后面要讲的很多点击穿透点击无效问题。1.2 窗口命中测试为什么透明拦截能让点击失灵窗口命中测试的核心数据在WMS里。系统里每一个窗口PhoneWindow、Dialog、PopupWindow、甚至系统状态栏在添加时都会在WMS创建一个WindowState里面保存了窗口的Frame位置和尺寸、TouchableRegion可触摸区域、Z-order层级等信息。InputDispatcher把坐标传给WMS查询时WMS会倒序遍历窗口列表——先从最上层窗口开始检查点击坐标是否落在TouchableRegion内落在哪个窗口就算命中谁。这里有三个开发者常踩的坑全屏透明Activity会“吃掉”点击如果某个窗口覆盖在Launcher上方且没有设置FLAG_NOT_TOUCHABLE点击坐标就会命中这个窗口。它哪怕完全透明底下的Launcher也收不到事件。很多通知栏弹出后桌面点不了的问题就是这么来的。FLAG_NOT_TOUCH_MODAL不等于穿透它只是把窗口区域之外的触摸事件分发给下层窗口窗口区域内的触摸依然优先给当前窗口。Launcher的桌面窗口本身也不是一个全屏View桌面上有壁纸区域、图标区域、Dock栏区域不同区域命中不同ViewGroup最终事件会从View树根节点一路dispatch到能响应的子View。1.3 Launcher响应点击从MotionEvent到startActivity事件到达Launcher进程后经历的是标准View事件分发流程Activity.dispatchTouchEvent → ViewGroup.dispatchTouchEvent → onTouchEvent → performClick → onClick。桌面上的每个图标是一个快捷方式绑定着一个Intent。Launcher的onClick回调会调用startActivitySafely给Intent加一个FLAG_ACTIVITY_NEW_TASK然后调用startActivity。也就是在这一步调用栈从Launcher进程跨进了系统服务Instrumentation.execStartActivity通过ActivityManager.getService().startActivity走了一次Binder调用把启动请求交到了真正的管事发那里——ActivityTaskManagerService。到这里第一条链路已经闭合触摸事件从内核串到输入系统再从窗口系统命中桌面图标最后变成一次Binder启动请求。肉眼可见的点击在系统里其实已经过了三四个进程。2. 启动请求进入ActivityTaskManagerServiceATMS在系统里到底管什么2.1 ATMS与AMS的分工为什么Android 10要把Task管理拆出来Android 10之前AMS是一个超级大管家Activity生命周期、Task栈、进程调度、内存管理、权限检查全塞在它里面。后来系统越来越复杂多窗口、分屏、画中画、多Display接踵而至单一体积已经难以维护于是Google做了拆分ActivityTaskManagerServiceATMS专门负责Activity、Task、Stack、Display的调度AMS保留进程管理ProcessRecord、内存管理、权限检查。对开发者来说这个拆分有个很实际的感知点如果你在Android 10设备上抓logcat看到的是ActivityTaskManager、ActivityTaskManagerService的日志而不是以前满屏的ActivityManager。排查Activity启动问题时去找ATMS日志更靠谱。2.2 Intent解析和匹配显式与隐式的处理差异ATMS收到启动请求后先要对Intent做体检。显式Intent自带ComponentName直接锁定目标Activity隐式Intent则要交给ActivityResolver去查PackageManagerServicePMS里的包数据按action / category / data逐项匹配IntentFilter。匹配完成后拿到ActivityInfo从这里知道目标Activity属于哪个包、哪个进程、什么启动模式。这里有一个Android 11API 30之后的重点坑如果应用targetSdk 30且没有在Manifest里声明queries隐式Intent查询外部包时拿不到结果系统会直接抛ActivityNotFoundException。很多点击图标没反应日志里也没崩溃的案例最后查到就是这一行的包可见性配置问题。2.3 进程身份核查为什么重复启动会被合并Intent解析完ATMS要决定目标Activity跑在哪个进程里。它先去AMS拿目标应用的ProcessRecord——AMS维护着一张进程表以包名 UID为key。如果目标进程已经存在比如应用已通过Service或ContentProvider启动ATMS就直接把Activity启动流程安排进这个进程如果进程不存在则触发进程创建。这个机制解释了两个常见现象热启动为什么快进程还在内存Activity只是被销毁了启动Activity无需fork进程省掉了最耗时的1/3流程。同进程的多个Activity同一个App的Activity共享一个进程启动第二个Activity只是创建ActivityRecord不会再额外拉进程。确认进程不存在后ATMS会把这个Activity的启动任务挂在准备中的Activity队列里然后向AMS请求创建进程。接下来就轮到Android系统里最特别的一个进程出场了——Zygote。3. Zygote的fork工厂新进程是怎么脱离母体开始呼吸的3.1 AMS-Zygote socket协议一次fork请求的完整参数AMS不会自己fork进程它通过本机socket向Zygote进程发送一条特殊命令命令里包含了要在子进程里执行的入口参数进程名niceName、UID、GID、seinfo、runtimeFlags、应用信息等。Zygote收到命令后进入forkAndSpecialize()逻辑基于自身进程镜像直接复制出一个子进程返回子进程pid。这里注意一个细节AMS传的Binder对象不是直接从AMS进程fork出来的而是Zygote父进程fork出子进程后子进程自己初始化Binder线程池。整个过程核心是先fork再通信新进程保留的是Zygote的地址空间而不是AMS的地址空间。如果你尝试直接从AMS fork子进程会带着AMS的Binder连接状态和系统服务的进程模型会完全错乱。3.2 预加载与Copy-on-Write为什么无法绕过Zygote单独启动进程Zygote最大的贡献不是fork本身而是预加载。系统开机时Zygote会主动加载Java虚拟机、核心类库ActivityThread、资源类、LocationManager等、常用系统资源、主题资源。所有后续App进程都从Zygote fork天然继承了这些预加载内容。fork采用Copy-on-Write写时复制父子进程共享同一块物理内存页只有当子进程要修改某个页时内核才复制一份。这才是Android能够快速创建上百个应用进程的秘密。如果你在开发自定义ROM时绕过Zygote直接启动进程会丢失所有预加载的类库和资源启动时间反而慢几十倍。3.3 新生进程的入口从entry point到ActivityThread.mainZygote fork出的子进程并不会直接执行App的类。它先进入runtime初始化流程RuntimeInit.zygoteInit启动Binder线程池使这个新进程具备跨进程通信能力然后反射调用android.app.ActivityThread.main()。ActivityThread就是Android UI线程的入口它内部会创建主线程Looper然后执行Looper.loop()。这里要插一句很关键的认知App进程不是从你的Application.onCreate开始的而是从ActivityThread.main开始的。SystemServer也是类似只不过入口是SystemServer.main。搞清楚这个先后关系才理解为什么在Application.onCreate里做耗时操作会推迟系统调度。4. ActivityThread的启动序曲进程反过来向AMS报到4.1 attachApplicationAMS等待的报平安信号新进程的ActivityThread.main创建完Binder对象后会顺着Binder调用一次ActivityManagerService.getService().attachApplication(mAppThread)把本进程的ApplicationThread接口传给AMS。这个调用非常关键它意味着新进程主动向AMS报到告诉AMS我起来了可以派活了。AMS拿到ApplicationThread引用后更新ProcessRecord里的进程绑定信息和线程引用。如果这一步失败或超时比如进程构造阶段崩溃AMS会丢弃这个进程标记等待中的Activity启动任务也会被中止。很多点击图标白屏后像没发生过一样的故障其实就断在这个报到环节。4.2 Application与ContentProvider的创建顺序以及容易踩坑的初始化点attachApplication成功之后AMS通过bindApplication把一堆信息打包发给新进程ApplicationInfo、ProviderInfoList、InstrumentationInfo、主题资源等。新进程在handleBindApplication里按严格顺序完成应用侧初始化创建ContextImpl和应用上下文加载Instrumentation安装ContentProvider并执行ContentProvider.onCreate创建Application对象执行Application.onCreate。特别注意ContentProvider的onCreate顺序在Application.onCreate之前。Google官方文档其实也提过当ContentProvider组件被实例化时它运行的宿主进程会先加载并初始化这个进程的Application对象。但在实际执行序列里installContentProviders先于mInstrumentation.callApplicationOnCreate所以Provider的onCreate确实会先跑。如果你在Application.onCreate里依赖某个Provider已经完成初始化一旦Provider里也要读Application状态就容易出现空指针或者奇怪的时序问题。我自己的习惯是任何两边都会用到的初始化代码放到一个独立的初始化类里做幂等处理而不是在Application和Provider里各写一遍。4.3 等待中的Activity如何被安排进进程AMS在确认新进程可用后会在准备中的Activity队列里找出属于这个进程的ActivityRecord通过realStartActivityLocked把启动消息发给新进程。这一步匹配使用的是进程记录的pid IApplicationThread。从此刻起控制权从系统服务回到应用进程应用收到LaunchActivityItemActivityThread的handleLaunchActivity开始真正创建Activity对象。等待中的Activity就像一个在候诊室挂号排队的病人终于等到医生应用进程正式开始问诊。5. ActivityRecord的舞台调度从onCreate到onResume系统消息怎么流转5.1 ActivityRecord的诞生与启动token的作用前面提到ATMS在启动流程一开始就为Activity创建了ActivityRecord它是AMS/ATMS内部对一个Activity实例的活动档案。ActivityRecord里保存了Intent、ActivityInfo、启动模式、所属Task、窗口TokenWindowToken等。首次启动的应用进程里ActivityThread收到LaunchActivityItem后通过performLaunchActivity反射创建Activity实例然后调用activity.attach(context, windowManager, token, ...)把系统侧token传给Activity。这个token在后面的窗口添加中会起到关联作用——WMS靠它确认这个窗口属于哪个ActivityRecord。5.2 LaunchActivityItem穿越Binder后做了什么LaunchActivityItem是一个ClientTransactionItem由ATMS通过ClientLifecycleManager发送。它在应用进程的TransactionExecutor里被解析最终纵向执行一套预定义生命周期序列onCreate → onStart → onPostCreate → onResume。有几个顺序经常被搞混onStart之后Activity已经可见但还没有窗口因为setContentView渲染出的只是View树还没有真正和WMS建立联系。onResume也不是窗口可显示的信号它只是生命周期状态标记。真正把窗口和WMS关联起来的动作发生在ActivityThread.handleResumeActivity里它调用了WindowManager.addView(decorView, layoutParams)。所以想在onCreate里截屏、测量DecorView尺寸拿到的大概率是0或者全屏高度因为这个阶段窗口还没有完成布局。必须等onWindowFocusChanged回调或者监听addOnPreDrawListener才能拿到稳定尺寸。5.3 resume逻辑与ViewRootImpl登场handleResumeActivity执行完onResume后把DecorView交给WindowManagerImplWindowManagerImpl内部创建ViewRootImpl并调用setView。这个setView才是应用窗口真正向系统注册的开始ViewRootImpl通过WindowSession.addToDisplay发起跨进程调用把窗口信息交给WMS。到了这一步Activity生命周期这套舞台调度基本结束接下来是窗口体系的舞台。一个Activity从诞生到真正被系统认可为一个窗口需要经过ActivityRecord系统侧档案、token两边通信凭证、DecorView应用侧视图、ViewRootImpl应用窗口大脑四个对象协同。6. WMS的窗口账簿addWindow到Surface分配的完整过程6.1 WindowManagerGlobal.addView应用侧先记账再向WMS报备你可能以为应用进程的窗口注册是直接调WMS.addWindow实际上应用侧先在自己进程里维护了一个窗口列表WindowManagerGlobal.mViews。每次addView它把View、LayoutParams、ViewRootImpl记录下来然后通过ViewRootImpl的setView → requestLayout → mWindowSession.addToDisplay把窗口信息同步到WMS。了解这个应用侧记账的意义在于如果你在代码里用WindowManager.addView加了一个自定义弹窗但忘记为它设置正确的token类型应用进程可能没报错但WMS会因为token非法而拒绝。很多悬浮窗、弹窗无法显示的bug本质是“应用侧以为注册了WMS侧查无此窗”。6.2 WMS如何确定窗口层级type、token与z-orderWMS里维护着一整套窗口树。新窗口到来时要经过合法性检查、权限检查、token检查确认它是有效的Activity窗口/SurfaceControl窗口然后确定它的Z-order。对普通应用窗口来说层级主要由LayoutParams.type决定TYPE_APPLICATION是普通Activity窗口层级在应用列表里默认。TYPE_APPLICATION_PANEL是依附于Activity的对话框窗口默认在Activity之上。TYPE_APPLICATION_OVERLAYAPI 26是系统级悬浮窗权限要求严格层级高于普通应用窗口。SystemUI状态栏、导航栏的层级会更高这就是为什么系统UI永远不会被普通App窗口盖住。通过dumpsys window windows可以清晰地看到每个窗口的mAttrs与z-order顺序这也是排查我的窗口为什么没显示或我的窗口为什么挡住了别人的第一步。6.3 relayout与Surface分配starting window为什么先出现窗口注册完成后ViewRootImpl会在performTraversals里通过mWindowSession.relayout请求WMS为窗口分配Surface。这一步的relayout是一次同步调用会一直等到SurfaceFlinger那边把BufferQueue和Layer创建好返回SurfaceControl。返回后应用侧才拿到可以绘制的画布也就是PhoneWindow的Surface。这里有一个很重要的系统设计在应用窗口还没准备好之前系统会先为Activity显示一个StartingWindow启动预览窗口。它由系统进程在WMS里直接创建背景取自应用主题的windowBackground。所以你的启动闪屏如果显示的是白色多半是主题windowBackground用了白色或者默认Theme背景是白底如果设置成了透明背景启动瞬间就会看到黑屏/白屏透明窗的叠加真正属于自己的首帧画面一旦readyWMS会remove掉StartingWindow换成应用窗口。很多应用启动一闪白屏/黑屏的问题优化思路根本不是去改启动Activity的Splash逻辑而是先检查LaunchTheme里的windowBackground和windowIsTranslucent。7. 上屏前的最后接力渲染管线与SurfaceFlinger的合成7.1 View绘制到缓冲区Choreographer与vsync怎么配合应用拿到Surface后剩下的工作就是画和交。主线程里ViewRootImpl执行performTraversals依次做measure、layout、draw。硬件加速开启之后draw阶段生成的其实是显示列表DisplayList和绘制指令真正的GPU提交在RenderThread里进行。绘制节奏由Choreographer协调。Choreographer注册vsync信号每个vsync周期依次触发输入事件处理、动画、视图遍历、绘制。这套节拍意味着你的每一次绘制都是和屏幕刷新率对齐的。如果某个阶段耗时超过一个vsync周期16.6ms这一帧就会延迟到下一个周期表现为掉帧。很多启动慢的App瓶颈就在Application.onCreate里的阻塞操作导致首帧遍历迟迟排不上队。7.2 BufferQueue的生产消费模型只要生产者比消费者慢一拍App端Surface背后是BufferQueue采用典型的生产者-消费者模型。应用进程是生产者调用dequeueBuffer取出空闲buffer绘制并提交SurfaceFlinger是消费者消费完buffer后会归还给队列。系统为每个窗口创建多个buffer槽位形成双缓冲/三缓冲。一个容易理解的比喻应用是厨师SurfaceFlinger是服务员。厨师炒好一道菜放进备餐台服务员按固定节奏把菜端给客人。如果厨师炒得太慢服务员只能重复上老菜那就是掉帧如果厨师比服务员快很多备餐台满员厨师就得等着那就是缓存阻塞。7.3 SurfaceFlinger合成时机为什么掉帧发生在vsync之后SurfaceFlinger不会拿到一帧就立刻上屏它每次在vsync唤醒后把所有图层Layer按照Z-order整理判断哪些需要GPU合成、哪些可以直接交给硬件合成器HWC合成最终通过Display接口送到屏幕。这个过程说起来简单但测量时要注意dumpsys SurfaceFlinger --list可以列出所有Layerdumpsys gfxinfo 包名 framestats可以看App每帧在应用侧的耗时真正落到屏幕上的时间点还得靠Perfetto的Display Pipeline Event查看SF侧时序。到这里点击图标到应用显示的整条链路已经闭环触摸事件→Input系统→Launcher→ATMS→AMS→Zygote→新进程→ActivityThread→Activity→WMS→Surface→Choreographer→BufferQueue→SurfaceFlinger→屏幕。中间任何一环出现异常表象可能都是启动慢、白屏、点击无响应但各自的日志特征完全不同。8. 从可观测指标到常见故障一条链路上最常出问题的几个环节8.1 用am start -W把启动耗时拆开看最直观的启动耗时测量工具是am start系统命令。用法如下adb shell am start -W -n com.example.app/.MainActivity执行完会输出一段耗时统计要留意两个值TotalTime从ATMS收到启动请求到App窗口完成addView并交给WMS的总耗时覆盖进程创建、Activity创建、首帧绘制准备。WaitTime从请求发出到AMS返回结果的总耗时包含系统切换Activity的等待时间通常大于或等于TotalTime。实际开发中很多人只盯着TotalTime但更靠谱的拆分方式是用Perfetto抓trace从trace里能看到process start、activity start、traversal等具体阶段的起止时间。比如冷启动时process start阶段如果超过300ms基本是Zygote fork或Binder通信异常如果进程创建很快但直到handleResumeActivity阶段才耗时过长问题大概率在应用自身初始化。8.2 点击图标没反应、白屏黑屏、空进程被杀各自指向哪一层我整理了一份快速定位表按现象排查通常能省一半时间现象可能断点优先排查对象点击图标完全无反应无崩溃日志Intent解析失败ATMS未进入启动流程adb shell dumpsys activity activities看start请求是否到达检查logcat里ActivityNotFound点击后进程创建成功但界面一直白屏主线程阻塞 / Application初始化太慢 / App窗口未加入WMSPerfetto看traversal时间检查Application.onCreate耗时用dumpsys window windows确认窗口是否存在启动瞬间黑屏后恢复LaunchTheme.windowBackground透明/黑底查看Manifest里LaunchTheme设置不透明背景进程反复被重建切后台回来Activity重新创建进程被lmkd回收或Task被移除查看dumpsys activity里进程adj和Reason排查Service保活策略是否误判点击后图标闪一下Activity从未onCreateContentProvider初始化崩溃导致进程启动失败adb logcat -b crash查看崩溃信息重点查Provider构造和onCreate窗口一直在但内容不更新渲染线程卡死 / BufferQueue消费失败dumpsys gfxinfo看帧耗时dumpsys SurfaceFlinger看Layer有无更新这表里的逻辑是顺着链路来的现象发生在哪个阶段就去看那个阶段的负责人。点击无反应先查ATMS白屏查进程和应用初始化窗口存在但内容不刷新才轮到渲染管线。8.3 调试工具清单与个人经验最后分享一套自己常用的排查组合拳。从最简单的命令开始自下而上# 1. 查看Activity任务栈和进行状态 adb shell dumpsys activity activities # 2. 查看窗口系统里到底有哪些窗口 adb shell dumpsys window windows # 3. 查看进程中的所有Surface/图层信息 adb shell dumpsys SurfaceFlinger --list # 4. 查看指定包最近帧耗时 adb shell dumpsys gfxinfo com.example.app framestats个人经验先看窗口再看Activity最后看帧。窗口层能看到你的Activity窗口有没有加入WMS、z-order对不对、Surface是什么状态。如果窗口层没有你的窗口Activity生命周期再正常也没用多半是addWindow被拒绝如果窗口在但帧不更新才值得去看gfxinfo和SurfaceFlinger。别一上来就抓systrace信息量太大反而容易漏掉最基础的窗口注册问题。我在这条链路上折腾过最久的一次是桌面点击图标后偶尔无响应。现象是应用进程创建成功ActivityRecord创建成功但窗口一直没有加入WMS。排查了很久dumpsys window也没看到那个窗口最后在ATMS日志里发现是TaskSnapshot相关的窗口token异常导致ActivityRecord的token没有正确注册到WMS窗口addWindow时被拒。如果你以后遇到类似Activity活着但窗口出不来优先验证token链路再去怀疑SurfaceFlinger。顺着这条链路的思路你其实可以把很多看似无关的Android机制都拆成事件链路来理解比如摇一摇交互就是一条从传感器到应用触发操作的链路权限弹窗是一条从AMS到WindowManager再到界面的链路。摸清链路的工作方式远比死记几个API更值钱。最后再分享一个小技巧adb shell dumpsys activity activities的输出会直接标注当前聚焦的Activity和它的状态排查启动问题时最喜欢看这两行。多抓几次冷启动的dump你能清晰看到ActivityRecord从INITIATING到RESUMED的状态跃迁——当你能顺着状态机说出现在卡在哪一步的时候这条完整链路就算真正吃透了。
返回列表