安卓MediaRecorder(2)录制源码分析

news2024/11/20 9:45:54

文章目录

    • 前言
    • JAVA new MediaRecorder() 源码分析
      • android_media_MediaRecorder.cpp native_init()
      • MediaRecorder.java postEventFromNative
      • android_media_MediaRecorder.cpp native_setup()
    • MediaRecorder 参数设置
    • MediaRecorder.prepare 分析
    • MediaRecorder.start 分析
    • MediaRecorder.stop 分析
    • 结语

本文首发地址 https://blog.csdn.net/CSqingchen/article/details/134634628
最新更新地址 https://gitee.com/chenjim/chenjimblog

前言

通过前文 安卓MediaRecorder(1)录制音频的详细使用,我们已经知道如何使用。
本文主要分析一下 Framework 中相关流程。
下图是谷歌提供的MediaRecorder状态关系图
在这里插入图片描述

JAVA new MediaRecorder() 源码分析

public class MediaRecorder implements AudioRouting,AudioRecordingMonitor,
        AudioRecordingMonitorClient,MicrophoneDirection {

    static {
        // 静态代码块,加载链接 liblibmedia_jni.so
        System.loadLibrary("media_jni");
        native_init();
    }

    // 已经废弃 
    public MediaRecorder() {
        // 传入 APP Context
        this(ActivityThread.currentApplication());
    }

    public MediaRecorder(@NonNull Context context) {
        // 要求 Context 不为空
        Objects.requireNonNull(context);

        // 创建EventHandler,主要用于JNI层回调时切换到当前App端线程中
        Looper looper;
        if ((looper = Looper.myLooper()) != null) {
            // 如果当前线程有Looper,就使用当前线程的Looper
            mEventHandler = new EventHandler(this, looper);
        } else if ((looper = Looper.getMainLooper()) != null) {
            // 使用主线程的 Looper,Jni回调信息会在主线程执行   
            mEventHandler = new EventHandler(this, looper);
        } else {
            mEventHandler = null;
        }

        // 录制音频的声道数,此处默认 1 (mono即单声道),2 (stereo即双声道立体声),可以通过 setAudioChannels 修改
        mChannelCount = 1;

        // 创建弱引用,并初始化
        try (ScopedParcelState attributionSourceState = context.getAttributionSource().asScopedParcelState()) {
            native_setup(new WeakReference<>(this), ActivityThread.currentPackageName(),
                    attributionSourceState.getParcel());
        }
    }

android_media_MediaRecorder.cpp native_init()

获取JAVA层 android.media.MediaRecorder 对象
并将 JAVA 对象的属性 mNativeContext、mSurface、postEventFromNative 保存在 fields
详细代码如下

static void
android_media_MediaRecorder_native_init(JNIEnv *env)
{
    jclass clazz;
   
    clazz = env->FindClass("android/media/MediaRecorder");
    if (clazz == NULL) {
        return;
    }

    fields.context = env->GetFieldID(clazz, "mNativeContext", "J");
    if (fields.context == NULL) {
        return;
    }

    fields.surface = env->GetFieldID(clazz, "mSurface", "Landroid/view/Surface;");
    if (fields.surface == NULL) {
        return;
    }

    jclass surface = env->FindClass("android/view/Surface");
    if (surface == NULL) {
        return;
    }

    fields.post_event = env->GetStaticMethodID(clazz, "postEventFromNative",
                                               "(Ljava/lang/Object;IIILjava/lang/Object;)V");
    if (fields.post_event == NULL) {
        return;
    }

    clazz = env->FindClass("java/util/ArrayList");
    if (clazz == NULL) {
        return;
    }
    gArrayListFields.add = env->GetMethodID(clazz, "add", "(Ljava/lang/Object;)Z");
    gArrayListFields.classId = static_cast<jclass>(env->NewGlobalRef(clazz));
}

MediaRecorder.java postEventFromNative

这个是Jni消息回来的接口,最终会发到 MediaRecorder.EventHandler 的 handleMessage 中
进而可以通过 MediaRecorder.OnInfoListener 、MediaRecorder.OnErrorListener、
AudioRouting.OnRoutingChangedListener 回调到 APP
对应源码如下

private static void postEventFromNative(Object mediarecorder_ref,
                                        int what, int arg1, int arg2, Object obj) {
    MediaRecorder mr = (MediaRecorder)((WeakReference)mediarecorder_ref).get();
    if (mr == null) {
        return;
    }

    if (mr.mEventHandler != null) {
        Message m = mr.mEventHandler.obtainMessage(what, arg1, arg2, obj);
        mr.mEventHandler.sendMessage(m);
    }
}

// EventHandler 如下  
public class MediaRecorder {
    ...
    private class EventHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
           switch(msg.what) {
            case MEDIA_RECORDER_EVENT_ERROR:
            case MEDIA_RECORDER_TRACK_EVENT_ERROR:
                if (mOnErrorListener != null)
                    mOnErrorListener.onError(mMediaRecorder, msg.arg1, msg.arg2);
                return;
            case MEDIA_RECORDER_EVENT_INFO:
            case MEDIA_RECORDER_TRACK_EVENT_INFO:
                if (mOnInfoListener != null)
                    mOnInfoListener.onInfo(mMediaRecorder, msg.arg1, msg.arg2);
                return;
            case MEDIA_RECORDER_AUDIO_ROUTING_CHANGED:
                // 耳机使能的消息  
                return;
        }
    }

}

