Android Camera了解一下
首发于公众号
Android Camera了解一下
Camera 演进简介
最近在项目中遇到 Camera相关的场景,之前对这块不是很了解,趁机补了一下盲区。Android Camera 相关也是生态碎片化较为严重的一块,Android FrameWorkt提供Camera API来实现拍照与屏幕录制的能力,目前Android有三类API
- Camera (为了便于区分 下面简称 Camera1)
此类是用于控制设备相机的旧版API,在Android5.0以下使用,现已Deprecated
- Camera2
Android 5.0以上升级的方案,控制设备相机的API,并且开放出硬件支持级别的厂商定制
(谷歌开放出官方库CameraView 帮助解决相机兼容性问题,也有其他一些三方库)
- CameraX
JetPack中引入,基与Cmaera2 API封装,简化了开发流程,并增加生命周期控制
\
那么 Camera和Camera2如何使用?Camera2优秀在哪里?官方开发的的CameraView和CameraX又是为了解决什么问题,下面来一个个了解下。
Camera1和Camera2实践
相机开发核心流程如下
- 检查权限
- 检测设备摄像头,打开相机
- 创建预览帧 显示实时画面 (一般是通过 SurfaceView、TextureView进行实时预览),每个相机有支持预览的尺寸比如4:3或者16:9、11:9等,比如定制或者横竖屏场景 需要计算合适的预览尺寸
- 设置相机参数,进行拍照监听
- 拍照 保存图片或者操作原始数据
- 释放相机资源
其中步骤3在Camera1和Camera2中稍有不同,Camera1拍照前必须先开启预览,而Camera2流程做了解耦,可以无需预览直接拍照
Camera1
代码
权限声明
<uses-featureandroid:name="android.hardware.camera"android:required="true" /><uses-permission android:name="android.permission.CAMERA" />
在Android 6.0以上需要动态申请权限
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA},REQUEST_CAMERA_PERMISSION);
打开相机
支持传入id
Camera.open() //默认后置摄像头
Camera.open(int id)
创建预览
private SurfaceView mSurfaceView;
private SurfaceHolder mSurfaceHolder;
...
mSurfaceHolder = mSurfaceView.getHolder();
mSurfaceHolder.addCallback(new SurfaceHolder.Callback() {@Overridepublic void surfaceCreated(SurfaceHolder holder) {...startPreview();}@Overridepublic void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {}@Overridepublic void surfaceDestroyed(SurfaceHolder holder) {releaseCamera();}
});
...
private void startPreview() {try {//设置实时预览mCamera.setPreviewDisplay(mSurfaceHolder);//OrientationsetCameraDisplayOrientation();//开始预览mCamera.startPreview();……} catch (Exception e) {e.printStackTrace();}
}
在设置预览时候可以通过 setPreviewCallback( Camera.PreviewCallback
)监听预览数据的数据
设置相机参数
Camera.Parameters
//获取Parameters对象
mParameters = camera?.parameters
设置相机相关参数内容非常丰富,这里介绍几个常用的
- setFocusMode 设置对焦模式
- setPreviewSize 设置预览图片大小 (相机支持不同的预览尺寸,比如横竖屏需要计算预览尺寸d可以看下谷歌的CameraView中的CmaeraView1#chooseOptimalSize处理)
- setPreviewFormat 设置预览格式 默认返回NV21
- setPictureSize 设置保存图片的大小
- setPictureFormat 设置保存图片的格式
- setDisplayOrientation 设置相机预览画面旋转的角度,degree取值 0,90,180,270,这里详细可以参考 oritation 或者 Android相机开发中尺寸和方向问题
- setPreviewDisplay 设置实时预览 SurfaceHolder
- setPreviewCallback 监听相机预览数据回调
拍照
mCamera.takePicture
private void takePicture() {if (null != mCamera) {mCamera.takePicture(new Camera.ShutterCallback() {@Overridepublic void onShutter() {//按下快门后的回调}}, new Camera.PictureCallback() {@Overridepublic void onPictureTaken(byte[] data, Camera camera) {//回调没压缩的 base data}}, new Camera.PictureCallback() {@Overridepublic void onPictureTaken(final byte[] data, Camera camera) {mCamera.startPreview();//save data}});}
}
释放相机资源
mCamera.stopPreview
mCamera.release
private void releaseCamera() {if (null != mCamera) {mCamera.stopPreview();mCamera.stopFaceDetection();mCamera.setPreviewCallback(null);mCamera.release();mCamera = null;}
}
Camera2
从 Android 5.0 开始,Google 引入了一套全新的相机框架 Camera2(android.hardware.camera2)并且废弃了旧的相机框架 Camera1(android.hardware.Camera) ,相较于Camera1,Camera2架构上发生了变化,主要是将相机设备模拟成一个管道,按照顺序处理每一帧的请求并返回给调用方,API上的使用难度,本节先介绍下Camera2在核心流程上的使用
\
由于Camera2架构设计成了管道,在拍照流程中细分出了通过3个类来协同
- CaptureRequest
相机捕获图像的设置请求,包含传感器,镜头,闪光灯等
- CaptureRequest.Builder
CaptureRequest的构造器,使用Builder模式,设置更加方便
- CameraCaptureSession
请求抓取相机图像帧的会话,会话的建立主要会建立起一个通道。一个CameraDevice一次只能开启一个CameraCaptureSession。
源端是相机,另一端是 Target,Target可以是Preview,也可以是ImageReader。
相机过程中处理数据也做了一些优化,抽象出了
- ImageReader
用于从相机打开的通道中读取需要的格式的原始图像数据,可以设置多个ImageReader。
- CameraCharacteristics
主要用于获取相机信息,内部携带大量的信息信息
- CameraDevice
相机设备类,和Camera1中的Camera同级
- CmaeraManager
相机系统服务,用于管理和连接相机设备
代码
\
拍摄流程重新抽象
- 创建一个用于从Pipeline获取图片的CaptureRequest
- 修改CaptureRequest的配置
- 创建两个不同尺寸的Surface用于接收图片数据,并且将他们添加到CaptureRequest中
- 发送配置好的CaptureRequest 到Pieline中等待结果
一个新的 CaptureRequest 会被放入一个被称作 Pending Request Queue 的队列中等待被执行,当 In-Flight Capture Queue 队列空闲的时候就会从 Pending Request Queue 获取若干个待处理的 CaptureRequest,并且根据每一个 CaptureRequest 的配置进行 Capture 操作。最后我们从不同尺寸的 Surface 中获取图片数据并且还会得到一个包含了很多与本次拍照相关的信息的 CaptureResult,流程结束
获取相机服务
CameraManager cameraManager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
根据相机ID获取相机信息 CameraCharateristics
private boolean chooseCameraIdByFacing() {try {int internalFacing = INTERNAL_FACINGS.get(mFacing);final String[] ids = mCameraManager.getCameraIdList();……for (String id : ids) {CameraCharacteristics characteristics = mCameraManager.getCameraCharacteristics(id);Integer level = characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);if (level == null ||level == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) {continue;}Integer internal = characteristics.get(CameraCharacteristics.LENS_FACING);if (internal == null) {throw new NullPointerException("Unexpected state: LENS_FACING null");}if (internal == internalFacing) {mCameraId = id;mCameraCharacteristics = characteristics;return true;}}// Not found……// The operation can reach here when the only camera device is an external one.// We treat it as facing back.mFacing = Constants.FACING_BACK;return true;} catch (CameraAccessException e) {throw new RuntimeException("Failed to get a list of camera devices", e);}}
初始化ImageReader
ImageReader是获取图像数据的重要途径,通过它可以获取到不同格式的图像数据,例如JPEG、YUV、RAW等。通过ImageReader.newInstance(int width, int height, int format, int maxImages)创建ImageReader对象,有4个参数:
- width:图像数据的宽度
- height:图像数据的高度
- format:图像数据的格式,例如ImageFormat.JPEG,ImageFormat.YUV_420_888等
- maxImages:最大Image个数,Image对象池的大小,指定了能从ImageReader获取Image对象的最大值,过多获取缓冲区可能导致OOM,所以最好按照最少的需要去设置这个值
ImageReader其他相关的方法和回调:
- ImageReader.OnImageAvailableListener:有新图像数据的回调
- acquireLatestImage():从ImageReader的队列里面,获取最新的Image,删除旧的,如果没有可用的Image,返回null
- acquireNextImage():获取下一个最新的可用Image,没有则返回null
- close():释放与此ImageReader关联的所有资源
- getSurface():获取为当前ImageReader生成Image的Surface
private void prepareImageReader() {if (mImageReader != null) {mImageReader.close();}Size largest = mPictureSizes.sizes(mAspectRatio).last();mImageReader = ImageReader.newInstance(largest.getWidth(), largest.getHeight(),ImageFormat.JPEG, /* maxImages */ 2);mImageReader.setOnImageAvailableListener(mOnImageAvailableListener, null);
}private final ImageReader.OnImageAvailableListener mOnImageAvailableListener= new ImageReader.OnImageAvailableListener() {@Overridepublic void onImageAvailable(ImageReader reader) {try (Image image = reader.acquireNextImage()) {Image.Plane[] planes = image.getPlanes();if (planes.length > 0) {ByteBuffer buffer = planes[0].getBuffer();byte[] data = new byte[buffer.remaining()];buffer.get(data);mCallback.onPictureTaken(data);}}}};
打开相机设备
cameraManager.openCamera(@NonNull String cameraId,@NonNull final CameraDevice.StateCallback callback, @Nullable Handler handler)的三个参数:
- cameraId:摄像头的唯一标识
- callback:设备连接状态变化的回调
- handler:回调执行的Handler对象,传入null则使用当前的主线程Handler
mCameraManager.openCamera(mCameraId, mCameraDeviceCallback, null);private final CameraDevice.StateCallback mCameraDeviceCallback= new CameraDevice.StateCallback() {@Overridepublic void onOpened(@NonNull CameraDevice camera) {//表示相机打开成功,可以真正开始使用相机,创建Capture会话mCamera = camera;mCallback.onCameraOpened();//创建Capture会话startCaptureSession();}@Overridepublic void onClosed(@NonNull CameraDevice camera) {//调用Camera.close()后的回调方法mCallback.onCameraClosed();}@Overridepublic void onDisconnected(@NonNull CameraDevice camera) {//当相机断开连接时回调该方法,需要进行释放相机的操作mCamera = null;}@Overridepublic void onError(@NonNull CameraDevice camera, int error) {//当相机打开失败时,需要进行释放相机的操作Log.e(TAG, "onError: " + camera.getId() + " (" + error + ")");mCamera = null;}};
创建 CaptureRequest及其target配置
Camera2是通过管道链接 request+target建立会话,首先我们得通过CaptureRequest.Builder配置好
使用 TEMPLATE_STILL_CAPTURE 模板创建一个用于拍照的 CaptureRequest.Builder 对象,并且添加拍照的 Surface 和预览的 Surface 到其中:
captureImageRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE)
captureImageRequestBuilder.addTarget(previewDataSurface)
captureImageRequestBuilder.addTarget(jpegSurface)
通过CameraDevice.createCaptureRequest()创建CaptureRequest.Builder对象,传入一个templateType参数,templateType用于指定使用何种模板创建CaptureRequest.Builder对象,templateType的取值:
- TEMPLATE_PREVIEW:预览模式
- TEMPLATE_STILL_CAPTURE:拍照模式
- TEMPLATE_RECORD:视频录制模式
- TEMPLATE_VIDEO_SNAPSHOT:视频截图模式
- TEMPLATE_MANUAL:手动配置参数模式
除了模式的配置,CaptureRequest还可以配置很多其他信息,例如图像格式、图像分辨率、传感器控制、闪光灯控制、3A(自动对焦-AF、自动曝光-AE和自动白平衡-AWB)控制等。在createCaptureSession的回调中可以进行设置
// Auto focus should be continuous for camera preview.
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
// Flash is automatically enabled when necessary.
setAutoFlash(mPreviewRequestBuilder);// Finally, we start displaying the camera preview.
mPreviewRequest = mPreviewRequestBuilder.build();
拍照和预览
在Camera2中拍照和预览是解耦开的,有了CaptureRequest之后,需要借助CaptureSession(Capture会话)来描述
mCameraDevice.createCaptureSession()创建Capture会话,它接受了三个参数:
- outputs:用于接受图像数据的surface集合
- callback:用于监听 Session 状态的CameraCaptureSession.StateCallback对象
- handler:用于执行CameraCaptureSession.StateCallback的Handler对象,传入null则使用当前的主线程Handler
\
拍照
mCaptureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, mBackgroundHandler);
该方法也有三个参数,和mCaptureSession.setRepeatingRequest一样:
- request:CaptureRequest对象
- listener:监听Capture 状态的回调
- handler:用于执行CameraCaptureSession.CaptureCallback的Handler对象,传入null则使用当前的主线程Handler
这里设置了mCaptureCallback:
PictureCaptureCallback mCaptureCallback = new PictureCaptureCallback() {@Overridepublic void onPrecaptureRequired() {mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START);setState(STATE_PRECAPTURE);try {mCaptureSession.capture(mPreviewRequestBuilder.build(), this, null);mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE);} catch (CameraAccessException e) {Log.e(TAG, "Failed to run precapture sequence.", e);}}@Overridepublic void onReady() {captureStillPicture();}};
\
预览
Camera2中,通过连续重复的Capture实现预览功能,每次Capture会把预览画面显示到对应的Surface上。连续重复的Capture操作通过mCaptureSession.setRepeatingRequest(mPreviewRequest,mCaptureCallback, mBackgroundHandler)实现,该方法有三个参数:
- request:CaptureRequest对象
- listener:监听Capture 状态的回调
- handler:用于执行CameraCaptureSession.CaptureCallback的Handler对象,传入null则使用当前的主线程Handler
停止预览使用mCaptureSession.stopRepeating()方法。
\
释放相机资源
先后对CaptureSession,CameraDevice,ImageReader进行close操作,释放资源
void stop() {if (mCaptureSession != null) {mCaptureSession.close();mCaptureSession = null;}if (mCamera != null) {mCamera.close();mCamera = null;}if (mImageReader != null) {mImageReader.close();mImageReader = null;}
}
\
Cmaera2做了哪些优化
架构升级
参考 android 官网 https://source.android.com/devices/camera/camera3
架构上,Camera2 的 API 模型被设计成一个 Pipeline(管道),它按顺序处理每一帧的请求并返回请求结果给客户端。下面这张来自官方的图展示了 Pipeline 的工作流程
\
在CaptureRequest中设置不同的Surface用于接收不同的图片数据,最后从不同的Surface中获取到图片数据和包含拍照相关信息的CaptureResult
假设我们想要同时拍摄两张不同尺寸的图片,并且在拍摄的过程中闪光灯必须亮起来。整个拍摄流程如下:
- 创建一个用于从 Pipeline 获取图片的 CaptureRequest。
- 修改 CaptureRequest 的闪光灯配置,让闪光灯在拍照过程中亮起来。
- 创建两个不同尺寸的 Surface 用于接收图片数据,并且将它们添加到 CaptureRequest 中。
- 发送配置好的 CaptureRequest 到 Pipeline 中等待它返回拍照结果。
一个新的 CaptureRequest 会被放入一个被称作 Pending Request Queue 的队列中等待被执行,当 In-Flight Capture Queue 队列空闲的时候就会从 Pending Request Queue 获取若干个待处理的 CaptureRequest,并且根据每一个 CaptureRequest 的配置进行 Capture 操作。最后我们从不同尺寸的 Surface 中获取图片数据并且还会得到一个包含了很多与本次拍照相关的信息的 CaptureResult,流程结束。
其中Caputure有以下三种工作模式
- 单次模式 One-shot
指的是一次的Capture操作,例如设置闪光灯、对焦模式、拍一张照片,多个单次模式的Capture会进入队列按照顺序执行
- 多次模式 Burst
指的是连续多次执行指定的Capture操作,该模式执行期间不允许插入其他Capture操作,如连续拍摄100张照片,在这100张照片拍摄期间任何新的capture都会等待
- 重复模式 Repeating
指的是不断重复执行指定的Capture操作,当有其他模式的Capture提交会暂停改模式转而执行其他模式的Capture
\
Supported Hardware Level
相机功能的强大与否和硬件息息相关,不同厂商对 Camera2 的支持程度也不同,所以 Camera2 定义了一个叫做 Supported Hardware Level 的重要概念,其作用是将不同设备上的 Camera2 根据功能的支持情况划分成多个不同级别以便开发者能够大概了解当前设备上 Camera2 的支持情况。截止到 Android P 为止,从低到高一共有 LEGACY、LIMITED、FULL 和 LEVEL_3 四个级别:
- LEGACY:向后兼容的级别,处于该级别的设备意味着它只支持 Camera1 的功能,不具备任何 Camera2 高级特性。
- LIMITED:除了支持 Camera1 的基础功能之外,还支持部分 Camera2 高级特性的级别。
- FULL:支持所有 Camera2 的高级特性。
- LEVEL_3:新增更多 Camera2 高级特性,例如 YUV 数据的后处理等
\
新特性
- 支持在开启相机前检查相机信息
- 在不开启预览情况下拍照
- 一次拍摄多张不同格式和尺寸的图片
- 控制曝光时间
- 连拍
\
简化开发谷歌做的努力
- CameraView
- CameraX
其他优秀三方库
https://github.com/CameraKit/camerakit-android
https://github.com/natario1/CameraView
CameraView
主要是为了解决不同版本Camer使用兼容性问题
根据官方的说明:
API Level | Camera API | Preview View |
---|---|---|
9-13 | Camera1 | SurfaceView |
14-20 | Camera1 | TextureView |
21-23 | Camera2 | TextureView |
24 | Camera2 | SurfaceView |
- Camera 区分:Android5.0(21)以下使用 Camera1,以上使用 Camera2
- Preview View:Android6.0(23)以上使用SurfaceView(SurfaceView在Android7.0上增加了新特性(平移、旋转等)),这里应该是 Android7.0以上(>23)使用SurfaceView,其他都使用TextureView,最新的源码sdk最低版本要求14。
类图如下
通过桥接模式+ 适配器模式,抽象出相机操作的抽象类CameraViewImpl和预览抽象类PreviewImpl,业务侧只操作接口
public CameraView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);……// Internal setup// 1.创建预览视图final PreviewImpl preview = createPreviewImpl(context);mCallbacks = new CallbackBridge();// 2.根据 Android SDK 版本选择不同的 Cameraif (Build.VERSION.SDK_INT < 21) {mImpl = new Camera1(mCallbacks, preview);} else if (Build.VERSION.SDK_INT < 23) {mImpl = new Camera2(mCallbacks, preview, context);} else {mImpl = new Camera2Api23(mCallbacks, preview, context);}……// 设置相机 ID,如前置或者后置 setFacing(a.getInt(R.styleable.CameraView_facing, FACING_BACK));// 设置预览界面的比例,如 4:3 或者 16:9String aspectRatio = a.getString(R.styleable.CameraView_aspectRatio);if (aspectRatio != null) {setAspectRatio(AspectRatio.parse(aspectRatio));} else {setAspectRatio(Constants.DEFAULT_ASPECT_RATIO);}// 设置对焦方式setAutoFocus(a.getBoolean(R.styleable.CameraView_autoFocus, true));// 设置闪光灯setFlash(a.getInt(R.styleable.CameraView_flash, Constants.FLASH_AUTO));a.recycle();// Display orientation detector// 初始化显示设备(主要指手机屏幕)的旋转监听,主要用来设置相机的旋转方向mDisplayOrientationDetector = new DisplayOrientationDetector(context) {@Overridepublic void onDisplayOrientationChanged(int displayOrientation) {mImpl.setDisplayOrientation(displayOrientation);}};
}
createPreViewImpl实现
private PreviewImpl createPreviewImpl(Context context) {PreviewImpl preview;if (Build.VERSION.SDK_INT >= 23) {preview = new SurfaceViewPreview(context, this);} else {preview = new TextureViewPreview(context, this);}return preview;
}
使用起来也比较简单
<com.google.android.cameraview.CameraViewandroid:id="@+id/camera"android:layout_width="match_parent"android:layout_height="wrap_content"android:keepScreenOn="true"android:adjustViewBounds="true"app:autoFocus="true"app:aspectRatio="4:3"app:facing="back"app:flash="auto"/>
CameraX
CameraX 是一个 Jetpack 支持库,目的是简化Camera的开发工作,它是基于Camera2 API的基础,向后兼容至 Android 5.0(API 级别 21)。
它有以下几个特性:
- 易用性,只需要几行代码就可以实现预览和拍照
- 保持设备的一致性,在不同相机设备上,对宽高比、屏幕方向、旋转、预览大小和高分辨率图片大小,做到都可以正常使用
- 相机特性的扩展,增加人像、HDR、夜间模式和美颜等功能
- 具备生命周期的管理
具体使用可以参考官方网址使用文档
欢迎关注我的公众号,一起学习,共同提高~
Android Camera了解一下相关推荐
- android camera 降低帧率_Android性能问题分析之bugreport
Android手机性能问题一直是用户关注的重点,分析性能问题则成为工程师日常工作的一部分.根据问题的类型通常有适合的工具可供使用,比如systrace ,traceview,simpleperf等可视 ...
- Android Camera 通过V4L2与kernel driver的完整交互过程
Android Camera 通过V4L2与kernel driver的完整交互过程 之前在 Android Camera 的执行流程 http://blog.chinaunix.net/uid ...
- Android Camera设置setPreviewCallback实现onPreviewFrame接口实时截取每一帧视频流数据
1 概述 通过Android Camera拍摄预览中设置setPreviewCallback实现onPreviewFrame接口,实时截取每一帧视频流数据 2 知识点 ① Android Camera ...
- qcom Android Camera【转】
本文转载自:http://blog.csdn.net/Wilsonboliu/article/details/54949196 1.总体架构 Android Camera 框架从整体上看是一个 cli ...
- android camera之nv21旋转
android camera之nv21旋转 这周做的一个android的camera开发,需要获取到视频帧数据,并且需要是nv21格式的byte数组,并且视频帧的图像需要是正方向的.和android相 ...
- 【Android RTMP】Android Camera 视频数据采集预览 ( 图像传感器方向设置 | Camera 使用流程 | 动态权限申请 )
文章目录 安卓直播推流专栏博客总结 一. Camera 传感器方向简介 二. Camera 图像传感器横向显示数据 三. Camera 图像传感器纵向显示数据 四. 设置 Camera 预览数据方向 ...
- 【Android RTMP】Android Camera 视频数据采集预览 ( 视频采集相关概念 | 摄像头预览参数设置 | 摄像头预览数据回调接口 )
文章目录 安卓直播推流专栏博客总结 一. Android 端数据采集涉及到的相关概念 二. Camera 预览图像尺寸设置 三. 获取摄像头采集的数据格式 安卓直播推流专栏博客总结 Android R ...
- android camera(三):camera V4L2 FIMC
关键词: android camera CMM 模组 camera参数 CAMIF V4L2 平台信息: 内核: linux 系统: android 平台:S5PV310(samsung ...
- android camera(二):摄像头工作原理、s5PV310 摄像头接口(CAMIF)
关键词: android camera CMM 模组 camera参数 CAMIF 平台信息: 内核: linux 系统: android 平台:S5PV310(samsung exynos 42 ...
- Android Camera 系统架构源码分析
Android Camera 系统架构源码分析(1)---->Camera的初始化 Android Camera 系统架构源码分析(2)---->Camera的startPreview和s ...
最新文章
- 站在巨人的肩膀上“思考”问题,重在思考而不是拿来主义
- 【Java设计模式】单例模式
- codeforces 938D Buy a Ticket 有初值的Dijkstra、有趣的题目
- 腾讯,字节等大厂面试真题汇总,深夜思考
- mysql 表ful,你所不知的table is full那些事
- ubuntu 远程桌面
- itextPdf~将PDF页面大小转为A4格式
- cudnn.deterministic = True 固定随机种子
- 使用foreach循环遍历集合元素
- python plot map_python的colormap总结(matplotlib+ncl+气象家园调色盘)
- 使用邻接矩阵实现有向图最短路径Dijkstra算法
- Android 功耗(11)---Android 功耗分析之wakelock
- echarts 环形图中间添加html,echarts配置一个中间显示文字的环形图
- Spring Boot + Activiti 在浏览器显示工作流图
- 7个现象告诉你手游圈为什么会有寒冬
- C#语言-04.OOP基础
- CentOS更换阿里yum源
- linux中括号命令,Linux中的括号用法
- MySQL数据导出:ERROR 1 (HY000) 错误解决
- win7(32bit)下完整的搭建apache(2.2.x)+openssl(0.9.6-1.0.1升级)过程