Android图像显示SurfaceFlinger总结

news2024/11/16 18:42:59

1 介绍

1.1 框架中位置

​​Android图像显示系统框架图
上图为Android的图形显示系统框架图。
首先上层应用通过ViewRoot的scheduleTraversals函数发起绘制任务,并通过HWUI调用OpenGL接口将绘制数据传递给GPU处理;SF会接收所有应用更新的绘制数据,并根据Z-Order、透明度、大小、位置等参数计算出每个应用图层在最终合成图像中的位置;SF完成图层处理后会把所有的应用图层提供给HWC,由HWC来决定这些图层的合成策略并调用屏显驱动做合成;最后屏显驱动会将合成的最终画面送到硬件屏幕显示。

Android系统通过buffer缓冲区来保存图形信息,这个buffer的内存空间是有SF向图形内存分配器Gralloc申请的,而Gralloc是通过ION Driver从kernel中开辟出一块共享内存。在整个图形显示流程中,buffer缓冲区会在App、SF、HWC之间来回流转。

1.2 职责

  1. surfaceFlinger是一个服务进程
  2. 负责管理应用程序窗口的创建、显示、更新和销毁等操作
    3)向上和WindowManager交互,向下和HwComposer交互。
    SF会接收所有应用更新的绘制数据,并根据Z-Order、透明度、大小、位置等参数计算出每个应用图层在最终合成图像中的位置;
    SF完成图层处理后会把所有的应用图层提供给HWC,由HWC来决定这些图层的合成策略并调用屏显驱动做合成

负责显示系统界面和应用程序的图形渲染工作。

  • 图像合成:SurfaceFlinger 接收来自多个应用程序和系统服务的图像缓冲区,根据它们的位置、大小、透明度、Z轴顺序等属性,将它们合成到一个最终的缓冲区中,然后发送到显示设备上 。
  • 管理 Surface:SurfaceFlinger 管理所有 Surface 对象,包括视频、图片等,并为每个 Surface 对象分配 BufferQueue(缓冲区队列),确保每个 Surface 都能按时完成显示 。
  • 三缓冲与 VSYNC 机制:SurfaceFlinger 采用双缓冲区机制,即前台和后台两个缓冲区,以及 VSYNC 机制,以提高渲染效率和质量,减少屏幕撕裂现象 。
  • 硬件加速:SurfaceFlinger 提供多种硬件加速技术,如 OpenGL ES、Vulkan 等,使应用程序能够更快地渲染 UI 界面 。
  • 窗口叠加与透明度支持:SurfaceFlinger 支持窗口叠加、透明度、混合模式等特性,以支持复杂的多层 UI 界面 。
  • 与 Hardware Composer 通信:SurfaceFlinger 与硬件抽象层 Hardware Composer 进行通信,利用硬件加速的方式来合成 Surface,提高性能和节省电量 。
  • 处理输入事件:SurfaceFlinger 中的 EventThread 方法负责处理输入事件,将输入事件分发给对应的窗口或 Surface 进行处理

2 源码分析

首先看下系统中SurfaceFlinger的进程
在这里插入图片描述
代码中入口:
SF是一个独立的可执行程序,路径是/system/bin/surfaceflinger,开机过程中init进程会解析surfaceflinger.rc,启动surfaceflinger进程。
frameworks/native/services/surfaceflinger/surfaceflinger.rc,

service surfaceflinger /system/bin/surfaceflinger
    class core animation
    user system
    group graphics drmrpc readproc
    capabilities SYS_NICE
    onrestart restart --only-if-running zygote
    task_profiles HighPerformance
    socket pdx/system/vr/display/client     stream 0666 system graphics u:object_r:pdx_display_client_endpoint_socket:s0
    socket pdx/system/vr/display/manager    stream 0666 system graphics u:object_r:pdx_display_manager_endpoint_socket:s0
    socket pdx/system/vr/display/vsync      stream 0666 system graphics u:object_r:pdx_display_vsync_endpoint_socket:s0

on property:vendor.debug.sf.restart=1
     restart surfaceflinger

on property:init.svc.zygote=restarting && property:debug.sf.toomanylayers=1
     restart surfaceflinger
     setprop debug.sf.toomanylayers "0"

2.1 SurfaceFlinger的初始化

init.rc中调用到SF的main函数,这里是SF进程的入口。
路径:frameworks/native/services/surfaceflinger/main_surfaceflinger.cpp

