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

Pluma 插件管理框架

1. 概述

    Pluma 是一个用 C++ 开发的可用于管理插件的开源架构,其官网地址为:http://pluma-framework.sourceforge.net/。该架构是个轻量级架构,非常易于理解。
    Pluma 架构有以下基本概念:
1)插件的外在行为体现为一个纯虚类,可以叫作插件接口;
2)继承于同一个插件接口的若干派生类,被认为属于同一种插件,可以叫作插件类;
3)每一个插件接口或插件类都有个一一对应的 Provider 类,其中,插件接口对应的 Provider 类里会定义一个特殊字符串常量:PLUMA_PROVIDER_TYPE,表示这一类 “插件 Provider” 共同的类型名称,而这个类型名称其实就是插件接口的类名字符串。
4)多个插件类可以被放入一个插件动态库中,而这个动态库文件名(不包括后缀部分)可以叫作 “插件名”。
5)插件机制使用者可以在自己的架构中包含一个 Pluma 管理类,该类支持从所指定的位置加载一个或多个插件动态库,并将每个插件类对应的 Provider,记录进内部的表中。
6)插件机制使用者可以在合适时机,利用 Pluma 获取内含的插件 Provider,并调用某个插件 Provider 的 create () 函数,创建出对应的插件对象。
7)使用完插件对象后,不要忘了 delete 它。

    现在我们画一张示意图:

2. Pluma 管理类

我们刚刚也说了,插件机制使用者可以包含一个 Pluma 管理类。该类继承于 PluginManager 类。

【pluma-1.1/include/pluma/PluginManager.hpp】

class PLUMA_API PluginManager{
public:~PluginManager();bool load(const std::string& path);bool load(const std::string& folder, const std::string& pluginName);int loadFromFolder(const std::string& folder, bool recursive = false);bool unload(const std::string& pluginName);void unloadAll();bool addProvider(Provider* provider);void getLoadedPlugins(std::vector<const std::string*>& pluginNames) const;bool isLoaded(const std::string& pluginName) const;protected:PluginManager();void registerType(const std::string& type, unsigned int version, unsigned int lowestVersion);const std::list<Provider*>* getProviders(const std::string& type) const;private:static std::string getPluginName(const std::string& path);static std::string resolvePathExtension(const std::string& path);private:typedef bool fnRegisterPlugin(Host&);typedef std::map<std::string,DLibrary*> LibMap;LibMap libraries;  ///< Map containing the loaded librariesHost host;         ///< Host app proxy, holding all providers
};

    从上面的 load () 函数和 loadFromFolder () 函数可以看出,插件管理器既允许用户单独加载某个插件动态库,也允许批量性加载某个目录下所有的插件动态库。另外,值得注意的是,getProviders () 函数是 protected 的成员,也就是说,这套架构是不希望用户直接使用这个 PluginManager 类的,即便用了,你也拿不到 Provider。正确的做法是,使用 PluginManager 的子类:Pluma 管理类。

    另外,上面的成员变量 libraries,就是记录所有已加载的插件动态库的映射表。而成员变量 host 则负责记录每个插件类对应的 Provider 信息。之所以被称为 host(宿主),是针对插件而言的。也就是说插件本身实际上是没资格知道其真实宿主的全貌的,它只能访问和它相关的很小一部分数据而已,因此 Pluma 将这一小部分数据整理成一个 host 代理,供插件使用。    Pluma 管理类的代码截选如下:
【pluma-1.1/include/pluma/Pluma.hpp】

class Pluma: public PluginManager{
public:Pluma();template<typename ProviderType>void acceptProviderType();template<typename ProviderType>void getProviders(std::vector<ProviderType*>& providers);
};#include <Pluma/Pluma.inl>

请大家注意上面代码中最后一行,这个 Pluma.hpp 还真是有点手黑,偷偷摸摸 #include 了个 Pluma.inl 文件,其实展开来就是 acceptProviderType () 和 getProviders () 这两个模板函数的实现。Pluma.inl 文件的内容如下:
【pluma-1.1/include/pluma/Pluma.inl】

