前言

前段时间调查一个死机重启问题,里面涉及到 Binder 调用的一些比较细节的地方,因此将 binder 调用的整个过程大致缕了一遍,并将所得整理下来。

问题 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)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 reference10-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 究竟是如何产生的。

一、这样的调用栈是如何产生的?

1. 三个进程

从未贴出的 log 可以看出,这段 log 是在 app 更新或者添加 notification 的过程中产生的,所以我们先看一下更新或添加 notification 的调用流程:

点击查看大图

  • 不同的颜色代表不同的进程,红色运行在 app 进程,黑色在 system_server 进程,蓝色在 systemui (也可能是其他注册过的进程)进程
  • 其中涉及到三次跨进程调用,依次为:app 进程调用 system_server 进程的 enqueueNotificationWithTag 方法、system_server 进程调用 systemui 进程的 onNotificationPosted 方法、systemui 进程调用 system_server 进程的 get 方法

第一次跨进程调用很常见,我们就不做讨论了,主要看一下第二、三次跨进程调用是如何实现的。首先,我们需要了解一下 NotificationListener 的注册流程(以 systemui 为例),如下图所示:

点击查看大图

  • 其中红色部分运行在 systemui 进程,蓝色部分运行在 system_server 进程
  • StatusBar 的成员 mNotificationListener 是一个 NotificationListenerService 实例,其会调用 registerAsSystemService 方法来进行注册

下面我们来看一下 registerAsSystemService 的实现:

1.1 NotificationListenerService.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;}
  • 通过 getNotificationInterface 方法得到 NMS (NotificationManagerService) 的代理,NMS 是在 ServiceManager 中注册过的
  • 如果 mWrapper == null,那么创建一个 INotificationListenerWrapper 对象(INotificationListenerWrapper extends INotificationListener.Stub)赋给它
  • 将 mWrapper 作为参数(其写入 parcel 时会调用 writeStrongBinder 方法,将其 IBinder 对象写入),调用 NMS 的 registerListener 方法,由此看来 mWrapper 提供了一个匿名 Binder 服务

(匿名 Binder 服务机制,本文不详细展开)
下面我们来看一下 registerService 的实现:

1.2 ManagedServices.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;}
  • 将 service 以及 component 等数据包装成一个 ManagedServiceInfo 对象
  • 将包装好的这个对象添加到 mServices 中

1.3 NotificationListeners.notifyPostedLocked

上面 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 进程。

1.4 StatusBarNotificationHolder

我们看一下 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 进程的调用。

2. Binder 调用流程

从第一段 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 的调用流程逆推回去。

2.1 Binder.execTransact

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());    }
  • e 不是上面判断中任何一个 Exception 的实例,所以 code 一直为 0
  • 因为之前执行过 setDataPosition(0),所以 writeInt(code) 会将 parcel 对象的 mData 上的第一个值置为 0,并将 mDataPos 增加 4(填充数据的 length),即指向下一个需要填充的数据的位置
  • 上一步完成后,将 Exception e 抛出

2.2 JavaBBinder::onTransact

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 是怎么来的

2.3 BBinder::transact

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

2.4 IPCThreadState::executeCommand 中的 case BR_TRANSACTION

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,所以可知这里会依次调用:

  • reply.setError(error); // error = UNKNOWN_TRANSACTION
  • sendReply(reply, 0);

由于 IPCThreadState 的调用过程涉及到跨进程,比较复杂,所以这里详细讲解一下

2.5 IPCThreadState 的 transact 过程(非 TF_ONE_WAY)

下面看一下 IPCThreadState 是如何跨进程调用到对端的:

  • talkWithDriver 时,会将 parcel 对象 mOut 的数据写入内核空间,并将读到的数据放入 parcel 对象 mIn 中
  • 由用户空间传入 Binder Driver 的 cmd 是以 BC 开头的,经过 Binder Driver 的处理会转化为相应的 BR 开头的 cmd 传给对端(例如 BC_TRANSACTION -> BR_TRANSACTION

结合我们调查的这个问题,将 IPCThreadState 跨进程调用的具体过程用图示表示出来:


点击查看大图

从图中我们可以看出:

  • Server 端 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 端的业务实现,我们这里的返回值 errorUNKNOWN_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 退出
  • writeTransactionData(BC_REPLY, …) 时,因为 reply 中的 mError 为 UNKNOWN_TRANSACTION,所以会给传输数据 tr 添加 flag TF_STATUS_CODE,并且在 tr 的 data 数据中保存错误信息 UNKNOWN_TRANSACTION
  • 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; 结束
  • 可以看到,在我们这种情况下 Server 端传回来的数据中只有一个 status code(UNKNOWN_TRANSACTION),Client 端做的也只是提取出 status code 赋值给 reply 对象的 mError 成员,值得注意的是 Client 端的 reply 对象在此之前没有其他额外操作,也就是 reply 对象现在的 mDataSize 仍旧为 0

到此,IPCThreadState 非 oneway 情况下的 transact 过程就讲解完了,剩下的流程比较简单,沿着 2 中给出的流程图推导过去即可,这里不再详细说明;下面来具体看一下 null 是怎么来的。

2.6 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,由此可知:

  • 这种情况下无论执行多少次 readInt() 最终得到的结果都为 0
  • 需要注意的是, Parcel::readAligned(T *pArg) 时,如果 (mDataPos+sizeof(T)) <= mDataSize,那么每执行一次 Parcel::readAligned(T *pArg)mDataPos 的值都会改变,因此 Java 层多次调用 readInt() 得到的值可以不相同,其他 read*() 函数同理;

因为 _reply.readInt() 的值为 0,因此最终会返回 nullnull 的由来我们知道了,那么第二段 log 是在什么位置打印的呢?下面我们来具体看一下这个问题。

2.7 第二段 log 的打印

由 1 可知,NPE 是在 INotificationListenerWrapperonNotificationPosted 中抛出的;换个角度,此时是 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。

更多相关文章

  1. android 通话记录去重查询方法
  2. android NDK开发 静态/动态注册 jni
  3. Android(安卓)WebView中无法用JS调用Java对象的问题
  4. GIT和repo使用方法:下载内核 android源码包
  5. android 网络编程 HttpGet类和HttpPost类使用详解
  6. 使用Android(安卓)studio3.6的java api方式调用opencv
  7. Android的Linux内核的电源管理:Early Suspend
  8. android init 进程分析 (2 初始化流程)
  9. Android本地存储——SQLite数据库

随机推荐

  1. css伪类与盒模型对比写法
  2. 样式分类与选择器详解
  3. html基础:css自定义样式的来源及选择器优
  4. CSS样式优先级和权重和常用选择器代码演
  5. HTML标签与属性
  6. 2022年0707结构伪类与状态伪类与盒模型常
  7. css基础:链接形式及选择器的用法
  8. 跟着大牛学PHP--CSS样式和选择器
  9. css自定义样式来源选择器与选择器的权重
  10. 自定义样式的来源与优先级与常用选择器与