当前位置:网站首页>Gstreamer Plugin注册流程详解
Gstreamer Plugin注册流程详解
2022-08-02 13:49:00 【Geek.Fan】
1、从plugin_init()函数
gstreamer插件,直接就是经过plugin_init()函数注册到Gstreamer中的,每一个plugin都是在plugin_init()函数中经过gst_element_register()函数将plugin的相应信息注册到gstreamer中。
函数如下:
static gboolean plugin_init (GstPlugin * plugin)
{
if (!gst_element_register (plugin, "mssdemux",
GST_RANK_PRIMARY, GST_TYPE_MSS_DEMUX))
return FALSE;
return TRUE;
}从以上函数可知,plugin_init()函数只有一个参数,这个参数就是GSTPlugin指针类型的plugin指针,如今咱们不知道它具体是干吗的,可是咱们能够先猜想一下,这个应该是在gstreamer核心在进行注册plugin的时候,经过传进指针获得plugin的相应信息,然后方便管理和查找,咱们后续验证一下,看看这个函数调用的时候,是否如同咱们猜想的那样。编程
在plugin_init()函数中,经过gst_element_register()函数将plugin的相应信息注册到gstreamer中,gst_element_register()函数声明以下:
/** * gst_element_register: * @plugin: (allow-none): #GstPlugin to register the element with, or %NULL for * a static element. * @name: name of elements of this type * @rank: rank of element (higher rank means more importance when autoplugging) * @type: GType of element to register * * Create a new elementfactory capable of instantiating objects of the * @type and add the factory to @plugin. * * Returns: %TRUE, if the registering succeeded, %FALSE on error */
gboolean
gst_element_register (GstPlugin * plugin, const gchar * name, guint rank, GType type);
从gst_element_register()的函数声明咱们能够了解到,经过该函数,能够建立一个名称为name、优先级为rank的type类型elementfactory,并将elementfactory添加到registry。在gstreamer中,每一个plugin都应对应的名称,也都会有相应的信息说明它具有什么功能,可是颇有可能在gstreamer中具有相同功能的plugin有多个呢,这个时候它又如何知道,该选那个了呢。这时就是rank起做用了,每一个plugin都具有相应的功能,同时在注册plugin的时候也都会经过rank说明plugin的优先级,在遇到相同功能的plugin,gstreamer会优先选择rank数值大的plugin,rank在gstreamer系统中,默认的plugin rank最大值也就是GST_RANK_PRIMARY(256),因此当你本身编写一个插件,但愿gstreamer在使用你插件所具有的功能,在注册plugin的时候,将rank的值设置为比GST_RANK_PRIMARY大便可,这样就将会优先选择你的plugin。
在上一节 gstreamer学习笔记—Gobject类对象,咱们说到过,在系统第一次调用某类型的type_name##_get_type()函数时,将会向gobject系统注册该种新的类型,那么,类型的第一次调用,是在何时呢,就是在plugin_init()函数中,将type_name##_get_type()做为函数参数type传递给gst_element_register()函数,从而完成类型向gobject系统的注册,只不过大多数的时候,type_name##_get_type()函数都被封装为一个宏定义GST_TYPE_xxx。
下面咱们继续来分析一下gst_element_register()函数都干了什么。gst_element_register()代码有点多,就没有直接贴代码,下面经过流程图介绍一下它的工做与步骤。

