1 理解 Capture 工作流程

在正式介绍如何拍照之前,我们有必要深入理解几种不同模式的 Capture 的工作流程,只要理解它们的工作流程就很容易掌握各种拍照模式的实现原理,在第一章《Camera2 概览》 里我们介绍了 Capture 有以下几种不同模式:

  • 单次模式(One-shot):指的是只执行一次的 Capture 操作,例如设置闪光灯模式、对焦模式和拍一张照片等。多个单次模式的 Capture 会进入队列按顺序执行。

  • 多次模式(Burst):指的是连续多次执行指定的 Capture 操作,该模式和多次执行单次模式的最大区别是连续多次 Capture 期间不允许插入其他任何 Capture 操作,例如连续拍摄 100 张照片,在拍摄这 100 张照片期间任何新的 Capture 请求都会排队等待,直到拍完 100 张照片。多组多次模式的 Capture 会进入队列按顺序执行。

  • 重复模式(Repeating):指的是不断重复执行指定的 Capture 操作,当有其他模式的 Capture 提交时会暂停该模式,转而执行其他被模式的 Capture,当其他模式的 Capture 执行完毕后又会自动恢复继续执行该模式的 Capture,例如显示预览画面就是不断 Capture 获取每一帧画面。该模式的 Capture 是全局唯一的,也就是新提交的重复模式 Capture 会覆盖旧的重复模式 Capture。

我们举个例子来进一步说明上面三种模式,假设我们的相机应用程序开启了预览,所以会提交一个重复模式的 Capture 用于不断获取预览画面,然后我们提交一个单次模式的 Capture,接着我们又提交了一组连续三次的多次模式的 Capture,这些不同模式的 Capture 会按照下图所示被执行:

Capture 工作原理

下面是几个重要的注意事项:

  1. 无论 Capture 以何种模式被提交,它们都是按顺序串行执行的,不存在并行执行的情况。

  2. 重复模式是一个比较特殊的模式,因为它会保留我们提交的 CaptureRequest 对象用于不断重复执行 Capture 操作,所以大多数情况下重复模式的 CaptureRequest 和其他模式的 CaptureRequest 是独立的,这就会导致重复模式的参数和其他模式的参数会有一定的差异,例如重复模式不会配置 CaptureRequest.AF_TRIGGER_START,因为这会导致相机不断触发对焦的操作。

  3. 如果某一次的 Capture 没有配置预览的 Surface,例如拍照的时候,就会导致本次 Capture 不会将画面输出到预览的 Surface 上,进而导致预览画面卡顿的情况,所以大部分情况下我们都会将预览的 Surface 添加到所有的 CaptureRequest 里。

2 如何拍摄单张照片

拍摄单张照片是最简单的拍照模式,它使用的就是单次模式的 Capture,我们会使用 ImageReader 创建一个接收照片的 Surface,并且把它添加到 CaptureRequest 里提交给相机进行拍照,最后通过 ImageReader 的回调获取 Image 对象,进而获取 JPEG 图像数据进行保存。

2.1 定义回调接口

当拍照完成的时候我们会得到两个数据对象,一个是通过 onImageAvailable() 回调给我们的存储图像数据的 Image,一个是通过 onCaptureCompleted() 回调给我们的存储拍照信息的 CaptureResult,它们是一一对应的,所以我们定义了如下两个回调接口:

