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

文章目录

    • 前言
    • 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)视频采集编码写入详细源码分析

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Navicat 技术指引 | 适用于 GaussDB 分布式的服务器对象的创建/设计

Navicat Premium&#xff08;16.3.3 Windows版或以上&#xff09;正式支持 GaussDB 分布式数据库。GaussDB分布式模式更适合对系统可用性和数据处理能力要求较高的场景。Navicat 工具不仅提供可视化数据查看和编辑功能&#xff0c;还提供强大的高阶功能&#xff08;如模型、结构…

JavaSE知识点回顾,附学习思维导图

第一阶段 day01 java 发展&#xff0c;java 环境( path, java_home, class_path)&#xff0c;java 原理&#xff0c; java 执行 &#xff0c; jvm , jre , jdk day02 变量 标识符命名规则 数据类型 数据类型的转换 运算符 day03 选择结构 if , switch day04 循环结构 for , whi…

java--Collection的遍历方式

1.迭代器概述 迭代器是用来遍历集合的专用方式(数组没有迭代器)&#xff0c;在java中迭代器是Iterator。 2.Collection集合获取迭代器的方法 3.Iterator迭代器中的常用方法 4.增强for循环 ①增强for可以用来遍历集合或数组。 ②增强for遍历集合&#xff0c;本质就是迭代器遍…

005、Softmax损失

之——softmax与交叉熵 杂谈 我们常用到softmax函数与交叉熵的结合作为损失函数以监督学习&#xff0c;这里做一个小小的总结。 正文 1.softmax的基本改进 所谓softmax就是在对接全连接层输出时候把输出概率归一化&#xff0c;最基础的就是这样&#xff1a; 效果就是这样&…

(第65天)PDB 快照

介绍 PDB 快照是一个 PDB 指定时间点的副本。在创建快照时,源 PDB 可以是只读或者读写模式。 PDB 快照可以用于快速创建 PDB。 PDB 快照可以分为手动和自动两种创建方式(create pluggable database|alter pluggable database): 手动快照使用 SNAPSHOT 子句的方式来创建自动…

Jmeter beanshell编程实例

1、引言 BeanShell是一种小型的&#xff0c;免费的&#xff0c;可嵌入的符合Java语法规范的源代码解释器&#xff0c;具有对象脚本语言特性。 在Jmeter实践中&#xff0c;由于BeanShell组件较高的自由度&#xff0c;通常被用来处理较为复杂&#xff0c;其它组件难以处理的问题…

jmeter接口测试之登录测试

注册登录_登陆接口文档 1.登录 请求地址&#xff1a; POST xxxxxx/Home/Login 请求参数&#xff1a; args{LoginName:"mtest", // 登录名&#xff0c;可以为用户名或邮箱Password:"123456" // 密码" }响应数据&#xff1a; 成功 {"S…

微表情检测(四)----SL-Swin

SL-Swin: A Transformer-Based Deep Learning Approach for Macro- and Micro-Expression Spotting on Small-Size Expression Datasets 在本文中&#xff0c;我们致力于解决从视频中检测面部宏观和微观表情的问题&#xff0c;并通过使用深度学习方法分析光流特征提出了引人注…

[GFCTF 2021]文件查看器

文章目录 前置知识可调用对象数组对方法的调用GC回收机制phar修改签名 解题步骤 前置知识 可调用对象数组对方法的调用 我们先来看下面源码 <?phperror_reporting(0);class User{public $username;public $password;public function check(){if($this->username"…

用perl查找文件夹中的所有文件和目录

查找文件夹中的文件和目录是一个很常见的操作&#xff0c;使用perl的File::Find模块可以很方便的实现。首先使用perldoc File::Find 查看一下文档: 这个核心的就是文档中描述的回调函数。我们举一个实际的例子&#xff0c;一个空的git仓库为例&#xff0c;下面的脚本用于查找…

Aduino实现音频频谱效果

看到这样一个效果,于是想用arduino实现类似效果。需要的组件如下 1 arduino开发板 2 音频传感器 3 灯带 接线图如图 代码如下 #include <EEPROM.h>#include <Adafruit_NeoPixel.h>#define PIN 2 // input pin Neopixel is attached to#define NUMPIXELS …

流程控制之条件判断

流程控制之条件判断 2.1.if语句语法 2.1.1单分支结构 # 语法1: if <条件表达式> then 指令 fi #语法2: if <条件表达式>;then 指令 fi # if&#xff0c;if 标志循环起始终止…

现代雷达车载应用——第2章 汽车雷达系统原理 2.2节

经典著作&#xff0c;值得一读&#xff0c;英文原版下载链接【免费】ModernRadarforAutomotiveApplications资源-CSDN文库。 2.2 汽车雷达架构 从顶层来看&#xff0c;基本的汽车雷达由发射器&#xff0c;接收器和天线组成。图2.2给出了一种简化的单通道连续波雷达结构[2]。这…

【Docker】vxlan的原理与实验

VXLAN&#xff08;Virtual eXtensible Local Area Network&#xff0c;虚拟可扩展局域网&#xff09;&#xff0c;是一种虚拟化隧道通信技术。它是一种Overlay&#xff08;覆盖网络&#xff09;技术&#xff0c;通过三层的网络来搭建虚拟的二层网络。 VXLAN介绍 VXLAN是在底层…

常用的测试用例大全

登录、添加、删除、查询模块是我们经常遇到的&#xff0c;这些模块的测试点该如何考虑 1)登录 ① 用户名和密码都符合要求(格式上的要求) ② 用户名和密码都不符合要求(格式上的要求) ③ 用户名符合要求&#xff0c;密码不符合要求(格式上的要求) ④ 密码符合要求&#xf…

使用Java将图片添加到Excel的几种方式

1、超链接 使用POI&#xff0c;依赖如下 <dependency><groupId>org.apache.poi</groupId><artifactId>poi</artifactId><version>4.1.2</version></dependency>Java代码如下,运行该程序它会在桌面创建ImageLinks.xlsx文件。 …

【从零开始学习JVM | 第六篇】快速了解 直接内存

前言&#xff1a; 当谈及Java虚拟机&#xff08;JVM&#xff09;的内存管理时&#xff0c;我们通常会想到堆内存和栈内存。然而&#xff0c;还有一种被称为"直接内存"的特殊内存区域&#xff0c;它在Java应用程序中起着重要的作用。直接内存提供了一种与Java堆内存和…

十几个软件测试实战项目【外卖/医药/银行/电商/金融】

项目一&#xff1a;ShopNC商城 项目概况&#xff1a; ShopNC商城是一个电子商务B2C电商平台系统&#xff0c;功能强大&#xff0c;安全便捷。适合企业及个人快速构建个性化网上商城。 包含PCIOS客户端Adroid客户端微商城&#xff0c;系统PC后台是基于ThinkPHP MVC构架开发的跨…

windows如何解决端口冲突(实用篇)

在项目设计中&#xff0c;环境配置成功点击运行瞬间&#xff0c;一大堆红爆出&#xff0c;8080端口占用&#xff0c;这个是很烦人的。。。 解决方式&#xff1a; 笨方法&#xff1a;一、查看所有端口实用情况&#xff08;挨个扫&#xff09; 按住【WINR】快捷键打开运行输入…

Android View闪烁动画AlphaAnimation,Kotlin

Android View闪烁动画AlphaAnimation&#xff0c;Kotlin private fun flickerAnimation(view: View?) {val animation: Animation AlphaAnimation(1f, 0f) //不透明到透明。animation.duration 500 // 1次过程时长。animation.interpolator LinearInterpolator() // 线性速…