Android基于共享内存实现跨进程大文件传输

转载请注明出处:juejin.cn/spost/75872...

本文出自 容华谢后的博客

0.写在前面

在实际的项目开发中,为了更好的性能和结构,我们经常会把一个项目应用分成多个进程或者多个组件,当不同进程的业务之间需要通信时,通常会选择AIDL、Bundle或者广播的方式进行通信,这些通信手段有一个共同的特点,就是只支持简单、小容量、不频繁的数据传输,当我们的应用存在这样的业务场景,比如视频流跨进程传输、大文件跨进程传输等需要复杂数据、频繁数据交换的功能时,就不能使用上述的通信手段了。

为了解决这个问题,可以选择共享内存的方式进行通信,在设备的内存中开辟一个固定容量的空间,使用生产者-消费者模型,生产者在内存中写入数据,通过PV操作通知消费者进行读取,消费者读取完成后再通知生产者进行写入。

1.实现

1.1 数据结构

首先定义下共享内存中的数据结构,首先定义3个信号量:

  • sem_empty: 定义是否允许写,生产者写前 P(empty),消费者读后 V(empty)

  • sem_full: 定义是否允许读,消费者读前 P(full),生产者写后 V(full)

  • sem_mutex: 定义读写互斥锁,保护 state/data_len/data 的互斥

接下来定义数据传输的状态,分别是 IDLE(空闲状态)、DATA(传输数据状态)、EOF(文件传输结束状态),然后定义每次传输数据的有效长度 data_len 和数据块 data属性。

arduino 复制代码
/**
 * 数据传输结构
 */
struct SharedBlock {
    // 是否允许写(Producer)
    sem_t sem_empty;
    // 是否允许读(Consumer)
    sem_t sem_full;
    // 读写互斥锁
    sem_t sem_mutex;
    // 当前状态
    uint32_t state;
    // data 中有效数据长度
    uint32_t data_len;
    // 数据
    uint8_t data[SHM_DATA_SIZE];
};

/**
 * 共享内存状态机
 *
 * IDLE : 空闲状态
 * DATA : 有有效数据
 * EOF  : 文件传输结束
 */
enum ShmState : uint32_t {
    SHM_STATE_IDLE = 0,
    SHM_STATE_DATA = 1,
    SHM_STATE_EOF = 2,
};

1.2 创建共享内存

通过 open("/dev/ashmem", O_RDWR) 方法创建共享内存,得到文件描述符,fd 可通过 AIDL 传递给其他进程,在其他进程通过 fd 映射相同的内存区域进行操作。创建完成后通过 ioctl 方法设置共享内存的名称和大小,再使用 mmap 对内存的地址空间进行映射,然后通过 reinterpret_cast 对内存空间进行结构化。

接下来进行信号量初始化,初始状态为可写、不可读、可进入临界区状态,数据传输状态初始为空闲状态,到这里共享内存就创建完成了,继续往下看共享内存的数据是如何进行读写的。

scss 复制代码
/**
 * 创建共享内存
 */
