SurfaceFlinger GraphicBuffer介绍(6)

SurfaceFlinger GraphicBuffer介绍

简介

在前面介绍BlastBufferQueue的时候我们经常提到GraphicBuffer,这篇文章就来介绍一下这个GraphicBuffer。因为GraphicBuffer最终实现是基于linux的drm。我们目前不会介绍太多关于drm的知识,所以这篇文章主要是介绍一下GraphicBuffer分配的通路流程,不会进一步去看linux的drm实现。
drm分配的buffer是dma buffer,dma buffer是一块可以在cpu和其他io外设共享的buffer,他关联一个物理buffer和一个文件fd,fd是媒介,我们就是通过fd将buffer在不同驱动之间流转。

GraphicBuffer构造

我们就从GraphicBuffer构造入手,来了解一下一下他们怎么调用到drm接口构造buffer的
在这里插入图片描述

1.1 GraphicBuffer::GraphicBuffer
构造函数传入来许多参数,然后调用initWithSize来初始化

GraphicBuffer::GraphicBuffer(uint32_t inWidth, uint32_t inHeight, PixelFormat inFormat,
                            uint32_t inLayerCount, uint64_t inUsage, std::string requestorName)
    : GraphicBuffer() {
    mInitCheck = initWithSize(inWidth, inHeight, inFormat, inLayerCount, inUsage,
                            std::move(requestorName));
}

1.2 status_t GraphicBuffer::initWithHandle
GraphicBuffer继承自ANativeWindowBuffer,下面也列出了ANativeWindowBuffer结构体,标注了部分变量的含义。
initWithSize通过调用GraphicBufferAllocator::allocate分配一个buffer,然后将参数赋值到这些变量里

status_t GraphicBuffer::initWithSize(uint32_t inWidth, uint32_t inHeight,
        PixelFormat inFormat, uint32_t inLayerCount, uint64_t inUsage,
        std::string requestorName)
{
    // 分配buffer的逻辑在这里
    // mBufferMapper是一个GraphicBufferMapper实例,我们先看下GraphicBufferMapper构造函数,见1.2.1
    // allocate,详见1.3
    GraphicBufferAllocator& allocator = GraphicBufferAllocator::get();
    uint32_t outStride = 0;
    status_t err = allocator.allocate(inWidth, inHeight, inFormat, inLayerCount,
            inUsage, &handle, &outStride, mId,
            std::move(requestorName));
    if (err == NO_ERROR) {
        // 将入参存到ANativeWindowBuffer结构体里
        mBufferMapper.getTransportSize(handle, &mTransportNumFds, &mTransportNumInts);
        width = static_cast<int>(inWidth);
        height = static_cast<int>(inHeight);
        format = inFormat;
        layerCount = inLayerCount;
        usage = inUsage;
        usage_deprecated = int(usage);
        stride = static_cast<int>(outStride);
    }
    return err;
}

typedef struct ANativeWindowBuffer
{
    struct android_native_base_t common; // 里面有一个魔数,标记他是GraphicBuffer,有一个版本号,还有两个函数指针,分别是incRef,decRef
    int width; // buffer代表的图像宽度
    int height; // buffer代表的图像高度
    int stride; // 步长,表示一个像素需要多少字节
    int format; // 图像的编码格式,比如RGBA
    int usage_deprecated;
    uintptr_t layerCount;
    void *reserved[1];
    const native_handle_t *handle; // 句柄,指向底层的buffer
    uint64_t usage;
    void *reserved_proc[8 - (sizeof(uint64_t) / sizeof(void *))];
} ANativeWindowBuffer_t;

1.2.1 GraphicBufferAllocator::GraphicBufferAllocator()
我们一会会看到,实际调用allocate会调用mAllocator的allocate,这里不同版本就有不同的逻辑。
mAllocator也会调用GraphicBufferMapper的一些方法,GraphicBufferMapper的结构和GraphicBufferAllocator有一些类似的地方,我们看看他的构造函数就知道来,详见1.2.2。