int main(int, char**) {
	startGraphicsAllocatorService(); //启动图形服务
	...
	sp<SurfaceFlinger> flinger = surfaceflinger::createSurfaceFlinger(); //1. 创建SF实例
	flinger->init(); //2. 对SF实例进行初始化
	// publish surface flinger
	//3. 将SF添加到serviceManager中。 这样客户端通过getService()+SurfaceFlinger的名称,
	// 就可以获取SF的远程代理对象,进而和SF服务跨进程通信
	//static char const* getServiceName() ANDROID_API { return "SurfaceFlinger"; } @SurfaceFinger.h
    sp<IServiceManager> sm(defaultServiceManager());
    sm->addService(String16(SurfaceFlinger::getServiceName()),  flinger,  false,
                   IServiceManager::DUMP_FLAG_PRIORITY_CRITICAL | IServiceManager::DUMP_FLAG_PROTO);

    // publish gui::ISurfaceComposer, the new AIDL interface
    //4. 新建SurfaceComposerAIDL对象,注册到serviceManager中
    sp<SurfaceComposerAIDL> composerAIDL = new SurfaceComposerAIDL(flinger);
    sm->addService(String16("SurfaceFlingerAIDL"), composerAIDL, false,
                   IServiceManager::DUMP_FLAG_PRIORITY_CRITICAL | IServiceManager::DUMP_FLAG_PROTO);
	// 启动Display服务
	startDisplayService(); // dependency on SF getting registered above
	...
	// run surface flinger in this thread
    flinger->run(); //5. 启动SF线程
}

2.2 SurfaceFlinger构造函数详解

先来看下SurfaceFlinger的类关系图
//frameworks/native/services/surfaceflinger/SurfaceFlinger.h
在这里插入图片描述
ISurfaceComposer的接口定义:
它的职责是:

class ISurfaceComposer: public IInterface {
public:
    DECLARE_META_INTERFACE(SurfaceComposer)
    static constexpr size_t MAX_LAYERS = 4096;
    ...
    //Create a connection with SurfaceFlinger.
    virtual sp<ISurfaceComposerClient> createConnection() = 0;
    ...

2.2.1 createSurfaceFlinger详解

接下来看下surfaceflinger::createSurfaceFlinger()具体做了什么?
新建一个SurfaceFlinger实例,将DefaultFactory 做为参数传入,其中DefaultFactory继承了surfaceflinger::Factory类。
主要做了什么?

