Android App规范处理中版本设置、发布模式、给数据集SQLite加密的讲解及使用(附源码 超详细必看)

news2025/2/28 5:13:55

运行有问题或需要源码请点赞关注收藏后评论区留言~~~

一、版本设置

每个App都有三个基础信息,第一个是App的图标,第二个是App的名称,第三个是App的版本号。

一旦安装了某个版本的App,那么之后只能安装版本更新的同名App,不能安装版本更低的App,

每次App升级重新导出APK的时候,versionCode与versionName都要一起更改,不能只改其中一个,而且两者升级后只能比原来大,不能比原来小,如果没有按照规范修改版本号就会出现问题

下面是获取App基础信息的例子 效果如下

可见我的图标也进行了修改 具体怎么修改可以参见我之前的博客

 代码如下

Java类

package com.example.chapter15;

import android.annotation.SuppressLint;
import android.os.Bundle;
import android.widget.ImageView;
import android.widget.TextView;

import androidx.appcompat.app.AppCompatActivity;

@SuppressLint("DefaultLocale")
public class AppVersionActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_app_version);
        ImageView iv_icon = findViewById(R.id.iv_icon);
        iv_icon.setImageResource(R.mipmap.ic_launcher); // 应用图标取自ic_launcher
        TextView tv_desc = findViewById(R.id.tv_desc);
        // 应用名称取自app_name,应用包名、版本号、版本名称均来自BuildConfig
        String desc = String.format("App名称为:%s\nApp包名为:%s\n" +
                        "App版本号为:%d\nApp版本名称为:%s",
                getString(R.string.app_name), BuildConfig.APPLICATION_ID,
                BuildConfig.VERSION_CODE, BuildConfig.VERSION_NAME);
        tv_desc.setText(desc);
    }

}

XML文件

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="5dp" >

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal" >

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:gravity="center"
            android:text="App图标为:"
            android:textColor="@color/black"
            android:textSize="17sp" />

        <ImageView
            android:id="@+id/iv_icon"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:scaleType="fitCenter"
            android:src="@mipmap/ic_launcher" />
    </LinearLayout>

    <TextView
        android:id="@+id/tv_desc"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:textColor="@color/black"
        android:textSize="17sp" />

</LinearLayout>

二、发布模式 

为了调试编码方便,开发者经常在代码里添加日志,还在页面上弹出各种提示,这样固然有利于发现BUG,但是调试信息过多往往容易泄漏敏感信息,从保密角度考虑,App在上线前必须去掉多余的调试信息,也就是生成发布模式的安装包 建立发布模式有以下两个优点

1:保护用户的敏感账户信息不被泄漏

2:保护业务逻辑与流程处理的交互数据不被泄漏

控制调试信息的工具类主要有两种,Log工具和Toast工具

1:日志Log 

Log工具用于打印调试日志,App运行过程中,日志信息会输出到logcat窗口,因为最终用户不关心App日志,所以除非特殊情况,发布上线的App应该屏蔽掉所有日志信息

2:提示Toast

Toast工具在界面下方弹出小窗,给用户一两句话的提示,小窗短暂停留一会然后消失,由于Toast窗口无交互动作,样式也基本固定,因此除了少数弹窗应该保留,其他弹窗应该在发布时屏蔽

效果如下

日志控制台输出如下

代码如下

Java类 

package com.example.chapter15;

import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;

import com.example.chapter15.util.LogUtil;

public class LogDebugActivity extends AppCompatActivity implements OnClickListener {
    private final static String TAG = "LogDebugActivity";
    private TextView tv_debug;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_log_debug);
        tv_debug = findViewById(R.id.tv_debug);
        findViewById(R.id.btn_debug).setOnClickListener(this);
        // 应用名称取自app_name,应用包名、版本号、版本名称均来自BuildConfig
        String desc = String.format("App调试标志为:%b", BuildConfig.DEBUG);
        tv_debug.setText(desc);
    }

    @Override
    public void onClick(View v) {
        if (v.getId() == R.id.btn_debug) {
            Toast.makeText(this, "已点击按钮,请注意观察日志", Toast.LENGTH_SHORT).show();
            LogUtil.d(TAG, "您点击了测试按钮,只有在调试模式之下才能看到本日志");
            Log.d(TAG, "这条日志无论是否调试模式都能看到");
        }
    }

}

XML文件 

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="5dp" >

    <TextView
        android:id="@+id/tv_debug"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:text="App调试标志为:"
        android:textColor="@color/black"
        android:textSize="17sp" />

    <Button
        android:id="@+id/btn_debug"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:text="点击按钮查看日志输出"
        android:textColor="@color/black"
        android:textSize="17sp" />