inline Pluma::Pluma(){// Nothing to do
}template<typename ProviderType>
void Pluma::acceptProviderType(){PluginManager::registerType(ProviderType::PLUMA_PROVIDER_TYPE,ProviderType::PLUMA_INTERFACE_VERSION,ProviderType::PLUMA_INTERFACE_LOWEST_VERSION);
}template<typename ProviderType>
void Pluma::getProviders(std::vector<ProviderType*>& providers){const std::list<Provider*>* lst = PluginManager::getProviders(ProviderType::PLUMA_PROVIDER_TYPE);if (!lst) return;providers.reserve(providers.size() + lst->size());std::list<Provider*>::const_iterator it;for (it = lst->begin() ; it != lst->end() ; ++it)providers.push_back(static_cast<ProviderType*>(*it));
}

    看到了吧,重新定义了个 getProviders (),还搞成一个模板函数,在函数体内会反过来通过模板参数,进一步得到所涉及的插件 Provider 的 PLUMA_PROVIDER_TYPE 信息,这个技巧挺重要。也就是说,外界传来的是 vector<ProviderType>,而函数内部可以推断出 ProviderType::PLUMA_PROVIDER_TYPE。将 PLUMA_PROVIDER_TYPE 传入父类的 PluginManager::getProviders () 函数,就可以拿到符合所指类型的所有 Provider。

    我们画一张 Pluma 简图,后面再细说相关细节:

同一类插件类,会对应一个 ProviderInfo 节点,该节点内部的 providers 列表,记录着同属一类的若干 Provider。

2.1 Host 代理

【pluma-1.1/include/pluma/Host.hpp】

class PLUMA_API Host{
friend class PluginManager;
friend class Provider;public:bool add(Provider* provider);private:Host();~Host();bool knows(const std::string& type) const;unsigned int getVersion(const std::string& type) const;unsigned int getLowestVersion(const std::string& type) const;void registerType(const std::string& type, unsigned int version, unsigned int lowestVersion);const std::list<Provider*>* getProviders(const std::string& type) const;void clearProviders();bool validateProvider(Provider* provider) const;bool registerProvider(Provider* provider);void cancelAddictions();bool confirmAddictions();private:struct ProviderInfo{unsigned int version;unsigned int lowestVersion;std::list<Provider*> providers;};typedef std::map<std::string, ProviderInfo > ProvidersMap;typedef std::map<std::string, std::list<Provider*> > TempProvidersMap;ProvidersMap knownTypes;       ///< Map of registered types.TempProvidersMap addRequests;  ///< Temporarily added providers
};

正如前文所说,Host 代理是针对插件而言的。而 Host 只有一个 public 成员函数 add (),说明其主要对外行为就是让插件将对应的 provider 注册进 Host。

3. 插件类和其对应的 Provider 类

    在说了一大堆插件管理类代码后,现在终于要开始说插件部分了。前文已经说过,插件的外在行为体现为一个纯虚类,可以叫作插件接口。我们现在就以 Pluma 源码中给出的例子为准,来说明一些细节。

3.1 Warrior 接口和 WarriorProvider 类

    Pluma 中的插件接口例子是 Warrior,其源码截选如下:
【pluma-1.1/example/src/interface/Warrior.hpp】

#include <Pluma/Pluma.hpp>
class Warrior{
public:virtual std::string getDescription() = 0;// (...)
};
PLUMA_PROVIDER_HEADER(Warrior);

这个接口里只象征性的写了一个成员函数 getDescription (),大家明白意思即可。

    需要注意的是类定义之后的那句 PLUMA_PROVIDER_HEADER,这个宏负责定义和插件接口对应的 Provider 类。相关的宏定义如下:
【pluma-1.1/include/pluma/Pluma.hpp】

#define PLUMA_PROVIDER_HEADER(TYPE)\
PLUMA_PROVIDER_HEADER_BEGIN(TYPE)\
virtual TYPE* create() const = 0;\
PLUMA_PROVIDER_HEADER_END#define PLUMA_PROVIDER_HEADER_BEGIN(TYPE)\
class TYPE##Provider: public pluma::Provider{\
private:\friend class pluma::Pluma;\static const unsigned int PLUMA_INTERFACE_VERSION;\static const unsigned int PLUMA_INTERFACE_LOWEST_VERSION;\static const std::string PLUMA_PROVIDER_TYPE;\std::string plumaGetType() const{ return PLUMA_PROVIDER_TYPE; }\
public:\unsigned int getVersion() const{ return PLUMA_INTERFACE_VERSION; }#define PLUMA_PROVIDER_HEADER_END };