android_media_MediaRecorder.cpp native_setup()

static void
android_media_MediaRecorder_native_setup(JNIEnv *env, jobject thiz, jobject weak_this,
                                         jstring packageName, jobject jAttributionSource)
{
    // 构建 JNI 对象 MediaRecorder,attributionSource 可以认为是 JNI 中的上下文 Context 
    sp<MediaRecorder> mr = new MediaRecorder(attributionSource);
    ...

    // 创建 JNI JNIMediaRecorderListener , 其收到的消息最终通过 fields.post_event 回到JAVA
    sp<JNIMediaRecorderListener> listener = new JNIMediaRecorderListener(env, thiz, weak_this);
    mr->setListener(listener);
    ...
    // 传递客户端包名,以进行权限跟踪  
    mr->setClientName(clientName);

    // 将创建的 mr 保存到 fields.context,也就是 Java 层 MediaRecorder 中的 mNativeContext  
    setMediaRecorder(env, thiz, mr);
}

MediaRecorder JNI 构造

// frameworks/av/media/libmedia/mediarecorder.cpp
MediaRecorder::MediaRecorder(const AttributionSourceState &attributionSource): mSurfaceMediaSource(NULL)
{
    // 通过 binder 获取 MediaPlayerService 
    const sp<IMediaPlayerService> service(getMediaPlayerService());
    if (service != NULL) {
        // 通过 MediaPlayerService 创建 MediaRecorderClient  
        mMediaRecorder = service->createMediaRecorder(attributionSource);
    }
    ...
}

MediaRecorder 类关系如下

class MediaRecorder : public BnMediaRecorderClient, public virtual IMediaDeathNotifier {...}
class BnMediaRecorderClient: public BnInterface<IMediaRecorderClient> {...}

MediaPlayerService 类关系如下

class MediaPlayerService : public BnMediaPlayerService {...}
class BnMediaPlayerService: public BnInterface<IMediaPlayerService> {...}

MediaPlayerService 中 createMediaRecorder 如下

// frameworks/av/media/libmediaplayerservice/MediaPlayerService.cpp
sp<IMediaRecorder> MediaPlayerService::createMediaRecorder(const AttributionSourceState& attributionSource)
{
    ...
    sp<MediaRecorderClient> recorder = new MediaRecorderClient(this, verifiedAttributionSource);
    ...
    return recorder;
}

MediaRecorderClient 构造如下

// frameworks/av/media/libmediaplayerservice/MediaRecorderClient.cpp
MediaRecorderClient::MediaRecorderClient(const sp<MediaPlayerService>& service,
        const AttributionSourceState& attributionSource)
{
    // 构造StagefrightRecorder,
    mRecorder = new StagefrightRecorder(attributionSource);
    mMediaPlayerService = service;
}

StagefrightRecorder 类继承关系如下 
struct StagefrightRecorder : public MediaRecorderBase {...}

到此 JAVA 层 new MediaRecord() 相关源码已经分析完成

MediaRecorder 参数设置

Java层 setAudioSource 、 setOutputFormat 等均调用了 Native 接口,下面以 setAudioSource 为例

// frameworks/base/media/java/android/media/MediaRecorder.java
public native void setAudioSource(@Source int audioSource) throws IllegalStateException;
// frameworks/base/media/jni/android_media_MediaRecorder.cpp  
static void android_media_MediaRecorder_setAudioSource(JNIEnv *env, jobject thiz, jint as)
{
    ...
    // 读取初始化时保存的mr
    sp<MediaRecorder> mr = getMediaRecorder(env, thiz);
    if (mr == NULL) {
        jniThrowException(env, "java/lang/IllegalStateException", NULL);
        return;
    }
    process_media_recorder_call(env, mr->setAudioSource(as), "java/lang/RuntimeException", "setAudioSource failed.");
}