</LinearLayout>

 三、给数据库SQLite加密

App的业务数据大多保存在SQLite数据库中,但是该数据库的保密性不太强,很容易一被盗取数据,为了增强SQLite的安保措施,可以考虑以下两种技术方案

1:对于写操作,先把数据加密,再把加密后的数据写入数据库,读操作则先解密再处理,但是如此业务量巨大

2:加密整个数据库,此时要用到第三方的加密开源库,比如常见的开源框架SQLCipher,该方案封装了加密算法,性能高且使用方便,便于开发者迅速切换加密数据库

App中引入SQLCipher步骤如下

1:打开模块的build.gradle 引入如下代码

implementation'net.zetetic:android-database-sqlcipher:4.4.0'

2:把代码中的SQLite相关类路径更换为SQLCipher对应类的路径

3:初始化SQLCipher的依赖库

4:在读写数据库的时候传入密匙

效果如下 可以实现数据库的读写操作

 

 

 

 代码如下

Java类

package com.example.chapter15;

import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.EditText;

import androidx.appcompat.app.AppCompatActivity;

import com.example.chapter15.bean.UserInfo;
import com.example.chapter15.database.UserDBHelper;
import com.example.chapter15.util.DateUtil;
import com.example.chapter15.util.ToastUtil;

public class SQLiteWriteActivity extends AppCompatActivity implements OnClickListener, CompoundButton.OnCheckedChangeListener {
    private UserDBHelper mHelper; // 声明一个用户数据库帮助器的对象
    private EditText et_name;
    private EditText et_age;
    private EditText et_height;
    private EditText et_weight;
    private boolean bMarried = false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_sqlite_write);
        et_name = findViewById(R.id.et_name);
        et_age = findViewById(R.id.et_age);
        et_height = findViewById(R.id.et_height);
        et_weight = findViewById(R.id.et_weight);
        CheckBox ck_married = findViewById(R.id.ck_married);
        ck_married.setOnCheckedChangeListener(this);
        findViewById(R.id.btn_save).setOnClickListener(this);
    }

    @Override
    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
        bMarried = isChecked;
    }

    @Override
    protected void onStart() {
        super.onStart();
        // 获得数据库帮助器的实例
        mHelper = UserDBHelper.getInstance(this, 1);
        mHelper.openWriteLink(); // 打开数据库帮助器的写连接
    }

    @Override
    protected void onStop() {
        super.onStop();
        mHelper.closeLink(); // 关闭数据库连接
    }

    @Override
    public void onClick(View v) {
        if (v.getId() == R.id.btn_save) {
            String name = et_name.getText().toString();
            String age = et_age.getText().toString();
            String height = et_height.getText().toString();
            String weight = et_weight.getText().toString();
            if (TextUtils.isEmpty(name)) {
                ToastUtil.show(this, "请先填写姓名");
                return;
            } else if (TextUtils.isEmpty(age)) {
                ToastUtil.show(this, "请先填写年龄");
                return;
            } else if (TextUtils.isEmpty(height)) {
                ToastUtil.show(this, "请先填写身高");
                return;
            } else if (TextUtils.isEmpty(weight)) {
                ToastUtil.show(this, "请先填写体重");
                return;
            }
            // 以下声明一个用户信息对象,并填写它的各字段值
            UserInfo info = new UserInfo();
            info.name = name;
            info.age = Integer.parseInt(age);
            info.height = Long.parseLong(height);
            info.weight = Float.parseFloat(weight);
            info.married = bMarried;
            info.update_time = DateUtil.getNowDateTime("yyyy-MM-dd HH:mm:ss");
            mHelper.insert(info); // 执行数据库帮助器的插入操作
            ToastUtil.show(this, "数据已写入SQLite数据库");
        }
    }

}

读数据库类

package com.example.chapter15;

import android.annotation.SuppressLint;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.TextView;

import androidx.appcompat.app.AppCompatActivity;

import com.example.chapter15.bean.UserInfo;
import com.example.chapter15.database.UserDBHelper;
import com.example.chapter15.util.ToastUtil;

import java.util.List;