private val captureResults: BlockingQueue<CaptureResult> = LinkedBlockingDeque()private inner class CaptureImageStateCallback : CameraCaptureSession.CaptureCallback() {@MainThreadoverride fun onCaptureCompleted(session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult) {super.onCaptureCompleted(session, request, result)captureResults.put(result)}
}
private inner class OnJpegImageAvailableListener : ImageReader.OnImageAvailableListener {@WorkerThreadoverride fun onImageAvailable(imageReader: ImageReader) {val image = imageReader.acquireNextImage()val captureResult = captureResults.take()if (image != null && captureResult != null) {// Save image into sdcard.}}
}

2.2 创建 ImageReader

创建 ImageReader 需要我们指定照片的大小,所以首先我们要获取支持的照片尺寸列表,并且从中筛选出合适的尺寸,假设我们要求照片的尺寸最大不能超过 4032x3024,并且比例必须是 4:3,所以会有如下筛选尺寸的代码片段:

@WorkerThread
private fun getOptimalSize(cameraCharacteristics: CameraCharacteristics, clazz: Class<*>, maxWidth: Int, maxHeight: Int): Size? {val streamConfigurationMap = cameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)val supportedSizes = streamConfigurationMap?.getOutputSizes(clazz)return getOptimalSize(supportedSizes, maxWidth, maxHeight)
}@AnyThread
private fun getOptimalSize(supportedSizes: Array<Size>?, maxWidth: Int, maxHeight: Int): Size? {val aspectRatio = maxWidth.toFloat() / maxHeightif (supportedSizes != null) {for (size in supportedSizes) {if (size.width.toFloat() / size.height == aspectRatio && size.height <= maxHeight && size.width <= maxWidth) {return size}}}return null
}

接着我们就可以筛选出合适的尺寸,然后创建一个图像格式是 JPEG 的 ImageReader 对象,并且获取它的 Surface:

val imageSize = getOptimalSize(cameraCharacteristics, ImageReader::class.java, maxWidth, maxHeight)!!
jpegImageReader = ImageReader.newInstance(imageSize.width, imageSize.height, ImageFormat.JPEG, 5)
jpegImageReader?.setOnImageAvailableListener(OnJpegImageAvailableListener(), cameraHandler)
jpegSurface = jpegImageReader?.surface

2.3 创建 CaptureRequest

接下来我们使用 TEMPLATE_STILL_CAPTURE 模板创建一个用于拍照的 CaptureRequest.Builder 对象,并且添加拍照的 Surface 和预览的 Surface 到其中:

captureImageRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE)
captureImageRequestBuilder.addTarget(previewDataSurface)
captureImageRequestBuilder.addTarget(jpegSurface)

你可能会疑问为什么拍照用的 CaptureRequest 对象需要添加预览的 Surface,这一点我们在前面有解释过了,如果某一次的 Capture 没有配置预览的 Surface,例如拍照的时候,就会导致本次 Capture 不会将画面输出到预览的 Surface 上,进而导致预览画面卡顿的情况,所以大部分情况下我们都会将预览的 Surface 添加到所有的 CaptureRequest 里。

2.4 矫正 JPEG 图片方向

摄像头传感器的方向很多时候都不是 0°,这就会导致我们拍出来的照片方向是错误的,例如手机摄像头传感器方向是 90° 的时候,垂直拿着手机拍出来的照片很可能是横着的:

在进行图片方向矫正的时候,我们的目的是做到所见即所得,也就是用户在预览画面里看到的是什么样,输出的图片就是什么样。为了做到图片所见即所得,我们要同时考虑设备方向和摄像头传感器方向,下面是一段来自官方的图片矫正代码:

private fun getJpegOrientation(cameraCharacteristics: CameraCharacteristics, deviceOrientation: Int): Int {var myDeviceOrientation = deviceOrientationif (myDeviceOrientation == android.view.OrientationEventListener.ORIENTATION_UNKNOWN) {return 0}val sensorOrientation = cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION)!!// Round device orientation to a multiple of 90myDeviceOrientation = (myDeviceOrientation + 45) / 90 * 90// Reverse device orientation for front-facing camerasval facingFront = cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONTif (facingFront) {myDeviceOrientation = -myDeviceOrientation}// Calculate desired JPEG orientation relative to camera orientation to make// the image upright relative to the device orientationreturn (sensorOrientation + myDeviceOrientation + 360) % 360
}

唯一特别的地方是前置摄像头输出的画面底层默认做了镜像的翻转才能保证我们在预览的时候看到的画面就想照镜子一样,所以前置摄像头给的 SENSOR_ORIENTATION 值也是经过镜像的,但是相机在输出 JPEG 的时候并没有进行镜像操作,所以在计算 JPEG 矫正角度的时候要对这个默认镜像的操作进行逆向镜像。

计算出图片的矫正角度后,我们要通过 CaptureRequest.JPEG_ORIENTATION 配置这个角度,相机在拍照输出 JPEG 图像的时候会参考这个角度值从以下两种方式选一种进行图像方向矫正:

  1. 直接对图像进行旋转,并且将 Exif 的 ORIENTATION 标签赋值为 0。
  2. 不对图像进行旋转,而是将旋转信息写入 Exif 的 ORIENTATION 标签里。