基于这些宏定义,我们可以将 PLUMA_PROVIDER_HEADER (Warrior) 展开为:

class WarriorProvider: public pluma::Provider{
private:friend class pluma::Pluma;static const unsigned int PLUMA_INTERFACE_VERSION;static const unsigned int PLUMA_INTERFACE_LOWEST_VERSION;static const std::string PLUMA_PROVIDER_TYPE;std::string plumaGetType() const{ return PLUMA_PROVIDER_TYPE; }
public:unsigned int getVersion() const{ return PLUMA_INTERFACE_VERSION; }virtual Warrior* create() const = 0;
};

代码很清晰,为 Warrior 接口声明一个配套的 WarriorProvider 类。这个类里包含着重要的 PLUMA_PROVIDER_TYPE 常量,以及最关键的 create () 函数。

    Warrior 的实现文件更加简单:
【pluma-1.1/example/src/interface/Warrior.cpp】

#include "Warrior.hpp"
PLUMA_PROVIDER_SOURCE(Warrior, 1, 1);

也在使用宏,展开宏后可见:

const std::string WarriorProvider::PLUMA_PROVIDER_TYPE = "Warrior";
const unsigned int WarriorProvider::PLUMA_INTERFACE_VERSION = 1;
const unsigned int WarriorProvider::PLUMA_INTERFACE_LOWEST_VERSION = 1;

因为 Warrior 本身是个纯虚类,所以 WarriorProvider 里也不用实现 create () 函数。

3.2 Warrior 派生类和派生 Provider

    在 pluma 源码的例子中,提供了三个 Warrior 派生类,SimpleWarrior、Eagle 和 Jaguar。默认的是 SimpleWarrior,它被集成进 example/src/host 目录。也就是说,即便我们一个额外的插件库都不提供,示例至少还可以使用 SimpleWarrior。而 Eagle 和 Jaguar 则位于 example/src/plugin 目录,可以打包进一个插件动态库。

【pluma-1.1/example/src/host/SimpleWarrior.hpp】

#include "Warrior.hpp"
class SimpleWarrior: public Warrior{
public:std::string getDescription(){return "Commoner: leaded by calpoleque";}
};
PLUMA_INHERIT_PROVIDER(SimpleWarrior, Warrior);

前文我们已经看到,对于插件接口(Warrior)来说,用到的宏是 PLUMA_PROVIDER_HEADER(Warrior),现在针对实际插件类(SimpleWarrior),会用到另一个宏 PLUMA_INHERIT_PROVIDER(SimpleWarrior, Warrior)。这个宏的定义如下:
【pluma-1.1/include/pluma/Pluma.hpp】

#define PLUMA_INHERIT_PROVIDER(SPECIALIZED_TYPE, BASE_TYPE)\
class SPECIALIZED_TYPE##Provider: public BASE_TYPE##Provider{\
public:\BASE_TYPE * create() const{ return new SPECIALIZED_TYPE (); }\
};

展开后可见:

class SimpleWarriorProvider: public WarriorProvider{
public:Warrior * create() const { return new SimpleWarrior (); }
};

很简单,就是在完成 Provider 的核心使命,提供一个创建插件类对象的 create () 函数。与 SimpleWarriorProvider 类似,另外两个 Warrior 派生类 Eagle 和 Jaguar 大体也是这么写的。示意图如下:

在研究 Pluma 所给示例时,我已事先将 Pluma 封装成静态库了,现在要把 Eagle 和 Jaguar 编译并封装成一个动态库,就需要链接 Pluma 静态库,除此之外,还需要编译其他一些辅助文件,列举如下:
1)Connector.cpp
2)dllmain.cpp
3)Eagle.hpp
4)Jaguar.hpp
5)Warrior.cpp
其中 Connector.cpp 文件,是插件动态库向外界 Host 注册自己所有 Provider 的地方。它必须实现一个 connect () 函数,代码截选如下:

#include <Pluma/Connector.hpp>
#include "Eagle.hpp"
#include "Jaguar.hpp"PLUMA_CONNECTOR
bool connect(pluma::Host& host){host.add( new EagleProvider() );host.add( new JaguarProvider() );return true;
}

    我们先不要着急分析上面的 connect () 动作,可以先跟着我看看插件的加载流程,后文我们就会知道,connect () 只是加载流程的一环而已。