// frameworks/av/media/libmedia/mediarecorder.cpp  
status_t MediaRecorder::setVideoSource(int vs)
{
    ...
    // 上面知道,这里的 mMediaRecorder 是 MediaRecorderClient 
    status_t ret = mMediaRecorder->setVideoSource(vs);
    return ret;
}

// frameworks/av/media/libmediaplayerservice/MediaRecorderClient.cpp
status_t MediaRecorderClient::setVideoSource(int vs)
{
    ...
    // 上面知道,这里的 mRecorder 是 StagefrightRecorder 
    return mRecorder->setVideoSource((video_source)vs);
}

// frameworks/av/media/libmediaplayerservice/StagefrightRecorder.cpp
status_t StagefrightRecorder::setVideoSource(video_source vs) {
    ...
    // 最终数据保存在 mVideoSource 中 
    if (vs == VIDEO_SOURCE_DEFAULT) {
        mVideoSource = VIDEO_SOURCE_CAMERA;
    } else {
        mVideoSource = vs;
    }

    return OK;
}

同理,其它参数设置多数最终都是保存在 StagefrightRecorder 中,录制相关的流程很多也是在其中控制

本文首发地址 https://blog.csdn.net/CSqingchen/article/details/134634628

MediaRecorder.prepare 分析

依据前面的分析,最终 prepare 真正实现如下

// frameworks/av/media/libmediaplayerservice/StagefrightRecorder.cpp
status_t StagefrightRecorder::prepareInternal() {
    ...
    status_t status = OK;

    // 依据不同的输出格式,执行不同的 setup 
    switch (mOutputFormat) {
        case OUTPUT_FORMAT_DEFAULT:
        case OUTPUT_FORMAT_THREE_GPP:
        case OUTPUT_FORMAT_MPEG_4:
        case OUTPUT_FORMAT_WEBM:
            status = setupMPEG4orWEBMRecording();
            break;

        case OUTPUT_FORMAT_AMR_NB:
        case OUTPUT_FORMAT_AMR_WB:
            status = setupAMRRecording();
            break;

        case OUTPUT_FORMAT_AAC_ADIF:
        case OUTPUT_FORMAT_AAC_ADTS:
            status = setupAACRecording();
            break;

        case OUTPUT_FORMAT_RTP_AVP:
            status = setupRTPRecording();
            break;

        case OUTPUT_FORMAT_MPEG2TS:
            status = setupMPEG2TSRecording();
            break;

        case OUTPUT_FORMAT_OGG:
            status = setupOggRecording();
            break;

        default:
            ALOGE("Unsupported output file format: %d", mOutputFormat);
            status = UNKNOWN_ERROR;
            break;
    }
    return status;
}

这里我们分析一下录制 MP4 的 prepare 流程 setupMPEG4orWEBMRecording

status_t StagefrightRecorder::setupMPEG4orWEBMRecording() {
    // 先清理 MediaWriter 
    mWriter.clear();
    mTotalBitRate = 0;

    status_t err = OK;
    sp<MediaWriter> writer;
    sp<MPEG4Writer> mp4writer;
    if (mOutputFormat == OUTPUT_FORMAT_WEBM) {
        writer = new WebmWriter(mOutputFd);
    } else {
        // 我们这里分析 MP4 录制,MPEG4Writer 主要是用来写入编码后的音视频内容   
        writer = mp4writer = new MPEG4Writer(mOutputFd);
    }

    if (mVideoSource < VIDEO_SOURCE_LIST_END) {
        // 如果编码器未配置,设置默认的编码器  
        setDefaultVideoEncoderIfNecessary();

        sp<MediaSource> mediaSource;
        // 设置 视频源
        err = setupMediaSource(&mediaSource);
        if (err != OK) {
            return err;
        }

        sp<MediaCodecSource> encoder;
        // 编码参数配置,然后通过 MediaCodecSource::Create 创建编码器
        err = setupVideoEncoder(mediaSource, &encoder);
        if (err != OK) {
            return err;
        }

        // MPEG4Writer 添加编码通道,一般会有audio、video 两个,这里是 video
        writer->addSource(encoder);
        mVideoEncoderSource = encoder;
        // 输出文件的码率,是视频和音频总码率之和
        mTotalBitRate += mVideoBitRate;
    }

    // Audio source is added at the end if it exists.
    // This help make sure that the "recoding" sound is suppressed for
    // camcorder applications in the recorded files.
    // disable audio for time lapse recording
    const bool disableAudio = mCaptureFpsEnable && mCaptureFps < mFrameRate;
    if (!disableAudio && mAudioSource != AUDIO_SOURCE_CNT) {
        // 通过  createAudioSource() 配置音频编码参数,进而通过 MediaCodecSource::Create 创建 编码器
        err = setupAudioEncoder(writer);
        
        if (err != OK) return err;
        mTotalBitRate += mAudioBitRate;
    }
    ...
    // 监听 MPEG4Writer 一些参数的回调 
    writer->setListener(mListener);
    mWriter = writer;
    return OK;
}