GraphicBufferAllocator::GraphicBufferAllocator() : mMapper(GraphicBufferMapper::getInstance()) {
    switch (mMapper.getMapperVersion()) {
        case GraphicBufferMapper::GRALLOC_5:
            mAllocator = std::make_unique<const Gralloc5Allocator>(
                    reinterpret_cast<const Gralloc5Mapper&>(mMapper.getGrallocMapper()));
            break;
        case GraphicBufferMapper::GRALLOC_4:
            mAllocator = std::make_unique<const Gralloc4Allocator>(
                    reinterpret_cast<const Gralloc4Mapper&>(mMapper.getGrallocMapper()));
            break;
        case GraphicBufferMapper::GRALLOC_3:
            mAllocator = std::make_unique<const Gralloc3Allocator>(
                    reinterpret_cast<const Gralloc3Mapper&>(mMapper.getGrallocMapper()));
            break;
        case GraphicBufferMapper::GRALLOC_2:
            mAllocator = std::make_unique<const Gralloc2Allocator>(
                    reinterpret_cast<const Gralloc2Mapper&>(mMapper.getGrallocMapper()));
            break;
    }
    LOG_ALWAYS_FATAL_IF(!mAllocator->isLoaded(),
                        "Failed to load matching allocator for mapper version %d",
                        mMapper.getMapperVersion());
}

1.2.2 GraphicBufferMapper::GraphicBufferMapper
实际的逻辑都是通过调用mMapper实现的,mMapper是一个Mapper类型的变量,GraphicBufferMapper相当于是Mapper装饰类,通过这种方式去在这里判断使用不同版本的逻辑。

GraphicBufferMapper::GraphicBufferMapper() {
    mMapper = std::make_unique<const Gralloc5Mapper>();
    if (mMapper->isLoaded()) {
        mMapperVersion = Version::GRALLOC_5;
        return;
    }
    mMapper = std::make_unique<const Gralloc4Mapper>();
    if (mMapper->isLoaded()) {
        mMapperVersion = Version::GRALLOC_4;
        return;
    }
    mMapper = std::make_unique<const Gralloc3Mapper>();
    if (mMapper->isLoaded()) {
        mMapperVersion = Version::GRALLOC_3;
        return;
    }
    mMapper = std::make_unique<const Gralloc2Mapper>();
    if (mMapper->isLoaded()) {
        mMapperVersion = Version::GRALLOC_2;
        return;
    }

    LOG_ALWAYS_FATAL("gralloc-mapper is missing");
}

1.3 GraphicBufferAllocator::allocate
什么都没做,就直接调用了allocateHelper

status_t GraphicBufferAllocator::allocate(uint32_t width, uint32_t height, PixelFormat format,
                                      uint32_t layerCount, uint64_t usage,
                                      buffer_handle_t* handle, uint32_t* stride,
                                      std::string requestorName) {
    // 其他什么也没做,直接调用了allocateHelper,详见1.4
    return allocateHelper(width, height, format, layerCount, usage, handle, stride, requestorName,
                        true);
}

1.4 GraphicBufferAllocator::allocateHelper
调用了mAllocator->allocate来分配buffer,前面1.2.1初始化来mAllocator变量,我们这里就看最新版本的,Gralloc5Allocator的流程。
后面计算了buffer大小,然后将buffer的信息和句柄存储到一个list中(实际是个map)。

status_t GraphicBufferAllocator::allocateHelper(uint32_t width, uint32_t height, PixelFormat format,
                                                uint32_t layerCount, uint64_t usage,
                                                buffer_handle_t* handle, uint32_t* stride,
                                                std::string requestorName, bool importBuffer) {
    // 。。。参数的基本校验
    // 调用mAllocator->allocate,由1.2.1可知道,不同版本这里mAllocate不同,我们来分析Gralloc5Allocator的版本
    // 详见1.5
    status_t error = mAllocator->allocate(requestorName, width, height, format, layerCount, usage,
                                        1, stride, handle, importBuffer);
    // 。。。返回值错误情况处理

    if (!importBuffer) {
        return NO_ERROR;
    }
    size_t bufSize;

    // 计算buffer大小
    if ((*stride) != 0 &&
        std::numeric_limits<size_t>::max() / height / (*stride) < static_cast<size_t>(bpp)) {
        bufSize = static_cast<size_t>(width) * height * bpp;
    } else {
        bufSize = static_cast<size_t>((*stride)) * height * bpp;
    }
    // 将buffer的信息存储到list中
    Mutex::Autolock _l(sLock);
    KeyedVector<buffer_handle_t, alloc_rec_t>& list(sAllocList);
    alloc_rec_t rec;
    rec.width = width;
    rec.height = height;
    rec.stride = *stride;
    rec.format = format;
    rec.layerCount = layerCount;
    rec.usage = usage;
    rec.size = bufSize;
    rec.requestorName = std::move(requestorName);
    list.add(*handle, rec);

    return NO_ERROR;
}

1.5 Gralloc5Allocator::allocate
这里的mAllocator是aidl,会binder调用到另一个进程实现最终的buffer分配。
下面调用的mMapper.importBuffer,这里的mMapper是加载了一个so,将里面的方法地址映射到mMapper中去。
我们先来看下mAllocator的初始化以及mMapper的方法映射,然后再来看allocate2的实现。

status_t Gralloc5Allocator::allocate(std::string requestorName, uint32_t width, uint32_t height,
                                    android::PixelFormat format, uint32_t layerCount,
                                    uint64_t usage, uint32_t bufferCount, uint32_t *outStride,
                                    buffer_handle_t *outBufferHandles, bool importBuffers) const {
    // 将参数封装到BufferDescriptorInfo内
    auto descriptorInfo = makeDescriptor(requestorName, width, height, format, layerCount, usage);
    if (!descriptorInfo) {
        return BAD_VALUE;
    }

    AllocationResult result;
    // 这里mAllocator是一个aidl,这里会binder调用到另一个进程实现最终的buffer调用。
    // 同时下面mMapper.importBuffer这个方法是执行一个so库里的逻辑,这个会根据不同版本做映射
    // 继续往下看之前,我们来看一下这里的mAllocator赋值的地方,以及加载so,映射方法的逻辑,详见1.5.1。
    // allocate2详见1.6
    auto status = mAllocator->allocate2(*descriptorInfo, bufferCount, &result);
    // 。。。 错误检测

    if (importBuffers) {
        for (uint32_t i = 0; i < bufferCount; i++) {
            auto handle = makeFromAidl(result.buffers[i]);
            auto error = mMapper.importBuffer(handle, &outBufferHandles[i]);
            native_handle_delete(handle);
            if (error != NO_ERROR) {
                for (uint32_t j = 0; j < i; j++) {
                    mMapper.freeBuffer(outBufferHandles[j]);
                    outBufferHandles[j] = nullptr;
                }
                return error;
            }
        }
    } else {
        for (uint32_t i = 0; i < bufferCount; i++) {
            outBufferHandles[i] = dupFromAidl(result.buffers[i]);
            if (!outBufferHandles[i]) {
                for (uint32_t j = 0; j < i; j++) {
                    auto buffer = const_cast<native_handle_t *>(outBufferHandles[j]);
                    native_handle_close(buffer);
                    native_handle_delete(buffer);
                    outBufferHandles[j] = nullptr;
                }
                return NO_MEMORY;
            }
        }
    }

    *outStride = result.stride;

    return OK;
}

1.5.1 Gralloc5Allocator::Gralloc5Allocator/Gralloc5Mapper::Gralloc5Mapper
在Gralloc5Allocator和Gralloc5Mapper构造函数都会调用getInstance,在getInstance里会构造mAllocator和mMapper。
其中在waitForAllocator中就会去获取IAllocator的binder handle,返回IAllocator。
然后回加载一个so,loadIMapper里会将so方法的一直填充到mMapper里。
这里会从IAllocator方法里获取so的一部分名字,也就是说不同版本的IAllocator就会对应不同版本的so。

Gralloc5Allocator::Gralloc5Allocator(const Gralloc5Mapper &mapper) : mMapper(mapper) {
    mAllocator = getInstance().allocator;
}

Gralloc5Mapper::Gralloc5Mapper() {
    mMapper = getInstance().mapper;
}

static const Gralloc5 &getInstance() {
    static Gralloc5 instance = []() {
        // 这里的在等IAllocator::descriptor + std::string("/default")这个名字的service
        // 这里是一个binder, 也就是说真正实现的分配buffer的逻辑是在另一个进程里,我们通过binder去请求那个进程来分配
        // 并且会获取一个IAllocator对象,就是binder的client端。
        auto allocator = waitForAllocator();
        if (!allocator) {
            return Gralloc5{};
        }
        // 这里会通过IAllocator binder调用getIMapperLibrarySuffix获取一个前缀,然后和固定的文本拼接出一个so库的名字,然后加载这个so
        void *so = loadIMapperLibrary();
        if (!so) {
            return Gralloc5{};
        }
        // 加载这个so中的方法,这个方法是用来填充AIMapper的,AIMapper里面是一个版本号和一些函数指针。
        auto loadIMapper = (AIMapper_loadIMapperFn)dlsym(so, "AIMapper_loadIMapper");
        AIMapper *mapper = nullptr;
        // 通过so中的方法来填充AIMapper,主要是填充里面的函数指针,后续就可以通过这些函数来访问对端做buffer的操作了,详见1.5.3
        AIMapper_Error error = loadIMapper(&mapper);
        if (error != AIMAPPER_ERROR_NONE) {
            ALOGE("AIMapper_loadIMapper failed %d", error);
            return Gralloc5{};
        }
        return Gralloc5{std::move(allocator), mapper};
    }();
    return instance;
}

1.5.2 waitForAllocator
获取IAllocator binder代理

static std::shared_ptr<IAllocator> waitForAllocator() {
    if (__builtin_available(android 31, *)) {
        if (!AServiceManager_isDeclared(kIAllocatorServiceName.c_str())) {
            return nullptr;
        }
        // 就是获取一个IAllocator binder代理
        auto allocator = IAllocator::fromBinder(
                ndk::SpAIBinder(AServiceManager_waitForService(kIAllocatorServiceName.c_str())));
        // 。。。
        return allocator;
    } else {
        // TODO: LOG_ALWAYS_FATAL("libui is not backwards compatible");
        return nullptr;
    }
}

1.5.3 load
映射逻辑的代码路径在/hardware/interfaces/graphics/mapper/stable-c/implutils/include/android/hardware/graphics/mapper/utils/IMapperProvider.h
这里前面还有一层调用就不看来,最终是调用这个load方法,通过bindV5绑定方法地址

AIMapper_Error load(AIMapper* _Nullable* _Nonnull outImplementation) {
    std::call_once(mLoadOnceFlag, [this] {
        LOG_ALWAYS_FATAL_IF(provider::sIMapperInstance != nullptr,
                            "AIMapper implementation already loaded!");
        provider::sIMapperInstance = this;
        mImpl.emplace();
        mMapper.version = IMPL::version;
        if (IMPL::version >= AIMAPPER_VERSION_5) {
            // 绑定函数地址,详见1.5.4
            bindV5();
        }
    });
    *outImplementation = &mMapper;
    return AIMAPPER_ERROR_NONE;
}

1.5.4 bindV5

void bindV5() {
    mMapper.v5 = {
            .importBuffer = [](const native_handle_t* _Nonnull handle,
                               buffer_handle_t _Nullable* _Nonnull outBufferHandle)
                    -> AIMapper_Error { return impl().importBuffer(handle, outBufferHandle); },

            .freeBuffer = [](buffer_handle_t _Nonnull buffer) -> AIMapper_Error {
                return impl().freeBuffer(buffer);
            },

            // 。。。下面都是类似的方法绑定,这里的impl是template模板构造的时候传入的,这里是CrosGrallocMapperV5
            // 所以1.5的mMapper.importBuffer最终调用的是CrosGrallocMapperV5::importBuffer
    };
}

1.6 Allocator::allocate2
这里的文件路径在/external/minigbm/cros_gralloc/aidl/Allocator.cpp

ndk::ScopedAStatus Allocator::allocate2(const BufferDescriptorInfo& descriptor, int32_t count,
                        allocator::AllocationResult* outResult) {
    // 。。。初始化检测
    // client侧将buffer相关的参数都封装到BufferDescriptorInfo里
    // 这里将BufferDescriptorInfo转换成一个BufferDescriptorInfoV4对象,他们内部的字段都差不多
    BufferDescriptorInfoV4 descriptionV4 = convertAidlToIMapperV4Descriptor(descriptor);

    std::vector<native_handle_t*> handles;
    handles.resize(count, nullptr);
    // count是需要分配的buffer个数,就是GraphicBuffer的layerCount决定的。
    for (int32_t i = 0; i < count; i++) {

        ndk::ScopedAStatus status = allocate(descriptionV4, &outResult->stride, &handles[i]);
        if (!status.isOk()) {
            for (int32_t j = 0; j < i; j++) {
                // 如果分配失败了,就把之前分配的全部释放掉。
                releaseBufferAndHandle(handles[j]);
            }
            return status;
        }
    }

    outResult->buffers.resize(count);
    for (int32_t i = 0; i < count; i++) {
        auto handle = handles[i];
        outResult->buffers[i] = ::android::dupToAidl(handle);
        releaseBufferAndHandle(handle);
    }

    return ndk::ScopedAStatus::ok();
}

1.7 Allocator::allocate

ndk::ScopedAStatus Allocator::allocate(const BufferDescriptorInfoV4& descriptor, int32_t* outStride,
                                    native_handle_t** outHandle) {
    // 。。。初始化检测
    // 这里又把BufferDescriptorInfoV4转化成cros_gralloc_buffer_descriptor格式。
    struct cros_gralloc_buffer_descriptor crosDescriptor;
    if (convertToCrosDescriptor(descriptor, &crosDescriptor)) {
        return ToBinderStatus(AllocationError::UNSUPPORTED);
    }

    crosDescriptor.reserved_region_size += sizeof(CrosGralloc4Metadata);

    // 。。。格式检测

    native_handle_t* handle;
    // 调用mDriver->allocate分配Buffer,mDriver是cros_gralloc_driver的实例
    // 详见1.8
    int ret = mDriver->allocate(&crosDescriptor, &handle);
    // 。。。返回值处理

    cros_gralloc_handle_t crosHandle = cros_gralloc_convert_handle(handle);

    auto status = initializeMetadata(crosHandle, crosDescriptor);
    // 。。。返回值处理

    *outStride = static_cast<int32_t>(crosHandle->pixel_stride);
    *outHandle = handle;

    return ndk::ScopedAStatus::ok();
}

1.8 cros_gralloc_driver::allocate
这段代码可能比较难懂,不是代码逻辑很复杂,主要是因为我们之前说过Android现在的GUI架构最终是基于linux drm的渲染架构的,这里用到来drm的一些接口,需要有一些预备知识。
我们不会对这段介绍的太深入,主要就是看一下大体流程,让大家不会觉得GraphicBuffer那么抽象。
drm是会提供几个文件节点,然后通过iocal的方式来个drm进行交互,操作显卡硬件。
libdrm是对这些操作进行了封装,提供的api给上层使用。
我们接下来看到的大多数的drm开头的方法都是libdrm里面的方法。

int32_t cros_gralloc_driver::allocate(const struct cros_gralloc_buffer_descriptor *descriptor,
                    native_handle_t **out_handle)
{
    // 。。。
    // 这里会构建一个bo结构,bo结构里面有一个driver指针,记录这里传入的drv
    // 有一个bo_handle数组,这里记录的是drm构建的buffer的handle。
    // bo_metadata里面记录来一些基础信息,比如width,height,format等。
    // 这里的drv是一个driver结构体,里面有一个fd,这个fd就是drm提供的驱动节点的fd(/dev/dri/renderX或者/dev/dri/cardX)
    // driver结构体还有一个backend结构体指针,里面是一些函数指针,不同的硬件会有不同的实现。  
    // buffer的分配也在这里面实现的,详见1.9
    bo = drv_bo_create(drv_.get(), descriptor->width, descriptor->height, resolved_format,
            resolved_use_flags);
    // 。。。
    // cros_gralloc_handle是最后返回回去的对象,会将前面bo里面的信息都填充到hnd里面。
    hnd =
        reinterpret_cast<struct cros_gralloc_handle *>(native_handle_create(num_fds, num_ints));

    // 。。。
    hnd->num_planes = num_planes;
    for (size_t plane = 0; plane < num_planes; plane++) {
        // 调用drm相关接口将buffer handle转换成fd存储到hnd,返回给请求的应用需要跨进程传输,所以需要转换成fd。
        ret = drv_bo_get_plane_fd(bo, plane);
        if (ret < 0)
            goto destroy_hnd;

        hnd->fds[plane] = ret;
        hnd->strides[plane] = drv_bo_get_plane_stride(bo, plane);
        hnd->offsets[plane] = drv_bo_get_plane_offset(bo, plane);
        hnd->sizes[plane] = drv_bo_get_plane_size(bo, plane);
    }

    // 将基础信息都填充到hnd里
    // 将bo和hnd封装成cros_gralloc_buffer
    buffer = cros_gralloc_buffer::create(bo, hnd);

    // 。。。
    *out_handle = hnd;
    return 0;
    // 。。。
}

1.9 drv_bo_create

struct bo *drv_bo_create(struct driver *drv, uint32_t width, uint32_t height, uint32_t format,
            uint64_t use_flags)
{
    int ret;
    struct bo *bo;
    bool is_test_alloc;

    is_test_alloc = use_flags & BO_USE_TEST_ALLOC;
    use_flags &= ~BO_USE_TEST_ALLOC;
    // 这里会new一个bo结构体,然后用参数填充结构体。
    bo = drv_bo_new(drv, width, height, format, use_flags, is_test_alloc);

    if (!bo)
        return NULL;

    ret = -EINVAL;
    if (drv->backend->bo_compute_metadata) {
        ret = drv->backend->bo_compute_metadata(bo, width, height, format, use_flags, NULL,
                            0);
        if (!is_test_alloc && ret == 0)
            ret = drv->backend->bo_create_from_metadata(bo);
    } else if (!is_test_alloc) {
        // 前面说过backend是基于不同硬件不同的一组函数指针。
        // 我们随便找一个简单一些的实现来看一眼,详见1.10
        ret = drv->backend->bo_create(bo, width, height, format, use_flags);
    }

    if (ret) {
        errno = -ret;
        free(bo);
        return NULL;
    }

    drv_bo_acquire(bo);

    return bo;
}

1.10 bo_create
在这里我们可以看到这个buffer是由drm驱动提供的接口分配出来的,分配buffer后会返回一个handle存储在出参create_dumb,然后会把这个handle放到bo结构中
由drm 接口分配的buffer就是dma buffer,这里的fd返回给请求侧,后续就可以通过fd在不同驱动中使用这个buffer了。

