Hi3861的SAMGR--系统服务框架子系统-2

系统
文章由鸿蒙社区产出,想要了解更多内容请前往:51CTO和华为官方战略合作共建的鸿蒙技术社区https://harmonyos.51cto.com

[[404768]]

想了解更多内容,请访问:

51CTO和华为官方合作共建的鸿蒙技术社区

https://harmonyos.51cto.com

 接前文《Hi3861的SAMGR--系统服务框架子系统-1

4 结构体的分解

4.1 先上samgr的展开图【附件有原图】

4.2 Samgr:SamgrLiteImpl g_samgrImpl

  1.      typedef struct SamgrLiteImpl SamgrLiteImpl; 
  2.         struct SamgrLiteImpl  { 
  3. A:          SamgrLite vtbl;       //SAMGR_GetInstance() return this SamgrLite Instance 
  4. B:          MutexId mutex; 
  5. C:          BootStatus status;    //see BootStatus 
  6. D:          Vector services;      //a vector to record all services 
  7. E:          TaskPool *sharedPool[MAX_POOL_NUM]; 
  8.         }; 

 注册第一个服务的时候,首先要通过SAMGR_GetInstance()从全局变量g_samgrImpl获取一个samgr的实例对象,g_samgrImpl已经初始化过了的话,可以直接返回samgr对象的引用,g_samgrImpl没有初始化的话,就需要通过Init函数进行初始化相关的配置之后,才能返回samgr对象的引用。

以后的各种service/feature的操作,基本是都是需要通过g_samgrImpl进行管理的。下面把这个全局变量展开来看一下:

A: SamgrLite vtbl;

vtbl 就是SamgrLite 的实例,SAMGR_GetInstance() 时,返回的就是指向它的指针。SamgrLite结构体(或者直接说类)定义了四组十个函数指针,如下图,同一组用同一个颜色框起来:

在g_samgrImpl的初始化(init)时与对应的实现函数对应关联起来,之后就可以通过这四组函数来对service/ feature进行注册、注销、获取API等操作了。

而具体的service、feature在各自INIT时,会通过g_samgrImpl的samgr实例调用这里的Register接口,向g_samgrImpl注册自己和API,把自己纳入samgr的管理体系中,然后就是samgr为service、feature创建queue/taskpool/task等运行环境了。

B: MutexId mutex;

互斥锁。关键代码段的操作要加锁,保证多线程下对共享数据的操作的完整性。

在g_samgrImpl的初始化(init)时就会通过MUTEX_InitValue() 生成互斥锁,MUTEX_InitValue()声明在thread_adapter.h,它的实现则取决于平台使用的内核,M核平台用cmsis接口来实现,A核/Linux平台,则使用posix接口来实现。

类似的还有内存、队列、线程、timer的相关操作,代码见:

  1. Hi3861/foundation/distributedschedule/samgr_lite/samgr/adapter/ 

理论上,全局对象g_samgrImpl初始化(init)时就会生成一个mutex,且只要g_samgrImpl没有重新初始化,这个mutex就应该是不变的,Hi3861平台上我看到的也确实如此。

但是Hi3516平台上我看到了一个奇怪的地方,见前文《鸿蒙系统框架层的启动细节》的附件log,搜索关键字“SAMGR_GetInstance|StartServiceByName|Invoke: msg”可以看到:

有若干处mutex是为NULL的,需要重新初始化g_samgrImpl,这样岂不是会把之前注册的service Vector/TaskPool等清除掉吗?先存疑,待进一步仔细阅读Hi3516的代码后再确认。

C: BootStatus status;

系统的启动阶段状态标记。

Hi3861平台上电启动到 system_init.c 的

  1. void HOS_SystemInit(void) 
  2.     ...... 
  3.     SYS_INIT(service); 
  4.     SYS_INIT(feature); 
  5.     SAMGR_Bootstrap(); 

 这一步时,通过SYS_INIT() 启动的都是用SYS_SERVICE_INIT()和SYS_FEATURE_INIT() 标记的service和feature,而通过SYSEX_SERVICE_INIT/APP_SERVICE_INIT/ SYSEX_FEATURE_INIT/APP_FEATURE_INIT 标记的service和feature,则会在系统启动到 BOOT_APP 这一步时,由Bootstrap service来启动,见 bootstrap_service.c 的MessageHandle()函数内的 INIT_APP_CALL() 的调用:

系统启动状态标记到 BOOT_DYNAMIC 这一步时,意味着系统所有的应用服务也启动完毕了,进入了一个比较稳定的状态,至于BOOT_DYNAMIC状态还会做其他什么事情,我暂时还没有更多的了解。

为了理解例程代码如何工作,我在这里加了 BOOT_DEBUG和BOOT_DEBUG_WAIT两个状态,用来调用例程里的INIT_TEST_CALL(),跑相关的测试流程,通过相关的log来验证自己的分析,详见附件的log。

D: Vector services;

g_samgrImpl 初始化时:

  1. g_samgrImpl.services = VECTOR_Make((VECTOR_Key)GetServiceName, (VECTOR_Compare)strcmp); 

创建了一个Vector:{0, 0, 0, NULL, key, compare},Vector的定义如下:

其中:

  • max:表示下面的**data指针数组的大小,也就是data[max]所能存储的最大指针数目;
  • top: 当前使用到了的指针数组的最高位置data[top],当top==max,同时free为0时,表示data[max]已经装满了,需要扩容,一次扩容增加4个element位置,变成data[max+4],详情见VECTOR_Add()函数的实现。
  • free:当前0~top之间,释放了的位置的数量。当已注册在册的service unregister时,对应记录这个service的data[i]会被清空,free+1;下次有新的service注册时,会优先使用data[i]来记录新service的Impl对象指针,free-1。详情见VECTOR_Swap()函数的实现
  • **data:指针数组data[max],每一个data[i]记录了一个注册进来的service对应的ServiceImpl 对象的指针,初始化为NULL,通过VECTOR_Add()函数内的操作扩容。
  • key:是samgr_lite.c定义的GetServiceName(ServiceImpl*)函数指针,它可以通过参数来获取对应的service Name。
  • compare:是strcmp函数指针,也就是string标准库提供的字符串比较函数。

Samgr通过这个Vector来管理所有注册进来的service(实际上管理的是serviceImpl对象,通过serviceImpl来关联具体的service和service对应的feature)。

每个service可以有0个/1个或多个feature,每个service(serviceImpl对象)也是通过自己的Vector来管理feature。

对Vector的操作,全部定义在:

Hi3861/foundation/distributedschedule/services/samgr_lite/samgr/source/common.c

里面了,需要仔细阅读分析,去理解相关操作。

下面是几处要点:

  • VECTOR_Make() 会创建一个{0, 0, 0, NULL, key, compare} Vector,当需要往Vector中添加element时,会在VECTOR_Add()中扩容。
  • VECTOR_Add(Vector *vector, void *element) 的时候,发现Vector的data[max] 空间用尽了,就会重新申请一块增大了4个element的内存空间Newdata[max+4],把旧的data[x]全部拷贝进去(还有4个新的空余的element位置),g_samgrImpl.services.data重新指向Newdata,再把旧的data[x]占用的空间释放掉,这样,新的element (ServiceImpl 对象的指针)就可以记录进来了。
  • VECTOR_Swap()用来删除一个已经记录在案的element(也就是unregister一个service),把它对应的 data[x] 置为NULL,free+1,空出的位置会给未来新注册的service 优先使用。
  • VECTOR_Find()/VECTOR_FindByKey() 可以查找element(serviceImpl),并返回它的index。

特别是VECTOR_FindByKey(Vector *vector, const void *key),第二个参数“void *key”实际上是一个service的名字字符串,如“Broadcast”。

FindByKey会循环获取data[0~top]的ServiceImpl 对象的指针,并将其作为key函数指针(GetServiceName())的参数来获取service的Name,将Name其与上面的第二个参数的service Name,用compare函数指针(strcmp)进行对比,匹配则返回对应的data[i]的 index i。

E: TaskPool *sharedPool[MAX_POOL_NUM]; // MAX_POOL_NUM 是8

Hi3861平台启动到 system_init.c 的最后一步时:

  1. void HOS_SystemInit(void) 
  2.     ...... 
  3.     SAMGR_Bootstrap();   

 调用SAMGR_Bootstrap()去启动已经注册了的service,会为service创建queue和taskPool资源,每个service有一个GetTaskConfig()接口,可以返回这个service运行起来的task配置参数,如 bootstrap service的TaskConfig为:

请自行查看 TaskConfig的定义。

这里需要关注的是 SHARED_TASK 这个标记,在samgr的 AddTaskPool()这一步时:

