NullPointerException引发的对Android跨进程通信的思考
事故是这样的:
java.lang.NullPointerException: Attempt to read from field
'android.os.MessageQueue android.os.Looper.mQueue'
on a null object reference
at android.os.Parcel.readException(Parcel.java:1626)
at android.os.Parcel.readException(Parcel.java:1573)
at com.zhangxiang.IBooster$Stub$Proxy.startControl(IBooster.java:136)
从log上看这个crash是从一个aidl接口抛出的,实际上是我在client端通过Binder去调用server端的接口抛出的异常。紧接着我试着在server端断点调试,看看是server端走到哪一步抛出的异常,调试的结果是在一个类的初始化函数里面抛出的。一开始我是一脸懵逼的,一个类的初始化函数也能抛出异常?后来在server端try-catch了这个exception打印了一下这个log的堆栈信息找到问题了(log如下)
MSG: java.lang.NullPointerException: Attempt to read from field ‘android.os.MessageQueue android.os.Looper.mQueue’ on a null object reference at android.os.Handler.
(Handler.java:229) at android.os.Handler. (Handler.java:137) at android.telephony.PhoneStateListener$1. (PhoneStateListener.java:279) at android.telephony.PhoneStateListener. (PhoneStateListener.java:279) at android.telephony.PhoneStateListener. (PhoneStateListener.java:249) ```
问题是这个类的一个成员变量进行初始化的时候默认会用当前线程的handler作为这个成员变量(类)初始化的一个成员变量的赋值,其实就是需要一个handler。如果不显示的调用的这个成员变量带有handler参数的初始化方法,就会默认取当前线程的handler。很遗憾,server端的当前线程没有handler(因为是在binder线程),如果需要handler则需要perpare一个looper使得当前线程可以循环读取消息队列接着再创建一个handler去处理消息。当然也可以直接把server的逻辑post到mainThread,mainThread默认就有一个looper.
这就引发了我的一些思考,android跨进程调用的时候回调方法都在进程的哪些线程呢?
我试着总结了一下几种跨进程通信的方式:
AIDL(上面已经分析了,aidl的通信是在binder线程,后面我会分析一下这个binder线程是如何创建的,这里仅仅只要知道是在binder线程就行了)
Broadcast (当我们注册broadcastReceiver你知道onReiver的回调是在哪个线程吗?)
ContentProvider(当你通过uri访问一个进程的provider的时候,这个provider的增删改查方法是在哪个线程中回调的嘛?)
好的,我们花5分钟思考一下回答一下这个问题。。。
算了,我们还是做一个简单的实验看下结果吧。
- 先看下BroadcastReceiver的onReceiver回调是在哪个线程里面 (打印线程信息)
11-01 18:38:08.618 10023 10023 I GameService: 当前线程信息:Thread[main,5,main]
很好,是在MainThread里面,我想跟大部分人的想法一样,我们经常在这个回调里面做一些主线程的ui更新操作,没问题。但是我们常常都是程序的主线程去
register
一个BroadcastReceiver
,假设我们new Thread
去register
,你猜onReceiver还是在主线程中回调吗?看下结果:I GameService: 当前线程信息:Thread[main,5,main]
还是在MainThread,所以你要注意了,如果在onReceiver里面做一些耗时操作的话一定要在10s内解决。开玩笑,如果要做耗时操作请在service里面做,也不要在子线程做,因为当你的进程不是常驻的时候(比如你静态注册了一个receiver)很有可能接受到这个broadcast后这个进程变成空进程(没有组件了)会被系统回收掉,这个时候你的子线程也不会执行完,所以可以用一个service去做这个耗时操作。另外,androidO已经禁止了除系统进程外的三方应用通过静态注册的方式注册系统广播,这个应该是为了减轻ams的负担做的考虑,也防止了三方应用通过系统广播这种方式自启动。 - 那么ContentProvider的增删改查操作是不是也在进程的mainThread中执行呢?
11-01 18:37:04.220 10023 10036 I com.miui.&*!@.Provider: 当前线程信息:Thread[Binder:10023_2,5,main]
很遗憾,并不是,是在binder线程中,所以即便在增删改查中做一些耗时操作也不会导致ANR,我也试了在里面sleep很长时间并不会有异常。这个设计是合理的,contentProvider作为跨进程的数据访问方式,增删改查消耗一些时间是很正常的,但是作为client端去访问的时候需要在子线程去异步执行这个操作否则这个时间过长会抛出异常。
好了,结果分析完了,知其然也要知其所以然,我们通过源码来分析一下android系统是如何做到跨进程调用在不同的线程中执行(很多是辉辉博客里面的代码摘要,这里我们仅仅关注上述跨进程是发生在不同的线程中的,省略其他的代码分析)。
1.通过binder接口调用(AIDL也属于这种)
我们通常使用aidl接口的时候都会生成类似下面的代码:
@Override public void updateArticle(int type, com.example.binderServer.Article article) throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
try {
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeInt(type);
if ((article!=null)) {
_data.writeInt(1);
article.writeToParcel(_data, 0);
}
else {
_data.writeInt(0);
}
mRemote.transact(Stub.TRANSACTION_updateArticle, _data, _reply, 0);
_reply.readException();
}
finally {
_reply.recycle();
_data.recycle();
}
}
当我们在client端调用updateArticle做跨进程通信时实际上是调用的这段代码,这就是一个标准的binderIPC调用,mRemote指向IBinder类型的server端代理,mRemote.transact()实际上是binderProxy.transact():
final class BinderProxy implements IBinder {
public boolean transact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
//用于检测Parcel大小是否大于800k
Binder.checkParcel(this, code, data, "Unreasonably large binder buffer");
return transactNative(code, data, reply, flags);
}
}
最终会通过jni调用native层的IPCThreadState::self()->transact(
mHandle, code, data, reply, flags);
,而这个方法又会经过层层调用通过sysCall的方式从用户空间调用至内核空间(即binderDriver所在空间),而这个空间是真正可以共享内存的地方,我们简单看一下从用户空间是经历了哪些方法调用到内核空间的(这里的syscall指的是Binder ioctl,syscall还有其他的方式比如Binder_open等)
简单的过程如上(横向代表同一个方法内部的线形执行),最终会调用talkWithDriver()
陷入内核(方式是ioctl
)。我们接着分析看一下陷入内核之后的调用:
这个过程看代码较为复杂,这里我们关注binder线程的创建,其实这段调用实际上最终会产生一个BR_SPAWN_LOOPER
的cmd,接着调用executeCommand
来执行这个cmd:
status_t IPCThreadState::executeCommand(int32_t cmd)
{
status_t result = NO_ERROR;
switch ((uint32_t)cmd) {
...
case BR_SPAWN_LOOPER:
//创建新的binder线程
mProcess->spawnPooledThread(false);
break;
...
}
return result;
}
创建出一个新的binder普通线程,它的创建需要一些条件:
- 当前进程中没有请求创建binder线程;
- 当前进程没有空闲可用的binder线程;(线程进入休眠状态的个数就是空闲线程数)
- 当前进程已启动线程个数小于最大上限(默认15);
- 当前线程已接收到BC_ENTER_LOOPER或者BC_REGISTER_LOOPER命令,即当前处于BINDER_LOOPER_STATE_REGISTERED或者BINDER_LOOPER_STATE_ENTERED状态。【小节2.6】已设置状态为BINDER_LOOPER_STATE_ENTERED,显然这条件是满足的。
请注意,上述说的是binder普通线程,实际上还有一个binder主线程,这个实际上是在进程启动的时候创建的:
Zygote进程发出创建进程的socket消息,Zygote收到消息后会调用Zygote.forkAndSpecialize()来fork出新进程,在新进程中会调用到RuntimeInit.nativeZygoteInit方法,该方法经过jni映射,最终会调用到app_main.cpp中的onZygoteInit
virtual void onZygoteInit()
{
//获取ProcessState对象
sp<ProcessState> proc = ProcessState::self();
//启动新binder线程 【见下图中2.2】
proc->startThreadPool();
}
接下来我们就用一张代码流程图来看看整个创建binder主线程的过程:
一张图不够用,我们再继续分析一下上图2.6代码逻辑:
这里基本上就分析完了,我们再看下面一张图来看看整个binder线程的创建:
2.通过BroadCastReceiver回调
通过注册广播回调的方式我们要分析两个过程,一个是注册广播,一个是发送广播。
2.1注册广播
广播注册较与发送过程简单一点,我们来看一下我们通常调用的方式:
public Intent registerReceiver(BroadcastReceiver receiver,IntentFilter filter) {
return registerReceiver(receiver, filter, null, null);
}
public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter,String broadcastPermission, Handler scheduler) {
return registerReceiverInternal(receiver, getUserId(),
filter, broadcastPermission, scheduler, getOuterContext());
}
private Intent registerReceiverInternal(BroadcastReceiver receiver, int userId,
IntentFilter filter, String broadcastPermission,
Handler scheduler, Context context) {
IIntentReceiver rd = null;
if (receiver != null) {
if (mPackageInfo != null && context != null) {
if (scheduler == null) {
//将主线程Handler赋予scheuler
scheduler = mMainThread.getHandler();
}
//获取IIntentReceiver对象【2.1.1】
rd = mPackageInfo.getReceiverDispatcher(
receiver, context, scheduler,
mMainThread.getInstrumentation(), true);
} else {
if (scheduler == null) {
scheduler = mMainThread.getHandler();
}
rd = new LoadedApk.ReceiverDispatcher(
receiver, context, scheduler, null, true).getIIntentReceiver();
}
}
try {
//调用AMP.registerReceiver 【2.1.2】
return ActivityManagerNative.getDefault().registerReceiver(
mMainThread.getApplicationThread(), mBasePackageName,
rd, filter, broadcastPermission, userId);
} catch (RemoteException e) {
return null;
}
}
registerReciverInternal
我们可以看到当我们不指定handler
默认就是注册receiver
进程中的主线程handler
赋值给scheduler
,这个scheduler
会和receiver
以及context
等被封装成ReceiverDispatcher
(过程2.3中返回的IIntentReceiver
就是该类的一个内部类),我们仔细看一下这个广播分发者ReceiverDispatcher
和其内部类InnerReceiver
2.1.1 创建ReceiverDispatcher和InnerReceiver
- ReceiverDispatcher
ReceiverDispatcher(BroadcastReceiver receiver, Context context,
Handler activityThread, Instrumentation instrumentation,
boolean registered) {
//创建InnerReceiver【2.3.2】
mIIntentReceiver = new InnerReceiver(this, !registered);
mReceiver = receiver;
mContext = context;
mActivityThread = activityThread;
mInstrumentation = instrumentation;
mRegistered = registered;
mLocation = new IntentReceiverLeaked(null);
mLocation.fillInStackTrace();
}
- InnerReceiver
final static class InnerReceiver extends IIntentReceiver.Stub {
final WeakReference<LoadedApk.ReceiverDispatcher> mDispatcher;
final LoadedApk.ReceiverDispatcher mStrongRef;
InnerReceiver(LoadedApk.ReceiverDispatcher rd, boolean strong) {
mDispatcher = new WeakReference<LoadedApk.ReceiverDispatcher>(rd);
mStrongRef = strong ? rd : null;
}
...
}
实际上我们不用太关心外部类ReceiverDispatcher,内部类InnerReceiver已经存有外部类的弱引用rd,这个InnerReceiver是个binder类,可以进行binder通信,即2.4中data.writeStrongBinder(receiver != null ? receiver.asBinder() : null);
2.1.2 与ams进行binder通信进行注册过程
public Intent registerReceiver(IApplicationThread caller, String packageName,IIntentReceiver receiver,
IntentFilter filter, String perm, int userId) throws RemoteException
{
Parcel data = Parcel.obtain();
Parcel reply = Parcel.obtain();
data.writeInterfaceToken(IActivityManager.descriptor);
data.writeStrongBinder(caller != null ? caller.asBinder() : null);
data.writeString(packageName);
data.writeStrongBinder(receiver != null ? receiver.asBinder() : null);
filter.writeToParcel(data, 0);
data.writeString(perm);
data.writeInt(userId);
//Command为REGISTER_RECEIVER_TRANSACTION
mRemote.transact(REGISTER_RECEIVER_TRANSACTION, data, reply, 0);
reply.readException();
Intent intent = null;
int haveIntent = reply.readInt();
if (haveIntent != 0) {
intent = Intent.CREATOR.createFromParcel(reply);
}
reply.recycle();
data.recycle();
return intent;
}
接下来的过程在ams中,最终会会将注册者进程中的receiver保存到ams中的AMS.mRegisteredReceivers(已注册广播队列)
,
其中mRegisteredReceivers是记录着所有已注册的广播,以receiver IBinder为key, ReceiverList为value为HashMap。其中reveiver代表广播接收者队列。
2.2发送广播
发送广播过程比较复杂,我们先来看一张时序图:(从左至右按颜色划分分别代表客户端进程,ams所在进程,”ActivityManager”的线程(实际就是在主线程),广播调用者进程,广播接收者线程(主线程)) 我们以并行广播为例跳过前面的步骤直接从12(2.2.1)开始分析,12通过binderIPC调用到发送广播调用者进程ApplicationThread(AT)最终将调用至广播接收者主线程
2.2.1 scheduleRegisteredReceiver
public void scheduleRegisteredReceiver(IIntentReceiver receiver, Intent intent,
int resultCode, String dataStr, Bundle extras, boolean ordered,
boolean sticky, int sendingUser, int processState) throws RemoteException {
//更新虚拟机进程状态
updateProcessState(processState, false);
//【见小节2.2.2】
receiver.performReceive(intent, resultCode, dataStr, extras, ordered,
sticky, sendingUser);
}
此处的reveiver就是注册广播时创建的,见小节[2.1.1],可知该receiver=LoadedApk.ReceiverDispatcher.InnerReceiver。
2.2.2 InnerReceiver.performReceive和ReceiverDispatcher.performReceive
- InnerReceiver.performReceive
public void performReceive(Intent intent, int resultCode, String data,
Bundle extras, boolean ordered, boolean sticky, int sendingUser) {
LoadedApk.ReceiverDispatcher rd = mDispatcher.get();
if (rd != null) {
//【见ReceiverDispatcher.performReceive】
rd.performReceive(intent, resultCode, data, extras, ordered, sticky, sendingUser);
} else {
...
}
}
- ReceiverDispatcher.performReceive
public void performReceive(Intent intent, int resultCode, String data,
Bundle extras, boolean ordered, boolean sticky, int sendingUser) {
Args args = new Args(intent, resultCode, data, extras, ordered,
sticky, sendingUser);
//通过handler消息机制发送args.
if (!mActivityThread.post(args)) {
//消息成功post到主线程,则不会走此处。
if (mRegistered && ordered) {
IActivityManager mgr = ActivityManagerNative.getDefault();
args.sendFinished(mgr);
}
}
}
其中Args继承于BroadcastReceiver.PendingResult,实现了接口Runnable; 其中mActivityThread是当前进程的主线程, 是由[小节2.1.1]完成赋值过程.最终通过消息分发机制post到主线程执行ReceiverDispatcher.Args.run方法,通过类加载器完成onReveiver回调2.2.3
2.2.3ReceiverDispatcher.Args.run
public final class LoadedApk {
static final class ReceiverDispatcher {
final class Args extends BroadcastReceiver.PendingResult implements Runnable {
public void run() {
final BroadcastReceiver receiver = mReceiver;
final boolean ordered = mOrdered;
final IActivityManager mgr = ActivityManagerNative.getDefault();
final Intent intent = mCurIntent;
mCurIntent = null;
if (receiver == null || mForgotten) {
if (mRegistered && ordered) {
sendFinished(mgr);
}
return;
}
try {
//获取mReceiver的类加载器
ClassLoader cl = mReceiver.getClass().getClassLoader();
intent.setExtrasClassLoader(cl);
setExtrasClassLoader(cl);
receiver.setPendingResult(this);
//回调广播onReceive方法
receiver.onReceive(mContext, intent);
} catch (Exception e) {
...
}
if (receiver.getPendingResult() != null) {
finish();
}
}
}
}
接下来,便进入主线程,最终调用BroadcastReceiver
具体实现类的onReceive()
方法。
3.通过ContentProvider跨进程调用
实际上通过contentprovider访问调用者进程返回的cursor对象时也是一个类似1过程的binder通信,只不过这里的ontranct的协议是QUERY_TRANSACTION:
mRemote.transact(IContentProvider.QUERY_TRANSACTION, data, reply, 0);
最终也会调用至被调用进程的binder线程中,这个过程很类似,我们就归作一类。contentProvider跨进程通信复杂且核心的地方在于provider的发布和安装过程.
- 什么是发布过程(即publish)?
发布过程实际上是调用者进程通过binder call调用至ams中,ams发现调用者进程需要请求的provider(以ContentProviderRecord对象描述)尚未保存在mProviderMap中,通知provider所在进程启动并安装该进程下的所有的provider,当provider安装完成之后,被调用者进程再binder call至ams进行publish,将所有provider保存至ams的mProvdermap中(保存的是ContentProviderRecord)。
- 什么事安装过程(即install)?
其实就是请求provider进程的activitythread将provider对象保存至一个以
new ProviderKey(auth, userId)
为key和以new ProviderClientRecord(auths, provider, localProvider, holder)
为value的hashmap中,同时被请求进程的acitivitythread通过反射创建目标ContentProvider对象并回调目标ContentProvider.onCreate方法