前段时间调查一个死机重启问题,里面涉及到 Binder 调用的一些比较细节的地方,因此将 binder 调用的整个过程大致缕了一遍,并将所得整理下来。
10-17 12:13:02.006 2096 5712 E JavaBinder: *** Uncaught remote exception! (Exceptions are not yet supported across processes.)
10-17 12:13:02.006 2096 5712 E JavaBinder: java.lang.RuntimeException: Could not copy bitmap to parcel blob.
10-17 12:13:02.006 2096 5712 E JavaBinder: at android.graphics.Bitmap.nativeWriteToParcel(Native Method)
10-17 12:13:02.006 2096 5712 E JavaBinder: at android.graphics.Bitmap.writeToParcel(Bitmap.java:1553)
10-17 12:13:02.006 2096 5712 E JavaBinder: at android.widget.RemoteViews$BitmapCache.writeBitmapsToParcel(RemoteViews.java:984)
10-17 12:13:02.006 2096 5712 E JavaBinder: at android.widget.RemoteViews.writeToParcel(RemoteViews.java:2854)
10-17 12:13:02.006 2096 5712 E JavaBinder: at android.app.Notification.writeToParcel(Notification.java:1687)
10-17 12:13:02.006 2096 5712 E JavaBinder: at android.service.notification.StatusBarNotification.writeToParcel(StatusBarNotification.java:124)
10-17 12:13:02.006 2096 5712 E JavaBinder: at android.service.notification.IStatusBarNotificationHolder$Stub.onTransact(IStatusBarNotificationHolder.java:53)
10-17 12:13:02.006 2096 5712 E JavaBinder: at android.os.Binder.execTransact(Binder.java:453)
10-17 12:13:02.007 3362 15429 W Binder : Caught a RuntimeException from the binder stub implementation.
10-17 12:13:02.007 3362 15429 W Binder : java.lang.NullPointerException: Attempt to invoke virtual method 'android.app.Notification android.service.notification.StatusBarNotification.getNotification()' on a null object reference
10-17 12:13:02.007 3362 15429 W Binder : at android.service.notification.NotificationListenerService$INotificationListenerWrapper.onNotificationPosted(NotificationListenerService.java:692)
10-17 12:13:02.007 3362 15429 W Binder : at android.service.notification.INotificationListener$Stub.onTransact(INotificationListener.java:71)
10-17 12:13:02.007 3362 15429 W Binder : at android.os.Binder.execTransact(Binder.java:453)
下面我们来看一下这段 log 究竟是如何产生的。
从未贴出的 log 可以看出,这段 log 是在 app 更新或者添加 notification 的过程中产生的,所以我们先看一下更新或添加 notification 的调用流程:
enqueueNotificationWithTag
方法、system_server 进程调用 systemui 进程的 onNotificationPosted
方法、systemui 进程调用 system_server 进程的 get
方法第一次跨进程调用很常见,我们就不做讨论了,主要看一下第二、三次跨进程调用是如何实现的。首先,我们需要了解一下 NotificationListener 的注册流程(以 systemui 为例),如下图所示:
下面我们来看一下 registerAsSystemService 的实现:
NotificationListenerService.java
public void registerAsSystemService(Context context, ComponentName componentName,
int currentUser) throws RemoteException {
mSystemContext = context;
if (mWrapper == null) {
mWrapper = new INotificationListenerWrapper();
}
INotificationManager noMan = getNotificationInterface();
noMan.registerListener(mWrapper, componentName, currentUser);
mCurrentUser = currentUser;
}
(匿名 Binder 服务机制,本文不详细展开)
下面我们来看一下 registerService 的实现:
ManagedServices.java
public void registerService(IInterface service, ComponentName component, int userid) {
checkNotNull(service);
ManagedServiceInfo info = registerServiceImpl(service, component, userid);
if (info != null) {
onServiceAdded(info);
}
}
可以看到其分为两部分,我们主要看一下 registerServiceImpl 部分:
ManagedServices.java
private ManagedServiceInfo registerServiceImpl(final IInterface service,
final ComponentName component, final int userid) {
synchronized (mMutex) {
try {
// 这里的 service 实际上可以看作之前 mWrapper 的代理
ManagedServiceInfo info = newServiceInfo(service, component, userid,
true /*isSystem*/, null, Build.VERSION_CODES.LOLLIPOP);
service.asBinder().linkToDeath(info, 0);
mServices.add(info);
return info;
} catch (RemoteException e) {
// already dead
}
}
return null;
}
上面 1.2 中的 mServices 会在 notify notification 流程的 notifyPostedLocked 方法中用到:
NotificationManagerService.java
public void notifyPostedLocked(StatusBarNotification sbn, StatusBarNotification oldSbn) {
......
for (final ManagedServiceInfo info : mServices) {
......
mHandler.post(new Runnable() {
@Override
public void run() {
notifyPosted(info, sbnToPost, update);
}
});
}
}
从中可以看出此方法是从 mServices 中取出之前添加的 ManagedServiceInfo 对象,然后执行 notifyPosted(info, sbnToPost, update) 方法,看一下 notifyPosted 的实现:
NotificationManagerService.java
private void notifyPosted(final ManagedServiceInfo info,
final StatusBarNotification sbn, NotificationRankingUpdate rankingUpdate) {
final INotificationListener listener = (INotificationListener)info.service;
StatusBarNotificationHolder sbnHolder = new StatusBarNotificationHolder(sbn);
try {
listener.onNotificationPosted(sbnHolder, rankingUpdate);
} catch (RemoteException ex) {
Log.e(TAG, "unable to notify listener (posted): " + listener, ex);
}
}
info.service 就是包装之前的 service,因此 listener 即为之前 mWrapper 的代理,通过 listener.onNotificationPosted(sbnHolder, rankingUpdate) 也就实现了这次跨进程调用,由 system_server 进程进入了 systemui 进程。
我们看一下 StatusBarNotificationHolder 这个类:
NotificationManagerService.java
private static final class StatusBarNotificationHolder
extends IStatusBarNotificationHolder.Stub {
private StatusBarNotification mValue;
public StatusBarNotificationHolder(StatusBarNotification value) {
mValue = value;
}
@Override
public StatusBarNotification get() {
StatusBarNotification value = mValue;
mValue = null;
return value;
}
}
因此 notifyPosted 方法中传过去的 sbnHolder 其实与之前的 mWrapper 一致,都是为了提供一个匿名 Binder 服务,systemui 进程可以借助于其方法实现到 system_server 进程的调用。
从第一段 log 我们可以看出,第一个 Exception 是在服务端执行 onTransact 中的 writeToParcel 的时候产生的,那我们思考这样几个问题:此处产生 RuntimeException 会被怎样处理?返回给 Client 端的结果会是怎样的?下一段 log 中的空指针是怎么来的?
我们看一下 IStatusBarNotificationHolder.aidl 编译出的 IStatusBarNotificationHolder.java 文件,IStatusBarNotificationHolder.aidl 中只定义了一个接口:
StatusBarNotification get();
在 IStatusBarNotificationHolder.java 中看一下其对应的部分:
private static class Proxy implements android.service.notification.IStatusBarNotificationHolder {
/** Fetch the held StatusBarNotification. This method should only be called once per Holder */
@Override
public android.service.notification.StatusBarNotification get() throws android.os.RemoteException {
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
android.service.notification.StatusBarNotification _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
mRemote.transact(Stub.TRANSACTION_get, _data, _reply, 0);
_reply.readException();
if ((0!=_reply.readInt())) {
_result = android.service.notification.StatusBarNotification.CREATOR.createFromParcel(_reply);
} else {
_result = null;
}
} finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
}
由 mRemote.transact 开始的调用流程为:
最后产生 Exception 的地方是在 Server 端的 onTransact 方法中,因此,我们想知道这个 Exception 会被如何处理、以及最终返回给 Client 端的结果是什么,需要顺着 Binder 的调用流程逆推回去。
Binder.java
private boolean execTransact(int code, long dataObj, long replyObj,
int flags) {
Parcel data = Parcel.obtain(dataObj);
Parcel reply = Parcel.obtain(replyObj);
boolean res;
try {
res = onTransact(code, data, reply, flags);
} catch (RemoteException e) {
...
} catch (RuntimeException e) { // onTransact 中抛出的 RuntimeException 会被这里 catch 住
if ((flags & FLAG_ONEWAY) != 0) { // 为 oneway 调用
Log.w(TAG, "Caught a RuntimeException from the binder stub implementation.", e);
} else {
reply.setDataPosition(0);
reply.writeException(e);
}
res = true;
} catch (OutOfMemoryError e) {
...
}
checkParcel(this, code, reply, "Unreasonably large binder reply buffer");
reply.recycle();
data.recycle();
return res;
}
由上面代码可知,onTransact 中抛出的 RuntimeException 会被 catch 住,并且 get()
方法不是 oneway 的,因此,接下来会依次执行:
reply.setDataPosition(0);
reply.writeException(e);
reply.setDataPosition(0)
可以理解为将 parcel 中 mData
段上的 mDataPos
偏移置为 0下面我们来看一下 writeException 方法
Parcel.java
public final void writeException(Exception e) {
int code = 0;
if (e instanceof SecurityException) {
code = EX_SECURITY;
} else if (e instanceof BadParcelableException) {
code = EX_BAD_PARCELABLE;
} else if (e instanceof IllegalArgumentException) {
code = EX_ILLEGAL_ARGUMENT;
} else if (e instanceof NullPointerException) {
code = EX_NULL_POINTER;
} else if (e instanceof IllegalStateException) {
code = EX_ILLEGAL_STATE;
} else if (e instanceof NetworkOnMainThreadException) {
code = EX_NETWORK_MAIN_THREAD;
} else if (e instanceof UnsupportedOperationException) {
code = EX_UNSUPPORTED_OPERATION;
}
writeInt(code);
StrictMode.clearGatheredViolations();
if (code == 0) {
if (e instanceof RuntimeException) {
throw (RuntimeException) e;
}
throw new RuntimeException(e);
}
writeString(e.getMessage());
}
setDataPosition(0)
,所以 writeInt(code)
会将 parcel 对象的 mData 上的第一个值置为 0,并将 mDataPos
增加 4(填充数据的 length),即指向下一个需要填充的数据的位置android_util_Binder.cpp
virtual status_t onTransact(
uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags = 0)
{
JNIEnv* env = javavm_to_jnienv(mVM);
IPCThreadState* thread_state = IPCThreadState::self();
const int32_t strict_policy_before = thread_state->getStrictModePolicy();
// 调用 Binder.java 的 execTransact 方法
jboolean res = env->CallBooleanMethod(mObject, gBinderOffsets.mExecTransact,
code, reinterpret_cast(&data), reinterpret_cast(reply), flags);
if (env->ExceptionCheck()) {
jthrowable excep = env->ExceptionOccurred();
report_exception(env, excep,
"*** Uncaught remote exception! "
"(Exceptions are not yet supported across processes.)");
res = JNI_FALSE;
/* clean up JNI local ref -- we don't return to Java code */
env->DeleteLocalRef(excep);
}
if (thread_state->getStrictModePolicy() != strict_policy_before) {
set_dalvik_blockguard_policy(env, strict_policy_before);
}
if (env->ExceptionCheck()) {
jthrowable excep = env->ExceptionOccurred();
report_exception(env, excep,
"*** Uncaught exception in onBinderStrictModePolicyChange");
/* clean up JNI local ref -- we don't return to Java code */
env->DeleteLocalRef(excep);
if (code == SYSPROPS_TRANSACTION) {
BBinder::onTransact(code, data, reply, flags);
}
// 这里 res == JNI_FALSE,所以 return UNKNOWN_TRANSACTION
return res != JNI_FALSE ? NO_ERROR : UNKNOWN_TRANSACTION;
}
因为在 2.1 中的 writeException
时会 throw (RuntimeException) e;
,所以 env->ExceptionCheck()
为 true;因此会执行 report_exception
,这就是下面这段 log 的由来:
10-17 12:13:02.006 2096 5712 E JavaBinder: *** Uncaught remote exception! (Exceptions are not yet supported across processes.)
10-17 12:13:02.006 2096 5712 E JavaBinder: java.lang.RuntimeException: Could not copy bitmap to parcel blob.
10-17 12:13:02.006 2096 5712 E JavaBinder: at android.graphics.Bitmap.nativeWriteToParcel(Native Method)
10-17 12:13:02.006 2096 5712 E JavaBinder: at android.graphics.Bitmap.writeToParcel(Bitmap.java:1553)
10-17 12:13:02.006 2096 5712 E JavaBinder: at android.widget.RemoteViews$BitmapCache.writeBitmapsToParcel(RemoteViews.java:984)
10-17 12:13:02.006 2096 5712 E JavaBinder: at android.widget.RemoteViews.writeToParcel(RemoteViews.java:2854)
10-17 12:13:02.006 2096 5712 E JavaBinder: at android.app.Notification.writeToParcel(Notification.java:1687)
10-17 12:13:02.006 2096 5712 E JavaBinder: at android.service.notification.StatusBarNotification.writeToParcel(StatusBarNotification.java:124)
10-17 12:13:02.006 2096 5712 E JavaBinder: at android.service.notification.IStatusBarNotificationHolder$Stub.onTransact(IStatusBarNotificationHolder.java:53)
10-17 12:13:02.006 2096 5712 E JavaBinder: at android.os.Binder.execTransact(Binder.java:453)
下面我们继续来看一下剩下的 log 是怎么来的
Binder.cpp
status_t BBinder::transact(
uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags)
{
data.setDataPosition(0);
status_t err = NO_ERROR;
switch (code) {
case PING_TRANSACTION:
reply->writeInt32(pingBinder());
break;
default:
// err = UNKNOWN_TRANSACTION
err = onTransact(code, data, reply, flags);
break;
}
if (reply != NULL) {
reply->setDataPosition(0);
}
return err;
}
由上面这段代码可知,此函数会返回 UNKNOWN_TRANSACTION
,并且将 reply 对象的 mDataPos
再次置为 0
IPCThreadState.cpp
case BR_TRANSACTION:
{
binder_transaction_data tr;
result = mIn.read(&tr, sizeof(tr));
if (result != NO_ERROR) break;
Parcel buffer;
buffer.ipcSetDataReference(
reinterpret_cast<const uint8_t*>(tr.data.ptr.buffer),
tr.data_size,
reinterpret_cast<const binder_size_t*>(tr.data.ptr.offsets),
tr.offsets_size/sizeof(binder_size_t), freeBuffer, this);
Parcel reply;
status_t error;
...
// 重点部分
if (tr.target.ptr) {
if (reinterpret_cast(
tr.target.ptr)->attemptIncStrong(this)) {
// 此处调用 BBinder::transact,error = UNKNOWN_TRANSACTION
error = reinterpret_cast(tr.cookie)->transact(tr.code, buffer,
&reply, tr.flags);
reinterpret_cast(tr.cookie)->decStrong(this);
} else {
error = UNKNOWN_TRANSACTION;
}
} else {
...
}
// 重点部分
if ((tr.flags & TF_ONE_WAY) == 0) {// 不为 oneway 的情况
LOG_ONEWAY("Sending reply to %d!", mCallingPid);
if (error < NO_ERROR) {
alog << "error < NO_ERROR, reply = " << reply << endl;
reply.setError(error);
}
sendReply(reply, 0);
} else {
LOG_ONEWAY("NOT sending reply to %d!", mCallingPid);
}
...
}
break;
由于 BBinder::transact 的返回值为 UNKNOWN_TRANSACTION
,所以可知这里会依次调用:
由于 IPCThreadState 的调用过程涉及到跨进程,比较复杂,所以这里详细讲解一下
下面看一下 IPCThreadState 是如何跨进程调用到对端的:
talkWithDriver
时,会将 parcel 对象 mOut 的数据写入内核空间,并将读到的数据放入 parcel 对象 mIn 中BC_TRANSACTION
-> BR_TRANSACTION
)结合我们调查的这个问题,将 IPCThreadState 跨进程调用的具体过程用图示表示出来:
waitForResponse
时,收到 Binder Driver 的 BR_TRANSACTION
指令,于是调用 executeCommand(cmd)
,并进入 switch 语句的 case BR_TRANSACTION:
情况error = reinterpret_cast(tr.cookie)->transact(tr.code, buffer, &reply, tr.flags);
并沿着 2 中给出的调用流程一直执行到 Server 端的业务实现,我们这里的返回值 error
为 UNKNOWN_TRANSACTION
error < NO_ERROR
时,会先 reply.setError(error);
然后再 sendReply(reply, 0);
sendReply
时会先调用 writeTransactionData(BC_REPLY, ...)
将 cmd BC_REPLY
以及 parcel reply 写入输出 parcel mOut 中,然后再调用 waitForResponse(NULL, NULL);
收到 BR_TRANSACTION_COMPLETE
退出BC_REPLY
通过 Binder Driver 会被转化为 BR_REPLY
,Client 端在 talkWithDriver()
接收到 BR_REPLY
后会进入 case BR_REPLY:
情况,可以看到因为传过来的数据 tr
带有 flag TF_STATUS_CODE
,因此会执行 err = *reinterpret_cast(tr.data.ptr.buffer);
获取 status code
,即 err = UNKNOWN_TRANSACTION
,而后执行 freeBuffer
后直接 goto finish;
finish:
中会执行 reply->setError(err);
而后 return err;
结束status code(UNKNOWN_TRANSACTION)
,Client 端做的也只是提取出 status code
赋值给 reply
对象的 mError
成员,值得注意的是 Client 端的 reply
对象在此之前没有其他额外操作,也就是 reply
对象现在的 mDataSize
仍旧为 0到此,IPCThreadState
非 oneway 情况下的 transact
过程就讲解完了,剩下的流程比较简单,沿着 2 中给出的流程图推导过去即可,这里不再详细说明;下面来具体看一下 null
是怎么来的。
我们继续看一下 2 中 get()
方法的代码,在 mRemote.transact(Stub.TRANSACTION_get, _data, _reply, 0);
返回后,会依次执行以下几步:
_reply.readException();
if ((0!=_reply.readInt())) {
_result = android.service.notification.StatusBarNotification.CREATOR.createFromParcel(_reply);
}
else {
_result = null;
}
return _result;
我们来看一下 readException()
的实现:
Parcel.java
public final void readException() {
int code = readExceptionCode();
if (code != 0) {
String msg = readString();
readException(code, msg);
}
}
public final int readExceptionCode() {
int code = readInt();
if (code == EX_HAS_REPLY_HEADER) {
...
}
return code;
}
也就是说最终还是要看 readInt()
方法,其会执行到 Parcel.cpp 的 Parcel::readAligned()
方法:
Parcel.cpp
template<class T>
T Parcel::readAligned() const {
T result;
if (readAligned(&result) != NO_ERROR) {
result = 0;
}
return result;
}
template<class T>
status_t Parcel::readAligned(T *pArg) const {
COMPILE_TIME_ASSERT_FUNCTION_SCOPE(PAD_SIZE_UNSAFE(sizeof(T)) == sizeof(T));
if ((mDataPos+sizeof(T)) <= mDataSize) {
const void* data = mData+mDataPos;
mDataPos += sizeof(T);
*pArg = *reinterpret_cast<const T*>(data);
return NO_ERROR;
} else {
return NOT_ENOUGH_DATA;
}
}
2.5 中提到,reply 中的 mDataSize
为 0,所以 mDataPos+sizeof(T)
必定大于 mDataSize
,因此 Parcel::readAligned(T *pArg)
返回 NOT_ENOUGH_DATA
;所以 Parcel::readAligned()
最终会返回 0,由此可知:
Parcel::readAligned(T *pArg)
时,如果 (mDataPos+sizeof(T)) <= mDataSize
,那么每执行一次 Parcel::readAligned(T *pArg)
,mDataPos
的值都会改变,因此 Java 层多次调用 readInt()
得到的值可以不相同,其他 read*()
函数同理;因为 _reply.readInt()
的值为 0,因此最终会返回 null
,null
的由来我们知道了,那么第二段 log 是在什么位置打印的呢?下面我们来具体看一下这个问题。
由 1 可知,NPE 是在 INotificationListenerWrapper
的 onNotificationPosted
中抛出的;换个角度,此时是 systemui 作为 system_server 的 Server 端,因此这个 NPE 会被 2.1 中的 execTransact
函数 catch 住;而此调用为 oneway 调用,因此如下:
catch (RuntimeException e) {
if ((flags & FLAG_ONEWAY) != 0) { // 执行这里
Log.w(TAG, "Caught a RuntimeException from the binder stub implementation.", e);
} else {
reply.setDataPosition(0);
reply.writeException(e);
}
res = true;
}
直接会将 NPE 信息输出,即第二段 log。