@SuppressLint("DefaultLocale")
public class SQLiteReadActivity extends AppCompatActivity implements OnClickListener {
    private UserDBHelper mHelper; // 声明一个用户数据库帮助器的对象
    private TextView tv_sqlite;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_sqlite_read);
        tv_sqlite = findViewById(R.id.tv_sqlite);
        findViewById(R.id.btn_delete).setOnClickListener(this);
    }

    @Override
    protected void onStart() {
        super.onStart();
        // 获得数据库帮助器的实例
        mHelper = UserDBHelper.getInstance(this, 1);
        mHelper.openReadLink(); // 打开数据库帮助器的读连接
        readSQLite(); // 读取数据库中保存的所有用户记录
    }

    @Override
    protected void onStop() {
        super.onStop();
        mHelper.closeLink(); // 关闭数据库连接
    }

    // 读取数据库中保存的所有用户记录
    private void readSQLite() {
        if (mHelper == null) {
            ToastUtil.show(this, "数据库连接为空");
            return;
        }
        // 执行数据库帮助器的查询操作
        List<UserInfo> userList = mHelper.query("1=1");
        String desc = String.format("数据库查询到%d条记录,详情如下:", userList.size());
        for (int i = 0; i < userList.size(); i++) {
            UserInfo info = userList.get(i);
            desc = String.format("%s\n第%d条记录信息如下:", desc, i + 1);
            desc = String.format("%s\n 姓名为%s", desc, info.name);
            desc = String.format("%s\n 年龄为%d", desc, info.age);
            desc = String.format("%s\n 身高为%d", desc, info.height);
            desc = String.format("%s\n 体重为%f", desc, info.weight);
            desc = String.format("%s\n 婚否为%b", desc, info.married);
            desc = String.format("%s\n 更新时间为%s", desc, info.update_time);
        }
        if (userList.size() <= 0) {
            desc = "数据库查询到的记录为空";
        }
        tv_sqlite.setText(desc);
    }

    @Override
    public void onClick(View v) {
        if (v.getId() == R.id.btn_delete) {
            mHelper.closeLink(); // 关闭数据库连接
            mHelper.openWriteLink(); // 打开数据库帮助器的写连接
            mHelper.deleteAll(); // 删除所有记录
            mHelper.closeLink(); // 关闭数据库连接
            mHelper.openReadLink(); // 打开数据库帮助器的读连接
            readSQLite(); // 读取数据库中保存的所有用户记录
        }
    }

}

XML文件

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="5dp" >

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="40dp" >

        <TextView
            android:id="@+id/tv_name"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_alignParentLeft="true"
            android:gravity="center"
            android:text="姓名:"
            android:textColor="@color/black"
            android:textSize="17sp" />

        <EditText
            android:id="@+id/et_name"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginBottom="5dp"
            android:layout_marginTop="5dp"
            android:layout_toRightOf="@+id/tv_name"
            android:background="@drawable/editext_selector"
            android:gravity="left|center"
            android:hint="请输入姓名"
            android:inputType="text"
            android:maxLength="12"
            android:textColor="@color/black"
            android:textSize="17sp" />
    </RelativeLayout>

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="40dp" >

        <TextView
            android:id="@+id/tv_age"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_alignParentLeft="true"
            android:gravity="center"
            android:text="年龄:"
            android:textColor="@color/black"
            android:textSize="17sp" />

        <EditText
            android:id="@+id/et_age"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginBottom="5dp"
            android:layout_marginTop="5dp"
            android:layout_toRightOf="@+id/tv_age"
            android:background="@drawable/editext_selector"
            android:gravity="left|center"
            android:hint="请输入年龄"
            android:inputType="number"
            android:maxLength="2"
            android:textColor="@color/black"
            android:textSize="17sp" />
    </RelativeLayout>
    
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="40dp" >

        <TextView
            android:id="@+id/tv_height"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_alignParentLeft="true"
            android:gravity="center"
            android:text="身高:"
            android:textColor="@color/black"
            android:textSize="17sp" />

        <EditText
            android:id="@+id/et_height"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginBottom="5dp"
            android:layout_marginTop="5dp"
            android:layout_toRightOf="@+id/tv_height"
            android:background="@drawable/editext_selector"
            android:gravity="left|center"
            android:hint="请输入身高"
            android:inputType="number"
            android:maxLength="3"
            android:textColor="@color/black"
            android:textSize="17sp" />
    </RelativeLayout>
    
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="40dp" >

        <TextView
            android:id="@+id/tv_weight"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_alignParentLeft="true"
            android:gravity="center"
            android:text="体重:"
            android:textColor="@color/black"
            android:textSize="17sp" />

        <EditText
            android:id="@+id/et_weight"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginBottom="5dp"
            android:layout_marginTop="5dp"
            android:layout_toRightOf="@+id/tv_weight"
            android:background="@drawable/editext_selector"
            android:gravity="left|center"
            android:hint="请输入体重"
            android:inputType="numberDecimal"
            android:maxLength="5"
            android:textColor="@color/black"
            android:textSize="17sp" />
    </RelativeLayout>
    
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="40dp" >

        <CheckBox
            android:id="@+id/ck_married"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_alignParentLeft="true"
            android:gravity="center"
            android:checked="false"
            android:text="已婚"
            android:textColor="@color/black"
            android:textSize="17sp" />
    </RelativeLayout>
    
    <Button
        android:id="@+id/btn_save"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="保存到数据库"
        android:textColor="@color/black"
        android:textSize="17sp" />