  • 创建CompositionEngine
  • 添加了WindowInfosListenerInvoker回调
//frameworks/native/services/surfaceflinger/SurfaceFlingerFactory.cpp
sp<SurfaceFlinger> createSurfaceFlinger() {
    static DefaultFactory factory;
    return new SurfaceFlinger(factory);
}

SurfaceFlinger::SurfaceFlinger(Factory& factory, SkipInitializationTag)
      : mFactory(factory),
        mPid(getpid()),
        mInterceptor(mFactory.createSurfaceInterceptor()),
        mTimeStats(std::make_shared<impl::TimeStats>()),
        mFrameTracer(mFactory.createFrameTracer()),
        mFrameTimeline(mFactory.createFrameTimeline(mTimeStats, mPid)),
        mCompositionEngine(mFactory.createCompositionEngine()), //创建CompositionEngine
        ...
        //添加了WindowInfosListenerInvoker回调
        mWindowInfosListenerInvoker(sp<WindowInfosListenerInvoker>::make(*this)) {
    ALOGI("Using HWComposer service: %s", mHwcServiceName.c_str());
}
//frameworks/native/services/surfaceflinger/SurfaceFlinger.cpp
//SurfaceFlinger构造函数中对一些成员变量进行赋值初始化。
SurfaceFlinger::SurfaceFlinger(Factory& factory) : SurfaceFlinger(factory, SkipInitialization) {
    ALOGI("SurfaceFlinger is starting");
    hasSyncFramework = running_without_sync_framework(true);
    dispSyncPresentTimeOffset = present_time_offset_from_vsync_ns(0);
    useHwcForRgbToYuv = force_hwc_copy_for_virtual_displays(false);
    maxFrameBufferAcquiredBuffers = max_frame_buffer_acquired_buffers(2);
    ...
}

2.2.2 SurfaceFlinger的init详解

flinger->init()对SF实例进行初始化,具体做了什么?

void SurfaceFlinger::init() {
    ALOGI(  "SurfaceFlinger's main thread ready to run. "
            "Initializing graphics H/W...");
    Mutex::Autolock _l(mStateLock);
    //构造renderengine的builder
    auto builder = renderengine::RenderEngineCreationArgs::Builder()
                           .setPixelFormat(static_cast<int32_t>(defaultCompositionPixelFormat))
                           .setImageCacheSize(maxFrameBufferAcquiredBuffers)
                           .setUseColorManagerment(useColorManagement)
                           .setEnableProtectedContext(enable_protected_contents(false))
                           .setPrecacheToneMapperShaderOnly(false)
                           .setSupportsBackgroundBlur(mSupportsBlur)
                           .setContextPriority(
                                   useContextPriority
                                           ? renderengine::RenderEngine::ContextPriority::REALTIME
                                           : renderengine::RenderEngine::ContextPriority::MEDIUM);
    ...
    //创建renderEngine,并给mCompositionEngine设置RenderEngine
    mCompositionEngine->setRenderEngine(renderengine::RenderEngine::create(builder.build()));
    ...
    //调用mCompositionEngine来createHWComposer,给mCompositionEngine设置回调函数。思考:这里回调是什么接口?作用是?
	mCompositionEngine->setTimeStats(mTimeStats);
    mCompositionEngine->setHwComposer(getFactory().createHWComposer(mHwcServiceName));
    mCompositionEngine->getHwComposer().setCallback(*this);
    ...
    // Process any initial hotplug and resulting display changes.
    processDisplayHotplugEventsLocked(); //重要调用,第一次会做一次插拔检测
    const auto display = getDefaultDisplayDeviceLocked();
    LOG_ALWAYS_FATAL_IF(!display, "Missing primary display after registering composer callback.");
    const auto displayId = display->getPhysicalId();
    LOG_ALWAYS_FATAL_IF(!getHwComposer().isConnected(displayId),  "Primary display is disconnected.");
    // initialize our drawing state
    mDrawingState = mCurrentState; //更新drawState, 思考:其中共有几种状态?都是什么时候切换的?
    initializeDisplays();
    ...
    ALOGV("Done initializing");
}

2.2.3 processDisplayHotplugEventsLocked详解

//frameworks/native/services/surfaceflinger/SurfaceFlinger.cpp
void SurfaceFlinger::processDisplayHotplugEventsLocked() {
    for (const auto& event : mPendingHotplugEvents) {
    	std::optional<DisplayIdentificationInfo> info =
                getHwComposer().onHotplug(event.hwcDisplayId, event.connection);
        if (!info) { continue; }
        //获取displayId和token 
        const auto displayId = info->id;
        const auto token = mPhysicalDisplayTokens.get(displayId);
    	if (event.connection == hal::Connection::CONNECTED) { //如果此时有显示屏幕连接
    		... //
    	} else { //如果此时没有显示屏幕连接
            ALOGV("Removing display %s", to_string(displayId).c_str());
            if (const ssize_t index = mCurrentState.displays.indexOfKey(token->get()); index >= 0) {
                const DisplayDeviceState& state = mCurrentState.displays.valueAt(index);
                mInterceptor->saveDisplayDeletion(state.sequenceId);
                mCurrentState.displays.removeItemsAt(index);
            }

            mPhysicalDisplayTokens.erase(displayId);
        }
         processDisplayChangesLocked(); //主要步骤,处理显示改变
    }

    mPendingHotplugEvents.clear();
}

void SurfaceFlinger::processDisplayChangesLocked() {
    const KeyedVector<wp<IBinder>, DisplayDeviceState>& curr(mCurrentState.displays);
    const KeyedVector<wp<IBinder>, DisplayDeviceState>& draw(mDrawingState.displays);
    if (!curr.isIdenticalTo(draw)) {
        mVisibleRegionsDirty = true;

        // find the displays that were removed (ie: in drawing state but not in current state)
        // also handle displays that changed (ie: displays that are in both lists)
        for (size_t i = 0; i < draw.size(); i++) {
            const wp<IBinder>& displayToken = draw.keyAt(i);
            const ssize_t j = curr.indexOfKey(displayToken);
            if (j < 0) {
                // in drawing state but not in current state
                processDisplayRemoved(displayToken);
            } else {
                // this display is in both lists. see if something changed.
                const DisplayDeviceState& currentState = curr[j];
                const DisplayDeviceState& drawingState = draw[i];
                processDisplayChanged(displayToken, currentState, drawingState); //核心步骤,调用processDisplayAdded
            }
        }
		...
	}
    mDrawingState.displays = mCurrentState.displays;
}

2.2.4 processDisplayAdded详解

主要做了几件事:
1)通过CompositionEngine创建display
2)创建BufferQueue 和 DisplaySurface对象
3)调用initscheduler来初始化整个Vsync系统