4. 插件加载流程

    我们看一下 Pluma 架构所给例子的 main () 函数,就可以了解插件的加载流程了:

int main() 
{pluma::Pluma pluma;pluma.acceptProviderType<WarriorProvider>();   // 表明用户感兴趣东西,添加ProviderInfopluma.load("plugins", "PlumaDemoWarriorPlugin");  // 加载动作,向ProviderInfo里加料std::vector<WarriorProvider*> providers;pluma.getProviders(providers);std::vector<WarriorProvider*>::iterator it;for (it = providers.begin(); it != providers.end(); ++it) {Warrior* warrior = (*it)->create();std::cout << warrior->getDescription() << std::endl;delete warrior;}pluma.unloadAll();std::cout << "Press any key to exit";std::cin.ignore(10000, '\n');return 0;
}

其中和加载插件相关的句子主要就是 pluma.acceptProviderType 和 pluma.load 两句了。前者主要负责在 Host 的 knownTypes 映射表中添加一个 ProviderInfo 节点,后者负责加载插件动态库,并将动态库里匹配的 Provider 指针记入 ProviderInfo 节点。

4.1 pluma.acceptProviderType<>()

    我们先说 pluma.acceptProviderType 一句。在前文介绍 Pluma.inl 文件的内容时,我们已经看到一个叫作 acceptProviderType 的模板函数了,当时没有细说,现在我把它的代码再贴一下:
【pluma-1.1/include/pluma/Pluma.inl】

template<typename ProviderType>
void Pluma::acceptProviderType(){PluginManager::registerType(ProviderType::PLUMA_PROVIDER_TYPE,ProviderType::PLUMA_INTERFACE_VERSION,ProviderType::PLUMA_INTERFACE_LOWEST_VERSION);
}

里面调用的是 PluginManager 基类的 registerType () 函数。我们前文主要关心的是 PLUMA_PROVIDER_TYPE,现在再说一下后两个参数。PLUMA_INTERFACE_VERSION 表示管理器当前应该使用的插件接口的版本,因为我们不能确定更高版本的插件接口会不会增加或删除成员函数,所以这个值其实是个限定值,如果后续用户尝试加载更高版本的插件,那么是无法通过校验的。第三个参数 PLUMA_INTERFACE_LOWEST_VERSION 则是限定最低值,如果尝试加载比这个值更低版本的插件,肯定也是不会通过的。

    在刚刚看到的 main () 函数里,是这样写的:

pluma.acceptProviderType<WarriorProvider>();

也就是说,Pluma 插件管理器对 Warrior 接口对应的 WarriorProvider 类感兴趣。而当初定义 Warrior 时,在 Warrior.cpp 文件里的确指明了 WarriorProvider 能限定的当前版本号和最低版本号:

PLUMA_PROVIDER_SOURCE(Warrior, 1, 1);

    这些类型信息、版本号限定信息都会被注册在 Host 的 knownTypes 映射表中,每种接口类型对应一个 ProviderInfo 节点。注册动作的代码如下:
【pluma-1.1/src/pluma/PluginManager.cpp】

void PluginManager::registerType(const std::string& type, unsigned int version,unsigned int lowestVersion){host.registerType(type, version, lowestVersion);
}

【pluma-1.1/src/pluma/Host.cpp】

void Host::registerType(const std::string& type, unsigned int version, unsigned int lowestVersion){if (!knows(type)){ProviderInfo pi;pi.version = version;pi.lowestVersion = lowestVersion;knownTypes[type] = pi;}
}

当然,新加的 ProviderInfo 节点的 providers 列表是个空列表,待后续再添加 Provider * 内容。

4.2 pluma.load()

    接着,我们继续看 main () 函数里调用的 pluma.load (),其实调用的是其父类 PluginManager 的 load ()。相关代码截选如下:
【pluma-1.1/src/pluma/PluginManager.cpp】

