Binder的异常之分析
在Adroid的世界里,每个程序是互相独立运行的。若要请求协助就需要Binder机制,而此Binder就是一种Client –Server的设计。其中间运行机制不在这里详细描述,间单来讲Binder就是一种程序沟通接口,运用的基本原理就是共享内存加代理模式。因此每个程序是不会知道对方的运作流程,只能知道有求必应这功能。但是若对方一发生异常,虽然使用方无法得知,但也不能置之不理。这篇心得就是要来分析当Server在处理Client端需求发生异常时,Binder是如何来处理。
目录
1. 如果服务对象的方法中发生异常,Binder将如何处理异常? 2
1.1.1 由Binder.execTransactInternal来处理异常 4
1.1.2 由JavaBBinder::onTransact来处理异常 6
2.2 TransactionTooLargeException 16
首先分为Server和Client来分析。
1.1 Server
AIDL工具生成的Stub抽象类主要用于方法分发。如果报出异常的话,异常将首先会报给Stub类的onTransact方法。以如下foo方法为例,如果其内部发生异常,则该异常将会被onTransact方法感知。由上图可以知道,处理异常的部分会有两个地方,一个是Binder.execTransactInternal、另一个是JavaBBinder::onTransact。接下来就分别分析一下这两个地方。
48行是呼叫foo的位置,不过内部发生的异常并没有在onTransact中被处理,因此会继续回传给Binder.execTransactInternal方法。
[file]: /frameworks/base/core/java/android/os/Binder.java
[method]: execTransactInternal
1021行是呼叫onTransact的位置。如果异常没有在此方法中被处理,将会进一步抛出给execTransact,最终进入JNI方法:JavaBBinder::onTransact。
异常的传递关系正如本节开始的那幅图所示,其最终的处理分为了两种情况。
以下是Binder会处理的相关异常和异常码。
Exception | Code |
SecurityException | EX_SECURITY |
BadParcelableException | EX_BAD_PARCELABLE |
IllegalArgumentException | EX_ILLEGAL_ARGUMENT |
NullPointerException | EX_NULL_POINTER |
IllegalStateException | EX_ILLEGAL_STATE |
NetworkOnMainThreadException | EX_NETWORK_MAIN_THREAD |
UnsupportedOperationException | EX_UNSUPPORTED_OPERATION |
ServiceSpecificException | EX_SERVICE_SPECIFIC |
在execTransactInternal的程序代码中,在1022行发现只catch两种exception。一个是RemoteException、另一个是RuntimeException。其他的Exception就继续传到JNI(JavaBBinder::onTransact)去处理。在execTransactInternal的程序代码的1039行发现使用Parcel来写入所catch到的RemoteException或RuntimeException。接下来就来看看Parcel的writeException方法。
[file]: /frameworks/base/core/java/android/os/Parcel.java
[method]: writeException
由程序代码可以看到如上8种异常,Server进程会将异常的信息串行化写入Parcel对象,然后经由Driver发送给Client进程。而其他的Exception就在1897行打包为RuntimeException传到JNI(JavaBBinder::onTransact)处理。
[file]: /frameworks/base/core/jni/android_util_Binder.cpp
[method]: onTransact
由程序代码可知368行是呼叫Java层execTransact方法的位置。当该方法抛出异常时,371行的异常检测将为true,而其后的373行会将异常打印出来。需要注意的是,这些信息并不会被送回Client进程只会写log。
以下是范例。
综合以上两种处理情况,可以推断所有服务对象方法中发生的异常都会被处理。一种是将异常信息发送给对端进程,另一种是将异常信息在本进程输出。而这些处理都不会使Server进程退出。
作为Server进程,它在什么时候执行,该执行些什么都不由自己掌控,而是由Client进程控制。因此抛出异常本质上与Client进程相关,让一个Client进程的行为导致Server进程退出显然是不合理的。此外,Server进程可能关联着千百个Client,不能由于一个Client的错误行为而影响本可以正常获取服务的其他Client。因此Server的方法一旦因为某个Client的失误导致发生异常,实在不能因为这样的异常而把Server终止掉。
1.2 Client
Client端受到的影响完全取决于Server端如何处理异常。上文中已经说明,Server进程会分两种情况来处理异常:一种是将八种特定异常信息发送给Client进程,另一个种是将异常信息在Server进程中只有输出不传送。以下按照这两个情况分别讨论Client进程受到的影响。
1.2.1 从Parcel对象中读回异常信息
Client端利用Binder proxy呼叫完Sever端的方法后会经由Parcel来读取异常讯息。
由上面的范例可以看到Server进程通过Parcel[_reply]对象发送的异常信息最终会在79行被读回。
[file]: /frameworks/base/core/java/android/os/Parcel.java
[method]: readException
首先会从readExceptionCode去读取Exception code,此Exception code就是Binder Catch到的exception时利用Parcel的writeException方法写进,可参考如上writeException程序代码的1892行。这些code除了如之前所提到的8种异常之外,一律都是0。因此这里会做个判断,若不是0的话就把相关的message和code继续往下处理,若是0的话就不处理。
[file]: /frameworks/base/core/java/android/os/Parcel.java
[method]: readException(int code, String msg)
由程序代码可知,根据异常code,msg和stack tracey在2039行重新构建出Exception对象,在2053行抛出。由于readException方法并没有用throws修饰,所以如果该异常是Checked Exception(譬如RemoteException,IOException)就不能够直接抛出,否则会产生编译错误。因此这里采用了SneakyThrow来进行。
结合Server端的异常处理可以知道Client端只会读到8种RuntimeException中的一种。由于RuntimeException属于Unchecked Exception,因此编译过程并不会去检查对它的处理。换句话说,在使用代理对象的方式通常只会去catch RemoteException,而不会去catch RuntimeException。如此一来,Parcel读回来的RuntimeException将会导致Client进程退出。以下为示例,注意这些Log是在Client进程中输出的。
1.2.2 Binder 自行处理异常
先从JNI遇到异常处理开始分析,前面有提到一旦检测到有异常,Binder只是做log而已。
[file]: /frameworks/base/core/jni/android_util_Binder.cpp
[method]: onTransact
[…]
由程序代码看来,log完讯息之后在376行就指定res为JNI_FALSE,因此JavaBBinder::onTransact最终返回UNKNOWN_TRANSACTION。就继续往回追踪。
呼叫JavaBBinder::onTransact的是BBinder::transact。
[file]: /frameworks/native/libs/binder/Binder.cpp
[method]: transact
UNKNOWN_TRANSACTION 继续往回传。BBinder::transact会是由IPCThreadState::executeCommand来呼叫。
[file]: /frameworks/native/libs/binder/IPCThreadState.cpp
[method]: executeCommand
[…]
1213行从transact取回error之后在1230行会将该error写入Parcel对象中,并呼叫sendReply传回Client进程。
[file]: /frameworks/native/libs/binder/IPCThreadState.cpp
[method]: sendReply
825行将含有error data的parcel [程序里的reply]透过writeTransactionData方法交由Binder driver处理,之后在透过waitForResponse方法等Binder driver的响应。
[file]: /frameworks/native/libs/binder/IPCThreadState.cpp
[method]: writeTransactionData
由于1038行data中读取的err是UNKNOWN_TRANSACTION,所以最终写入驱动的数据并不是data本身,而是data中的err值。之后就再也没把err的信息往其他地方传送了。因此这些非如前面所提的8种异常一到此就习事宁人了。而Client端和Server端都完好如初的还活着,只差别在Client对Server的需求并不会有反应。
到此还没有描述到跟RemoteException的相关信息,在1.1节中也只知道8种特定异常和其他异常被包成RuntimeException的处理方式。而RemoteException又为何在Client端使用时被强迫要Catch,这一定有设计上的道理。接下来就来看看RemoteException是如何被触发?
2. RemoteException 设计
RemoteException是一种Checked Exception。必须按照如下两种方式中的一种来处理,否则会在编译时报错。
-
用try catch代码块来捕获异常。
-
将该方法用throws关键词修饰,告诉调用者此方法可能会抛出该异常。
Checked Exception会对程序员的代码有强制要求。之所以这么做,是因为该类异常希望程序员可以提前预知并做好准备,它们本可以被处理,用不着让进程退出。
以下是Oracle官网的解释,表示对于那些进程可以从中恢复的异常,都应该把它声明为Checked Exception。像远程呼叫/网络请求/IO请求之类的异常,这些异常多是数据无法获取,但并不意味着进程到了无法继续运行的地步,因此用Checked Exception最为合适。
Generally speaking, do not throw a RuntimeException or create a subclass of RuntimeException simply because you don’t want to be bothered with specifying the exceptions your methods can throw. Here’s the bottom line guideline: If a client can reasonably be expected to recover from an exception, make it a checked exception. If a client cannot do anything to recover from the exception, make it an unchecked exception. |
对于Android Q而言,RemoteException的子类有三个:
-
DeadObjectException
-
TransactionTooLargeException
-
DeadSystemException,是DeadObjectException的子类
2.1 DeadObjectException
[file]: /frameworks/base/core/java/android/os/DeadObjectException.java
由批注可以知道,此异常是用来反应Server端已经挂掉了。事实上DeadObjectException不仅仅是远程进程挂掉这一种情况可以触发,binder线程池耗尽也会触发该异常。
接下来就来看看在甚么的地方下会触发DeadObjectException。因为是Client端在使用Server端的方法要去Catch这个RemoteException,所以就从Proxy要transact Binder request这一点来看。Binder porxy传送Binder request的地方最终会到JNI android_os_BinderProxy_transact。
[file]: /frameworks/base/core/jni/android_util_Binder.cpp
[method]: android_os_BinderProxy_transact
可以发现1334行就是Exception的触发点并回传JNI_FALSE。可以发现这方法有一个参数是用来控制是否要丢RemoteException,代表这方法会按照1319行所回传的error会有不一样的Exception。这里只要探讨RemoteException,所以只会看有关RemoteException的信息。
[file]: /frameworks/base/core/jni/android_util_Binder.cpp
[method]: signalExceptionForError
由程序代码的780和806行可知,Java层接收到的DeadObjectException有两处:
-
BpBinder::transact返回值为DEAD_OBJECT。
-
BpBinder::transact返回值为FAILED_TRANSACTION,但是本次传输的Parcel小于200K。
BpBinder:transact返回的error是藉由IPCThreadState的waitForResponse方法去跟Binder driver询问而所指定的。
[file]: /frameworks/native/libs/binder/IPCThreadState.cpp
[method]: waitForResponse
837行就是透过talkWithDriver方法去跟Binder driver要的err 状态。
2.1.1 DEAD_OBJECT
[file]: /frameworks/native/libs/binder/IPCThreadState.cpp
[method]: waitForResponse
BR_DEAD_REPLY是由Binder driver所指定的,在855行可以看到只要从Binder driver得到BR_DEAD_REPLY就会把要回传的err设为DEAD_OBJECT。之后就会在JNI那一层触发DeadObjectException。
2.1.2 FAILED_TRANSACTION
[file]: /frameworks/native/libs/binder/IPCThreadState.cpp
[method]: waitForResponse
同样的Binder driver若回传BR_FAILED_REPLY就指定err为FAILED_TRANSACTION。
2.2 TransactionTooLargeException
由signalExceptionForError程序代码可知收到BR_FAILED_REPLY且此次传输的数据大于200K,则Java层会接收到TransactionTooLargeException的异常。
2.3 DeadSystemException
[file]: /frameworks/base/core/java/android/os/RemoteException.java
[method]: rethrowFromSystemServer
特别包在RemoteException中,表示当Client端收到DeadObjectException可以呼叫这方法重新丢一个DeadSystemException,代表DeadSystemException也是一种RemoteException。