使用 CameraX 在 Jetpack Compose 中构建相机 Android 应用程序

news2024/11/25 13:48:31

使用 CameraX 在 Jetpack Compose 中构建相机 Android 应用程序

Jetpack Compose+ CameraX
CameraX 是一个 Jetpack 库,旨在帮助简化相机应用程序的开发。

[camerax官方文档] https://developer.android.com/training/camerax

CameraX的几个用例:

  • Image Capture
  • Video Capture
  • Preview
  • Image analyze
    具体如何使用相关用例,请查看上面的官方链接。
    下面仅就视频录制用例来叙述相关实现流程。

视频录制

camerax

  1. 添加camerax依赖
// CameraX
cameraxVersion = '1.2.0-beta01'
implementation "androidx.camera:camera-lifecycle:$cameraxVersion"
implementation "androidx.camera:camera-video:$cameraxVersion"
implementation "androidx.camera:camera-view:$cameraxVersion"
implementation "androidx.camera:camera-extensions:$cameraxVersion"

// Accompanist
accompanistPermissionsVersion = '0.23.1'
implementation "com.google.accompanist:accompanist-permissions:$accompanistPermissionsVersion"

在录制之前,需要请求摄像头和音频权限,代码如下:

val permissionState = rememberMultiplePermissionsState(
    permissions = listOf(
        Manifest.permission.CAMERA,
        Manifest.permission.RECORD_AUDIO
    )
)

LaunchedEffect(Unit) {
    permissionState.launchMultiplePermissionRequest()
}

PermissionsRequired(
    multiplePermissionsState = permissionState,
    permissionsNotGrantedContent = { /* ... */ },
    permissionsNotAvailableContent = { /* ... */ }
) {
  // Rest of the compose code will be here
}

创建录制对象

val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current

var recording: Recording? = remember { null }
val previewView: PreviewView = remember { PreviewView(context) }
val videoCapture: MutableState<VideoCapture<Recorder>?> = remember { mutableStateOf(null) }
val recordingStarted: MutableState<Boolean> = remember { mutableStateOf(false) }

val audioEnabled: MutableState<Boolean> = remember { mutableStateOf(false) }
val cameraSelector: MutableState<CameraSelector> = remember {
    mutableStateOf(CameraSelector.DEFAULT_BACK_CAMERA)
}

LaunchedEffect(previewView) {
    videoCapture.value = context.createVideoCaptureUseCase(
        lifecycleOwner = lifecycleOwner,
        cameraSelector = cameraSelector.value,
        previewView = previewView
    )
}

录制(Recording)是一个对象,允许我们控制当前活动的录制。它允许我们停止、暂停和恢复当前的录制。我们在开始录制时创建该对象。

PreviewView 是一个自定义视图,用于显示摄像头的视频。我们将其与生命周期绑定,将其添加到 AndroidView 中,它将显示我们当前正在录制的内容。

VideoCapture 是一个通用类,提供适用于视频应用程序的摄像头流。在这里,我们传递 Recorder 类,它是 VideoOutput 接口的实现,它允许我们开始录制。

recordingStartedaudioEnabled 是辅助变量,我们将在该屏幕上使用它们,它们的含义应该很明显。

CameraSelector 是一组用于选择摄像头或返回经过筛选的摄像头集合的要求和优先级。在这里,我们将仅使用默认的前置和后置摄像头。

LaunchedEffect 中,我们调用一个函数来创建一个视频捕获用例。该函数的示例如下:

suspend fun Context.createVideoCaptureUseCase(
    lifecycleOwner: LifecycleOwner,
    cameraSelector: CameraSelector,
    previewView: PreviewView
): VideoCapture<Recorder> {
    val preview = Preview.Builder()
        .build()
        .apply { setSurfaceProvider(previewView.surfaceProvider) }

    val qualitySelector = QualitySelector.from(
        Quality.FHD,
        FallbackStrategy.lowerQualityOrHigherThan(Quality.FHD)
    )
    val recorder = Recorder.Builder()
        .setExecutor(mainExecutor)
        .setQualitySelector(qualitySelector)
        .build()
    val videoCapture = VideoCapture.withOutput(recorder)

    val cameraProvider = getCameraProvider()
    cameraProvider.unbindAll()
    cameraProvider.bindToLifecycle(
        lifecycleOwner,
        cameraSelector,
        preview,
        videoCapture
    )

    return videoCapture
}

