ARTICLE DETAIL

资讯详情

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

Android Framework学习路线:从Binder到AMS的系统级进阶指南

Android Framework学习路线:从Binder到AMS的系统级进阶指南 对于大多数做Android开发的人来说天天跟Activity、Fragment、RecyclerView打交道写起来行云流水但一旦涉及到系统级问题——App启动慢、进程被杀、ANR、混合开发通信卡顿就很容易束手无策。原因很简单你只站在了应用层看问题而真正决定App性能和稳定性的核心逻辑全部藏在framework层。这篇文章我想从自己的实际学习经历出发把Android framework这条学习路径上最关键的环节、最容易踩的坑、以及真正值得你花时间研究的部分完整梳理一遍。不管你是刚入行的初级开发还是做了几年业务想突破技术瓶颈这篇内容都值得你认真看完。1. 为什么说framework是Android开发的“内功心法”很多人有个误区觉得framework是系统工程师才需要学的东西做应用开发用不上。这个想法在我带团队这几年来看实际上是大错特错的。1.1 应用层开发的“天花板”就在framework先抛一个很现实的问题你写了一个列表页加载50张高清大图滑动起来掉帧严重。你去优化图片加载库、调整复用逻辑、减少布局层级……能做的都做了帧率还是上不去。这时候如果你能看懂framework层的绘制流程就会明白问题可能出在CPU栅格化上——每一次滑动触发的invalidate、measure、layout、draw这四步的耗时直接决定了你的帧率上限。再比如你定位App启动慢的问题如果只看应用层代码Activity的onCreate、onStart、onResume都写得无可挑剔但启动时间就是比别人长。这时候你需要往framework层看一下——system_server进程里AMS是怎么调度你的Application的、ContentProvider的初始化顺序、Binder调用的耗时。这些内容不看framework层永远不知道问题出在哪。1.2 从“会用API”到“理解系统设计”的分水岭说实话绝大多数应用开发者的水平停留在“会用API”的层面。知道startActivity能跳转页面但不知道startActivity经过了多少次Binder调用才最终走到ActivityThread的handleLaunchActivity知道Handler可以切换线程但不知道Handler背后的Looper和MessageQueue是怎么跟主线程的生命周期绑定的。framework层的学习真正带给你的是让你从“调用者思维”升级为“设计者思维”。你会理解为什么Google要设计Binder这种跨进程通信机制而不是直接用Linux的socket或者共享内存你会理解为什么四大组件能够在不同进程之间协同工作你会理解为什么系统需要Zygote进程来孵化所有的应用进程。这些理解不能直接转化为一行代码的产出但决定了你遇到问题时的判断力。1.3 面试场上真正拉差距的内容如果你到大厂面试过高级开发岗应该见过类似这样的问题Activity启动过程中AMS和ActivityThread是怎么协作的Binder一次拷贝的原理是什么为什么说Binder比共享内存更安全系统是怎么杀死后台进程的lmkd的判断依据是什么自定义View的onMeasure和onLayout的调用时机和条件是什么这些问题无一例外全都在考察framework层的理解深度。业务代码写得再漂亮面对这类问题时答不上来面试官心里对你的定级就会自动降一档。我做技术面试官这几年有一个很直观的感受在简历上写“精通Android”的人很多但真正能讲清framework层原理的候选人比例不超过10%。这10%的人往往拿到的offer级别和薪资都会明显更高。1.4 系统级问题排查能力的根基日常开发中你会遇到各种各样的疑难杂症App在后台莫名其妙被杀多任务切换回来直接重启某些机型上Notification不显示但代码逻辑没有任何问题应用内WebView加载网页偶发崩溃定位不到原因多进程架构下一个进程崩溃导致另一个进程数据丢失这类问题的根因往往不在应用层而在framework层的某个调度逻辑或者系统服务里。如果没有framework层的知识储备你连排查的方向都找不到只能靠网上搜帖子和试错效率极低。2. 学习framework前必须搞定的基础储备framework层学习有一个特点它不是一个可以“跳着学”的知识领域前面的基础不牢固后面的内容越看越糊涂。我自己最开始学framework的时候就是因为基础没打牢导致看AMS的源码看到崩溃效率非常低下。2.1 Java反射和动态代理framework层大量使用了反射和动态代理机制。尤其是动态代理Binder的AIDL实现里就用了类似的思想理解动态代理的原理对理解跨进程通信特别重要。你需要熟练掌握Class类的加载机制和反射获取类的构造器、方法、字段Proxy和InvocationHandler怎么配合使用Android中的Hook技术本质对系统方法的代理和替换这块不熟练的话看framework源码的时候很多地方会卡住。2.2 设计模式特别是观察者和模板方法整个framework就是一套设计模式的经典案例库。举几个典型的观察者模式ContentObserver监听数据库变化、BroadcastReceiver接收广播、View的setOnClickListener模板方法模式Activity的生命周期方法就是典型的模板方法系统定义好了框架你在里面填具体的实现单例模式几乎所有的系统服务AMS、WMS、PKMS都是单例Binder可以理解为一套基于代理模式的跨进程通信框架如果你对设计模式不熟悉建议先花一周时间把常用的设计模式过一遍不要求背定义但看到代码能认出来是哪种模式、解决了什么问题。2.3 操作系统的进程、线程和内存管理framework的核心就是管理进程和线程。你需要了解Linux进程管理的基本概念进程的创建fork、进程调度、进程优先级线程与进程的关系、线程同步机制synchronized、Lock、volatile虚拟内存、物理内存、内存映射的基本概念进程间通信的基本方式管道、消息队列、共享内存、Socket其中共享内存是Binder的底层基础理解Binder的高效性就必须理解内存映射的原理。2.4 熟练使用AIDLAIDL是理解Binder机制最直观的入口。你在应用层自己写一个AIDL接口生成对应的Java代码然后去看看这些代码的结构——IInterface、Stub、Proxy、transact、onTransact这些概念认全了之后再去看system_server里的Binder调用就轻松多了。建议动手做一个小实验写一个AIDL的跨进程示例一个客户端进程、一个服务端进程手动把生成的代码打开看一遍搞清楚Stub和Proxy各自做了什么事。2.5 源码阅读工具的准备阅读framework源码工具链很重要Android Studio主要用来阅读framework的Java源码配合sources.jar或者AOSP源码导入Sublime Text阅读大型工程文件时比AS更流畅尤其适合看大型的C和Java混排源码Source Insight老牌的源码阅读工具看跨文件调用关系特别方便很多人看AOSP源码的首选我个人习惯是用Android Studio看单个类的结构和继承关系用Source Insight看全局的调用链和交叉引用。工欲善其事必先利其器这部分基础工作没做好后面学习效率会差很多。3. framework核心机制逐个拆解从Binder到AMS框架的基础打好之后就可以开始逐个攻克framework的核心机制了。我按照我自己的学习顺序来拆解这个顺序是经过验证的前后知识有依赖关系不建议打乱。3.1 Binder——整个Android系统的“神经网络”Binder是Android系统中最核心的机制没有之一。四大组件之间的一切通信、应用进程与系统进程的一切交互底层都是Binder。学习Binder要抓住一条主线一次跨进程调用数据是怎么从A进程到B进程的。具体来说你需要搞懂这几个关键点为什么Android要设计Binder而不是直接用Linux自带的IPC方式Binder驱动在Linux内核中的作用mmap内存映射这是Binder只需要“一次拷贝”的关键ServiceManager是怎么管理系统服务注册和查询的AIDL生成代码的完整运行流程网上讲Binder的文章很多但大多数停留在概念层面。建议你看两本硬核内容《Android系统源代码情景分析》里Binder相关章节以及罗升阳博客的Binder系列文章配合把源码里的binder.c读一遍C层的BnInterface和BpInterface也过一遍。这一部分啃下来需要时间但啃下来之后看framework其他部分会顺畅很多。3.2 Handler机制——理解主线程的“心脏”Handler机制是framework中最“亲民”的知识点也是最容易被面试官问到深处的点。核心要搞清的是Handler、Looper、MessageQueue、ThreadLocal这四者的关系以及这个机制是怎么实现线程间通信的。我的理解套路是Looper是老板MessageQueue是任务队列Handler是员工和老板之间的通信工具。主线程创建的时候就通过Looper.prepareMainLooper()创建了一个Looper并且通过Looper.loop()进入了无限循环不断从MessageQueue中取出消息来执行。几个值得深挖的点ThreadLocal是怎么保证每个线程持有自己的Looper实例的MessageQueue的next方法是怎么阻塞和唤醒的IdleHandler的触发时机和使用场景同步屏障机制SyncBarrier是怎么保证UI绘制消息优先执行的epoll机制在MessageQueue阻塞唤醒中的应用最后一个问题是最容易被忽略的因为MessageQueue的阻塞和唤醒底层是基于Linux的epoll机制来实现的。理解了epoll才能理解为什么Handler机制既高效又不浪费CPU。3.3 AMS和ActivityThread——“一记耳光”式的顿悟Activity的启动流程是framework学习中绕不过去的大山。这座山分为两部分system_server进程里的AMS以及应用进程里的ActivityThread。整体流程是应用进程通过Binder调用AMS的startActivity方法AMS完成一系列的校验和调度之后通过ApplicationThread这个Binder接口回调应用进程最终由ActivityThread的handleLaunchActivity执行真正的Activity创建和生命周期回调。学习这一块很容易陷入一个误区死抠每一行代码。我的建议是先把主流程走通再逐步展开细节。主流程需要记住的核心步骤ActivityManagerService.startActivityActivityStarter.execute——处理Intent解析、任务栈管理等ActivityStackSupervisor.startActivityLocked——分配Task和栈ActivityStack.startActivityLocked——执行栈操作ActivityStackSupervisor.realStartActivityLocked——真正启动Activity通过Binder调用ApplicationThread.scheduleLaunchActivityActivityThread.main中通过handler分发最终调用Activity的onCreate等生命周期方法Android 10和Android 12的源码在Activity启动这块改动很大引入了ActivityTaskManager等新模块建议以某一个主要版本为主深入学习其他版本对比差异即可。我自己的经验是把这一条链路画在一张A4纸上每一步标注涉及的关键类、关键方法、进程变化反复默写直到能不看资料完整画出来。这个笨办法看起来费时间但效果非常扎实。3.4 四大组件的工作过程把Activity启动流程搞清楚之后其他几大组件就简单多了因为套路一样——都是通过Binder跟AMS同步状态。Service的启动过程、ContentProvider的创建和CRUD、BroadcastReceiver的注册和接收思路完全一致只需要重点看跟Activity流程不一样的地方Service的生命周期方法是从哪个Binder接口回调的ContentProvider的onCreate在Application的onCreate之前还是之后静态广播和动态广播的注册与分发路径差异这四个组件串起来学效率是最高的。3.5 WMS和View的绘制体系View相关的framework知识对你的日常UI开发帮助最直接。核心要搞明白的是你写的布局文件是怎么变成屏幕上的像素的。需要掌握的知识点ViewRootImpl的创建和performTraversals的执行时机measure、layout、draw三步的完整流程和触发条件Choreographer的角色——它是怎么跟Vsync信号配合来驱动UI刷新的SurfaceFlinger是怎么把各个App的图像数据合成并显示到屏幕上的触摸事件从屏幕到App再到具体View的分发流程其中Choreographer和Vsync的知识特别重要。你滑动列表时候的掉帧问题大多数人只能归因于“布局太复杂”“图片加载太慢”但真正内核级的原因是UI绘制没有在Vsync信号到来时及时完成。理解了这套机制做UI性能优化时你就能精准定位问题而不是瞎调参数。3.6 Zygote、SystemServer和App进程的启动最后一个大块头是系统启动流程开机从BootLoader到Linux内核然后init进程启动fork出Zygote进程Zygote孵化SystemServerSystemServer里启动一系列的SystemService包括我们前面提到的AMS、WMS、PKMS等最后由SystemServer通过socket请求Zygote fork出第一个App进程桌面。这部分的重点在于Zygote为什么用socket而不是Binder跟AMS通信——因为Binder依赖Zygote预先创建好的binder线程池鸡生蛋蛋生鸡的问题App进程被fork出来之后的初始化过程ActivityThread的main方法、主线程Looper的创建、Application的创建App进程之间是隔离的但代码是通过Zygote的预加载来共享的这就是为什么很多App启动时内存占用看起来都差不多4. 动手实战从“看源码”到“改源码”的跨越只看不练framework的知识永远是空中楼阁。我自己的经验是真正让我对framework理解产生质的飞跃的不是看完某本书、某篇文章而是亲手改了几次AOSP源码、跑通了几次系统编译。4.1 搭建AOSP源码编译环境AOSP源码编译是学习framework的第一道坎也是很多人放弃的地方。环境搭建确实有门槛但只要你耐心走一遍收获远大于痛苦。硬件要求方面我的建议是CPU至少8核16核以上编译体验好很多内存16GB起步32GB才能比较流畅地跑编译磁盘空间预留500GB源码加编译产物很容易超过这个数强烈建议SSD机械硬盘编译一次可能要等一晚上操作系统方面官方推荐Ubuntu 16.04到22.04之间都可以不同Android版本对JDK版本有要求这点需要特别留意。我第一次搭AOSP编译环境的时候在macOS上编译Android 8.1因为文件系统大小写敏感的问题折腾了整整两天。后来换成Linux环境基本上半天就能搞定整套环境。如果你也是Mac用户建议直接虚拟机装Ubuntu不要强行走Mac原生编译。4.2 第一个定制实验修改系统版本号环境搭好之后不要急着上手改大功能先做一个最小化的实验验证整个编译和刷机链路是通的。选择一个最简单的修改改frameworks/base/core/java/android/os/Build.java中的版本号把它改成你的名字或者一个自定义的字符串然后重新编译、刷机、开机验证。这一步的价值在于确认整条链路是通的源码修改、编译、生成system.img、刷机、开机查看效果。跑通一次之后你会对整个AOSP的工作流程有一个非常直观的感受。4.3 二次开发实验在Settings里加一个选项第二个实验我建议做一个稍微有点功能性的修改在系统设置里加入一个自定义的开关选项。具体实现路径是在frameworks/base/packages/SettingsProvider里增加一个默认值在frameworks/base/core/java/android/provider/Settings.java中定义新的常量在packages/apps/Settings里增加一个UI选项来读写这个设置项在framework层读取这个设置项实现某个逻辑的开关控制这个实验看起来简单但会让你完整走一遍framework层、SettingsProvider、SystemUI或者核心应用之间的数据流转对理解系统设置项的实现机制帮助巨大。4.4 进阶实验Hook系统方法验证原理如果你不想走完整的AOSP编译流程毕竟编译很耗时还有一个轻量级的实验方式用Android系统签名在自己的App里对系统方法进行反射Hook。比如你可以Hook掉ActivityManager的getRunningAppProcesses方法伪造进程列表Hook PackageManager的getInstalledPackages方法隐藏指定App通过反射替换系统Service的Binder对象实现“伪系统服务”这些实验不需要编译系统只需要在Android Studio里创建App工程然后用系统签名或者用Magisk模块的方式注入系统进程就能体验framework层Hook的原理。这种方式非常直观地验证了Binder和系统服务的工作机制同样是“亲手动手”的好办法。我强烈建议你把这两条路都走一遍一条是正统的源码修改编译烧录另一条是常用的运行时Hook。前者的价值在于让你理解系统是如何构建出来的后者的价值在于让你理解系统运行的动态机制两者互为补充。4.5 利用好既有的调试工具除了改源码framework学习还有一个很重要的手段利用现有的调试工具做“逆向观察”。最常用的是systrace通过抓取一段时间内的系统trace可以看到App启动过程中AMS、WMS、View绘制等各个阶段的时间消耗分布。你自己写一个启动页App用systrace抓一次然后对照代码看启动链路这种“观察真实系统行为”的学习方式效果特别好。另一个是dumpsys系列命令比如dumpsys activity top可以看当前界面的Activity信息和任务栈结构dumpsys package可以看包管理和权限的信息dumpsys window可以看窗口层级结构。每次用完dumpsys再去源码里找到对应Service的实现代码读一读收获会非常扎实。5. 全链路思维从应用进程到系统服务的调用链路分析法framework学习到一定阶段后你会发现最大的难点不是某个具体机制的细节记不住而是当问题出现时你不知道该从哪个环节入手去排查。我自己用的一个高效方法是链路分析法任何一个应用层行为都能对应到一条从应用进程到系统服务的完整调用链。把这条链路想清楚问题就解决了一半。5.1 启动一个App的完整链路画像以“点击桌面图标启动一个App”为例完整链路是桌面App首先通过Binder调用system_server进程中的AMS.startActivityAMS进行必要的校验权限、Activity是否存在等AMS确定这个App的进程不存在通过Socket向Zygote进程发送fork请求Zygote fork出新进程同时预加载到新进程的类包括ActivityThread新进程入口是ActivityThread.main创建主线程Looper然后通过Binder向AMS报告“我已经启动了”AMS收到之后通过ApplicationThread这个Binder接口回调进程通知它创建Application和ActivityActivityThread创建Application对象调用onCreateActivityThread创建MainActivity通过Instrumentation调用Activity的onCreate、onStart、onResume页面绘制内容通过SurfaceFlinger合成并显示到屏幕这一条链上有三次进程切换桌面到system_server、system_server到Zygote、system_server到新App进程。你仔细体会这三次切换的逻辑会发现整个framework的设计核心就是通过Binder通信来协调不同进程的工作最终达成一个完整的用户行为闭环。5.2 源码阅读的“从结果倒推”技巧很多初学者拿到源码不知道从哪里读起我的建议是从结果倒推。比如你想搞清楚“App是怎么被系统杀死的”可以先找到结局——一个进程被杀通常会打印日志。用logcat抓到“Killing xxxx (adj x)”这类日志顺着日志里的方法名就能反推出lmkd或AMS里的杀进程逻辑。再比如你想搞清楚“通知是怎么显示到状态栏的”可以从发送通知的API入手在NotificationManager.notify方法上打断点然后一步步跟进到NotificationManagerService里的处理逻辑再跟进到SystemUI如何通过监听器接收到通知数据更新UI。这种“从结果倒推、以断点追踪路径”的方式比对着源码一行行读效率高得多而且学到的是真正的调用链不是一个孤立的类。5.3 断点日志的打印技巧framework源码中大量代码在system_server进程中运行很多情况下你没法直接给system_server打断点调试所以日志打印是最核心的定位手段。常用的手段修改framework代码加入Log.i语句在关键位置加Log.wtf打印堆栈Log.wtf会输出当前线程完整的调用栈用Log.getStackTraceString(new Throwable())手动获取调用栈在bind到system_server的Binder接口上打日志可以看到所有的Binder调用来源我排过很多棘手的系统级问题最后都是靠一两个关键堆栈日志定位到根因的。系统源码的日志打印远比想象中少加日志、跑测试、分析日志这个循环就是framework开发的日常。6. 那些年我踩过的坑framework学习中的常见误区我见过太多人在framework学习这个方向上花了大量时间却收效甚微几乎都是踩了下面这些坑。我用自己的经历把它们列出来希望你能避开。6.1 死磕编译环境忽略了知识本身这是一个特别常见的坑很多人搭AOSP编译环境就花了两三周期间不断遇到磁盘不够、内存不足、JDK版本不对、依赖库缺失等问题折腾到心态爆炸然后宣布“framework太难了放弃了”。其实编译环境是一次性的工作慢点没关系但一定要控制时间预算。我的建议是如果搭建环境超过三天还没搞定就先用“代码阅读断点调试”的方式先学起来不要卡在这上面。AOSP编译环境的坑网上有大量的解决方案照着做就行不需要自己琢磨。6.2 只见树木不见森林陷入源码细节出不来很多人在读AMS源码的时候看到某个方法几百行就非要每一行都搞清楚才肯往下走。结果一周过去了还在ActivityStarter里面打转。frameowrk源码非常庞大如果每个细节都要死磕这辈子都读不完。正确的姿势是先建立主干再填充枝叶。每一块机制的主干是什么呢我给你一个参考Binder的主干是一次跨进程数据流转的过程Handler的主干是消息从生产到消费的过程Activity启动的主干是状态从App到AMS再到App的回环View绘制的主干是measure/layout/draw三件套的触发链先把主干画出来代码细节是用来验证和补充主干理解的不是用来阻碍你前进的。6.3 只读Java层不碰Native和KernelAndroid framework层往上到App、往下到Native和Linux内核是一个完整的分层体系。只盯着frameworks/base下的Java代码看很多机制的理解是不完整的。最典型的就是Binder如果你只看Java层的AIDL代码永远无法真正理解“一次拷贝”是怎么实现的。你必须看到C层的BpBinder/BBinder、看到Kernel层的binder.c里binder_transaction函数对数据的处理才会有那种“原来如此”的顿悟。我建议你把Java层作为入口理解业务逻辑然后深入JNI层理解Java与Native的衔接再往下的Kernel层不必逐行读但关键数据结构要认识。6.4 不动手写代码、不修Bug只看书看博客framework是工科知识不是文科背诵。只看书、看博客不动手验证永远都只能停留在“好像懂了”的层面。我特别推崇的一种学习方式就是自己给自己出题然后去源码里找答案。比如当系统内存不足时是谁决定先杀哪个进程发送一个有序广播中间某个接受者调用了abortBroadcast后续的接收者还能收到吗View.post和Handler.post区别是什么原理上差在哪你能把这类问题答清楚说明你真的理解了答不清楚的地方就是你的知识盲区重点去翻源码补齐。这个过程就是在给自己“打补丁”。6.5 学了不用知识快速遗忘framework知识遗忘速度极快特别是那些你不常用的部分。对抗遗忘最好的方式是用起来——在日常工作中刻意地去运用framework知识。比如你遇到一个偶现的UI卡顿可以分析是不是主线程被某个耗时的Binder调用阻塞了遇到一个内存泄漏可以深入想一下是不是AMS或PMS持有了某个应用对象的引用遇到进程被杀的Bug可以主动去查lmkd的adj值是怎么算出来的。当framework知识能帮你在实际项目中定位一两个真实问题之后你就再也不会忘记这套知识的框架了——因为你已经在实战中把它用活了。7. 从入门到精通的路线图和自我检验清单最后给一张我整理的路线图以及每个阶段的自我检验清单。你可以对照这个清单看自己目前处在哪个阶段下一步该往哪个方向用力。7.1 四阶段路线图阶段一基础扫盲期1-2个月目标理解Android系统的整体架构建立framework知识的地图框架。掌握Android的系统架构分层应用层、framework层、Native层、Kernel层看懂Android系统启动流程的整体框架能说清Binder、Handler、AMS等核心组件的核心职责阶段二核心机制深耕期2-4个月目标把Binder、Handler、AMS、WMS、View体系这五个核心机制逐个吃透。能画出Activity完整启动流程的时序图能讲清Binder在一次数据传递中的完整路径能分析一个View从创建到显示的完整流程阶段三源码实践期2-3个月目标结合AOSP代码进行验证和二次开发。能独立完成AOSP源码的下载、编译、刷机能在系统源码中修改代码并验证效果能通过调试技能定位系统级问题阶段四系统架构融合期长期目标把framework知识融会贯通能够从系统层面理解和设计架构。遇到问题能快速定位调用链找到对应系统模块能理解不同Android版本系统机制的变化和演进逻辑能结合framework原理优化应用层架构解决深层次的性能和稳定性问题7.2 关键能力自测清单对照下面这些问题如果你大部分能不看资料回答上来说明你已经真正迈入framework的大门了Binder为什么比传统的IPC方式更适合Android系统Android启动过程涉及哪几个进程它们的父子关系是什么一次Activity启动中App进程和system_server进程之间发生了多少次Binder调用系统是怎么标识一个进程的优先级的adj是如何计算的View.post为什么能够保证在View绘制完成之后再执行一个App进程被杀掉后它的任务栈和Activity记录是怎么被恢复的主线程的Looper如果死循环了为什么App不会变成无响应7.3 最后一个建议找到一个“载体项目”如果让我给一个最重要、最核心的建议那就是找一个新的Android“载体项目”来承载你的framework学习。这里说的载体项目不是那种典型的CRUD项目而是你真正感兴趣、愿意长期投入维护的项目。它可以是一个精简的开源ROM、一个系统优化工具或者一个基于Xposed的模块。像我早期的载体项目就是自己维护的一个纯净版ROM。为了做这个ROM我需要深入了解开机流程、系统裁剪、权限控制、性能调优等一系列framework的知识。每一次深挖一个知识点都是为了解决ROM中一个具体的问题。这种“带着问题去学习”的方式效率比漫无目的地翻源码高出好几倍。学framework是一个需要长期投入的过程但它的回报也是长久的。当你能从系统层面上理解整个Android的运行逻辑时你再回头看应用层开发中的各种问题会发现自己多了一双“透视眼”。希望这篇文章能给你一个清晰的方向也欢迎在实践过程中碰到问题的时候回来一起交流。
返回列表