bootstrap service的TaskConfig配置会被修改成默认的DEFAULT_TASK_CFG,意味着在bootstrap_service.c 中定义的TaskConfig 参数(stackSize和queueSize)其实并没有起作用,想要修改SHARED_TASK的stackSize和queueSize,要去修改DEFAULT_TASK_CFG的配置。

而上面case SHARED_TASK的操作以及g_samgrImpl.sharedPool[]的存在,可以为若干个共同标记了SHARED_TASK的service共享一个Queue和taskPool资源(这样可以节约好多资源),TaskEntry在Queue中收到msg时,可以通过Exchange 消息里面的Identity字段解析出Sid/Fid/Qid信息,以此来确认到底是哪个service/feature需要处理这个消息。

TaskConfig里的priority字段,则确定了service用的是哪个sharePool[x],优先级越高,x越大。

我在代码里全局搜索了一下“SHARED_TASK”关键字,得到如下结果:

主要是在samgr例程里,不同优先级别的service,共享着不同x的sharedPool[x]资源。

不是SHARED_TASK的service则有自己独立的Queue和taskPool,不会直接记录在g_samgrImpl.sharedPool[]里,而是记录在各自service的serviceImpl对象的taskPool* 里。

4.3 ServiceImpl 类

  1.     struct ServiceImpl { 
  2. A:      Service*      service; 
  3. B:      IUnknown* defaultApi; 
  4. C:      TaskPool*   taskPool; 
  5. D:      Vector    features; 
  6. E:      int16      serviceId; 
  7. F:      uint8      inited; 
  8. G:      Operations ops; 
  9.     }; 

 g_samgrImpl 全局变量的services Vector里,只直接记录ServiceImpl对象的指针,并不直接记录和管理service、feature对象本身。service在向samgr注册自己时,samgr会首先生成一个ServiceImpl对象,将service对象的指针记录在ServiceImpl的Service* service里,而ServiceImpl对象本身的指针,则记录在g_samgrImpl.services.data[i] 中,这样就建立了g_samgrImpl 到具体service的联系,见本文上面的展开图。

A: Service* service;    

指向当前ServiceImpl对象所对应的具体的service对象,这些具体的service对象都是Service类的子类对象。

B: IUnknown* defaultApi;

继承了IUnknown接口(INHERIT_IUNKNOWN)的service或者feature,都会有IUnknown的一组三个默认的接口,这组接口主要是记录service/feature对象的引用数量,还可以通过其中的QueryInterface接口实现父类(IUnknown)指针到具体的service/feature子类对象指针的类型转换,以获取子类提供的功能。

详情见下面对IUnknown的分析。

我在《鸿蒙的DFX子系统 》一文中,有对hiview service做过一个展开,可以去那里了解一下。

C: TaskPool* taskPool;

这就是上一小节中,samgr为service创建的taskPool的指针(同时创建的还有消息队列,queueId同时保存在taskPool里面)。

如果service task是SHARED_TASK类型的,那就会有多个service共享一个taskPool和queue,这里的taskPool指针就会指向同一块内存空间,同时,g_samgrImpl的对应优先级别的sharedPool[x]也会记录着这个taskPool指针。

如果service task不是SHARED_TASK类型的,那就只会在这里记录service的taskPool(包括queue),而不会在g_samgrImpl中做记录。

D: Vector features;

feature需要依赖于对应的service才能注册和运行,一个service可以有0个、1个或多个feature。service本身不记录它对应的feature的信息,而是由这个ServiceImpl.features来记录。ServiceImpl 的这个Vector features类似于g_samgrImpl用于记录serviceImpl的Vector services。

一个service没有feature时,features Vector就保持初始化的样子,对应的features.data 是NULL。

一个service有若干个feature时,就会在feature注册时,由samgr生成对应FeatureImpl类对象,将此对象与具体的feature关联起来,再将此对象的指针保存在Vector features.data[x]里,并将这个 x 作为对应feature的ID,另做保存。以后samgr就可以通过它自己的vector找到serviceImpl,再进一步通过这个vector找到对应的featureImpl,从而找到最终的feature。

E: int16 serviceId;

当前的ServiceImpl 对象指针保存在g_samgrImpl.services vector内的data[x]上的这个 x 序号,就作为当前service的ID,保存在这里。这个serviceId也可能同时保存在具体的service对象的Identity 结构体里。

