0%

Android跨进程传送Bitmap

平常都在一个进程中使用Bitmap,最近用了用跨进程使用Bitmap。Bitmap本来就是内存大户,再叠加跨进程传送,难免会想到会不会导致使用的内存剧增呢?

最简单的方式

跨进程传送Bitmap肯定不能用Intent+Bundle的方式传送,都是知道会抛出TransactionTooLargeException。那就走AIDL跨进程通信呗,如果把Bitmap当做一个普通的类来对待,写一段跨进程传送Bitmap的AIDL代码,就是这普通的写法:

1
2
3
4
5
6
7
8
9
// IO.aidl
package com.msisuzney.common;
import android.graphics.Bitmap;

interface IO {

void sendBitmap(in Bitmap io);

}

形参前in是我加上的,AIDL文件默认的就是in,它表示调用方传过来的这个Bitmap的修改不会同步给调用方。其实它们就是两个不同的Bitmap了,对这个Bitmap的修改丝毫不会影响到调用方的Bitmap,这不是我们想要的,我们需要的是对这个Bitmap的修改能够返回给调用方,很自然的就会想到使用inout关键字:

1
void changeColor(inout Bitmap io);

inout表示对该对象的修改,会在执行完这个方法后再序列化该对象传送到调用方,调用方会阻塞等待完成。但这段代码编译不过,提示没有Bitmap实现readFromParcel方法,为什么居然没实现呢?想想也是累,调用方进程一个Bitmap,跨进程到这里是另外一个Bitmap,返回给调用方又是一个新的Bitmap,一来回三Bitmap,显然太不合理。我们需要最理想情况是:两个进程持有、修改的Bitmap始终是同一个Bitmap。

使用共享内存

Bitmap的内存在Android 7之后就保存在了native层,我们实际上可以使用mmap的方式把它这块内存共享出去,这样其他进程对它的修改就可以实时的同步到自己进程。Android中提供了SharedMemory用来共享内存,我们可以用它来共享Bitmap的native内存,定义的数据结构如下:

1
2
3
4
5
6
7
class IOBitmap(
var bitmapWidth: Int = 0,
var bitmapHeight: Int = 0,
var bitmapMemory: SharedMemory? = null
) : Parcelable {

}

AIDL文件修改为:

1
void changeColor(inout IOBitmap io);

下面这段代码就是将Bitmap封装进SharedMemory传送:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
lifecycleScope.launch(Dispatchers.IO) {
io?.apply {
val bitmap = this@MainActivity.bitmap!!
Log.d("Cxx", "bitmap size:${bitmap.allocationByteCount * 1.0 / 1024 / 1024}")
val sharedMemory = SharedMemory.create("bitmap_memory", bitmap.allocationByteCount)
val buffer = sharedMemory.mapReadWrite()
bitmap.copyPixelsToBuffer(buffer)
val ioBitmap = IOBitmap(bitmapWidth, bitmapHeight, sharedMemory)
Log.d("Cxx", "before ipc:$ioBitmap")
SharedMemory.unmap(buffer)
changeColor(ioBitmap)
Log.d("Cxx", "after ipc:$ioBitmap")
val changedBuffer = sharedMemory.mapReadWrite()
bitmap.copyPixelsFromBuffer(changedBuffer)
SharedMemory.unmap(changedBuffer)
withContext(Dispatchers.Main) {
this@MainActivity.bitmap = bitmap
imageView!!.setImageBitmap(this@MainActivity.bitmap)
}
}
}

changeColor(ioBitmap)是IPC方法,会堵塞该线程,等待服务端进程修改完成后继续执行,服务端进程的代码:

1
2
3
4
5
6
7
8
9
10
11
private inner class MyBinder : IO.Stub() {

override fun changeColor(io: IOBitmap?) {
io?.apply {
Log.d("Cxx", "received:${io}")
val buffer = this.bitmapMemory!!.mapReadWrite()
DirectBufferInterface().changeColor(buffer)
SharedMemory.unmap(buffer)
}

}

收到SharedMemory后拿出它的ByteBuffer修改,这里的ByteBuffer是DirectByteBuffer,没有意外,它也是堆外内存,所以对它的操作放在了native层,DirectBufferInterface的changeColor方法会调用native方法直接修改DirectByteBuffer里面的数组。
完成之后,再次序列化IOBitmap对象,并通知到正在堵塞的对方线程,对方线程的收到后执行changeColor方法后面的代码,显示修改完成的Bitmap。

验证

调用方进程新建一个10000x10000像素、ARGB8888的Bitmap,大小为10000x10000x4/1024/1024=381.5MB,使用共享内存的方式发送给服务方进程,服务方进程修改像素的色值后,再同步给调用方进程。以下是使用Profiler查看两个进程内存变化的情况:

客户端进程新建Bitmap

新建这个Bitmap并显示出来,可以看见Native和Graphics都增加了相同大小的区域

服务端进程修改Bitmap

客户端进程发起跨进程到服务端进程修改这个Bitmap的像素,服务端进程的内存变化情况:

服务端进程的Others内存增加后迅速恢复到以前的水平

客户端进程更新Bitmap

服务端进程修改完这个Bitmap后客户端继续执行,显示修改后的Bitmap。内存变化参考上面客户端进程内存变化图的最后部分,可以确定Native内存并没有波澜,只是显示出来时Graphics内存增加了。

使用HardwareBuffer传送Bitmap

Android 12后Bitmap多了一个方法:

1
2
3
4
5
Bitmap:
public @NonNull HardwareBuffer getHardwareBuffer() {

}

HardwareBuffer类:

HardwareBuffer wraps a native AHardwareBuffer object, which is a low-level object representing a memory buffer >accessible by various hardware units. HardwareBuffer allows sharing buffers across different application >processes. In particular, HardwareBuffers may be mappable to memory accessibly to various hardware systems, >such as the GPU, a sensor or context hub, or other auxiliary processing units. For more information, see the >NDK documentation for AHardwareBuffer.

1
public final class HardwareBuffer implements Parcelable, AutoCloseable

所以我们还可以新建一个Hardware的Bitmap,再通过共享HardwareBuffer的方式把这Bitmap跨进程传送。
服务端提供Hardware Bitmap:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RequiresApi(Build.VERSION_CODES.S)
override fun getHardwareBitmap(): HardwareBuffer {
val options = BitmapFactory.Options()
options.inScaled = false
//4000 * 4000 * 4 =
options.inSampleSize = 2
options.inPreferredConfig = Bitmap.Config.HARDWARE
val bitmap =
BitmapFactory.decodeResource(
this@MyService.resources,
R.drawable.test,
options
)
return bitmap.hardwareBuffer
}

客户端跨进程加载并显示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RequiresApi(Build.VERSION_CODES.Q)
fun get(view: View) {
lifecycleScope.launch(Dispatchers.IO) {
io?.apply {
val bitmap = Bitmap.wrapHardwareBuffer(
this.hardwareBitmap,
ColorSpace.get(ColorSpace.Named.SRGB)
)
withContext(Dispatchers.Main) {
imageView?.setImageBitmap(bitmap)
}
}
}
}

服务端进程内存变化:

客户端进程内存变化:

可以发现最后只剩下Graphics内存了。但这样做有一个缺点,这块Bitmap的像素不可以操作,只能显示。

代码

以上演示的Demo代码在AndroidBitmapSharingTest