在Android OpenGL基础(三、绘制Bitmap纹理)一文中,我们简单介绍了如何绘制如何把一张图片贴到四边形上。本文介绍如何用GLSurfaceView来实现预览相机。与单张图片纹理不同的地方在于,相机是一个内容不断变化的纹理。
首先,先简单介绍相机的几个常用方法:
如果APP需要使用相机,则需要在manifest.xml中声明:
<uses-permission android:name="android.permission.CAMERA" />
Android权限类型有两种:
// 方法1:在Activity中调用Activity提供的API
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
this.checkSelfPermission(Manifest.permission.CAMERA)
}
// 方法2:androidx提供的API
ContextCompat.checkSelfPermission(this,
Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED
请求相机权限的方法如下:
// 方法1::在Activity中调用Activity提供的API
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val permissions = arrayOf(Manifest.permission.CAMERA)
this.requestPermissions(permissions, PERMISSION_REQUEST_CODE)
}
// 方法2:androidx提供的API
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.CAMERA),1)
下面简单列举Camera几个常用的方法:
public class Camera1Utils {
private Camera camera;
/**
* 打开相机
**/
public void openCamera() {
// 打开相机
camera = Camera.open();
Camera.Parameters parameters = camera.getParameters();
// 自动对焦
parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
camera.setParameters(parameters);
// 开始相机预览
camera.startPreview();
}
public void stopCamera() {
if (camera != null) {
camera.stopPreview();
camera.release();
}
}
/**
* 用SurfaceHolder承接相机预览数据
**/
public void setPreviewDisplay(SurfaceHolder surfaceHolder) {
try {
// 把相机预览数据传给SurfaceHolder
camera.setPreviewDisplay(surfaceHolder);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 用SurfaceTexture承接相机预览数据
**/
public void setPreviewTexture(SurfaceTexture surfaceTexture){
try {
// 把相机预览数据传给SurfaceTexture
camera.setPreviewTexture(surfaceTexture);
} catch (Exception e) {
e.printStackTrace();
}
}
}
为了可以在屏幕上看到相机预览画面,在打开相机后,需要把相机数据传递给一个View进行显示。常用的方式是用SurfaceView来显示相机实时画面。
在SurfaceView创建成功后,可以将相机数据传递给SurfaceView的SurfaceHolder,来在SurfaceView中显示相机画面。
class CameraPreview(context: Context,=private val mCamera: Camera
) : SurfaceView(context), SurfaceHolder.Callback {
override fun surfaceCreated(holder: SurfaceHolder) {
// Surface创建成功后,把相机数据传递给SurfaceView的SurfaceHolder
mCamera.apply {
try {
setPreviewDisplay(holder)
startPreview()
} catch (e: IOException) {
Log.d(TAG, "Error setting camera preview: ${e.message}")
}
}
}
GLSurfaceView类提供了帮助管理 EGL 上下文、在线程间通信以及与 activity 生命周期交互的辅助程序类。GLSurfaceView本身无法和相机数据直接关联起来,需要通过SurfaceTexture。在打开相机后,可以把相机数据传递给SurfaceTexture,在SurfaceTexture中将相机纹理绘制到GLSurfaceView中。本文主要介绍这种方式,在第三节详细介绍。
用OpenGL实现相机预览,下面分为SurfaceTexture、GLSurfaceView、GLSurfaceView.Render、绘制相机纹理几部分来介绍。
SurfaceTexture用于在相机启动后,承接相机预览数据,常用方法如下:
public class SurfaceTexture {
/**
* 注册OnFrameAvailableListener回调;
* 当SurfaceTexture有新的数据可用时会回调OnFrameAvailableListener的onFrameAvailable方法
*/
public void setOnFrameAvailableListener(SurfaceTexture.OnFrameAvailableListener listener) {
setOnFrameAvailableListener(listener, null);
}
/**
* Update the texture image to the most recent frame from the image stream.
* 把SurfaceTexture中的数据更新为最新一次的数据
*/
public void updateTexImage() {
nativeUpdateTexImage();
}
}
在GLSurfaceView中,在SurfaceTexture中有新的数据(onFrameAvailable)时,调用自身的requestRender(),即可触发自身的重新渲染(onDrawFrame()方法):
class MyGLSurfaceView(context: Context?, attrs: AttributeSet?) : GLSurfaceView(context, attrs),
SurfaceTexture.OnFrameAvailableListener {
private val renderer: MyGLRenderer
init {
setEGLContextClientVersion(2)
renderer = MyGLRenderer(this)
setRenderer(renderer)
renderMode = RENDERMODE_WHEN_DIRTY
}
override fun onFrameAvailable(surfaceTexture: SurfaceTexture?) {
// renderMode设置为RENDERMODE_WHEN_DIRTY;
// 在相机把新的数据传给SurfaceTexture时会回调onFrameAvailable()方法
// 在onFrameAvailable()方法里调用requestRender()触发渲染更新Surface
requestRender()
}
}
GLSurfaceView.Renderer的工作比较简单,在onSurfaceCreated后启动相机,并把相机预览数据传递给SurfaceTexture,将SurfaceTexture的listener设置为GLSurfaceView,绘制的主要工作在我们自定义的CameraDrawer类中:
class MyGLRenderer(private val frameAvailableListener: SurfaceTexture.OnFrameAvailableListener)
: GLSurfaceView.Renderer {
private lateinit var cameraDrawer: CameraDrawer
private val cameraManager: Camera1Utils = Camera1Utils()
override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f)
cameraDrawer = CameraDrawer()
cameraDrawer.getSurfaceTexture().setOnFrameAvailableListener(frameAvailableListener)
cameraManager.openCamera()
cameraManager.setPreviewTexture(cameraDrawer.getSurfaceTexture())
}
override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
GLES20.glViewport(0, 0, width, height)
}
override fun onDrawFrame(gl: GL10?) {
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
cameraDrawer.getSurfaceTexture().updateTexImage()
cameraDrawer.draw()
}
}
下面看我们自定义的CameraDrawer类是如何完成相机预览画面的绘制的。
创建纹理仍然使用Android OpenGL基础(三、绘制Bitmap纹理)中,1.2小节提供的OpenGLUtils工具类,不同的是相机的纹理类型是GLES11Ext.GL_TEXTURE_EXTERNAL_OES:
val texture = OpenGLUtils.createTextures(
GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 1,
GLES20.GL_NEAREST, GLES20.GL_LINEAR,
GLES20.GL_CLAMP_TO_EDGE, GLES20.GL_CLAMP_TO_EDGE
)
相机纹理的GLSL代码与Android OpenGL基础(三、绘制Bitmap纹理)中2D图片纹理类似,不同点在于相机纹理需要声明uniform samplerExternalOES s_texture:
/**
* 顶点着色器代码
*/
private val vertexShaderCode = """
attribute vec4 vPosition;
attribute vec2 inputTextureCoordinate;
varying vec2 textureCoordinate;
void main(){
gl_Position = vPosition;
textureCoordinate = inputTextureCoordinate;}
"""
/**
* 片段着色器代码
*/
private val fragmentShaderCode = """
#extension GL_OES_EGL_image_external : require
precision mediump float;varying vec2 textureCoordinate;
uniform samplerExternalOES s_texture;
void main() { gl_FragColor = texture2D( s_texture, textureCoordinate );
}
"""
OpenGL预览相机画面其实就是将相机纹理绘制到一个四边形上,与Android OpenGL基础(三、绘制Bitmap纹理)中绘制2D图片纹理不同的地方在于,相机数据的起点是手机横屏时的左上角为(0,0)点,所以如果想要让相机的画面符合我们想要的竖屏预览,需要将顶点对应的纹理坐标设置为:
// 四边形顶点的坐标
private var squareCoords = floatArrayOf(
-1f, 1f, 0.0f, // top left
-1f, -1f, 0.0f, // bottom left
1f, -1f, 0.0f, // bottom right
1f, 1f, 0.0f // top right
)
// 顶点所对应的纹理坐标
private var textureVertices = floatArrayOf(
0f, 1f, // top left
1f, 1f, // bottom left
1f, 0f, // bottom right
0f, 0f // top right
)
在修改了以上代码后,实际的绘制方法draw()与Android OpenGL基础(三、绘制Bitmap纹理)中绘制2D纹理完全一致。完整代码如下:
class CameraDrawer {
/**
* 顶点着色器代码
*/
private val vertexShaderCode = """
attribute vec4 vPosition;
attribute vec2 inputTextureCoordinate;
varying vec2 textureCoordinate;
void main(){
gl_Position = vPosition;
textureCoordinate = inputTextureCoordinate;}
"""
/**
* 片段着色器代码
*/
private val fragmentShaderCode = """
#extension GL_OES_EGL_image_external : require
precision mediump float;varying vec2 textureCoordinate;
uniform samplerExternalOES s_texture;
void main() { gl_FragColor = texture2D( s_texture, textureCoordinate );
}
"""
/**
* 着色器程序ID引用
*/
private var mProgram = 0
/**
* 相机预览SurfaceTexture
*/
private var cameraSurfaceTexture: SurfaceTexture
// 四边形顶点的坐标
private var squareCoords = floatArrayOf(
-1f, 1f, 0.0f, // top left
-1f, -1f, 0.0f, // bottom left
1f, -1f, 0.0f, // bottom right
1f, 1f, 0.0f // top right
)
// 顶点所对应的纹理坐标
private var textureVertices = floatArrayOf(
0f, 1f, // top left
1f, 1f, // bottom left
1f, 0f, // bottom right
0f, 0f // top right
)
// 四个顶点的缓冲数组
private val vertexBuffer: FloatBuffer =
ByteBuffer.allocateDirect(squareCoords.size * 4).order(ByteOrder.nativeOrder())
.asFloatBuffer().apply {
put(squareCoords)
position(0)
}
// 四个顶点的绘制顺序数组
private val drawOrder = shortArrayOf(0, 1, 2, 0, 2, 3)
// 四个顶点绘制顺序数组的缓冲数组
private val drawListBuffer: ShortBuffer =
ByteBuffer.allocateDirect(drawOrder.size * 2).order(ByteOrder.nativeOrder())
.asShortBuffer().apply {
put(drawOrder)
position(0)
}
// 四个顶点的纹理坐标缓冲数组
private val textureVerticesBuffer: FloatBuffer =
ByteBuffer.allocateDirect(textureVertices.size * 4).order(ByteOrder.nativeOrder())
.asFloatBuffer().apply {
put(textureVertices)
position(0)
}
private var textureID = 0
// 每个顶点的坐标数
private val COORDS_PER_VERTEX = 3
// 每个纹理顶点的坐标数
private val COORDS_PER_TEXTURE_VERTEX = 2
private val vertexStride: Int = COORDS_PER_VERTEX * 4
private val textVertexStride: Int = COORDS_PER_TEXTURE_VERTEX * 4
init {
// 编译顶点着色器和片段着色器
val vertexShader: Int = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode)
val fragmentShader: Int = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode)
// glCreateProgram函数创建一个着色器程序,并返回新创建程序对象的ID引用
mProgram = GLES20.glCreateProgram().also {
// 把顶点着色器添加到程序对象
GLES20.glAttachShader(it, vertexShader)
// 把片段着色器添加到程序对象
GLES20.glAttachShader(it, fragmentShader)
// 连接并创建一个可执行的OpenGL ES程序对象
GLES20.glLinkProgram(it)
}
val texture = OpenGLUtils.createTextures(
GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 1,
GLES20.GL_NEAREST, GLES20.GL_LINEAR,
GLES20.GL_CLAMP_TO_EDGE, GLES20.GL_CLAMP_TO_EDGE
)
textureID = texture[0]
cameraSurfaceTexture = SurfaceTexture(textureID)
}
fun getSurfaceTexture(): SurfaceTexture {
return cameraSurfaceTexture
}
fun draw() {
// 激活着色器程序 Add program to OpenGL ES environment
GLES20.glUseProgram(mProgram)
// 获取顶点着色器中的vPosition变量(因为之前已经编译过着色器代码,所以可以从着色器程序中获取);用唯一ID表示
val position = GLES20.glGetAttribLocation(mProgram, "vPosition")
// 允许操作顶点对象position
GLES20.glEnableVertexAttribArray(position)
// 将顶点数据传递给position指向的vPosition变量;将顶点属性与顶点缓冲对象关联
GLES20.glVertexAttribPointer(
position, COORDS_PER_VERTEX, GLES20.GL_FLOAT,
false, vertexStride, vertexBuffer
)
// 激活textureID对应的纹理单元
GLES20.glActiveTexture(textureID)
// 绑定纹理
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureID)
// 获取顶点着色器中的inputTextureCoordinate变量(纹理坐标);用唯一ID表示
val textureCoordinate = GLES20.glGetAttribLocation(mProgram, "inputTextureCoordinate")
// 允许操作纹理坐标inputTextureCoordinate变量
GLES20.glEnableVertexAttribArray(textureCoordinate)
// 将纹理坐标数据传递给inputTextureCoordinate变量
GLES20.glVertexAttribPointer(
textureCoordinate, COORDS_PER_TEXTURE_VERTEX, GLES20.GL_FLOAT,
false, textVertexStride, textureVerticesBuffer
)
// 按drawListBuffer中指定的顺序绘制四边形
GLES20.glDrawElements(
GLES20.GL_TRIANGLE_STRIP, drawOrder.size,
GLES20.GL_UNSIGNED_SHORT, drawListBuffer
)
// 操作完后,取消允许操作顶点对象position
GLES20.glDisableVertexAttribArray(position)
GLES20.glDisableVertexAttribArray(textureCoordinate)
}
private fun loadShader(type: Int, shaderCode: String): Int {
// glCreateShader函数创建一个顶点着色器或者片段着色器,并返回新创建着色器的ID引用
val shader = GLES20.glCreateShader(type)
// 把着色器和代码关联,然后编译着色器
GLES20.glShaderSource(shader, shaderCode)
GLES20.glCompileShader(shader)
return shader
}
}
与Android OpenGL基础(四、图片后处理)中介绍的图片后处理类似,在相机的预览中,可以通过修改片段着色的代码实现相机滤镜。
/**
* 片段着色器代码
*/
private val fragmentShaderCode = """
#extension GL_OES_EGL_image_external : require
precision mediump float;varying vec2 textureCoordinate;
uniform samplerExternalOES s_texture;
void main() {
gl_FragColor = texture2D( s_texture, textureCoordinate );
float average = 0.2126 * gl_FragColor.r + 0.7152 * gl_FragColor.g + 0.0722 * gl_FragColor.b;
gl_FragColor = vec4(average, average, average, 1.0);
}
"""
/**
* 片段着色器代码
*/
private val fragmentShaderCode = """
#extension GL_OES_EGL_image_external : require
precision mediump float;varying vec2 textureCoordinate;
uniform samplerExternalOES s_texture;
const float offset = 1.0f / 300.0f;
void main() {
vec2 offsets[9];
offsets[0] = vec2(-offset, offset); // 左上
offsets[1] = vec2( 0.0f, offset); // 正上
offsets[2] = vec2( offset, offset); // 右上
offsets[3] = vec2(-offset, 0.0f); // 左
offsets[4] = vec2( 0.0f, 0.0f); // 中
offsets[5] = vec2( offset, 0.0f); // 右
offsets[6] = vec2(-offset, -offset); // 左下
offsets[7] = vec2( 0.0f, -offset); // 正下
offsets[8] = vec2( offset, -offset); // 右下
// 核函数
float kernel[9];
kernel[0] = 1.0f;
kernel[1] = 1.0f;
kernel[2] = 1.0f;
kernel[3] = 1.0f;
kernel[4] = -8.0f;
kernel[5] = 1.0f;
kernel[6] = 1.0f;
kernel[7] = 1.0f;
kernel[8] = 1.0f;
// 计算采样值
vec3 sampleTex[9];
for(int i = 0; i < 9; i++)
{
sampleTex[i] = vec3(texture2D(s_texture, textureCoordinate.xy + offsets[i]));
}r
vec3 col = vec3(0.0);
for(int i = 0; i < 9; i++)
col += sampleTex[i] * kernel[i];
gl_FragColor = vec4(col, 1.0);
}
"""
其他类型的滤镜也是类似地修改片段着色器代码即可,下面给出了相机原画面以及几种其他滤镜的效果:

