1. 为什么Binder值得你花时间啃透做Android系统开发或者Framework层改造的同学迟早会撞上Binder这堵墙。应用层写多了觉得进程间通信不就是发个消息嘛但一旦你要改系统服务、加自定义Native Service、排查跨进程调用超时或者死锁Binder的细节就会像潮水一样涌上来。我见过太多人在这块栽跟头有人搞不清一次拷贝和两次拷贝的区别面试被问得哑口无言有人写的Native Service跑着跑着就卡死查了三天才发现是线程池打满了还有人改传输数据大小限制时改错了地方导致系统起不来。这篇文章就是把我这些年跟Binder打交道攒下来的东西系统性地倒出来。从架构设计到源码关键路径从AIDL生成的代码到底干了什么到实际调试中怎么用工具抓Binder调用我都会掰开揉碎讲。不管你是刚接触Android系统开发的新人还是已经能写系统服务但想搞清楚底层机制的老手应该都能从里面找到对自己有用的东西。文章会涉及不少源码层面的分析但我会尽量用生活化的类比先把道理讲通再落到代码上避免一上来就堆砌结构体定义。需要提前说明的是Binder的源码在不同Android版本间有差异我主要以较新版本Android 12-14这个区间的代码为基准来梳理核心机制从Android 8之后基本稳定差异主要在细节实现和部分数据结构上。如果你手头的代码版本不同思路是一致的对照着看就行。2. Binder的整体架构与设计哲学2.1 从一次跨进程调用说起假设你在应用里调用了getSystemService(Context.ACTIVITY_SERVICE)拿到ActivityManager然后调用了getRunningAppProcesses()。这一行代码背后发生了什么你的进程客户端需要跟system_server进程服务端通信让后者去查当前运行的进程列表再把结果返回给你。两个进程各有各的虚拟地址空间默认情况下互相看不见对方的内存怎么把请求传过去、把结果拿回来最朴素的做法是socket或者管道但Android没走这条路。Android选了一个叫Binder的机制它在内核里有一个驱动/dev/binder所有跨进程通信都经过这个驱动中转。客户端把数据写进驱动驱动把数据交给服务端服务端处理完再把结果写回驱动驱动再还给客户端。听起来跟socket差不多差别在于Binder在驱动层做了大量优化尤其是内存映射那一块让数据拷贝次数降到了最低。我习惯用一个类比来理解Binder的角色它就像一栋写字楼的前台。你想找楼里某家公司的某个人办事你不能直接闯进去得先到前台登记前台确认你要找的人存在、你有权限然后帮你把材料转交过去对方办完事把结果交回前台前台再给你。前台Binder驱动掌握着整栋楼所有进程的人员信息Binder实体和引用是唯一的权威中转站。2.2 Binder的四层架构从下往上Binder可以分成四层内核层Binder驱动代码在drivers/android/binder.c新版本拆成了多个文件比如binder.c、binder_alloc.c等。这一层负责最核心的数据传输、内存管理、线程管理、引用计数。它注册为一个字符设备上层通过ioctl来跟它交互。Native层libbinderC实现代码在frameworks/native/libs/binder/。这一层把ioctl的裸调用封装成了IPCThreadState、ProcessState、BpBinder、BBinder这些类提供了面向对象的接口。ServiceManager也在这层。Java层frameworks/base/core/java/android/os/下的Binder.java、BinderProxy.java、Parcel.java等。Java层的Binder对象通过JNI跟Native层对应android_util_Binder.cpp是桥梁。应用层AIDL生成的Stub和Proxy以及各种Manager类如ActivityManager。开发者日常接触最多的就是这一层。这四层里真正干活的是内核层和Native层Java层和应用层更多是封装和便利。理解Binder的关键在于搞清楚Native层怎么跟驱动交互以及一次调用在驱动里走了哪些路径。2.3 为什么不用socket、共享内存或者管道这是个经典问题面试也常问。简单对比一下机制拷贝次数安全性易用性适用场景socket/管道2次以上依赖上层协议一般通用IPC跨主机共享内存0次但需同步差需自行保证复杂大数据量传输信号量/消息队列2次一般一般简单同步Binder1次内核保证UID/PID好Android进程间调用Binder的核心优势有三个一是性能通过mmap把接收方的内存映射到内核空间发送方数据只需从用户空间拷贝到内核空间一次接收方直接读映射区省掉一次拷贝二是安全性内核在传输时会带上发送方的UID和PID接收方可以据此做权限校验socket做不到这一点发送方可以伪造三是面向对象Binder把跨进程调用伪装成了本地对象调用AIDL生成的代码让开发者几乎感觉不到进程边界。共享内存虽然拷贝次数更少但它没有内置的同步机制和权限控制用起来要自己处理一堆并发问题不适合Android这种需要频繁、小量、高安全要求的调用场景。2.4 Binder通信模型中的四个角色理解Binder必须搞清楚四个角色Client发起调用的进程。它持有服务端的一个Binder引用BpBinder调用transact()发起请求。Server提供服务的进程。它有一个BBinder实体收到请求后执行onTransact()返回结果。ServiceManager服务注册与查询的中心。所有系统服务启动时都要向ServiceManager注册客户端通过名字向ServiceManager查询拿到Binder引用后才能调用。ServiceManager本身也是一个Binder服务它的handle固定为0。Binder驱动内核中的中转站管理Binder实体和引用的映射关系负责数据搬运和线程调度。这里有个容易混淆的点Binder实体binder_node和Binder引用binder_ref是驱动里的概念。一个Server进程里的BBinder对象在驱动里对应一个binder_nodeClient进程拿到的BpBinder在驱动里对应一个binder_refbinder_ref指向binder_node。当Client调用时驱动根据binder_ref找到binder_node从而找到Server进程。3. 一次Binder调用的完整生命周期3.1 从Java层调用到Native层还是用getRunningAppProcesses()这个例子。在应用进程里ActivityManager实际上是一个IActivityManager的Proxy对象ActivityManagerProxy它实现了IActivityManager接口。当你调用getRunningAppProcesses()时Proxy会做这几件事创建一个Parcel对象_data把方法参数写进去。对于getRunningAppProcesses没有参数但会写入一个整数标识这是哪个方法INTERFACE_TRANSACTION或者AIDL生成的TRANSACTION_xxx。创建另一个Parcel对象_reply用来接收返回值。调用mRemote.transact(GET_RUNNING_APP_PROCESSES_TRANSACTION, _data, _reply, 0)。这里的mRemote是一个IBinder实际类型是BinderProxy。BinderProxy.transact()是一个native方法通过JNI调用到android_util_Binder.cpp里的android_os_BinderProxy_transact()。这个JNI函数拿到BinderProxy对应的Native对象BpBinder调用BpBinder::transact()。到这里调用从Java层进入了Native层。BpBinder::transact()会调用IPCThreadState::self()-transact()IPCThreadState是每个线程一个实例的它负责跟驱动打交道。3.2 IPCThreadState与驱动的交互IPCThreadState::transact()的核心逻辑是status_t IPCThreadState::transact(int32_t handle, uint32_t code, const Parcel data, Parcel* reply, uint32_t flags) { // 把data写入mOut status_t err data.errorCheck(); if (err NO_ERROR) { err writeTransactionData(BC_TRANSACTION, flags, handle, code, data, nullptr); } // 如果不需要等待回复oneway直接返回 if ((flags TF_ONE_WAY) 0) { // 等待回复 err waitForResponse(reply); } else { // 发送后不等待 err talkWithDriver(); } return err; }writeTransactionData()把数据打包成binder_transaction_data结构写入mOut这个Parcel。然后waitForResponse()会调用talkWithDriver()后者通过ioctl跟驱动通信status_t IPCThreadState::talkWithDriver(bool doReceive) { binder_write_read bwr; bwr.write_size mOut.dataSize(); bwr.write_buffer (uintptr_t)mOut.data(); bwr.read_size mIn.dataCapacity(); bwr.read_buffer (uintptr_t)mIn.data(); ioctl(mProcess-mDriverFD, BINDER_WRITE_READ, bwr); // 处理返回的数据 }BINDER_WRITE_READ是跟驱动交互的核心命令它同时携带了要写的数据和用来读的缓冲区。驱动处理完写请求后如果有回复会填充读缓冲区。3.3 驱动层的处理路径驱动收到BINDER_WRITE_READ后会解析binder_write_read结构先处理写部分。写部分是一串命令每条命令以uint32_t开头标识类型比如BC_TRANSACTION、BC_REPLY、BC_FREE_BUFFER等。对于BC_TRANSACTION驱动会根据handle找到目标binder_ref再找到binder_node从而确定目标进程。把数据从发送方的用户空间拷贝到内核空间这是那一次拷贝。在目标进程的binder_proc里分配一块内存通过binder_alloc把数据映射过去。实际上驱动会尝试从目标进程的mmap区域里分配空间然后把数据拷贝到那块区域。由于目标进程已经mmap了/dev/binder这块区域在目标进程的用户空间是可见的所以不需要再拷贝一次。把目标进程的某个等待线程唤醒或者如果目标进程没有空闲线程就排队等待。目标进程的IPCThreadState在talkWithDriver()里阻塞在ioctl上被唤醒后读到BR_TRANSACTION命令然后调用executeCommand()处理case BR_TRANSACTION: { binder_transaction_data tr; // 从mIn读取tr if (tr.target.ptr) { // 有本地BBinder spBBinder b((BBinder*)tr.cookie); error b-transact(tr.code, buffer, reply, tr.flags); } else { // 没有本地对象交给context object error the_context_object-transact(tr.code, buffer, reply, tr.flags); } // 如果需要回复发送BC_REPLY }BBinder::transact()会调用onTransact()这是服务端真正执行业务逻辑的地方。执行完后如果有返回值会通过BC_REPLY命令把结果写回驱动驱动再转交给客户端。3.4 数据拷贝的真相很多人说Binder是一次拷贝这个说法需要精确理解。准确地说是发送方的数据从用户空间拷贝到内核空间一次接收方通过mmap直接访问内核空间里的数据不需要再拷贝。但接收方在处理数据时如果要把数据从Parcel里读出来比如读一个String那还是会有一次拷贝只不过这次拷贝是在接收方进程内部不涉及跨进程。对比socket发送方用户空间→内核空间→接收方用户空间两次拷贝。Binder省掉了第二次。共享内存是零次拷贝但需要自己处理同步和权限。这个优化在频繁小数据量传输时效果明显。Android里大量跨进程调用都是小数据量比如传递一个int、一个StringBinder的优势就体现出来了。4. AIDL背后的代码生成机制4.1 AIDL文件到Java代码的映射写一个AIDL文件比如// IMyService.aidl package com.example.demo; interface IMyService { int add(int a, int b); String getInfo(); void doSomething(); }编译时会生成IMyService.java里面有一个Stub类和一个Proxy类。Stub是服务端要继承的Proxy是客户端拿到的。生成的代码里方法调用被映射成了transact()调用。add方法对应的transaction code是TRANSACTION_add一个常量通常是IBinder.FIRST_CALL_TRANSACTION 0。Proxy.add()的实现大致是Override public int add(int a, int b) throws RemoteException { Parcel _data Parcel.obtain(); Parcel _reply Parcel.obtain(); int _result; try { _data.writeInterfaceToken(DESCRIPTOR); _data.writeInt(a); _data.writeInt(b); mRemote.transact(Stub.TRANSACTION_add, _data, _reply, 0); _reply.readException(); _result _reply.readInt(); } finally { _reply.recycle(); _data.recycle(); } return _result; }Stub.onTransact()的实现Override public boolean onTransact(int code, Parcel data, Parcel reply, int flags) { switch (code) { case TRANSACTION_add: { data.enforceInterface(DESCRIPTOR); int _arg0 data.readInt(); int _arg1 data.readInt(); int _result this.add(_arg0, _arg1); reply.writeNoException(); reply.writeInt(_result); return true; } case TRANSACTION_getInfo: { data.enforceInterface(DESCRIPTOR); String _result this.getInfo(); reply.writeNoException(); reply.writeString(_result); return true; } // ... } return super.onTransact(code, data, reply, flags); }这里有几个关键点值得注意writeInterfaceToken和enforceInterface这是用来校验接口的。客户端写入DESCRIPTOR通常是包名接口名服务端检查是否匹配。如果不匹配就抛异常。这防止了客户端调用错误的接口。writeNoException和readException服务端执行方法时如果抛了异常会通过writeException把异常信息写进reply客户端readException时重新抛出。这样跨进程调用也能传递异常。Parcel的读写顺序必须严格一致客户端按什么顺序写服务端就必须按什么顺序读。写一个int读一个int写一个String读一个String。顺序错了就会读到错误的数据甚至崩溃。这是新手最容易犯的错误之一。4.2 oneway关键字的影响如果AIDL方法声明为oneway生成的代码会不一样。oneway表示调用是异步的客户端发起调用后立即返回不等待服务端执行结果。生成的Proxy代码里transact()的flags会带上IBinder.FLAG_ONEWAYmRemote.transact(Stub.TRANSACTION_doSomething, _data, null, IBinder.FLAG_ONEWAY);注意_reply传的是null因为不需要接收返回值。服务端onTransact()里oneway方法执行完后不会发送BC_REPLY。oneway的使用有讲究。它适合那些不需要返回值、不需要确认执行结果的操作比如日志上报、状态通知。但要注意oneway调用是排队的如果服务端处理慢请求会堆积。而且oneway调用不保证顺序实际上在同一个Binder对象上oneway调用是保序的但跨Binder对象就不保证了。还有一个坑oneway方法不能有返回值也不能抛异常抛了也没人接收。AIDL编译器会检查这一点。4.3 Parcel的序列化细节Parcel是Binder传输的数据容器它的设计很讲究。Parcel内部是一块连续的内存写入数据时按顺序追加读取时按顺序消费。它支持基本类型int、long、float、double、boolean、String、数组、List、Map、Parcelable、IBinder等。几个容易踩坑的地方String的写入writeString()会先写一个int表示长度-1表示null然后写UTF-16的字符数据。读取时readString()先读长度再读字符。如果写入的是null读出来也是null。Parcelable的写入需要实现writeToParcel()和CREATOR。写入时会先写类名用于查找CREATOR再写数据。读取时根据类名反射找到CREATOR调用createFromParcel()。这个过程有反射开销频繁传输大量Parcelable时要注意性能。IBinder的写入writeStrongBinder()会把IBinder对象写入Parcel。如果是一个本地BBinder驱动会为它在目标进程创建一个binder_ref如果是一个BpBinder远程引用驱动会做引用传递。这是Binder支持对象传递的关键。Parcel的大小限制默认情况下Parcel的传输缓冲区是1MBBINDER_VM_SIZE在ProcessState.cpp里定义。但这个1MB是整个进程共享的不是每次调用1MB。如果并发调用多或者单次传输数据大很容易触发TransactionTooLargeException。这个限制在驱动层也有对应的binder_alloc缓冲区大小限制。注意1MB的限制是进程级别的不是单次调用的。如果你在一个进程里同时有多个Binder调用在传输数据它们共享这1MB。所以大文件传输绝对不要走Binder应该用文件描述符传递或者ashmem共享内存。5. Binder线程管理与并发模型5.1 Binder线程池的工作机制每个进程在启动Binder通信时会通过ProcessState::self()-startThreadPool()启动一个线程池。默认情况下主线程会加入Binder线程池另外还会启动若干个子线程。线程数量由binder_proc的max_threads控制默认是15DEFAULT_MAX_BINDER_THREADS。当驱动收到一个BC_TRANSACTION请求需要目标进程处理时它会看目标进程有没有空闲的Binder线程。如果有唤醒一个如果没有且线程数没达到上限会通知目标进程创建新线程如果达到上限请求会排队等待。这个机制导致一个常见问题如果服务端的所有Binder线程都在处理耗时操作新的请求就会排队客户端会阻塞。更严重的是如果服务端的某个Binder线程又去调用另一个服务形成嵌套调用而那个服务又在等这个服务就可能死锁。我遇到过的一个真实场景某系统服务在处理请求时同步调用了另一个系统服务而那个服务恰好也在等这个服务的另一个请求。两边线程池都满了互相等待整个系统卡死。排查这种问题非常痛苦因为日志里只看到一堆binder transaction超时。5.2 线程池大小的调整DEFAULT_MAX_BINDER_THREADS是15这个值在ProcessState.cpp里#define DEFAULT_MAX_BINDER_THREADS 15可以通过ProcessState::setThreadPoolMaxThreadCount()调整但一般不建议改。15个线程对大多数服务够用了改大了反而增加内存开销和上下文切换成本。真正需要关注的是避免在Binder线程里做耗时操作。如果某个操作确实耗时应该把它丢到工作线程去Binder线程立即返回。或者用oneway调用让客户端不阻塞。5.3 嵌套调用的风险与应对嵌套调用是指服务端在处理Binder请求时又发起了另一个Binder调用。这在系统服务里很常见比如ActivityManagerService调用PackageManagerService。嵌套调用的风险在于线程占用。假设服务A的线程池有15个线程每个线程都在处理请求每个请求又去调用服务B。服务B的线程池也有15个线程也在处理请求每个请求又回调服务A。如果两边同时打满就会死锁。应对方法有几个一是减少嵌套。能异步的就异步能合并的就合并。比如把多个小调用合并成一个大调用。二是设置超时。Binder调用可以设置超时超时后返回错误避免无限等待。但超时设置要合理太短会误判太长失去意义。三是用oneway打破循环。如果某个调用不需要返回值用oneway可以避免线程占用。四是监控线程池状态。通过dumpsys binder或者/sys/kernel/debug/binder/下的文件可以查看Binder线程状态及时发现异常。6. ServiceManager与服务注册发现6.1 ServiceManager的角色ServiceManager是Android Binder机制的电话簿。所有系统服务在启动时都要向它注册客户端通过名字查询服务。ServiceManager本身也是一个Binder服务它的Binder句柄固定为0BINDER_SERVICE_MANAGER。ServiceManager的启动很早在init进程解析init.rc时就会启动。它的实现分两部分servicemanager进程Native层和IServiceManager接口Java层通过ServiceManager类访问。注册服务的流程// 服务端 defaultServiceManager()-addService(String16(my.service), new MyService());查询服务的流程// 客户端 spIBinder binder defaultServiceManager()-getService(String16(my.service)); spIMyService service IMyService::asInterface(binder);asInterface()会判断传入的IBinder是本地对象还是远程代理。如果是本地BBinder直接返回Stub如果是远程BpBinder创建Proxy。6.2 服务注册的权限控制ServiceManager在注册服务时会检查权限。默认情况下只有system用户UID 1000和root用户UID 0可以注册服务。这个检查在servicemanager的svc_can_register()函数里static bool svc_can_register(const uint16_t *name, size_t name_len, pid_t spid, uid_t uid) { // 检查uid是否是root或system if (uid AID_ROOT || uid AID_SYSTEM) return true; // 检查是否有对应的selinux权限 // ... return false; }从Android 8开始SELinux策略进一步收紧了服务注册权限。普通应用进程无法注册系统服务必须是有对应SELinux域的系统进程才行。这防止了恶意应用冒充系统服务。6.3 服务查询的缓存机制客户端每次getService()都会向ServiceManager发起Binder调用这有开销。所以很多客户端会缓存查询结果。比如ActivityManager在第一次获取后会把IActivityManager对象缓存起来后续直接使用。但缓存有个问题如果服务重启了缓存的Binder引用就失效了。这时候调用会抛DeadObjectException。所以健壮的客户端需要处理DeathRecipient在服务死亡时收到通知清理缓存并重新查询。IBinder.DeathRecipient recipient new IBinder.DeathRecipient() { Override public void binderDied() { // 服务死了清理缓存 mService null; // 可以选择重新绑定 } }; binder.linkToDeath(recipient, 0);linkToDeath是Binder提供的重要机制它让客户端能感知服务端的死亡。系统服务里大量使用了这个机制比如ActivityManagerService会监听各种服务的死亡及时做清理。7. 实战调试与问题排查7.1 常用调试工具dumpsys最常用的工具。dumpsys -l列出所有服务dumpsys activity查看ActivityManager状态dumpsys binder查看Binder状态。dumpsys binder会输出每个进程的Binder线程数、事务数、引用数等信息。/sys/kernel/debug/binder/内核暴露的Binder调试信息。需要root权限。里面有stats、transactions、transaction_log等文件。transaction_log记录了最近的Binder事务排查死锁时非常有用。binderfs新版本内核用binderfs替代了debugfs路径在/dev/binderfs/或者/sys/fs/binder/。功能类似。ftrace可以跟踪Binder驱动的函数调用比如binder_transaction、binder_thread_read等。适合分析性能问题。PerfettoAndroid的官方性能分析工具可以抓取Binder调用的时间线直观看到每次调用的耗时和调用链。7.2 典型问题排查问题一TransactionTooLargeException这个异常表示传输数据超过了1MB限制。排查方法是看异常堆栈找到是哪个调用传输了大量数据。常见原因是传输了大的Bitmap、长String、大List。解决方法把大数据拆分成多次小传输或者用文件描述符传递ParcelFileDescriptor或者用ashmem共享内存。问题二Binder调用超时Binder调用默认没有超时同步调用会一直等。如果服务端卡住客户端会一直阻塞。排查方法是看服务端的Binder线程状态是不是所有线程都在忙。可以用dumpsys binder查看或者看/sys/kernel/debug/binder/proc/pid下的线程信息。问题三DeadObjectException服务端进程死了客户端调用时抛这个异常。排查方法是看服务端为什么死可能是崩溃、被kill、OOM。客户端应该注册DeathRecipient在服务死亡时做清理和重连。问题四Binder线程池耗尽表现为新的Binder调用一直排队客户端超时。排查方法是看服务端的Binder线程数是否达到上限以及线程都在干什么。常见原因是某个Binder方法执行时间太长或者发生了嵌套调用死锁。7.3 一个真实的排查案例之前遇到一个系统服务偶发卡死的问题。现象是某个功能偶尔无响应日志里看到大量binder transaction failed。用dumpsys binder查看发现该服务的Binder线程数一直是15上限所有线程都在waitForResponse状态。进一步用ftrace跟踪发现这些线程都在等另一个服务的回复。而那个服务的线程池也满了线程都在等第三个服务。形成了一个调用环。根因是三个服务之间的调用关系设计有问题在高并发时形成了循环等待。解决方法是把其中一个调用改成异步oneway打破循环。同时给关键调用加了超时避免无限等待。这个案例的教训是设计系统服务时一定要画清楚调用关系图避免循环依赖。如果无法避免至少要把其中一条边改成异步。8. 性能优化与进阶话题8.1 减少Binder调用次数Binder调用有固定开销ioctl、线程切换、数据拷贝频繁的小调用累积起来很可观。优化方法是合并调用。比如一个接口需要获取多个字段与其定义多个getter方法不如定义一个方法一次性返回所有字段。AIDL里可以用Parcelable打包多个字段一次传输。或者用Bundle。但要注意Bundle本身也有序列化开销字段太多时不一定划算。8.2 大数据传输的正确姿势前面说过Binder传输限制是1MB。超过这个大小的数据正确做法是用ParcelFileDescriptor传递文件描述符或者用ashmemAndroid的匿名共享内存。ParcelFileDescriptor的用法是服务端创建一个管道或者打开一个文件把文件描述符通过Binder传给客户端客户端通过这个描述符读写数据。数据本身不经过Binder绕开了大小限制。ashmem的用法是服务端创建一块共享内存区域把文件描述符传给客户端客户端mmap这块区域直接读写。适合大块数据的零拷贝传输。8.3 Binder与hidl、aidl的演进Android 8引入了HIDLHAL Interface Definition Language用于HAL层与Framework层的通信。HIDL底层也是Binder但用了不同的驱动/dev/hwbinder和不同的线程模型。Android 11之后AIDL开始支持HALHIDL逐渐被替代。AIDL在Android 10之后增加了稳定版本stable AIDL的支持允许跨版本传输。这对Treble架构下的Vendor与System分区通信很重要。这些演进的核心思路是一致的用接口定义语言描述跨进程接口自动生成序列化和反序列化代码底层用Binder传输。理解了这个核心具体用哪个工具就是查文档的事了。8.4 Binder的未来Binder机制从Android诞生至今核心设计没有大改。它的性能、安全性、易用性平衡得很好短期内不会被替代。但也有一些新的探索比如基于共享内存的IPC、基于io_uring的异步IPC等。这些在特定场景下可能有优势但通用性和生态成熟度还远不如Binder。对于开发者来说把Binder吃透比追逐新机制更实际。因为Android系统里到处都是Binder改任何一个系统服务都绕不开它。我个人在实际排查Binder问题时最深的体会是日志和工具比直觉可靠。很多次我以为问题在A处结果用ftrace一跟发现是B处。Binder的调用链可能很长跨多个进程靠猜是猜不准的。养成用工具的习惯能省下大量时间。另外设计阶段就把调用关系理清楚比事后排查划算得多。我见过太多项目因为服务间调用关系混乱后期维护成本极高。这一点值得每个做系统开发的人重视。