首先,我们创建一个 Preview,它是一个用例,用于提供用于在屏幕上显示的摄像头预览流。我们可以在这里设置多个参数,如纵横比、捕获处理器、图像信息处理器等。由于我们不需要这些参数,所以创建一个普通的 Preview 对象。

接下来是选择视频的质量。为此,我们使用QualitySelector定义所需的质量设置。我们希望使用全高清(Full HD)质量,因此我们将传递 Quality.FHD。某些手机可能没有所需的质量设置,因此您应该始终有备选方案,就像我们在这里通过传递 FallbackStrategy 一样。有几种策略可供选择:

  • higherQualityOrLowerThan — 选择最接近并高于输入质量的质量。如果无法得到支持的质量设置,则选择最接近并低于输入质量的质量。
  • higherQualityThan — 选择最接近并高于输入质量的质量。
  • lowerQualityOrHigherThan — 选择最接近并低于输入质量的质量。如果无法得到支持的质量设置,则选择最接近并高于输入质量的质量。
  • lowerQualityThan — 选择最接近并低于输入质量的质量。
    另一种方法是只传递Quality.LOWEST Quality.HIGHEST,这可能是更简单的方式,但我也想展示这种方式。

现在,我们创建一个 Recorder 并使用它通过调用 VideoCapture.withOutput(recorder) 来获取 VideoCapture 对象。

相机提供程序是 ProcessCameraProvider 单例的对象,它允许我们将相机的生命周期绑定到应用程序进程中的任何 LifecycleOwner。我们使用的用于获取相机提供程序的函数是:

suspend fun Context.getCameraProvider(): ProcessCameraProvider = suspendCoroutine { continuation ->
    ProcessCameraProvider.getInstance(this).also { future ->
        future.addListener(
            {
                continuation.resume(future.get())
            },
            mainExecutor
        )
    }
}

ProcessCameraProvider.getInstance(this) 返回一个 Future,我们需要等待它完成以获取实例。

接下来,我们需要将所有内容绑定到生命周期,并传递 lifecycleOwnercameraSelectorpreview videoCapture

现在是时候完成其余的 Compose 代码了,希望您还在我身边!

PermissionsRequired内容块中,我们添加AndroidView和用于录制的按钮。代码如下:

AndroidView(
    factory = { previewView },
    modifier = Modifier.fillMaxSize()
)
IconButton(
    onClick = {
        if (!recordingStarted.value) {
            videoCapture.value?.let { videoCapture ->
                recordingStarted.value = true
                val mediaDir = context.externalCacheDirs.firstOrNull()?.let {
                    File(it, context.getString(R.string.app_name)).apply { mkdirs() }
                }

                recording = startRecordingVideo(
                    context = context,
                    filenameFormat = "yyyy-MM-dd-HH-mm-ss-SSS",
                    videoCapture = videoCapture,
                    outputDirectory = if (mediaDir != null && mediaDir.exists()) mediaDir else context.filesDir,
                    executor = context.mainExecutor,
                    audioEnabled = audioEnabled.value
                ) { event ->
                    // Process events that we get while recording
                }
            }
        } else {
            recordingStarted.value = false
            recording?.stop()
        }
    },
    modifier = Modifier
        .align(Alignment.BottomCenter)
        .padding(bottom = 32.dp)
) {
    Icon(
        painter = painterResource(if (recordingStarted.value) R.drawable.ic_stop else R.drawable.ic_record),
        contentDescription = "",
        modifier = Modifier.size(64.dp)
    )
}

AndroidView 将显示我们的预览。

至于按钮,我们将用它来启动和停止录制。当我们想要开始录制时,首先获取媒体目录,如果目录不存在,我们将创建它。接下来调用 startRecordingVideo 函数,函数的代码如下:

fun startRecordingVideo(
    context: Context,
    filenameFormat: String,
    videoCapture: VideoCapture<Recorder>,
    outputDirectory: File,
    executor: Executor,
    audioEnabled: Boolean,
    consumer: Consumer<VideoRecordEvent>
): Recording {
    val videoFile = File(
        outputDirectory,
        SimpleDateFormat(filenameFormat, Locale.US).format(System.currentTimeMillis()) + ".mp4"
    )

    val outputOptions = FileOutputOptions.Builder(videoFile).build()

    return videoCapture.output
        .prepareRecording(context, outputOptions)
        .apply { if (audioEnabled) withAudioEnabled() }
        .start(executor, consumer)
}

