Android跨程序拋異常的原理
也就是說在Service端丟擲的異常需要可以在Client端接收。印象中binder是可以傳異常的,所以aidl直接走起:
// aidl檔案 interface ITestExceptionAidl { boolean testThrowException(); } // service端實現 public class AidlService extends Service { @Nullable @Override public IBinder onBind(Intent intent) { return new ITestExceptionAidl.Stub() { @Override public boolean testThrowException() throws RemoteException { if (true) { throw new RuntimeException("TestException"); } return true; } }; } } // client端實現 bindService(intent, new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { ITestExceptionAidl aidl = ITestExceptionAidl.Stub.asInterface(service); try { aidl.testThrowException(); } catch (Exception e) { Log.e("testtest", "Exception", e); } } @Override public void onServiceDisconnected(ComponentName name) { } }, Context.BIND_AUTO_CREATE);
但是這個程式實際上執行起來是這樣的:
01-01 05:31:55.47548684880 E JavaBinder: *** Uncaught remote exception!(Exceptions are not yet supported across processes.) 01-01 05:31:55.47548684880 E JavaBinder: java.lang.RuntimeException: TestException 01-01 05:31:55.47548684880 E JavaBinder:at me.linjw.demo.ipcdemo.AidlService$1.testThrowException(AidlService.java:22) 01-01 05:31:55.47548684880 E JavaBinder:at me.linjw.demo.ipcdemo.ITestExceptionAidl$Stub.onTransact(ITestExceptionAidl.java:48) 01-01 05:31:55.47548684880 E JavaBinder:at android.os.Binder.execTransact(Binder.java:565)
看日誌裡面的ITestExceptionAidl$Stub.onTransact,也就是說在service端就已經被異常打斷了,並沒有傳給client端,而且第一個大大的”Exceptions are not yet supported across processes.”是說異常不允許跨程序嗎?但是我明明記得AIDL生成的程式碼裡面就有向Parcel寫入異常啊:
public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException { switch (code) { case INTERFACE_TRANSACTION: { reply.writeString(DESCRIPTOR); return true; } case TRANSACTION_testThrowException: { data.enforceInterface(DESCRIPTOR); boolean _result = this.testThrowException(); reply.writeNoException(); // 這裡寫入的是沒有丟擲異常 reply.writeInt(((_result) ? (1) : (0))); return true; } } return super.onTransact(code, data, reply, flags); }
查詢Parcel的原始碼,其實是有writeException方法的:
public final void writeException(Exception e) { int code = 0; if (e instanceof Parcelable && (e.getClass().getClassLoader() == Parcelable.class.getClassLoader())) { // We only send Parcelable exceptions that are in the // BootClassLoader to ensure that the receiver can unpack them code = EX_PARCELABLE; } else 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; } else if (e instanceof ServiceSpecificException) { code = EX_SERVICE_SPECIFIC; } writeInt(code); StrictMode.clearGatheredViolations(); if (code == 0) { if (e instanceof RuntimeException) { throw (RuntimeException) e; } throw new RuntimeException(e); } writeString(e.getMessage()); ... }
可以看到其實Parcel是支援寫入異常的,但是隻支援Parcelable的異常或者下面這幾種異常:
- SecurityException
- BadParcelableException
- IllegalArgumentException
- NullPointerException
- IllegalStateException
- NetworkOnMainThreadException
- UnsupportedOperationException
- ServiceSpecificException
如果是普通的RuntimeException,這打斷寫入,繼續丟擲。
於是我們將RuntimeException改成它支援的UnsupportedOperationException試試:
// service端改成丟擲UnsupportedOperationException ppublic class AidlService extends Service { @Nullable @Override public IBinder onBind(Intent intent) { return new ITestExceptionAidl.Stub() { @Override public boolean testThrowException() throws RemoteException { if (true) { throw new UnsupportedOperationException("TestException"); } return true; } }; } } // client端實現還是一樣,不變 bindService(intent, new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { ITestExceptionAidl aidl = ITestExceptionAidl.Stub.asInterface(service); try { aidl.testThrowException(); } catch (Exception e) { Log.e("testtest", "Exception", e); } } @Override public void onServiceDisconnected(ComponentName name) { } }, Context.BIND_AUTO_CREATE);
這樣執行的話客戶端就能捕獲到異常:
01-01 05:49:46.770 19937 19937 E testtest: RemoteException 01-01 05:49:46.770 19937 19937 E testtest: java.lang.UnsupportedOperationException: TestException 01-01 05:49:46.770 19937 19937 E testtest:at android.os.Parcel.readException(Parcel.java:1728) 01-01 05:49:46.770 19937 19937 E testtest:at android.os.Parcel.readException(Parcel.java:1669) 01-01 05:49:46.770 19937 19937 E testtest:at me.linjw.demo.ipcdemo.ITestExceptionAidl$Stub$Proxy.testThrowException(ITestExceptionAidl.java:77) 01-01 05:49:46.770 19937 19937 E testtest:at me.linjw.demo.ipcdemo.MainActivity$3.onServiceConnected(MainActivity.java:132) 01-01 05:49:46.770 19937 19937 E testtest:at android.app.LoadedApk$ServiceDispatcher.doConnected(LoadedApk.java:1465) 01-01 05:49:46.770 19937 19937 E testtest:at android.app.LoadedApk$ServiceDispatcher$RunConnection.run(LoadedApk.java:1482) 01-01 05:49:46.770 19937 19937 E testtest:at android.os.Handler.handleCallback(Handler.java:751) 01-01 05:49:46.770 19937 19937 E testtest:at android.os.Handler.dispatchMessage(Handler.java:95) 01-01 05:49:46.770 19937 19937 E testtest:at android.os.Looper.loop(Looper.java:154) 01-01 05:49:46.770 19937 19937 E testtest:at android.app.ActivityThread.main(ActivityThread.java:6097) 01-01 05:49:46.770 19937 19937 E testtest:at java.lang.reflect.Method.invoke(Native Method) 01-01 05:49:46.770 19937 19937 E testtest:at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1052) 01-01 05:49:46.770 19937 19937 E testtest:at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:942)
跨程序傳遞異常的原理
好,知道了如何去跨程序傳遞異常之後,然後我們來看看異常到底是如何傳遞過去的。
讓我們再來看看異常寫入的程式碼:
// 有異常的情況 public final void writeException(Exception e) { int code = 0; if (e instanceof Parcelable && (e.getClass().getClassLoader() == Parcelable.class.getClassLoader())) { // We only send Parcelable exceptions that are in the // BootClassLoader to ensure that the receiver can unpack them code = EX_PARCELABLE; } else 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; } else if (e instanceof ServiceSpecificException) { code = EX_SERVICE_SPECIFIC; } writeInt(code); StrictMode.clearGatheredViolations(); if (code == 0) { if (e instanceof RuntimeException) { throw (RuntimeException) e; } throw new RuntimeException(e); } writeString(e.getMessage()); // 之後還有一些寫入堆疊的操作,比較多,這裡可以不看 } public final void writeNoException() { if (StrictMode.hasGatheredViolations()) { // 如果StrictMode收集到了寫違規行為會走這裡,我們可以不關注它 writeInt(EX_HAS_REPLY_HEADER); ... } else { // 一般情況下會走這裡 writeInt(0); } }
這裡給每種支援的異常都編了個號碼,它會往Parcel寫入。而0代表的是沒有發生異常。然後再看看讀取異常的程式碼:
public boolean testThrowException() throws android.os.RemoteException { android.os.Parcel _data = android.os.Parcel.obtain(); android.os.Parcel _reply = android.os.Parcel.obtain(); boolean _result; try { _data.writeInterfaceToken(DESCRIPTOR); mRemote.transact(Stub.TRANSACTION_testThrowException, _data, _reply, 0); _reply.readException(); _result = (0 != _reply.readInt()); } finally { _reply.recycle(); _data.recycle(); } return _result; } // android.os.Parcel.readException public final void readException() { int code = readExceptionCode(); if (code != 0) { String msg = readString(); //在這個方法裡面建立異常並且丟擲 readException(code, msg); } }
然後這裡有個需要注意的點就是異常必須是寫在Parcel的頭部的,也就是說如果沒有異常,我們先要將0寫到頭部,然後再將返回值繼續往後面寫入。如果有異常,我們要先將異常編碼寫入頭部,然後就不需要再寫入返回值了。
這樣,在客戶端讀取的時候讀取的頭部就能知道到底有沒有異常,沒有異常就繼續讀取返回值,有異常就將異常讀取出來並且丟擲。
// service端程式碼 boolean _result = this.testThrowException(); reply.writeNoException(); // 先寫入異常 reply.writeInt(((_result) ? (1) : (0))); // 再寫入返回值 // client端程式碼 mRemote.transact(Stub.TRANSACTION_testThrowException, _data, _reply, 0); _reply.readException(); // 先讀取異常,有異常的話readException方法裡面會直接丟擲 _result = (0 != _reply.readInt()); // 再讀取返回值
也就是Parcel的頭部是一個標誌位,標誌了有異常或者無異常:

但是我們看到AIDL生成的程式碼都是寫入的無異常,那我們丟擲的異常是怎麼傳過去的呢?還記得這個列印嗎?
01-01 05:31:55.47548684880 E JavaBinder: *** Uncaught remote exception!(Exceptions are not yet supported across processes.) 01-01 05:31:55.47548684880 E JavaBinder: java.lang.RuntimeException: TestException 01-01 05:31:55.47548684880 E JavaBinder:at me.linjw.demo.ipcdemo.AidlService$1.testThrowException(AidlService.java:22) 01-01 05:31:55.47548684880 E JavaBinder:at me.linjw.demo.ipcdemo.ITestExceptionAidl$Stub.onTransact(ITestExceptionAidl.java:48) 01-01 05:31:55.47548684880 E JavaBinder:at android.os.Binder.execTransact(Binder.java:565)
我們去android.os.Binder.execTransact這裡找找看, onTransact方法實際就是在這裡被呼叫的
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|RuntimeException e) { ... reply.setDataPosition(0); reply.writeException(e); res = true; } catch (OutOfMemoryError e) { RuntimeException re = new RuntimeException("Out of memory", e); reply.setDataPosition(0); reply.writeException(re); res = true; } checkParcel(this, code, reply, "Unreasonably large binder reply buffer"); reply.recycle(); data.recycle(); return res; }
看,這裡如果catch到了方法,也就是說我們服務端有丟擲異常,就會在catch程式碼塊裡面先就Parcel的遊標重置回0,然後往Parcel頭部寫入異常。
好,到了這裡其實整個流程就差不多了,但是我發現我沒有看到那個”Exceptions are not yet supported across processes.”字串,這個不支援的提示又是哪裡來的呢?
讓我們再回憶下程式碼,在遇到不支援的異常型別的時候, writeException也會丟擲異常:
public final void writeException(Exception e) { int code = 0; if (e instanceof Parcelable && (e.getClass().getClassLoader() == Parcelable.class.getClassLoader())) { // We only send Parcelable exceptions that are in the // BootClassLoader to ensure that the receiver can unpack them code = EX_PARCELABLE; } else 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; } else if (e instanceof ServiceSpecificException) { code = EX_SERVICE_SPECIFIC; } writeInt(code); StrictMode.clearGatheredViolations(); // code為0,代表不支援這種異常,繼續把異常丟擲或者建立RuntimeException丟擲 if (code == 0) { if (e instanceof RuntimeException) { throw (RuntimeException) e; } throw new RuntimeException(e); } ... }
由於這個writeException,已經是在catch程式碼塊裡面執行的了,沒有人再去catch它,於是就會打斷這個流程,直接跳出。形成了一個Uncaught remote exception。
最後我們找到/frameworks/base/core/jni/android_util_Binder.cpp的onTransact方法,這裡通過jni調到Java的execTransact方法,呼叫完之後進行ExceptionCheck,如果發現有異常的話就report_exception:
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(); jboolean res = env->CallBooleanMethod(mObject, gBinderOffsets.mExecTransact, code, reinterpret_cast<jlong>(&data), reinterpret_cast<jlong>(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; env->DeleteLocalRef(excep); } ... }