F: uint8 inited;

标记当前ServiceImpl 对应的service的状态,service没有init起来是不能注册feature的,service在处理消息事件的时候,状态也要对应置为 BUSY,处理完消息又要将状态写回IDLE。具体可自行查阅代码。

G: Operations ops;

主要记录了service处理消息事件的时间戳、msg数量(编号?)、步骤和是否存在异常等信息,估计与跨设备的服务/消息处理的同步有关,对此暂未做深入理解,待作进一步的理解。

4.4 FeatureImpl类

  1.   typedef struct FeatureImpl FeatureImpl; 
  2.     struct FeatureImpl { 
  3. A:      Feature  *feature; 
  4. B:      IUnknown *iUnknown; 
  5.     }; 

 FeatureImpl 类看起来相对简单,直接是一个Feature指针(A)指向对应的具体的feature对象。

有些feature除了继承自Feature类之外,还继承了某些interface,为feature提供额外的功能,这些interface 都是继承了最原始的IUnknown接口类(INHERIT_IUNKNOWN)。

这里的IUnknown *iUnknown与上面的ServiceImpl 的IUnknown* defaultApi; 其实是同一个东西,它们都指 向了一个feature或者service所继承/实现的接口中,IUnknown接口所在的位置,见上面ServiceImpl对IUnknown* defaultApi;的说明。

更多详情见下面对IUnknown的分析。

