CameraX 简单使用 Android官方提供的相关指南

Google AndroidCamera2的基础上再开发了CameraX,用于解决以往Camera/Camera2配置繁杂的问题,力求做到开发者的开箱即用体验,本篇在Android Codelab的基础上,加入了少数注释,力求代码的清晰易懂

Android官方提供的相关指南

准备工作

创建一个新项目

  • 使用 Android Studio 菜单,新建项目并在收到系统提示时选择Empty Activity(空 Activity
    ddc817a03892e44_1920.png
  • 下一步,将应用命名为CameraX App。确保将语言设置为Kotlin、将最低API级别设为21(对于CameraX,这是所需的最低级别),确保AndroidX组件相关设置打开

2383e490b6550aed_1920.png

添加Gradle依赖项

  • 打开 build.gradle(Module: app) 文件并将CameraX依赖项添加到应用Gradle文件中的dependencies部分内:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
kotlin复制代码// CameraX core library using the camera2 implementation
def camerax_version = "1.3.0-beta01"
// The following line is optional, as the core library is included indirectly by camera-camera2
implementation("androidx.camera:camera-core:${camerax_version}")
implementation("androidx.camera:camera-camera2:${camerax_version}")
// If you want to additionally use the CameraX Lifecycle library
implementation("androidx.camera:camera-lifecycle:${camerax_version}")
// If you want to additionally use the CameraX VideoCapture library
implementation("androidx.camera:camera-video:${camerax_version}")
// If you want to additionally use the CameraX View class
implementation("androidx.camera:camera-view:${camerax_version}")
// If you want to additionally add CameraX ML Kit Vision Integration
implementation("androidx.camera:camera-mlkit-vision:${camerax_version}")
// If you want to additionally use the CameraX Extensions library
implementation("androidx.camera:camera-extensions:${camerax_version}")
  • CameraX需要用到Java 8中的一些方法,因此我们需要对编译选项进行相应设置。 在 android 块末尾,紧跟 buildTypes 的位置添加以下内容:
1
2
3
4
kotlin复制代码compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
}
  • 在弹出的消息中选择Sync Now,把在build gradle中配置的更改进行同步
  • 配置成功的build.gradle应该如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
kotlin复制代码plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
}