extern "C"
JNIEXPORT jint JNICALL
Java_com_yangle_ashmem_NativeShm_createShm(JNIEnv *, jobject) {
    // 创建共享内存区域
    int fd = open("/dev/ashmem", O_RDWR);
    if (fd < 0) {
        LOGE("ashmem create region failed");
        return -1;
    }
    if (ioctl(fd, ASHMEM_SET_NAME, "shared_memory") != 0) {
        LOGE("ASHMEM_SET_NAME failed: %s", strerror(errno));
        close(fd);
        return -1;
    }
    if (ioctl(fd, ASHMEM_SET_SIZE, sizeof(SharedBlock)) != 0) {
        LOGE("ASHMEM_SET_SIZE failed: %s", strerror(errno));
        close(fd);
        return -1;
    }

    // 映射地址空间
    void *addr = mmap(nullptr, sizeof(SharedBlock), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (addr == MAP_FAILED) {
        LOGE("mmap failed");
        close(fd);
        return -1;
    }

    // 共享内存空间用SharedBlock结构化
    g_block = reinterpret_cast<SharedBlock *>(addr);
    memset(g_block, 0, sizeof(SharedBlock));

    // 初始化进程间信号量
    // 初始可写
    sem_init(&g_block->sem_empty, 1, 1);
    // 初始不可读
    sem_init(&g_block->sem_full, 1, 0);
    // 互斥锁
    sem_init(&g_block->sem_mutex, 1, 1);

    g_block->state = SHM_STATE_IDLE;
    LOGI("createShm success, fd=%d", fd);
    return fd;
}

1.3 写入数据

首先通过 sem_wait(P 操作)方法判断是否可写,以及临界区是否加锁,然后复制数据到共享内存中,通过 sem_post(V 操作)释放临界区锁,通知消费者可以读取数据。

scss 复制代码
/**
 * 写入数据
 */
extern "C"
JNIEXPORT jint JNICALL
Java_com_yangle_ashmem_NativeShm_write(JNIEnv *env, jobject, jbyteArray data, jint len) {
    if (!g_block) {
        LOGE("write: g_block is null");
        return -1;
    }

    // P(empty),如果Consumer还没读完,上一次写会阻塞在这里
    sem_wait(&g_block->sem_empty);
    // 进入临界区, 保护state、data_len、data的一致性
    sem_wait(&g_block->sem_mutex);

    jbyte *src = env->GetByteArrayElements(data, nullptr);
    memcpy(g_block->data, src, len);
    g_block->data_len = len;
    g_block->state = SHM_STATE_DATA;
    env->ReleaseByteArrayElements(data, src, 0);

    // 离开临界区
    sem_post(&g_block->sem_mutex);
    // V(full),通知 Consumer 可以读取
    sem_post(&g_block->sem_full);
    return len;
}

1.4 读取数据

读取数据是在另一进程进行的,通过使用 AIDL 传递过来的 fd 可以映射与生产者进程相同的内存区域,然后再将内存区域结构化成 SharedBlock,可以拿到内存中的信号量标志和数据。

首先判断是否读取,如果生产者还在写入,会停在 sem_wait(&g_block->sem_full) 进行等待,然后判断是否传输完成,再进行数据读取,最后再释放临界区锁,通知生产者可以写入数据。

scss 复制代码
/**
 * 读取数据
 */
extern "C"
JNIEXPORT jint JNICALL
Java_com_yangle_ashmem_NativeShm_read(JNIEnv* env, jobject, jint fd, jbyteArray out) {
    if (!g_block) {
        void *addr = mmap(nullptr, sizeof(SharedBlock), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
        if (addr == MAP_FAILED) {
            LOGE("client mmap failed, fd=%d", fd);
            return -1;
        }
        g_block = reinterpret_cast<SharedBlock *>(addr);
    }

    // P(full),等待 Producer 写入
    sem_wait(&g_block->sem_full);
    sem_wait(&g_block->sem_mutex);

    // 识别 EOF
    if (g_block->state == SHM_STATE_EOF) {
        sem_post(&g_block->sem_mutex);
        sem_post(&g_block->sem_empty);
        LOGI("receive EOF");
        return -1;
    }

    int len = g_block->data_len;
    env->SetByteArrayRegion(out, 0, len, reinterpret_cast<jbyte*>(g_block->data));

    // 信号量可写
    sem_post(&g_block->sem_mutex);
    sem_post(&g_block->sem_empty);
    return len;
}

1.5 传输结束

数据传输完成后,发送结束标志,先判断是否读取完成,然后把传输状态修改为 SHM_STATE_EOF 结束,再释放临界区锁,通知消费者可以读取数据,消费者读取传输状态为结束,到此一轮数据传输完成。

scss 复制代码
/**
 * 数据传输结束
 */
extern "C"
JNIEXPORT void JNICALL
Java_com_yangle_ashmem_NativeShm_sendEof(JNIEnv*, jobject) {
    if (!g_block) return;
    sem_wait(&g_block->sem_empty);
    sem_wait(&g_block->sem_mutex);

    g_block->state = SHM_STATE_EOF;
    g_block->data_len = 0;

    sem_post(&g_block->sem_mutex);
    sem_post(&g_block->sem_full);
    LOGI("send EOF");
}

1.6 销毁共享内存

当不再传输数据后,也就是消费者收到 SHM_STATE_EOF 状态之后,可以通过 AIDL 通知生产者对共享内存进行销毁。

scss 复制代码
/**
 * 销毁共享内存
 */
extern "C"
JNIEXPORT void JNICALL
Java_com_yangle_ashmem_NativeShm_destroy(JNIEnv*, jobject, jint fd) {
    if (g_block) {
        munmap(g_block, sizeof(SharedBlock));
        g_block = nullptr;
    }
    close(fd);
    LOGI("destroy shm fd=%d", fd);
}

2.测试

到这里共享内存传输的基本功能就完成了,写个例子来测试下,定义一个 ShmService(android:process=":shm" ) 进程作为数据发送方,MainActivity 作为数据接收方,先看下项目结构:

流程如下:

  • 1.MainActivity 与 ShmService 进行服务绑定,绑定成功后调用 AIDL 的 startTransfer 方法通知 ShmService 开始传输

  • 2.ShmService 收到通知后开始创建共享内存,然后将 fd 通过 AIDL 的 onShmReady 回调方法传递给 MainActivity

  • 3.ShmService 开始发送文件,MainActivity 开始读取文件

  • 4.ShmService 发送文件完成后,通过共享内存信号量通知 MainActivity 结束

  • 5.MainActivity 读取到 SHM_STATE_EOF 状态后,通过 AIDL 的 endTransfer 方法通知 ShmService

  • 6.ShmService 收到结束通知后,销毁已创建的共享内存

MainActivity 如下:

kotlin 复制代码
class MainActivity : AppCompatActivity() {

    private val native = NativeShm()
    private lateinit var service: IShmService

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        bindService(Intent(this, ShmService::class.java), conn, BIND_AUTO_CREATE)
    }

    private val conn = object : ServiceConnection {
        override fun onServiceConnected(name: ComponentName, binder: IBinder) {
            service = IShmService.Stub.asInterface(binder)
            // 通知服务端开始发送文件
            service.startTransfer(object : IShmCallback.Stub() {
                override fun onShmReady(pfd: ParcelFileDescriptor) {
                    startReceive(pfd.fd)
                }
            })
        }

        override fun onServiceDisconnected(name: ComponentName) {}
    }

    /**
     * 接收文件
     *
     * @param fd 文件描述符
     */
    private fun startReceive(fd: Int) {
        Thread {
            val out = File(getExternalFilesDir(""), "test.jpg")
            if (out.exists()) {
                out.delete()
            }
            val buf = ByteArray(64 * 1024)

            FileOutputStream(out).use { fos ->
                while (true) {
                    val len = native.read(fd, buf)
                    if (len < 0) {
                        service.endTransfer()
                        break
                    }
                    fos.write(buf, 0, len)
                }
            }
        }.start()
    }
}

ShmService 如下:

kotlin 复制代码
class ShmService : Service() {

    private val native = NativeShm()
    private var client: IShmService? = null
    private var mFd: Int? = null

    override fun onBind(p0: Intent?): IBinder? {
        return object : IShmService.Stub() {
            override fun startTransfer(callback: IShmCallback?) {
                // 创建共享内存
                mFd = native.createShm()
                // 将文件描述符回调给接收端
                val pfd = ParcelFileDescriptor.fromFd(mFd!!)
                callback?.onShmReady(pfd)
                // 发送文件
                sendFile("test.jpg", mFd!!)
            }

            override fun endTransfer() {
                if (mFd != null) {
                    native.destroy(mFd!!)
                }
            }
        }.also {
            client = it
        }
    }

    /**
     * 发送文件
     *
     * @param fileName 文件名
     * @param fd       文件描述符
     */
    private fun sendFile(fileName: String, fd: Int) {
        Thread {
            val buffer = ByteArray(64 * 1024)
            try {
                assets.open(fileName).use { input ->
                    while (true) {
                        val len = input.read(buffer)
                        if (len <= 0) break
                        native.write(buffer, len)
                    }
                }
                native.sendEof()
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }.start()
    }
}

AIDL 如下:

csharp 复制代码
interface IShmService {
    /**
     * 开始传输
     *
     * @params callback 回调
     */
    void startTransfer(in IShmCallback callback);

    /**
     * 结束传输
     */
    void endTransfer();
}

interface IShmCallback {
   /**
     * 共享内存初始化完成
     *
     * @params pfd 文件描述符
     */
    void onShmReady(in ParcelFileDescriptor pfd);
}

注意在 onShmReady 方法中需要传递 ParcelFileDescriptor 类型,不能直接传递 int 类型的 fd,ParcelFileDescriptor 内部会自动实现 Binder 的自动映射。

  • Binder 会把 fd 复制到目标进程,目标进程拿到的 ParcelFileDescriptor 对象里有一个新的 fd。

  • 系统会在底层做 fd 映射和引用计数,保证两个进程都可以安全访问同一个底层资源。

  • 它不仅能封装 ashmem fd,也能封装普通文件、socket、pipe 等。

3.PV操作

在数据传输中使用PV信号量来控制生产者-消费者的读写操作,在这里再梳理下流程:

写数据

  • 1.sem_wait(sem_empty) --- 等待缓冲区可写

  • 2.sem_wait(sem_mutex) --- 进入临界区

  • 3.memcpy 数据到共享内存,设置 data_len、state = DATA

  • 4.sem_post(sem_mutex) --- 离开临界区

  • 5.sem_post(sem_full) --- 通知消费者有数据可读

读数据

  • 6.sem_wait(sem_full) --- 等待数据到达

  • 7.sem_wait(sem_mutex) --- 进入临界区,检查 state

  • 8.若 state == EOF 则 sem_post(sem_mutex) 并 sem_post(sem_empty),返回 EOF,否则拷贝数据到用户缓冲

  • 9.sem_post(sem_mutex) --- 离开临界区

  • 10.sem_post(sem_empty) --- 通知写数据端可以写下一个分片

4.写在最后

GitHub地址:github.com/alidili/Dem...

到这里,Android消息推送SSE方案就介绍完了,如有问题可以给我留言评论或者在GitHub中提交Issues,谢谢!

相关推荐
Kapaseker2 小时前
面试官最爱问的 Android 数据传递问题
android·kotlin
TheNextByte13 小时前
如何轻松地将音乐从Mac传输到Android ?
android·stm32·macos
似霰3 小时前
HIDL Hal 开发笔记1----Android 8 HAL 变迁
android·framework·hal
SinFeeLoo_星凤楼3 小时前
Android Studio 中gradle.properties 中的中文注释显示乱码,如何解决?
android·ide·gradle·android studio·.properties
-suiyuan-3 小时前
sqli-labs靶场7笔记
android·笔记
zhlx28353 小时前
免费开源跨平台听歌自由!自定义音乐源 + 桌面歌词!LX Music 落雪音乐开源版
android·windows·macos
程序员码歌12 小时前
短思考第263天,每天复盘10分钟,胜过盲目努力一整年
android·前端·后端
安卓兼职framework应用工程师12 小时前
Android 10.0 按键智能机按键连续响两次的异常处理
android·audio·audioservice·按键音·按键声音
studyForMokey12 小时前
【Android 项目】个人学习demo随笔
android