Qt 插件機制使用及原理

目錄

1.引言

2.插件原理

3.插件實現

3.1.定義一個接口集(只有純虛函數的類)

3.2.實現接口

4.插件的加載

4.1.靜態插件

4.1.1.靜態插件實現方式

4.1.2.靜態插件加載的過程

4.1.3.示例

4.2.動態插件

4.2.1.動態插件的加載過程

5.定位插件

6.插件開發的優勢

7.總結


1.引言

? ? ? ? 在設計大型軟件時,插件式開發都會被考慮到。無論在普通的桌面軟件還是大型的游戲軟件,都可以看到插件的身影。例如著名的Qt Creator 系統開發軟件都用插件架構。插件系統最大的功能是在一定程度內提高了軟件的靈活度和可擴展性。一個設計精良插件系統甚至可以在宿主軟件不退出的情況下加入新的插件,實現熱插拔的功能。這一點在我的博客C++架構設計中也有涉及:

C++架構設計-CSDN博客

那么,Qt的插件系統是怎么實現的呢?Qt 提供了兩種API用于創建插件:一種是高階 API,用于擴展 Qt 本身的功能,如自定義數據庫驅動,圖像格式,文本編碼,自定義樣式等;一種是低階 API,用于擴展 Qt 應用程序。本文主要是通過低階 API 來創建 Qt 插件,并通過靜態、動態兩種方式來調用插件。

2.插件原理

????????在C++ 中,插件一般以動態庫的顯示加載方式提供。利用C++多態的原理,在程序中首先聲明一個插件的interface。該interface 只需要實現構造和析構函數,所有用到的功能函數都先定義為虛函數,然后在插件中實現該interface 的具體接口。那么當插件創建的時候,把插件中的子類對象賦值到宿主程序中的基類對象interface。即可實現不同的插件實現不同的功能。

3.插件實現

3.1.定義一個接口集(只有純虛函數的類)

interfaces.h

#ifndef INTERFACES_H
#define INTERFACES_H#include <QtPlugin>QT_BEGIN_NAMESPACE
class QImage;
class QPainter;
class QWidget;
class QPainterPath;
class QPoint;
class QRect;
class QString;
class QStringList;
QT_END_NAMESPACE//! [0]
class BrushInterface
{
public:virtual ~BrushInterface() {}virtual QStringList brushes() const = 0;virtual QRect mousePress(const QString &brush, QPainter &painter,const QPoint &pos) = 0;virtual QRect mouseMove(const QString &brush, QPainter &painter,const QPoint &oldPos, const QPoint &newPos) = 0;virtual QRect mouseRelease(const QString &brush, QPainter &painter,const QPoint &pos) = 0;
};
//! [0]//! [1]
class ShapeInterface
{
public:virtual ~ShapeInterface() {}virtual QStringList shapes() const = 0;virtual QPainterPath generateShape(const QString &shape,QWidget *parent) = 0;
};
//! [1]//! [2]
class FilterInterface
{
public:virtual ~FilterInterface() {}virtual QStringList filters() const = 0;virtual QImage filterImage(const QString &filter, const QImage &image,QWidget *parent) = 0;
};
//! [2]QT_BEGIN_NAMESPACE
//! [3] //! [4]
#define BrushInterface_iid "org.qt-project.Qt.Examples.PlugAndPaint.BrushInterface/1.0"Q_DECLARE_INTERFACE(BrushInterface, BrushInterface_iid)
//! [3]#define ShapeInterface_iid  "org.qt-project.Qt.Examples.PlugAndPaint.ShapeInterface/1.0"Q_DECLARE_INTERFACE(ShapeInterface, ShapeInterface_iid)
//! [5]
#define FilterInterface_iid "org.qt-project.Qt.Examples.PlugAndPaint.FilterInterface/1.0"Q_DECLARE_INTERFACE(FilterInterface, FilterInterface_iid)
//! [4] //! [5]
QT_END_NAMESPACE#endif

????????創建一個BrushInterface,ShapeInterface,FilterInterface基類,直接可以把它定義為純虛接口。和普通的多態不同的是,在interfece 中我們需要定義 iid。iid 可以理解為插件的一個標識或者ID。在加載插件的過程中會對IID 進行判斷,如果插件中的IID 和 interface 中的IID 不匹配,那么該插件就不會被加載。Q_DECLARE_INTERFACE把IID 和類名進行綁定,這也是必須的。作用是導出一些可以通過 定義了接口ID查找函數和幾個QObject到接口的轉換函數。

