目录
1. 定义新的AIDL接口
1.1 编译hidl2aidl工具
1.2 执行转换
1.3 调整编译规则(bp文件)
2. 向vendor镜像添加AIDL接口
2.1 使用update-api freeze-api管理接口版本
2.2 配置 Framework Compatibility Matrix (FCM,兼容性矩阵)
2.3 配置使AIDL编译
2章报错速查
3. 实现service
3.1 编写service代码
3.2 创建service编译规则
3.3 将service添加进系统
3章报错速查
4. 确保service开机启动
4.0 开始配置SEpolicy
4.1 添加新feature目录
4.2 创建 hal_sensorscalibrate_default.te
4.3 创建 file_contexts
4.1 无法启动时的排查方法
5. java层调用service(含sepolicy配置)
欢迎指正交流,谢谢!
1. 定义新的AIDL接口
目标:将旧接口全部转换为新接口
验证目标:肉眼检查所有的接口文件已转换,即可
我们使用AOSP自带的转换工具:hidl2aidl,将旧版hidl接口转换为aidl接口。
1.1 编译hidl2aidl工具
编译可以在任意位置执行,这里以vendor根目录为例
source /opt/conf/openjdk18.conf
source build/envsetup.sh
lunch 项目名-userdebug
m hidl2aidl
1.2 执行转换
运行hidl2aidl工具。它会读取我们指定的hidl接口,并生成转换后的aidl。
在命令里,我们需要指定hidl包的所在位置,不然工具会找不到对应的hidl包。
hidl2aidl -o 输出路径 -r hidl包名:hidl包根目录(即/1.0的上层目录) hidl包名@要转换的hidl版本
其中的输出路径,我们创建一个aidl文件夹,此文件夹与hidl接口(1.0、2.0等文件夹)同级。冒号两侧不能有空格。
例如:
hidl2aidl -o vendorabcd/hardware/interfaces/sensorscalibrate/aidl/ -r vendorabcd.hardware.sensorscalibrate:vendorabcd/hardware/interfaces/sensorscalibrate vendorabcd.hardware.sensorscalibrate@1.0
1.3 调整编译规则(bp文件)
首先检查转换生成的文件。转换会生成 .aidl 和 Android.bp 两种文件。
.aidl文件
检查所在路径,要与包名匹配。例如包名:vendorabcd.hardware.sensorscalibrate,文件路径应为: aidl/vendorabcd/hardware/sensorscalibrate/ISensorsCalibrate.aidl
Android.bp文件
生成的bp文件需要修改,示例如下:(红色部分为需要修改的部分)
aidl_interface {
name: "vendorabcd.hardware.sensorscalibrate",
vendor_available: true, //这里设置为true,因为是vendor目录下的模块
owner: "vendorabcd", //作为vendor模块,需要设定owner才可以通过编译
srcs: ["vendorabcd/hardware/sensorscalibrate/*.aidl"],
stability: "vintf", //标示此接口为Stable AIDL,必需
backend: {
cpp: {
enabled: false, //产生一个cpp backend,.cpp文件(unstable版)
//如果想要使用旧版unstable AIDL,设置为true
},
java: {
sdk_version: "module_current", //产生一个java backend,
//默认enabled,用来在framework层使用此接口
},
ndk: {
enabled: true, //产生一个ndk backend,.cpp文件(stable版)
//要使用aidl通信,需要ndk后端
//不允许使用vndk,因为它不是stable的
},
},
}
2. 向vendor镜像添加AIDL接口
目标:通过编译,编译生成vendor镜像,其中包含aidl接口
验证目标:out_hal/soong/.intermediates/system/tools/aidl/build/aidl_metadata_json/此目录下生成了对应的.aidl文件,并成功全编译
2.1 使用update-api freeze-api管理接口版本
AOSP要求我们使用此工具,用来管理接口版本。这个工具做的事情是:把我们写的接口复制一份,然后标上版本号,作为一个冻结的(stable的)版本。之后正式编译时,就自动使用它复制出来的版本。
进入aidl目录(即我们为hidl2aidl指定的output目录),执行以下命令:
source <项目位置>/build/envsetup.sh
lunch 项目名-userdebug
m vendorabcd.hardware.sensorscalibrate-update-api //复制现在的版本到aidl_api/current
m vendorabcd.hardware.sensorscalibrate-freeze-api //从current生成一个新的版本(号)
以上命令会生成一个叫aidl_api的目录,并在里面做各种修改。我们不要手动修改这个目录的内容。
以上命令还会在Android.bp中生成一个新的字段:versions: [“1”], 代表版本号。这一字段由api工具管理&修改,我们不要手动修改这个字段。
2.2 配置 Framework Compatibility Matrix (FCM,兼容性矩阵)
FCM指定了msi与vendor间的兼容关系。因此,对我们新定义的接口,需要在msi和vendor两个目录中,都配置FCM兼容关系。
vendor目录中
FCM文件位置:
vendor/device/vendorabcd/<机型project名>(/……)/framework_compatibility_matrix.xml
在FCM文件中,我们可能会找到如下的旧版hidl配置:
<hal format="hidl" optional="true">
<name>vendorabcd.hardware.sensorscalibrate</name>
<version>1.0</version>
<interface>
<name>ISensorsCalibrate</name>
<instance>default</instance>
</interface>
</hal>
在新的vendor image中,我们的新接口将要取代旧接口。因此,我们将hidl更改为aidl:
<hal format="aidl" optional="true">
<name>vendorabcd.hardware.sensorscalibrate</name>
<version>1.0</version><interface>
<name>ISensorsCalibrate</name>
<instance>default</instance>
</interface>
</hal>
msi目录中
FCM文件位置:
msi/device/vendorabcd/<机型project名>(/mssi)/framework_compatibility_matrix.xml
我们希望system image既具有对新版aidl的支持,又能够兼容原本的hidl接口。因此这里无需修改,直接添加新的aidl接口到文件中:
<hal format="aidl" optional="true">
<name>vendorabcd.hardware.sensorscalibrate</name>
<interface>
<name>ISensorsCalibrate</name>
<instance>default</instance>
</interface>
</hal>
2.3 配置使AIDL编译
配置vendor镜像编译。我们用aidl取代原本的hidl接口,无需保留原有hidl接口。
首先找到此前的hidl接口编译manifest。它在device/vendorabcd/<project名或chipset名>目录下的某个.xml文件之中,其中会有类似这样的内容:
<manifest version="1.0" type="device">
<hal format="hidl">
<name>vendorabcd.hardware.sensorscalibrate</name>
<transport>hwbinder</transport>
<version>1.0</version>
<interface>
<name>ISensorsCalibrate</name>
<instance>default</instance>
</interface>
</hal>
</manifest>
我们将其改为aidl:
<manifest version="1.0" type="device">
<hal format="aidl">
<name>vendorabcd.hardware.sensorscalibrate</name>
<transport>hwbinder</transport><version>1</version>
[可以保留原本的写法:]
<interface>
<name>ISensorsCalibrate</name>
<instance>default</instance>
</interface>
[或者使用fqname:]
<fqname>ISensorsCalibrate/default</fqname>
[两种任选一个即可]
</hal>
</manifest>
现在,尝试进行先msi后vendor的全编译。应该能够正常完成整个编译。
编译完成后,会在以下目录中,有生成对应的aidl文件:
out_hal/soong/.intermediates/system/tools/aidl/build/aidl_metadata_json/metadata_包名
以上我们配置的aidl manifest,其位置并不强制一定要放在此路径下面。之后我将会移动此manifest的位置,将其和service放在一起。您也可以选择之后不移动。
2章报错速查
module "..." already defined
解释:包名称重复
错误原因:通常是因为原本的hidl接口带有如下结构:在接口根目录下的bp文件中,定义了如下package root:
hidl_package_root {
name: "vendorabcd.hardware.sensorscalibrate",
}
因此导致了新的aidl接口与旧package root名字冲突。
解决方法:修改新的aidl接口的名称。请注意之前的每一步骤中xml文件中的<name>都需要修改。也需要修改.aidl文件中的package名。也需要修改.aidl文件所存放的路径,与新接口名称相符合。还需要对应修改Android.bp中的srcs路径。
FAILED: out/soong/... echo 包名-api missing dependencies
解释:找不到对应版本的api
错误原因:可能是因为手动修改了Android.bp中的versions字段,导致编译工具去寻找一个不存在的版本。
解决方法:将versions字段清空,并完全删除aidl_api目录。之后,再重新执行update-api和freeze-api。
module "包名-api" (created by module "包名_interface"): srcs: No sources provided.
解释:srcs目录下没有找到有效的源文件
错误原因:Android.bp中srcs配置有问题,可能是因为修改了aidl所在目录,导致找不到aidl文件。
解决方法:修改srcs,aidl文件应该直接放在srcs目录内。
files are incompatible: The following instances are in the device manifest but not specified in framework compatibility matrix
解释:模块在device manifest中指定了,但是找不到对应的FCM
错误原因:FCM配置有问题,条目未添加,或添加了但写错了导致不匹配。
解决方法:请检查两个FCM内是否有拼写错误等,同时检查manifest中的名字是否写对。
3. 实现service
目标:成功编译service
验证目标:检查手机中/vendor/bin/hw/目录下是否有service编译出的二进制文件,例如/vendor/bin/hw/vendorabcd.hardware.sensorscalibrate-service
service为一个可执行文件。我们之前步骤中完成的AIDL接口,定义了一种信息结构,不承担功能。接下来要做的service,负责功能执行,service会接收AIDL接口发来的信息,并处理相关的功能运作。可以用Server-Client模型理解,此service即为server端,负责处理client端发来的请求,并和后台交互以执行功能。
3.1 编写service代码
首先,请在aidl目录下,创建一个default目录。我们所有的service相关内容都会放在这个default目录下。
*理论上,service可以放在任何地方,但和aidl放在一处更好,便于管理。
进入default目录。我们将要创建三个文件:service.cpp,SensorsCalibrate.h,SensorsCalibrate.cpp。
service.cpp
创建这个文件,只需修改标红的部分,其余照抄即可:
#define LOG_TAG "vendorabcd.hardware.sensorscalibrate-service"
#include "SensorsCalibrate.h"
#include <android-base/logging.h>
#include <android/binder_manager.h>
#include <android/binder_process.h>
using aidl::vendorabcd::hardware::sensorscalibrate::SensorsCalibrate;
int main() {
ABinderProcess_setThreadPoolMaxThreadCount(0);
std::shared_ptr<SensorsCalibrate> senCal = ndk::SharedRefBase::make<SensorsCalibrate>();
if (!senCal) {
return EXIT_FAILURE;
}
const std::string instance = std::string() + SensorsCalibrate::descriptor + "/default";
binder_status_t status = AServiceManager_addService(senCal->asBinder().get(), instance.c_str());
CHECK(status == STATUS_OK);
ABinderProcess_joinThreadPool();
return EXIT_FAILURE;
}
SensorsCalibrate.h
在这个文件中,我们将aidl接口所提供的Bn类,扩展为方便使用的类。具体来说,我们需要对aidl中的每一个接口,都进行一次“翻译”。
例子如下:
#pragma once
#include <aidl/vendorabcd/hardware/sensorscalibrate/BnSensorsCalibrate.h>
namespace aidl {
namespace vendorabcd {
namespace hardware {
namespace sensorscalibrate {
class SensorsCalibrate : public BnSensorsCalibrate{
ndk::ScopedAStatus SensorsCal(int32_t sensor_type,
int8_t operation_type,
std::string* _aidl_return) override;
};
} // namespace sensorscalibrate
} // namespace hardware
} // namespace vendorabcd
} // namespace aidl
标蓝部分和aidl文件中的接口一一对应。我的例子中只有一个方法,是因为我的aidl文件只定义了一个接口。对于多个aidl接口,我们对每一个都要写一条这样的ndk“翻译”。
因为我们上一章已经成功编译了aidl,标蓝部分的写法可以直接复制。对应文件所在路径为:
out/soong/.intermediates/vendorabcd/hardware/interfaces/sensorscalibrate/vendorabcd.hardware.sensorscalibrate-V1-ndk_platform-source/gen/include/aidl/vendorabcd/hardware/sensorscalibrate/BpSensorsCalibrate.h
其中标红的部分容易混淆,请注意路径名。标蓝的部分与aidl文件所在的路径对应。标绿的部分与包名相对应。
在复制时,只需要复制其中的与aidl对应的方法即可。 getInterfaceVersion, getInterfaceHash 、构造析构函数、以及成员变量等,都不需要复制。
(可看可不看的)详细解释:
对照来看:由.aidl
String SensorsCal(in int sensor_type, in byte operation_type)
改写到.h
ndk::ScopedAStatus SensorsCal(int32_t sensor_type,
int8_t operation_type,
std::string* _aidl_return) override;
在.h文件中,我们将aidl返回值作为最后的参数。.h文件中返回类型改为ndk::ScopedAStatus。
SensorsCalibrate.cpp
此文件直接复制旧版hidl中的对应cpp即可。旧cpp文件可能与hidl定义不在同一处,我们已知文件名,可以通过搜索找到。
文件中需要修改以下地方:
namespace aidl {
namespace vendorabcd {
namespace hardware {
namespace sensorscalibrate {
namespace V1_0 {
namespace implementation {
Return<void>SensorsCalibrate::SensorsCal(int32_t sensor_type, int8_t operation_type,SensorsCal_cb _hidl_cb) {ndk::ScopedAStatus SensorsCalibrate::SensorsCal(
int32_t sensor_type,
int8_t operation_type,
std::string* _aidl_return) {//与.h声明的一致
// ……
// 功能实现部分,已省略
_hidl_cb("Successfully cal prox");*_aidl_return = "Successfully cal prox"; //此为hidl/aidl接口返回信息
return Void();return ndk::ScopedAStatus::ok();
}
ISensorsCalibrate* HIDL_FETCH_ISensorsCalibrate(const char* /* name */) {
return new SensorsCalibrate();
}//hidl的FETCH方法现在不需要,删除
} // namespace implementation
} // namespace V1_0} // namespace sensorscalibrate
} // namespace hardware
} // namespace vendorabcd
} // namespace aidl
3.2 创建service编译规则
要配置编译规则,我们在default目录下,继续添加以下三个文件:Android.mk/bp .rc .xml
Android.mk / Android.bp
指定service的编译规则。
具体使用mk文件还是bp文件,可以和hidl service保持一致。bp是更现代的方式;但如果要引用mk定义的模块,则也必须使用mk。因此,和hidl版本保持一致是最方便的选择。
例如,hidl版本此service的Android.mk文件如下。我们直接在其上进行修改,用作aidl版本service的mk文件:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := vendorabcd.hardware.sensorscalibrate@1.0-serviceLOCAL_MODULE := vendorabcd.hardware.sensorscalibrate-service
LOCAL_PROPRIETARY_MODULE := true
LOCAL_MODULE_CLASS := EXECUTABLES
LOCAL_MODULE_RELATIVE_PATH := hw
LOCAL_INIT_RC := vendorabcd.hardware.sensorscalibrate@1.0-service.rcLOCAL_INIT_RC := vendorabcd.hardware.sensorscalibrate-service.rc
//声明此service对aidl接口的依赖。之后,我们将创建这个文件。
LOCAL_VINTF_FRAGMENTS := vendorabcd.hardware.sensorscalibrate.xml
LOCAL_SRC_FILES := \
SensorsCalibrate.cpp \
service.cpp
LOCAL_SHARED_LIBRARIES := \
libhidlbase \
libhidltransport \libbinder_ndk \ //Stable aidl必需
libutils \
libdl \
liblog \
libcutils \
libhardware \
libbase \
vendorabcd.hardware.sensorscalibrate@1.0 \// 包名-V1-ndk_platform
vendorabcd.hardware.sensorscalibrate-V1-ndk_platform \
libhfmanager
include $(BUILD_EXECUTABLE)
.rc
文件名:vendorabcd.hardware.sensorscalibrate-service.rc
配置开机启动。
此rc文件将被init.rc链接。我们可以在此文件中,指定开机时启动aidl service。示例如下:
service vendor.sensorscalibrate-hal /vendor/bin/hw/vendorabcd.hardware.sensorscalibrate-service
//service + 注册名(可随意起一个) + 编译后的模块所在路径
class hal
user system
group system
.xml (manifest)
文件名:vendorabcd.hardware.sensorscalibrate.xml
我把aidl接口的manifest转移到此处,因为service依赖于aidl。如果未来不需此service,则aidl接口也不编译。这一依赖关系,我们已在Android.mk中的LOCAL_VINTF_FRAGMENTS声明过,此项配置指向的xml文件即为我们现在创建的xml文件。
把2.3步中的aidl manifest删除,将其中内容转移到default目录下的此文件中。
内容原样复制即可:
<manifest version="1.0" type="device">
<hal format="aidl">
<name>vendorabcd.hardware.sensorscalibrate</name>
<version>1</version>
<interface>
<name>ISensorsCalibrate</name>
<instance>default</instance>
</interface>
</hal>
</manifest>
尝试编译
在default目录下运行mm进行编译。(请记得先在vendor根目录下执行source build/envsetup.sh 和 lunch)现在应该能够完成模块编译。
编译成功后,在out/target/product/<project名>/vendor/bin/hw能看到编译得到的service可执行文件。
3.3 将service添加进系统
修改device.mk文件,以包含此service。
我们首先找到hidl版本的编译声明,通常位置为device/vendorabcd/<project名或chipset名>/device.mk
例如,我找到这样的hidl编译配置:
PRODUCT_PACKAGES += vendorabcd.hardware.sensorscalibrate@1.0-service \
vendorabcd.hardware.sensorscalibrate@1.0-service.rc \
vendorabcd.hardware.sensorscalibrate@1.0 \
vendorabcd.hardware.sensorscalibrate-V1.0-java \
vendorabcd-hardware-sensorscalibrate.xml
DEVICE_MANIFEST_FILE += device/vendorabcd/chipset/mgvi/manifest_sensorscalibrate.xml
升级到aidl以后,只需配置好service,对应的xml即会自动加入编译。
把以上的内容全部删除。替换为:
PRODUCT_PACKAGES += vendorabcd.hardware.sensorscalibrate-service
(可选)现在可以全编译一下,确认能够正常完成整个系统的全编译。
3章报错速查
FAILED: ninja: '.xml', needed by 'out_hal/(...)/.xml', missing and no known rule to make it
解释:ninja错误:第2个路径xml中,需求了第1个路径的xml,但是找不到对应的文件
错误原因:可能因为manifest中的旧版本没有删除干净,导致编译器寻找已被删除的xml。
解决方法:检查 DEVICE_MANIFEST_FILE 中旧版的xml配置,并删除干净
ld.lld: error: undefined symbol: >>> the vtable symbol may be undefined because the class is missing its key function
解释:clang错误:没有找到对应的类,因为没有实现对应的虚函数。
错误原因:cpp编写有误,例如实现函数时未声明namespace :SensorsCalibrate::SensorsCal() 错写成 SensorsCal()
解决方法:检查cpp文件并修改
4. 确保service开机启动
目标:开机时service能够自启动
验证目标:开机后,执行adb shell service list,能看到新的service
本章将要配置SEpolicy以确保service开机启动。SEpolicy是引入了SELinux(kernel)后所需求的一种安全策略。SELinux会默认阻止一些关键操作(例如文件操作,ioctl等),我们需要逐条声明后,才能合法进行这些操作。
4.0 开始配置SEpolicy
在第3章最后,我们能编译得到一个新的系统版本。但在这个版本中,service不会随开机启动,因为我们还没有配置SEpolicy。一种更具有演示性的做法是,我们收集log中的拒绝信息,再对应配置SEpolicy。然而,由于全编译一次的时间成本过高,在这里,我们先配置完所需的SEpolicy,再检查是否能自动启动。
需要注意的是,在配置SEpolicy相关文件时,每个文件末尾必须另有一行空白行,否则会在编译时遇到无法预知的错误。具体原因我也不太清楚。
4.1 添加新feature目录
文件vendor/vendorabcd/security/sepolicy_vendor/features/sepolicy_features_vendor.mk 中定义了vendorabcd的vendor相关sepolicy。我们按格式增加一个feature。
添加如下一行:
BOARD_SEPOLICY_DIRS += $(LOCAL_PATH)/sensorscalibrate/vendor
并在此目录下创建/sensorscalibrate/vendor目录。进入这个新创建的目录。
4.2 创建 hal_sensorscalibrate_default.te
创建一个文件,名为hal_sensorscalibrate_default.te,内容如下:
type hal_sensorscalibrate_default, domain;
type hal_sensorscalibrate_default_exec, exec_type, vendor_file_type, file_type;
init_daemon_domain(hal_sensorscalibrate_default)
其中的init_daemon_domain是一段宏,可以自动帮助我们完成与init相关的policy声明。这段宏的括号中的输入x应为一个domain(即本文件,在第一行定义),并有名为x_exec的对象被定义为一个executable(在第二行定义)。第一行和第二行满足了init_daemon_domain对输入的需求。
我们现在还没有把 hal_sensorscalibrate_default_exec 与实际文件相绑定。于是另创建一个文件,名为file_contexts。
4.3 创建 file_contexts
file_contexts负责声明实际文件为某个SEpolicy中的对象。
我们将手机中service可执行文件的路径,声明为hal_sensorscalibrate_default_exec。
创建一个文件,名为file_contexts,内容如下:
/(vendor|system/vendor)/bin/hw/vendorabcd\.hardware\.sensorscalibrate-service u:object_r:hal_sensorscalibrate_default_exec:s0
4.1 无法启动时的排查方法
1. 确认service本身可正常运行
我们首先尝试手动启动服务,以收集相关log。
在adb shell内,切换到su用户。将SEpolicy调整到宽容模式,放行未声明的操作。
su root
setenforce 0
getenforce #检查是否打开宽容模式(输出Permissive为宽容模式)
启动服务:
/vendor/bin/hw/vendorabcd.hardware.sensorscalibrate-service
检查服务是否正常启动。不要关闭前一个终端窗口,另开一个窗口,在adb shell中执行
service list | grep sensorscalibrate
有如下输出,说明启动成功:
159 vendorabcd.hardware.sensorscalibrate.ISensorsCalibrate/default: [vendorabcd.hardware.sensorscalibrate.ISensorsCalibrate]
如果方括号[ ]内没有内容,可能是未成功打开宽容模式。
2. 检查启动阶段的log
dmesg是linux提供的内核log工具,与系统启动相关的log在其中有记录。
打开终端,进入sudo模式,收集本次启动的dmesg:
adb root
adb shell dmesg > dmesg.txt
如果看到类似于以下的dmesg内容,说明系统启动时,由于SEpolicy阻拦,service启动失败:
[ 10.220035] init: init 27: [10038][0]Could not start service 'vendor.sensorscalibrate-hal' as part of class 'hal': File /vendor/bin/hw/vendorabcd.hardware.sensorscalibrate-service(labeled "u:object_r:vendor_file:s0") has incorrect label or no domain transition from u:r:init:s0 to another SELinux domain defined. Have you configured your service correctly?
3 查看系统运行阶段的log
在宽容模式下,系统不会阻止未声明的操作,但仍记录这些操作的拒绝记录。我们可以通过拒绝记录来添加对应的声明。
在adb shell中执行
logcat | grep sensorscalibrate
若看到有如下log,记录了被拒绝的的操作:
11-10 16:29:41.311 633 633 E SELinux : avc: denied { find } for pid=3223 uid=2000 name=vendorabcd.hardware.sensorscalibrate.ISensorsCalibrate/default scontext=u:r:shell:s0 tcontext=u:object_r:default_android_service:s0 tclass=service_manager permissive=1
#这个log的意思是我试图从shell启动service但是失败了
log的格式解释如下:
avc: denied { 操作权限 } for pid=7201 comm=“进程名” scontext=u:r:源类型:s0 tcontext=u:r:目标类型:s0 tclass=访问类别 permissive=0
修改步骤:
找相应的“源类型.te ”文件
按如下格式在该文件中添加:
allow 源类型 目标类型:访问类别 {权限};
allow vendorabcd_app hal_sensorscalibrate_default:binder {call};
5. java层调用service(含sepolicy配置)
后面写不动了,先到这里吧。
在msi镜像中,也要定义同样的aidl接口,这样才能进行aidl通信。文件内容使用和vendor中完全相同即可。
推荐阅读:
sepolicy进阶小记 - LibXZR 的小本本 解释了许多关键的sepolicy宏,很有帮助文章来源:https://www.toymoban.com/news/detail-425919.html
AIDL for HALs - Code Inside Out 一个AIDL for HAL全流程的文档,比较细文章来源地址https://www.toymoban.com/news/detail-425919.html
到了这里,关于HAL: 将 HIDL 接口改造为 Stable AIDL的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!