欢迎关注我,一起解锁更多技能:BC的掘金主页~💐 BC的CSDN主页~💐💐

Android OpenGL开发者文档:https://developer.android.com/guide/topics/graphics/opengl
opengl学习资料:https://learnopengl-cn.github.io/
相机开发文档:https://developer.android.com/guide/topics/media/camera
surfaceview开发者文档:https://source.android.com/devices/graphics/arch-sv-glsv?hl=zh-cn
滤镜开源库GPUImage:https://github.com/cats-oss/android-gpuimage
Android OpenGL基础(一、绘制三角形四边形):https://juejin.cn/post/7076751737461145630
Android OpenGL基础(二、坐标系统):https://juejin.cn/post/7077132016759603208/
Android OpenGL基础(三、绘制Bitmap纹理):https://juejin.cn/post/7079678062849163277/
Android OpenGL基础(四、图片后处理):https://juejin.cn/post/7080732149388673060
Android OpenGL基础专栏:https://juejin.cn/column/7076751675595653150
1.postman介绍Postman一款非常流行的API调试工具。其实,开发人员用的更多。因为测试人员做接口测试会有更多选择,例如Jmeter、soapUI等。不过,对于开发过程中去调试接口,Postman确实足够的简单方便,而且功能强大。2.下载安装官网地址:https://www.postman.com/下载完成后双击安装吧,安装过程极其简单,无需任何操作3.使用教程这里以百度为例,工具使用简单,填写URL地址即可发送请求,在下方查看响应结果和响应状态码常用方法都有支持请求方法:getpostputdeleteGet、Post、Put与Delete的作用get:请求方法一般是用于数据查询,
Ⅰ软件测试基础一、软件测试基础理论1、软件测试的必要性所有的产品或者服务上线都需要测试2、测试的发展过程3、什么是软件测试找bug,发现缺陷4、测试的定义使用人工或自动的手段来运行或者测试某个系统的过程。目的在于检测它是否满足规定的需求。弄清预期结果和实际结果的差别。5、测试的目的以最小的人力、物力和时间找出软件中潜在的错误和缺陷6、测试的原则28原则:20%的主要功能要重点测(eg:支付宝的支付功能,其他功能都是次要的)80%的错误存在于20%的代码中7、测试标准8、测试的基本要求功能测试性能测试安全性测试兼容性测试易用性测试外观界面测试可靠性测试二、质量模型衡量一个优秀软件的维度①功能性功
ES一、简介1、ElasticStackES技术栈:ElasticSearch:存数据+搜索;QL;Kibana:Web可视化平台,分析。LogStash:日志收集,Log4j:产生日志;log.info(xxx)。。。。使用场景:metrics:指标监控…2、基本概念Index(索引)动词:保存(插入)名词:类似MySQL数据库,给数据Type(类型)已废弃,以前类似MySQL的表现在用索引对数据分类Document(文档)真正要保存的一个JSON数据{name:"tcx"}二、入门实战{"name":"DESKTOP-1TSVGKG","cluster_name":"elasticsear
📢博客主页:https://blog.csdn.net/weixin_43197380📢欢迎点赞👍收藏⭐留言📝如有错误敬请指正!📢本文由Loewen丶原创,首发于CSDN,转载注明出处🙉📢现在的付出,都会是一种沉淀,只为让你成为更好的人✨文章预览:一.分辨率(Resolution)1、工业相机的分辨率是如何定义的?2、工业相机的分辨率是如何选择的?二.精度(Accuracy)1、像素精度(PixelAccuracy)2、定位精度和重复定位精度(RepeatPrecision)三.公差(Tolerance)四.课后作业(Post-ClassExercises)视觉行业的初学者,甚至是做了1~2年
(本文是网络的宏观的概念铺垫)目录计算机网络背景网络发展认识"协议"网络协议初识协议分层OSI七层模型TCP/IP五层(或四层)模型报头以太网碰撞路由器IP地址和MAC地址IP地址与MAC地址总结IP地址MAC地址计算机网络背景网络发展 是最开始先有的计算机,计算机后来因为多项技术的水平升高,逐渐的计算机变的小型化、高效化。后来因为计算机其本身的计算能力比较的快速:独立模式:计算机之间相互独立。 如:有三个人,每个人做的不同的事物,但是是需要协作的完成。 而这三个人所做的事是需要进行协作的,然而刚开始因为每一台计算机之间都是互相独立的。所以前面的人处理完了就需要将数据
我在我的机器上安装了ruby版本1.9.3,并且正在为我的个人网站开发一个octopress项目。我为我的gems使用了rvm,并遵循了octopress.org记录的所有步骤。但是我在我的rake服务器中发现了一些错误。这是我的命令日志。Tin-Aung-Linn:octopresstal$ruby--versionruby1.9.3p448(2013-06-27revision41675)[x86_64-darwin12.4.0]Tin-Aung-Linn:octopresstal$rakegenerate##GeneratingSitewithJekyllidenticals
文章目录概念索引相关操作创建索引更新副本查看索引删除索引索引的打开与关闭收缩索引索引别名查询索引别名文档相关操作新建文档查询文档更新文档删除文档映射相关操作查询文档映射创建静态映射创建索引并添加映射概念es中有三个概念要清楚,分别为索引、映射和文档(不用死记硬背,大概有个印象就可以)索引可理解为MySQL数据库;映射可理解为MySQL的表结构;文档可理解为MySQL表中的每行数据静态映射和动态映射上面已经介绍了,映射可理解为MySQL的表结构,在MySQL中,向表中插入数据是需要先创建表结构的;但在es中不必这样,可以直接插入文档,es可以根据插入的文档(数据),动态的创建映射(表结构),这就
目录1关系运算符2运算符优先级3关系表达式的书写代码实例:下面是面试中可能遇到的问题:1关系运算符C++中有6个关系运算符,用于比较两个值的大小关系,它们分别是:运算符描述==等于!=不等于小于>大于小于等于>=大于等于这些运算符返回一个布尔值,即true或false。例如,当x等于y时,x==y的结果为true,否则结果为false。2运算符优先级在C++中,关系运算符的优先级高于赋值运算符,但低于算术运算符。以下是关系运算符的优先级,从高到低排列:运算符描述>,,>=,关系运算符==,!=相等性运算符&&逻辑与`如果在表达式中有多个运算符,则按照优先级顺序依次进行运算。3关系表达式的书写在
一.计算机组成原理 这本书利用组合逻辑、同步时序逻辑电路设计的相关知识,从逻辑门开始逐步构建运算器、存储器、数据通路和控制器,最终集成为完整的CU原型系统,使读者从设计者的角度理解计算机部件构成及运行的基本原理,掌握软硬件协同的概念。 全书共9章,主要内容包括计算机系统概述、数据信息的表示、运算方法与运算器、存储系统、指令系统、中央处理器、指令流水线、总线系统、输入输出系统。1.计算机系统概述1.1计算机发展历程 计算机是一种能够按照事先存储的程序,自动、高速、准确地对相关信息进行处理的电子设备。1946年2月,世界上第一台电子数字计算机ENIAC(ElectronicNum
其实现在基础的资料和视频到处都是,就是看你有没有认真的去找学习资源了,去哪里学习都是要看你个人靠谱不靠谱,再好的教程和老师,你自己学习不进去也是白搭在正式选择之前,大可以在各种学习网站里面找找学习资源先自己学习一下为什么选择学软件测试?同学们理由众多!大概分这几类:①不受开发语言、行业产品变化限制;②入门更简单,对零基础、女生都友好;③软件项目都需要测试人员,职业生涯稳;④学习周期短,但薪资并不低。要想“肩扛”一条线?需掌握三大技能:技能1:掌握测试流程,熟悉系统框架能提前与开发人员一起制定测试计划,通过测试左移,推动代码评审,代码审计,单元测试,自动化冒烟测试,来保证研发阶段的质量。技能2: