一,Ndk开发【基础】

1.1-基本概念

Android SDK【Java层】

  Android SDK全称是:Android Software Development Kit【Android 软件开发工具包】,Android SDK主要为开发者提供Java层的API调用以及开发过程中所需要的一些构建工具和其它工具;所以说你可以把Android SDK理解成就是Android应用的Java层开发【执行环境:虚拟机】
在这里插入图片描述
在这里插入图片描述

Android NDK【C/C++层】

  Android NDK全称是:Android Native Development Kit【Android 本地开发工具包】,Android NDK主要为开发者提供C/C++层的API接口以及开发过程中所需要的一些构建工具和其它工具,NDK是SDK的一部分;所以说你可以把Android NDK理解成就是Android应用的C/C++底层库开发执行环境:操作系统
在这里插入图片描述
在这里插入图片描述

Android JNI【Java-桥梁-C/C++】

  Android JNI全称:Android native interface【Android 本地接口】,Android JNI主要是为上层Java/Kotlin与本地C/C++ 提供互通互调机制简单点说: Java代码中可以调用Native C/C++代码,Native C/C++代码可以调用上层Java代码;所以说Android JNI就是连接上层Java与本地C/C++互通互调的一个桥梁和机制!
  JNI【最先出来】最早出现在JDK中,Android NDK【后来出来】这个开发工具集集成了JNI!
在这里插入图片描述

在这里插入图片描述

1.2-环境配置

  • 安装Android Studio
    https://developer.android.google.cn/studio 下载地址
    https://developer.android.google.cn/studio/intro/ 帮助文档

  • 创建NDK项目
    在这里插入图片描述

  • 配置NDK
    在这里插入图片描述
    手动配置: https://developer.android.google.cn/ndk/downloads

1.3-JNI基本语法

声明本地方法【Java层:Native方法】

//Native方法-》目的-》调用本地C/C++方法!
public native String stringFromJNI();

实现本地方法【C++层:C++方法】

/** extern "C" 表示:以C语言的方式导出【保持函数名不变,如果是C++,函数名会发生变化】* JNIEXPORT 导出【暴露给外部使用】* JNICALL 调用约定【stdcall,fastcall,ccall等一系列,调用约定会决定入栈顺序和释放的问题】* */
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_ndkdemo_MainActivity_stringFromJNI(JNIEnv* env,jobject /* this */) {std::string hello = "Hello from C++";return env->NewStringUTF(hello.c_str());
}

JNI静态注册

Java层: Native声明-》对应一种固定格式-》C++层: C++方法【Java_包名_Java类名_方法名】
注意点: 如果包名中包含_,那么就会给下划线加上一个_1用来标识这是包名自带下划线,不是规则中的下划线!

JNI核心元素

JavaVM

  JavaVM是虚拟机在JNI层的表示,一个进程只有JVM,所有线程共用JavaVM;

JNIEnv

  JNIEnv它是一个与线程相关的用来代表java运行环境的,用来进行java-native互调的一个结构体,内部包含了一个JNI本地接口指针【JNINativeInterface* functions】,这个指针又指向了函数指针数组【定义了JNI相关的函数指针】;
  同一个线程调用本地方法,JNIEnv是同一个,不同线程调用native方法,JNIEnv不相同【java可以在不同线程中调用】

重点说明JNIEnv在C和C++中使用的区别之处:

  • 定义不同
#if defined(__cplusplus)
//C++对JNINativeInterface*进行了二次封装-》_JNIEnv 结构体
typedef _JNIEnv JNIEnv;
/*
struct _JNIEnv {/* do not rename this; it does not seem to be entirely opaque */const struct JNINativeInterface* functions;
*/
typedef _JavaVM JavaVM;
#else
//C语言直接使用的就是JNINativeInterface*
typedef const struct JNINativeInterface* JNIEnv;
typedef const struct JNIInvokeInterface* JavaVM;
#endif
  • 调用不同
JNIEnv *env
C++中:JNIEnv-》结构体【JNINativeInterface*】JNIEnv *env ===  _JNIEnv * 【_JNIEnv一级指针】
C中:JNIEnv-》指针【JNINativeInterface* JNIEnv】JNIEnv *env === JNINativeInterface**【JNINativeInterface二级指针】

jobject

  Native方法中传递过来的jobject其实就是一个java实例引用,实例就是一个具体的对象,这个native方法是一个具体的java对象调用的!

jclass

  Native方法中传递过来的jclass就是一个java类引用,这个native方法是一个静态类方法,jclass其实这个类引用!

JNI数据类型

基本类型

JNI基本类型,可以直接拿来使用,本质还是C/C++的基本数据类型,只不过使用typedef定义了而已!
在这里插入图片描述

引用类型

引用类型不能直接使用,必须通过JNIEnv中的JNI函数转换后,才能够使用!
在这里插入图片描述
在这里插入图片描述

类型描述

在这里插入图片描述

JNI基本操作

声明native函数:

    public native int senderBaseTypeToJNI(boolean boolValue,byte byteValue,char charValue,short shortValue,int intValue,long longValue,float floatValue,double doubleValue);

调用native函数:

        boolean boolValue = false;byte byteValue = 10;char charValue = 'a';short shortValue = 100;int intValue = 200;long longValue = 300;float floatValue = 3.14f;double doubleValue = 5.12;Log.d("JavaLog", "onCreate: senderBaseTypeToJNI");int iRet = senderBaseTypeToJNI(boolValue,byteValue,charValue,shortValue,intValue,longValue,floatValue,doubleValue);Log.d("JavaLog", "onCreate: iRet=" + iRet);

实现native函数:

extern "C"
JNIEXPORT jint JNICALL
Java_com_example_ndk_1study_1base_MainActivity_senderBaseTypeToJNI(JNIEnv *env, jobject thiz,jboolean bool_value,jbyte byte_value,jchar char_value,jshort short_value,jint int_value,jlong long_value,jfloat float_value,jdouble double_value) {//Java基本类型:
//    public native void senderBaseTypeToJNI(
//            boolean boolValue,
//            byte byteValue,
//            char charValue,
//            short shortValue,
//            int intValue,
//            long longValue,
//            float floatValue,
//            double doubleValue
//    );//JNI 基本类型:
//    typedef uint8_t  jboolean; /* unsigned 8 bits */
//    typedef int8_t   jbyte;    /* signed 8 bits */
//    typedef uint16_t jchar;    /* unsigned 16 bits */
//    typedef int16_t  jshort;   /* signed 16 bits */
//    typedef int32_t  jint;     /* signed 32 bits */
//    typedef int64_t  jlong;    /* signed 64 bits */
//    typedef float    jfloat;   /* 32-bit IEEE 754 */
//    typedef double   jdouble;  /* 64-bit IEEE 754 *///一,每个Java基本类型都对应一个j前缀修饰的JNI基本类型//例子:int -> jintLOGD("boolValue=%d,""byteValue=%d,""charValue=%c,""intValue=%d,""longValue=%ld,""floatValue=%f,""doubleValue=%f\n",bool_value,byte_value,char_value,int_value,long_value,float_value,double_value)//二,JNI基本类型本质:C/C++基本类型//结论:JNI基本类型,可以直接使用!int iNum = int_value;float fNum = float_value;LOGD("\niNum=%d\n,fNum=%f",iNum,fNum);return iNum;
}

JNI数组操作

jintArray

声明native函数:

    public native void senderArrayToJNI(int[] ary);public native int[] newArrayFromJNI();

调用native函数:

        int[] ary = {1,2,3,4,5};senderArrayToJNI(ary);for (int num: ary) {Log.d("JavaLog", "onCreate:print_array1 num=" + num);}int[] newAry = newArrayFromJNI();for (int num: newAry) {Log.d("JavaLog", "onCreate:print_array2 num=" + num);}

实现native函数:

extern "C"
JNIEXPORT void JNICALL
Java_com_example_ndk_1study_1base_MainActivity_senderArrayToJNI(JNIEnv *env,jobject thiz,jintArray jary) {//一,通过env获取数组长度【jsize->本质->jint->int】jsize jaryCount = env->GetArrayLength(jary);//二,获取数组指针,不拷贝jint* pJary = env->GetIntArrayElements(jary,NULL);//三,访问修改打印数组元素for(int i=0;i<jaryCount;i++){LOGD("before update the value : index=%d,value=%d",i,*(pJary + i));*(pJary + i) = *(pJary + i) + 10000;LOGD("after update the value : index=%d,value=%d",i,*(pJary + i));}//四,释放数组指针,并将C++数组的修改通过env同步至JVM java数组//1.pJary必须为数组初始位置,如果修改了,需要回到数组头//2.标记//JNI_OK【同步C++修改至JVM,并释放C++层数组】//JNI_COMMIT【同步C++修改至JVM,不释放C++层数组】//JNI_ABORT【释放C++层数组】env->ReleaseIntArrayElements(jary,pJary,JNI_OK);
}extern "C"
JNIEXPORT jintArray JNICALL
Java_com_example_ndk_1study_1base_MainActivity_newArrayFromJNI(JNIEnv *env,jobject thiz) {//一,指定长度jsize jaryCount = 5;//二,创建数组jintArray jary = env->NewIntArray(jaryCount);//三,获取数组指针jint* pJary =  env->GetIntArrayElements(jary,NULL);for(int i=0;i<jaryCount;i++){*(pJary + i) = *(pJary + i) + 20000 + i;}//四,同步,并释放指针//不同步,返回的jary,只有默认初始值,没有修改后的元素数据env->ReleaseIntArrayElements(jary,pJary,JNI_OK);return jary;
}

jobjectArray

声明native函数:

    public native void senderStrArrayToJNI(String[] strs);public native String[] newStrArrayToJNI();

调用native函数:

        String[] strAry = {"wawa","dada","gaga"};senderStrArrayToJNI(strAry);for (String str: strAry) {Log.d("JavaLog", "onCreate:print_array3 str=" + str);}String[] newStrAry = newStrArrayFromJNI();for (String str: newStrAry) {Log.d("JavaLog", "onCreate:print_array4 str=" + str);}

实现native函数:

extern "C"
JNIEXPORT void JNICALL
Java_com_example_ndk_1study_1base_MainActivity_senderStrArrayToJNI(JNIEnv *env,jobject thiz,jobjectArray jStrs) {//一,获取数组长度jsize jStrsCount = env->GetArrayLength(jStrs);//二,遍历数组元素for (int i = 0; i < jStrsCount; i++) {//获取对应index的字符串元素jstring jstr = (jstring)env->GetObjectArrayElement(jStrs,i);const char* strc = env->GetStringUTFChars(jstr,NULL);LOGD("修改前: index=%d,strValue=%s",i,strc);env->ReleaseStringUTFChars(jstr,strc);jstring jstrValue = env->NewStringUTF("lalala");env->SetObjectArrayElement(jStrs,i,jstrValue);env->DeleteLocalRef(jstrValue);jstring jstrUpdate = (jstring)env->GetObjectArrayElement(jStrs,i);const char* strcUpdate = env->GetStringUTFChars(jstrUpdate,NULL);LOGD("修改后: index=%d,strValue=%s",i,strcUpdate);env->ReleaseStringUTFChars(jstrUpdate,strcUpdate);}
}extern "C"
JNIEXPORT jobjectArray JNICALL
Java_com_example_ndk_1study_1base_MainActivity_newStrArrayFromJNI(JNIEnv *env,jobject thiz) {//一,声明数组长度jsize jAryCount = 5;//二,查找String类jclass jstrCls = env->FindClass("java/lang/String");//三,创建字符串jstring,作为初始默认对象jstring jstr = env->NewStringUTF("auto_value");//四,创建一个object数组jobjectArray jstrAry = env->NewObjectArray(jAryCount,jstrCls,jstr);//释放jstr本地引用env->DeleteLocalRef(jstr);//释放jstrcls本地引用env->DeleteLocalRef(jstrCls);for (int i = 0; i < jAryCount; i++) {//获取对应index的字符串元素jstring jstr = (jstring)env->GetObjectArrayElement(jstrAry,i);const char* strc = env->GetStringUTFChars(jstr,NULL);LOGD("默认值: index=%d,strValue=%s",i,strc);env->ReleaseStringUTFChars(jstr,strc);jstring jstrValue = env->NewStringUTF("xixixi");env->SetObjectArrayElement(jstrAry,i,jstrValue);env->DeleteLocalRef(jstrValue);jstring jstrUpdate = (jstring)env->GetObjectArrayElement(jstrAry,i);const char* strcUpdate = env->GetStringUTFChars(jstrUpdate,NULL);LOGD("修改值: index=%d,strValue=%s",i,strcUpdate);env->ReleaseStringUTFChars(jstrUpdate,strcUpdate);}return jstrAry;
}

JNI对象操作

JNI操作对象

package com.example.ndk_study_base;import android.util.Log;public class Student {private String name;private int age;private char sex;public String getName() {Log.d("Student", "getName:run ");return name;}public void setName(String name) {this.name = name;}public int getAge() {return age;}public void setAge(int age) {this.age = age;}public char getSex() {return sex;}public void setSex(char sex) {this.sex = sex;}@Overridepublic String toString() {return "com.example.ndk_study_base.Student{" +"name='" + name + '\'' +", age=" + age +", sex=" + sex +'}';}
}

声明Native函数:

    public native void senderStudentToJNI(Student stu,String name);public native Student newStudentFromJNI();

调用Native函数:

        Student stu = new Student();stu.setName("娃哈哈");stu.setAge(18);stu.setSex('M');senderStudentToJNI(stu,"奥利给给");stu.getName();Student stu2 = newStudentFromJNI();Log.d("JavaLog", "RunJNIObjectCode: " + stu2.toString());

实现Native函数:

extern "C"
JNIEXPORT void JNICALL
Java_com_example_ndk_1study_1base_MainActivity_senderStudentToJNI(JNIEnv *env,jobject thiz,jobject stu,jstring name) {//一,修改Student name属性//1.获取类引用//方式1:获取jclassjclass stuCls = env->GetObjectClass(stu);//2.获取属性idjfieldID stuNameFieldId = env->GetFieldID(stuCls,"name", "Ljava/lang/String;");//3.获取属性jstring jstrName = (jstring)env->GetObjectField(stu,stuNameFieldId);//获取const char* cstrName = env->GetStringUTFChars(jstrName,NULL);//打印LOGD("student name=%s",cstrName);//释放env->ReleaseStringUTFChars(jstrName,cstrName);//4.修改属性env->SetObjectField(stu,stuNameFieldId,name);//二,调用Student func方法jmethodID getNameMethodId = env->GetMethodID(stuCls,"getName", "()Ljava/lang/String;");jstring jstrRet = (jstring)env->CallObjectMethod(stu,getNameMethodId);//调用返回类型是object的方法API//获取const char* cstrNameRet = env->GetStringUTFChars(jstrRet,NULL);//打印LOGD("student cstrNameRet=%s",cstrNameRet);//释放env->ReleaseStringUTFChars(jstrRet,cstrNameRet);env->DeleteLocalRef(stuCls);
}extern "C"
JNIEXPORT jobject JNICALL
Java_com_example_ndk_1study_1base_MainActivity_newStudentFromJNI(JNIEnv *env,jobject thiz) {//方式2:获取jclassjclass stuCls = env->FindClass("com/example/ndk_study_base/Student");//开辟对象控件jobject jstuObj = env->AllocObject(stuCls);//获取方法idjmethodID setNameMethodId = env->GetMethodID(stuCls,"setName", "(Ljava/lang/String;)V");jmethodID setAgeMethodId = env->GetMethodID(stuCls,"setAge", "(I)V");jmethodID setSexMethodId = env->GetMethodID(stuCls,"setSex", "(C)V");//参数-调用-释放jstring jstrName = env->NewStringUTF("JNIStudentLala");env->CallVoidMethod(jstuObj,setNameMethodId,jstrName);//调用返回值void的APIenv->DeleteLocalRef(jstrName);env->CallVoidMethod(jstuObj,setAgeMethodId,99);env->CallVoidMethod(jstuObj,setSexMethodId,'M');env->DeleteLocalRef(stuCls);return jstuObj;
}