android {
namespace 'com.mobilescanner.cameraxdemo'
compileSdk 33

defaultConfig {
applicationId "com.mobilescanner.cameraxdemo"
minSdk 24
targetSdk 33
versionCode 1
versionName "1.0"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}

dependencies {

// CameraX core library using the camera2 implementation
def camerax_version = "1.3.0-beta01"
// The following line is optional, as the core library is included indirectly by camera-camera2
implementation("androidx.camera:camera-core:${camerax_version}")
implementation("androidx.camera:camera-camera2:${camerax_version}")
// If you want to additionally use the CameraX Lifecycle library
implementation("androidx.camera:camera-lifecycle:${camerax_version}")
// If you want to additionally use the CameraX VideoCapture library
implementation("androidx.camera:camera-video:${camerax_version}")
// If you want to additionally use the CameraX View class
implementation("androidx.camera:camera-view:${camerax_version}")
// If you want to additionally add CameraX ML Kit Vision Integration
implementation("androidx.camera:camera-mlkit-vision:${camerax_version}")
// If you want to additionally use the CameraX Extensions library
implementation("androidx.camera:camera-extensions:${camerax_version}")

implementation 'androidx.core:core-ktx:1.10.1'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.9.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'android.arch.lifecycle:livedata:1.1.1'
implementation 'android.arch.lifecycle:viewmodel:1.1.1'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}

AndroidManifest配置

  • 添加用于确保设备配备有相机的 android.hardware.camera.any。指定 .any,用以表示相机可以是前置摄像头或后置摄像头

如果您使用不带 .anyandroid.hardware.camera,则在您使用没有后置摄像头的设备(例如,大多数 Chromebook)的情况下,此类将无法工作。在第二行中添加对于该相机的访问权限。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
xml复制代码<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- Declare features -->
<uses-feature android:name="android.hardware.camera.any" />

<!-- Declare permissions -->
<uses-permission android:name="android.permission.CAMERA" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.CameraXDemo"
tools:targetApi="31">
<!--下文创建MainActivity时会自动生成 -->
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>

注意

  • Codelab编写教程时采用了kotlin-android-extensions,但如今其已经被弃用,故文中需要使用控件处都使用findViewById代替,在githubAndroid团队发布的用例中,已经采用了更轻量级的ViewBinding
  • 如果Sync Now过慢,考虑把源更改为阿里源,在目前该文章编写版本的Android Studio(2022.2.1)中,仓库配置源的设置已更改至settings.gradle,配置成功文件如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
kotlin复制代码pluginManagement {
repositories {
maven { url 'https://maven.aliyun.com/repository/public' }
maven { url 'https://maven.aliyun.com/repository/gradle-plugin' }
maven { url 'https://maven.aliyun.com/repository/central' }
maven { url 'https://maven.aliyun.com/repository/google' }
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {

repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
maven { url 'https://maven.aliyun.com/repository/public' }
maven { url 'https://maven.aliyun.com/repository/gradle-plugin' }
maven { url 'https://maven.aliyun.com/repository/central' }
maven { url 'https://maven.aliyun.com/repository/google' }
google()
mavenCentral()
}
}
rootProject.name = "CameraXDemo"
include ':app'

项目实现

创建取景器布局

  • 创建一个MainActivity,并在layout/activity_main.xml布局文件中,填入以下代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
xml复制代码<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
   xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:tools="http://schemas.android.com/tools"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">

   <Button
       android:id="@+id/camera_capture_button"
       android:layout_width="100dp"
       android:layout_height="100dp"
       android:layout_marginBottom="50dp"
       android:scaleType="fitCenter"
       android:text="Take Photo"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       app:layout_constraintBottom_toBottomOf="parent"
       android:elevation="2dp" />

   <androidx.camera.view.PreviewView
       android:id="@+id/viewFinder"
       android:layout_width="match_parent"
       android:layout_height="match_parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

MainActivity的基本实现

  • 定义所需常量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
kotlin复制代码package com.example.cameraxdemo
import android.Manifest
import android.content.pm.PackageManager
import android.icu.text.SimpleDateFormat
import android.net.Uri
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.widget.Button
import android.widget.Toast
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import java.io.File
import java.util.Locale
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors

class MainActivity : AppCompatActivity() {
/**
* TAG:后续编写中需要使用Log.e的Tag
* FILENAME_FORMAT:该示例中保存图片文件的文件名为时间戳,该变量为时间戳定义形式
* REQUEST_CODE_PERMISSIONS:申请相机使用权限时的申请码
* REQUIRED_PERMISSIONS:需要申请使用的权限
*/
companion object {
private const val TAG = "CameraXBasic"
private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
private const val REQUEST_CODE_PERMISSIONS = 10
private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)
/**
* 注意在此处Manifest.permission.CAMERA可能会出现找不到的情况
* 查看你使用的是否为Android包的Manifest而不是你本项目里的Manifest(删了在Android Studio的提示下重打)
*/
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
  • 申请权限
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
kotlin复制代码...
class MainActivity : AppCompatActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Request camera permissions
if (allPermissionsGranted()) {
startCamera()
} else {
ActivityCompat.requestPermissions(
this,
REQUIRED_PERMISSIONS,
REQUEST_CODE_PERMISSIONS
)
}
}
private fun startCamera(){ }
private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
ContextCompat.checkSelfPermission(
baseContext, it) == PackageManager.PERMISSION_GRANTED
}
override fun onRequestPermissionsResult(
requestCode: Int, permissions: Array<String>, grantResults:IntArray) {
//检查请求代码是否正确;如果此代码不正确,则将其忽略。
if (requestCode == REQUEST_CODE_PERMISSIONS) {
//如果权限已经被授予,调用相机
if (allPermissionsGranted()) {
startCamera()
} else {
//权限未授予,告诉用户权限授权不成功
Toast.makeText(
this,
"Permissions not granted by the user.",
Toast.LENGTH_SHORT).show()
finish()
}
}
}
}
  • 初始化图片输出文件夹
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
kotlin复制代码...
class MainActivity : AppCompatActivity() {
...
private lateinit var outputDirectory: File
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
...
outputDirectory = getOutputDirectory()
}
private fun getOutputDirectory(): File {
val mediaDir = externalMediaDirs.firstOrNull()?.let {
File(it, resources.getString(R.string.app_name)).apply { mkdirs() } }
return if (mediaDir != null && mediaDir.exists())
mediaDir else filesDir
}
}
  • 初始化线程池
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
kotlin复制代码...
class MainActivity : AppCompatActivity() {
...
private lateinit var cameraExecutor: ExecutorService
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
...
val camera_capture_button = findViewById<Button>(R.id.camera_capture_button)
// Set up the listener for take photo button
camera_capture_button.setOnClickListener { takePhoto() }
cameraExecutor = Executors.newSingleThreadExecutor()
}
private fun takePhoto(){ }
//生命周期结束时关闭线程池,以免内存泄漏
override fun onDestroy() {
super.onDestroy()
cameraExecutor.shutdown()
}
}
  • 运行代码,界面应如下所示

