欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android跨进程抛异常的原理的实现

程序员文章站 2023-11-09 13:37:52
今天接到了个需求,需要用到跨进程抛异常。 怎样将异常从服务端抛到客户端 也就是说在service端抛出的异常需要可以在client端接收。印象中binder是可以传异常...

今天接到了个需求,需要用到跨进程抛异常。

怎样将异常从服务端抛到客户端

也就是说在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的头部是一个标志位,标志了有异常或者无异常:

Android跨进程抛异常的原理的实现

但是我们看到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);
  }
  ...
}

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。