|
| 1 | +> 转载请注明出处:https://www.jianshu.com/p/48c5826359ef |
| 2 | +> |
| 3 | +> 本文出自 [容华谢后的博客](https://www.jianshu.com/p/48c5826359ef) |
| 4 | +
|
| 5 | +# 0.写在前面 |
| 6 | + |
| 7 | +在实际的项目开发中,为了更好的性能和结构,我们经常会把一个项目应用分成多个进程或者多个组件,当不同进程的业务之间需要通信时,通常会选择AIDL、Bundle或者广播的方式进行通信,这些通信手段有一个共同的特点,就是只支持简单、小容量、不频繁的数据传输,当我们的应用存在这样的业务场景,比如视频流跨进程传输、大文件跨进程传输等需要复杂数据、频繁数据交换的功能时,就不能使用上述的通信手段了。 |
| 8 | + |
| 9 | +为了解决这个问题,可以选择共享内存的方式进行通信,在设备的内存中开辟一个固定容量的空间,使用生产者-消费者模型,生产者在内存中写入数据,通过PV操作通知消费者进行读取,消费者读取完成后再通知生产者进行写入。 |
| 10 | + |
| 11 | +# 1.实现 |
| 12 | + |
| 13 | +## 1.1 数据结构 |
| 14 | + |
| 15 | +首先定义下共享内存中的数据结构,首先定义3个信号量: |
| 16 | + |
| 17 | +- sem_empty: 定义是否允许写,生产者写前 P(empty),消费者读后 V(empty) |
| 18 | + |
| 19 | +- sem_full: 定义是否允许读,消费者读前 P(full),生产者写后 V(full) |
| 20 | + |
| 21 | +- sem_mutex: 定义读写互斥锁,保护 state/data_len/data 的互斥 |
| 22 | + |
| 23 | +接下来定义数据传输的状态,分别是 IDLE(空闲状态)、DATA(传输数据状态)、EOF(文件传输结束状态),然后定义每次传输数据的有效长度 data_len 和数据块 data属性。 |
| 24 | + |
| 25 | +``` |
| 26 | +/** |
| 27 | + * 数据传输结构 |
| 28 | + */ |
| 29 | +struct SharedBlock { |
| 30 | + // 是否允许写(Producer) |
| 31 | + sem_t sem_empty; |
| 32 | + // 是否允许读(Consumer) |
| 33 | + sem_t sem_full; |
| 34 | + // 读写互斥锁 |
| 35 | + sem_t sem_mutex; |
| 36 | + // 当前状态 |
| 37 | + uint32_t state; |
| 38 | + // data 中有效数据长度 |
| 39 | + uint32_t data_len; |
| 40 | + // 数据 |
| 41 | + uint8_t data[SHM_DATA_SIZE]; |
| 42 | +}; |
| 43 | +
|
| 44 | +/** |
| 45 | + * 共享内存状态机 |
| 46 | + * |
| 47 | + * IDLE : 空闲状态 |
| 48 | + * DATA : 有有效数据 |
| 49 | + * EOF : 文件传输结束 |
| 50 | + */ |
| 51 | +enum ShmState : uint32_t { |
| 52 | + SHM_STATE_IDLE = 0, |
| 53 | + SHM_STATE_DATA = 1, |
| 54 | + SHM_STATE_EOF = 2, |
| 55 | +}; |
| 56 | +``` |
| 57 | + |
| 58 | +## 1.2 创建共享内存 |
| 59 | + |
| 60 | +通过 open("/dev/ashmem", O_RDWR) 方法创建共享内存,得到文件描述符,fd 可通过 AIDL 传递给其他进程,在其他进程通过 fd 映射相同的内存区域进行操作。创建完成后通过 ioctl 方法设置共享内存的名称和大小,再使用 mmap 对内存的地址空间进行映射,然后通过 reinterpret_cast 对内存空间进行结构化。 |
| 61 | + |
| 62 | +接下来进行信号量初始化,初始状态为可写、不可读、可进入临界区状态,数据传输状态初始为空闲状态,到这里共享内存就创建完成了,继续往下看共享内存的数据是如何进行读写的。 |
| 63 | + |
| 64 | +``` |
| 65 | +/** |
| 66 | + * 创建共享内存 |
| 67 | + */ |
| 68 | +extern "C" |
| 69 | +JNIEXPORT jint JNICALL |
| 70 | +Java_com_yangle_ashmem_NativeShm_createShm(JNIEnv *, jobject) { |
| 71 | + // 创建共享内存区域 |
| 72 | + int fd = open("/dev/ashmem", O_RDWR); |
| 73 | + if (fd < 0) { |
| 74 | + LOGE("ashmem create region failed"); |
| 75 | + return -1; |
| 76 | + } |
| 77 | + if (ioctl(fd, ASHMEM_SET_NAME, "shared_memory") != 0) { |
| 78 | + LOGE("ASHMEM_SET_NAME failed: %s", strerror(errno)); |
| 79 | + close(fd); |
| 80 | + return -1; |
| 81 | + } |
| 82 | + if (ioctl(fd, ASHMEM_SET_SIZE, sizeof(SharedBlock)) != 0) { |
| 83 | + LOGE("ASHMEM_SET_SIZE failed: %s", strerror(errno)); |
| 84 | + close(fd); |
| 85 | + return -1; |
| 86 | + } |
| 87 | +
|
| 88 | + // 映射地址空间 |
| 89 | + void *addr = mmap(nullptr, sizeof(SharedBlock), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); |
| 90 | + if (addr == MAP_FAILED) { |
| 91 | + LOGE("mmap failed"); |
| 92 | + close(fd); |
| 93 | + return -1; |
| 94 | + } |
| 95 | +
|
| 96 | + // 共享内存空间用SharedBlock结构化 |
| 97 | + g_block = reinterpret_cast<SharedBlock *>(addr); |
| 98 | + memset(g_block, 0, sizeof(SharedBlock)); |
| 99 | +
|
| 100 | + // 初始化进程间信号量 |
| 101 | + // 初始可写 |
| 102 | + sem_init(&g_block->sem_empty, 1, 1); |
| 103 | + // 初始不可读 |
| 104 | + sem_init(&g_block->sem_full, 1, 0); |
| 105 | + // 互斥锁 |
| 106 | + sem_init(&g_block->sem_mutex, 1, 1); |
| 107 | +
|
| 108 | + g_block->state = SHM_STATE_IDLE; |
| 109 | + LOGI("createShm success, fd=%d", fd); |
| 110 | + return fd; |
| 111 | +} |
| 112 | +``` |
| 113 | + |
| 114 | +## 1.3 写入数据 |
| 115 | + |
| 116 | +首先通过 sem_wait(P 操作)方法判断是否可写,以及临界区是否加锁,然后复制数据到共享内存中,通过 sem_post(V 操作)释放临界区锁,通知消费者可以读取数据。 |
| 117 | + |
| 118 | +``` |
| 119 | +/** |
| 120 | + * 写入数据 |
| 121 | + */ |
| 122 | +extern "C" |
| 123 | +JNIEXPORT jint JNICALL |
| 124 | +Java_com_yangle_ashmem_NativeShm_write(JNIEnv *env, jobject, jbyteArray data, jint len) { |
| 125 | + if (!g_block) { |
| 126 | + LOGE("write: g_block is null"); |
| 127 | + return -1; |
| 128 | + } |
| 129 | +
|
| 130 | + // P(empty),如果Consumer还没读完,上一次写会阻塞在这里 |
| 131 | + sem_wait(&g_block->sem_empty); |
| 132 | + // 进入临界区, 保护state、data_len、data的一致性 |
| 133 | + sem_wait(&g_block->sem_mutex); |
| 134 | +
|
| 135 | + jbyte *src = env->GetByteArrayElements(data, nullptr); |
| 136 | + memcpy(g_block->data, src, len); |
| 137 | + g_block->data_len = len; |
| 138 | + g_block->state = SHM_STATE_DATA; |
| 139 | + env->ReleaseByteArrayElements(data, src, 0); |
| 140 | +
|
| 141 | + // 离开临界区 |
| 142 | + sem_post(&g_block->sem_mutex); |
| 143 | + // V(full),通知 Consumer 可以读取 |
| 144 | + sem_post(&g_block->sem_full); |
| 145 | + return len; |
| 146 | +} |
| 147 | +``` |
| 148 | + |
| 149 | +## 1.4 读取数据 |
| 150 | + |
| 151 | +读取数据是在另一进程进行的,通过使用 AIDL 传递过来的 fd 可以映射与生产者进程相同的内存区域,然后再将内存区域结构化成 SharedBlock,可以拿到内存中的信号量标志和数据。 |
| 152 | + |
| 153 | +首先判断是否读取,如果生产者还在写入,会停在 sem_wait(&g_block->sem_full) 进行等待,然后判断是否传输完成,再进行数据读取,最后再释放临界区锁,通知生产者可以写入数据。 |
| 154 | + |
| 155 | +``` |
| 156 | +/** |
| 157 | + * 读取数据 |
| 158 | + */ |
| 159 | +extern "C" |
| 160 | +JNIEXPORT jint JNICALL |
| 161 | +Java_com_yangle_ashmem_NativeShm_read(JNIEnv* env, jobject, jint fd, jbyteArray out) { |
| 162 | + if (!g_block) { |
| 163 | + void *addr = mmap(nullptr, sizeof(SharedBlock), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); |
| 164 | + if (addr == MAP_FAILED) { |
| 165 | + LOGE("client mmap failed, fd=%d", fd); |
| 166 | + return -1; |
| 167 | + } |
| 168 | + g_block = reinterpret_cast<SharedBlock *>(addr); |
| 169 | + } |
| 170 | +
|
| 171 | + // P(full),等待 Producer 写入 |
| 172 | + sem_wait(&g_block->sem_full); |
| 173 | + sem_wait(&g_block->sem_mutex); |
| 174 | +
|
| 175 | + // 识别 EOF |
| 176 | + if (g_block->state == SHM_STATE_EOF) { |
| 177 | + sem_post(&g_block->sem_mutex); |
| 178 | + sem_post(&g_block->sem_empty); |
| 179 | + LOGI("receive EOF"); |
| 180 | + return -1; |
| 181 | + } |
| 182 | +
|
| 183 | + int len = g_block->data_len; |
| 184 | + env->SetByteArrayRegion(out, 0, len, reinterpret_cast<jbyte*>(g_block->data)); |
| 185 | +
|
| 186 | + // 信号量可写 |
| 187 | + sem_post(&g_block->sem_mutex); |
| 188 | + sem_post(&g_block->sem_empty); |
| 189 | + return len; |
| 190 | +} |
| 191 | +``` |
| 192 | + |
| 193 | +## 1.5 传输结束 |
| 194 | + |
| 195 | +数据传输完成后,发送结束标志,先判断是否读取完成,然后把传输状态修改为 SHM_STATE_EOF 结束,再释放临界区锁,通知消费者可以读取数据,消费者读取传输状态为结束,到此一轮数据传输完成。 |
| 196 | + |
| 197 | +``` |
| 198 | +/** |
| 199 | + * 数据传输结束 |
| 200 | + */ |
| 201 | +extern "C" |
| 202 | +JNIEXPORT void JNICALL |
| 203 | +Java_com_yangle_ashmem_NativeShm_sendEof(JNIEnv*, jobject) { |
| 204 | + if (!g_block) return; |
| 205 | + sem_wait(&g_block->sem_empty); |
| 206 | + sem_wait(&g_block->sem_mutex); |
| 207 | +
|
| 208 | + g_block->state = SHM_STATE_EOF; |
| 209 | + g_block->data_len = 0; |
| 210 | +
|
| 211 | + sem_post(&g_block->sem_mutex); |
| 212 | + sem_post(&g_block->sem_full); |
| 213 | + LOGI("send EOF"); |
| 214 | +} |
| 215 | +``` |
| 216 | + |
| 217 | +## 1.6 销毁共享内存 |
| 218 | + |
| 219 | +当不再传输数据后,也就是消费者收到 SHM_STATE_EOF 状态之后,可以通过 AIDL 通知生产者对共享内存进行销毁。 |
| 220 | + |
| 221 | +``` |
| 222 | +/** |
| 223 | + * 销毁共享内存 |
| 224 | + */ |
| 225 | +extern "C" |
| 226 | +JNIEXPORT void JNICALL |
| 227 | +Java_com_yangle_ashmem_NativeShm_destroy(JNIEnv*, jobject, jint fd) { |
| 228 | + if (g_block) { |
| 229 | + munmap(g_block, sizeof(SharedBlock)); |
| 230 | + g_block = nullptr; |
| 231 | + } |
| 232 | + close(fd); |
| 233 | + LOGI("destroy shm fd=%d", fd); |
| 234 | +} |
| 235 | +``` |
| 236 | + |
| 237 | +# 2.测试 |
| 238 | + |
| 239 | +到这里共享内存传输的基本功能就完成了,写个例子来测试下,定义一个 ShmService(android:process=":shm" ) 进程作为数据发送方,MainActivity 作为数据接收方,流程如下: |
| 240 | + |
| 241 | +- 1.MainActivity 与 ShmService 进行服务绑定,绑定成功后调用 AIDL 的 startTransfer 方法通知 ShmService 开始传输 |
| 242 | + |
| 243 | +- 2.ShmService 收到通知后开始创建共享内存,然后将 fd 通过 AIDL 的 onShmReady 回调方法传递给 MainActivity |
| 244 | + |
| 245 | +- 3.ShmService 开始发送文件,MainActivity 开始读取文件 |
| 246 | + |
| 247 | +- 4.ShmService 发送文件完成后,通过共享内存信号量通知 MainActivity 结束 |
| 248 | + |
| 249 | +- 5.MainActivity 读取到 SHM_STATE_EOF 状态后,通过 AIDL 的 endTransfer 方法通知 ShmService |
| 250 | + |
| 251 | +- 6.ShmService 收到结束通知后,销毁已创建的共享内存 |
| 252 | + |
| 253 | +**MainActivity 如下:** |
| 254 | + |
| 255 | +``` |
| 256 | +class MainActivity : AppCompatActivity() { |
| 257 | +
|
| 258 | + private val native = NativeShm() |
| 259 | + private lateinit var service: IShmService |
| 260 | +
|
| 261 | + override fun onCreate(savedInstanceState: Bundle?) { |
| 262 | + super.onCreate(savedInstanceState) |
| 263 | + setContentView(R.layout.activity_main) |
| 264 | + bindService(Intent(this, ShmService::class.java), conn, BIND_AUTO_CREATE) |
| 265 | + } |
| 266 | +
|
| 267 | + private val conn = object : ServiceConnection { |
| 268 | + override fun onServiceConnected(name: ComponentName, binder: IBinder) { |
| 269 | + service = IShmService.Stub.asInterface(binder) |
| 270 | + // 通知服务端开始发送文件 |
| 271 | + service.startTransfer(object : IShmCallback.Stub() { |
| 272 | + override fun onShmReady(pfd: ParcelFileDescriptor) { |
| 273 | + startReceive(pfd.fd) |
| 274 | + } |
| 275 | + }) |
| 276 | + } |
| 277 | +
|
| 278 | + override fun onServiceDisconnected(name: ComponentName) {} |
| 279 | + } |
| 280 | +
|
| 281 | + /** |
| 282 | + * 接收文件 |
| 283 | + * |
| 284 | + * @param fd 文件描述符 |
| 285 | + */ |
| 286 | + private fun startReceive(fd: Int) { |
| 287 | + Thread { |
| 288 | + val out = File(getExternalFilesDir(""), "test.jpg") |
| 289 | + if (out.exists()) { |
| 290 | + out.delete() |
| 291 | + } |
| 292 | + val buf = ByteArray(64 * 1024) |
| 293 | +
|
| 294 | + FileOutputStream(out).use { fos -> |
| 295 | + while (true) { |
| 296 | + val len = native.read(fd, buf) |
| 297 | + if (len < 0) { |
| 298 | + service.endTransfer() |
| 299 | + break |
| 300 | + } |
| 301 | + fos.write(buf, 0, len) |
| 302 | + } |
| 303 | + } |
| 304 | + }.start() |
| 305 | + } |
| 306 | +} |
| 307 | +``` |
| 308 | + |
| 309 | +**ShmService 如下:** |
| 310 | + |
| 311 | +``` |
| 312 | +class ShmService : Service() { |
| 313 | +
|
| 314 | + private val native = NativeShm() |
| 315 | + private var client: IShmService? = null |
| 316 | + private var mFd: Int? = null |
| 317 | +
|
| 318 | + override fun onBind(p0: Intent?): IBinder? { |
| 319 | + return object : IShmService.Stub() { |
| 320 | + override fun startTransfer(callback: IShmCallback?) { |
| 321 | + // 创建共享内存 |
| 322 | + mFd = native.createShm() |
| 323 | + // 将文件描述符回调给接收端 |
| 324 | + val pfd = ParcelFileDescriptor.fromFd(mFd!!) |
| 325 | + callback?.onShmReady(pfd) |
| 326 | + // 发送文件 |
| 327 | + sendFile("test.jpg", mFd!!) |
| 328 | + } |
| 329 | +
|
| 330 | + override fun endTransfer() { |
| 331 | + if (mFd != null) { |
| 332 | + native.destroy(mFd!!) |
| 333 | + } |
| 334 | + } |
| 335 | + }.also { |
| 336 | + client = it |
| 337 | + } |
| 338 | + } |
| 339 | +
|
| 340 | + /** |
| 341 | + * 发送文件 |
| 342 | + * |
| 343 | + * @param fileName 文件名 |
| 344 | + * @param fd 文件描述符 |
| 345 | + */ |
| 346 | + private fun sendFile(fileName: String, fd: Int) { |
| 347 | + Thread { |
| 348 | + val buffer = ByteArray(64 * 1024) |
| 349 | + try { |
| 350 | + assets.open(fileName).use { input -> |
| 351 | + while (true) { |
| 352 | + val len = input.read(buffer) |
| 353 | + if (len <= 0) break |
| 354 | + native.write(buffer, len) |
| 355 | + } |
| 356 | + } |
| 357 | + native.sendEof() |
| 358 | + } catch (e: Exception) { |
| 359 | + e.printStackTrace() |
| 360 | + } |
| 361 | + }.start() |
| 362 | + } |
| 363 | +} |
| 364 | +``` |
| 365 | + |
| 366 | +# 3.PV操作 |
| 367 | + |
| 368 | +在数据传输中使用PV信号量来控制生产者-消费者的读写操作,在这里再梳理下流程: |
| 369 | + |
| 370 | +**写数据** |
| 371 | + |
| 372 | +- 1.sem_wait(sem_empty) — 等待缓冲区可写 |
| 373 | + |
| 374 | +- 2.sem_wait(sem_mutex) — 进入临界区 |
| 375 | + |
| 376 | +- 3.memcpy 数据到共享内存,设置 data_len、state = DATA |
| 377 | + |
| 378 | +- 4.sem_post(sem_mutex) — 离开临界区 |
| 379 | + |
| 380 | +- 5.sem_post(sem_full) — 通知消费者有数据可读 |
| 381 | + |
| 382 | +**读数据** |
| 383 | + |
| 384 | +- 6.sem_wait(sem_full) — 等待数据到达 |
| 385 | + |
| 386 | +- 7.sem_wait(sem_mutex) — 进入临界区,检查 state |
| 387 | + |
| 388 | +- 8.若 state == EOF 则 sem_post(sem_mutex) 并 sem_post(sem_empty),返回 EOF,否则拷贝数据到用户缓冲 |
| 389 | + |
| 390 | +- 9.sem_post(sem_mutex) — 离开临界区 |
| 391 | + |
| 392 | +- 10.sem_post(sem_empty) — 通知写数据端可以写下一个分片 |
| 393 | + |
| 394 | +# 4.写在最后 |
| 395 | + |
| 396 | +GitHub地址:https://github.com/alidili/Demos/tree/master/AshmemDemo |
| 397 | + |
| 398 | +到这里,Android消息推送SSE方案就介绍完了,如有问题可以给我留言评论或者在GitHub中提交Issues,谢谢! |
0 commit comments