86b7a9ccc8e13fb0_1920.png

实现预览功能

在相机应用中,用户可借助取景器预览他们要拍摄的照片。您可以使用 CameraX Preview 类实现取景器功能。如要使用 Preview,您首先需要定义配置,然后使用该配置创建用例的实例。所生成的实例是您要绑定到CameraX生命周期的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
kotlin复制代码private fun startCamera() {
   val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

   cameraProviderFuture.addListener(Runnable {
//创建ProcessCameraProvider的实例。
//此实例用于将相机的生命周期绑定到lifecycler Owner。
//由于 CameraX 具有生命周期感知能力,所以这样可以省去打开和关闭相机的任务。
       val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

       //Preview预览类
       val preview = Preview.Builder()
          .build()
          .also {
              it.setSurfaceProvider(viewFinder.surfaceProvider())
          }

       //把后置摄像头选为默认相机
       val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
       try {
           //在重新绑定前解绑lifecycle,确保没有任何owner在此之前已经绑定
           cameraProvider.unbindAll()


           //把camera和lifecycle进行绑定
           cameraProvider.bindToLifecycle(
               this, cameraSelector, preview)

       } catch(exc: Exception) {
           Log.e(TAG, "Use case binding failed", exc)
       }
   }, ContextCompat.getMainExecutor(this))
}
  • 运行,此时的预览效果
    392602bf3da0a336_1920.png

拍摄照片并保存

  • takePhoto()的实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
kotlin复制代码//此处变量声明可在onCreate()前那些变量声明处一起编写
private var imageCapture: ImageCapture? = null
private fun takePhoto() {
   //获取对ImageCapture用例的引用
//如果用例为 null,则退出函数。
//如果您在设置拍摄图像之前点按拍照按钮,则这将为 null。
//如果没有 `return` 语句,则在用例为 `null` 的情况下,应用会崩溃。
   val imageCapture = imageCapture ?: return

   //创建用于保存图片的时间戳File
   val photoFile = File(
       outputDirectory,
       SimpleDateFormat(FILENAME_FORMAT, Locale.US
       ).format(System.currentTimeMillis()) + ".jpg")

   // 创建输出图片设置,包含File和META-DATA
   val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()

   //设置iamgeCapture的监听,在图片被拍摄完成后触发
   imageCapture.takePicture(
       outputOptions, ContextCompat.getMainExecutor(this), object : ImageCapture.OnImageSavedCallback {
           override fun onError(exc: ImageCaptureException) {
               Log.e(TAG, "Photo capture failed: ${exc.message}", exc)
           }

           override fun onImageSaved(output: ImageCapture.OutputFileResults) {
               val savedUri = Uri.fromFile(photoFile)
               val msg = "Photo capture succeeded: $savedUri"
               Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
               Log.d(TAG, msg)
           }
       })
}
  • startCamera()中保存图片设置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
kotlin复制代码private fun startCamera() {
   val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

   cameraProviderFuture.addListener(Runnable {
...
//初始化imageCaptrue
imageCapture = ImageCapture.Builder().build()
       //把后置摄像头选为默认相机
       val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
       try {
           //在重新绑定前解绑lifecycle,确保没有任何owner在此之前已经绑定
           cameraProvider.unbindAll()
           //把camera和lifecycle进行绑定
//把camera和imageCaptrue
           cameraProvider.bindToLifecycle(
               this, cameraSelector, preview,imageCapture
)

       } catch(exc: Exception) {
           Log.e(TAG, "Use case binding failed", exc)
       }
   }, ContextCompat.getMainExecutor(this))
}
  • 运行应用,即可正常拍摄照片并保存

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
kotlin复制代码package com.mobilescanner.cameraxdemo