????????宏Q_DECLARE_INTERFACE導入名為PluginName的插件,它與Q_PLUGIN_METADATA()為插件聲明元數據的類的名稱相對應。

3.2.實現接口

basictoolsplugin.h

#ifndef BASICTOOLSPLUGIN_H
#define BASICTOOLSPLUGIN_H//! [0]
#include <interfaces.h>#include <QRect>
#include <QObject>
#include <QtPlugin>
#include <QStringList>
#include <QPainterPath>
#include <QImage>//! [1]
class BasicToolsPlugin : public QObject,public BrushInterface,public ShapeInterface,public FilterInterface
{Q_OBJECT
//! [4]Q_PLUGIN_METADATA(IID "org.qt-project.Qt.Examples.PlugAndPaint.BrushInterface" FILE "basictools.json")
//! [4]Q_INTERFACES(BrushInterface ShapeInterface FilterInterface)
//! [0]//! [2]
public:
//! [1]// BrushInterfaceQStringList brushes() const override;QRect mousePress(const QString &brush, QPainter &painter,const QPoint &pos) override;QRect mouseMove(const QString &brush, QPainter &painter,const QPoint &oldPos, const QPoint &newPos) override;QRect mouseRelease(const QString &brush, QPainter &painter,const QPoint &pos) override;// ShapeInterfaceQStringList shapes() const override;QPainterPath generateShape(const QString &shape, QWidget *parent) override;// FilterInterfaceQStringList filters() const override;QImage filterImage(const QString &filter, const QImage &image,QWidget *parent) override;
//! [3]
};
//! [2] //! [3]#endif

????????實現interface 也是和多態一樣,只是多了兩個宏 Q_PLUGIN_METADATAQ_INTERFACES。前面講到了,因為宿主程序是不知道插件的名字的,所以如何讓插件的子類在宿主程序中實例化是插件系統的關鍵,在Qt 中 該功能由Q_PLUGIN_METADATA 完成。

? ? ? ? 跟蹤源碼發現Q_PLUGIN_METADATA的定義如下:

#define Q_PLUGIN_METADATA(x) QT_ANNOTATE_CLASS(qt_plugin_metadata, x)# ifndef Q_COMPILER_VARIADIC_MACROS
#  define QT_ANNOTATE_CLASS(type, x)
# else
#  define QT_ANNOTATE_CLASS(type, ...)
# endif
#endif

? ? ? ? 發現根本沒干什么,那么該宏應該在編譯的時候由元對象系統Moc解析。在moc 解析的時候將 Q_PLUGIN_METADATA 轉化為QT_MOC_EXPORT_PLUGIN 宏并插入到代碼中。我們在編譯后生成的" moc_basictoolsplugin.cpp" 文件中發現了此宏:

????????翻看Qt的源碼(5.12.12版本)找到了該宏返回了插件的元對象數據以及 qt_plugin_instance 函數的實現,該函數的函數體Q_PLUGIN_INSTANCE 定義,函數中的 _instance = new IMPLEMENTATION; 也就是插件的對象,然后通過QObject 指針 _instance 的形式返回(代碼如下)。上層通過該方式(見插件的加載過程),獲取插件的子類對象賦值給基類,實現多態的調用。

????????下面是插件的具體實現代碼:basictoolsplugin.cpp

