├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── camera_client ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── example │ │ └── multi │ │ └── camera │ │ └── client │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── aidl │ │ └── com │ │ │ └── example │ │ │ └── multi │ │ │ └── camera │ │ │ └── service │ │ │ └── ICameraService.aidl │ ├── java │ │ └── com │ │ │ └── example │ │ │ └── multi │ │ │ └── camera │ │ │ └── client │ │ │ ├── ImageUtil.java │ │ │ └── MainActivity.java │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── example │ └── multi │ └── camera │ └── client │ └── ExampleUnitTest.java ├── camera_server ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── example │ │ └── multi │ │ └── camera │ │ └── service │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── aidl │ │ └── com │ │ │ └── example │ │ │ └── multi │ │ │ └── camera │ │ │ └── service │ │ │ └── ICameraService.aidl │ ├── java │ │ └── com │ │ │ └── example │ │ │ └── multi │ │ │ └── camera │ │ │ └── service │ │ │ ├── CameraService.java │ │ │ └── MainActivity.java │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── example │ └── multi │ └── camera │ └── service │ └── ExampleUnitTest.java ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── iamges ├── buffer_queue.png ├── linux_pipe.jpg ├── multi_camera_demo.jpg ├── shared_memory_draft.jpg └── shared_memory_revised.jpg └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | local.properties 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Xin Xiao 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 前言 2 | 提到 Android 进程间的通信方式,即使是 Android 客户端开发初学者,也能列举出来几种,无外乎: 3 | 1. bundle 4 | 2. 文件共享 5 | 3. AIDL(Binder) 6 | 4. Messenger 7 | 5. ContentProvider 8 | 6. Socket 9 | 10 | 然而都2022年了,本文如果只是介绍下以上的几种进程间通信的方式,就没什么意义了,也太对不起观众了,同时以上几种方式,也不能满足题目的需求:大数据,高效的跨进程传输。 11 | 有些同学可能会提到另外一种方式:共享内存(MemoryFile & SharedMemory),这种方式的确是可以满足题目的需求,不过共享内存的使用不是很简单,没有进程间同步机制,这个需要使用者自行处理,这样就加大了该方式的使用难度,下文会详细说明下用共享内存进行通信的难点。 12 | # 1 使用场景 13 | 在介绍这种通信方式之前,先看下为什么需要进行跨进程的大数据的高效传输,有哪些场景需要进行跨进程的数据传输。 14 | 对于大部分的 app 开发同学,一般应用都是单进程的模式,并不需要进行跨进程的数据通信,即使有多进程的场景,一般数据量不会特别大,也不是持续性的,频繁性的。 15 | 那么在 Android 系统中,哪些数据是大量的,需要跨进程传递的,对 Android 图像系统比较了解的同学会想到屏幕上渲染的数据,对多媒体比较了解的同学会想到音视频数据,这些数据都有类似的特点:大数据量(高分辨率),持续性(高采样率,高刷新率)。 16 | 此处以一路30帧的720p的 camera NV21数据为例,1秒钟的数据量为:1280 * 720 * 3 / 2 * 30 = 41472000Byte = 39.5MB,对于这个数量级的数据,一次内存 copy 对系统资源都是很大的损耗,这也证明了为什么前面介绍的方式1,2,4,5,6的通信方式不适合大数据的传输,一种原因就是因为他们需要进行多次的内存 copy 操作,效率较低。方式3:AIDL(Binder)虽然只有一次 copy 操作,但是 Binder 对单次通信数据量有大小限制(默认< 1Mb),同时由于很多其他通信操作是共享Binder内存的,如果Binder通信过于频繁,是会拖慢应用的响应时间。 17 | 而方式7:共享内存理论是可以满足这个需求,不过我们来看下,如果要基于共享内存来实现数据的传输需要完成哪些事情。 18 | # 2 解决方案 19 | 假设目前有两个进程:进程A,进程B,进程A和进程B之间已经建立好了一块共享内存,两个进程都可以对该内存区域进行访问。目前进程A需要向进程B持续的传输大量数据,那么需要哪些步骤呢? 20 | ## 2.1 设计思路 21 | ![shared_memory_draft](https://github.com/TheOne-Xin/camera-ipc-sample/blob/master/iamges/shared_memory_draft.jpg) 22 | 23 | 1. step 1:进程A向共享内存写入一段数据。 24 | 2. step 2:进程B读取这段数据。 25 | 3. 进程A重复 step 1:再向共享内存写入一段数据。 26 | 27 | 以上1-2-1-2循环,这样就可以了吗?当然不会这么简单,这里面有一些同步的问题: 28 | 1. 进程A写入后,如何通知进程B读取。 29 | 2. 在进程B没有取走之前,进程A如果有新数据生成,怎么办? 30 | 3. 进程B取走数据后,如何通知进程A继续写入? 31 | 32 | 对于问题1,2,进程A需要完成写入后触发 step3 通知进程B可以读取,进程B完成读取后,触发 step4 通知进程A可以继续写入。 33 | ![shared_memory_revised](https://github.com/TheOne-Xin/camera-ipc-sample/blob/master/iamges/shared_memory_revised.jpg) 34 | 35 | 如果解决以上问题后,我们会发现其实已经实现了一个基础的生产者消费者模型。(对于问题2,又可以扩展出缓存,ping-pong buffer,3-buffer等等)。 36 | 而实现以上这套模型的成本应该说还是很高的,但是理论上完全可行,不过为了让事情更简单,是否有更简单的方法呢,是否有这样一个组件,在进程A和进程B直接建立一个管道,进程A只管写,写不下了,阻塞或者返回出错,有空间可以继续写了,通知进程A继续进行写,进程B只管读,读不到,阻塞或者返回出错,有数据了,通知进程B继续读,由这个管道处理同步通知这些事情,linux下有提供 pipe 这种通信方式,不过pipe需要多次内存 copy,也不适合大数据的传输,且 Android 系统并没有在应用层暴露这个 pipe 的接口。 37 | ![linux_pipe](https://github.com/TheOne-Xin/camera-ipc-sample/blob/master/iamges/linux_pipe.jpg) 38 | 39 | 对于 Android 系统,渲染,音视频等模块比较了解的同学应该会想到 Android 系统里面的 BufferQueue,那么先了解下 BufferQueue。 40 | ## 2.2 BufferQueue 41 | 对业务开发来说,无法接触到 BufferQueue,甚至不知道 BufferQueue 是什么东西。对系统来说,BufferQueue 是很重要的传递数据的组件,Android 显示系统依赖于 BufferQueue,只要显示内容到“屏幕”(此处指抽象的屏幕,有时候还可以包含编码器),就一定需要用到 BufferQueue,可以说在显示/播放器相关的领域中,BufferQueue 无处不在。即使直接调用 Opengl ES 来绘制,底层依然需要 BufferQueue 才能显示到屏幕上。 42 | BufferQueue 是 Android 显示系统的核心,它的设计思想是生产者-消费者模型,只要往 BufferQueue 中填充数据,则认为是生产者,只要从 BufferQueue 中获取数据,则认为是消费者。有时候同一个类,在不同的场景下既可能是生产者也有可能是消费者。如 SurfaceFlinger,在合成并显示 UI 内容时,UI 元素作为生产者生产内容,SurfaceFlinger 作为消费者消费这些内容。而在截屏时,SurfaceFlinger 又作为生产者将当前合成显示的 UI 内容填充到另一个 BufferQueue,截屏应用此时作为消费者从 BufferQueue 中获取数据并生产截图。 43 | ![buffer_queue](https://github.com/TheOne-Xin/camera-ipc-sample/blob/master/iamges/buffer_queue.png) 44 | 45 | 同时使用 BufferQueue 的生产者和消费者往往处在不同的进程,BufferQueue 内部使用共享内存和 Binder 在不同的进程传递数据,减少数据拷贝提高效率。 46 | “同时使用 BufferQueue 的生产者和消费者往往处在不同的进程,BufferQueue 内部使用共享内存和 Binder 在不同的进程传递数据,减少数据拷贝提高效率。” 47 | 通过这段可以明确 BufferQueue 是可以进行跨进程间通信的,而且 Android 显示系统是用 BufferQueue 来做数据传递,那么 BufferQueue 是一定可以用来做大数据的传输,而且性能应该是很高的,否则 Android 系统的显示也会卡顿,可以看出 BufferQueue 是 Android 系统中比较重要的组件。 48 | 那么我们是不是用 BufferQueue 就可以进行应用间的通信了呢,抱歉,BufferQueue 这个组件在 Android 应用层是没有暴露出来的,App 是无法使用的。(至少目前我还没有找到相关接口) 49 | 在实际应用中,除了直接使用 BuferQueue 外,更多的是使用 Surface/SurfaceTexture,其对 BufferQueue 做了包装,方便业务使用 BufferQueue。Surface 作为 BufferQueue 的生产者,SurfaceTexture 作为 BufferQueue 的消费者。 50 | 此处提到 Surface,那么 Android 应用是否可以使用它呢,抱歉,查看了下 Surface 暴露的接口,也未发现有可用的接口来实现进程间的通信。 51 | 难道 BuferQueue 这么好用的组件应用层就只能眼睁睁看着用不上吗?同时对 Android 系统设计也感觉有些奇怪,为什么这种可用于大数据传递的组件不对应用层暴露,可能是对于大部分App的业务来说,分多个进程,进程间又有这么大数据量交互的场景不多,所以没有暴露出相关的接口来。之前通过大量的搜索和文档阅读,接口类代码查阅,并没有发现应用层如何使用 BuferQueue 的介绍,网络上也没有人讨论过类似的方案。 52 | 那么是不是 BuferQueue 我们就完全用不了呢,如果用不了,那么可能就没有这篇文章了!!!! 53 | **柳暗花明**:在研究 Camera Api2 相关接口时,一个类 ImageReader 引起了注意,这个类是基于 Surface 的封装,用于获取 Camera 的数据。既然有 reader 是不是有 writer 呢,不出所料 ImageWriter 也是存在的,这两个类都是在 Android 6.0(API level 23)加入的。 54 | ## 2.3 ImageReader & ImageWriter 55 | 下面我们看下如何基于 ImageReader、ImageWriter 实现一个消费者生产者模型,首先生产者和消费者处于两个进程: 56 | 消费者进程:ImageReader 57 | 58 | ```java 59 | // step 1: 创建一个ImageReader 60 | ImageReader imageReader = ImageReader.newInstance(width, height,ImageFormat.YUV_420_888, 2); 61 | // step 2: 设置ImageReader回调 62 | imageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() { 63 | @Override 64 | public void onImageAvailable(ImageReader imageReader) { 65 | Image image = imageReader.acquireNextImage(); 66 | Image.Plane[] planes = image.getPlanes(); 67 | for (int i = 0; i < planes.length; i++) { 68 | ByteBuffer byteBuffer = planes[i].getBuffer(); 69 | byte[] bytes = new byte[byteBuffer.capacity()]; 70 | byteBuffer.get(bytes); 71 | } 72 | image.close(); 73 | } 74 | }, mCameraHandler); 75 | ``` 76 | 生产者进程:ImagerWriter 77 | 78 | ```java 79 | // step 1 : 获得ImageWriter对象 80 | ImageWriter imageWriter = ?; 81 | // step 2 : ImageWriter.dequeueInputImage、ImageWriter.queueInputImage写入需要传递的data数据 82 | Image image = imageWriter.dequeueInputImage(); 83 | Image.Plane[] planes = image.getPlanes(); 84 | for (int i = 0; i < planes.length; i++) { 85 | ByteBuffer byteBuffer = planes[i].getBuffer(); 86 | byteBuffer.put(data, 0, data.length); 87 | } 88 | imageWriter.queueInputImage(image); 89 | ``` 90 | 通过以上示例代码,我们是不是可以实现一个生产者和消费者模型呢,当然不能,细心的同学应该会发现生产者示例中 step 1的 ImageWriter 不知道是如何来的? 91 | 这里面有一个重要的环节:ImageReader 和 ImageWriter 是如何关联起来的? 92 | 先看下 ImageWriter 类源码,看看如何创建一个 ImageWriter 对象: 93 | 94 | ```java 95 | public class ImageWriter implements AutoCloseable { 96 | ImageWriter() { 97 | throw new RuntimeException("Stub!"); 98 | } 99 | 100 | @NonNull 101 | public static ImageWriter newInstance(@NonNull Surface surface, int maxImages) { 102 | throw new RuntimeException("Stub!"); 103 | } 104 | 105 | @NonNull 106 | public static ImageWriter newInstance(@NonNull Surface surface, int maxImages, int format) { 107 | throw new RuntimeException("Stub!"); 108 | } 109 | } 110 | ``` 111 | 我们看到如果要创建 ImageWriter 一定需要 Surface 这个参数,回头在看下 ImageReader 类源码,如果仔细的浏览过源码的话,会发现 ImageReader 有一个 getSurface() 接口,那么是不是把 ImageReader 的 surface 传递给 ImageWriter 就可以建立关联呢,答案是肯定的,那么剩下的工作就是把 ImageReader 的 surface 从消费者进程传递到生产者进程里就好了,这里通过 AIDL 进行传递就可以了([Android AIDL 使用教程](https://blog.csdn.net/hello_1995/article/details/122094512))。 112 | 完成以上步骤,生成者进程就可以向 ImageWriter 中写入数据,消费者进程就可以通过 ImageReader 的回调收到这个数据了,通过实测这种方式传输 Camera NV21 数据,资源消耗非常低,可以满足 **大数据**,**高效** 的要求,同时实现又比较简单,不到100行代码就可以完成整个通信流程。 113 | # 3 示例代码 114 | 按照以上介绍的方式,相信大家都可以实现一个高效的跨进程的消费者生产者模型。我基于该方式实现了一个多路 Camera 分发的 demo,供参考:[camera-ipc-sample](https://github.com/TheOne-Xin/camera-ipc-sample)。 115 | 该工程包含两个App:MultiCameraService、MultiCameraClient。 116 | 安装这两个 apk,手动给 MultiCameraService App 授予 Camera 访问权限,然后打开 MultiCameraClient App,点击预览开关按钮,正常情况下即可实现 Camera 预览。 117 | 有些同学会说这有什么啊,不就是 Camera 预览功能,注意这里面是在 MultiCameraService app 中打开的 Camera,而在 MultiCameraClient app 看到预览画面,Camera 的数据是通过跨进程的方式,从 MultiCameraService App 传递到 MultiCameraClient App 中的。如图: 118 | ![multi_camera_demo](https://github.com/TheOne-Xin/camera-ipc-sample/blob/master/iamges/multi_camera_demo.jpg) 119 | 120 | # 4 总结 121 | 以上即是如何在 Android 实现跨进程大数据的高效传输,虽然该方案对于纯粹的手机 App 开发同学不一定有很大的帮助,但是目前有很多智能设备采用了 Android 系统,对 Camera,图形渲染都有很多不同于手机 App 的需求,在没有很好的跨进程传输方案的情况,有些项目只能把很多业务功能杂糅在一个 App 进程中,使模块承载的业务功能不是很清晰,有了这种方案,就可以更加优化项目模型架构的设计。 122 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | dependencies { 8 | classpath "com.android.tools.build:gradle:3.6.4" 9 | 10 | // NOTE: Do not place your application dependencies here; they belong 11 | // in the individual module build.gradle files 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | google() 18 | jcenter() 19 | 20 | } 21 | } 22 | 23 | task clean(type: Delete) { 24 | delete rootProject.buildDir 25 | } -------------------------------------------------------------------------------- /camera_client/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /camera_client/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 30 5 | buildToolsVersion "30.0.3" 6 | 7 | defaultConfig { 8 | applicationId "com.example.multi.camera.client" 9 | minSdkVersion 21 10 | targetSdkVersion 30 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | 15 | buildTypes { 16 | release { 17 | minifyEnabled false 18 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 19 | } 20 | } 21 | compileOptions { 22 | sourceCompatibility JavaVersion.VERSION_1_8 23 | targetCompatibility JavaVersion.VERSION_1_8 24 | } 25 | } 26 | 27 | dependencies { 28 | 29 | implementation 'androidx.appcompat:appcompat:1.3.1' 30 | } -------------------------------------------------------------------------------- /camera_client/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /camera_client/src/androidTest/java/com/example/multi/camera/client/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.example.multi.camera.client; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.test.platform.app.InstrumentationRegistry; 6 | import androidx.test.ext.junit.runners.AndroidJUnit4; 7 | 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | 11 | import static org.junit.Assert.*; 12 | 13 | /** 14 | * Instrumented test, which will execute on an Android device. 15 | * 16 | * @see Testing documentation 17 | */ 18 | @RunWith(AndroidJUnit4.class) 19 | public class ExampleInstrumentedTest { 20 | @Test 21 | public void useAppContext() { 22 | // Context of the app under test. 23 | Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); 24 | assertEquals("com.example.multi.camera.client", appContext.getPackageName()); 25 | } 26 | } -------------------------------------------------------------------------------- /camera_client/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /camera_client/src/main/aidl/com/example/multi/camera/service/ICameraService.aidl: -------------------------------------------------------------------------------- 1 | // ICameraService.aidl 2 | package com.example.multi.camera.service; 3 | 4 | // Declare any non-default types here with import statements 5 | 6 | interface ICameraService { 7 | /** 8 | * The client sends the surface object to the server. 9 | */ 10 | void onSurfaceShared(in Surface surface); 11 | } -------------------------------------------------------------------------------- /camera_client/src/main/java/com/example/multi/camera/client/ImageUtil.java: -------------------------------------------------------------------------------- 1 | package com.example.multi.camera.client; 2 | 3 | import android.graphics.ImageFormat; 4 | import android.media.Image; 5 | import android.os.Build; 6 | import android.util.Log; 7 | 8 | import androidx.annotation.RequiresApi; 9 | 10 | import java.nio.ByteBuffer; 11 | 12 | /** 13 | * Created by Xin Xiao on 2022/8/17 14 | */ 15 | class ImageUtil { 16 | public static final int YUV420P = 0; 17 | public static final int YUV420SP = 1; 18 | public static final int NV21 = 2; 19 | private static final String TAG = "ImageUtil"; 20 | 21 | /*** 22 | * 此方法内注释以640*480为例 23 | * 未考虑CropRect的 24 | */ 25 | @RequiresApi(api = Build.VERSION_CODES.KITKAT) 26 | public static byte[] getBytesFromImageAsType(Image image, int type) { 27 | try { 28 | //获取源数据,如果是YUV格式的数据planes.length = 3 29 | //plane[i]里面的实际数据可能存在byte[].length <= capacity (缓冲区总大小) 30 | final Image.Plane[] planes = image.getPlanes(); 31 | 32 | //数据有效宽度,一般的,图片width <= rowStride,这也是导致byte[].length <= capacity的原因 33 | // 所以我们只取width部分 34 | int width = image.getWidth(); 35 | int height = image.getHeight(); 36 | 37 | //此处用来装填最终的YUV数据,需要1.5倍的图片大小,因为Y U V 比例为 4:1:1 38 | byte[] yuvBytes = new byte[width * height * ImageFormat.getBitsPerPixel(ImageFormat.YUV_420_888) / 8]; 39 | //目标数组的装填到的位置 40 | int dstIndex = 0; 41 | 42 | //临时存储uv数据的 43 | byte uBytes[] = new byte[width * height / 4]; 44 | byte vBytes[] = new byte[width * height / 4]; 45 | int uIndex = 0; 46 | int vIndex = 0; 47 | 48 | int pixelsStride, rowStride; 49 | for (int i = 0; i < planes.length; i++) { 50 | pixelsStride = planes[i].getPixelStride(); 51 | rowStride = planes[i].getRowStride(); 52 | 53 | ByteBuffer buffer = planes[i].getBuffer(); 54 | 55 | //如果pixelsStride==2,一般的Y的buffer长度=640*480,UV的长度=640*480/2-1 56 | //源数据的索引,y的数据是byte中连续的,u的数据是v向左移以为生成的,两者都是偶数位为有效数据 57 | byte[] bytes = new byte[buffer.capacity()]; 58 | buffer.get(bytes); 59 | 60 | int srcIndex = 0; 61 | if (i == 0) { 62 | //直接取出来所有Y的有效区域,也可以存储成一个临时的bytes,到下一步再copy 63 | for (int j = 0; j < height; j++) { 64 | System.arraycopy(bytes, srcIndex, yuvBytes, dstIndex, width); 65 | srcIndex += rowStride; 66 | dstIndex += width; 67 | } 68 | } else if (i == 1) { 69 | //根据pixelsStride取相应的数据 70 | for (int j = 0; j < height / 2; j++) { 71 | for (int k = 0; k < width / 2; k++) { 72 | uBytes[uIndex++] = bytes[srcIndex]; 73 | srcIndex += pixelsStride; 74 | } 75 | if (pixelsStride == 2) { 76 | srcIndex += rowStride - width; 77 | } else if (pixelsStride == 1) { 78 | srcIndex += rowStride - width / 2; 79 | } 80 | } 81 | } else if (i == 2) { 82 | //根据pixelsStride取相应的数据 83 | for (int j = 0; j < height / 2; j++) { 84 | for (int k = 0; k < width / 2; k++) { 85 | vBytes[vIndex++] = bytes[srcIndex]; 86 | srcIndex += pixelsStride; 87 | } 88 | if (pixelsStride == 2) { 89 | srcIndex += rowStride - width; 90 | } else if (pixelsStride == 1) { 91 | srcIndex += rowStride - width / 2; 92 | } 93 | } 94 | } 95 | } 96 | 97 | //根据要求的结果类型进行填充 98 | switch (type) { 99 | case YUV420P: 100 | System.arraycopy(uBytes, 0, yuvBytes, dstIndex, uBytes.length); 101 | System.arraycopy(vBytes, 0, yuvBytes, dstIndex + uBytes.length, vBytes.length); 102 | break; 103 | case YUV420SP: 104 | for (int i = 0; i < vBytes.length; i++) { 105 | yuvBytes[dstIndex++] = uBytes[i]; 106 | yuvBytes[dstIndex++] = vBytes[i]; 107 | } 108 | break; 109 | case NV21: 110 | for (int i = 0; i < vBytes.length; i++) { 111 | yuvBytes[dstIndex++] = vBytes[i]; 112 | yuvBytes[dstIndex++] = uBytes[i]; 113 | } 114 | break; 115 | } 116 | return yuvBytes; 117 | } catch (final Exception e) { 118 | if (image != null) { 119 | image.close(); 120 | } 121 | Log.i(TAG, e.toString()); 122 | } 123 | return null; 124 | } 125 | 126 | /*** 127 | * YUV420 转化成 RGB 128 | */ 129 | public static int[] decodeYUV420SP(byte[] yuv420sp, int width, int height) { 130 | final int frameSize = width * height; 131 | int rgb[] = new int[frameSize]; 132 | for (int j = 0, yp = 0; j < height; j++) { 133 | int uvp = frameSize + (j >> 1) * width, u = 0, v = 0; 134 | for (int i = 0; i < width; i++, yp++) { 135 | int y = (0xff & ((int) yuv420sp[yp])) - 16; 136 | if (y < 0) 137 | y = 0; 138 | if ((i & 1) == 0) { 139 | v = (0xff & yuv420sp[uvp++]) - 128; 140 | u = (0xff & yuv420sp[uvp++]) - 128; 141 | } 142 | int y1192 = 1192 * y; 143 | int r = (y1192 + 1634 * v); 144 | int g = (y1192 - 833 * v - 400 * u); 145 | int b = (y1192 + 2066 * u); 146 | if (r < 0) 147 | r = 0; 148 | else if (r > 262143) 149 | r = 262143; 150 | if (g < 0) 151 | g = 0; 152 | else if (g > 262143) 153 | g = 262143; 154 | if (b < 0) 155 | b = 0; 156 | else if (b > 262143) 157 | b = 262143; 158 | rgb[yp] = 0xff000000 | ((r << 6) & 0xff0000) 159 | | ((g >> 2) & 0xff00) | ((b >> 10) & 0xff); 160 | } 161 | } 162 | return rgb; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /camera_client/src/main/java/com/example/multi/camera/client/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.example.multi.camera.client; 2 | 3 | import android.content.ComponentName; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.content.ServiceConnection; 7 | import android.graphics.Bitmap; 8 | import android.graphics.ImageFormat; 9 | import android.graphics.Matrix; 10 | import android.media.Image; 11 | import android.media.ImageReader; 12 | import android.os.Bundle; 13 | import android.os.Handler; 14 | import android.os.HandlerThread; 15 | import android.os.IBinder; 16 | import android.util.Log; 17 | import android.view.Surface; 18 | import android.view.View; 19 | import android.widget.ImageView; 20 | import android.widget.TextView; 21 | 22 | import androidx.appcompat.app.AppCompatActivity; 23 | 24 | import com.example.multi.camera.service.ICameraService; 25 | 26 | public class MainActivity extends AppCompatActivity { 27 | private final String TAG = "MultiCameraClient"; 28 | private ICameraService iCameraService; 29 | 30 | //views 31 | private ImageView mPreview; 32 | private TextView mPreviewIsOff; 33 | 34 | //handler 35 | private HandlerThread mCameraThread; 36 | private Handler mCameraHandler; 37 | 38 | // 当前获取的帧数 39 | private int currentIndex = 0; 40 | // 处理的间隔帧,可根据自己情况修改 41 | private static final int PROCESS_INTERVAL = 2; 42 | 43 | @Override 44 | protected void onCreate(Bundle savedInstanceState) { 45 | super.onCreate(savedInstanceState); 46 | setContentView(R.layout.activity_main); 47 | 48 | mPreview = findViewById(R.id.preview); 49 | mPreview.setVisibility(View.INVISIBLE); 50 | mPreviewIsOff = findViewById(R.id.preview_is_off); 51 | mPreviewIsOff.setVisibility(View.VISIBLE); 52 | 53 | findViewById(R.id.start_preview).setOnClickListener(new View.OnClickListener() { 54 | @Override 55 | public void onClick(View view) { 56 | //连接服务 57 | Intent intent = new Intent(); 58 | intent.setAction("com.example.camera.aidl"); 59 | intent.setPackage("com.example.multi.camera.service"); 60 | bindService(intent, mConnection, Context.BIND_AUTO_CREATE); 61 | Log.d(TAG, "bindService"); 62 | //更新视图 63 | mPreview.setVisibility(View.VISIBLE); 64 | mPreviewIsOff.setVisibility(View.INVISIBLE); 65 | } 66 | }); 67 | findViewById(R.id.stop_preview).setOnClickListener(new View.OnClickListener() { 68 | @Override 69 | public void onClick(View view) { 70 | //断开服务 71 | unbindService(mConnection); 72 | iCameraService = null; 73 | Log.d(TAG, "unbindService"); 74 | 75 | //更新视图 76 | mPreview.setVisibility(View.INVISIBLE); 77 | mPreviewIsOff.setVisibility(View.VISIBLE); 78 | } 79 | }); 80 | 81 | mCameraThread = new HandlerThread("CameraClientThread"); 82 | mCameraThread.start(); 83 | mCameraHandler = new Handler(mCameraThread.getLooper()); 84 | } 85 | 86 | ServiceConnection mConnection = new ServiceConnection() { 87 | 88 | @Override 89 | public void onServiceDisconnected(ComponentName name) { 90 | Log.d(TAG, "onServiceDisconnected"); 91 | iCameraService = null; 92 | } 93 | 94 | @Override 95 | public void onServiceConnected(ComponentName name, IBinder service) { 96 | Log.d(TAG, "onServiceConnected"); 97 | iCameraService = ICameraService.Stub.asInterface(service); 98 | startCamera(mPreview.getWidth(), mPreview.getHeight()); 99 | Log.d(TAG, "mPreview width:" + mPreview.getWidth() + ";mPreview height:" + mPreview.getHeight()); 100 | } 101 | }; 102 | 103 | //开始预览,处理预览数据 104 | private void startCamera(int width, int height) { 105 | //手机摄像头的图像数据来源于摄像头硬件的图像传感器,这个图像传感器被固定到手机上后默认的取景方向是手机横放时的方向.所以竖屏时宽高需要调整 106 | ImageReader imageReader = ImageReader.newInstance(height, width, 107 | ImageFormat.YUV_420_888, 2); 108 | imageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() { 109 | @Override 110 | public void onImageAvailable(ImageReader imageReader) { 111 | Log.d(TAG, "onImageAvailable"); 112 | 113 | Image image = imageReader.acquireNextImage(); 114 | 115 | int imageWidth = image.getWidth(); 116 | int imageHeight = image.getHeight(); 117 | Log.d(TAG, "imageWidth:" + imageWidth + ";imageHeight:" + imageHeight); 118 | 119 | byte[] data68 = ImageUtil.getBytesFromImageAsType(image, 2); 120 | 121 | if (currentIndex++ % PROCESS_INTERVAL == 0) { 122 | int rgb[] = ImageUtil.decodeYUV420SP(data68, imageWidth, imageHeight); 123 | Bitmap originalBitmap = Bitmap.createBitmap(rgb, 0, imageWidth, 124 | imageWidth, imageHeight, 125 | android.graphics.Bitmap.Config.ARGB_8888); 126 | Matrix matrix = new Matrix(); 127 | // //手机摄像头的图像数据来源于摄像头硬件的图像传感器,这个图像传感器被固定到手机上后默认的取景方向是手机横放时的方向.所以竖屏时需要做旋转处理 128 | matrix.postRotate(90); 129 | final Bitmap previewBitmap = Bitmap.createBitmap(originalBitmap, 0, 0, originalBitmap.getWidth(), originalBitmap.getHeight(), matrix, false); 130 | runOnUiThread(new Runnable() { 131 | @Override 132 | public void run() { 133 | mPreview.setImageBitmap(previewBitmap); 134 | } 135 | }); 136 | originalBitmap.recycle(); 137 | } 138 | image.close(); 139 | } 140 | }, mCameraHandler); 141 | try { 142 | Surface surface = imageReader.getSurface(); 143 | iCameraService.onSurfaceShared(surface); 144 | Log.d(TAG, "share the surface."); 145 | } catch (Exception e) { 146 | e.printStackTrace(); 147 | } 148 | } 149 | 150 | @Override 151 | protected void onDestroy() { 152 | if (iCameraService != null) { 153 | unbindService(mConnection); 154 | Log.d(TAG, "unbindService"); 155 | } 156 | super.onDestroy(); 157 | } 158 | } -------------------------------------------------------------------------------- /camera_client/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /camera_client/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /camera_client/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 11 | 12 | 19 | 20 |