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.475 4868 4880 e javabinder: *** uncaught remote exception! (exceptions are not yet supported across processes.)
01-01 05:31:55.475 4868 4880 e javabinder: java.lang.runtimeexception: testexception
01-01 05:31:55.475 4868 4880 e javabinder: at me.linjw.demo.ipcdemo.aidlservice$1.testthrowexception(aidlservice.java:22)
01-01 05:31:55.475 4868 4880 e javabinder: at me.linjw.demo.ipcdemo.itestexceptionaidl$stub.ontransact(itestexceptionaidl.java:48)
01-01 05:31:55.475 4868 4880 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.475 4868 4880 e javabinder: *** uncaught remote exception! (exceptions are not yet supported across processes.)
01-01 05:31:55.475 4868 4880 e javabinder: java.lang.runtimeexception: testexception
01-01 05:31:55.475 4868 4880 e javabinder: at me.linjw.demo.ipcdemo.aidlservice$1.testthrowexception(aidlservice.java:22)
01-01 05:31:55.475 4868 4880 e javabinder: at me.linjw.demo.ipcdemo.itestexceptionaidl$stub.ontransact(itestexceptionaidl.java:48)
01-01 05:31:55.475 4868 4880 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); } ... }
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
上一篇: php-7.3.6 编译安装过程
下一篇: JNI方法实现图片压缩(压缩率极高)