#include "basictoolsplugin.h"#include <QtMath>
#include <QtWidgets>#include <stdlib.h>//! [0]
QStringList BasicToolsPlugin::brushes() const
{return {tr("Pencil"), tr("Air Brush"), tr("Random Letters")};
}
//! [0]//! [1]
QRect BasicToolsPlugin::mousePress(const QString &brush, QPainter &painter,const QPoint &pos)
{return mouseMove(brush, painter, pos, pos);
}
//! [1]//! [2]
QRect BasicToolsPlugin::mouseMove(const QString &brush, QPainter &painter,const QPoint &oldPos, const QPoint &newPos)
{painter.save();int rad = painter.pen().width() / 2;QRect boundingRect = QRect(oldPos, newPos).normalized().adjusted(-rad, -rad, +rad, +rad);QColor color = painter.pen().color();int thickness = painter.pen().width();QColor transparentColor(color.red(), color.green(), color.blue(), 0);
//! [2] //! [3]if (brush == tr("Pencil")) {painter.drawLine(oldPos, newPos);} else if (brush == tr("Air Brush")) {int numSteps = 2 + (newPos - oldPos).manhattanLength() / 2;painter.setBrush(QBrush(color, Qt::Dense6Pattern));painter.setPen(Qt::NoPen);for (int i = 0; i < numSteps; ++i) {int x = oldPos.x() + i * (newPos.x() - oldPos.x()) / (numSteps - 1);int y = oldPos.y() + i * (newPos.y() - oldPos.y()) / (numSteps - 1);painter.drawEllipse(x - (thickness / 2), y - (thickness / 2),thickness, thickness);}} else if (brush == tr("Random Letters")) {QChar ch(QRandomGenerator::global()->bounded('A', 'Z' + 1));QFont biggerFont = painter.font();biggerFont.setBold(true);biggerFont.setPointSize(biggerFont.pointSize() + thickness);painter.setFont(biggerFont);painter.drawText(newPos, QString(ch));QFontMetrics metrics(painter.font());boundingRect = metrics.boundingRect(ch);boundingRect.translate(newPos);boundingRect.adjust(-10, -10, +10, +10);}painter.restore();return boundingRect;
}
//! [3]//! [4]
QRect BasicToolsPlugin::mouseRelease(const QString & /* brush */,QPainter & /* painter */,const QPoint & /* pos */)
{return QRect(0, 0, 0, 0);
}
//! [4]//! [5]
QStringList BasicToolsPlugin::shapes() const
{return {tr("Circle"), tr("Star"), tr("Text...")};
}
//! [5]//! [6]
QPainterPath BasicToolsPlugin::generateShape(const QString &shape,QWidget *parent)
{QPainterPath path;if (shape == tr("Circle")) {path.addEllipse(0, 0, 50, 50);} else if (shape == tr("Star")) {path.moveTo(90, 50);for (int i = 1; i < 5; ++i) {path.lineTo(50 + 40 * std::cos(0.8 * i * M_PI),50 + 40 * std::sin(0.8 * i * M_PI));}path.closeSubpath();} else if (shape == tr("Text...")) {QString text = QInputDialog::getText(parent, tr("Text Shape"),tr("Enter text:"),QLineEdit::Normal, tr("Qt"));if (!text.isEmpty()) {QFont timesFont("Times", 50);timesFont.setStyleStrategy(QFont::ForceOutline);path.addText(0, 0, timesFont, text);}}return path;
}
//! [6]//! [7]
QStringList BasicToolsPlugin::filters() const
{return {tr("Invert Pixels"), tr("Swap RGB"), tr("Grayscale")};
}
//! [7]//! [8]
QImage BasicToolsPlugin::filterImage(const QString &filter, const QImage &image,QWidget * /* parent */)
{QImage result = image.convertToFormat(QImage::Format_RGB32);if (filter == tr("Invert Pixels")) {result.invertPixels();} else if (filter == tr("Swap RGB")) {result = result.rgbSwapped();} else if (filter == tr("Grayscale")) {for (int y = 0; y < result.height(); ++y) {for (int x = 0; x < result.width(); ++x) {QRgb pixel = result.pixel(x, y);int gray = qGray(pixel);int alpha = qAlpha(pixel);result.setPixel(x, y, qRgba(gray, gray, gray, alpha));}}}return result;
}
//! [8]

到此為止,一個插件的實現就已經完成了。下面來看一看插件加載的過程。

4.插件的加載

4.1.靜態插件

4.1.1.靜態插件實現方式

? ? ? ??靜態插件可以把下面這個宏插入到插件應用程序的源代碼中:

  Q_IMPORT_PLUGIN(qjpeg)

? ? ? ? 或在pro文件中配置項目為靜態插件:

  TEMPLATE      = libCONFIG       += plugin static

????????在構建應用程序時,靜態插件也必須包含在鏈接器中。對于Qt預定義的插件,您可以使用QTPLUGIN來添加所需的插件到您的構建中。例如:

  TEMPLATE      = appQTPLUGIN     += qjpeg qgif    # image formats

? ? ? ? 靜態插件的加載比較簡單,直接使用 QPluginLoader::staticInstances() 加載,代碼如下:

void PluginDialog::findPlugins(const QString &path,const QStringList &fileNames)
{label->setText(tr("Plug & Paint found the following plugins\n""(looked in %1):").arg(QDir::toNativeSeparators(path)));const QDir dir(path);const auto staticInstances = QPluginLoader::staticInstances();for (QObject *plugin : staticInstances)populateTreeWidget(plugin, tr("%1 (Static Plugin)").arg(plugin->metaObject()->className()));for (const QString &fileName : fileNames) {QPluginLoader loader(dir.absoluteFilePath(fileName));QObject *plugin = loader.instance();if (plugin)populateTreeWidget(plugin, fileName);}
}

4.1.2.靜態插件加載的過程

QPluginLoader::staticInstances()為什么就可以加載所有插件呢?翻看QPluginLoader的源碼:

/*!Returns a list of static plugin instances (root components) heldby the plugin loader.\sa staticPlugins()
*/
QObjectList QPluginLoader::staticInstances()
{QObjectList instances;const StaticPluginList *plugins = staticPluginList();if (plugins) {const int numPlugins = plugins->size();instances.reserve(numPlugins);for (int i = 0; i < numPlugins; ++i)instances += plugins->at(i).instance();}return instances;
}/*!Returns a list of QStaticPlugins held by the pluginloader. The function is similar to \l staticInstances()with the addition that a QStaticPlugin also containsmeta data information.\sa staticInstances()
*/
QVector<QStaticPlugin> QPluginLoader::staticPlugins()
{StaticPluginList *plugins = staticPluginList();if (plugins)return *plugins;return QVector<QStaticPlugin>();
}typedef QVector<QStaticPlugin> StaticPluginList;
Q_GLOBAL_STATIC(StaticPluginList, staticPluginList)

從以上代碼可以看出:

1)首先定義了全局的向量集合StaticPluginList來存儲所有的插件信息

2)然后,插件靜態加載的過程中,需要要把自己的插件信息注冊上來,這個也是最常見的設計手法,于是Qt庫提供了注冊的函數:

/*!\relates QPluginLoader\since 5.0Registers the \a plugin specified with the plugin loader, and is usedby Q_IMPORT_PLUGIN().
*/
void Q_CORE_EXPORT qRegisterStaticPluginFunction(QStaticPlugin plugin)
{staticPluginList()->append(plugin);
}

3)看到注冊函數,估計你要恍然大悟了吧,為什么在4.1.1節中要使用Q_IMPORT_PLUGIN來聲明靜態插件,肯定是要調用qRegisterStaticPluginFunction注冊自己:

#define Q_IMPORT_PLUGIN(PLUGIN) \extern const QT_PREPEND_NAMESPACE(QStaticPlugin) qt_static_plugin_##PLUGIN(); \class Static##PLUGIN##PluginInstance{ \public: \Static##PLUGIN##PluginInstance() { \qRegisterStaticPluginFunction(qt_static_plugin_##PLUGIN()); \} \}; \static Static##PLUGIN##PluginInstance static##PLUGIN##Instance;

果然,定義全局變量static##PLUGIN##Instance,在構造函數中調用了注冊函數,注冊的對象正是宏QT_MOC_EXPORT_PLUGIN定義的QStaticPlugin,下面看一下QT_MOC_EXPORT_PLUGIN

4)QT_MOC_EXPORT_PLUGIN定義

#define Q_PLUGIN_INSTANCE(IMPLEMENTATION) \{ \static QT_PREPEND_NAMESPACE(QPointer)<QT_PREPEND_NAMESPACE(QObject)> _instance; \if (!_instance) {    \QT_PLUGIN_RESOURCE_INIT \_instance = new IMPLEMENTATION; \} \return _instance; \}#  define QT_MOC_EXPORT_PLUGIN(PLUGINCLASS, PLUGINCLASSNAME) \static QT_PREPEND_NAMESPACE(QObject) *qt_plugin_instance_##PLUGINCLASSNAME() \Q_PLUGIN_INSTANCE(PLUGINCLASS) \static const char *qt_plugin_query_metadata_##PLUGINCLASSNAME() { return reinterpret_cast<const char *>(qt_pluginMetaData); } \const QT_PREPEND_NAMESPACE(QStaticPlugin) qt_static_plugin_##PLUGINCLASSNAME() { \QT_PREPEND_NAMESPACE(QStaticPlugin) plugin = { qt_plugin_instance_##PLUGINCLASSNAME, qt_plugin_query_metadata_##PLUGINCLASSNAME}; \return plugin; \}