经过阅读代码可知传进的plugin指针,只是经过它获取到plugin的一些描述信息,并将factory与plugin创建弱链接。而factory与gstreamer的联系,真正的都是在全局指针变量_gst_registry_default中。通常的,padtemplates信息是在plugin的类初始化函数xxx_class_init()经过gst_element_class_add_static_pad_template()函数添加到plugin。
而在gst_element_register()函数最后,调用了gst_registry_add_feature()函数,该函数又是如何操做的,函数声明以下:
/** * gst_registry_add_feature: * @registry: the registry to add the plugin to * @feature: (transfer floating): the feature to add * * Add the feature to the registry. The feature-added signal will be emitted. * * @feature's reference count will be incremented, and any floating * reference will be removed (see gst_object_ref_sink()) * * Returns: %TRUE on success. * * MT safe. */
gboolean
gst_registry_add_feature (GstRegistry * registry, GstPluginFeature * feature)在gst_registry_add_feature()函数中,很简单,就是将传进来的feature保存到全局变量的_gst_registry_default的priv->features成员中,同时根据feature的name生成相应的hash保存(在查找feature时就是根据name的hash快速查找),这里的feature,就是element支持的caps,将会说明element所支持的功能。最后会经过 g_signal_emit (registry, gst_registry_signals[FEATURE_ADDED], 0, feature) 函数发送一个添加了feature的信号,这个信息是谁来进行接收呢,我们再继续看代码。
2、plugin_init()函数调用
从以上代码,我们知道,gstreamer的plugin注册是经过在plugin_init()函数中调用gst_element_register()函数完成的,那么plugin_init()函数又是何时调用呢?
实际上每一个plugin_init()函数定义都是个静态函数,也就证实该函数只是在本文件起做用,同时,在同一个C文件中有经过GST_PLUGIN_DEFINE这样的一个宏定义对plugin_init()进行修饰,而GST_PLUGIN_DEFINE这个宏是干吗的呢,来看一下它的定义。
GST_PLUGIN_DEFINE详细定义在gstplugin.h,将其实现展开以下:
#define GST_PLUGIN_DEFINE(major,minor,name,description,init,version,license,package,origin)
/* 文本中的name都将使用GST_PLUGIN_DEFINE传进的name替换 */
GST_PLUGIN_EXPORT const GstPluginDesc * gst_plugin_name_get_desc (void);
GST_PLUGIN_EXPORT void gst_plugin_name_register (void);
static const GstPluginDesc gst_plugin_desc = {
major,
minor,
G_STRINGIFY(name),
(gchar *) description,
init,
version,
license,
PACKAGE,
package,
origin,
__GST_PACKAGE_RELEASE_DATETIME,
GST_PADDING_INIT
};
const GstPluginDesc *
gst_plugin_name_get_desc (void)
{
return &gst_plugin_desc;
}
void
gst_plugin_name_register (void)
{
gst_plugin_register_static (major, minor, G_STRINGIFY(name),
description, init, version, license,
PACKAGE, package, origin);
}从GST_PLUGIN_DEFINE的展开咱们能够看到,其实它就是定义了两个函数,分别是:
GST_PLUGIN_EXPORT const GstPluginDesc * gst_plugin_name_get_desc (void);
GST_PLUGIN_EXPORT void gst_plugin_name_register (void);gst_plugin_name_get_desc()函数将会返回一个描述plugin的结构体,里面包含了一系列plugin详细信息,主要有plugin编译的gstreamer-core版本号、插件名称、描述信息、传进来做为初始化函数的plugin_init()函数、版本号、许可证等。
gst_plugin_name_register()函数则是将经过gst_plugin_register_static()函数完成plugin的静态注册登记。gst_plugin_register_static()函数主体实现以下:
gboolean
gst_plugin_register_static (gint major_version, gint minor_version,
const gchar * name, const gchar * description, GstPluginInitFunc init_func,
const gchar * version, const gchar * license, const gchar * source,
const gchar * package, const gchar * origin)
{
GstPluginDesc desc = { major_version, minor_version, name, description,
init_func, version, license, source, package, origin, NULL,
};
GstPlugin *plugin;
gboolean res = FALSE; /* 一系列的输入参数检查以及gstreamer core初始化检查 */
GST_LOG ("attempting to load static plugin \"%s\" now...", name);
plugin = g_object_new (GST_TYPE_PLUGIN, NULL);
if (gst_plugin_register_func (plugin, &desc, NULL) != NULL) {
GST_INFO ("registered static plugin \"%s\"", name);
res = gst_registry_add_plugin (gst_registry_get (), plugin);
GST_INFO ("added static plugin \"%s\", result: %d", name, res);
}
return res;
}在该函数中,将会先经过gst_plugin_register_func()函数进行plugin的注册,gst_plugin_register_func()将会检查版本之类的信息,而后拷贝关于plugin的描述信息,在经过函数指针desc->plugin_init真正的完成plugin的注册(desc->plugin_init函数指针指向的就是上述说到的plugin_init()函数),这样,就调用到了plugin_init()。接着,在gst_plugin_register_static()函数中还调用了gst_registry_add_plugin()函数,gst_registry_add_plugin()函数与gst_registry_add_feature()函数相似,gst_registry_add_feature()是将feature保存到全局变量的_gst_registry_default的priv->features成员,而gst_registry_add_plugin()函数则是将plugin保存到_gst_registry_default的priv->plugins成员。
经过分析宏定义GST_PLUGIN_DEFINE以后,可以知道plugin_init()函数是被gst_plugin_name_register()函数调用的,那么gst_plugin_name_register()函数,又将会是被谁调用呢,我们接着往下看。
在GST_PLUGIN_DEFINE的展开咱们能够看到,gst_plugin_name_register()函数是被GST_PLUGIN_EXPORT修饰的,可是,经过查看gstconfig.h中GST_PLUGIN_EXPORT的解析咱们能够知道,这个宏只是在Windows导出符号给DLL,其余平台更多的是为空操做。在gstreamer编程中,都会在main()函数首先调用gst_init(NULL, NULL)函数,插件的注册登记是否会和它有关呢,看看它的实现。通过阅读代码,我们可以看到gst_init()函数的与插件注册相关的函数调用关系如下:

在1处,gst_init_get_option_group() 函数中,会设置post_parse_func函数指针指向 init_post(),这个最终对在2处进行调用。而在 init_post() 函数,会先准备gstreamer-core环境,检查当前环境有没有库更新,如果有,将会执行更新操作,这部分就是在3处进行的。在4ensure_current_registry() 函数中,将会检查一系列的环境变量,确认搜索库的路径,然后会在5gst_registry_scan_path_level() 进行循环,搜索相应的库进行加载,最后是在 gst_registry_scan_plugin_file() 中,通过 _priv_gst_plugin_loader_funcs.load 成员函数plugin_loader_load()进行plugin加载。
gstreamer plugin加载方式——协助者
所谓的plugin加载,并不是什么动态库加载,动态库的加载过程,在程序一开始运行的时候就已经加载了,我们所说的plugin注册过程(或者说加载过程),只是从机器端的相应路径搜索gstreamer的plugin库,同时将相应的plugin信息保存下来,方便后续在查找使用的时候,可以加快这一个过程。大家都知道,什么搜索啊,查找啊,都是比较耗时的,所以gstreamer将plugin的库都放到同一个路径下,比如/usr/lib/gstreamer-1.0,而一些并没有plugin信息的库,则是和平常的库没有什么区别,放到/usr/lib/目录下,通过这样的方式,减少搜索的范围。同时,gstreamer还通过创建子进程的方式,让子进程来帮忙获取plugin的信息。详细是怎么做的呢,接下来我们看看 plugin_loader_load() 函数是如何获取plugin相关信息。plugin_loader_load()声明如下:
static gboolean
plugin_loader_load (GstPluginLoader * loader, const gchar * filename,
off_t file_size, time_t file_mtime)
函数也就四个参数,一个是loader,将会通过它保存plugin的信息,filename则是需要检查的库路径,file_size则是这个库的大小,file_mtime则是库的更新时间。通过函数声明的参数来看,通过库的路径获取库,从而获取plugin信息,同时保存库的大小以及最后的修改时间,以确定是否需要更新。在plugin_loader_load()函数中,将会通过gst_plugin_loader_spawn (loader)函数创建子进程,该函数的具体实现在这里就不再述说了,它的主要流程大概如下:
通过检查环境变量GST_PLUGIN_SCANNER_1_0以及GST_PLUGIN_SCANNER,检测是否有指定的gst-plugin-scanner;
如果没有,将会检查,编译的时候Makefile有没有传进GST_PLUGIN_SCANNER_INSTALLED;
一般的,会通过Makefile获取到scanner为/usr/lib/gstreamer-1.0/gst-plugin-scanner;
然后,通过fork()创建子进程运行gst-plugin-scanner,父进程将从plugin的库路径搜索库,并检查有效性,而子进程则是负责从相应的库中获取plugin信息并反馈;
可能有人会问,那么是通过创建一个子进程帮忙获取plugin信息的,那么他们父子进程是怎么通信的呢,这个正是通过linux系统的管道进行通信的,详细的创建过程,有兴趣可以看看代码的实现。
gstreamer plugin加载过程——起端(父进程)
父进程,也就是我们的主体运行程序,在找到相应的库的时候,将会通过下面的那种形式,将库的相关信息通过管道发送到子进程gst-plugin-scanner。
entry = g_slice_new (PendingPluginEntry);
entry->tag = loader->next_tag++;
entry->filename = g_strdup (filename);
entry->file_size = file_size;
entry->file_mtime = file_mtime;
loader->pending_plugins_tail = g_list_append (loader->pending_plugins_tail, entry);
if (loader->pending_plugins == NULL)
loader->pending_plugins = loader->pending_plugins_tail;
else
loader->pending_plugins_tail = g_list_next (loader->pending_plugins_tail);
len = strlen (filename);
put_packet (loader, PACKET_LOAD_PLUGIN, entry->tag, (guint8 *) filename, len + 1);
通过上面的代码,我们可以清晰的看到,在调用put_packet()函数将消息push到管道之前,会有个tag标记现在是第几个plugin,以及文件的路径、大小、更新时间等信息,同时会通过一个链表保存这些信息,最后发送一个PACKET_LOAD_PLUGIN消息到管道,接下来,就是子进程gst-plugin-scanner表演的时候了。
gstreamer plugin加载过程——解决(子进程)
上述已经说了,在父进程通过管道将PACKET_LOAD_PLUGIN类型的消息到子进程gst-plugin-scanner,那么子进程又将会进行什么操作呢,下面我们来看看。gst-plugin-scanner的源码是gstreamer-1.xx.0/libs/gst/helpers目录下的gst-plugin-scanner.c:
int
main (int argc, char *argv[])
{
gboolean res;
char **my_argv;
int my_argc;
/* We may or may not have an executable path */
if (argc != 2 && argc != 3)
return 1;
if (strcmp (argv[1], "-l"))
return 1;
my_argc = 2;
my_argv = g_malloc (my_argc * sizeof (char *));
my_argv[0] = argv[0];
my_argv[1] = (char *) "--gst-disable-registry-update";
#ifndef GST_DISABLE_REGISTRY
_gst_disable_registry_cache = TRUE;
#endif
if (argc == 3)
_gst_executable_path = g_strdup (argv[2]);
res = gst_init_check (&my_argc, &my_argv, NULL);
g_free (my_argv);
if (!res)
return 1;
/* Create registry scanner listener and run */
if (!_gst_plugin_loader_client_run ())
return 1;
return 0;
}
通过上面可以看到,就是通过检查运行时候的输入参数,参数一般为-l + 库的查找根路径,而后在_gst_plugin_loader_client_run ()中循环等待父进程发过来的消息并处理。而在_gst_plugin_loader_client_run ()函数中设置好管道的接收和发送端口之后,就是通过以下循环进行接收处理操作的。
/* Loop, listening for incoming packets on the fd and writing responses */
while (!l->rx_done && exchange_packets (l));
而在exchange_packets()中,将会检查,是否有数据可以进行接收或者发送,如果有,将会进行相应的处理。现在,我们假设,父进程已经发送PACKET_LOAD_PLUGIN类型的数据过来,接下来,子进程gst-plugin-scanner将会在exchange_packets()函数中,见过一系列的检查之后,通过read_one()函数进行处理。而在read_one()函数中,按照协商好的协议,读取packet,再通过以下函数处理:
return handle_rx_packet (l, l->rx_buf[0], tag,
l->rx_buf + HEADER_SIZE, packet_len);
而在handle_rx_packet()函数中,根据pack_type的类型,即l->rx_buf[0]的值进行相应的操作,比如加载plugin是PACKET_LOAD_PLUGIN,将会调用do_plugin_load()函数进行处理。
case PACKET_LOAD_PLUGIN:{
if (!l->is_child)
return TRUE;
/* Payload is the filename to load */
res = do_plugin_load (l, (gchar *) payload, tag);
break;
}
在do_plugin_load()函数中,先通过gst_plugin_load_file()加载plugin并将相应信息反馈父进程。
static gboolean
do_plugin_load (GstPluginLoader * l, const gchar * filename, guint tag)
{
GstPlugin *newplugin;
GList *chunks = NULL;
GST_DEBUG ("Plugin scanner loading file %s. tag %u", filename, tag);
/* 通过库的文件路径,搜索库并得到相应的plugin数据 */
newplugin = gst_plugin_load_file ((gchar *) filename, NULL);
if (newplugin) {
guint hdr_pos;
guint offset;
/* 将plugin信息保存到chunks */
/* Now serialise the plugin details and send */
if (!_priv_gst_registry_chunks_save_plugin (&chunks,gst_registry_get (), newplugin))
goto fail;
/* Store where the header is, write an empty one, then write
* all the payload chunks, then fix up the header size */
hdr_pos = l->tx_buf_write;
offset = HEADER_SIZE;
/* 发送PACKET_PLUGIN_DETAILS类型消息 */
put_packet (l, PACKET_PLUGIN_DETAILS, tag, NULL, 0);
if (chunks) {
GList *walk;
for (walk = chunks; walk; walk = g_list_next (walk)) {
GstRegistryChunk *cur = walk->data;
/* 发送external deps、plugin features、element desc等信息 */
put_chunk (l, cur, &offset);
_priv_gst_registry_chunk_free (cur);
}
g_list_free (chunks);
/* Store the size of the written payload */
GST_WRITE_UINT32_BE (l->tx_buf + hdr_pos + 4, offset - HEADER_SIZE);
}
gst_object_unref (newplugin);
} else {
put_packet (l, PACKET_PLUGIN_DETAILS, tag, NULL, 0);
}
return TRUE;
}
不知道大家有没有发现,在_priv_gst_plugin_load_file_for_registry()函数中,先后调用了gst_plugin_register_func()和gst_registry_add_plugin()函数,这两个函数,不正是第二节说到的宏定义GST_PLUGIN_DEFINE实现展开中gst_plugin_name_register()函数中所调用的gst_plugin_register_static()函数中的具体实现吗?原来,在相应的plugin注册函数中的相应函数,只是使用了gst_plugin_name_get_desc()函数,而gst_plugin_name_register()函数则是没有调用,而是在子进程进行插件注册的时候,直接调用了更底层的函数动态完成plugin注册的这一过程。
通过gst_plugin_register_func()函数进行plugin的注册,gst_plugin_register_func()将会检查版本之类的信息,然后拷贝关于plugin的描述信息,在通过函数指针desc->plugin_init真正的完成plugin的注册(desc->plugin_init函数指针指向的就是上述说到的plugin_init()函数),这样,就调用到了plugin_init()。gst_registry_add_plugin()函数与gst_registry_add_feature()函数类似,gst_registry_add_feature()是将feature保存到全局变量的_gst_registry_default的priv->features成员,而gst_registry_add_plugin()函数则是将plugin保存到_gst_registry_default的priv->plugins成员。
我们再回到do_plugin_load()函数,在通过gst_plugin_load_file()函数获取库中的plugin信息之后,子进程将会把相应的信息通过管道发送给父进程,至此,子进程的协助工作就相当于是完成了一次,下面我们将切换回父进程,看看父进程又是如何处理这些信息的。
gstreamer plugin加载过程——保存完成(父进程)
前面已经说到,当父进程在指定的路径下搜索到相应的库时,将相应的信息发给子进程之后,将会运行到以下程序:
if (!exchange_packets (loader)) {
if (!plugin_loader_replay_pending (loader))
return FALSE;
}
父进程发送完消息之后,也将会通过exchange_packets()函数处理消息,管道消息的真正发送也是在这里的,前面的put_packet()函数只是封装消息packet。当子进程完成plugin信息的注册时将会发送PACKET_PLUGIN_DETAILS类型的消息,下面我们来看看,父进程是如何处理该消息的。
case PACKET_PLUGIN_DETAILS:{
gchar *tmp = (gchar *) payload;
PendingPluginEntry *entry = NULL;
GList *cur;
/* 在我们发送PACKET_LOAD_PLUGIN消息给子进程的时候,
* 我们就已经将plugin的一些信息保存到l->pending_plugins,
* 所以现在我们得到反馈,也应该检查一下,是哪个plugin */
cur = l->pending_plugins;
while (cur) {
PendingPluginEntry *e = (PendingPluginEntry *) (cur->data);
if (e->tag > tag)
break;
if (e->tag == tag) {
entry = e;
break;
} else {
cur = g_list_delete_link (cur, cur);
g_free (e->filename);
g_slice_free (PendingPluginEntry, e);
}
}
l->pending_plugins = cur;
if (cur == NULL)
l->pending_plugins_tail = NULL;
if (payload_len > 0) {
GstPlugin *newplugin = NULL;
/* 通过_priv_gst_registry_chunks_load_plugin()函数解析数据包 */
if (!_priv_gst_registry_chunks_load_plugin (l->registry, &tmp,
tmp + payload_len, &newplugin)) {
return FALSE;
}
GST_OBJECT_FLAG_UNSET (newplugin, GST_PLUGIN_FLAG_CACHED);
newplugin->registered = TRUE;
/* We got a set of plugin details - remember it for later */
l->got_plugin_details = TRUE;
} else if (entry != NULL) {
/* Create a blacklist entry for this file to prevent scanning every time */
plugin_loader_create_blacklist_plugin (l, entry);
l->got_plugin_details = TRUE;
}
/* Remove the plugin entry we just loaded */
cur = l->pending_plugins;
if (cur != NULL)
cur = g_list_delete_link (cur, cur);
l->pending_plugins = cur;
if (cur == NULL)
l->pending_plugins_tail = NULL;
break;
}
从上面我们可以看到,具体的数据包解析是在_priv_gst_registry_chunks_load_plugin()函数中,需要注意的,该函数的第一个参数,传进的是l->registry,实质上就是通过gst_registry_get ()函数获取到的静态全局变量指针_gst_registry_default,所以显然,plugin的信息是保存到_gst_registry_default指向的数据区域。
在_priv_gst_registry_chunks_load_plugin()函数中,除了保存plugin的更新时间、文件大小、desc信息以及将plugin添加到_gst_registry_defaultwait,还会将plugin features以及相应的外部依赖保存到_gst_registry_defaultwait。至此,plugin注册完成,相应信息保存到全局变量_gst_registry_default中,可通过gst_registry_get()函数获取该指针。
就是这样,通过两个进程的协作,完成plugin库的相关信息搜索、保存,当plugin都查找完之后,将会给子进程发送退出信号,至此,子进程功成身退。
四、总结
前面说到的,在gst_registry_add_feature()函数中,最后会通过g_signal_emit (registry, gst_registry_signals[FEATURE_ADDED], 0, feature)发出信号,这个信号最后是谁接收的,我们现在还不知道,后续学习过程中,如果发现这个点,再回头进程论述。
在plugin注册的过程,我们还有很多是没有提到的,比如,上面提到的,会保存plugin的外部依赖等信息,是在plugin_init()函数中,通过gst_plugin_add_dependency()函数设置的,通过设置该函数,可以使plugin随硬件的变换而具备不同的属性。同时,通过保存plugin的库更新时间以及大小,是为了下一次进行plugin库扫描的时候,可以将没有修改的库舍弃,减少扫描时间。以及,我们上面所述说的,plugin的信息都是保存到静态全局变量_gst_registry_default中,如果说,我们的程序退出了相应的信息也就没有了,但是大家都会发现,我们只有第一次运行gstreamer的时候,启动速度很慢,接下来的启动都是以比较快的速度启动gstreamer的,原因就在于,在扫描完plugin库的信息之后,会将_gst_registry_default保存的数据信息通过priv_gst_registry_binary_write_cache()函数保存为二进制文件,在下一次启动gstreamer的时候,可以直接读取文件获得plugin的信息而不用扫描。
gst_plugin_name_get_desc(),然后在gstreamer启动过程,通过获取库的gst_plugin_name_get_desc()函数取得plugin信息,这样gstreamer core就知道,它自己具备什么plugin,可以实现什么功能,在需要的时候就可以进行调用。
边栏推荐
猜你喜欢