#define INIT_DUMB_DRIVER_WITH_NAME(driver, _name)                                      \
const struct backend backend_##driver = {                                              \
	.name = _name,                                                                     \
	.init = dumb_driver_init,                                                          \
    // bo_create在这里
	.bo_create = drv_dumb_bo_create,                                                   \
	.bo_create_with_modifiers = dumb_bo_create_with_modifiers,                         \
	.bo_destroy = drv_dumb_bo_destroy,                                                 \
	.bo_import = drv_prime_bo_import,                                                  \
	.bo_map = drv_dumb_bo_map,                                                         \
	.bo_unmap = drv_bo_munmap,                                                         \
	.resolve_format_and_use_flags = drv_resolve_format_and_use_flags_helper,           \
};

int drv_dumb_bo_create(struct bo *bo, uint32_t width, uint32_t height, uint32_t format,
            uint64_t use_flags)
{
    return drv_dumb_bo_create_ex(bo, width, height, format, use_flags, BO_QUIRK_NONE);
}

int drv_dumb_bo_create_ex(struct bo *bo, uint32_t width, uint32_t height, uint32_t format,
            uint64_t use_flags, uint64_t quirks)
{
    // 。。。参数填充构造的细节我们就不看了
    // 可以看到最终调用的drm构建buffer,返回的create_dumb.handle就是buffer的句柄来,我们会存到bo结构里
    ret = drmIoctl(bo->drv->fd, DRM_IOCTL_MODE_CREATE_DUMB, &create_dumb);
    // 。。。
    for (plane = 0; plane < bo->meta.num_planes; plane++)
        //将buffer句柄存到bo结构
        bo->handles[plane].u32 = create_dumb.handle;

    bo->meta.total_size = create_dumb.size;
    return 0;
}

总结

我们介绍了GraphicBuffer构造流程,它是如何通过binder到分配服务,然后调用drm的接口进行buffern分配。
关于drm,如果后面有机会我们再去深入学习(估计是遥远的以后了,因为我们会先学习framework的一些核心服务,还是以android的内容为主)
dma-buffer我们目前也没去深入了解,目前就是大致知道他的作用是通过物理内存buffer关联fd,然后以fd为媒介在不同驱动中流转,使得不同驱动可以共享buffer。(同样是遥远的以后可能会深入的学习他)

  • 18
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
探索全栈前端技术的魅力:HTML+CSS+JS+JQ+Bootstrap网站源码深度解析 在这个数字化时代,构建一个既美观又功能强大的网站成为了许多开发者和企业追逐的目标。本份资源精心汇集了一套完整网站源码,融合了HTML的骨架搭建、CSS的视觉美化、JavaScript的交互逻辑、jQuery的高效操作以及Bootstrap的响应式设计,全方位揭秘了现代网页开发的精髓。 HTML,作为网页的基础,它构建了信息的框架;CSS则赋予网页生动的外观,让设计创意跃然屏上;JavaScript的加入,使网站拥有了灵动的交互体验;jQuery,作为JavaScript的强力辅助,简化了DOM操作与事件处理,让编码更为高效;而Bootstrap的融入,则确保了网站在不同设备上的完美呈现,响应式设计让访问无界限。 通过这份源码,你将: 学习如何高效组织HTML结构,提升页面加载速度与SEO友好度; 掌握CSS高级技巧,如Flexbox与Grid布局,打造适应各种屏幕的视觉盛宴; 理解JavaScript核心概念,动手实现动画、表单验证等动态效果; 利用jQuery插件快速增强用户体验,实现滑动效果、Ajax请求等; 深入Bootstrap框架,掌握移动优先的开发策略,响应式设计信手拈来。 无论是前端开发新手渴望系统学习,还是资深开发者寻求灵感与实用技巧,这份资源都是不可多得的宝藏。立即深入了解,开启你的全栈前端探索之旅,让每一个网页都成为技术与艺术的完美融合!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值