bool PluginManager::load(const std::string& path){std::string plugName = getPluginName(path);std::string realPath = resolvePathExtension(path);DLibrary* lib = DLibrary::load(realPath);......fnRegisterPlugin* registerFunction;registerFunction = reinterpret_cast<fnRegisterPlugin*>(lib->getSymbol("connect"));......if (!registerFunction(host)){......return false;}if (host.confirmAddictions())libraries[plugName] = lib;else{......return false;}return true;
}

可以看到,一开始就在着手加载动态库,并调用动态库里的 connect () 函数。前文我们实际上已经列举过示例代码里的 connect () 函数了,现在再贴一次:

PLUMA_CONNECTOR
bool connect(pluma::Host& host){host.add( new EagleProvider() );host.add( new JaguarProvider() );return true;
}

    前文在阐述到 connect () 时,暂时没有细说 add () 动作,现在我们来看看它的代码:
【pluma-1.1/src/pluma/Host.cpp】

bool Host::add(Provider* provider){if (provider == NULL){fprintf(stderr, "Trying to add a null provider.\n");return false;}if (!validateProvider(provider)){delete provider;return false;}// 临时放进 addRequests表addRequests[ provider->plumaGetType() ].push_back(provider);return true;
}

    上面代码中那个 plumaGetType () 函数其实是 Provider 的私有成员,一般人访问不了,但 Host 是它的友元类,所以可以访问。代码中会先校验待添加的 Provider 是否合格,如果合格则以 plumaGetType () 返回值为 key 值,并向临时映射表 addRequests 中添加该 Provider 指针。所谓合格是指,这个 Provider 的类型是 Host 感兴趣的,并且其版本号也是合适的。

    值得注意的是,待添加的 Provider*,只是临时先放进一个 addRequests 映射表中。addRequests 映射表的定义如下:
【pluma-1.1/include/pluma/Host.hpp】

typedef std::map<std::string, std::list<Provider*> > TempProvidersMap;
......
TempProvidersMap addRequests;

那么这个临时性的 addRequests 映射表的内容会怎样处理呢?说起来也简单,会被 “搬移” 进 Host 的 knownTypes 映射表中某个 ProviderInfo 的内部列表去。main () 在调用完 connect () 函数后,调用的 confirmAddictions () 就是做这个事情的:
【pluma-1.1/src/pluma/Host.cpp】

bool Host::confirmAddictions(){if (addRequests.empty()) return false;TempProvidersMap::iterator it;for( it = addRequests.begin() ; it != addRequests.end() ; ++it){std::list<Provider*> lst = it->second;std::list<Provider*>::iterator providerIt;for (providerIt = lst.begin() ; providerIt != lst.end() ; ++providerIt){knownTypes[it->first].providers.push_back(*providerIt);}}TempProvidersMap().swap(addRequests);  // 清空addRequests表return true;
}

    我们画一张调用关系图看看:

    我们可以通过这张调用关系图回顾一下,主要流程就是在加载插件动态库,并执行动态库里的 connect () 函数。该函数会将动态库里可用的所有 Provider * 记入 Host 的 knownTypes 映射表中。同时,动态库对应的 DLibrary 对象,也会插入 Pluma 管理类内部的 libraries 映射表中。

    为了巩固知识,我们把前文的两张图再整合一下。

5. 使用插件 Provider

5.1 pluma.getProviders()

    在 Providers 都添加进 Pluma 管理类后,我们就可以在需要时获取 provider 了,为此 Pluma 类提供了 getProviders () 函数:
【pluma-1.1/src/pluma/PluginManager.cpp】

const std::list<Provider*>* PluginManager::getProviders(const std::string& type) const{return host.getProviders(type);
}

【pluma-1.1/src/pluma/Host.cpp】

const std::list<Provider*>* Host::getProviders(const std::string& type) const{ProvidersMap::const_iterator it = knownTypes.find(type);if (it != knownTypes.end())return &it->second.providers;return NULL;
}

代码很简单,就是帮使用者把感兴趣的某类插件 Provider 全部找出来。如果当初我们已经通过 acceptProviderType () 注册了对应的类型(PLUMA_PROVIDER_TYPE),那么至少可以拿到一个 list,否则就只能拿到 NULL 了。如果我们可以拿到若干 Provider,就可以调用其 create () 函数创建对应的插件对象了。

    当工作做完后,用户应该及时 delete 掉之前创建出的插件对象。在程序退出之前,用户应该调用 pluma.unloadAll () 删除所有插件 Provider 及 DLibrary 对象。DLibrary 对象析构时,会自动关闭已经打开的动态链接库。
【pluma-1.1/src/pluma/PluginManager.cpp】

void PluginManager::unloadAll(){host.clearProviders();LibMap::iterator it;for (it = libraries.begin() ; it != libraries.end() ; ++it){delete it->second;   // delete掉DLibrary对象}libraries.clear();
}
void Host::clearProviders(){ProvidersMap::iterator it;for (it = knownTypes.begin() ; it != knownTypes.end() ; ++it){std::list<Provider*>& providers = it->second.providers;std::list<Provider*>::iterator provIt;for (provIt = providers.begin() ; provIt != providers.end() ; ++provIt){delete *provIt;   // delete掉Provider对象}std::list<Provider*>().swap(providers);}
}

6. 结束

    至此,Pluma 架构的主体代码就分析完毕了,希望对大家有所帮助。

相关文章:

Pluma 插件管理框架

1. 概述 Pluma 是一个用 C 开发的可用于管理插件的开源架构&#xff0c;其官网地址为&#xff1a;http://pluma-framework.sourceforge.net/。该架构是个轻量级架构&#xff0c;非常易于理解。 Pluma 架构有以下基本概念&#xff1a; 1&#xff09;插件的外在行为体现为一个…...

Leetcode11 盛最多水的容器

Leetcode11 盛最多水的容器 来源&#xff1a;力扣&#xff08;LeetCode&#xff09; 链接&#xff1a;https://leetcode.cn/problems/container-with-most-water/description 博主Github&#xff1a;https://github.com/GDUT-Rp/LeetCode 题目&#xff1a; 给定一个长度为 n…...

Java

FileOutputStream写数据的3种方式 void write(int b) //一次写一个字节的数据 void write(byte[] b) //一次写一个字节数组数据 void write(byte[] b, int off,int len) //一次写一个字节数组的部分数据 参数一:数组;参数二:起始索引 0;参数三:个数换行: windows:“\r\n” lin…...

第十四章行为性模式—策略模式

文章目录 命令模式解决的问题结构实例存在的问题适用场景 JDK源码解析 行为型模式用于描述程序在运行时复杂的流程控制&#xff0c;即描述多个类或对象之间怎样相互协作共同完成单个对象无法单独完成的任务&#xff0c;它涉及算法与对象间职责的分配。行为型模式分为类行为模式…...

Leaflet基本用法

使用 阿里云地理工具 获取相应的地理JSON数据&#xff0c;用于对地图边界绘制。 如何使用leaflet&#xff1f; 这里用HTML5进行操作&#xff1b; 因为我是用的是Leaflet库&#xff0c;所以要引入JavaScript 和 CSS 文件&#xff08;可参考官网https://leafletjs.com/&#x…...

Unity | HDRP高清渲染管线学习笔记:示例场景解析

目录 一、HDRP入门 1.HDRP设置 1.1 HDRP配置文件中的全部设置项 1.1.1 Rendering下的Lit Shader Mode 1.1.2 Lighting 下的Volumetrics&#xff08;体积光&#xff09;和Screen Space Reflection&#xff08;屏幕空间反射&#xff09; 2.离线渲染VS实时渲染 3.Volume组件 …...

【Netty】Netty 编码器(十三)

文章目录 前言一、MessageToByteEncoder 抽象类二、MessageToMessageEncoder 抽象类总结 前言 回顾Netty系列文章&#xff1a; Netty 概述&#xff08;一&#xff09;Netty 架构设计&#xff08;二&#xff09;Netty Channel 概述&#xff08;三&#xff09;Netty ChannelHan…...

Netty和Tomcat的区别、性能对比

文章目录 一、Netty和Tomcat有什么区别&#xff1f;二、为什么Netty受欢迎&#xff1f;三、Netty为什么并发高 &#xff1f; 一、Netty和Tomcat有什么区别&#xff1f; Netty和Tomcat最大的区别就在于通信协议&#xff0c;Tomcat是基于Http协议的&#xff0c;他的实质是一个基…...

chatgpt赋能python:Python函数调用局部变量-深入了解

Python函数调用局部变量-深入了解 函数调用局部变量是Python中的一个重要概念&#xff0c;特别是在大型项目中&#xff0c;其中多个函数共享相同变量时。在本文中&#xff0c;我们将深入探讨Python函数调用局部变量&#xff0c;并为您介绍一些实用技巧。 什么是Python函数调用…...

Android 12.0 NavigationBarView 导航栏 左边显示的修改

1.概述 在12.0定制化开发中,要求导航栏左边显示的定制化,这时需要了解导航栏的显示控制方向,然后修改显示方向 在10.0以后关于导航栏显示位置都是在DisplayPolicy.java中处理的所以查询相关的设置方法,然后修改导航栏显示方向2.NavigationBarView 导航栏 左边显示的修改的…...

Mybatis源码细节探究:二级缓存Cache对象是在什么时候创建的?

给自己的每日一句 不从恶人的计谋&#xff0c;不站罪人的道路&#xff0c;不坐亵慢人的座位&#xff0c;惟喜爱耶和华的律法&#xff0c;昼夜思想&#xff0c;这人便为有福&#xff01;他要像一棵树栽在溪水旁&#xff0c;按时候结果子&#xff0c;叶子也不枯干。凡他所做的尽…...

【数据库】无效数据:软件测试对无效数据的处理

目录 一、无效数据的常见场景 &#xff08;1&#xff09;测试阶段 &#xff08;2&#xff09;测试方法 二、无效数据的概念 三、无效数据的影响 四、无效数据的识别 五、无效数据的处理方法 &#xff08;1&#xff09;拒绝无效数据 ① 拒绝无效数据的概念 ② 拒绝…...

高精度电压源如何设计出来的

高精度电压源是一种用于提供高精度电压的电子设备&#xff0c;通常用于测量和控制系统。高精度电压源的设计是一个复杂的过程&#xff0c;需要考虑多个因素&#xff0c;包括电路设计、元件选型、测量误差、稳定性等。下面将从电路设计和元件选型两个方面&#xff0c;详细介绍高…...

混合属性mix-blend-mode不生效

下面的ABCDE是混合图层&#xff0c;box是他们的父级&#xff0c;一般浏览器支持都没什问题需要注意的是&#xff0c;确保父元素不是透明的&#xff0c; 我使用的时候发现给父元素rgba设置透明度这种方式没啥作用&#xff0c;还得是纯色&#xff0c;没去深究&#xff0c;设置纯色…...

测试计划编写说明

第1章 引言 1.1目的 简述本计划的目的,旨在说明各种测试阶段任务、人员分配和时间安排、工作规范等。 测试计划在策略和方法的高度说明如何计划、组织和管理测试项目。测试计划包含足够的信息使测试人员明白项目需要做什么是如何运作的。另外,清晰的文档结构能使任何一个读…...

Android 12.0Recent列表不显示某个app

1.概述 在12.0 的产品定制化开发中,在点击导航栏最近任务列表时,如果做到不显示某个app 呢 一种做法是在app中直接处理 一种做法是在framework中处理 接下来看这两种处理方法 1, app中处理 为该应用AndroidManifest xml文件中主MainActivity设置属性 android:excludeFromR…...

力扣sql中等篇练习(二十七)

力扣sql中等篇练习(二十七) 1 连续两年有3个及以上订单的产品 1.1 题目内容 1.1.1 基本题目信息 1.1.2 示例输入输出 1.2 示例sql语句 # Write your MySQL query statement below WITH T as (SELECT t.product_id,t.d,count(order_id) numFROM(SELECT order_id,product_id,…...

Linux:LNMP的架构与环境配置

Linux&#xff1a;LNMP的架构与环境配置 一、安装 Nginx 服务1.1 安装依赖包1.2 创建运行用户1.3 编译安装1.4 优化路径1.5 添加 Nginx 系统服务 二、安装 MySQL 服务2.1安装Mysql环境依赖包2.2 创建运行用户2.3 编译安装2.4 修改mysql 配置文件2.5 更改mysql安装目录和配置文件…...

【Zero to One系列】在WSL linux系统上,使用docker运行Mysql与Nacos,以及如何启动与停止WSL

前期回顾&#xff1a; 【Zero to One系列】window系统安装Linux、docker 1、下载docker-compose 1.下载&#xff1a; curl -SL https://github.com/docker/compose/releases/download/v2.17.2/docker-compose-linux-x86_64 -o /usr/local/bin/docker-compose 2.授予权限&a…...

WASender - Whatsapp server and bulk sender

WASender 是一个 whatsapp 营销平台&#xff0c;它使用 Laravel 和 Node Js 构建。WhatsApp 是世界上最受欢迎的消息应用程序之一&#xff0c;拥有超过 20 亿活跃用户。这使其成为企业接触潜在客户并与现有客户群互动的有吸引力的平台。WASender 客户可以创建多个设备来向他的目…...

回溯递归的剪枝模版

题目传送门 主要看灵神的二分模版&#xff0c;如何使用递归实现在 O ( m k ) O(mk) O(mk)时间内&#xff0c;实现对于二分中每个条件的判断。 一般套路&#xff1a; dfs函数返回值为布尔类型 循环中使用一个dfs&#xff0c;如果其返回true&#xff0c;那么直接这个dfs返回tru…...

2023-5-30第三十天

effort力气&#xff0c;精力&#xff0c;努力 affect影响&#xff0c;改变&#xff0c;感动 effect结果&#xff0c;效果&#xff0c;影响 worker ampersand &号 asterrisk *号 deal difficulty lose magic proprientary专卖的&#xff0c;所有权 property vow…...

我国中央商务区(CBD)的空间重构及发展模式

中央商务区&#xff08;Central Business District&#xff0c;简称为CBD&#xff09;&#xff0c;原始意义为“商业会聚之地”是指一个国家或城市商务活动的主要集中的区域&#xff0c;是汇聚商务服务、金融服务、科技服务、咨询服务、会展服务、文化服务等服务业的集聚区域&a…...

Shell脚本的基本例子

版权声明&#xff1a;本文为博主原创文章&#xff0c;遵循 CC 4.0 BY-SA 版权协议&#xff0c;转载请附上原文出处链接和本声明。 大数据系列文章目录 目录 定义变量&#xff0c;输出变量输盘输入&#xff0c;执行Lunix命令变量禁止修改变量删除获取传递的变量字符串拼接&…...

C++设计模式介绍与分类

目录 一、设计模式定义 二、设计模式的优点 三、设计模式缺点 四、设计模式中的抽象思维 五、抽象的方法 六、设计模式应用场景 七、设计模式分类 附加知识 &#xff08;1&#xff09;C面向对象三种访问修饰符 &#xff08;2&#xff09;父类析构函数必须为虚函数 &…...

【Redis25】Redis进阶:分布式锁实现

Redis进阶&#xff1a;分布式锁实现 锁这个概念&#xff0c;不知道大家掌握的怎么样。我是先通过 Java &#xff0c;知道在编程语言中是如何使用锁的。一般 Java 的例子会是操作一个相同的文件&#xff0c;但其实我们知道&#xff0c;不管是文件&#xff0c;还是数据库中的一条…...

【蓝桥杯算法题】输入输出流问题

【蓝桥杯算法题】输入输出流问题 题目&#xff1a;对文本文件进行带缓存的读写操作&#xff0c;可以读取文件不同位置的信息&#xff0c;可以进行对象序列化和对象反序列化。解释&#xff1a;总结&#xff1a; 题目&#xff1a;对文本文件进行带缓存的读写操作&#xff0c;可以…...

BUG提交单模版一

提交人员 XX 提交时间 2005-06-16 产品名称...

Android 12.0系统默认授予读写权限给第三方app

1.概述 在12.0的系统rom定制化开发中, 在6.0以前读写权限是默认授予的,app不需要申请权限 在10.0之前需要android.permission.WRITE_EXTERNAL_STORAGE和android.permission.READ_EXTERNAL_STORAGE 权限就可以了而在安卓11的时候继续强化对SD卡读写的管理,引入了MANAGE_EXTER…...

【生信】R语言在RNA-seq中的应用

R语言在RNA-seq中的应用 文章目录 R语言在RNA-seq中的应用生成工作流环境读取和处理数据由targets文件提供实验定义对实验数据进行质量过滤和修剪生成FASTQ质量报告 比对建立HISAT2索引并比对 读长量化读段计数样本间的相关性分析 差异表达分析运行edgeR可视化差异表达结果计算…...