客户端在显示图片的时候一定要去检查 Exif 的ORIENTATION 标签的值,并且根据这个值对图片进行对应角度的旋转才能保证图片显示方向是正确的。

val deviceOrientation = deviceOrientationListener.orientation
val jpegOrientation = getJpegOrientation(cameraCharacteristics, deviceOrientation)
captureImageRequestBuilder[CaptureRequest.JPEG_ORIENTATION] = jpegOrientation

2.5 设置缩略图尺寸

相机在输出 JPEG 图片的时候,同时会根据我们通过 CaptureRequest.JPEG_THUMBNAIL_SZIE 配置的缩略图尺寸生成一张缩略图写入图片的 Exif 信息里。在设置缩略图尺寸之前,我们首先要获取相机支持哪些缩略图尺寸,与获取预览尺寸或照片尺寸列表方式不一样的是,缩略图尺寸列表是直接通过 CameraCharacteristics.JPEG_AVAILABLE_THUMBNAIL_SIZES 获取的。配置缩略图尺寸的代码如下所示:

val availableThumbnailSizes = cameraCharacteristics[CameraCharacteristics.JPEG_AVAILABLE_THUMBNAIL_SIZES]
val thumbnailSize = getOptimalSize(availableThumbnailSizes, maxWidth, maxHeight)

在获取图片缩略图的时候,我们不能总是假设图片一定会在 Exif 写入缩略图,当 Exif 里面没有缩略图数据的时候,我们要转而直接 Decode 原图获取缩略图,另外无论是原图还是缩略图,都要根据 Exif 的 ORIENTATION 角度进行角度矫正才能正确显示,下面是我们 Demo 中获取图片缩略图的代码:

@WorkerThread
private fun getThumbnail(jpegPath: String): Bitmap? {val exifInterface = ExifInterface(jpegPath)val orientationFlag = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)val orientation = when (orientationFlag) {ExifInterface.ORIENTATION_NORMAL -> 0.0FExifInterface.ORIENTATION_ROTATE_90 -> 90.0FExifInterface.ORIENTATION_ROTATE_180 -> 180.0FExifInterface.ORIENTATION_ROTATE_270 -> 270.0Felse -> 0.0F}var thumbnail = if (exifInterface.hasThumbnail()) {exifInterface.thumbnailBitmap} else {val options = BitmapFactory.Options()options.inSampleSize = 16BitmapFactory.decodeFile(jpegPath, options)}if (orientation != 0.0F && thumbnail != null) {val matrix = Matrix()matrix.setRotate(orientation)thumbnail = Bitmap.createBitmap(thumbnail, 0, 0, thumbnail.width, thumbnail.height, matrix, true)}return thumbnail
}

2.6 设置定位信息

拍照的时候,通常都会在图片的 Exif 写入定位信息,我们可以通过 CaptureRequest.JPEG_GPS_LOCATION 配置定位信息,代码如下:

@WorkerThread
private fun getLocation(): Location? {val locationManager = getSystemService(LocationManager::class.java)if (locationManager != null && ContextCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {return locationManager.getLastKnownLocation(LocationManager.PASSIVE_PROVIDER)}return null
}
val location = getLocation()
captureImageRequestBuilder[CaptureRequest.JPEG_GPS_LOCATION] = location

2.7 播放快门音效

在进行拍照之前,我们还需要配置拍照时播放的快门音效,因为 Camera2 和 Camera1 不一样,拍照时不会有任何声音,需要我们在适当的时候通过 MediaSoundPlayer 播放快门音效,通常情况我们是在 CaptureStateCallback.onCaptureStarted() 回调的时候播放快门音效:

private val mediaActionSound: MediaActionSound = MediaActionSound()private inner class CaptureImageStateCallback : CameraCaptureSession.CaptureCallback() {@MainThreadoverride fun onCaptureStarted(session: CameraCaptureSession, request: CaptureRequest, timestamp: Long, frameNumber: Long) {super.onCaptureStarted(session, request, timestamp, frameNumber)// Play the shutter click sound.cameraHandler?.post { mediaActionSound.play(MediaActionSound.SHUTTER_CLICK) }}@MainThreadoverride fun onCaptureCompleted(session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult) {super.onCaptureCompleted(session, request, result)captureResults.put(result)}
}

2.8 拍照并保存图片

经过一连串的配置之后,我们终于可以开拍照了,直接调用 CameraCaptureSession.capture() 方法把 CaptureRequest 对象提交给相机就可以等待相机输出图片了,该方法要求我们设置三个参数:

  • request:本次 Capture 操作使用的 CaptureRequest 对象。
  • listener:监听 Capture 状态的回调接口。
  • handler:回调 Capture 状态监听接口的 Handler 对象。
captureSession.capture(captureImageRequest, CaptureImageStateCallback(), mainHandler)

如果一切顺利,相机在拍照完成的时候会通过 CaptureStateCallback.onCaptureCompleted() 回调一个 CaptureResult 对象给我们,里面包含了本次拍照的所有信息,另外还会通过 OnImageAvailableListener.onImageAvailable() 回调一个代表图像数据的 Image 对象给我们。在我们的 Demo 中,我们将获取到的 CaptureResult 对象保存到一个阻塞队列中,在 OnImageAvailableListener.onImageAvailable() 回调的时候就从这个阻塞队列获取 CaptureResult 对象,结合 Image 对象对图片进行保存操作,并且还会在图片保存完毕的时候获取图片的缩略图用于刷新 UI,代码如下所示:

private inner class OnJpegImageAvailableListener : ImageReader.OnImageAvailableListener {private val dateFormat: DateFormat = SimpleDateFormat("yyyyMMddHHmmssSSS", Locale.getDefault())private val cameraDir: String = "${Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)}/Camera"@WorkerThreadoverride fun onImageAvailable(imageReader: ImageReader) {val image = imageReader.acquireNextImage()val captureResult = captureResults.take()if (image != null && captureResult != null) {image.use {val jpegByteBuffer = it.planes[0].buffer// Jpeg image data only occupy the planes[0].val jpegByteArray = ByteArray(jpegByteBuffer.remaining())jpegByteBuffer.get(jpegByteArray)val width = it.widthval height = it.heightsaveImageExecutor.execute {val date = System.currentTimeMillis()val title = "IMG_${dateFormat.format(date)}"// e.g. IMG_20190211100833786val displayName = "$title.jpeg"// e.g. IMG_20190211100833786.jpegval path = "$cameraDir/$displayName"// e.g. /sdcard/DCIM/Camera/IMG_20190211100833786.jpegval orientation = captureResult[CaptureResult.JPEG_ORIENTATION]val location = captureResult[CaptureResult.JPEG_GPS_LOCATION]val longitude = location?.longitude ?: 0.0val latitude = location?.latitude ?: 0.0// Write the jpeg data into the specified file.File(path).writeBytes(jpegByteArray)// Insert the image information into the media store.val values = ContentValues()values.put(MediaStore.Images.ImageColumns.TITLE, title)values.put(MediaStore.Images.ImageColumns.DISPLAY_NAME, displayName)values.put(MediaStore.Images.ImageColumns.DATA, path)values.put(MediaStore.Images.ImageColumns.DATE_TAKEN, date)values.put(MediaStore.Images.ImageColumns.WIDTH, width)values.put(MediaStore.Images.ImageColumns.HEIGHT, height)values.put(MediaStore.Images.ImageColumns.ORIENTATION, orientation)values.put(MediaStore.Images.ImageColumns.LONGITUDE, longitude)values.put(MediaStore.Images.ImageColumns.LATITUDE, latitude)contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)// Refresh the thumbnail of image.val thumbnail = getThumbnail(path)if (thumbnail != null) {runOnUiThread {thumbnailView.setImageBitmap(thumbnail)thumbnailView.scaleX = 0.8FthumbnailView.scaleY = 0.8FthumbnailView.animate().setDuration(50).scaleX(1.0F).scaleY(1.0F).start()}}}}}}
}

2.9 前置摄像头拍照的镜像问题

如果你使用前置摄像头进行拍照,虽然照片的方向已经被我们矫正了,但是你会发现画面却是相反的,例如你在预览的时候人脸在左边,拍出来的照片人脸却是在右边。出现这个问题的原因是默认情况下相机不会对 JPEG 图像进行镜像操作,导致输出的原始画面是非镜像的。解决这个问题的一个办法是拿到 JPEG 数据之后再次对图像进行镜像操作,然后才保存图片。

3 如何连续拍摄多张图片

在我们的 Demo 中有一个特殊的拍照功能,就是当用户双击快门按钮的时候会连续拍摄 10 张照片,其实现原理就是采用了多次模式的 Capture,所有的配置流程和拍摄单张照片一样,唯一的区别是我们使用 CameraCaptureSession.captureBurst() 进行拍照,该方法要求我们传递一下三个参数:

  • requests:按顺序连续执行的 CaptureRequest 对象列表,每一个 CaptureRequest 对象都可以有自己的配置,在我们的 Demo 里出于简化的目的,10 个 CaptureRequest 对象实际上的都是同一个。
  • listener:监听 Capture 状态的回调接口,需要注意的是有多少个 CaptureRequest 对象就会回调该接口多少次。
  • handler:回调 Capture 状态监听接口的 Handler 对象。
val captureImageRequest = captureImageRequestBuilder.build()
val captureImageRequests = mutableListOf<CaptureRequest>()
for (i in 1..burstNumber) {captureImageRequests.add(captureImageRequest)
}
captureSession.captureBurst(captureImageRequests, CaptureImageStateCallback(), mainHandler)

接下来所有的流程就和拍摄单招照片一样了,每输出一张图片我们就将其保存到 SD 卡并且刷新媒体库和缩略图。

4 如何连拍

连拍这个功能在 Camera2 出现之前是不可能实现的,现在我们只需要使用重复模式的 Capture 就可以轻松实现连拍功能。重复模式的 Capture 来实现预览功能,而这一次我们不仅要用该模式进行预览,还要在预览的同时也输出照片,所以我们会使用 CameraCaptureSession.setRepeatingRequest() 方法开始进行连拍:

val captureImageRequest = captureImageRequestBuilder.build()
captureSession.setRepeatingRequest(captureImageRequest, CaptureImageStateCallback(), mainHandler)

停止连拍有以下两种方式:

  1. 调用 CameraCaptueSession.stopRepeating() 方法停止重复模式的 Capture,但是这会导致预览也停止。
  2. 调用 CameraCaptueSession.setRepeatingRequest() 方法并且使用预览的 CaptureRequest 对象,停止输出照片。

在我们的 Demo 里使用了第二种方式:

@MainThread
private fun stopCaptureImageContinuously() {// Restart preview to stop the continuous image capture.startPreview()
}

5 如何切换前后置摄像头

切换前后置摄像头是一个很常见的功能,虽然和本章的主要内容不相关,但是在 Demo 中已经实现,所以这里也顺便提一下。我们只要按照以下顺序进行操作就可以轻松实现前后置摄像头的切换:

  1. 关闭当前摄像头
  2. 开启新的摄像头
  3. 创建新的 Session
  4. 开启预览

下面是代码片段,详细代码大家可以自行查看 Demo 源码:

@MainThread
private fun switchCamera() {val cameraDevice = cameraDeviceFuture?.get()val oldCameraId = cameraDevice?.idval newCameraId = if (oldCameraId == frontCameraId) backCameraId else frontCameraIdif (newCameraId != null) {closeCamera()openCamera(newCameraId)createCaptureRequestBuilders()setPreviewSize(MAX_PREVIEW_WIDTH, MAX_PREVIEW_HEIGHT)setImageSize(MAX_IMAGE_WIDTH, MAX_IMAGE_HEIGHT)createSession()startPreview()}
}

6 总结

本章主要讲述了如何实现几种常见的拍照模式,其核心要领就是理解【重复模式】、【单词模式】和【多次模式】的工作流程,根据实际业务情况灵活运用,下面是几个小建议:

  1. 重复模式和多次模式都可以实现连拍功能,其中重复模式适合没有连拍上限的情况,而多次模式适合有连拍上限的情况。
  2. 一个 CaptureRequest 可以添加多个 Surface,这就意味着你可以同时拍摄多张照片。
  3. 拍照获取 CaptureResult 和 Image 对象走的是两个不同的回调接口,灵活运用子线程的阻塞操作可以简化你的代码逻辑。

Camera2 四拍照相关推荐

  1. 十分钟实现 Android Camera2 相机拍照

    1. 前言 因为工作中要使用Android Camera2 API,但因为Camera2比较复杂,网上资料也比较乱,有一定入门门槛,所以花了几天时间系统研究了下,并在CSDN上记录了下,希望能帮助到更 ...

  2. 【Android -- 相机】Camera2 实现拍照 预览功能

    前言 上篇文章,我们已经用 Camera1 实现了预览和拍照的功能,但也说到,在API21的时候,Camera1已经被标注为弃用,因为它的API功能和灵活性满足不了现在日益复杂的相机开发了,所以在 A ...

  3. Android 使用Camera2 实现拍照录像的功能

    职场小白迷上优美句子: 还是电影  <无问西东>中的台词,这句有点感人: 沈光耀的妈妈对沈光耀说:"当初你离家千里,来到这个地方读书,你父亲和我都没有反对过,因为,是我们想你,能 ...

  4. 第37讲 Android Camera2 API 拍照打闪实战

    本讲是Android Camera专题系列的第37讲,我们介绍Android Camera2 API专题的拍照打闪实战,包括如下内容: 设置不同的Flash模式 拍照打闪流程 视频在线观看: 极客笔记 ...

  5. Android Camera2 相机拍照流程详解

    实现特点 实现自动对焦 选择性正常触发闪光灯flash 复用CaptureRequest.Builder, 参数完全一致 拍照注意事项讲解 代码片段详解 流程 按照常规方式打开预览 设置好相应的全局变 ...

  6. Camera2相机拍照流程之拍照功能梳理

    /*** 拍照时调用方法*/ private void captureStillPicture() {try {if (mCameraDevice == null) {return;}// 创建作为拍 ...

  7. android 基础一 Camera2实现拍照功能

    public class SurfaceViewActivity extends AppCompatActivity {String TAG="SurfaceViewActivity_AA& ...

  8. android camera(6)---camera2 拍照流程

    android camera2 拍照流程 正文 camera2 API 的加入是从AndroidV5.0(21)开始的,因此我们使用Camera2应该是在Android 5.0(含5.0)之后.同时, ...

  9. Camera2预览拍照流程

    Camera2是现在Andoird相机开发中经常使用的框架,最近一直在学习Camera2的使用,今天简单分享一下我学到的Camera2的预览拍照的流程. 1.获取相机服务,在Camera2中相机服务的 ...

最新文章

  1. 如何高效快速搞散一个团队?
  2. java用户输入解析_Java中的3种输入方式实现解析
  3. 前端项目如何用eslint提高代码质量
  4. java和python的web自动化有什么区别-三分钟看懂Python和Java的区别
  5. 【Windows 逆向】Cheat Engine 数据挖掘搜索方法和技巧 ( 数值类型选择 | 字符串数值类型选择 | 全部数值类型模糊选择 )
  6. 一个不错的游戏 - flash webgame
  7. Android官方开发文档Training系列课程中文版:Activity测试之UI组件测试
  8. 笔记一 Redis基础
  9. drbd(三):drbd的状态说明
  10. 微软sharepoint团队博客
  11. AI考拉技术分享会--Node.js APM 软件调研报告
  12. wince车机刷系统刷机包_2020年刷机包是不是越小越精简,越小越流畅好用
  13. 使用 POI 读取 Word docx 中的书签、替换书签内容(汉字或合并外部文档内容)
  14. smarty 模板 php,PHP smarty模板
  15. 下载谷歌瓦片地图并拼接为高清大图
  16. 网络编辑必学:网络新闻标题之争
  17. chromium 浏览器多进程架构小科普
  18. 光学系统像差的计算机模拟,XCCHJJ-B 光学系统像差传函焦距测量综合实验装置
  19. clion之Clion License Activation破解
  20. mysql 怎么设置ip地址_Mysql如何设置用户指定ip地址操作数据库

热门文章

  1. Unity插件:PATCH - Updating System INDIE PC程序补丁插件
  2. 金士顿无线网络驱动器内部监督办公室的Wi-Fi无线加载服务器现在发货
  3. 方舟服务器制作修改,方块方舟服务器怎么架设?架设教程及选择服务器方法
  4. 什么是java rtti_浅析Java RTTI 和 反射的概念
  5. 算法(第四版)-xmind查找-符号表
  6. 更换计算机电源线,生活小事不求人之教你自己更换电脑硬盘
  7. MyBatis 一对多嵌套查询
  8. editplus java快捷键_editplus快捷键大全
  9. 扎克伯格、元宇宙和性
  10. 觅伊产品体验:凭借独特的定位,如何立足社交市场?