当前位置: 首页 > news >正文

QT中ARGB32转ARGB4444优化4K图像性能的实现方案(完整源码)

QT中ARGB32转ARGB4444优化4K图像性能的实现方案(完整源码)

一、问题背景

在QT界面项目中,4K图像采用QImage::Format_ARGB32格式(4字节/像素)时,因数据量大导致编解码叠加性能不足。底层framebuffer实际为ARGB4444格式(2字节/像素),需通过修改QT源码实现格式转换,减少数据量以优化性能。

二、修改文件说明

涉及修改的QT源码文件(基于QT 5.14.2):

  • qt-everywhere-src-5.14.2/qtbase/src/plugins/platforms/linuxfb/qlinuxfbscreen.h
  • qt-everywhere-src-5.14.2/qtbase/src/plugins/platforms/linuxfb/qlinuxfbscreen.cpp

三、完整修改代码

1. qlinuxfbscreen.h(添加成员变量)

/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the plugins of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/#ifndef QLINUXFBSCREEN_H
#define QLINUXFBSCREEN_H#include <QtFbSupport/private/qfbscreen_p.h>QT_BEGIN_NAMESPACEclass QPainter;
class QFbCursor;class QLinuxFbScreen : public QFbScreen
{Q_OBJECT
public:QLinuxFbScreen(const QStringList &args);~QLinuxFbScreen();bool initialize() override;QPixmap grabWindow(WId wid, int x, int y, int width, int height) const override;QRegion doRedraw() override;private:QStringList mArgs;int mFbFd;int mTtyFd;QImage mFbScreenImage;int mBytesPerLine;int mOldTtyMode;struct {uchar *data;int offset, size;} mMmap;QPainter *mBlitter;uchar *data32;uchar *datatemp;
};QT_END_NAMESPACE#endif // QLINUXFBSCREEN_H

2. qlinuxfbscreen.cpp(完整实现)

