AOSP(五)搞懂跨系统通信SOA架构设计
作者:访客发布时间:2023-12-24分类:程序开发学习浏览:101
前言
今天就拿车载来讲,各汽车主机厂家会根据自身的设计理念差异将整车划分成不同的域。博世、大陆等传统 Tier1 划分了5个域:动力域、底盘域、车身域、座舱域和自动驾驶域,也有的(如大众MEB平台、东软睿驰等)把动力域、底盘域和车身域融合为整车控制域,基于安全性和独立性要求,不同的系统之间数据不能直接访问。但是不同域下的不同系统之间,同一域下的不同系统之间的通信需求现实存在。现有的解决方案主要是基于TCP/IP协议栈的Socket(套接字)通讯。
一、SOA架构设计
SOA,或Service-Oriented Architecture(面向服务的架构),是一种软件架构模式,旨在促进系统中不同组件之间的松耦合和重用性。在 SOA 中,软件系统被划分为多个服务,每个服务代表一个具体的业务功能或操作。这些服务通过标准化的接口进行通信,可以独立部署、维护和扩展:
-
服务:服务是 SOA 架构的基本构建块。每个服务代表一个独立的业务功能或操作。服务通常具有清晰的接口,定义了其提供的功能和数据格式。
-
松耦合:SOA 旨在实现松耦合,这意味着系统的不同部分可以独立开发、部署和维护,而不会对其他部分造成影响。这使得系统更加灵活且容易扩展。
-
标准化接口:每个服务都通过标准化的接口提供其功能。这些接口通常是基于开放标准的,如Web服务描述语言(WSDL)或RESTful API。
-
服务注册与发现:在 SOA 中,服务通常需要在注册表或目录中进行注册,以便其他组件能够找到并使用它们。服务发现机制用于在运行时查找服务的位置和接口。
-
重用性:SOA 提倡服务的重用,这意味着相同的服务可以在不同的应用程序或系统中使用。这有助于减少重复开发和提高效率。
-
跨平台通信:SOA 允许不同的系统和应用程序在不同的平台和编程语言中进行通信。这通过使用标准的协议和数据格式来实现。
-
安全性:SOA 架构强调安全性,确保只有经过身份验证和授权的组件才能访问服务。
-
错误处理:SOA 考虑了错误处理和异常处理机制,以确保系统的可靠性和稳定性。
二、Android安全机制
2.1、权限分类
DAC(Discretionary Access Control)和 MAC(Mandatory Access Control)是两种不同的访问控制模型,用于管理操作系统和应用程序对系统资源的访问权限。它们之间的主要区别在于授权的方式和级别:
2.1.1 DAC(Discretionary Access Control)
-
DAC 是一种相对较简单的访问控制模型,它授权的权力由资源的所有者决定。每个资源(如文件、目录、进程)都有一个所有者,所有者可以自行决定谁可以访问这些资源,以及如何访问。
-
在 DAC 模型中,资源的所有者可以分配权限给其他用户或组。这些权限通常包括读取、写入和执行等。其他用户可以请求访问资源,但只有资源的所有者或被赋予适当权限的用户才能获得访问权限。
-
DAC 模型适用于多用户系统,但容易受到滥用和误用。因为权限由资源的所有者决定,所以如果资源的所有者不谨慎,可能会导致系统安全性问题。
2.1.2 MAC(Mandatory Access Control)
-
MAC 是一种更严格的访问控制模型,它不由资源的所有者决定,而是由系统管理员或安全政策规定。在 MAC 模型中,资源的访问权限是强制执行的,不受资源的所有者控制。
-
MAC 模型通常使用标签或安全上下文来标识资源和用户。每个资源和用户都分配有一个唯一的安全标签。然后,系统根据预定义的安全政策来决定哪些标签可以访问哪些资源。
-
MAC 模型适用于需要严格安全控制的环境,如政府、军事和高度敏感的系统。它可以防止滥用权限,但也需要更复杂的配置和管理。
2.2、SEAndroid管理策略(MAC)
Android中使用的MAC机制就是SEAndroid。SELinux(Security-Enhanced Linux) 是美国国家安全局(NSA)在Linux社区的帮助下设计的一个Linux历史上最杰出的安全系统,是一种MAC机制(Mandatory Access Control,强制访问控制)。在这种访问控制体系的限制下,进程只能访问那些在他的任务中所需要文件。 SEAndroid安全机制又称为是基于TE(Type Enforcement)策略的安全机制。在/system/sepolicy目录中,所有以.te为后缀的文件均为策略配置文件。 策略编写流程:
2.2.1 编写.te文件
我们来看看zxx-server进程,因此在/system/sepolicy/private目录下会有一个zxx-server.te作为该进程的策略文件。文件内容如下:
# 声明zxx-server类型,并将domain属性关联到该类型 (进程)
#必须具备mlstrustedsubject属性才具备unix_stream_socket {connectto}
#参考:https://blog.csdn.net/a572423926/article/details/123209225
typeattribute zxx-server, domain,coredomain,mlstrustedsubject;
# 声明zxx-server_exec类型(可执行文件,类型要与file_contexts中的相同)
typeattribute zxx-server_exec, exec_type, file_type, system_file_type;
init_daemon_domain(zxx-server)
#allow语句表示允许的权限。
allow zxx-server self:tcp_socket { read write getattr getopt setopt shutdown create bind connect name_connect };
allow zxx-server self:netlink_route_socket {create};
allow zxx-server fwmarkd_socket:sock_file {write};
allow zxx-server port:tcp_socket {name_connect};
allow zxx-server netd:unix_stream_socket {connectto};
allow zxx-server self:capability {net_raw};
2.2.2 执行文件分配上下文
为文件分配安全上下文:file_contexts 文件列出了 Android 系统中的各种文件以及它们的安全上下文标签。这有助于 SELinux 确定哪些进程(主体)可以访问这些文件以及以何种方式访问,需要我们在olicy/private/file_contexts中声明SEAndroid系统文件的安全上下文:
//在Android.bp编译底下在/system/bin/有个可执行程序
/system/bin/zxx-server u:object_r:name-server_exec:s0
2.2.3 检查缺少权限
运行某个进程,如果该进程不具备对应的权限,则可以在logcat 或者在执行 adb root 后执行 adb shell dmesg查看:
avc: denied { bind } for pid=417 comm="zxx-server" scontext=u:r:name-server:s0 tcontext=u:r:name-server:s0 tclass=tcp_socket permissive=0
avc: denied { connectto } for pid=417 comm="zxx-server" scontext=u:r:name-server:s0 tcontext=u:r:netd:s0 tclass=unix_stream_socket permissive=0
说明 | 案例 |
---|---|
缺少什么权限 | { bind } |
谁缺少权限 | scontext=u:r:name-server:s0 |
对谁缺少权限 | tcontext=u:r:name-server:s0 |
什么类型的权限 | tclass=tcp_socket |
此时就需要在TE文件中声明:
#allow [谁缺少权限] [对谁缺少权限]:[什么类型的权限] [缺少什么权限]
#当sconext与tcontext都是自己时候,可以将 [对谁缺少权限]写为:self
allow zxx-server self:tcp_socket {bind}
allow zxx-server netd:unix_stream_socket {connectto};
三、跨系统通信方案
大众在FDBUS基础上开发通信中间件完成系统间的通信。
FDBus:gitee.com/jeremyczhen…
Fast Distributed Bus,基于Socket(Unix Domain和TCP)的快速分布式总线。
UDS:Unix Domain Socket,专用于IPC。Zygote中的Local Socket即为UDS。
FDBUS为了更方便的进行寻址,允许不通过IP+端口或者固定UDS地址,而是采用域名的方式进行寻址。类似于在浏览器输入www.baidu.com
,与百度完成通信,则需要进行DNS解析,将域名转化为IP地址。
FDBUS同样提供了name-server
,负责管理server名字(域名)与地址(IP)的映射,和DNS服务器一样,完成server名字到IP的解析。
3.1 部署name-server
name--server和Android中的ServiceManager一样,都是负责完成服务的注册与查询。与ServiceManager一样,系统启动第一时间就需要启动该服务进程,以便于其他进程进行服务注册与查询。因此我们需要将name-server部署到init.rc中,让其第一时间随系统而启动。
3.2 name-server编译
将FDBUS放入Android AOSP源码 /frameworks/native/services
中:
不是必须放入该目录,这里以此目录为例。
打开Android.bp文件,将其修改为:
//=====================================================================================
// makefile to build fdbus in aosp source tree |
//=====================================================================================
//=====================================================================================
// build libfdbus.so |
//=====================================================================================
SRC_FILES = [
"fdbus/CBaseClient.cpp",
"fdbus/CFdbBaseObject.cpp",
"fdbus/CFdbMessage.cpp",
"fdbus/CFdbSimpleSerializer.cpp",
"fdbus/CBaseEndpoint.cpp",
"fdbus/CFdbCJsonMsgBuilder.cpp",
"fdbus/CFdbSessionContainer.cpp",
"log/CLogProducer.cpp",
"fdbus/CBaseServer.cpp",
"fdbus/CFdbContext.cpp",
"fdbus/CFdbBaseContext.cpp",
"fdbus/CFdbSession.cpp",
"fdbus/CFdbMsgDispatcher.cpp",
"fdbus/CEventSubscribeHandle.cpp",
"fdbus/CFdbUDPSession.cpp",
"fdbus/CBaseSession.cpp",
"fdbus/CFdbWatchdog.cpp",
"fdbus/CFdbEventRouter.cpp",
"platform/CEventFd_eventfd.cpp",
"platform/linux/CBaseMutexLock.cpp",
"platform/linux/CBasePipe.cpp",
"platform/linux/CBaseSysDep.cpp",
"platform/linux/CBaseThread.cpp",
"platform/socket/CBaseSocketFactory.cpp",
"platform/socket/linux/CLinuxSocket.cpp",
"platform/socket/sckt-0.5/sckt.cpp",
"platform/socket/CGenericClientSocket.cpp",
"platform/socket/CGenericServerSocket.cpp",
"platform/socket/CGenericSession.cpp",
"platform/socket/CGenericSocket.cpp",
"platform/socket/CGenericTcpSession.cpp",
"platform/socket/CGenericUdpSession.cpp",
"platform/socket/CGenericUdpSocket.cpp",
"security/CApiSecurityConfig.cpp",
"security/CFdbToken.cpp",
"security/CFdbusSecurityConfig.cpp",
"security/CHostSecurityConfig.cpp",
"security/CServerSecurityConfig.cpp",
"utils/fdb_option_parser.cpp",
"worker/CBaseEventLoop.cpp",
"worker/CBaseWorker.cpp",
"worker/CFdEventLoop.cpp",
"worker/CThreadEventLoop.cpp",
"worker/CSysFdWatch.cpp",
"utils/CBaseNameProxy.cpp",
"fdbus/CIntraNameProxy.cpp",
"server/CAddressAllocator.cpp",
"log/CLogPrinter.cpp",
"log/CFdbLogCache.cpp",
"utils/cJSON/cJSON.c",
"fdbus/CFdbAFComponent.cpp",
"datapool/CDataPool.cpp",
"datapool/CDpClient.cpp",
"datapool/CDpServer.cpp",
]
cc_library_shared {
name: "libfdbus",
vendor_available: true,
cppflags: [
"-Wno-non-virtual-dtor",
"-frtti",
"-fexceptions",
"-Wno-unused-parameter",
"-D__LINUX__",
"-DFDB_CFG_SOCKET_PATH=\"/data/misc/fdbus\"",
"-DCONFIG_DEBUG_LOG",
"-DCONFIG_SOCKET_CONNECT_TIMEOUT=0",
"-DCONFIG_LOG_TO_STDOUT",
"-DCONFIG_FDB_NO_RTTI",
"-DCONFIG_FDB_MESSAGE_METADATA",
"-DFDB_CONFIG_UDS_ABSTRACT",
"-DCFG_ALLOC_PORT_BY_SYSTEM",
],
cflags: [
"-Wno-non-virtual-dtor",
"-Wno-unused-parameter",
"-D__LINUX__",
"-DFDB_CFG_SOCKET_PATH=\"/data/misc/fdbus\"",
"-DCONFIG_DEBUG_LOG",
"-DCONFIG_SOCKET_CONNECT_TIMEOUT=0",
"-DCONFIG_LOG_TO_STDOUT",
"-DCONFIG_FDB_MESSAGE_METADATA",
"-DFDB_CONFIG_UDS_ABSTRACT",
"-DCFG_ALLOC_PORT_BY_SYSTEM",
],
shared_libs: [
"liblog",
"libutils",
],
srcs: SRC_FILES,
export_include_dirs: ["public"],
local_include_dirs: [],
}
//=====================================================================================
// build libfdbus-jni.so |
//=====================================================================================
cc_library_shared {
name: "libfdbus-jni",
cppflags: [
"-Wno-non-virtual-dtor",
"-frtti",
"-fexceptions",
"-Wno-unused-parameter",
"-D__LINUX__",
"-DFDB_CFG_SOCKET_PATH=\"/data/misc/fdbus\"",
"-DCONFIG_DEBUG_LOG",
"-DCFG_JNI_ANDROID",
"-DFDB_CFG_KEEP_ENV_TYPE",
],
cflags: [
"-Wno-non-virtual-dtor",
"-Wno-unused-parameter",
"-D__LINUX__",
"-DFDB_CFG_SOCKET_PATH=\"/data/misc/fdbus\"",
"-DCONFIG_DEBUG_LOG",
"-DCFG_JNI_ANDROID",
],
srcs: [
"jni/src/cpp/CJniClient.cpp",
"jni/src/cpp/CJniMessage.cpp",
"jni/src/cpp/CJniServer.cpp",
"jni/src/cpp/FdbusGlobal.cpp",
"jni/src/cpp/CJniAFComponent.cpp",
],
shared_libs: ["libfdbus"],
include_dirs: [
"frameworks/base/core/jni",
"frameworks/base/core/jni/include",
],
}
//=====================================================================================
// build name-server |
//=====================================================================================
cc_binary {
name: "name-server",
vendor_available: true,
//init_rc: ["fdbus-name-server.rc"],
cppflags: [
"-Wno-non-virtual-dtor",
"-frtti",
"-fexceptions",
"-Wno-unused-parameter",
"-D__LINUX__",
"-DFDB_CFG_SOCKET_PATH=\"/data/misc/fdbus\"",
"-DCONFIG_DEBUG_LOG",
],
cflags: [
"-Wno-non-virtual-dtor",
"-Wno-unused-parameter",
"-D__LINUX__",
"-DFDB_CFG_SOCKET_PATH=\"/data/misc/fdbus\"",
"-DCONFIG_DEBUG_LOG",
],
srcs: [
"server/main_ns.cpp",
"server/CNameServer.cpp",
"server/CInterNameProxy.cpp",
"server/CIntraHostProxy.cpp",
"server/CBaseHostProxy.cpp",
"server/CSvcAddrUtils.cpp",
"server/CNameProxyContainer.cpp",
"security/CServerSecurityConfig.cpp",
],
shared_libs: [
"libfdbus",
"liblog",
"libutils",
],
}
上述脚本会编译出libfdbus.so、libfdbus-jni.so与name-server可执行文件。其中so需要放入编译出的系统ROM中的/system/lib目录,而name-server需要放入/system/bin。
为了在编译ROM时能编译fdbus,需要在 /build/target/product/base.mk
中的中加入一个base_system_custom.mk文件专门中的中加入:libfdbus、libfdbus-jni与name-server。
PRODUCT_PACKAGES += \
libfdbus \
libfdbus-jni \
name-server \
3.3 name-server启动
为了让name-server开机启动,需要在 init.rc
中配置启动脚本。
把FDBUS源码根目录下的fdbus-name-server.rc 删掉!
打开 /system/core/rootdir/init.rc
在文件最后加入:
#1、在挂载 /data 后创建/data/misc/fdbus 并设置system用户组权限
on post-fs-data
mkdir /data/misc/fdbus 0755 system system
#2、启动name-server
service name-server /system/bin/name-server -u tcp://139.224.136.101:60000 -n android
class core
user root
group system root inet
writepid /dev/cpuset/system-background/tasks
1、因为FDBUS支持uds与tcp。在当前系统中其他进程需要连接name-server会采用UDS的方式,/data/misc/fdbus 则为name-server的uds固定地址。
mkdir /data/misc/fdbus 0755 system system:创建文件并设置权限。
2、-u 参数后文解释,-n 参数为当前name-server的别名可以随意传递。
class core:当前服务属主,系统能根据属主统一管理同属主下所有的进程,同一个class的所有服务必须同时启动或者停止。 group inet:当前服务的用户组。 inet 组表示允许网络访问!
在/framework/base/data/etc/platform.xml 中可以查看权限对应的用户组:
3.4 name-server权限
更详细内容见《SEAndroid安全机制》
完成上述配置后,系统启动就会拉起name-server,但是此时name-server还无法正常工作。Android是建立在标准的Linux Kernel基础上,通过Linux的 SELinux(SEAndroid)进行访问权限控制。name-server需要访问tcp_socket、unix_stream_socket则必须开启SElinux的访问限制。
在 system/sepolicy/private/file_contexts
的最后一行加入:
/system/bin/name-server u:object_r:name-server_exec:s0
接着在system/sepolicy/private/
下创建name-server.te 在文件中写入:
type name-server, domain, mlstrustedsubject;
type name-server_exec, exec_type, file_type;
allow name-server self:tcp_socket { read write getattr getopt setopt shutdown create bind connect name_connect };
allow name-server self:netlink_route_socket {create};
allow name-server fwmarkd_socket:sock_file {write};
allow name-server port:tcp_socket {name_connect};
allow name-server netd:unix_stream_socket {connectto};
allow name-server self:capability {net_raw};
init_daemon_domain(name-server)
同时name-server需要与netd进程(网络管理进程)交互,还需要在当前目录下的netd.te 增加:
typeattribute netd coredomain;
typeattribute netd domain_deprecated;
#增加==============
allow netd name-server:fd {use};
allow netd name-server:tcp_socket {getopt};
allow netd name-server:tcp_socket {setopt};
allow netd name-server:tcp_socket {read write};
#==============
init_daemon_domain(netd)
# Allow netd to spawn dnsmasq in it's own domain
domain_auto_trans(netd, dnsmasq_exec, dnsmasq)
# Allow netd to start clatd in its own domain
domain_auto_trans(netd, clatd_exec, clatd)
如果需要普通APP使用name-server, 还需要在system/sepolicy/private/untrusted_app.te
中加入:
# ......
create_pty(untrusted_app)
# 加入此处规则
allow untrusted_app name-server:unix_stream_socket{connectto};
3.5 FDBUS API
在上一步,我们不仅完成了name-server的部署,同时编译出libfdbus-jni.so,该动态库为FDBUS的Java层API的JNI封装。为了在Java中完成FDBUS的使用,可以将FDBUS集成进入Framework与SDK中。
3.5.1 Framework/SDK 集成
将FDBUS中的jni\src\java
下的Java代码放入AOSP源码中的/frameworks/base/core/java
中
并修改AOSP源码中的`/build/soong/scripts/check_boot_jars/package_allowed_list.txt
接着在/framework/base/Android.dp
中找到 **packages_to_document **,并在其中加入ipc.fdbus
3.5.2 系统内IPC通信
按照《Android源码编译》课程,编译Android源码,并将ROM刷至设备。然后按照《自定义系统服务》将SDK制作完成,接下来就可以在APP中使用FDBUS完成IPC通信:
服务端:
public class IPCServer implements FdbusServerListener {
private static final String TAG = "IPCServer";
private FdbusServer server;
public IPCServer() {
server = new FdbusServer();
server.setListener(this);
server.bind("svc://enjoy");
}
public void destroy() {
server.unbind();
server.destroy();
}
@Override
public void onOnline(int sid, boolean is_first, int qos) {
Log.i(TAG, "onOnline: " + sid);
}
@Override
public void onOffline(int sid, boolean is_last, int qos) {
Log.i(TAG, "onOffline: " + sid);
}
@Override
public void onInvoke(FdbusMessage fdbusMessage) {
String s = new String(fdbusMessage.byteArray());
Log.i(TAG, "onInvoke: " + fdbusMessage.code() + " " + s);
fdbusMessage.reply(("服务端响应 ==>" + s).getBytes());
}
@Override
public void onSubscribe(FdbusMessage msg, ArrayList<SubscribeItem> sub_list) {
for (SubscribeItem subscribeItem : sub_list) {
Log.i(TAG, "客户端注册事件监听: " + subscribeItem.code() + " " + subscribeItem.topic());
}
}
public FdbusServer getService() {
return server;
}
}
客户端:
public class IPCClient implements FdbusClientListener {
private static final String TAG = "IPCClient";
private final FdbusClient mClient;
public IPCClient() {
mClient = new FdbusClient();
mClient.setListener(this);
}
public FdbusClient getClient() {
return mClient;
}
@Override
public void onOnline(int sid, int i1) {
Log.i(TAG, "onOnline: " + sid);
ArrayList<SubscribeItem> subscribe_items = new ArrayList<SubscribeItem>();
subscribe_items.add(SubscribeItem.newEvent(100));
mClient.subscribe(subscribe_items);
}
@Override
public void onOffline(int sid, int i1) {
Log.i(TAG, "onOffline: " + sid);
}
@Override
public void onReply(FdbusMessage fdbusMessage) {
Log.i(TAG, "onReply: " + new String(fdbusMessage.byteArray()));
}
@Override
public void onGetEvent(FdbusMessage fdbusMessage) {
// server.initEventCache()
//
Log.i(TAG, "onGetEvent: " +new String(fdbusMessage.byteArray()));
}
@Override
public void onBroadcast(FdbusMessage fdbusMessage) {
Log.i(TAG, "onBroadcast: " + new String(fdbusMessage.byteArray()));
fdbusMessage.destroy();
}
}
//......
IPCClient ipcClient = new IPCClient();
ipcClient.getClient().connect("svc://enjoy");
// 发送消息给服务端
FdbusMessage fdbusMessage = ipcClient.getClient().invokeSync(1, "我是客户端1".getBytes());
ipcClient.getClient().invokeAsync(2, "我是客户端2".getBytes(), new FdbusAppListener.Action() {
@Override
public void handleMessage(FdbusMessage fdbusMessage) {
Log.i(TAG, "ipc: " + new String(fdbusMessage.byteArray()));
}
});
ipcClient.getClient().send(3, "我是客户端3".getBytes());
3.5.3 跨系统通信
车载系统中,由于Hypervisor的采用,有的域内可能会有多个节点。如智能座舱域,一个SOC芯片上可能会同时运行QNX和Android,虽然位于同一个SOC上,但还是被认为是两个节点。这两个节点必须打通,才能相互通信!
在Android与QNX系统中各自部署name-server,Android与QNX的name-server只能管理系统自身内部服务,为了打通两个系统,FDBUS提供了host-server。
就好像DNS解析,本地DNS服务器无法解析,就会请求远程服务器。如果说name-server是本地DNS服务器,那么host-server就是远程DNS服务器。
3.6 host-server部署
host-server可以搭建在域内,也可以搭建在域外,只要保证Android与QNX都能够访问即可。
我们以阿里云服务器为例,现在让Android与阿里云使用FDBUS完成通信。Android端的name-server已经搭建完成,我们还需要在阿里云上搭建name-server与host-server。
按照:fdbus.readthedocs.io/en/latest/r… 中的介绍,在阿里云中编译fdbus:
然后启动服务:
#若没有执行权限,则先执行:sudo chmod +x host_server
./host_server &
./name_server &
服务启动后随时可以执行 lssvc 查看:
可以看到host-server端口为:60000,配置阿里云安全规则,打开阿里云60000端口访问。在Android端的init.rc中配置的name-server填写阿里云的IP与60000端口:
#1、在挂载 /data 后创建/data/misc/fdbus 并设置system用户组权限
on post-fs-data
mkdir /data/misc/fdbus 0755 system system
#2、启动name-server
service name-server /system/bin/name-server -u tcp://139.224.136.101:60000 -n android
class core
group system inet
writepid /dev/cpuset/system-background/tasks
-u 参数就是阿里云host-server的地址
-n 则为在当前服务端别名,相当于当前的name-server在host_server中的用户名
至此,基于host-server即可完成跨系统的通信!
感谢
享学课堂 码牛学院
相关推荐
- 如何在wordPress和WooCommerce中添加架构标记
- 开启Android学习之旅-2-架构组件实现数据列表及添加(kotlin)
- 如何在WooCommerce和WordPress中添加GTIN、ISBN和MPN架构
- 如何在windows搭建Android系统源码学习环境
- 巨详细的Andorid13系统的编译、构建 & FrameWork基础环境搭建教程!
- 手机上开发Android车机应用一 预制系统apk
- 榨干Pixel5最后的价值:编译刷写Android12L车机系统
- 移动端防截屏录屏技术在百度账户系统实践
- AIDL 跨进程通信学习实验 API34
- 如何为您的网站设置自动总机电话系统
- 程序开发学习排行
-
- 1鸿蒙HarmonyOS:Web组件网页白屏检测
- 2HTTPS协议是安全传输,为啥还要再加密?
- 3HarmonyOS鸿蒙应用开发——数据持久化Preferences
- 4记解决MaterialButton背景颜色与设置值不同
- 5鸿蒙HarmonyOS实战-ArkUI组件(RelativeContainer)
- 6鸿蒙HarmonyOS实战-ArkUI组件(Stack)
- 7[Android][NDK][Cmake]一文搞懂Android项目中的Cmake
- 8鸿蒙HarmonyOS实战-ArkUI组件(mediaquery)
- 9鸿蒙HarmonyOS实战-ArkUI组件(GridRow/GridCol)
- 最近发表
-
- WooCommerce最好的WordPress常用插件下载博客插件模块的相关产品
- 羊驼机器人最好的WordPress常用插件下载博客插件模块
- IP信息记录器最好的WordPress常用插件下载博客插件模块
- Linkly for WooCommerce最好的WordPress常用插件下载博客插件模块
- 元素聚合器Forms最好的WordPress常用插件下载博客插件模块
- Promaker Chat 最好的WordPress通用插件下载 博客插件模块
- 自动更新发布日期最好的WordPress常用插件下载博客插件模块
- WordPress官方最好的获取回复WordPress常用插件下载博客插件模块
- Img to rss最好的wordpress常用插件下载博客插件模块
- WPMozo为Elementor最好的WordPress常用插件下载博客插件模块添加精简版