How to connect DBeaver TDengine?

苏州大学:从 PostgreSQL 到 TDengine

方正璞华“劳动人事法律自助咨询服务平台”在武汉武昌区投入使用!

"Second Uncle" is popular, do you know the basic elements of "exploding" short videos from the media?

苹果,与Web3 “八字不合”

Differences and concepts between software testing and hardware testing

面试SQL语句,学会这些就够了!!!

【C语言】手撕循环结构 —— while语句

鲲鹏devkit & boostkit

二分查找 && 树
随机推荐
基于深度学习的图像检索方法!
关于Google词向量模型(googlenews-vectors-negative300.bin)的导入问题
保姆级教程:写出自己的移动应用和小程序(篇三)
百日刷题计划 ———— DAY1
移动端适配,华为浏览器底色无法正常显示
Get out of the machine learning world forever!
【typescript】使用antd中RangePicker组件实现时间限制 当前时间的前一年(365天)
A number of embassies and consulates abroad have issued reminders about travel to China, personal and property safety
电脑死机,Word忘了保存怎么办?怎么恢复?(编辑器是WPS)
stack && queue
els long block deformation conditions, boundary collision judgment
【C语言】剖析函数递归(1)
RISC-V instruction format and 6 basic integer instructions
requestparam注解接的收的是什么格式(玄机赋注解)
k8s之KubeSphere部署有状态数据库中间件服务 mysql、redis、mongo
供应磷脂-聚乙二醇-羧基,DSPE-PEG-COOH,DSPE-PEG-Acid,MW:5000
【C语言】函数哪些事儿,你真的get到了吗?(1)
鲁大师7月新机性能/流畅榜:性能跑分突破123万!
劲爆!阿里巴巴面试参考指南(嵩山版)开源分享,程序员面试必刷
删除链表的节点