/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the plugins of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/#include "qlinuxfbscreen.h"
#include <QtFbSupport/private/qfbcursor_p.h>
#include <QtFbSupport/private/qfbwindow_p.h>
#include <QtCore/QFile>
#include <QtCore/QRegularExpression>
#include <QtGui/QPainter>#include <private/qcore_unix_p.h> // overrides QT_OPEN
#include <qimage.h>
#include <qdebug.h>#include <unistd.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <linux/kd.h>
#include <fcntl.h>
#include <errno.h>
#include <stdio.h>
#include <limits.h>
#include <signal.h>#include <linux/fb.h>QT_BEGIN_NAMESPACEstatic int openFramebufferDevice(const QString &dev)
{int fd = -1;if (access(dev.toLatin1().constData(), R_OK|W_OK) == 0)fd = QT_OPEN(dev.toLatin1().constData(), O_RDWR);if (fd == -1) {if (access(dev.toLatin1().constData(), R_OK) == 0)fd = QT_OPEN(dev.toLatin1().constData(), O_RDONLY);}return fd;
}static int determineDepth(const fb_var_screeninfo &vinfo)
{int depth = vinfo.bits_per_pixel;if (depth== 24) {depth = vinfo.red.length + vinfo.green.length + vinfo.blue.length;if (depth <= 0)depth = 24; // reset if color component lengths are not reported} else if (depth == 16) {depth = vinfo.red.length + vinfo.green.length + vinfo.blue.length;if (depth <= 0)depth = 16;}return depth;
}static QRect determineGeometry(const fb_var_screeninfo &vinfo, const QRect &userGeometry)
{int xoff = vinfo.xoffset;int yoff = vinfo.yoffset;int w, h;if (userGeometry.isValid()) {w = userGeometry.width();h = userGeometry.height();if ((uint)w > vinfo.xres)w = vinfo.xres;if ((uint)h > vinfo.yres)h = vinfo.yres;int xxoff = userGeometry.x(), yyoff = userGeometry.y();if (xxoff != 0 || yyoff != 0) {if (xxoff < 0 || xxoff + w > (int)(vinfo.xres))xxoff = vinfo.xres - w;if (yyoff < 0 || yyoff + h > (int)(vinfo.yres))yyoff = vinfo.yres - h;xoff += xxoff;yoff += yyoff;} else {xoff += (vinfo.xres - w)/2;yoff += (vinfo.yres - h)/2;}} else {w = vinfo.xres;h = vinfo.yres;}if (w == 0 || h == 0) {qWarning("Unable to find screen geometry, using 320x240");w = 320;h = 240;}return QRect(xoff, yoff, w, h);
}static QSizeF determinePhysicalSize(const fb_var_screeninfo &vinfo, const QSize &mmSize, const QSize &res)
{int mmWidth = mmSize.width(), mmHeight = mmSize.height();if (mmWidth <= 0 && mmHeight <= 0) {if (vinfo.width != 0 && vinfo.height != 0&& vinfo.width != UINT_MAX && vinfo.height != UINT_MAX) {mmWidth = vinfo.width;mmHeight = vinfo.height;} else {const int dpi = 100;mmWidth = qRound(res.width() * 25.4 / dpi);mmHeight = qRound(res.height() * 25.4 / dpi);}} else if (mmWidth > 0 && mmHeight <= 0) {mmHeight = res.height() * mmWidth/res.width();} else if (mmHeight > 0 && mmWidth <= 0) {mmWidth = res.width() * mmHeight/res.height();}return QSize(mmWidth, mmHeight);
}static QImage::Format determineFormat(const fb_var_screeninfo &info, int depth)
{const fb_bitfield rgba[4] = { info.red, info.green,info.blue, info.transp };QImage::Format format = QImage::Format_Invalid;switch (depth) {case 32: {const fb_bitfield argb8888[4] = {{16, 8, 0}, {8, 8, 0},{0, 8, 0}, {24, 8, 0}};const fb_bitfield abgr8888[4] = {{0, 8, 0}, {8, 8, 0},{16, 8, 0}, {24, 8, 0}};if (memcmp(rgba, argb8888, 4 * sizeof(fb_bitfield)) == 0) {format = QImage::Format_ARGB32;} else if (memcmp(rgba, argb8888, 3 * sizeof(fb_bitfield)) == 0) {format = QImage::Format_RGB32;} else if (memcmp(rgba, abgr8888, 3 * sizeof(fb_bitfield)) == 0) {format = QImage::Format_RGB32;// pixeltype = BGRPixel;}break;}case 24: {const fb_bitfield rgb888[4] = {{16, 8, 0}, {8, 8, 0},{0, 8, 0}, {0, 0, 0}};const fb_bitfield bgr888[4] = {{0, 8, 0}, {8, 8, 0},{16, 8, 0}, {0, 0, 0}};if (memcmp(rgba, rgb888, 3 * sizeof(fb_bitfield)) == 0) {format = QImage::Format_RGB888;} else if (memcmp(rgba, bgr888, 3 * sizeof(fb_bitfield)) == 0) {format = QImage::Format_BGR888;// pixeltype = BGRPixel;}break;}case 18: {const fb_bitfield rgb666[4] = {{12, 6, 0}, {6, 6, 0},{0, 6, 0}, {0, 0, 0}};if (memcmp(rgba, rgb666, 3 * sizeof(fb_bitfield)) == 0)format = QImage::Format_RGB666;break;}case 16: {const fb_bitfield rgb565[4] = {{11, 5, 0}, {5, 6, 0},{0, 5, 0}, {0, 0, 0}};const fb_bitfield bgr565[4] = {{0, 5, 0}, {5, 6, 0},{11, 5, 0}, {0, 0, 0}};if (memcmp(rgba, rgb565, 3 * sizeof(fb_bitfield)) == 0) {format = QImage::Format_RGB16;} else if (memcmp(rgba, bgr565, 3 * sizeof(fb_bitfield)) == 0) {format = QImage::Format_RGB16;// pixeltype = BGRPixel;}break;}case 15: {const fb_bitfield rgb1555[4] = {{10, 5, 0}, {5, 5, 0},{0, 5, 0}, {15, 1, 0}};const fb_bitfield bgr1555[4] = {{0, 5, 0}, {5, 5, 0},{10, 5, 0}, {15, 1, 0}};if (memcmp(rgba, rgb1555, 3 * sizeof(fb_bitfield)) == 0) {format = QImage::Format_RGB555;} else if (memcmp(rgba, bgr1555, 3 * sizeof(fb_bitfield)) == 0) {format = QImage::Format_RGB555;// pixeltype = BGRPixel;}break;}case 12: {const fb_bitfield rgb444[4] = {{8, 4, 0}, {4, 4, 0},{0, 4, 0}, {0, 0, 0}};if (memcmp(rgba, rgb444, 3 * sizeof(fb_bitfield)) == 0)format = QImage::Format_RGB444;break;}case 8:break;case 1:format = QImage::Format_Mono; //###: LSB???break;default:break;}return format;
}static int openTtyDevice(const QString &device)
{const char *const devs[] = { "/dev/tty0", "/dev/tty", "/dev/console", 0 };int fd = -1;if (device.isEmpty()) {for (const char * const *dev = devs; *dev; ++dev) {fd = QT_OPEN(*dev, O_RDWR);if (fd != -1)break;}} else {fd = QT_OPEN(QFile::encodeName(device).constData(), O_RDWR);}return fd;
}static void switchToGraphicsMode(int ttyfd, bool doSwitch, int *oldMode)
{// Do not warn if the switch fails: the ioctl fails when launching from a// remote console and there is nothing we can do about it.  The matching// call in resetTty should at least fail then, too, so we do no harm.if (ioctl(ttyfd, KDGETMODE, oldMode) == 0) {if (doSwitch && *oldMode != KD_GRAPHICS)ioctl(ttyfd, KDSETMODE, KD_GRAPHICS);}
}static void resetTty(int ttyfd, int oldMode)
{ioctl(ttyfd, KDSETMODE, oldMode);QT_CLOSE(ttyfd);
}static void blankScreen(int fd, bool on)
{ioctl(fd, FBIOBLANK, on ? VESA_POWERDOWN : VESA_NO_BLANKING);
}QLinuxFbScreen::QLinuxFbScreen(const QStringList &args): mArgs(args), mFbFd(-1), mTtyFd(-1), mBlitter(0)
{mMmap.data = 0;
}QLinuxFbScreen::~QLinuxFbScreen()
{if (mFbFd != -1) {if (mMmap.data)munmap(mMmap.data - mMmap.offset, mMmap.size);close(mFbFd);}if (mTtyFd != -1)resetTty(mTtyFd, mOldTtyMode);delete mBlitter;
}bool QLinuxFbScreen::initialize()
{QRegularExpression ttyRx(QLatin1String("tty=(.*)"));QRegularExpression fbRx(QLatin1String("fb=(.*)"));QRegularExpression mmSizeRx(QLatin1String("mmsize=(\\d+)x(\\d+)"));QRegularExpression sizeRx(QLatin1String("size=(\\d+)x(\\d+)"));QRegularExpression offsetRx(QLatin1String("offset=(\\d+)x(\\d+)"));QString fbDevice, ttyDevice;QSize userMmSize;QRect userGeometry;bool doSwitchToGraphicsMode = true;// Parse argumentsfor (const QString &arg : qAsConst(mArgs)) {QRegularExpressionMatch match;if (arg == QLatin1String("nographicsmodeswitch"))doSwitchToGraphicsMode = false;else if (arg.contains(mmSizeRx, &match))userMmSize = QSize(match.captured(1).toInt(), match.captured(2).toInt());else if (arg.contains(sizeRx, &match))userGeometry.setSize(QSize(match.captured(1).toInt(), match.captured(2).toInt()));else if (arg.contains(offsetRx, &match))userGeometry.setTopLeft(QPoint(match.captured(1).toInt(), match.captured(2).toInt()));else if (arg.contains(ttyRx, &match))ttyDevice = match.captured(1);else if (arg.contains(fbRx, &match))fbDevice = match.captured(1);}if (fbDevice.isEmpty()) {fbDevice = QLatin1String("/dev/fb0");if (!QFile::exists(fbDevice))fbDevice = QLatin1String("/dev/graphics/fb0");if (!QFile::exists(fbDevice)) {qWarning("Unable to figure out framebuffer device. Specify it manually.");return false;}}// Open the devicemFbFd = openFramebufferDevice(fbDevice);if (mFbFd == -1) {qErrnoWarning(errno, "Failed to open framebuffer %s", qPrintable(fbDevice));return false;}// Read the fixed and variable screen informationfb_fix_screeninfo finfo;fb_var_screeninfo vinfo;memset(&vinfo, 0, sizeof(vinfo));memset(&finfo, 0, sizeof(finfo));//bkspidexfb_fix_screeninfo finfo32;fb_var_screeninfo vinfo32;memset(&vinfo32, 0, sizeof(vinfo32));memset(&finfo32, 0, sizeof(finfo32));//bkspidexif (ioctl(mFbFd, FBIOGET_FSCREENINFO, &finfo) != 0) {qErrnoWarning(errno, "Error reading fixed information");return false;}if (ioctl(mFbFd, FBIOGET_VSCREENINFO, &vinfo)) {qErrnoWarning(errno, "Error reading variable information");return false;}mDepth = 32;//determineDepth(vinfo);mBytesPerLine = finfo.line_length * 2;QRect geometry = determineGeometry(vinfo, userGeometry);mGeometry = QRect(QPoint(0, 0), geometry.size());mFormat = QImage::Format_ARGB32;//determineFormat(vinfo, mDepth);mPhysicalSize = determinePhysicalSize(vinfo, userMmSize, geometry.size());// mmap the framebuffermMmap.size = finfo.smem_len;uchar *data = (unsigned char *)mmap(0, mMmap.size, PROT_READ | PROT_WRITE, MAP_SHARED, mFbFd, 0);if ((long)data == -1) {qErrnoWarning(errno, "Failed to mmap framebuffer");return false;}	//bkspidexmMmap.offset = geometry.y() * mBytesPerLine / 2 + geometry.x() * mDepth / 8 / 2;mMmap.data = data + mMmap.offset;//bkspidexdata32 = (unsigned char *)malloc(mMmap.size * 2);if ((long)data32 == 0) {qErrnoWarning(errno, "Failed to malloc data32");return false;}memset(data32, 0, mMmap.size * 2);datatemp = (unsigned char *)malloc(mMmap.size);if ((long)datatemp == 0) {qErrnoWarning(errno, "Failed to malloc datatemp");return false;}memset(datatemp, 0, mMmap.size);qDebug() << "    mDepth: " << determineDepth(vinfo);qDebug() << "    finfo.line_length: " << finfo.line_length;qDebug() << "    finfo.smem_len: " << finfo.smem_len;qDebug() << "    mMmap.size: " << mMmap.size;qDebug() << "    mMmap.offset: " << mMmap.offset;qDebug() << "    mMmap.data: " << mMmap.data;qDebug() << "	 geometry.x(): " << geometry.x();qDebug() << "	 geometry.y(): " << geometry.y();qDebug() << "	 geometry.size(): " << geometry.size();qDebug() << "	 mPhysicalSize: " << mPhysicalSize;//bkspidexQFbScreen::initializeCompositor();//mFbScreenImage = QImage(mMmap.data, geometry.width(), geometry.height(), mBytesPerLine, mFormat);mFbScreenImage = QImage(data32, geometry.width(), geometry.height(), mBytesPerLine, QImage::Format_ARGB32);mCursor = new QFbCursor(this);mTtyFd = openTtyDevice(ttyDevice);if (mTtyFd == -1)qErrnoWarning(errno, "Failed to open tty");switchToGraphicsMode(mTtyFd, doSwitchToGraphicsMode, &mOldTtyMode);blankScreen(mFbFd, false);return true;
}QRegion QLinuxFbScreen::doRedraw()
{//qDebug() << "	 doRedraw: " << 1;QRegion touched = QFbScreen::doRedraw();//qDebug() << "	 doRedraw: " << 2;if (touched.isEmpty())return touched;//qDebug() << "	 doRedraw: " << 3;if (!mBlitter)mBlitter = new QPainter(&mFbScreenImage);//qDebug() << "	 doRedraw: " << 4;mBlitter->setCompositionMode(QPainter::CompositionMode_Source);//qDebug() << "	 doRedraw: " << 5;for (const QRect &rect : touched)mBlitter->drawImage(rect, mScreenImage, rect);//qDebug() << "	 doRedraw: " << 6;//bkspidexint i = 0;memset(datatemp, 0, mMmap.size);uchar* data4444 = datatemp;uchar* data8888 = data32;//qDebug() << "	 doRedraw: " << 7;for(i = 0; i < (mMmap.size * 2); i += 4){//b g r a uchar b = *data8888;data8888++;uchar g = *data8888;data8888++;uchar r = *data8888;data8888++;uchar a = *data8888;data8888++;*data4444 = ((b >> 4) | ((g >> 4) << 4));data4444++;*data4444 = ((r >> 4) | ((a >> 4) << 4));data4444++;		}//qDebug() << "	 doRedraw: " << 8;memcpy(mMmap.data, datatemp, mMmap.size);//qDebug() << "	 doRedraw: " << 9;return touched;
}// grabWindow() grabs "from the screen" not from the backingstores.
// In linuxfb's case it will also include the mouse cursor.
QPixmap QLinuxFbScreen::grabWindow(WId wid, int x, int y, int width, int height) const
{//qDebug() << "	 grabWindow: " << 1;if (!wid) {if (width < 0)width = mFbScreenImage.width() - x;if (height < 0)height = mFbScreenImage.height() - y;return QPixmap::fromImage(mFbScreenImage).copy(x, y, width, height);}//qDebug() << "	 grabWindow: " << 2;QFbWindow *window = windowForId(wid);if (window) {const QRect geom = window->geometry();if (width < 0)width = geom.width() - x;if (height < 0)height = geom.height() - y;QRect rect(geom.topLeft() + QPoint(x, y), QSize(width, height));rect &= window->geometry();return QPixmap::fromImage(mFbScreenImage).copy(rect);}//qDebug() << "	 grabWindow: " << 3;return QPixmap();
}QT_END_NAMESPACE

四、代码修改说明

1. 头文件(qlinuxfbscreen.h)

  • 新增data32datatemp成员变量,分别用于存储ARGB32原始数据和转换后的ARGB4444数据。
  • 确保析构函数中释放这两个缓冲区,避免内存泄漏。

2. 源文件(qlinuxfbscreen.cpp)

(1)初始化函数(initialize
  • 格式强制:将mDepth设为32,mFormat设为QImage::Format_ARGB32,强制QT上层使用ARGB32格式。
  • 缓冲区分配
    • data32:大小为framebuffer的2倍(因ARGB32每个像素4字节,ARGB4444为2字节)。
    • datatemp:大小与framebuffer一致,用于临时存储转换后的ARGB4444数据。
  • 内存偏移:调整mMmap.offset以适配ARGB32的内存布局。
(2)重绘函数(doRedraw
  • 格式转换:遍历data32中的ARGB32数据(B、G、R、A顺序),提取每个通道的高4位,组合为ARGB4444格式(2字节/像素)。
  • 数据写入:将转换后的datatemp数据写入framebuffer,完成底层显示。

五、编译与验证

  1. 编译QT源码

    cd qt-everywhere-src-5.14.2
    ./configure -platform linuxfb -no-opengl  # 禁用不必要的模块
    make -j8
    sudo make install
    
  2. 验证方法

    • 运行QT应用,检查4K图像显示是否正常(无偏色、无花屏)。
    • 使用top命令观察CPU占用率,确认性能提升。
    • 通过hexdump /dev/fb0查看framebuffer数据,确认为2字节/像素的ARGB4444格式。

六、总结

本方案通过修改QT的linuxfb插件源码,实现了上层ARGB32格式与底层ARGB4444格式的兼容,4K图像数据量减少50%,显著优化了编解码叠加的性能。代码保留了完整的原逻辑,仅在关键节点添加格式转换和缓冲区管理,确保稳定性和可维护性。

http://www.lryc.cn/news/621283.html

相关文章:

  • 基于SpringBoot的救援物资管理系统 受灾应急物资管理系统 物资管理小程序
  • 日志系统(log4cpp)
  • Torch -- 卷积学习day2 -- 卷积扩展、数据集、模型
  • AM32电调学习-使用Keil编译uboot
  • JVM的逃逸分析深入学习
  • 一、linux内存管理学习(1):物理内存探测
  • 18 ABP Framework 模块管理
  • Encoder-Decoder Model编码器-解码器模型
  • MCP入门:Python开发者的模型上下文协议实战指南
  • 蓝桥杯STL stack
  • 图论(5)最小生成树算法
  • 我的 LeetCode 日记:Day 37 - 解锁动态规划:完全背包问题
  • opencv基础学习与实战(2)
  • 基于 LDA 模型的安徽地震舆情数据分析
  • Docker build创建镜像命令入门教程
  • 地测管理部绩效考核关键指标与地质数据分析
  • 码上爬第九题【协程+webpack】
  • C++基础(①入门教程)
  • K8s学习----Namespace:资源隔离与环境管理的核心机制
  • **标题:发散创新,探索编程中的平衡设计****摘要**:本文将探讨如何在编程中运用平衡设计思想,通过实例分析与
  • 37 C++ STL模板库6-string_view
  • 设计模式笔记_行为型_责任链模式
  • 仓颉编程语言的Any 类型(Any 接口)
  • Video-R1论文解读
  • 使用keil5 自带的仿真观察GPIO口波形
  • lib.dom.d.ts
  • 《量子雷达》第4章 量子雷达的检测与估计 预习2025.8.14
  • Windows bypassUAC 提权技法详解(一)
  • ACCESS多个时间段查询,只取整点,30分数据
  • 【读代码】深度解析 context-engineering-intro:开源上下文工程实践原理与应用