这是一个简单的函数,它创建一个文件,准备录制并开始录制。如果启用了音频,我们还将启用音频录制。该函数返回的对象将用于停止录制。consumer参数是一个回调,在每个事件发生时都会被调用。您可以使用它在视频录制完成后获取文件的 URI

让我们为音频和相机选择器添加逻辑。

if (!recordingStarted.value) {
    IconButton(
        onClick = {
            audioEnabled.value = !audioEnabled.value
        },
        modifier = Modifier
            .align(Alignment.BottomStart)
            .padding(bottom = 32.dp)
    ) {
        Icon(
            painter = painterResource(if (audioEnabled.value) R.drawable.ic_mic_on else R.drawable.ic_mic_off),
            contentDescription = "",
            modifier = Modifier.size(64.dp)
        )
    }
}
if (!recordingStarted.value) {
    IconButton(
        onClick = {
            cameraSelector.value =
                if (cameraSelector.value == CameraSelector.DEFAULT_BACK_CAMERA) CameraSelector.DEFAULT_FRONT_CAMERA
                else CameraSelector.DEFAULT_BACK_CAMERA
            lifecycleOwner.lifecycleScope.launch {
                videoCapture.value = context.createVideoCaptureUseCase(
                    lifecycleOwner = lifecycleOwner,
                    cameraSelector = cameraSelector.value,
                    previewView = previewView
                )
            }
        },
        modifier = Modifier
            .align(Alignment.BottomEnd)
            .padding(bottom = 32.dp)
    ) {
        Icon(
            painter = painterResource(R.drawable.ic_switch_camera),
            contentDescription = "",
            modifier = Modifier.size(64.dp)
        )
    }
}

这两个按钮将启用或禁用音频,并在前置和后置摄像头之间进行切换。当我们切换摄像头时,我们需要创建一个新的 VideoCapture 对象来改变预览显示的内容。
camerax

这就是该屏幕的全部内容,但是现在我们希望能够查看我们录制的内容,对吗?当然了,为此,我们将创建另一个屏幕并使用ExoPlayer来显示视频。

首先,让我们在 consumer 回调函数中添加逻辑:

if (event is VideoRecordEvent.Finalize) {
    val uri = event.outputResults.outputUri
    if (uri != Uri.EMPTY) {
        val uriEncoded = URLEncoder.encode(
            uri.toString(),
            StandardCharsets.UTF_8.toString()
        )
        navController.navigate("${Route.VIDEO_PREVIEW}/$uriEncoded")
    }
}

如果事件是 VideoRecordEvent.Finalize,这意味着录制已经完成,我们可以获取视频的 URI。有几个视频录制事件可以使用,你可以选择任何一个,但在这里我们只需要 Finalize

  • Start
  • Finalize
  • Status
  • Pause
  • Resume

如果视频太短,例如不到半秒钟,URI 可能为空,这就是我们需要那个 if 语句的原因。
为了将 URI 作为导航参数传递,它应该被编码。

这个屏幕的最终代码如下:

@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun VideoCaptureScreen(
    navController: NavController
) {
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current

    val permissionState = rememberMultiplePermissionsState(
        permissions = listOf(
            Manifest.permission.CAMERA,
            Manifest.permission.RECORD_AUDIO
        )
    )

    var recording: Recording? = remember { null }
    val previewView: PreviewView = remember { PreviewView(context) }
    val videoCapture: MutableState<VideoCapture<Recorder>?> = remember { mutableStateOf(null) }
    val recordingStarted: MutableState<Boolean> = remember { mutableStateOf(false) }

    val audioEnabled: MutableState<Boolean> = remember { mutableStateOf(false) }
    val cameraSelector: MutableState<CameraSelector> = remember {
        mutableStateOf(CameraSelector.DEFAULT_BACK_CAMERA)
    }

    LaunchedEffect(Unit) {
        permissionState.launchMultiplePermissionRequest()
    }

    LaunchedEffect(previewView) {
        videoCapture.value = context.createVideoCaptureUseCase(
            lifecycleOwner = lifecycleOwner,
            cameraSelector = cameraSelector.value,
            previewView = previewView
        )
    }
    PermissionsRequired(
        multiplePermissionsState = permissionState,
        permissionsNotGrantedContent = { /* ... */ },
        permissionsNotAvailableContent = { /* ... */ }
    ) {
        Box(
            modifier = Modifier.fillMaxSize()
        ) {
            AndroidView(
                factory = { previewView },
                modifier = Modifier.fillMaxSize()
            )
            IconButton(
                onClick = {
                    if (!recordingStarted.value) {
                        videoCapture.value?.let { videoCapture ->
                            recordingStarted.value = true
                            val mediaDir = context.externalCacheDirs.firstOrNull()?.let {
                                File(it, context.getString(R.string.app_name)).apply { mkdirs() }
                            }

                            recording = startRecordingVideo(
                                context = context,
                                filenameFormat = "yyyy-MM-dd-HH-mm-ss-SSS",
                                videoCapture = videoCapture,
                                outputDirectory = if (mediaDir != null && mediaDir.exists()) mediaDir else context.filesDir,
                                executor = context.mainExecutor,
                                audioEnabled = audioEnabled.value
                            ) { event ->
                                if (event is VideoRecordEvent.Finalize) {
                                    val uri = event.outputResults.outputUri
                                    if (uri != Uri.EMPTY) {
                                        val uriEncoded = URLEncoder.encode(
                                            uri.toString(),
                                            StandardCharsets.UTF_8.toString()
                                        )
                                        navController.navigate("${Route.VIDEO_PREVIEW}/$uriEncoded")
                                    }
                                }
                            }
                        }
                    } else {
                        recordingStarted.value = false
                        recording?.stop()
                    }
                },
                modifier = Modifier
                    .align(Alignment.BottomCenter)
                    .padding(bottom = 32.dp)
            ) {
                Icon(
                    painter = painterResource(if (recordingStarted.value) R.drawable.ic_stop else R.drawable.ic_record),
                    contentDescription = "",
                    modifier = Modifier.size(64.dp)
                )
            }
            if (!recordingStarted.value) {
                IconButton(
                    onClick = {
                        audioEnabled.value = !audioEnabled.value
                    },
                    modifier = Modifier
                        .align(Alignment.BottomStart)
                        .padding(bottom = 32.dp)
                ) {
                    Icon(
                        painter = painterResource(if (audioEnabled.value) R.drawable.ic_mic_on else R.drawable.ic_mic_off),
                        contentDescription = "",
                        modifier = Modifier.size(64.dp)
                    )
                }
            }
            if (!recordingStarted.value) {
                IconButton(
                    onClick = {
                        cameraSelector.value =
                            if (cameraSelector.value == CameraSelector.DEFAULT_BACK_CAMERA) CameraSelector.DEFAULT_FRONT_CAMERA
                            else CameraSelector.DEFAULT_BACK_CAMERA
                        lifecycleOwner.lifecycleScope.launch {
                            videoCapture.value = context.createVideoCaptureUseCase(
                                lifecycleOwner = lifecycleOwner,
                                cameraSelector = cameraSelector.value,
                                previewView = previewView
                            )
                        }
                    },
                    modifier = Modifier
                        .align(Alignment.BottomEnd)
                        .padding(bottom = 32.dp)
                ) {
                    Icon(
                        painter = painterResource(R.drawable.ic_switch_camera),
                        contentDescription = "",
                        modifier = Modifier.size(64.dp)
                    )
                }
            }
        }
    }
}

ExoPlayer

camerax

ExoPlayer 是 Android 的 MediaPlayer API 的替代品,可用于播放本地和互联网上的音频和视频。它更易于使用,并提供更多功能。此外,它易于定制和扩展。

现在我们了解了 ExoPlayer,让我们创建下一个屏幕。添加依赖项:

//ExoPlayer Library
exoPlayerVersion = '2.18.1'
implementation "com.google.android.exoplayer:exoplayer:$exoPlayerVersion"

我们的代码像下面这样:

@Composable
fun VideoPreviewScreen(
    uri: String
) {
    val context = LocalContext.current

    val exoPlayer = remember(context) {
        ExoPlayer.Builder(context).build().apply {
            setMediaItem(MediaItem.fromUri(uri))
            prepare()
        }
    }

    DisposableEffect(
        Box(
            modifier = Modifier.fillMaxSize()
        ) {
            AndroidView(
                factory = { context ->
                    StyledPlayerView(context).apply {
                        player = exoPlayer
                    }
                },
                modifier = Modifier.fillMaxSize()
            )
        }
    ) {
        onDispose {
            exoPlayer.release()
        }
    }
}