void SurfaceFlinger::processDisplayAdded(const wp<IBinder>& displayToken,
                                         const DisplayDeviceState& state) {
	compositionengine::DisplayCreationArgsBuilder builder; 
	builder.setPixels(resolution);
    builder.setIsSecure(state.isSecure);
    builder.setPowerAdvisor(mPowerAdvisor.get());
    builder.setName(state.displayName);
    // 1. 通过CompositionEngine创建display,具体display参数通过builder设置
    auto compositionDisplay = getCompositionEngine().createDisplay(builder.build());
    compositionDisplay->setLayerCachingEnabled(mLayerCachingEnabled);

	sp<compositionengine::DisplaySurface> displaySurface;
    sp<IGraphicBufferProducer> producer; //为什么有producer 和BqProducer,两者有什么区别?
    sp<IGraphicBufferProducer> bqProducer;
    sp<IGraphicBufferConsumer> bqConsumer;
    //2. 创建BufferQueue
    getFactory().createBufferQueue(&bqProducer, &bqConsumer, /*consumerIsSurfaceFlinger =*/false);
	//3. 创建DisplaySurface对象
    if (state.isVirtual()) {
        const auto displayId = VirtualDisplayId::tryCast(compositionDisplay->getId());
        auto surface = sp<VirtualDisplaySurface>::make(getHwComposer(), *displayId, state.surface,
                                                       bqProducer, bqConsumer, state.displayName);
        displaySurface = surface;
        producer = std::move(surface);
    } else {
        const auto displayId = PhysicalDisplayId::tryCast(compositionDisplay->getId());
        displaySurface = sp<FramebufferSurface>::make(getHwComposer(), *displayId, bqConsumer,
                                             state.physical->activeMode->getResolution(),
                                             ui::Size(maxGraphicsWidth, maxGraphicsHeight));
        producer = bqProducer;
    }
	...
    auto display = setupNewDisplayDeviceInternal(displayToken, std::move(compositionDisplay), state,
                                                 displaySurface, producer);
    if (display->isPrimary()) {
        initScheduler(display); //主要步骤,调用initscheduler来初始化整个Vsync系统
    }
   
    mDisplays.try_emplace(displayToken, std::move(display));
}

2.2.5 initScheduler详解

initscheduler初始化整个Vsync系统,主要做几件事:
1)如果没有mScheduler ,创建mScheduler,有的话直接使用
2)创建APP连接
3)创建SF连接
4)初始化Vsync

void SurfaceFlinger::initScheduler(const sp<DisplayDevice>& display) {
    if (mScheduler) {
        // If the scheduler is already initialized, this means that we received
        // a hotplug(connected) on the primary display. In that case we should
        // update the scheduler with the most recent display information.
        ALOGW("Scheduler already initialized, updating instead");
        mScheduler->setRefreshRateConfigs(display->holdRefreshRateConfigs());
        return;
    }
    //如果没有mScheduler ,创建mScheduler
    mScheduler = std::make_unique<scheduler::Scheduler>(static_cast<ICompositor&>(*this),
                                                        static_cast<ISchedulerCallback&>(*this),
                                                        features);
	{
        auto configs = display->holdRefreshRateConfigs();
        if (configs->kernelIdleTimerController().has_value()) {
            features |= Feature::kKernelIdleTimer;
        }
        mScheduler->createVsyncSchedule(features);
        mScheduler->setRefreshRateConfigs(std::move(configs));
    }
    setVsyncEnabled(false);
    mScheduler->startTimers();

    const auto configs = mVsyncConfiguration->getCurrentConfigs();
    const nsecs_t vsyncPeriod = currRefreshRate.getPeriodNsecs();
	//创建APP连接
    mAppConnectionHandle =  mScheduler->createConnection("app", mFrameTimeline->getTokenManager(),
                                         /*workDuration=*/configs.late.appWorkDuration,
                                         /*readyDuration=*/configs.late.sfWorkDuration,
                                         impl::EventThread::InterceptVSyncsCallback());
	//创建SF连接
    mSfConnectionHandle = mScheduler->createConnection("appSf", mFrameTimeline->getTokenManager(),
                                         /*workDuration=*/std::chrono::nanoseconds(vsyncPeriod),
                                         /*readyDuration=*/configs.late.sfWorkDuration,
                                         [this](nsecs_t timestamp) {
                                             mInterceptor->saveVSyncEvent(timestamp);
                                         });
	//初始化Vsync
    mScheduler->initVsync(mScheduler->getVsyncDispatch(), *mFrameTimeline->getTokenManager(),
                          configs.late.sfWorkDuration);
    mFpsReporter = new FpsReporter(*mFrameTimeline, *this); 
    mScheduler->onPrimaryDisplayModeChanged(mAppConnectionHandle, display->getActiveMode());
}