通过如上,可以看到 MediaRecorder.prepare 主要是进行参数的配置、编码器的初始化

MediaRecorder.start 分析

依据前面的分析,最终 start 真正实现如下

//  frameworks/av/media/libmediaplayerservice/StagefrightRecorder.cpp
status_t StagefrightRecorder::start() {
    Mutex::Autolock autolock(mLock);
    if (mOutputFd < 0) {
        ALOGE("Output file descriptor is invalid");
        return INVALID_OPERATION;
    }

    status_t status = OK;

    if (mVideoSource != VIDEO_SOURCE_SURFACE) {
        status = prepareInternal();
        if (status != OK) {
            return status;
        }
    }

    switch (mOutputFormat) {
        case OUTPUT_FORMAT_DEFAULT:
        case OUTPUT_FORMAT_THREE_GPP:
        case OUTPUT_FORMAT_MPEG_4:
        case OUTPUT_FORMAT_WEBM:
        {
            sp<MetaData> meta = new MetaData;
            // 设置 meta 信息
            setupMPEG4orWEBMMetaData(&meta);

            // MPEG4Writer 传入 meta 信息,
            // startWriterThread 开启写入线程 
            // setupAndStartLooper 启动 ALooper, mReflector 用于信息传递 
            // 通过 writeFtypBox(MetaData *param) 写入
            // startTracks(MetaData *params) 启动音、视频Track,参见 MPEG4Writer::Track::start
            status = mWriter->start(meta.get());
            break;
        }
        ...
    }

    if (status != OK) {
        // start 异常 
        mWriter.clear();
        mWriter = NULL;
    }

    if ((status == OK) && (!mStarted)) {
        mAnalyticsDirty = true;
        mStarted = true;
        ...
        // 用于编码耗电统计 
        addBatteryData(params);
    }

    return status;
}

MediaRecorder.stop 分析

依据前面的分析,最终 stop 真正实现如下

//  frameworks/av/media/libmediaplayerservice/StagefrightRecorder.cpp
status_t StagefrightRecorder::stop() {
    Mutex::Autolock autolock(mLock);
    status_t err = OK;

    if (mCaptureFpsEnable && mCameraSourceTimeLapse != NULL) {
        // 延时录制,详细可参见 CameraSourceTimeLapse.cpp
        mCameraSourceTimeLapse->startQuickReadReturns();
        mCameraSourceTimeLapse = NULL;
    }

    int64_t stopTimeUs = systemTime() / 1000;
    for (const auto &source : { mAudioEncoderSource, mVideoEncoderSource }) {
        // 设置停止时间戳
        if (source != nullptr && OK != source->setStopTimeUs(stopTimeUs)) {}
    }

    if (mWriter != NULL) {
        // MPEG4Writer 的停止 ,实际调用其  reset(true, true)
        // stopWriterThread() 停止写的线程
        // Track 停止
        // release 关闭文件,停止释放Looper,资源状态重置
        err = mWriter->stop();
        mLastSeqNo = mWriter->getSequenceNum();
        mWriter.clear();
    }

    // 写入参数相关信息
    flushAndResetMetrics(true);

    // 重置参数状态
    mDurationRecordedUs = 0;
    mDurationPausedUs = 0;
    mNPauses = 0;
    mTotalPausedDurationUs = 0;
    mPauseStartTimeUs = 0;
    mStartedRecordingUs = 0;

    mGraphicBufferProducer.clear();
    mPersistentSurface.clear();
    mAudioEncoderSource.clear();
    mVideoEncoderSource.clear();
    ......
    return err;
}

结语

