Android 图像显示框架三——演示demo以及解析

目录

1.应用demo演示

2.应用demo代码解析

上面重点的函数是onFirstRef,详细讲解下此函数:

首先main函数详细解析如下:

[1. ​​缓冲区队列管理(核心机制)​​](#1. 缓冲区队列管理(核心机制))

出队-渲染-入队模式

[2. ​​同步机制(Fence系统)​​](#2. 同步机制(Fence系统))

[3. ​​CPU渲染技术​​](#3. CPU渲染技术)

软件渲染实现

[4. ​​颜色动画逻辑​​](#4. 颜色动画逻辑)


1.应用demo演示

首先要知道应用层与SurfaceFlinger如何交互,就要知道应用层用哪些api与SurfaceFlinger进行交互的,最快的方式是实现一个应用与SurfaceFlinger交互的演示demo,演示demo详细流程如下:

Android 12(S) 图像显示系统 - 示例应用(二) - 二的次方 - 博客园

2.应用demo代码解析

上述的demo可以实现一秒钟切换一种颜色,demo中源代码有NativeSurfaceWrapper.h、NativeSurfaceWrapper.cpp、main_NativeSFDemo.cpp三个文件,我们进行详细解析:

首先是NativeSurfaceWrapper.h文件:

复制代码
/*
 * Copyright (C) 2021 The Android Open Source Project
 * 
 * Native Surface包装器头文件
 * 功能:声明用于创建和管理Android Surface的包装类
 * 该类封装了与SurfaceFlinger交互的复杂逻辑,提供简化的接口
 */

#ifndef SURFACE_WRAPPER_H
#define SURFACE_WRAPPER_H

// BLASTBufferQueue - 现代缓冲区队列实现,用于高效的缓冲区管理
#include <gui/BLASTBufferQueue.h>

// IGraphicBufferProducer - 图形缓冲区生产者接口,用于向Surface提交图形数据
#include <gui/IGraphicBufferProducer.h>

// Surface相关头文件
#include <gui/Surface.h>
#include <gui/SurfaceControl.h>

// 系统窗口相关定义
#include <system/window.h>

// RefBase - Android引用计数基类,提供自动内存管理
#include <utils/RefBase.h>

namespace android {

/**
 * @brief Native Surface包装器类
 * 
 * 这个类封装了创建和管理Android Surface的复杂逻辑,提供简化的接口用于:
 * - 创建全屏Surface
 * - 配置显示属性(层级、尺寸、格式等)
 * - 管理图形缓冲区队列
 * - 与SurfaceFlinger服务交互
 * 
 * 继承自RefBase,支持Android智能指针系统,实现自动内存管理
 */
class NativeSurfaceWrapper : public RefBase {
public:
    /**
     * @brief 构造函数
     * @param name Surface的名称标识,用于调试和日志
     * @param layerStack 显示层栈ID,默认为0(主显示器)
     *            用于多显示器配置,指定Surface显示在哪个显示器上
     */
    NativeSurfaceWrapper(const String8& name, uint32_t layerStack=0);
    
    /**
     * @brief 虚析构函数
     * 
     * 声明为虚函数以确保正确的多态销毁
     * 使用默认实现,由智能指针系统自动管理资源释放
     */
    virtual ~NativeSurfaceWrapper() {}

    /**
     * @brief 首次引用回调函数
     * 
     * 当对象首次被sp<>智能指针引用时自动调用
     * 在此方法中完成实际的Surface创建和配置工作,包括:
     * - 连接SurfaceFlinger服务
     * - 获取显示器配置信息
     * - 创建SurfaceControl对象
     * - 初始化BLASTBufferQueue
     * - 配置Surface显示属性
     */
    virtual void onFirstRef();

    /**
     * @brief 设置图形缓冲区生产者
     * @param producer 输出的生产者接口引用
     * 
     * 获取配置好的IGraphicBufferProducer接口,供外部进行图形渲染:
     * - 从BLASTBufferQueue获取生产者接口
     * - 配置缓冲区队列参数(如双缓冲设置)
     * - 连接生产者到Surface
     */
    void setUpProducer(sp<IGraphicBufferProducer>& producer);

    /**
     * @brief 获取Surface宽度
     * @return Surface的像素宽度
     */
    int width() { return mWidth; }
    
    /**
     * @brief 获取Surface高度  
     * @return Surface的像素高度
     */
    int height() { return mHeight; }

private:
    // 禁止拷贝构造和赋值操作,确保单例性
    DISALLOW_COPY_AND_ASSIGN(NativeSurfaceWrapper);

    /**
     * @brief 根据系统限制调整Surface尺寸
     * @param width 原始宽度
     * @param height 原始高度
     * @return 调整后的尺寸,保持宽高比
     * 
     * 检查系统属性中的最大图形尺寸限制,防止创建过大的Surface
     * 系统属性:
     * - ro.surface_flinger.max_graphics_width
     * - ro.surface_flinger.max_graphics_height
     */
    ui::Size limitSurfaceSize(int width, int height) const;

    // 成员变量
    sp<BLASTBufferQueue> mBlastBufferQueue;  ///< BLAST缓冲区队列,管理图形缓冲区生命周期
    sp<SurfaceControl> mSurfaceControl;       ///< Surface控制对象,管理Surface属性和状态
    int mWidth;                               ///< Surface宽度(像素)
    int mHeight;                              ///< Surface高度(像素)
    String8 mName;                            ///< Surface名称,用于调试识别
    uint32_t mLayerStack;                     ///< 显示层栈ID,用于多显示器配置
};

} // namespace android

#endif // SURFACE_WRAPPER_H

然后来看下NativeSurfaceWrapper.cpp文件的详细解析:

cpp 复制代码
/*
 * Copyright (C) 2021 The Android Open Source Project
 * 
 * Native Surface包装器实现文件
 * 功能:封装Surface创建、配置和管理的复杂逻辑
 */

#define LOG_TAG "NativeSurfaceWrapper"

#include <android-base/properties.h>
#include <gui/ISurfaceComposerClient.h>
#include <gui/Surface.h>
#include <gui/SurfaceComposerClient.h>
#include <utils/Log.h>

#include "NativeSurfaceWrapper.h"

namespace android {

/**
 * @brief 构造函数
 * @param name Surface名称(用于调试识别)
 * @param layerStack 显示层栈ID
 */
NativeSurfaceWrapper::NativeSurfaceWrapper(const String8& name, uint32_t layerStack) 
    : mName(name), mLayerStack(layerStack) {}

/**
 * @brief 首次引用时的初始化回调
 * 
 * 这是Android智能指针系统的标准模式,在对象首次被sp<>引用时自动调用
 * 完成Surface创建、显示配置和缓冲区队列初始化等关键操作
 */
void NativeSurfaceWrapper::onFirstRef() {
    // 创建SurfaceComposerClient,建立与SurfaceFlinger服务的连接
    sp<SurfaceComposerClient> surfaceComposerClient = new SurfaceComposerClient;
    status_t err = surfaceComposerClient->initCheck();
    if (err != NO_ERROR) {
        ALOGD("SurfaceComposerClient::initCheck error: %#x\n", err);
        return;
    }

    // 获取主显示器的标识令牌
    sp<IBinder> displayToken = SurfaceComposerClient::getInternalDisplayToken();
    if (displayToken == nullptr) {
        ALOGE("Failed to get internal display token");
        return;
    }

    // 获取当前显示模式信息(分辨率、刷新率等)
    ui::DisplayMode displayMode;
    const status_t error = SurfaceComposerClient::getActiveDisplayMode(displayToken, &displayMode);
    if (error != NO_ERROR) {
        ALOGE("Failed to get active display mode: %#x", error);
        return;
    }

    // 根据系统限制调整Surface尺寸
    ui::Size resolution = displayMode.resolution;
    resolution = limitSurfaceSize(resolution.width, resolution.height);
    
    // 创建SurfaceControl对象,管理Surface的生命周期
    // eFXSurfaceBufferState表示使用现代BufferState图层类型
    sp<SurfaceControl> surfaceControl = surfaceComposerClient->createSurface(
        mName,                          // Surface名称
        resolution.getWidth(),          // 宽度
        resolution.getHeight(),         // 高度  
        PIXEL_FORMAT_RGBA_8888,         // 像素格式
        ISurfaceComposerClient::eFXSurfaceBufferState,  // Surface类型
        /*parent*/ nullptr              // 父Surface(无)
    );

    if (surfaceControl == nullptr) {
        ALOGE("Failed to create surface control");
        return;
    }

    // 配置Surface的显示属性
    SurfaceComposerClient::Transaction{}
            .setLayer(surfaceControl, std::numeric_limits<int32_t>::max())  // 设置最高层级(最前面)
            .show(surfaceControl)                                           // 显示Surface
            .setBackgroundColor(surfaceControl, half3{0, 0, 0}, 1.0f, ui::Dataspace::UNKNOWN) // 黑色背景
            .setAlpha(surfaceControl, 1.0f)                                 // 不透明度100%
            .setLayerStack(surfaceControl, mLayerStack)                     // 设置显示层栈
            .apply();                                                       // 应用所有配置

    // 保存Surface尺寸信息
    mWidth = resolution.getWidth();
    mHeight = resolution.getHeight();

    // 创建BLASTBufferQueue(BufferQueue的现代实现)
    // BLAST = Buffer Layout and Surface Control
    mBlastBufferQueue = new BLASTBufferQueue(
        "DemoBLASTBufferQueue",        // 队列名称
        surfaceControl,                // 关联的SurfaceControl
        resolution.getWidth(),         // 缓冲区宽度
        resolution.getHeight(),        // 缓冲区高度
        PIXEL_FORMAT_RGBA_8888        // 缓冲区格式
    );
    
    mSurfaceControl = surfaceControl;  // 保存SurfaceControl引用
}

/**
 * @brief 设置并配置图形缓冲区生产者
 * @param producer 输出的生产者接口指针
 * 
 * 配置缓冲区队列的生产者端,供应用进行图形渲染
 */
void NativeSurfaceWrapper::setUpProducer(sp<IGraphicBufferProducer>& producer) {
    // 获取BLASTBufferQueue的生产者接口
    producer = mBlastBufferQueue->getIGraphicBufferProducer();
    
    // 设置最大出队缓冲区数量(双缓冲配置)
    producer->setMaxDequeuedBufferCount(2);
    
    // 连接生产者,注册为CPU渲染客户端
    IGraphicBufferProducer::QueueBufferOutput qbOutput;
    producer->connect(
        new StubProducerListener,  // 空的监听器(简化实现)
        NATIVE_WINDOW_API_CPU,     // 使用CPU进行渲染
        false,                     // 不启用生产者受控缓冲
        &qbOutput                  // 输出队列配置信息
    );
}

/**
 * @brief 根据系统属性限制Surface尺寸
 * @param width 原始宽度
 * @param height 原始高度
 * @return 调整后的尺寸
 * 
 * 防止创建过大的Surface消耗过多系统资源,保持宽高比不变
 */
ui::Size NativeSurfaceWrapper::limitSurfaceSize(int width, int height) const {
    ui::Size limited(width, height);
    bool wasLimited = false;
    const float aspectRatio = float(width) / float(height);

    // 从系统属性读取最大允许的图形尺寸
    int maxWidth = android::base::GetIntProperty("ro.surface_flinger.max_graphics_width", 0);
    int maxHeight = android::base::GetIntProperty("ro.surface_flinger.max_graphics_height", 0);

    // 如果宽度超过限制,按比例调整高度
    if (maxWidth != 0 && width > maxWidth) {
        limited.height = maxWidth / aspectRatio;
        limited.width = maxWidth;
        wasLimited = true;
    }
    
    // 如果调整后的高度仍然超过限制,按比例调整宽度
    if (maxHeight != 0 && limited.height > maxHeight) {
        limited.height = maxHeight;
        limited.width = maxHeight * aspectRatio;
        wasLimited = true;
    }
    
    // 记录尺寸调整日志(调试用)
    SLOGV_IF(wasLimited, "Surface size has been limited to [%dx%d] from [%dx%d]",
             limited.width, limited.height, width, height);
             
    return limited;
}

} // namespace android

上面重点的函数是onFirstRef,详细讲解下此函数:

  1. 创建一个SurfaceComposerClient对象,这个是SurfaceFlinger的Client端,用于和SurfaceFlinger进行跨进程通信的
  2. 进行主显示器token令牌校验以及当前屏幕显示模式信息(分辨率、刷新率等)校验,都校验通过进行下面的流程
  3. 获取屏幕参数,SurfaceComposerClient::getActiveDisplayMode获取当前的DisplayMode,其中可以得到resolution信息,并根据当前系统限制调用limitSurfaceSize函数进行resolution的调整
  4. 创建Surface & SurfaceControl,然后将resolution中获取的宽高信息以及设置像素信息传递给createSurface函数,createSurface函数会进行与SurfaceFlinger进行binder跨进程通信,此时SurfaceFlinger会创建Layer等操作并返回SurfaceControl对象
  5. setLayer,设置视图在当窗口的z-order,最大值则在最前面,SurfaceFlinger根据当前设置的z-order决定窗口的可见性以及显示区域大小
  6. show,让当前的视图显示出来
  7. apply,提交事务,使透过Transaction进行设置的属性生效,一次性调用传递给SurfaceFlinger

然后再来看看main.cpp文件的详细解析:

cpp 复制代码
/*
 * Copyright (C) 2021 The Android Open Source Project
 * 
 * Native SurfaceFlinger 演示程序 - 主程序文件
 * 功能:创建一个全屏窗口并循环显示红绿蓝三种颜色
 */

#define LOG_TAG "NativeSFDemo"

#include <binder/IPCThreadState.h>
#include <binder/ProcessState.h>
#include <binder/IServiceManager.h>
#include <hardware/gralloc.h>
#include <ui/GraphicBuffer.h>
#include <utils/Log.h>

#include "NativeSurfaceWrapper.h"

using namespace android;

// 全局控制变量
bool mQuit = false;        // 程序退出标志
int mLayerStack = 0;       // 显示层栈(用于多显示器配置)

/**
 * @brief 使用指定颜色填充RGBA8888格式的图像缓冲区
 * @param img 指向图像数据的指针
 * @param width 图像宽度
 * @param height 图像高度
 * @param stride 图像行跨度(可能包含填充字节)
 * @param r 红色分量 (0-255)
 * @param g 绿色分量 (0-255)
 * @param b 蓝色分量 (0-255)
 * 
 * 该函数通过CPU直接写入像素数据,适用于软件渲染场景
 */
void fillRGBA8Buffer(uint8_t* img, int width, int height, int stride, int r, int g, int b) {
    // 遍历每个像素点进行填充
    for (int y = 0; y < height; y++) {
        for (int x = 0; x < width; x++) {
            // 计算当前像素位置(每个像素4字节:RGBA)
            uint8_t* pixel = img + (4 * (y*stride + x));
            pixel[0] = r;  // Red
            pixel[1] = g;  // Green
            pixel[2] = b;  // Blue
            pixel[3] = 0;  // Alpha(设置为0表示不透明)
        }
    }
}

/**
 * @brief 主绘制函数,负责图形缓冲区的循环渲染
 * @param nativeSurface NativeSurfaceWrapper实例
 * @return 错误状态码
 * 
 * 实现典型的图形渲染流水线:
 * 1. 出队缓冲区 → 2. CPU渲染 → 3. 入队显示
 */
int drawNativeSurface(sp<NativeSurfaceWrapper> nativeSurface) {
    status_t err = NO_ERROR;
    int countFrame = 0;  // 帧计数器,用于颜色循环
    
    // 获取图形缓冲区生产者接口
    sp<IGraphicBufferProducer> igbProducer;
    nativeSurface->setUpProducer(igbProducer);

    // 主渲染循环:每秒绘制一帧,直到收到退出信号
    while(!mQuit) {
        int slot;           // 缓冲区槽位索引
        sp<Fence> fence;   // 同步栅栏,确保缓冲区可用
        sp<GraphicBuffer> buf;  // 图形缓冲区对象
        
        // 步骤1: 从生产者队列中获取一个可用的缓冲区
        igbProducer->dequeueBuffer(&slot, &fence, nativeSurface->width(), nativeSurface->height(),
                                              PIXEL_FORMAT_RGBA_8888, GRALLOC_USAGE_SW_WRITE_OFTEN,
                                              nullptr, nullptr);
        
        // 获取对应槽位的缓冲区对象
        igbProducer->requestBuffer(slot, &buf);

        // 等待缓冲区就绪(同步操作,避免竞争条件)
        int waitResult = fence->waitForever("dequeueBuffer_EmptyNative");
        if (waitResult != OK) {
            ALOGE("dequeueBuffer_EmptyNative: Fence::wait returned an error: %d", waitResult);
            break;
        }
        
        // 步骤2: 锁定缓冲区并进行CPU渲染
        uint8_t* img = nullptr;
        err = buf->lock(GRALLOC_USAGE_SW_WRITE_OFTEN, (void**)(&img));
        if (err != NO_ERROR) {
            ALOGE("error: lock failed: %s (%d)", strerror(-err), -err);
            break;
        }

        // 计算当前帧颜色(红绿蓝循环)
        countFrame = (countFrame+1)%3;
        fillRGBA8Buffer(img, nativeSurface->width(), nativeSurface->height(), buf->getStride(),
                        countFrame == 0 ? 255 : 0,  // 红
                        countFrame == 1 ? 255 : 0,  // 绿  
                        countFrame == 2 ? 255 : 0); // 蓝

        // 解锁缓冲区,提交渲染结果
        err = buf->unlock();
        if (err != NO_ERROR) {
            ALOGE("error: unlock failed: %s (%d)", strerror(-err), -err);
            break;
        }
        
        // 步骤3: 将渲染完成的缓冲区提交到显示队列
        IGraphicBufferProducer::QueueBufferOutput qbOutput;
        IGraphicBufferProducer::QueueBufferInput input(
            systemTime(),                    // 时间戳
            true,                            // 自动时间戳
            HAL_DATASPACE_UNKNOWN,           // 数据空间
            {},                              // 裁剪区域(全屏)
            NATIVE_WINDOW_SCALING_MODE_FREEZE, // 缩放模式
            0,                               // 变换
            Fence::NO_FENCE                  // 同步栅栏
        );
        igbProducer->queueBuffer(slot, input, &qbOutput);

        // 控制帧率:每秒一帧
        sleep(1);
    }

    return err;
}

/**
 * @brief 信号处理函数,用于优雅退出程序
 * @param num 信号编号
 */
void sighandler(int num) {
    if(num == SIGINT) {
        printf("\nSIGINT: Force to stop !\n");
        mQuit = true;  // 设置退出标志,终止渲染循环
    }
}

/**
 * @brief 显示程序使用说明
 * @param me 程序名称
 */
static void usage(const char *me)
{
    fprintf(stderr, "\nusage: \t%s [options]\n"
                    "\t--------------------------------------- options ------------------------------------------------\n"
                    "\t[-h] help\n"
                    "\t[-d] layer stack(In the case of multi-display, NativeSFDemo shows on the specified displays \n"
                    "\t                 in addition to the primary display)\n"
                    "\t------------------------------------------------------------------------------------------------\n",
                    me);
    exit(1);
}

/**
 * @brief 解析命令行参数
 * @param argc 参数个数
 * @param argv 参数数组
 */
void parseOptions(int argc, char **argv) {
    const char *me = argv[0];
    int res;
    while((res = getopt(argc, argv, "d:")) >= 0) {
        switch(res) {
            case 'd':
                mLayerStack = atoi(optarg);  // 设置显示层栈
                break;
            case 'h':
            default:
            {
                usage(me);  // 显示帮助信息
            }
        }
    }
}

/**
 * @brief 程序主入口
 * @param argc 命令行参数个数
 * @param argv 命令行参数数组
 * @return 程序退出码
 * 
 * 程序执行流程:
 * 1. 解析参数 → 2. 设置信号处理 → 3. 创建Surface → 4. 进入渲染循环
 */
int main(int argc, char ** argv) {
    // 初始化阶段
    parseOptions(argc, argv);        // 解析命令行参数
    signal(SIGINT, sighandler);      // 注册Ctrl+C信号处理
    
    // 创建Native Surface包装器实例
    sp<NativeSurfaceWrapper> nativeSurface(new NativeSurfaceWrapper(String8("NativeSFDemo"), mLayerStack));
    
    // 进入主渲染循环
    drawNativeSurface(nativeSurface);
    
    return 0;
}

首先main函数详细解析如下:

  1. signal函数注册监听SIGINT信号的handler,注册Ctrl+C信号处理,使其可以优雅的退出程序
  2. 创建NativeSurfaceWrapper对象,并调用drawNativeSurface进行图片的绘制

终端默认的键盘信号映射:

Ctrl+C → SIGINT (中断进程)

Ctrl+Z → SIGTSTP (暂停进程)

Ctrl+\ → SIGQUIT (退出并核心转储)

Ctrl+D → EOF (文件结束)

然后来解析下drawNativeSurface函数中执行的流程以及重点内容

首先主要流程如下:

主循环开始

dequeueBuffer() 获取空闲缓冲区

requestBuffer() 获取缓冲区对象

fence->wait() 等待缓冲区可用

buf->lock() 锁定缓冲区

fillRGBA8Buffer() CPU渲染颜色

buf->unlock() 解锁缓冲区

queueBuffer() 提交显示

sleep(1) 控制帧率

检查mQuit标志 → 继续循环或退出

整体架构图

应用进程 (本程序)

↓ 通过igbProducer (Binder IPC)

SurfaceFlinger进程

显示硬件

具体角色定位

// igbProducer是连接应用和SurfaceFlinger的桥梁

应用层 (Producer) ←→ igbProducer ←→ BufferQueue ←→ SurfaceFlinger (Consumer)

(生产者接口) (缓冲区队列) (消费者)

重点内容提取:

1. ​​缓冲区队列管理(核心机制)​

出队-渲染-入队模式
cpp 复制代码
// 步骤1:出队缓冲区(获取可用的缓冲区)
igbProducer->dequeueBuffer(&slot, &fence, width, height, format, usage);

// 步骤2:获取缓冲区对象
igbProducer->requestBuffer(slot, &buf);

// 步骤3:渲染内容到缓冲区
// ... 渲染操作 ...

// 步骤4:入队缓冲区(提交显示)
igbProducer->queueBuffer(slot, input, &qbOutput);

​关键技术点​​:

  • ​双缓冲/三缓冲机制​​:避免显示撕裂

  • ​Slot管理​​:缓冲区槽位复用

  • ​生产者-消费者模式​​:解耦渲染和显示

2. ​​同步机制(Fence系统)​

cpp 复制代码
// 等待缓冲区就绪的同步操作
int waitResult = fence->waitForever("dequeueBuffer_EmptyNative");

// Fence的作用:
// - 确保GPU已完成对缓冲区的操作
// - 防止CPU和GPU访问冲突
// - 实现渲染流水线的正确同步

​重要概念​​:

  • ​GPU-CPU同步​​:避免资源竞争

  • ​流水线控制​​:保证渲染时序正确

  • ​超时处理​ ​:waitForevervs wait(带超时)

3. ​​CPU渲染技术​

软件渲染实现
cpp 复制代码
// 等待缓冲区就绪的同步操作
int waitResult = fence->waitForever("dequeueBuffer_EmptyNative");

// Fence的作用:
// - 确保GPU已完成对缓冲区的操作
// - 防止CPU和GPU访问冲突
// - 实现渲染流水线的正确同步

​渲染细节​​:

  • ​内存布局​​:RGBA8888格式(每个像素4字节)

  • ​步长(Stride)处理​​:可能包含内存对齐的填充字节

  • ​颜色格式​​:RGBA颜色空间,Alpha通道未使用

4. ​​颜色动画逻辑​

cpp 复制代码
// 简单的颜色循环算法
countFrame = (countFrame+1)%3;
fillRGBA8Buffer(..., 
    countFrame == 0 ? 255 : 0,  // 红
    countFrame == 1 ? 255 : 0,  // 绿  
    countFrame == 2 ? 255 : 0); // 蓝

​动画特性​​:

  • ​三色循环​​:红→绿→蓝→红...

  • ​帧率控制​​:每秒1帧(sleep(1))

  • ​状态保持​ ​:countFrame维护动画状态

上述就是对demo的解析

相关推荐
QuantumLeap丶3 小时前
《Flutter全栈开发实战指南:从零到高级》- 11 -状态管理Provider
android·flutter·ios
百锦再3 小时前
第6章 结构体与方法
android·java·c++·python·rust·go
gustt3 小时前
用小程序搭建博客首页:从数据驱动到界面展示
android·前端·微信小程序
金鸿客3 小时前
Compose从相册和系统相机拍照获取照片
android
IT乐手4 小时前
Android 获取定位信息工具类
android
yangjunjin4 小时前
Android ANR的解决方案
android
低调小一4 小时前
Android Gradle 的 compileOptions 与 Kotlin jvmTarget 全面理解(含案例)
android·开发语言·kotlin
苦学编程啊8 小时前
【2025Flutter 入门指南】Dart SDK 安装与 VS Code 环境配置-Windows
android·dart
朝新_14 小时前
【SpringMVC】详解用户登录前后端交互流程:AJAX 异步通信与 Session 机制实战
前端·笔记·spring·ajax·交互·javaee