思考几个问题:
1)创建mScheduler的用处?
2)mScheduler->createConnection()具体做了什么?
3)mScheduler->initVsync()做了什么?

2.2.6 mScheduler详解

2.3

3 小结

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

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

相关文章

实现一个自定义前端脚手架

一、背景 什么前端脚手架&#xff1f; 前端脚手架是一种工具&#xff0c;用于快速搭建前端项目的基础结构&#xff0c;提供了一套约定和标准&#xff0c;帮助开发人员快速启动项目开发。常见的前端脚手架包括Create React App、Vue CLI、Angular CLI、nestjs/cli等。 我为什么…

C语言——二维数组和字符数组

二维数组 二维数组本质上是一个行列式的组合&#xff0c;也就是二维数组是有行和列两部分构成。二维数组数据是通过行列进行解读。 定义形式&#xff1a; 类型&#xff08;说明符&#xff09; 数组名[行数&#xff08;常量表达式1&#xff09;][列数&#xff08;常量表达式…

nginx负载均衡、java、tomcat装包

一、nginx 七层负载均衡 1、七层负载均衡基础配置 2、负载均衡状态 [rootserver]# vim /usr/local/nginx/conf/nginx.confworker_processes 1;event {worker_connections 1024&#xff1b;}http { # 七层负载均衡支持http、ftp协议include mime.types;default_type app…

T3学员领取资料通知0803

T3学员领取资料通知0803 各位学员∶本周MF系列VBA技术资料增加671-680讲&#xff0c;T3学员看到通知后请免费领取,领取时间8月2日晚上19:00-8月3日12:00。本次增加内容&#xff1a; MF671:Shell命令输出计算机的详细信息 MF672:Shell命令输出网络配置信息 MF673:解锁和启用…

QtQuick Text-文本省略

