音视频 系列文章
Android 音视频开发(一) – 使用AudioRecord 录制PCM(录音);AudioTrack播放音频
Android 音视频开发(二) – Camera1 实现预览、拍照功能
Android 音视频开发(三) – Camera2 实现预览、拍照功能
Android 音视频开发(四) – CameraX 实现预览、拍照功能
Android 音视频开发(五) – 使用 MediaExtractor 分离音视频,并使用 MediaMuxer合成新视频(音视频同步)
音视频工程

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

一. Camera2 简介

从功能来讲,Camera2 废弃了 Camera1 的框架,它支持更多的功能,比如:

  1. 获取更多的帧(预览/拍照)信息,以及每一帧的参数配置
  2. 支持更多的图片格式(yuv/raw)等
  3. 一些新特性

而今天要完成的效果如下:

1.1. Pipeline

Camera2 的将摄像头包装成管道(Pipeline),它会捕获单个帧的输入请求,每个请求捕获单个图像,然后把这些数据包装成数据包,从而把这些图像数据输出到图片缓冲区中。

这些请求时按顺序处理的,它按顺序处理每一帧的请求,并返回请求结果给客户端,看下面这张图:


假设我们要拍摄同时拍摄两张不同尺寸的图片,那么它的过程应该是这样的:

  1. 创建一个用于 Pipeline 获取图片信息的 CaptureRequest
  2. 创建两个不同的 Surface ,用来接收图片数据,并把他们加到 CaptureRequest 中
  3. 发送配置好的的 CaptureRequest 到Pipeline 中,等待返回拍照结果

上面需要记住的是,CaptureReuqest 创建之前,我们已经把相机的数据都配置好,比如聚焦、闪光灯等,接着才把它输入给 Camrea2 的底层,它会被放入到一个被叫做 In-Flight Capture Queue 的队列中,当 In-Flight Capture Queue 队列空闲时,我们就可以从它拿到不同的 图片数据 给到Surface ,且 能拿到 CaptureResult 这个返回结果信息。

1.2. Supported Hardware Level

我们支持,Camera 支持了很多新功能的特性,但这也要看你的手机厂商的支持程度,所以,为了方便区分,Camera2 使用 Supported Hardware Level 来判断你是否支持 Camera2 的特性,它分为4个登记:

  • LEGACY :向后兼容模式,支持Camera1 的功能,不支持 Camera2 的新特性
  • LIMITED :除了支持 Camera1 的特性,还支持部分 Camera2 的高级特性
  • FULL :支持所有 Camera2 的高级特性
  • LEVEL_3 :新增更多的 Camera2 特性,利于YUV 数据等

下面我们将根据这些特性,来完成 Camera2 的开发。

二. 相机预览

要注意的是,Camera2 与 Camera1 是两个不同的框架,不要被 Camera1 的思想禁锢,把它当做新知识学习即可,一个相机的流程图如下:

  1. 通过context.getSystemService(Context.CAMERA_SERVICE) 获取CameraManager.
  2. 通过 getCameraCharacteristics() 方法,拿到相机的所有信息,比如支持的预览大小,level 等
  3. 通过 CameraManager 的 openCamera() 方法,从回调中拿到 CameraDevice ,它表示当前相机设备
  4. CameraDevice 通过 createCaptureRequest 创建 CaptureRequest.Builder ,用来配置相机属性,通过 createCaptureSession 创建 CameraCaptureSession ,它是 Pipeline 的实例,然后交给底层处理

别忘了,您要添加以下权限:

    <uses-permission android:name="android.permission.CAMERA" /> <!-- 支持相机才能运行 --><!--需要设备有相机--><uses-featureandroid:name="android.hardware.camera"android:required="true" /><uses-feature android:name="android.hardware.camera.autofocus" />

2.1. 获取相机信息

从上面的流程图支持,我们需要通过 CameraManager 的 getCameraCharacteristics() 方法,来获取相机的信息;
CameraManager 是一个负责查询和建立相机连接的系统服务,它的功能不多,主要如下:

  1. 将相机信息装载到 CameraCharacteristics 中。
  2. 根据指定的相机ID 连接相机
  3. 提供将闪光灯设置为手电筒的快捷方式

所以它的代码如下:

try {//获取相机服务 CameraManagermCameraManager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);//遍历设备支持的相机 ID ,比如前置,后置等String[] cameraIdList = mCameraManager.getCameraIdList();for (String cameraId : cameraIdList) {// 拿到装在所有相机信息的  CameraCharacteristics 类CameraCharacteristics characteristics = mCameraManager.getCameraCharacteristics(cameraId);//拿到相机的方向,前置,后置,外置Integer facing = characteristics.get(CameraCharacteristics.LENS_FACING);if (facing != null) {//后置摄像头if (facing == CameraCharacteristics.LENS_FACING_BACK) {mBackCameraId = cameraId;mBackCameraCharacteristics = characteristics;}else if (facing == CameraCharacteristics.LENS_FACING_FRONT){//前置摄像头mFrontCameraId = cameraId;mFrontCameraCharacteristics = characteristics;}mCameraId = cameraId;}//是否支持 Camera2 的高级特性Integer level = characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);/*** 不支持 Camera2 的特性*/if (level == null || level == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY){//  Toast.makeText(this, "您的手机不支持Camera2的高级特效", Toast.LENGTH_SHORT).show();//   break;}}} catch (CameraAccessException e) {e.printStackTrace();
}

上面的注释都很清晰了,我们是通过 CameraCharacteristics 去知道当前的相机方向;除了这些,它还包含相机的其他信息,比如:

  • 是否有闪光灯 FLASH_INFO_AVAILABLE
  • 是否有 AE 模式 CONTROL_AE_AVAILABLE_MODES
  • 光爆等,如果你对 Camera1 比较熟悉,那么 CameraCharacteristics 有点像 Camera1 的 Camera.CameraInfo 或者 Camera.Parameters

2.2 打开摄像头

比 Camera1 好的就是,Camera2 在打开摄像头之前,就可以进行参数的配置,比如预览尺寸等。
我们知道,摄像头需要 Surface 来装载数据,这里使用的是 TextureView 来装载:

  mTextureView = findViewById(R.id.surface);

所以,当它创建完成拿到宽高之后,我们就可以打开摄像头了:

    private void openCamera(int width, int height) {//判断不同摄像头,拿到 CameraCharacteristicsCameraCharacteristics characteristics = mCameraId.equals(mBackCameraId) ? mBackCameraCharacteristics : mFrontCameraCharacteristics;//拿到配置的mapStreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);//获取摄像头传感器的方向mSensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);//获取预览尺寸Size[] previewSizes = map.getOutputSizes(SurfaceTexture.class);//获取最佳尺寸Size bestSize = getBestSize(width, height, previewSizes);/*** 配置预览属性* 与 Cmaera1 不同的是,Camera2 是把尺寸信息给到 Surface (SurfaceView 或者 ImageReader),* Camera2 会根据 Surface 配置的大小,输出对应尺寸的画面;* 注意摄像头的 width > height ,而我们使用竖屏,所以宽高要变化一下*/mTextureView.getSurfaceTexture().setDefaultBufferSize(bestSize.getHeight(),bestSize.getWidth());/*** 设置图片尺寸,这里图片的话,选择最大的分辨率即可*/Size[] sizes = map.getOutputSizes(ImageFormat.JPEG);Size largest = Collections.max(Arrays.asList(sizes),new CompareSizesByArea());//设置imagereader,配置大小,且最大Image为 1,因为是 JPEGmImageReader = ImageReader.newInstance(largest.getWidth(),largest.getHeight(),ImageFormat.JPEG,1);//拍照监听mImageReader.setOnImageAvailableListener(new ImageAvailable(),null);try {//打开摄像头,监听数据mCameraManager.openCamera(mCameraId,new CameraDeviceCallback(),null);} catch (CameraAccessException e) {e.printStackTrace();}}

在用 mCameraManager.openCamera() 打开摄像头之前,我们通过

//拿到配置的map
StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);

拿到可用流的map,这样就可以通过 getOutputSizes() 等拿到相机支持的所有尺寸了,通过上一章 Android 音视频开发(二) – Camera1 实现预览、拍照功能 也解释了,为啥要有最佳尺寸,不然会有图片拉伸的问题。
而与 Camera1 不同的是,Camera2 会根据 Surface 配置的大小,输出对应尺寸的画面,所以这里设置 mTextureView 的大小即可,注意摄像头的 width > height ,而我们使用竖屏,所以宽高要变化一下