定義了生成插件對象的全局函數和QtPluginMetaDataFunction,并返回了QStaticPlugin:

struct Q_CORE_EXPORT QStaticPlugin
{//...// Since qdoc gets confused by the use of function// pointers, we add these dummes for it to parse instead:QObject *instance();const char *rawMetaData();//...
};

4.1.3.示例

運行上面的實例,顯示出全部的插件信息,如下:

4.2.動態插件

?動態插件一般以動態庫的方式來加載的。

4.2.1.動態插件的加載過程

????????在Qt 中加載這些插件的流程比較復雜的,我們自己寫的時候就簡單多了。流程大體相似,首先定義QPluginLoader 對象。QPluginLoader 的構造函數加載插件目錄下所有插件,然后調用 instance 創建插件對象,最后就可以調用插件功能了。

QString loadPlugin(QString pluginPath)
{QPluginLoader loader(pluginPath);if(! loader.load()) {qDebug()<<"load failed ";}QObject* plugin = loader.instance();if(plugin) {BrushInterface* interface = qobject_cast<BrushInterface*>(plugin);if(interface) {//...}}else {qDebug()<<"loader.instance failed!";return QString();}
}

QPluginLoader的load過程如下:

bool QPluginLoader::load()
{if (!d || d->fileName.isEmpty())return false;if (did_load)return d->pHnd && d->instance;if (!d->isPlugin())return false;did_load = true;return d->loadPlugin();
}bool QLibraryPrivate::loadPlugin()
{if (instance) {libraryUnloadCount.ref();return true;}if (pluginState == IsNotAPlugin)return false;if (load()) {instance = (QtPluginInstanceFunction)resolve("qt_plugin_instance");return instance;}if (qt_debug_component())qWarning() << "QLibraryPrivate::loadPlugin failed on" << fileName << ":" << errorString;pluginState = IsNotAPlugin;return false;
}

上述代碼的核心在:

1) load() 為加載動態庫,window和linux實現方式不同,分別調用各自系統的API實現,windows一般調用LoadLibrary實現,linux一般調用dlopen實現,很具體的請自行查閱資料。

2)找到函數qt_plugin_instance的地址,并保存此地址,這個就是實例化插件對象的函數。

那么qt_plugin_instance是在那里定義的呢?自然會聯想到QT_MOC_EXPORT_PLUGIN:

#define Q_PLUGIN_INSTANCE(IMPLEMENTATION) \{ \static QT_PREPEND_NAMESPACE(QPointer)<QT_PREPEND_NAMESPACE(QObject)> _instance; \if (!_instance) {    \QT_PLUGIN_RESOURCE_INIT \_instance = new IMPLEMENTATION; \} \return _instance; \}#  define QT_MOC_EXPORT_PLUGIN(PLUGINCLASS, PLUGINCLASSNAME)      \Q_EXTERN_C Q_DECL_EXPORT \const char *qt_plugin_query_metadata() \{ return reinterpret_cast<const char *>(qt_pluginMetaData); } \Q_EXTERN_C Q_DECL_EXPORT QT_PREPEND_NAMESPACE(QObject) *qt_plugin_instance() \Q_PLUGIN_INSTANCE(PLUGINCLASS)

在QT_MOC_EXPORT_PLUGIN里面就有qt_plugin_instance函數。

5.定位插件

????????Qt 應用程序將會自動感知可用的插件,因為插件都被存儲在標準的子目錄當中。因此應用程序不需要任何查找或者加載插件的代碼。

????????在開發過程中,插件的目錄是 QTDIR/plugins(QTDIR 是 Qt 的安裝目錄),每個類型的插件放在相應類型的目錄下面。如果想要應用程序使用插件,但不想用標準的插件存放路徑,可以在應用程序的安裝過程中指定要使用的插件的路徑,可以使用 QSettings,保存插件路徑,在應用程序運行時讀取配置文件。應用程序可以通過QCoreApplication::addLibraryPath()函數將指定的插件路徑加載到應用程序中。

????????使插件可加載的一種方法是在應用程序所在目錄創建一個子目錄,用于存放插件。如果要發布和 Qt 一起發布的插件(存放在 plugins 目錄)中的任何插件,必須拷貝 plugins 目錄下的插件子目錄到應用程序的根目錄下。?