我们将使用构建器来创建 ExoPlayer,设置要加载的视频的 URI,然后准备播放器。

我们使用 AndroidView 来显示视频,并将 StyledPlayerView 附加到它上面。

StyledPlayerView 是用于播放器媒体播放的高级视图。它在播放期间显示视频、字幕和专辑封面,并使用 StyledPlayerControlView 显示播放控件。
StyledPlayerView 可以通过设置属性(或调用相应的方法)或覆盖绘图进行自定义。
camerax

源码地址

https://github.com/Giga99/CameraApp

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/559588.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【多线程】什么是线程死锁?形成条件是什么?如何避免?

文章目录 一、什么是线程死锁二、线程死锁三、形成死锁的四个必要条件是什么四、如何避免线程死锁 一、什么是线程死锁 死锁是指两个或两个以上的进程&#xff08;线程&#xff09;在执行过程中&#xff0c;由于竞争资源或者由于彼此通信而造成的一种阻塞的现象&#xff0c;若…

Unity 天空盒

在 Unity 中&#xff0c;天空盒是使用天空盒着色器的一种材质。 创建天空盒材质 1.从菜单栏中&#xff0c;单击 Assets > Create > Material。 2.在 Shader 下拉选单中&#xff0c;单击 Skybox&#xff0c;然后单击要使用的天空盒着色器。 有Skybox/6 Sided、Skybox/…

人民大学与加拿大女王金融硕士项目——在现在憧憬美好的未来

未来是一个虚无缥缈的词汇&#xff0c;抓不住也看不到。未来里有着我们无限的希望&#xff0c;也有着美好的憧憬。未来究竟是怎样的呢&#xff0c;有人说现在的样子里藏着未来的模样。在职的你有没有为未来编织一副美丽的画卷呢&#xff1f;未来很远&#xff0c;远到只能靠想象…

MySQL小记——约束、多表查询

目录 约束 常见约束 主键约束 非空约束 唯一约束 自增长约束 非负约束 外键约束之一对多 外键约束之多对多 多表查询 内连接 外连接 左外连接 右外连接 子查询 自查询 case when语句 约束 在MySQL中&#xff0c;约束是对字段规则的一种限制。 常见约束 1.主…

Linux安装并使用seatunnel2.3.1

SeaTunnel是一个非常易用的超高性能分布式数据集成平台&#xff0c;支持海量数据的实时同步。 下载安装包 设置版本 export version"2.3.1" 通过命令下载 wget "https://archive.apache.org/dist/incubator/seatunnel/${version}/apache-seatunnel-incubat…

Linux/Ubuntu系统运行Python+Yolov5物体识别

程序示例精选 Linux/Ubuntu系统运行PythonYolov5物体识别 如需安装运行环境或远程调试&#xff0c;见文章底部个人QQ名片&#xff0c;由专业技术人员远程协助&#xff01; 前言 这篇博客针对<<Linux/Ubuntu系统运行PythonYolov5物体识别>>编写代码&#xff0c;代码…

汽车最强大脑ECU和单片机是什么关系

摘要&#xff1a; 有效解决线路信息传递所带来的复杂化问题 ECU的定义 ECU原来指的是engine control unit&#xff0c;即发动机控制单元&#xff0c;特指电喷发动机的电子控制系统。但是随着汽车电子的迅速发展&#xff0c;ECU的定义也发生了巨大的变化&#xff0c;变成了elec…

31岁才转行程序员,目前34了,我来说说我的经历和一些感受吧...

最近刷知乎&#xff0c;发现有很多朋友有年龄焦虑了&#xff0c;比如&#xff1a;“我今年28了转行来不来得及”&#xff0c;“我今年30了还能转软件测试吗&#xff1f;”......这种问题在知乎上有很多&#xff0c;仿佛大家都觉得年纪大了&#xff0c;很多事情都来不及了&#…

tps和qps的区别和理解

QPS&#xff08;TPS&#xff09; 并发数/平均响应时间 或者 并发数 QPS*平均响应时间 TPS Transactions Per Second&#xff08;每秒传输的事物处理个数&#xff09;&#xff0c;即服务器每秒处理的事务数。TPS包括一条消息入和一条消息出&#xff0c;加上一次用户数据库访…