</LinearLayout>

创作不易 觉得有帮助请点赞关注收藏~~~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/9547.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

[公派访问学者]申请条件及选拔方法

国家公派访问学者绝大多数是经过层层选拔的&#xff0c;那么具体有哪些条件呢?又是如何选拔的呢?知识人网访问学者申请咨询老师和大家分享以下这篇文章。 一、申请条件 1、高级研究学者 申请时年龄不超过55岁。教学科研人员应为教授、博士生导师。申请人须同时具备以下条件…

基础类型存放在栈上,引用类型存放在堆上,请问是为什么? 字符串是存放在栈上么?

基础类型存放在栈上&#xff0c;引用类型存放在堆上&#xff0c;请问是为什么&#xff1f; 记住一句话&#xff1a;能量是守衡的&#xff0c;无非是时间换空间&#xff0c;空间换时间的问题 堆比栈大&#xff0c;栈比堆的运算速度快&#xff0c;对象是一个复杂的结构&#xf…

苹果手机和电脑怎么录屏?详细教程来了!

​相信小伙伴身边有不少人使用的是苹果手机和电脑。安卓手机和windows电脑怎么录屏不少人都已经知道了&#xff0c;那么苹果手机和电脑怎么录屏 呢&#xff1f;现在&#xff0c;小编就来详细的教教大家如何录屏&#xff0c;快拿出小本本记下来哦&#xff01; 一&#xff0e;苹…

C++11标准模板(STL)- 算法(std::partition_copy)

定义于头文件 <algorithm> 算法库提供大量用途的函数&#xff08;例如查找、排序、计数、操作&#xff09;&#xff0c;它们在元素范围上操作。注意范围定义为 [first, last) &#xff0c;其中 last 指代要查询或修改的最后元素的后一个元素。 复制一个范围&#xff0c;…

扒去 Spring 事件监听机制的外衣,竟是观察者模式

Spring 中提供了一套默认的事件监听机制&#xff0c;在容器初始化时便使用了这套机制。同时&#xff0c;Spring 也提供了事件监听机制的接口扩展能力&#xff0c;开发者基于此可快速实现自定义的事件监听功能。Spring 的事件监听机制是在 JDK 事件监听的基础上进行的扩展&#…

基于结构应力方法的焊接结构疲劳评估及实例分析(上篇)

作者 | 裴宪军博士 &#xff0c;仿真秀专栏作者 一、写在文前 焊接技术作为现代制造业中的支柱技术之一&#xff0c;由于其整体性强、轻量化、经济性好等优点&#xff0c;焊接结构被广泛应用于轨道交通、航空航天&#xff0c;船舶、重型装备等领域&#xff0c;安全承载问题也…

八大排序总结篇

一、前言 到这里&#xff0c;数据结构的八大排序就算是全部写完了。这一期总结篇我们来测试一下八大排序的效率&#xff0c;印证一下八大排序的时间复杂度&#xff0c;以及深度剖析一下八大排序的稳定性问题。 二、八大排序 1、直接插入排序 http://t.csdn.cn/CdFFu 2、希尔排…

css:为什么我设置宽高百分比不生效

很多新手朋友写 css 的时候&#xff0c;有时发现设置宽高百分比有用&#xff0c;有时候又没用&#xff0c;到底怎么回事呢&#xff1f; 核心原则 设置百分比的时候&#xff0c;需要父元素有固定的高度&#xff0c;注意&#xff0c;这里说的只是需要父元素有固定的高度&#x…

机器学习极简入门笔记-4-有监督学习进阶-HMM

目录 15.1 基本概念 概率模型 生成模型与判别模型 概率图模型 马尔可夫链&#xff0c;马尔可夫随机场和 CRF 15.2 数学中的HMM HMM的两个基本假设 15.3 HMM的三个基本问题 概率计算问题 预测问题 学习问题 15.4 HMM3个基本问题的计算 概率计算问题 预测问题 学习…