6.插件開發的優勢

  • 方便功能擴展:通過插件,可以輕松地擴展應用程序的功能,而不需要修改應用程序本身。
  • 更新量小:當底層接口不變時,只需要更新插件即可,而不需要重新發布整個應用程序。
  • 降低模塊間依賴:插件與主程序之間通過接口交互,降低了模塊間的依賴關系,支持并行開發。
  • 面向未來:通過插件,可以進一步演化API的功能,使API在長時間內保持可用性和適用性。

7.總結

????????Qt插件機制是一種強大且靈活的功能擴展方式,它允許開發者通過創建和加載插件來增強Qt應用程序的功能。通過遵循一定的規范,開發者可以輕松地創建和使用插件,從而滿足各種復雜和多變的需求。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/19844.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/19844.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/19844.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

GPT-4o有點坑

GPT-4o有點坑 0. 前言1. GPT-4o簡介2. GPT-4o帶來的好處2.1 可以上傳圖片和文件2.2 更豐富的功能以及插件 3. "坑"的地方3.1 使用時間短3.2 GPT-4o變懶了 4. 總結 0. 前言 原本不想對GPT-4o的內容來進行評論的&#xff0c;但是看了相關的評論一直在說&#xff1a;技…

Ai晚班車531

1.中央網信辦等三部門&#xff1a;加快推進大模型、生成式人工智能標準研制。 2.中國石油與中國移動、華為、科大訊飛簽署合作協議。 3.Opera瀏覽器與谷歌云合作&#xff0c;接入 Gemini 大模型。 4.谷歌 Gemini 加持Chromebook Plus。 5.英飛凌&#xff1a;開發 8kW和12kW…

速盾:cdn和udp的區別?

CDN&#xff08;Content Delivery Network&#xff09;和UDP&#xff08;User Datagram Protocol&#xff09;是網絡領域中常用的兩個術語&#xff0c;分別代表了不同的技術和功能。下面將分別介紹它們的區別。 功能和應用場景 CDN是一種分布式架構的網絡服務&#xff0c;通過在…

改進YOLOv8系列:構建新型單頭transformer模塊,加入到骨干尾部

改進YOLOv8系列:構建新型單頭transformer模塊,加入到骨干尾部 需要修改的代碼self attention代碼創建yaml文件測試是否創建成功本文提供了改進 YOLOv8注意力系列包含不同的注意力機制以及多種加入方式,在本文中具有完整的代碼和包含多種更有效加入YOLOv8中的yaml結構,讀者…

【論文導讀】Grid Graph Reduction for Efficient Shortest Pathfinding(2023 Access)

Grid Graph Reduction for Efficient Shortest Pathfinding 作者&#xff1a;CHAN-YOUNG KIM AND SANGHOON SULL 文章提出了一種“基于模式識別的網格阻塞”&#xff08; Pattern-Based Blocking on grid graphs&#xff0c;PBGG&#xff09;的預處理方法&#xff0c;以加快最…

XML Web 服務技術解析:WSDL 與 SOAP 原理、應用案例一覽

XML Web服務是一種用于在網絡上發布、發現和使用應用程序組件的技術。它基于一系列標準和協議&#xff0c;如WSDL、SOAP、RDF和RSS。下面是一些相關的內容&#xff1a; WSDL&#xff08;Web服務描述語言&#xff09;&#xff1a;用于描述Web服務的基于XML的語言&#xff0c;定義…

安卓手機APP開發___廣播概述

安卓手機APP開發___廣播概述 目錄 概述 關于系統廣播 系統廣播所發生的更改 接收廣播 清單聲明的接收器 上下文注冊的接收器 對進程狀態的影響 發送廣播 通過權限限制廣播 帶權限的發送 帶權限的接收 安全注意事項和最佳做法 概述 Android 應用可以通過 Android …

數據分析案例-在線食品訂單數據可視化分析與建模分類

&#x1f935;?♂? 個人主頁&#xff1a;艾派森的個人主頁 ?&#x1f3fb;作者簡介&#xff1a;Python學習者 &#x1f40b; 希望大家多多支持&#xff0c;我們一起進步&#xff01;&#x1f604; 如果文章對你有幫助的話&#xff0c; 歡迎評論 &#x1f4ac;點贊&#x1f4…

springmvc揭秘參數解析

參數解析 說到參數解析&#xff0c;springmvc中處理參數的是HandlerMethodArgumentResolver接口 public interface HandlerMethodArgumentResolver { // 判斷是否支持該類型參數 boolean supportsParameter(MethodParameter parameter); // 進行參數解析 Object resolv…