效果 import QtQuickColumn{spacing: 20Text{width: 200text: qsTr("1使文本在单行中对于超出部分不要进行省略")font.pointSize: 20}Text{width: 200elide: Text.ElideLefttext: qsTr("2使文本在单行中对于超出部分从左边进行省略")font.pointSize: 20}Te…

【协作提效 Go - gin ! swagger】

什么是swagger Swagger 是一个用于设计、构建、记录和使用 RESTful Web 服务的工具集。它的主要作用包括&#xff1a; API 文档生成&#xff1a;Swagger 可以自动生成详细的 API 文档&#xff0c;包括每个端点的请求和响应格式、参数、状态码等。这使得开发者和用户可以轻松理…

【香橙派系列教程】(五)Linux的热拔插UDEV机制

【五】Linux的热拔插UDEV机制 在上一篇中我们发现&#xff0c;当手机接入开发板时&#xff0c;系统并不认识&#xff0c;当我们在/etc/udev目录下创建一个规则后&#xff0c;就可以通过adb访问到手机了&#xff0c;这里到底是怎么回事&#xff1f; 文章目录 【五】Linux的热拔插…

【Python】数据类型(上)

本篇文章将讲解&#xff1a; &#xff08;1&#xff09;整型 &#xff08;2&#xff09;布尔类型 一&#xff1a;整型 整型其实就是十进制整数的统称&#xff0c;例如&#xff1a;1 666 都属于整型。 &#xff08;1&#xff09;定义 num11 age45 &#xff08…

【网络】网络入门(第一篇)

网络入门可以从多个方面开始&#xff0c;以下是一个基本的网络入门指南&#xff0c;涵盖了网络的基本概念、网络类型、网络协议、网络拓扑、网络设备以及网络地址等方面。 一、网络基本概念 计算机网络&#xff1a;将多个计算机系统和设备连接在一起&#xff0c;以实现资源共…

Opencv学习-LUT函数

这个函数大概意思根据自己设定的查找表&#xff0c;改变原本像素点值 例如&#xff1a;我们想将一张图片灰度为0-100的像素的灰度变成0,101-200的变成100,201-255的变成255。我们就可已建立如下的一张表格 ​​​​​​​ ​​​​​​​ ​​​​​​​…

Studying-代码随想录训练营day52| 101.孤岛的总面积、102沉没孤岛、103.水流问题、104.建造最大岛屿

第52天&#xff0c;图论part03&#xff0c;岛屿问题继续&#xff01;&#xff01;&#x1f4aa;(ง •_•)ง&#xff0c;编程语言&#xff1a;C 目录 101.孤岛的总面积 102沉没孤岛 103.水流问题 104.建造最大岛屿 101.孤岛的总面积 文档讲解&#xff1a;手撕孤岛的总…

昇思25天学习打卡营第XX天|SSD目标检测

感觉目标检测还是yolo相对最火&#xff1f;ssd有点老了可以更新下 SSD算法数学描述 SSD算法使用卷积神经网络&#xff08;CNN&#xff09;进行特征提取&#xff0c;并通过多尺度的特征图进行目标检测。设 ( C ) 为CNN输出的特征层数量&#xff0c;( F_i ) 为第 ( i ) 层特征…

【Postman的接口测试工具介绍】

🌈个人主页: 程序员不想敲代码啊 🏆CSDN优质创作者,CSDN实力新星,CSDN博客专家 👍点赞⭐评论⭐收藏 🤝希望本文对您有所裨益,如有不足之处,欢迎在评论区提出指正,让我们共同学习、交流进步! 👉Postman接口.👋 👉Postman是一款常用的API开发测试工具,它提…

Harmony学习(四)(应用程序框架基础)

1.应该程序框架基础 多Module设计机制 模块化开发&#xff1a;一个应用多个功能&#xff0c;每个功能可作为一个模块&#xff0c;Module中可以包含源代码、资源文件、第三方库、配置文件等&#xff0c;每一个Module可以独立编译&#xff0c;实现特定的功能支持多设备&#xf…

jdk的版本匹配 Usage of ApI documented as @since 11+

IDEA 解决 Usage of API documented as since XX 的方法 如下所示&#xff0c;代码已经报错提示。 这个问题的原因是IDEA 设置的jdk Language level 语法级别太低&#xff0c;不适配代码导致的&#xff0c;只要在项目结构中将语法级别调相应的级别就可以了。具体解决思路见下图…

vue-创建自己的CLI脚手架

1.自定义命令和入口配置 首先创建一个文件夹&#xff0c;然后npm init -y生成package.json文件 添加bin命令配置入口文件 新建lib/index.js文件 然后在控制台npm link 建立软连接 、然后执行felix-cli 就可以输出代码 2.查看当前版本号命令 安装 commander npm i commander…

系统架构设计师 - 企业信息化战略与实施

企业信息化战略与实施 企业信息化战略与实施信息与信息化的概念信息的定义信息的特点信息化的概念信息化对组织的意义 信息系统生命周期 ★立项阶段开发阶段运维阶段消亡阶段 信息系统战略规划 ★ ★ ★政府信息化与电子政务 ★企业信息化与电子商务 ★ ★ ★企业资源计划企业资…

2024年让短片制作不再难,4款剪辑软件助你一臂之力!

在这个短视频流行的时代&#xff0c;每一个创意都值得被展现&#xff0c;每一份热情都值得被激发。你是不是也曾经想过&#xff0c;用镜头来讲述你的故事&#xff0c;用剪辑来展示你的才华&#xff1f;今天&#xff0c;我们一起来探索2024年制作高质量短片的秘密武器——4款强大…

gitignore文件设置,git提交时忽略部分文件

在git提交时&#xff0c;出现了非常多无用的文件&#xff0c;包括.idea、.iml文件等等&#xff0c;使得commit变得麻烦&#xff0c;要自己在勾选框中点击半天。 右键单击项目名&#xff0c;选择New 选择File,命名为.gitignore&#xff08;注意&#xff1a;开头符号是英文.&…

文件描述符(fileno)及文件系统

fileno: #include <stdio.h> main() {FILE *fp;int fd;fp fopen("/etc/passwd", "r");fd fileno(fp);printf("fd %d\n", fd);fclose(fp); } 一&#xff0e;fileno()函数-CSDN博客https://blog.csdn.net/TuxedoLinux/article/detai…