html使用elementui案例

<!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><title>Title</title><!--引入 element-ui 的样式&#xff0c;--><link rel"stylesheet" href"static/css/index.css">…

轻松将Win10系统备份到U盘的2种方法!

问题&#xff1a;我能将Win10系统备份到U盘吗&#xff1f; ​“我想将Win10系统备份到U盘&#xff0c;然后通过增量或差异备份定期备份。我使用了系统自带的工具进行备份&#xff0c;但它无法识别这个U盘。有没有好用的方法可以轻松的将电脑系统备份到u盘/移动硬盘&#xf…

Ubuntu系统下Nginx安装

一、使用apt安装nginx 0-如果本机安装了nginx&#xff0c;就进行卸载&#xff1a; apt-get --purge autoremove nginx 检查本机是否还有nginx程序在后台运行&#xff0c;如果有直接kill掉。 ps -ef | grep nginx 1-默认版本安装 apt-get update apt-get install nginx 2…

【备战秋招】每日一题:3月18日美团春招第四题:题面+题目思路 + C++/python/js/Go/java带注释

2023大厂笔试模拟练习网站&#xff08;含题解&#xff09; www.codefun2000.com 最近我们一直在将收集到的各种大厂笔试的解题思路还原成题目并制作数据&#xff0c;挂载到我们的OJ上&#xff0c;供大家学习交流&#xff0c;体会笔试难度。现已录入200道互联网大厂模拟练习题&…

spring Security 认证失败,用户名和密码是正确的还是失败

项目用登录输入正确的用户名和密码为什么还是告知,用户名和密码是不正确? 有这几种情况 第一种是不是开启缓存,数据库中存储的是加密后的密码 第二种,查看源代码,这句是关键,presentedPassword是明文密码,userDetails.getPassword()是加密后的密码,进行比较 this.pa…

做好个人黄金投资,学习黄金投资交易原则

随着经济的发展,黄金逐渐成为金融投资的重要工具&#xff0c;越来越多的人开始关注黄金投资。想要做好个人黄金投资&#xff0c;建议先熟悉和学习黄金投资交易原则的内容。 黄金投资交易原则一、跟随趋势入场 在买入之前&#xff0c;首先应对行情的运行趋势有个明确的判断。一…

全网最全postman接口测试教程和项目实战~从入门到精通!!!

Postman实现接口测试内容大纲一览&#xff1a; 一、什么是接口&#xff1f;为什么需要接口&#xff1f; 接口指的是实体或者软件提供给外界的一种服务。 因为接口能使我们的实体或者软件的内部数据能够被外部进行修改。从而使得内部和外部实现数据交互。所以需要接口。 比如&…

支撑企业未来10年高增长,用友资金管理平台助力新零售企业逆境破局

随着大数据、云服务、5G等技术的深化发展&#xff0c;人们消费观念及需求逐渐变化&#xff0c;我国新零售业数字化进程不断加快。近年来&#xff0c;新零售已经不仅局限于单一的零售范畴&#xff0c;逐步演变为集零售、快递物流、金融科技、企业服务、人工智能等各行各业为一体…

公司只有1个测试,领导却让我测试10个项目,这不是为难我....

读者提问&#xff1a;公司只有 1个测试&#xff0c;领导让我同时测试 10个项目&#xff0c;我该怎么办&#xff1f;回答&#xff1a;如果我是那个测试&#xff0c;我会做这三件事 1、向上申请资源2、任务分配到人3、执行测试任务 一、向上申请资源 1个测试同时对接 10个项目&a…

华为云认证的含金量高吗?数通考什么内容?

对于从事信息通信行业的人来说&#xff0c;拥有一份专业的证书&#xff0c;对提升自己的技能、职业竞争力是非常有帮助的&#xff0c;而华为云虽然是一个新兴的云计算厂商&#xff0c;但是咋断电几年之内&#xff0c;已经发展到了国内第二的位置&#xff0c;而且还在不断地发展…

java序列化和文件的输入和输出

文章目录 一、JAVA的序列化1. 简介2. 对象序列化的步骤3. 小结4. 序列化4. 解序列化 二、对象的序列化1. 简介2. Java.io.File.class3. 缓冲区 三、序列化版本控制 一、JAVA的序列化 1. 简介 如何将我们的java对象存储起来&#xff0c;这里介绍两种思路&#xff1a; 如果我们…