[羊城杯 2021]BabySmc

運行就是輸入flag 不知道怎么跳過去的 這個應該就是smc加密的函數了 運行完這個函數才能繼續往下 int __cdecl main(int argc, const char **argv, const char **envp) {__int64 v3; // rbx__int64 v4; // r12__int64 v5; // r13unsigned __int64 v6; // raxchar v7; // spcha…

學習Vue中圖片上傳前進行壓縮的實現方法

學習Vue中圖片上傳前進行壓縮的實現方法 一、前言1. 為什么要在客戶端進行圖片壓縮&#xff1f;2. Vue組件中實現圖片上傳前壓縮的方法3. 注意事項與優化4. 總結 一、前言 在Web開發中&#xff0c;圖片上傳是一個常見的功能需求&#xff0c;而客戶端對圖片進行壓縮可以有效減小…

企業如何進行快遞運費對賬?

在電子面單寄件取代手寫紙質面單之后&#xff0c;加上月結寄件模式的推行&#xff0c;企業快遞運費對賬&#xff0c;成了行政的一個難題...... 早期的手寫紙質面單寄件&#xff0c;企業行政或者財務相關人員&#xff0c;遵循寄前審批&#xff0c;寄后報銷的原則進行對賬。隨著電…

FinalShell無法連接Linux

Linux使用Vmware會創建一個網絡&#xff0c;讓兩個子網處于一個網關&#xff0c;這樣就能在windows中連接Linux&#xff0c;只有在這種情況下才能FinalShell才能連接Linux

面試題合集(2)

1. Self Attention的時候 Q K T QK^T QKT之后要除以 d ? \sqrt{d}? d ?? 參考蘇劍林大神&#xff1a; 淺談Transformer的初始化、參數化與標準化 模型初始化&#xff1a;介紹了常用的采樣分布&#xff0c;包括正態分布、均勻分布和截尾正態分布。并從代數角度理解初始化方…

module_param的用法

在Linux內核模塊編程中,`module_param`宏允許你聲明一個模塊參數。模塊參數是指可以在加載模塊時從命令行設置的參數,也可以通過/sys文件系統(如果內核配置了CONFIG_SYSFS)在模塊加載后進行修改。這些參數對于調整模塊的行為而不需要重新編譯模塊代碼非常有用。 使用方法 …

KT6368A雙模藍牙芯片上電到正常發送AT指令或指令復位需要多久

一、簡介 KT6368A芯片上電到正常發送AT指令&#xff0c;或者開啟藍牙廣播被搜索到&#xff0c;或者指令復位需要多久等等系列問題總結 詳細描述 其實這些問題歸結到一起&#xff0c;就還是一個問題&#xff0c;芯片上電需要多久的時間 在另外一份文檔里面&#xff0c;是有描…

跟我學C++中級篇——if constexpr的應用

一、場景應用 在一個開發場景下&#xff0c;需要動態處理不同類型的數據寫入。本來這個非常簡單&#xff0c;只要定義一個模板即可搞定&#xff0c;但這里偏偏有一個細節&#xff0c;是調用別人的庫來實現寫入。而這個庫對不同的數據類型的寫入&#xff0c;提供了N種不同的函數…

Python實戰開發及案例分析(28)—— 預編碼算法

預編碼算法&#xff08;Precoding Algorithm&#xff09;通常用于無線通信系統中&#xff0c;尤其是多輸入多輸出&#xff08;MIMO&#xff09;系統中&#xff0c;以提高數據傳輸的可靠性和效率。預編碼是為了在發送端對信號進行處理&#xff0c;以優化傳輸性能。 在MIMO系統中…

Java設計模式 _行為型模式_訪問者模式

一、訪問者模式 1、訪問者模式 訪問者模式&#xff08;Visitor Pattern&#xff09;是一種行為型模式。它允許在不修改已有類結構的情況下&#xff0c;向類中添加新的操作。訪問者模式通過將操作封裝在一個訪問者對象中&#xff0c;使得可以在不改變各個元素類的前提下&#x…

RedisTemplate實戰應用--隊列等

一、RedisTemplate隊列插入 1、從集合左邊插入值 https://blog.csdn.net/weixin_43658899/article/details/121040307 leftPush(K key, V value) redisTemplate.opsForList().leftPush("leftdatakey","bbbb");2、從集合左邊開始在v1值后邊插入新值v2 le…