Skip to content

Commit be036c4

Browse files
committed
增加 README.md
1 parent d10ddd3 commit be036c4

3 files changed

Lines changed: 400 additions & 3 deletions

File tree

‎AshmemDemo/README.md‎

Lines changed: 398 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,398 @@
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

Comments
 (0)