接着设置图片的尺寸,这里当然是越轻越好了,所以选择最大尺寸即可。ImageReader 等到后面拍照时再讲解。

最后调用 mCameraManager.openCamera() ,它有三个参数:

  • cameraId :Camera 的 ID,比如前置、后置和外置
  • CameraDevice.StateCallback :当连接到相机时,该回调就会被调用,生成 CameraDevice
  • handler : 调用 CameraDevice.StateCallback 的 Handler,传null,则调用主线程,建议传入 HandlerThread 的hander,毕竟这种都是耗时的。

2.3. CameraDevice

CameraDevice 表示当前的相机设备,它的主要职责有:

  1. 根据指定的参数创建 CameraCaptureSession
  2. 根据指定的模板创建 CaptureRequest
  3. 关闭相机设备
  4. 监听相机状态,比如断开,开启成功失败的监听

如下:

    class CameraDeviceCallback extends CameraDevice.StateCallback{@Overridepublic void onOpened(@NonNull CameraDevice camera) {mCameraDevice = camera;//此时摄像头已经打开,可以预览了createPreviewPipeline(camera);}@Overridepublic void onDisconnected(@NonNull CameraDevice camera) {camera.close();}@Overridepublic void onError(@NonNull CameraDevice camera, int error) {camera.close();}}

在 onOpened 创建我们CaptureRequest ,配置相机参数信息,如下:

    private void createPreviewPipeline(CameraDevice cameraDevice){try {//创建作为预览的 CaptureRequst.builderfinal CaptureRequest.Builder captureBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);Surface surface = new Surface(mTextureView.getSurfaceTexture());//添加 surface 容器captureBuilder.addTarget(surface);// 创建CameraCaptureSession,该对象负责管理处理预览请求和拍照请求,这个必须在创建 Seesion 之前就准备好,传递给底层用于皮遏制 pipelinecameraDevice.createCaptureSession(Arrays.asList(surface, mImageReader.getSurface()), new CameraCaptureSession.StateCallback() {@Overridepublic void onConfigured(@NonNull CameraCaptureSession session) {mCameraCaptureSession = session;try {//设置自动聚焦captureBuilder.set(CaptureRequest.CONTROL_AE_MODE,CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);//设置自动曝光captureBuilder.set(CaptureRequest.CONTROL_AE_MODE,CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);//创建 CaptureRequestCaptureRequest build = captureBuilder.build();//设置预览时连续捕获图片数据session.setRepeatingRequest(build,null,null);}catch (Exception e){}}@Overridepublic void onConfigureFailed(@NonNull CameraCaptureSession session) {Toast.makeText(Camera2Activity.this, "配置失败", Toast.LENGTH_SHORT).show();}},null);} catch (Exception e) {e.printStackTrace();}}

在开启预览之前,我们需要先创建 CaptureRequest ,上面已经说过 CaptureRequest 是向 CameraCaptureSession 提交 Capture 请求的信息载体,内部包含了本次的 Capture 参数配置和接受图像数据的 Surface。

CaptureRequest 可以配置的信息非常多,比如图像格式、图像分辨率、聚焦、闪光灯控制等,可以说绝大部分配置都是通过 CaptureRequest 配置的。

上面通过 cameraDevice.createCaptureRequest() 来创建一个 CaptureRequest.Builder 对象,其中createCaptureRequest() 方法的参数是 templateType 用于指定哪种模板,Camera2 根据不同场景,为我们配置了一些常用的参数模板:

  • TEMPLATE_PREVIEW:适用于配置预览的模板
  • TEMPLATE_RECORD:适用于视频录制的模板。
  • TEMPLATE_STILL_CAPTURE:适用于拍照的模板。
  • TEMPLATE_VIDEO_SNAPSHOT:适用于在录制视频过程中支持拍照的模板。
  • TEMPLATE_MANUAL:适用于希望自己手动配置大部分参数的模板。

这里我们需要一个预览的 CaptureRequest ,所以选择 TEMPLATE_PREVIEW的模板。

接着,需要设置要承载图像数据的 Surface,我们用到两个,一个是 TextureView 用来预览的,一个是 ImageReader 用来拍照的

目前这个 CaptureRequest 是用来预览的,所以通过 addTarget 设置进去:

 //添加 surface 容器captureBuilder.addTarget(surface);

最后通过 cameraDevice.createCaptureSession() 创建 CameraCaptureSession ,然后再配置一下聚焦和曝光的配置,就可以把 CaptureRequest 通过 Session 发送给底层了:

// 创建CameraCaptureSession,该对象负责管理处理预览请求和拍照请求,这个必须在创建 Seesion 之前就准备好,传递给底层用于配置 pipelinecameraDevice.createCaptureSession(Arrays.asList(surface, mImageReader.getSurface()), new CameraCaptureSession.StateCallback() {@Overridepublic void onConfigured(@NonNull CameraCaptureSession session) {mCameraCaptureSession = session;try {//设置自动聚焦captureBuilder.set(CaptureRequest.CONTROL_AE_MODE,CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);//设置自动曝光captureBuilder.set(CaptureRequest.CONTROL_AE_MODE,CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);//创建 CaptureRequestCaptureRequest build = captureBuilder.build();//设置预览时连续捕获图片数据session.setRepeatingRequest(build,null,null);}catch (Exception e){}}@Overridepublic void onConfigureFailed(@NonNull CameraCaptureSession session) {Toast.makeText(Camera2Activity.this, "配置失败", Toast.LENGTH_SHORT).show();}},null);

2.4. 开启和关闭预览

在 Camera2 中,本质上是不断的重复 Captrue 的过程,每一次 Capture 都会把预览的数据输出到对应的 Surface 中,所以,为了达到预览的效果,需要使用:

    session.setRepeatingRequest(build,null,null);

它的三个参数如下:

  • request : 在不断重复执行 Capture 时使用的 CaptureRequest 对象

  • callback :监听每一次 Capture 状态的 CameraCaptureSession.CaptureCallback 对象,例如 onCaptureStarted() 意味着一次 Capture 的开始,而 onCaptureCompleted() 意味着一次 Capture 的结束。

  • handler :用于执行 CameraCaptureSession.CaptureCallback 的Handler 对象,传null为主线程,也可以使用其他线程的 Handler

关闭预览
通过上面的理解,关闭预览也很简单啦:

 //停止预览mCameraCaptureSession.stopRepeating();

三. 拍照

上面我们学习了预览,也提到了 ImageReader 是用来接收图像数据的,那怎么拍照呢?

拍照其实也是一个 Captrue,这样的话,我们就可以再创建一个 CaptureRequest 去执行拍照的就可以了,代码如下:

 //创建一个拍照的 sessionfinal CaptureRequest.Builder captureRequest = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);//设置装在图像数据的 SurfacecaptureRequest.addTarget(mImageReader.getSurface());//聚焦captureRequest.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);//自动曝光captureRequest.set(CaptureRequest.CONTROL_AF_MODE,CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);// 获取设备方向int rotation = getWindowManager().getDefaultDisplay().getRotation();// 根据设备方向计算设置照片的方向captureRequest.set(CaptureRequest.JPEG_ORIENTATION, getOrientation(rotation));// 先停止预览mCameraCaptureSession.stopRepeating();

代码比较好理解,只是通过 mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE) 创建了一个 拍照的模板

而且在执行拍照之前,先停止预览:

 mCameraCaptureSession.stopRepeating();

接着就可以使用 mCameraCaptureSession.capture() 执行拍照了:

mCameraCaptureSession.capture(captureRequest.build(), new CameraCaptureSession.CaptureCallback() {@Overridepublic void onCaptureCompleted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) {super.onCaptureCompleted(session, request, result);try {//拍完之后,让它继续可以预览CaptureRequest.Builder captureRequest1 = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);captureRequest1.addTarget(new Surface(mTextureView.getSurfaceTexture()));mCameraCaptureSession.setRepeatingRequest(captureRequest1.build(),null,null);} catch (CameraAccessException e) {e.printStackTrace();}}},null);

可以看到,当拍照结束的时候,我们又让它重新预览了,当然这里也看你的需求去设置。

3.1 保存图片

那保存图片在哪里弄呢?还记得我们在打开摄像头的时候,配置了 ImageReader 监听吗:

//拍照监听
mImageReader.setOnImageAvailableListener(new ImageAvailable(),null);

在保存之前,我们先来了解一下,什么是 ImageReader

3.2 ImageReader

在 Camrea2 中,Imageread 是获取图像数据的一个重要途径,我们可以通过它获取各种各样格式的图像数据,比如 JPEG、YUV和 RAW 等。通过 ImageReader.newInstance() 方法创建 ImageReader 对象,如下:

//设置imagereader,配置大小,且最大Image为 1,因为是 JPEG
mImageReader = ImageReader.newInstance(largest.getWidth(),largest.getHeight(),ImageFormat.JPEG,1);
mImageReader.setOnImageAvailableListener(new ImageAvailable(),null);

其中前面两个号理解,第三个参数,则是你要获取的图像数据的格式,这里使用 JPEG 即可,而最后一个参数,则表示最大 Image 的个数,可以理解成 图像池的大小。

当有图像数据生成时,就是调用 ImageReader.OnImageAvailableListener 里面的 onImageAvailable() 方法

    /*** 拍照监听,当有图片数据时,回调该接口*/class ImageAvailable implements ImageReader.OnImageAvailableListener{@Overridepublic void onImageAvailable(ImageReader reader) {new SavePicAsyncTask(reader).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);}}

接着,我们可以调用 ImageReader 的acquireNextImage()方法,来获取存有最新的 Image 对象,而 Image 对象里的图像数据又根据不同格式被划分成多个部分,分别存储在单独的 Plane 对象里,我们可以调用 Image.getPalnes() 获取所有有的 Palne 对象的数组,如下

//获取捕获的照片数据
Image image = imageReader.acquireLatestImage();
//拿到所有的 Plane 数组
Image.Plane[] planes = image.getPlanes();

最后则通过 Plane.getBuffer() 获取每一个在 Plane 里存储的图像数据 ByteBuffer。比如

格式 Image个数 Plane 层级
JPEG 1 压缩过的数据,所以行数为0,解压缩需要使用BitmapFactory#decodeByteArray
YUV 3 一个明度通道+两个色彩CbCr通道,UV的宽高是Y的一半。

YUV 可以用下图来表示(图片来源):


所以,我们获取的图片如下:

FileOutputStream fos = null;
Image image = null;
try {fos = new FileOutputStream(file);//获取捕获的照片数据image = imageReader.acquireLatestImage();//拿到所有的 Plane 数组Image.Plane[] planes = image.getPlanes();//由于是 JPEG ,只需要获取下标为 0 的数据即可ByteBuffer buffer = planes[0].getBuffer();data = new byte[buffer.remaining()];//把 bytebuffer 的数据给 byte数组buffer.get(data);Bitmap bitmap = BitmapFactory.decodeByteArray(data,0,data.length);//旋转图片if (mCameraId.equals(mFrontCameraId)){bitmap = BitmapUtils.rotate(bitmap,270);bitmap = BitmapUtils.mirror(bitmap);}else{bitmap = BitmapUtils.rotate(bitmap,90);}bitmap.compress(Bitmap.CompressFormat.JPEG,100,fos);fos.flush();return bitmap;
}catch (Exception e){Log.d(TAG, "zsr doInBackground: "+e.toString());
}finally {CloseUtils.close(fos);//记得关闭 imageif (image != null) {image.close();}
}

这样,我们就写好了,下一章,我们学习 CameraX。

参考:
https://developer.android.google.cn/reference/android/hardware/camera2/package-summary?hl=en
https://www.jianshu.com/p/9a2e66916fcb
https://www.jianshu.com/p/23e8789fbc10
https://www.jianshu.com/p/067889611ae7

Android 音视频开发(三) -- Camera2 实现预览、拍照功能相关推荐

  1. Android 音视频开发(二) -- Camera1 实现预览、拍照功能

    音视频 系列文章 Android 音视频开发(一) – 使用AudioRecord 录制PCM(录音):AudioTrack播放音频 Android 音视频开发(二) – Camera1 实现预览.拍 ...

  2. Android 音视频开发(一) -- 使用AudioRecord 录制PCM(录音);AudioTrack播放音频

    前言,音视频这块,确实比较难入门,本着学习的态度,我这边也跟着 Android 音视频开发入门指南 打怪升级,留下个脚印,大家共勉. 音视频 系列文章 Android 音视频开发(一) – 使用Aud ...

  3. Android 音视频开发(六) -- Android Mediaprojection 截屏和录屏

    Android 音视频开发(一) – 使用AudioRecord 录制PCM(录音):AudioTrack播放音频 Android 音视频开发(二) – Camera1 实现预览.拍照功能 Andro ...

  4. Android 音视频开发学习思路

    Android 音视频开发这块目前的确没有比较系统的教程或者书籍,网上的博客文章也都是比较零散的.只能通过一点点的学习和积累把这块的知识串联积累起来. 初级入门篇: Android 音视频开发(一) ...

  5. android音频开发6,Android 音视频开发(一) : 通过三种方式绘制图片

    想要逐步入门音视频开发,就需要一步步的去学习整理,并积累.本文是音视频开发积累的第一篇. 对应的要学习的内容是:在 Android 平台绘制一张图片,使用至少 3 种不同的 API,ImageView ...

  6. Android 音视频开发(二):使用 AudioRecord 采集音频PCM并保存到文件(学习笔记)

    关于 AudioRecord Android SDK 提供了两套音频采集的API,分别是:MediaRecorder 和 AudioRecord,前者是一个更加上层一点的API,它可以直接把手机麦克风 ...

  7. 直播平台源码搭建教程之Android音视频开发

    直播平台源码搭建教程之Android音视频开发 音频 将声音保存成音频的过程,其实就是将模拟音频数字化的过程,为了实现这个过程,就需要对模拟音频进行采样.量化和编码.接下来我们详细讲解这一过程. 采样 ...

  8. Android 音视频开发之基础篇 使用 imageview绘制一张图片

    Android 音视频开发 任务一 ImageView 绘制图片 文章目录 Android 音视频开发 任务一 ImageView 绘制图片 前言 一.配置activity_main.xml 二.添加 ...

  9. Android音视频开发基础(七):视频采集-系统API基础

    前言 在Android音视频开发中,网上知识点过于零碎,自学起来难度非常大,不过音视频大牛Jhuster提出了<Android 音视频从入门到提高 - 任务列表>.本文是Android音视 ...

最新文章

  1. 王坚十年前的坚持,才有了今天世界顶级大数据计算平台MaxCompute...
  2. %@ INCLUDE FILE=%与JSP:INCLUDE PAGE=/区别
  3. JavaScript编程知识
  4. JVM 的内存结构和内存分配
  5. 在python3.x下使用如下代码: import cPickle as pk 报错
  6. php打印输出小于10的数字,PHP 字符串输出 echo、print 与 printf 函数
  7. Spring Boot应用程序的“本地服务”
  8. android 字符串调用方法名,AndroidJNI.CallStringMethod 调用字符串方法
  9. 一个月学会Python的Quora指南和资料放送
  10. 【Docker】docker 执行 apt-get E: Could not open lock file /var/lib/dpkg/lock - open
  11. 【原创】Extjs4 通用CURD方法
  12. 机器学习Sklearn学习总结
  13. Android Studio 通过一个登录功能介绍SQLite数据库的使用
  14. 【Unity3D】2D动画
  15. 推荐:学习人工智能(AI)的一些网站及教程资源
  16. 第1章 操作系统引论课后答案
  17. 硬盘模式IDE和AHCI
  18. 修改设备管理器信息,修改我的电脑系统属性,修改dxdiag信息
  19. 基于JAVA EE的临床科室管理系统
  20. 超级计算机燕 排名,正文 第一卷 六年之前 HK171 舍弃的超级计算机燕? (485加更)...

热门文章

  1. Win10深色模式和白天模式设置
  2. 懂23种语言 2019年上市 宝马的AI助理有哪些不同!
  3. Unicode对汉字的编码
  4. PHP获取微信支付v2预支付参数prepay_id后在小程序端完成支付
  5. 用Python进行身份证号校验
  6. 家族关系查询系统(2021-9-22更新)
  7. [译]Unity3D Shader教程(二)HLSL
  8. Axure 制作验证码交互
  9. 基于C#的超市收银终端软件--小孩玩具
  10. Android一键加QQ群