4.5 Service类及其子类

  1. struct Service { 
  2.     const char *(*GetName)(Service *service);                   //获取service名称 
  3.     BOOL (*Initialize)(Service *service, Identity identity);    //service的初始化 
  4.     BOOL (*MessageHandle)(Service *service, Request *request);  //service的消息处理函数 
  5.     TaskConfig (*GetTaskConfig)(Service *service);              //获取service 任务运行的配置 

 Service类是所有service类的父类,它声明了四个函数指针,这是每一个服务都必须要实现的生命周期函数,见上面的注释。

每一个具体的服务类都继承自这个Service类,然后可以扩展自己独特的功能。

下面分别看一下Hi3861默认的三个具体的服务。

A: Bootstrap service

  1. typedef struct Bootstrap { 
  2.     INHERIT_SERVICE;  //继承上面的Sevice类 
  3.     Identity identity;   //bootstrap service对象的id信息 
  4.     uint8 flag; 
  5. } Bootstrap; 

 Bootstrap 除了继承Service之外,还增加了一个Identity identity 和 uint8 flag。

  • Identity identity

这是Bootstrap service具体对象的身份信息,里面包括了: serviceId/featureId/queueId三个信息。

serviceId:Bootstrap service 对应的 serviceImpl对象,在g_samgrImpl.services这个Vector.data[]中存放位置 的index,这里值为0.

featureId:Bootstrap service不带feature,所以值为 -1。

queueId: Bootstrap service启动时,samgr会为其创建消息队列,这就是消息队列的ID,是一串数字。

  • uint8 flag

一个标记,主要是LOAD_FLAG 0x01这一个位,用来标记非系统service/feature是否已经加载和注册,见 bootstrap_service.c的MessageHandle()内对flag的使用。

B. Broadcast service

  1. typedef struct BroadcastService BroadcastService; 
  2. struct BroadcastService { 
  3.     INHERIT_SERVICE; 
  4. }; 

 Broadcast service仅仅直接继承了Service类,确保它的service对象的生命周期的完整,因为它还会有feature,会在具体的feature对象中保存id信息和其它扩展信息,详见下面的PubSubFeature类的解析。

C. Hiview service

  1. typedef struct { 
  2.     INHERIT_IUNKNOWN;     
  3.     void (*Output)(IUnknown *iUnknown, int16 msgId, uint16 type); 
  4. } HiviewInterface; 
  5.  
  6. typedef struct { 
  7.     INHERIT_SERVICE; 
  8.     INHERIT_IUNKNOWNENTRY(HiviewInterface); 
  9.     Identity identity; 
  10. } HiviewService;  

 Hiview service除了继承自Service类实现service的生命周期函数之外,还继承了HiviewInterface,这个HiviewInterface又继承了最原始的IUnknown接口类(INHERIT_IUNKNOWN)。通过这种多继承机制,既实现了服务所需的生命周期,又具备了类似feature所提供的部分接口功能(Hiview service实际上又不带feature)。

详情见下面对IUnknown类的分析。

identity则是Hiview service对象的身份信息,同样包括了: serviceId/featureId/queueId三个信息。

4.6 Feature类及其子类

  1. struct Feature { 
  2.     const char *(*GetName)(Feature *feature);                         //获取feature的名字 
  3.     void (*OnInitialize)(Feature *feature, Service *parent, Identity identity);  //feature 的初始化 
  4.     void (*OnStop)(Feature *feature, Identity identity);                   //停止对外提供feature功能 
  5.     BOOL (*OnMessage)(Feature *feature, Request *request);             //对本feature的消息处理 
  6. }; 

 Feature类是所有feature类的父类,也是声明了四个函数指针,这是每一个feature都必须要实现的生命周期函数,见上面的注释。

每一个具体的feature类都继承自这个Feature类,然后扩展自己独特的功能。

下面是Hi3861的Broadcast service 提供的feature类及Impl类的定义:

A: PubSubFeature g_broadcastFeature

  1. typedef struct PubSubFeature PubSubFeature; 
  2. struct PubSubFeature { 
  3.     INHERIT_FEATURE;   //继承 Feature 类 
  4.     Relation *(*GetRelation)(PubSubFeature *feature, const Topic *topic); 
  5.     MutexId mutex; 
  6.     Relation relations; 
  7.     Identity identity; 
  8. }; 

 PubSubFeature 本尊,它被FeatureImpl 对象以及下面的PubSubImplement 对象引用。

FeatureImpl 对象又会被记录在 ServiceImpl 的Features Vector向量里,获得一个Fid,连同Sid/Qid一起记录在PubSubFeature 的identity里。

PubSubFeature 还提供一个双向链表结构的Relation,以及基于这个双向链表结构的查找节点的函数GetRelation(),这个结构及其作用,我后面再另写文章详细分析。

B: PubSubImplement g_pubSubImplement

  1. typedef struct PubSubInterface PubSubInterface; 
  2. struct PubSubInterface { 
  3.     INHERIT_IUNKNOWN; 
  4.     Subscriber subscriber; 
  5.     Provider provider; 
  6. }; 
  7.  
  8. typedef struct PubSubImplement { 
  9.     INHERIT_IUNKNOWNENTRY(PubSubInterface); 
  10.     PubSubFeature *feature; 
  11. } PubSubImplement; 

 PubSubImplement 对象会引用上面的PubSubFeature对象,记录在这里的PubSubFeature *feature上。

PubSubImplement 还继承了PubSubInterface,实现了Subscriber 和Provider的功能。

它们的关系,见本文最上面的展开图。

这个PubSubFeature 和PubSubImplement 深究下去就有点复杂了,它们是SOA(面向服务的架构)的具体实现:

  • Provider:服务的提供者,为系统提供能力(对外接口)。
  • Consumer:服务的消费者,调用服务提供的功能(对外接口)。
  • Samgr:作为中介者,管理Provider提供的能力,同时帮助Consumer发现Provider的能力。

如下是官方readme上画的架构图。

这里我就先不做进一步详细的分析了,后面会结合broadcast_example.c示例程序来做分析和验证,再单独写一篇文章来做总结。

4.7 IUnknown 接口类及其相关定义

首先需要理解,C语言的struct本质上与C++中的class是一样的,都是一块存储区域,里面有数据和对数据的操作。C++通过“:”关键字来标记继承关系,如Aa继承自A表示为“class Aa : public A”,而C语言直接是struct Aa内嵌套struct A来标记“继承关系”:

  1. struct A { 
  2.     dataA; 
  3.     funcPtr* funcA; 
  4. struct Aa { 
  5.     struct A; 
  6.     dataAa; 
  7.     funcPtr* funcAa; 

 更具体的一些细节,可以自行在网上搜索和学习。

接下来,我们仔细对比一下HivieService 和PubSubImplement,它们都分别通过INHERIT_IUNKNOWNENTRY() 这个关键字来分别继承HiviewInterface和PubSubInterface,而这HiviewInterface和PubSubInterface又都通过INHERIT_IUNKNOWN来继承IUnknown接口类。

下面我们就跟着Hi3861/foundation/distributedschedule/interfaces/innerkits/samgr_lite/samgr/iunknown.h中的定义去展开和理解一下这两个宏(类)。

INHERIT_IUNKNOWN 宏定义是为了方便用C语言的形式“继承”IUnknown接口类。

INHERIT_IUNKNOWNENTRY() 宏定义是为了方便用C语言的形式“继承”【实现了IUnknown接口的】 IUnknownEntry接口类。

这两个宏之间的关系,就是上面struct A和struct Aa之间的父类子类关系,换回struct 的形式,就是:

IUnknown 是父类,IUnknownEntry 是子类,子类是父类的一个implement。

按上面的定义把HiviewService类(或结构体)的定义彻底展开,就是如下的样子:

把HiviewService的全局对象 g_hiviewService的初始化也按上面的形式展开,也会如下:

这样一来,.iUnknown的地址、HiviewService类型、g_hiviewService对象的地址(引用/指针)之间的关系,就可以通过计算和类型转换来互相获取了,这就是下面三个宏的作用:

a. GET_IUNKNOWN(T)

定义在://foundation/distributedschedule/interfaces/innerkits/samgr_lite/samgr/iunknown.h

简单理解为:从g_hiviewService对象中获取其内部的.iUnknown对象的地址。

b. GET_OFFSIZE(T, member)

c. GET_OBJECT(Ptr, T, member)

这两个定义在://foundation/distributedschedule/interfaces/innerkits/samgr_lite/samgr/common.h

简单理解为:从.iUnknown父类对象下转换得到子类HiviewInterface对象或者g_hiviewService对象的地址。

如本文开头的第一张展开图所示,BroadcastImpl和HiviewImpl都有自己的 IUnknown* defaultApi,分别是通过GET_IUNKNOWN(g_pubSubImplement) 和GET_IUNKNOWN(g_hiviewService)来得到的,得到了.iUnknown的地址,也就是得到了(*QueryInterface)/(*AddRef)/(*Release)这三个defaultApi的地址,就可以使用它们了,实际只直接使用(*QueryInterface)这个API,它的作用是“Queries the subclass object of the IUnknown interface of a specified version”

查询/获取指定的版本的IUnknown接口的子类对象(同时增加对该对象的引用次数),调用者通过这个子类对象就可以调用子类中定义的其它接口了。

对于上面的g_hiviewService例子来说,调用QueryInterface接口的例子在hiview_service.c 的HiviewSendMessage() 里,它返回的实际上就是g_hiviewService 这个service对象内部的一个区域的起始地址,这个区域就是HiviewInterface这个类的对象,同时增加了对这个对象的引用次数,之后,就可以通过这个对象来调用HiviewInterface所定义的全部API了(这里主要是Output函数)。

4.8 其它类/结构体

samgr子系统中还有其他的一些很重要的类或结构体,比如Request/Response/Exchange 等等,这里就先不进一步展开了,以后应该还会继续补充完整的,或者在接下来的流程分析中按需进行分解,也请各位自行做一下理解。

想了解更多内容,请访问:

51CTO和华为官方合作共建的鸿蒙技术社区

https://harmonyos.51cto.com

 

责任编辑:jianghua 来源: 鸿蒙社区
相关推荐

2021-06-18 10:02:10

鸿蒙HarmonyOS应用

2021-06-03 14:21:44

鸿蒙HarmonyOS应用

2021-06-18 15:23:59

鸿蒙HarmonyOS应用

2021-07-08 16:16:59

鸿蒙HarmonyOS应用

2021-07-05 09:35:36

鸿蒙HarmonyOS应用

2021-07-07 09:45:20

鸿蒙HarmonyOS应用

2021-07-12 09:50:39

鸿蒙HarmonyOS应用

2022-03-15 15:00:59

Hi3861Pin接口鸿蒙

2020-10-16 09:50:37

Hi3861WiFi热点

2021-04-30 09:43:27

鸿蒙HarmonyOS应用

2023-05-26 16:07:14

Hi3861Wifi模块

2020-11-03 11:39:22

wifi小车

2020-10-14 09:41:02

Hi3861GPIO点灯

2022-03-07 15:05:58

HTTPHi3861数据解析

2020-10-30 09:41:44

鸿蒙Hi3861WiFi小车

2021-07-01 14:21:58

鸿蒙HarmonyOS应用

2022-09-13 15:54:41

samgr鸿蒙

2022-04-12 11:07:11

Hi3861鸿蒙操作系统

2021-02-02 15:52:17

鸿蒙HarmonyOS应用开发

2022-05-30 15:21:27

Hi3861TCP通信
点赞
收藏

51CTO技术栈公众号