JNI字符串

在这里插入图片描述

JNI引用问题

  • 局部引用
    Native方法作用域中创建的引用对象,返回的引用被称之为局部引用,只能在本Native方法以及调用Native方法的范围内使用,执行完毕离开作用域会被自动释放,没有被释放前,是不能被垃圾回收的,不能保存局部引用至全局【内存异常或系统崩溃】!
    **局部引用手动释放:**DeleteLocalRef
  • 全局引用
    NewGlobalRef唯一创建全局引用API【局部引用的区别】
    DeleteGlobalRef 释放全局引用
    都是需要手动创建和释放!

JNI动态注册

Java层: native方法声明

    public native void DynamicFromJNI();public native int DynamicIntFromJNI(int nums);public native String DynamicStrFromJNI();

C++层: JNI方法实现

extern "C" JNIEXPORT void DynamicFuncCall(){LOGD("DynamicFuncCall successes!");
}extern "C" JNIEXPORT int DynamicFuncIntCall(jint nums){LOGD("DynamicFuncIntCall successes!");return 200;
}extern "C" JNIEXPORT jstring DynamicFuncStrCall(JNIEnv* env,jobject obj){LOGD("DynamicFuncStrCall successes!");jstring jStr = env->NewStringUTF("哈哈");return jStr;
}

桥梁打通Native-》JNI方法:
加载库:Java
static {
System.loadLibrary(“ndk_study_dynamic”);
}
自动调:JNI
extern “C” JNIEXPORT jint JNI_OnLoad(JavaVM* javaVm,void* param)

....
// 日志输出
#include 
#define TAG "NDKLog"
// __VA_ARGS__ 代表...的可变参数
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__);
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__);
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__);JavaVM* jvm;extern "C" JNIEXPORT void DynamicFuncCall(){LOGD("DynamicFuncCall successes!");
}extern "C" JNIEXPORT int DynamicFuncIntCall(jint nums){LOGD("DynamicFuncIntCall successes!");return 200;
}extern "C" JNIEXPORT jstring DynamicFuncStrCall(JNIEnv* env,jobject obj){LOGD("DynamicFuncStrCall successes!");jstring jStr = env->NewStringUTF("哈哈");return jStr;
}//    typedef struct {
//        const char* name;
//        const char* signature;
//        void*       fnPtr;
//    } JNINativeMethod;//(void*)DynamicFuncCall void不是返回值类型,就是一个函数强转void*指针类型//你写:int*,char*,void*都是可以赋值给void,但是直接将函数赋值给void*是不可以的,需要指针类型强制转换!
//void* pVoid = (void*)DynamicFuncIntCall;
//将Java Native方法和JNI方法进行绑定:
static const JNINativeMethod jniNativeMethods[] = {{"DynamicFromJNI","()V",(void*)DynamicFuncCall},{"DynamicIntFromJNI","(I)I",(void*)DynamicFuncIntCall},{"DynamicStrFromJNI","()Ljava/lang/String;",(void*)DynamicFuncStrCall}
};extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM* javaVm,void* param){//保存:jvm指针::jvm = javaVm;//获取JNIEnvJNIEnv* env = nullptr;jint jRet = javaVm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6);if(jRet != JNI_OK){return -1;}LOGD("JNI_OnLoad init successes!");//注册绑定
//    jint RegisterNatives(jclass clazz, const JNINativeMethod* methods,
//                         jint nMethods)jclass mainActiveCls = env->FindClass("com/example/ndk_study_dynamic/MainActivity");env->RegisterNatives(mainActiveCls,jniNativeMethods,sizeof(jniNativeMethods)/sizeof(JNINativeMethod));//返回版本return JNI_VERSION_1_6;
}

总结:

  • 静态绑定优缺点
    • 方便使用,快速生成
    • 名称过长,暴露信息
    • 效率低于动态绑定一丢对
  • 动态绑定优缺点
    • JNI方法名可以任意起,只要进行绑定注册即可
    • 一开始进行所有JNI方法的注册