到这里,已经完成了 MediaRecorder 录制 Framework 源码的分析。
其它部分流程,可以对照参见 StagefrightRecorder.cpp 中源码。希望对你有所帮助。
如果你在使用MediaRecorder的过程中遇到了其他问题,欢迎留言讨论。
如果你觉得本文还不错,可以点赞+收藏。


相关文章
安卓MediaRecorder(1)录制音频的详细使用
安卓MediaRecorder(2)录制源码分析
安卓MediaRecorder(3)音频采集编码写入详细源码分析
安卓MediaRecorder(4)视频采集编码写入详细源码分析

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

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

相关文章

[面试题~Docker] 云原生必问基础篇

文章目录 基础相关1. Docker 是什么&#xff1f;2. 镜像是什么3. 容器是什么4. 数据卷是什么5. Docker 和虚拟机的区别&#xff1f;6. Docker 常用命令有哪些&#xff1f; 原理相关1. docker 有几种网络模式host 模式container模式none模式bridge模式 2. docker 网络实现在Linu…

AGILE-SCRUM

一个复杂的汽车ECU开发。当时开发队伍遍布全球7个国家&#xff0c;10多个地区&#xff0c;需要同时为多款车型定制不同的软件&#xff0c;头疼的地方是&#xff1a; 涉及到多方人员协调&#xff0c;多模块集成和管理不同软件团队使用的设计工具、验证工具&#xff0c;数据、工…

C语言数据结构-基于单链表实现通讯录

文章目录 1 基础要求2 通讯录功能2.1 引入单链表的文件2.2 定义联系人数据结构2.3 打开通讯录2.4 保存数据后销毁通讯录2.5 添加联系人2.6 删除联系人2.7 修改联系人2.8 查找联系人2.9 查看通讯录 3 通讯录代码展示3.1 SeqList_copy.h3.2 SeqList_copy.c3.3 Contact.h3.4 Conta…

【论文阅读】Reachability and distance queries via 2-hop labels

Cohen E, Halperin E, Kaplan H, et al. Reachability and distance queries via 2-hop labels[J]. SIAM Journal on Computing, 2003, 32(5): 1338-1355. Abstract 图中的可达性和距离查询是许多应用的基础&#xff0c;从地理导航系统到互联网路由。其中一些应用程序涉及到巨…

免费开源-数字孪生城市污水处理平台

智慧城市污水处理平台&#xff0c;基于污水厂三维模型可视化场景&#xff0c;结合物联网IOT、视频监控以及综合运营数据&#xff0c;增加污水处理厂的掌控力度。飞渡科技利用数字孪生技术结合物联网IOT技术&#xff0c;直观展现各污水处理站点的整体建设规模&#xff0c;以及污…

pytorch:YOLOV1的pytorch实现

pytorch&#xff1a;YOLOV1的pytorch实现 注&#xff1a;本篇仅为学习记录、学习笔记&#xff0c;请谨慎参考&#xff0c;如果错误请评论指出。 参考&#xff1a; 动手学习深度学习pytorch版——从零开始实现YOLOv1 目标检测模型YOLO-V1损失函数详解 3.1 YOLO系列理论合集(YOL…

Windows Service Name重复问题

Windows Service Name重复问题 1&#xff0c;问题 2&#xff0c;打开命令提示符&#xff0c;管理员身份运行 3&#xff0c;输入命令&#xff1a;sc delete MYSQL57 4&#xff0c;验证一下&#xff0c;可以看见已经没有感叹号啦 &#xff0c;可以看见已经没有感叹号啦

基于Qt的Live2D模型显示以及控制

基于Qt的Live2D模型显示以及控制 基本说明 Live2D官方提供有控制Live2D模型的SDK,而且还提供了一个基于OpenGL的C项目Example,我们可以基于该项目改成Qt的项目&#xff0c;做一个桌面端的Live2D桌宠程序。 官方例子 经过改造效果如下图所示。 官方项目配置 下载官方提供的SD…

视觉检测系统在半导体行业的应用

一、半导体产业链概述 半导体产业链是现代电子工业的核心组成部分&#xff0c;涵盖了从原材料到最终产品的整个生产过程。这个产业链主要分为以下几个环节&#xff1a; 1.原材料供应&#xff1a;半导体行业的基石是半导体材料&#xff0c;如硅片、化合物半导体等。这些材料需要…

CentOS7安装Docker,DockerCompose