【Spring源码】18. 属性填充:populateBean()详解

进入populateBean() 对bean的属性进行填充&#xff0c;将各个属性值注入&#xff08;存在其他bean的属性&#xff0c;则会递归初始化依赖的bean&#xff09; ​一开始会先对传入的参数进行判断&#xff08;如下图红框框中的逻辑&#xff09; 如果传入的BeanWrapper和RootBeanD…

如何免费将pdf转word?看完这篇你就会了

pdf是我们学习工作中&#xff0c;经常会接触到的一种文件格式。通常我们都会以这种pdf格式来传输文件&#xff0c;因为它可以确保在不同的设备上打开以及不会出现文件内容格式错乱的情况。可是当我们需要对它的内容进行修改时&#xff0c;就有些困难&#xff0c;需要先将pdf转换…

软件测试之缺陷书写规范

1、标题&#xff1a;应保持简短、准确、提供缺陷的本质信息。 -尽量以缺陷发生的原因与结果的方式相结合的放式书写; -尽量避免使用模糊不清的词语&#xff0c;例如&#xff1a;“功能中断”、“功能不正确”、“行为不起作用”等&#xff0c;应该使用具体文字说明缺陷的症状; …

flink学习之sql-client之踩坑记录

flink/bin目录下会看到这个脚本&#xff0c;最开始以为是和spark-shell差不多的。结果自行摸索无果&#xff0c;网上查的文章也写的很垃圾&#xff0c;自己查官网看下吧。 SQL 客户端 | Apache Flink 直接./sql-client.sh SELECT Hello World; 报错 org.apache.flink.runtim…

NFT 泡沫是否已经被挤破

Sep. 2022, Dan LeBaron Data Source: Footprint Analytics - NFT Volume in 2021 Vs. 2022 虽然NFT已经存在了几年&#xff0c;但在无聊猿 (BAYC)等大型项目启动的推动下&#xff0c;该技术在2021年爆发式地流行。 似乎是突然间&#xff0c;名人、运动员和主要的艺术收藏家都…

深度剖析 Python 日志重复打印问题

python 日志处理流程 使用 python 做日志输出时&#xff0c;首先我们需要一个创建一个 Logger 对象&#xff1a;import logging; logger logging.getLogger() 然后就可以用 logger.info/debug/error(msg) 来输出日志 如果只是单纯地打印日志&#xff0c;这样做和 print 没有任…

高压功率放大器的作用(功率放大器的应用领域是什么)

高压功率放大器的适用范围和应用领域是很多电子工程师所关心的&#xff0c;那么高压功率放大器的作用以及有哪些使用场景呢&#xff0c;下面就让安泰电子来为大家介绍。 高压功率放大器是电子实验室会频繁使用的测试仪器&#xff0c;是在实验中能够帮助输出信号达到最大输出功率…

【黄啊码】用PHP7性能居然是5.6的三倍?赶紧看看它有什么新特性-续

大家好&#xff0c;我是黄啊码&#xff0c;上节课的东西学完了吧&#xff1f;脑瓜子嗡嗡的吧&#xff1f;来&#xff0c;继续&#xff0c;让脑瓜子一次性嗡个够&#xff0c;压力大&#xff0c;才有动力。 目录 PHP CSPRNG PHP 7 use 语句 PHP 7 错误处理 PHP intdiv() 函…

实验28:步进电机实验

OK,我是走程序猿的道路 我的blog侧重点在讲解代码 本实验结果: 步进电机正转 步进电机反转 步进电机工作原理我就不去讨论了 重点在于代码分析和讲解 01 硬件电路设计 硬件电路总图 接口: 步进电机驱动器板和Arduino Uno板之间的接线: 步进电机驱动器 Arduino Uno…

opencv之 drawContours() 函数说明应用

drawContours 之前使用mask图还进行了连通域有无status分析&#xff0c;然后才进行的绘制。 今天发现直接使用mask图进行绘制&#xff0c;然后通过设置drawContours的参数可以进行不同层次上缺陷的绘制&#xff0c;然后通过这个事情也说明&#xff0c;有问题可以直接找opencv官…

“综合”web项目编写------手把手0基础教学(一)

我们平常看到的项目代码一般都是分段单独的功能&#xff0c;但如何将功能汇总成一个完整的项目呢&#xff0c;下面我将利用IDEA来介绍一个基础的综合web项目 目录 一.创建项目 二.为项目建包 1.了解构建项目的思路 &#xff08;1&#xff09;构建模型&#xff08;模型包括数…