我们平时应用开发跨进程传递数据这个是经常有但是传递的数据量很大甚至超过了允许的最大值导致抛异常这个可能不那么常见。比如发了一个很长的列表或者一个很大的字符串或者发了一张大图都有可能出现异常。
这个问题虽然不常见但是一旦出现一般都很棘手而且一定要解决的啊。我们怎么解决呢怎么样才能突破这个限制呢这是个问题啊我们接下来就来讨论这个问题。
跨进程传递大图我们能想到哪些方案呢
最容易想到的一种就是先给图片保存到文件给路径跨进程传过去对方再从文件给图片decode出来这个方案是可行就是性能不怎么样你能想象我传个大图要好几秒么。
另一种方案就是通过跨进程通信的方式就是不走文件直接走内存这个肯定会快不少。跨进程通信有哪些方式呢
首先Binder性能是可以用起来也方便但是有大小的限制传的数据量大了就会抛异常。Socket或者管道性能不太好涉及到至少两次拷贝。共享内存性能还不错可以考虑关键看怎么实现。总之呢性能是重点考虑的因素。
我们来看通过Binder传图有几种方案一个是通过Intent传图还一个可以通过Binder调用传图。有人说了这两个不是一回事吗通过Intent传图在启动应用组件的时候还不是一个Binder调用但是你试试就会知道通过Intent传大图可能会抛TransactionTooLargeException异常但是通过普通的Binder调用传图就没事这是为什么呢我们来研究一下。
先看Intent传图哈
Bundle b = new Bundle(); b.putParcelable("bitmap", mBitmap); intent.putExtras(b);
再看Binder调用传图
Bundle b = new Bundle(); b.putBinder(“binder”, new IRemoteCaller.Stub() { @Override public Bitmap getBitmap() { return mBitmap; } }); intent.putExtras(b);
这里顺便说一下Binder调用传图是往Intent里塞了个Binder对象等到另一个组件启动之后读出这个Binder对象调用它的getBitmap函数拿到Bitmap。
这两个实现上有什么区别么我们来看一下源码就从startActivity开始吧
int startActivity(..., Intent intent, ...) { Parcel data = Parcel.obtain(); ...... intent.writeToParcel(data, 0); ...... mRemote.transact(START_ACTIVITY_TRANSACTION, data, reply, 0); ...... }
我们重点关注Bitmap是怎么传输的这里给Intent写到Parcel了通过下面这个writeToParcel函数其实就是给Intent里的Bundle写到Parcel了
public void writeToParcel(Parcel out, int flags) { ...... out.writeBundle(mExtras); }
继续往下走看Bundle怎么写到Parcel的原来是调到了Bundle的writeToParcel函数
public final void writeBundle(Bundle val) { val.writeToParcel(this, 0); }
继续往下走又调到了writeToParcelInner
public void writeToParcel(Parcel parcel, int flags) { final boolean oldAllowFds = parcel.pushAllowFds(mAllowFds); super.writeToParcelInner(parcel, flags); parcel.restoreAllowFds(oldAllowFds); }
这个pushAllowFds是啥呢就是说如果Bundle里不允许带描述符那Bundle写到Parcel里的时候Parcel也不许带描述符了。
bool Parcel::pushAllowFds(bool allowFds) { const bool origValue = mAllowFds; if (!allowFds) { mAllowFds = false; } return origValue; }
我们再看writeToParcelInner函数大家耐心一点马上就要接近真相了
void writeToParcelInner(Parcel parcel, int flags) { ...... parcel.writeArrayMapInternal(mMap); }
这里调到了Parcel的writeArrayMapInternal函数Bundle其实核心就是一个ArrayMap。写Bundle就是写这个ArrayMap。我们看这个Map是怎么写到Parcel的。
void writeArrayMapInternal(ArrayMap<String, Object> val) { final int N = val.size(); writeInt(N); for (int i = 0; i < N; i++) { writeString(val.keyAt(i)); writeValue(val.valueAt(i)); } }
这逻辑很简单啊就是在一个for循环里给map的key和value依次写到parcel。我们看writeValue是怎么写的里面会根据value的不同类型采取不同的写法
public final void writeValue(Object v) { ...... else if (v instanceof Parcelable) { writeInt(VAL_PARCELABLE); writeParcelable((Parcelable) v, 0); } ...... }
因为Bitmap是Parcelable的所以我们只关注这个分支这又调到了Bitmap的writeToParcel函数
void writeParcelable(Parcelable p, int parcelableFlags) { writeParcelableCreator(p); p.writeToParcel(this, parcelableFlags); }
我们继续看Bitmap的writeToParcel这又进入了native层
public void writeToParcel(Parcel p, int flags) { nativeWriteToParcel(mFinalizer.mNativeBitmap, ...); }
我们看native层是怎么实现的
jboolean Bitmap_writeToParcel(JNIEnv* env, jobject, ...) { android::Bitmap* androidBitmap = reinterpret_cast<Bitmap*>(bitmapHandle); androidBitmap->getSkBitmap(&bitmap); // 往parcel里写Bitmap的各种配置参数 int fd = androidBitmap->getAshmemFd(); if (fd >= 0 && !isMutable && p->allowFds()) { status = p->writeDupImmutableBlobFileDescriptor(fd); return JNI_TRUE; } android::Parcel::WritableBlob blob; status = p->writeBlob(size, mutableCopy, &blob); const void* pSrc = bitmap.getPixels(); memcpy(blob.data(), pSrc, size); }
这里首先拿到native层的Bitmap对象叫androidBitmap然后拿到对应的SkBitmap。先看bitmap里带不带ashmemFd如果带并且这个Bitmap不能改并且Parcel是允许带fd的话就给fd写到parcel里然后返回。否则的话继续往下先有个WriteBlob对象通过writeBlob函数给这个blob在parcel里分配了一块空间然后给bitmap拷贝到这块空间里。我们看这个writeBlob函数
status_t Parcel::writeBlob(size_t len, bool mutableCopy, WritableBlob* outBlob) { if (!mAllowFds || len <= BLOB_INPLACE_LIMIT) { status = writeInt32(BLOB_INPLACE); void* ptr = writeInplace(len); outBlob->init(-1, ptr, len, false); return NO_ERROR; } int fd = ashmem_create_region("Parcel Blob", len); void* ptr = mmap(NULL, len, ..., MAP_SHARED, fd, 0); ...... status = writeFileDescriptor(fd, true); outBlob->init(fd, ptr, len, mutableCopy); return NO_ERROR; }
这个writeBlob函数首先看如果不允许带fd或者这个数据小于16K就直接在parcel的缓冲区里分配一块空间来保存这个数据。不然的话呢就另外开辟一个ashmem映射出一块内存数据就保存在ashmem的内存里parcel里只写个fd就好了这样就算数据量很大parcel自己的缓冲区也不用很大。
bitmap的传输原理咱们清楚了但是还有一个问题没有解决为什么intent带大图会异常但是binder调用带大图就没事呢肯定是因为intent带bitmap的时候bitmap直接拷到parcel缓冲区了没有利用这个ashmem。为什么呢
咱们注意到只可能是这个allowFds没打开咱们研究一下。
startActivity的时候会调到execStartActivity这里会调到prepareToLeaveProcess里面会禁用intent的allowFdssendBroadcast也会这样bindService也一样哈。
public ActivityResult execStartActivity(..., Intent intent, ...) { ...... intent.prepareToLeaveProcess(); ActivityManagerNative.getDefault().startActivity(...); }
至于为什么应用组件通信的时候要专门禁用这个fd大家可以想一想哈我个人觉得可能是因为安全问题。
另外如果要传的不是图片而是一个数据列表那大家可以考虑用ContentProvider或者直接用MemoryFile。这两个底层其实都用到了共享内存跨进程传输大的数据刚好合适具体原理这就不讲了大家有兴趣的可以自己研究一下。
热门评论
从页面A跳转页面B,传递大图;两种方式都是走startActivity(intent); 不同的是一个bundle里面直接放bitmap,一个是binder调用,即bundle里面放的binder对象对bitmap做了一层包裹;也就是说两种方式都要走startActivity(); 都要走setAllowFd(false), 那么问题来了,为甚么这个设置对第二种方式失效呢?
为什么没有 IRemoteCaller.Stub这个接口啊