安装docker 1、卸载docker 查看是否有旧版本docker docker info首先检测我们虚拟机是否已经安装过Docker&#xff0c;如果安装则需卸载。代码中“\”符号为换行符&#xff0c;相当于一行内容分为多行&#xff0c;这是检测docker的各种组件 yum remove docker \docker-clien…

高项备考葵花宝典-项目进度管理输入、输出、工具和技术(上,很详细考试必过)

项目进度管理的目标是使项目按时完成。有效的进度管理是项目管理成功的关键之一&#xff0c;进度问题在项目生命周期内引起的冲突最多。 小型项目中&#xff0c;定义活动、排列活动顺序、估算活动持续时间及制定进度模型形成进度计划等过程的联系非常密切&#xff0c;可以视为一…

高项备考葵花宝典-项目进度管理输入、输出、工具和技术(中,很详细考试必过)

项目进度管理的目标是使项目按时完成。有效的进度管理是项目管理成功的关键之一&#xff0c;进度问题在项目生命周期内引起的冲突最多。 小型项目中&#xff0c;定义活动、排列活动顺序、估算活动持续时间及制定进度模型形成进度计划等过程的联系非常密切&#xff0c;可以视为一…

LeetCode算法题解(单调栈)|LeetCode84. 柱状图中最大的矩形

一、LeetCode84. 柱状图中最大的矩形 题目链接&#xff1a;84. 柱状图中最大的矩形 题目描述&#xff1a; 给定 n 个非负整数&#xff0c;用来表示柱状图中各个柱子的高度。每个柱子彼此相邻&#xff0c;且宽度为 1 。 求在该柱状图中&#xff0c;能够勾勒出来的矩形的最大…

更改Android Studio的.android和.gradle文件夹默认位置

一、首先关闭Android Studio&#xff0c; 二、目标位置新建文件夹 这一步&#xff0c;为了省去麻烦&#xff0c;我并没有直接在我的目标位置新建文件夹&#xff0c;而是把C盘下的.android和.gradle文件夹整个复制过来&#xff0c;和SDK都在同一目录下&#xff0c;感觉这样可以…

Sql Server 2017主从配置之:AlwaysOn高可用

AlwaysOn高可用功能&#xff0c;真正实现了数据库的灾备切换、高可用。 AlwaysOn通过Windows Server故障转移群集&#xff0c;部署高可用数据库组。 在故障转移群集基础上完成部署读写分离&#xff0c;只读负载平衡最多3个写入节点实现故障转移最多3个数据实时同步节点 环境…

网络入门---网络编程初步认识和实践(使用udp协议)

目录标题 前言准备工作udpserver.hpp成员变量构造函数初始化函数(socket,bind)start函数(recvfrom) udpServer.ccudpClient.hpp构造函数初始化函数run函数(sendto) udpClient.cc测试 前言 在上一篇文章中我们初步的认识了端口号的作用&#xff0c;ip地址和MAC地址在网络通信时…

Centos7、Mysql8.0 load_file函数返回为空的终极解决方法--暨selinux的深入理解

零、问题背景 最近想换房&#xff0c;为了方便自己对比感兴趣的房子&#xff0c;因此决定将目标房源的基本信息放在表里&#xff0c;特别是要一目了然的看到众多房子的各种图纸和照片&#xff0c;因此决定要在Mysql8.0.34数据库中以二进制形式保存图片&#xff08;抛开合理性和…

VSCode 报错 gopls was not able to find modules in your workspace.

由于在VSCode中打开一个项目时出现如下提示&#xff1a; 于是检索其解决办法 根据提示我们可以看到这是由于gopls所出的问题,在工作空间找不到相关module&#xff0c;这是VSCode中go插件所依赖附带的&#xff0c;该语言服务器提供诸如自动完成&#xff0c;转到定义&#xff0c;…

《python每天一小段》--12 数据可视化《1》

欢迎阅读《Python每天一小段》系列&#xff01;在本篇中&#xff0c;将使用Python Matplotlib实现数据可视化的简单图形。 一、概念 Matplotlib是一个流行的Python数据可视化库&#xff0c;它提供了丰富的绘图功能&#xff0c;可以创建各种类型的图表&#xff0c;包括折线图、…

echarts绘制一个环形图

其他echarts&#xff1a; echarts绘制一个柱状图&#xff0c;柱状折线图 echarts绘制一个饼图 echarts绘制一个环形图2 效果图&#xff1a; 代码&#xff1a; <template><div class"wrapper"><!-- 环形图 --><div ref"doughnutChart…