1.4-Ndk导库流程【第三方库】

CPU架构:

指令派系:

  • 精简指令集【RISC】
    Intel,AMD
  • 复杂指令集【CISC】
    IBM,ARM

常见架构:

x86,x86-64/x64
arm64-v8a,armeabi-v7a【32位】,armeabi【32位】

API-ABI:

  • API
    API【Application Program Interface 应用程序接口】其实就是一套预先定义的,无需了解内部的实现机理和细节,与硬件系统无关的一套调用接口!
  • ABI
    ABI【Application Binary Interface 应用程序二进制接口】定义了一套二进制规则,它允许在所有兼容该ABI编译的目标代码的操作系统和硬件系统中,无须改动即可运行,从这里我们就看出,ABI其实就是一套约束底层,系统,硬件,编译规则的一套二进制接口!
    【EABI : 是 arm 对于 ABI规范的较新(2005年)的实现】

配置支持ABI

大厂适配ABI文章:https://mp.weixin.qq.com/s/jnZpgaRFQT5ULk9tHWMAGg

module模块: build.gradle

    defaultConfig {applicationId "com.example.ndkdemo"minSdk 21targetSdk 31versionCode 1versionName "1.0"testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"externalNativeBuild {cmake {cppFlags ''
//                方法1:externalNativeBuild-》cmake:
//                abiFilters 'armeabi-v7a'}}
//        方法2:defaultConfig-》ndk:ndk{abiFilters 'armeabi-v7a','arm64-v8a','x86'}}

配置输出目录

默认输出目录:
在这里插入图片描述

#不同Android Studio版本有的生效有的不生效
#方式1:
#set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/../jniLibs/${CMAKE_ANDROID_ARCH_ABI})
#方式2:
#set(LIBRARY_OUTPUT_PATH ${CMAKE_CURRENT_SOURCE_DIR}/../jniLibs/${CMAKE_ANDROID_ARCH_ABI})

引用外部库流程:

cmake_minimum_required(VERSION 3.10.2) #cmake 最低版本号# Declares and names the project.project("ndk_study_fmod") #项目工程名#以音频音效库fmod作为Demo介绍导库流程:#一,导入头文件所在目录
#解释:include_directories 默认路径:CMakeLists所在目录,inc与它同级所以相对目录可以直接写
include_directories(inc) #inc我们放在了CMakeLists同级目录下
#二,导入库文件所在目录【将库文件所在目录配置到环境变量中,找库会自动去环境变量中配置的目录中寻找】
#解释:set 赋值【环境变量赋值】
#先获取环境变量中已有的目录,拼接上我们的目录,重新赋值给环境变量
#fmod库文件:放在了与cpp同级的jniLibs目录下:
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${CMAKE_CURRENT_SOURCE_DIR}/../jniLibs/${CMAKE_ANDROID_ARCH_ABI}") #定义一个变量allCpp 代表所有的.c,.h,.cpp
file(GLOB allCpp *.c *.h *.cpp)add_library( # Sets the name of the library.ndk_study_fmod #库名称 libndk_study_fmod.so# Sets the library as a shared library.SHARED #库类型 -> .a 还是 .so# Provides a relative path to your source file(s).#native-lib.cpp #源文件 通用写法:${allCpp})#查找库 并起一个别名 目的:查找一次,缓存下来,避免反复查找
find_library( # Sets the name of the path variable.log-liblog)#链接库:环境变量中配置的目录中,按照libxxx.so libxxx.a去找对应库,链接到一起
target_link_libraries( # Specifies the target library.ndk_study_fmod xxx库 #只需要写库名称,不需要写lib,.so前后缀,它会自动添加匹配对应的库# Links the target library to the log library# included in the NDK.${log-lib})#总结:导入库流程
#一,项目工程添加头文件,添加库文件#【.jar放在libs里面,动态库放在cpp同级目录,创建一个jniLibs目录,这个目录是gradle默认寻找的目录】
#二,设置头文件所在目录
#三,设置库文件所在目录-》环境变量-》追加方式不是覆盖
#四,链接库-》只写库名称省略前缀lib和后缀库类型.so


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部