import android.Manifest
import android.content.pm.PackageManager
import android.icu.text.SimpleDateFormat
import android.net.Uri
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.widget.Button
import android.widget.Toast
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import java.io.File
import java.util.Locale
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors

typealias LumaListener = (luma: Double) -> Unit
class MainActivity : AppCompatActivity() {
/**
* TAG:后续编写中需要使用Log.e的Tag
* FILENAME_FORMAT:该示例中保存图片文件的文件名为时间戳,该变量为时间戳定义形式
* REQUEST_CODE_PERMISSIONS:申请相机使用权限时的申请码
* REQUIRED_PERMISSIONS:需要申请使用的权限
*/
companion object {
private const val TAG = "CameraXBasic"
private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
private const val REQUEST_CODE_PERMISSIONS = 10
private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)
/**
* 注意在此处Manifest.permission.CAMERA可能会出现找不到的情况
* 查看你使用的是否为Android包的Manifest而不是你本项目里的Manifest(删了在Android Studio的提示下重打)
*/
}
private lateinit var outputDirectory: File
private lateinit var cameraExecutor: ExecutorService
private var imageCapture: ImageCapture? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

// Request camera permissions
if (allPermissionsGranted()) {
startCamera()
} else {
ActivityCompat.requestPermissions(
this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
}
val camera_capture_button = findViewById<Button>(R.id.camera_capture_button)
// Set up the listener for take photo button
camera_capture_button.setOnClickListener { takePhoto() }

outputDirectory = getOutputDirectory()

cameraExecutor = Executors.newSingleThreadExecutor()
}

private fun getOutputDirectory(): File {
val mediaDir = externalMediaDirs.firstOrNull()?.let {
File(it, resources.getString(R.string.app_name)).apply { mkdirs() } }
return if (mediaDir != null && mediaDir.exists())
mediaDir else filesDir
}

private fun takePhoto() {
// Get a stable reference of the modifiable image capture use case
val imageCapture = imageCapture ?: return

// Create time-stamped output file to hold the image
val photoFile = File(
outputDirectory,
SimpleDateFormat(FILENAME_FORMAT, Locale.US
).format(System.currentTimeMillis()) + ".jpg")

// Create output options object which contains file + metadata
val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()

// Set up image capture listener, which is triggered after photo has
// been taken
imageCapture.takePicture(
outputOptions, ContextCompat.getMainExecutor(this), object : ImageCapture.OnImageSavedCallback {
override fun onError(exc: ImageCaptureException) {
Log.e(TAG, "Photo capture failed: ${exc.message}", exc)
}

override fun onImageSaved(output: ImageCapture.OutputFileResults) {
val savedUri = Uri.fromFile(photoFile)
val msg = "Photo capture succeeded: $savedUri"
Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
Log.d(TAG, msg)
}
})
}

private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
ContextCompat.checkSelfPermission(
baseContext, it) == PackageManager.PERMISSION_GRANTED
}
private fun startCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
val viewFinder = findViewById<PreviewView>(R.id.viewFinder)
cameraProviderFuture.addListener(Runnable {
// Used to bind the lifecycle of cameras to the lifecycle owner
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

// Preview
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(viewFinder.surfaceProvider)
}
imageCapture = ImageCapture.Builder()
.build()
// Select back camera as a default
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

try {
// Unbind use cases before rebinding
cameraProvider.unbindAll()

// Bind use cases to camera
cameraProvider.bindToLifecycle(
this, cameraSelector, preview,imageCapture)

} catch(exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}

}, ContextCompat.getMainExecutor(this))
}
override fun onRequestPermissionsResult(
requestCode: Int, permissions: Array<String>, grantResults:
IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_CODE_PERMISSIONS) {
if (allPermissionsGranted()) {
startCamera()
} else {
Toast.makeText(this,
"Permissions not granted by the user.",
Toast.LENGTH_SHORT).show()
finish()
}
}
}
override fun onDestroy() {
super.onDestroy()
cameraExecutor.shutdown()
}
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%