SurfaceFlinger合成引擎

​一、逻辑链路全景图​

下面,我们逐层分析相关问题点

**#### ​二、应用层(Java)

2.1 View绘制流程​
  • ​关键类​​:

    • ViewRootImpl:管理View树,触发VSync信号
    • SurfaceHolder:封装Surface的访问接口
    • Choreographer:协调VSync与绘制帧
  • ​流程​​:

    1. View.invalidate()触发重绘
    2. ViewRootImpl注册VSync回调
    3. Choreographer接收VSync信号
    4. 调用ViewRootImpl.doTraversal()执行测量/布局/绘制

frameworks/base/core/java/android/view/ViewRootImpl.java

scss 复制代码
` @UnsupportedAppUsage
void invalidate() {
    mDirty.set(0, 0, mWidth, mHeight);
    if (!mWillDrawSoon) {
        scheduleTraversals();
    }
}

void invalidateWorld(View view) {
    view.invalidate();
    if (view instanceof ViewGroup) {
        ViewGroup parent = (ViewGroup) view;
        for (int i = 0; i < parent.getChildCount(); i++) {
            invalidateWorld(parent.getChildAt(i));
        }
    }
}

@Override
public void invalidateChild(View child, Rect dirty) {
    invalidateChildInParent(null, dirty);
}

@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
    checkThread();
    if (DEBUG_DRAW) Log.v(mTag, "Invalidate child: " + dirty);

    if (dirty == null) {
        invalidate();
        return null;
    } else if (dirty.isEmpty() && !mIsAnimating) {
        return null;
    }

    if (mCurScrollY != 0 || mTranslator != null) {
        mTempRect.set(dirty);
        dirty = mTempRect;
        if (mCurScrollY != 0) {
            dirty.offset(0, -mCurScrollY);
        }
        if (mTranslator != null) {
            mTranslator.translateRectInAppWindowToScreen(dirty);
        }
        if (mAttachInfo.mScalingRequired) {
            dirty.inset(-1, -1);
        }
    }

    invalidateRectOnScreen(dirty);

    return null;
}

private void invalidateRectOnScreen(Rect dirty) {
    if (DEBUG_DRAW) Log.v(mTag, "invalidateRectOnScreen: " + dirty);
    final Rect localDirty = mDirty;

    // Add the new dirty rect to the current one
    localDirty.union(dirty.left, dirty.top, dirty.right, dirty.bottom);
    // Intersect with the bounds of the window to skip
    // updates that lie outside of the visible region
    final float appScale = mAttachInfo.mApplicationScale;
    final boolean intersected = localDirty.intersect(0, 0,
            (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
    if (!intersected) {
        localDirty.setEmpty();
    }
    if (!mWillDrawSoon && (intersected || mIsAnimating)) {
        scheduleTraversals();
    }
}`

注册回调

scss 复制代码
`
private void registerCallbacksForSync(boolean syncBuffer,
        final SurfaceSyncGroup surfaceSyncGroup) {
    if (!isHardwareEnabled()) {
        return;
    }

    if (DEBUG_BLAST) {
        Log.d(mTag, "registerCallbacksForSync syncBuffer=" + syncBuffer);
    }

    final Transaction t;
    if (mHasPendingTransactions) {
        t = new Transaction();
        t.merge(mPendingTransaction);
        mHasPendingTransactions = false;
    } else {
        t = null;
    }

    mAttachInfo.mThreadedRenderer.registerRtFrameCallback(new FrameDrawingCallback() {
        @Override
        public void onFrameDraw(long frame) {
        }

        @Override
        public HardwareRenderer.FrameCommitCallback onFrameDraw(int syncResult, long frame) {
            if (DEBUG_BLAST) {
                Log.d(mTag,
                        "Received frameDrawingCallback syncResult=" + syncResult + " frameNum="
                                + frame + ".");
            }
            if (t != null) {
                mergeWithNextTransaction(t, frame);
            }

            // If the syncResults are SYNC_LOST_SURFACE_REWARD_IF_FOUND or
            // SYNC_CONTEXT_IS_STOPPED it means nothing will draw. There's no need to set up
            // any blast sync or commit callback, and the code should directly call
            // pendingDrawFinished.
            if ((syncResult
                    & (SYNC_LOST_SURFACE_REWARD_IF_FOUND | SYNC_CONTEXT_IS_STOPPED)) != 0) {
                surfaceSyncGroup.addTransaction(
                        mBlastBufferQueue.gatherPendingTransactions(frame));
                surfaceSyncGroup.markSyncReady();
                return null;
            }

            if (DEBUG_BLAST) {
                Log.d(mTag, "Setting up sync and frameCommitCallback");
            }

            if (syncBuffer) {
                boolean result = mBlastBufferQueue.syncNextTransaction(transaction -> {
                    Runnable timeoutRunnable = () -> Log.e(mTag,
                            "Failed to submit the sync transaction after 4s. Likely to ANR "
                                    + "soon");
                    mHandler.postDelayed(timeoutRunnable, 4000L * Build.HW_TIMEOUT_MULTIPLIER);
                    transaction.addTransactionCommittedListener(mSimpleExecutor,
                            () -> mHandler.removeCallbacks(timeoutRunnable));
                    surfaceSyncGroup.addTransaction(transaction);
                    surfaceSyncGroup.markSyncReady();
                });
                if (!result) {
                    // syncNextTransaction can only return false if something is already trying
                    // to sync the same frame in the same BBQ. That shouldn't be possible, but
                    // if it did happen, invoke markSyncReady so the active SSG doesn't get
                    // stuck.
                    Log.w(mTag, "Unable to syncNextTransaction. Possibly something else is"
                            + " trying to sync?");
                    surfaceSyncGroup.markSyncReady();
                }
            }

            return didProduceBuffer -> {
                if (DEBUG_BLAST) {
                    Log.d(mTag, "Received frameCommittedCallback"
                            + " lastAttemptedDrawFrameNum=" + frame
                            + " didProduceBuffer=" + didProduceBuffer);
                }

                // If frame wasn't drawn, clear out the next transaction so it doesn't affect
                // the next draw attempt. The next transaction and transaction complete callback
                // were only set for the current draw attempt.
                if (!didProduceBuffer) {
                    mBlastBufferQueue.clearSyncTransaction();

                    // Gather the transactions that were sent to mergeWithNextTransaction
                    // since the frame didn't draw on this vsync. It's possible the frame will
                    // draw later, but it's better to not be sync than to block on a frame that
                    // may never come.
                    surfaceSyncGroup.addTransaction(
                            mBlastBufferQueue.gatherPendingTransactions(frame));
                    surfaceSyncGroup.markSyncReady();
                    return;
                }

                // If we didn't request to sync a buffer, then we won't get the
                // syncNextTransaction callback. Instead, just report back to the Syncer so it
                // knows that this sync request is complete.
                if (!syncBuffer) {
                    surfaceSyncGroup.markSyncReady();
                }
            };
        }
    });
}`

测量布局计算

ini 复制代码
`  void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

        if (mProfile) {
            Debug.startMethodTracing("ViewAncestor");
        }

        performTraversals();

        if (mProfile) {
            Debug.stopMethodTracing();
            mProfile = false;
        }
    }
}`

Choreographer 接收 VSync 信号

less 复制代码
`    /**
 * Posts a vsync callback to run on the next frame.
 * <p>
 * The callback runs once then is automatically removed.
 * </p>
 *
 * @param callback The vsync callback to run during the next frame.
 *
 * @see #removeVsyncCallback
 */
public void postVsyncCallback(@NonNull VsyncCallback callback) {
    if (callback == null) {
        throw new IllegalArgumentException("callback must not be null");
    }

    postCallbackDelayedInternal(CALLBACK_ANIMATION, callback, VSYNC_CALLBACK_TOKEN, 0);
}  `

测量结束之后,进入surface创建阶段

代码体现:

WindowManagerService

WIndowState

csharp 复制代码
`void attach() {
    if (DEBUG) Slog.v(TAG, "Attaching " + this + " token=" + mToken);
    mSession.windowAddedLocked();
}`

Session

ini 复制代码
`  void windowAddedLocked() {
    if (mPackageName == null) {
        mPackageName = mProcess.mInfo.packageName;
        mRelayoutTag = "relayoutWindow: " + mPackageName;
    }
    if (mSurfaceSession == null) {
        if (DEBUG) {
            Slog.v(TAG_WM, "First window added to " + this + ", creating SurfaceSession");
        }
        mSurfaceSession = new SurfaceSession();
        ProtoLog.i(WM_SHOW_TRANSACTIONS, "  NEW SURFACE SESSION %s", mSurfaceSession);
        mService.mSessions.add(this);
        if (mLastReportedAnimatorScale != mService.getCurrentAnimatorScale()) {
            mService.dispatchNewAnimatorScaleLocked(this);
        }
    }
    mNumWindow++;
}`

​三、JNI桥接层​

​3.1 Native方法绑定​
  • Session 创建

android_view_SurfaceSession.cpp

scss 复制代码
static jlong nativeCreate(JNIEnv* env, jclass clazz) {
    SurfaceComposerClient* client = new SurfaceComposerClient();
    client->incStrong((void*)nativeCreate);
    return reinterpret_cast<jlong>(client);
}

SurfaceComposerClient内做的事情是什么呢?

最终通过使用reinterpret_cast将指向刚刚创建的SurfaceComposerClient对象的指针转换为一个整型返回给上层的SurfaceSession。

同样的创建Surface的还有 android_view_Surface.cpp 和 android_view_SurfaceControl.cpp

关键方法和代码如下:

scss 复制代码
static jlong nativeLockCanvas(JNIEnv* env, jclass clazz,
        jlong nativeObject, jobject canvasObj, jobject dirtyRectObj) {
    sp<Surface> surface(reinterpret_cast<Surface *>(nativeObject));

    if (!isSurfaceValid(surface)) {
        jniThrowException(env, IllegalArgumentException, NULL);
        return 0;
    }

    if (!ACanvas_isSupportedPixelFormat(ANativeWindow_getFormat(surface.get()))) {
        native_window_set_buffers_format(surface.get(), PIXEL_FORMAT_RGBA_8888);
    }

    Rect dirtyRect(Rect::EMPTY_RECT);
    Rect* dirtyRectPtr = NULL;

    if (dirtyRectObj) {
        dirtyRect.left   = env->GetIntField(dirtyRectObj, gRectClassInfo.left);
        dirtyRect.top    = env->GetIntField(dirtyRectObj, gRectClassInfo.top);
        dirtyRect.right  = env->GetIntField(dirtyRectObj, gRectClassInfo.right);
        dirtyRect.bottom = env->GetIntField(dirtyRectObj, gRectClassInfo.bottom);
        dirtyRectPtr = &dirtyRect;
    }

    ANativeWindow_Buffer buffer;
    status_t err = surface->lock(&buffer, dirtyRectPtr);
    if (err < 0) {
        const char* const exception = (err == NO_MEMORY) ?
                OutOfResourcesException : IllegalArgumentException;
        jniThrowException(env, exception, NULL);
        return 0;
    }

    graphics::Canvas canvas(env, canvasObj);
    canvas.setBuffer(&buffer, static_cast<int32_t>(surface->getBuffersDataSpace()));

    if (dirtyRectPtr) {
        canvas.clipRect({dirtyRect.left, dirtyRect.top, dirtyRect.right, dirtyRect.bottom});
    }

    if (dirtyRectObj) {
        env->SetIntField(dirtyRectObj, gRectClassInfo.left,   dirtyRect.left);
        env->SetIntField(dirtyRectObj, gRectClassInfo.top,    dirtyRect.top);
        env->SetIntField(dirtyRectObj, gRectClassInfo.right,  dirtyRect.right);
        env->SetIntField(dirtyRectObj, gRectClassInfo.bottom, dirtyRect.bottom);
    }

    // Create another reference to the surface and return it.  This reference
    // should be passed to nativeUnlockCanvasAndPost in place of mNativeObject,
    // because the latter could be replaced while the surface is locked.
    sp<Surface> lockedSurface(surface);
    lockedSurface->incStrong(&sRefBaseOwner);
    return (jlong) lockedSurface.get();
}
ini 复制代码
static jlong nativeCreate(JNIEnv* env, jclass clazz, jobject sessionObj,
        jstring nameStr, jint w, jint h, jint format, jint flags, jlong parentObject,
        jobject metadataParcel) {
    ScopedUtfChars name(env, nameStr);
    sp<SurfaceComposerClient> client;
    if (sessionObj != NULL) {
        client = android_view_SurfaceSession_getClient(env, sessionObj);
    } else {
        client = SurfaceComposerClient::getDefault();
    }
    SurfaceControl *parent = reinterpret_cast<SurfaceControl*>(parentObject);
    sp<SurfaceControl> surface;
    LayerMetadata metadata;
    Parcel* parcel = parcelForJavaObject(env, metadataParcel);
    if (parcel && !parcel->objectsCount()) {
        status_t err = metadata.readFromParcel(parcel);
        if (err != NO_ERROR) {
          jniThrowException(env, "java/lang/IllegalArgumentException",
                            "Metadata parcel has wrong format");
        }
    }

    sp<IBinder> parentHandle;
    if (parent != nullptr) {
        parentHandle = parent->getHandle();
    }

    status_t err = client->createSurfaceChecked(String8(name.c_str()), w, h, format, &surface,
                                                flags, parentHandle, std::move(metadata));
    if (err == NAME_NOT_FOUND) {
        jniThrowException(env, "java/lang/IllegalArgumentException", NULL);
        return 0;
    } else if (err != NO_ERROR) {
        jniThrowException(env, OutOfResourcesException, statusToString(err).c_str());
        return 0;
    }

    surface->incStrong((void *)nativeCreate);
    return reinterpret_cast<jlong>(surface.get());
}

衔接层,就不详细解释了,其目的是将c++层的逻暴露给java层。那接下来,我们看一下c++层,surfaceFlinger是如何操作的

​四、SurfaceFlinger服务端(C++)​

1.1入口代码:​

frameworks/native/services/surfaceflinger/main_surfaceflinger.cpp

关键代码如下:

scss 复制代码
`int main(int argc, char** argv) {
// 初始化Binder线程池(最大4线程)
hardware::configureRpcThreadpool(1, false);
ProcessState::self()->setThreadPoolMaxThreadCount(4);

// 创建SurfaceFlinger实例
sp<SurfaceFlinger> flinger = surfaceflinger::createSurfaceFlinger();

// 设置进程优先级(URGENT_DISPLAY)
setpriority(PRIO_PROCESS, 0, PRIORITY_URGENT_DISPLAY);
set_sched_policy(0, SP_FOREGROUND);

// 注册到ServiceManager
sp<IServiceManager> sm = defaultServiceManager();
sm->addService(String16("SurfaceFlinger"), flinger, false, ...);

// 启动主循环
flinger->run();
return 0;
}`

核心作用​​:初始化Binder通信、创建SF实例、设置优先级、注册系统服务

1.2 SurfaceFlinger构造函数​

frameworks/native/services/surfaceflinger/SurfaceFlinger.cpp

​关键操作​​:

  • 初始化成员变量(如mPrimaryDisplaymHwcmEventQueue)。
  • 注册VSync事件监听器。
  • 初始化硬件合成器(HWC)接口。

最关键的一步,在这里创建图层,即Layer的创建。

​1.3 图层管理​

​Layer创建​​:

scss 复制代码
`// frameworks/native/services/surfaceflinger/Layer.cpp
//简化版
Layer::Layer(const sp<SurfaceFlinger>& flinger)
    : mFlinger(flinger), mBufferQueue(new BufferQueue()) {
    mId = generateLayerId();  // 生成唯一ID
    mFlinger->registerLayer(this);  // 注册到LayerStack
    }`
    

在Laye做图层管理的时候,最重要的是z-order算法管理:

scss 复制代码
`// LayerStack.cpp
void LayerStack::sortLayers() {
    std::sort(mLayers.begin(), mLayers.end(),
              [](Layer* a, Layer* b) { return a->getZ() < b->getZ(); });
}`

当如上所有的全部做完之后,再次回到surfaceFlinger中进行合成,完成引擎合成

​合成流程​​:

  1. 处理事务(Transaction)
  2. 计算可见区域(computeVisibleRegions)
  3. 执行合成(handlePageFlip)
  4. 提交到DisplayDevice
VSync处理​
  • ​VSync信号接收​​:

    arduino 复制代码
    `// EventThread.cpp
    void EventThread::onVSyncReceived(int32_t sequenceId, nsecs_t timestamp) {
        mFlinger->onVSyncReceived(sequenceId, timestamp);
    }`

帧提交​​:

arduino 复制代码
    `// Choreographer.cpp
    void Choreographer::postFrameCallback(const FrameCallback& callback) {
        mFrameCallbacks.add(callback);
    }`
    
    

如此完成服务层的逻辑,进入合成帧数处理和Vsync的信号处理之后,进入硬件抽象层


​五、硬件抽象层(HAL)​

1.HAL在SurfaceFlinger中的核心作用​

硬件抽象层(HAL)是Android图形系统的关键组件,SurfaceFlinger通过HAL与底层显示硬件(如GPU、Display Controller)交互。其核心功能包括:

  1. ​硬件加速合成​:通过HWC(Hardware Composer)直接提交图层到硬件。
  2. ​显示参数配置​:设置分辨率、刷新率、色彩空间等。
  3. ​事件通知​:接收VSync、热插拔等硬件事件。

​2、HWC交互流程详解​

​2.1 HWC初始化流程​
  1. ​服务发现​

    • SurfaceFlinger启动时,通过hw_get_module(HWC_HARDWARE_MODULE_ID, ...)加载HWC HAL模块。
    • 代码路径:services/gui/SurfaceFlinger.cppinit()函数。
  2. ​创建HWC实例​

    scss 复制代码
    `// frameworks/native/services/surfaceflinger/SurfaceFlinger.cpp
    status_t SurfaceFlinger::init() {
        mHwc = getHwComposer();
        if (mHwc == nullptr) {
            ALOGE("Failed to get HWC instance");
            return NO_MEMORY;
        }
        mHwc->registerCallback(this);  // 注册回调
    }`
    
    ​**​显示设备初始化​**​
  3. ​通过HWC获取显示参数(如EDID信息):​

    scss 复制代码
    `mHwc->getDisplayCapabilities(displayId, &capabilities);
    mHwc->getDisplayIdentificationData(displayId, &type, &data);`
    
    ##### **​2.2 图层合成流程​**​
​2.2.图层提交​
  • 1.SurfaceFlinger将图层信息(位置、透明度、缓冲区)封装为hwc2_layer_t,调用mHwc->setLayerBuffer()提交。
arduino 复制代码
`// frameworks/native/services/surfaceflinger/HWComposer.cpp
arduino 复制代码
status_t HWComposer::setLayerBuffer(hwc2_layer_t layer, 
                                  const sp<GraphicBuffer>& buffer) {
    // 调用HWC HAL的setLayerBuffer接口
    return mHwc->setLayerBuffer(displayId, layer, buffer);
}`
  • ​2.合成策略决策​

  • HWC根据图层属性(如是否支持硬件合成)返回合成类型:

    arduino 复制代码
    `// HWC回调函数
    void HWComposer::onLayerSubmitted(hwc2_layer_t layer, 
                                   const sp<GraphicBuffer>& buffer) {
        if (buffer->hasProtectedContent()) {
            mHwc->setLayerSurfaceDamage(layer, damageRegion);
        }
    }`

硬件合成执行​

  • HWC调用底层驱动(如DRM/KMS)提交合成结果:

    arduino 复制代码
    `// vendor/qcom/opensource/display-hal/hwc2/ComposerEngine.cpp
    bool ComposerEngine::presentDisplay(hwc2_display_t display) {
        mDrmManager.commitFrame(display);  // DRM提交帧缓冲区
        return true;
    }`
​2.3 回退机制(Fallback)​

当HWC无法处理时(如复杂混合模式),SurfaceFlinger切换至软件合成:

  1. ​标记回退图层​

    arduino 复制代码
    `// frameworks/native/services/surfaceflinger/HWComposer.cpp
    void HWComposer::determineCompositionType() {
        if (mHwc->needsClientComposition(displayId)) {
            mCompositionType = CompositionType::Client;  // 回退到SF合成
        }
    }`

​OpenGL合成​

  • 通过RenderEngine执行GPU渲染:

    arduino 复制代码
    `// frameworks/native/services/surfaceflinger/RenderEngine.cpp
    void RenderEngine::drawLayers(const std::vector<Layer>& layers) {
        glUseProgram(mProgram);
        for (auto& layer : layers) {
            glBindTexture(GL_TEXTURE_2D, layer.buffer->getNativeBuffer()->handle);
            // 执行几何变换与混合
        }
    }`

​3、HWC与SurfaceFlinger的深度协作​

​3.1 事件通知机制​
  1. ​VSync信号传递​

    • HWC通过回调通知SurfaceFlinger垂直同步事件:

      arduino 复制代码
      `// frameworks/native/services/surfaceflinger/SurfaceFlinger.cpp
      void SurfaceFlinger::onVSyncReceived(hwc2_display_t display, nsecs_t timestamp) {
          mEventQueue->invalidate();  // 触发合成
      }`

​2.热插拔处理​

  • 当显示器连接/断开时,HWC触发回调:

    php 复制代码
    `// frameworks/native/services/surfaceflinger/SurfaceFlinger.cpp
    void HWComposer::registerCallback(ISurfaceComposer::VsyncSource vsyncSource,
                                    ISurfaceComposerCallback* callbacks) {
        mHwc->registerCallback(vsyncSource, callbacks);
    }`
3.2 内存管理​
  1. ​Gralloc缓冲区分配​

    • HWC通过gralloc模块分配图形内存:

      arduino 复制代码
      `// frameworks/native/services/surfaceflinger/GraphicBuffer.cpp
      sp<GraphicBuffer> GraphicBuffer::alloc(size_t w, size_t h, PixelFormat format) {
          hw_module_t* module;
          hw_get_module(GRALLOC_HARDWARE_MODULE_ID, (hw_module_t**)&module);
          return static_cast<alloc_device_t*>(module->methods->open(module, GRALLOC_HARDWARE_MODULE_ID))->alloc(w, h, format);
      }`
​3.3 多显示器支持​
  1. ​虚拟显示创建​

    arduino 复制代码
    `// frameworks/native/services/surfaceflinger/SurfaceFlinger.cpp
    sp<IBinder> SurfaceFlinger::createDisplay(const String8& displayName, bool secure) {
        sp<DisplayDevice> display = new DisplayDevice(this, displayName, secure);
        mHwc->createVirtualDisplay(displayId, width, height, &display->getHandle());
        return display;
    }`

如上流程,大概是surfaceFlinger的合成全流程,接下来就是验证测试的时刻。

​4、调试与验证方法​

​4.1 日志分析​
复制代码
csharp 复制代码
# 查看HWC合成日志
adb logcat | grep -E "HWC|SurfaceFlinger"

# 示例输出:
I/SurfaceFlinger( 1234): Layer 0x7f8e5a0000 (com.android.systemui) is hardware composed
​4.2 Perfetto跟踪​
复制代码
ini 复制代码
# 启动Perfetto捕获HWC事件
adb shell perfetto --config=android.surfaceflinger --out=surfacetrace.perfetto
​4.3 代码验证点​
关键流程 验证文件/函数 作用
HWC初始化 SurfaceFlinger::init() 加载HWC模块并注册回调
图层提交 HWComposer::setLayerBuffer() 传递图层缓冲区到硬件
合成策略决策 HWComposer::determineCompositionType() 判断是否启用硬件合成
DRM提交帧 ComposerEngine::presentDisplay() 通过DRM/KMS提交最终帧

​5、典型问题排查流程​

​问题:某应用界面显示异常(黑屏/撕裂)​

  1. ​检查HWC日志​

    复制代码
perl 复制代码
  adb logcat | grep "HWC error"
  • 若出现HWC: Failed to present display,可能为硬件驱动问题。

  • ​验证图层合成类型​

    复制代码
less 复制代码
  // 在HWComposer.cpp中添加调试输出
  ALOGD("Layer %p: Composition type %d", layer, mHwc->getCompositionType(displayId));
  • 若返回CLIENT,说明回退到软件合成。

  • ​检查BufferQueue状态​

    复制代码
css 复制代码
 adb shell dumpsys SurfaceFlinger --latency
  • 查看BufferQueue: mActiveBufferCount是否正常。

相关推荐
该换个名儿了2 分钟前
git多个commit合并成一个
前端·git
LaoZhangAI5 分钟前
2025最新OpenAI组织验证失败完全解决方案:8种有效方法彻底修复【实战指南】
前端·后端
siwangqishiq217 分钟前
Vulkan Tutorial 教程翻译(三) 绘制三角形 2.1 安装
前端
LaughingZhu17 分钟前
PH热榜 | 2025-06-05
前端·人工智能·经验分享·搜索引擎·产品运营
大模型真好玩18 分钟前
最强大模型评测工具EvalScope——模型好不好我自己说了算!
前端·人工智能·python
Dream耀34 分钟前
CSS选择器完全手册:精准控制网页样式的艺术
前端·css·html
wordbaby34 分钟前
React 19 亮点:让异步请求和数据变更也能用 Transition 管理!
前端·react.js
月亮慢慢圆35 分钟前
VUE3基础之Hooks
前端
我想说一句36 分钟前
CSS 基础知识小课堂:从“选择器”到“声明块”,带你玩转网页的时尚穿搭!
前端·javascript·面试