日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網(wǎng)為廣大站長提供免費收錄網(wǎng)站服務(wù),提交前請做好本站友鏈:【 網(wǎng)站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(wù)(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網(wǎng)站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

JAVA調(diào)用C/C++在Java語言里面本來就有的,并非Android獨有的,即JNI。JNI就是Java調(diào)用C++的規(guī)范。

JNI 概述

JNI,全稱為Java Native Interface,即Java本地接口,JNI是Java調(diào)用Native語言的一種特性,通過JNI可以使JAVA和 C/C++進(jìn)行交互。

Java語言是跨平臺的語言,而這跨平臺的背后都是依靠Java虛擬機(jī),虛擬機(jī)采用C/C++編寫,適配各個系統(tǒng),通過JNI為上層Java提供各種服務(wù),保證跨平臺性。

在Java語言出現(xiàn)前,就有很多程序和庫都是由Native語言寫的,如果想重復(fù)利用這些庫,就可以所使用JNI來實現(xiàn)。在Android平臺上,JNI就是一座將Java世界和Native世界聯(lián)通的一座橋梁。

Android NDK-深入理解JNI

 

jni.png

通過JNI,Java世界和Native世界的代碼就可以相互訪問了。JNI實例:Camera

最新有在看系統(tǒng)的Camera相關(guān),所以從系統(tǒng)Camera角度來分析下JNI的應(yīng)用,下面講的實例基于Camera2

Android5.0(21)之后android.hardware.Camera就被廢棄了,取而代之的是全新的android.hardware.Camera2

相關(guān)代碼:

frameworks/base/core/jni/AndroidRuntime.cpp
frameworks/base/core/java/android/hardware/camera2/impl/CameraMetadataNative.java
frameworks/base/core/jni/android_hardware_camera2_CameraMetadata.cpp

Camera2 Java層對應(yīng)的是CameraMetadataNative.java,Native層對應(yīng)的是android_hardware_camera2_CameraMetadata.cpp

Java層CameraMetadataNative

相關(guān)代碼在CameraMetadataNative.java

Camera2使用CameraManager(攝像頭管理器)進(jìn)行控制,CameraManager具體的操作會通過CameraMetadataNative來執(zhí)行。

CameraMetadataNative的初始化

public class CameraMetadataNative implements Parcelable
 static {
 /*
 * We use a class initializer to allow the native code to cache some field offsets
 */
 nativeClassInit();
 registerAllMarshalers();
 }
 private static native void nativeClassInit();
}

靜態(tài)方法初始化調(diào)用了Native層的方法nativeClassInit,這個方法對應(yīng)的Native層具體實現(xiàn),是在android_hardware_camera2_CameraMetadata.cpp

Native層CameraMetadata

Native層相關(guān)代碼在android_hardware_camera2_CameraMetadata.cpp

Native方法初始化

static const JNINativeMethod gCameraMetadataMethods[] = {
// static methods
 { "nativeClassInit",
 "()V",
 (void *)CameraMetadata_classInit }, //和Java層nativeClassInit()對應(yīng)
 { "nativeGetAllVendorKeys",
 "(Ljava/lang/Class;)Ljava/util/ArrayList;",
 (void *)CameraMetadata_getAllVendorKeys},
 { "nativeGetTagFromKey",
 "(Ljava/lang/String;)I",
 (void *)CameraMetadata_getTagFromKey },
 { "nativeGetTypeFromTag",
 "(I)I",
 (void *)CameraMetadata_getTypeFromTag },
 { "nativeSetupGlobalVendorTagDescriptor",
 "()I",
 (void*)CameraMetadata_setupGlobalVendorTagDescriptor },
// instance methods
 { "nativeAllocate",
 "()J",
 (void*)CameraMetadata_allocate },
 { "nativeAllocateCopy",
 "(L" CAMERA_METADATA_CLASS_NAME ";)J",
 (void *)CameraMetadata_allocateCopy },
 { "nativeIsEmpty",
 "()Z",
 (void*)CameraMetadata_isEmpty },
 { "nativeGetEntryCount",
 "()I",
 (void*)CameraMetadata_getEntryCount },
 { "nativeClose",
 "()V",
 (void*)CameraMetadata_close },
 { "nativeSwap",
 "(L" CAMERA_METADATA_CLASS_NAME ";)V",
 (void *)CameraMetadata_swap },
 { "nativeReadValues",
 "(I)[B",
 (void *)CameraMetadata_readValues },
 { "nativeWriteValues",
 "(I[B)V",
 (void *)CameraMetadata_writeValues },
 { "nativeDump",
 "()V",
 (void *)CameraMetadata_dump },
// Parcelable interface
 { "nativeReadFromParcel",
 "(Landroid/os/Parcel;)V",
 (void *)CameraMetadata_readFromParcel },
 { "nativeWriteToParcel",
 "(Landroid/os/Parcel;)V",
 (void *)CameraMetadata_writeToParcel },
};

gCameraMetadataMethods什么時候會被加載?

int register_android_hardware_camera2_CameraMetadata(JNIEnv *env)
{
 ......
 // Register native functions
 return RegisterMethodsOrDie(env,
 CAMERA_METADATA_CLASS_NAME,
 gCameraMetadataMethods,
 NELEM(gCameraMetadataMethods));
}
......
static inline int RegisterMethodsOrDie(JNIEnv* env, const char* className,
 const JNINativeMethod* gMethods, int numMethods) {
 int res = AndroidRuntime::registerNativeMethods(env, className, gMethods, numMethods);
 LOG_ALWAYS_FATAL_IF(res < 0, "Unable to register native methods.");
 return res;
}

register_android_hardware_camera2_CameraMetadata何時會被調(diào)用到,這個就需要了解下JNI的查找方式。

JNI查找方式

Android系統(tǒng)在啟動啟動過程中,先啟動Kernel創(chuàng)建init進(jìn)程,緊接著由init進(jìn)程fork第一個橫穿Java和C/C++的進(jìn)程,即Zygote進(jìn)程。Zygote啟動過程中會AndroidRuntime.cpp中的startVm創(chuàng)建虛擬機(jī),VM創(chuàng)建完成后,緊接著調(diào)用startReg完成虛擬機(jī)中的JNI方法注冊。

剛才CameraMetadata中register_android_hardware_camera2_CameraMetadata方法,在AndroidRuntime.cpp的聲明:

extern int register_android_hardware_camera2_CameraMetadata(JNIEnv *env);

然后在gRegJNI中的靜態(tài)聲明

static const RegJNIRec gRegJNI[] = {
 ......
 REG_JNI(register_android_hardware_camera2_CameraMetadata),
 ......
}

gRegJNI方法在startReg中被調(diào)用

/*static*/ int AndroidRuntime::startReg(JNIEnv* env)
{
 ATRACE_NAME("RegisterAndroidNatives");
 androidSetCreateThreadFunc((android_create_thread_fn) javaCreateThreadEtc);
 ALOGV("--- registering native functions ---n");
 env->PushLocalFrame(200);
 if (register_jni_procs(gRegJNI, NELEM(gRegJNI), env) < 0) {
 env->PopLocalFrame(NULL);
 return -1;
 }
 env->PopLocalFrame(NULL);
 //createJavaThread("fubar", quickTest, (void*) "hello");
 return 0;
}

register_jni_procs(gRegJNI, NELEM(gRegJNI), env)會循環(huán)調(diào)用gRegJNI數(shù)組成員所對應(yīng)的方法

static int register_jni_procs(const RegJNIRec array[], size_t count, JNIEnv* env)
{
 for (size_t i = 0; i < count; i++) {
 if (array[i].mProc(env) < 0) {
#ifndef NDEBUG
 ALOGD("----------!!! %s failed to loadn", array[i].mName);
#endif
 return -1;
 }
 }
 return 0;
}

這樣android_hardware_camera2_CameraMetadata.cpp中的int register_android_hardware_camera2_CameraMetadata(JNIEnv *env)就會被調(diào)用到。

除了這種Android系統(tǒng)啟動時,就注冊JNI所對應(yīng)的方法。還有一種就是程序自定義的JNI方法,以 MediePlay 為例:

相關(guān)代碼路徑

frameworks/base/media/java/android/media/MediaPlayer.java
frameworks/base/media/jni/android_media_MediaPlayer.cpp

MediaPlayer聲明:

public class MediaPlayer extends PlayerBase
 implements SubtitleController.Listener
{
 ......
 private static native final void native_init();
 ......
 static {
 System.loadLibrary("media_jni");
 native_init();
 }
}

靜態(tài)代碼塊中使用System.loadLibrary加載動態(tài)庫,media_jni在Android平臺對應(yīng)的是libmedia_jni.so庫。

在jni目錄/frameworks/base/media/jni/Android.mk中有相應(yīng)的聲明:

LOCAL_SRC_FILES:= 
android_media_MediaPlayer.cpp 
...
LOCAL_MODULE:= libmedia_jni

在android_media_MediaPlayer.cpp找到對應(yīng)的Native(natvie_init)方法:

static void
android_media_MediaPlayer_native_init(JNIEnv *env)
{
 jclass clazz;
 clazz = env->FindClass("android/media/MediaPlayer");
 if (clazz == NULL) {
 return;
 }
 ......
}

JNI注冊的方法就是上面描述的兩種方法:

  • 在Android系統(tǒng)啟動時注冊,在AndroidRuntime.cpp中的gRegJNI方法中聲明
  • 使用System.loadLibrary()方式注冊

JNI基礎(chǔ)

上面一節(jié)主要描述了系統(tǒng)中Java層和Native層交互和實現(xiàn),并沒有對JNI的基礎(chǔ)理論,流程進(jìn)行分析

JNI命名規(guī)則

JNI方法名規(guī)范 :

返回值 + Java前綴 + 全路徑類名 + 方法名 + 參數(shù)① JNIEnv + 參數(shù)② jobject + 其它參數(shù)

簡單的一個例子,返回一個字符串

extern "C" JNIEXPORT jstring JNICALL
Java_com_yeungeek_jnisample_NativeHelper_stringFromJNI(JNIEnv *env, jclass jclass1) {
 LOGD("##### from c");
 return env->NewStringUTF("Hello JNI");
}
  • 返回值:jstring
  • 全路徑類名:com_yeungeek_jnisample_NativeHelper
  • 方法名:stringFromJNI

JNI開發(fā)流程

  • 在Java中先聲明一個native方法
  • 編譯Java源文件javac得到.class文件
  • 通過javah -jni命令導(dǎo)出JNI的.h頭文件
  • 使用Java需要交互的本地代碼,實現(xiàn)在Java中聲明的Native方法(如果Java需要與C++交互,那么就用C++實現(xiàn)Java的Native方法。)
  • 將本地代碼編譯成動態(tài)庫(windows系統(tǒng)下是.dll文件,如果是linux系統(tǒng)下是.so文件,如果是mac系統(tǒng)下是.jnilib)
  • 通過Java命令執(zhí)行Java程序,最終實現(xiàn)Java調(diào)用本地代碼。

數(shù)據(jù)類型

基本數(shù)據(jù)類型

Android NDK-深入理解JNI

 

引用數(shù)據(jù)類型

Android NDK-深入理解JNI

 

方法簽名

JNI的方法簽名的格式:

(參數(shù)簽名格式...)返回值簽名格式

demo的native 方法:

public static native java.lang.String stringFromJNI();

可以通過javap命令生成方法簽名``:

()Ljava/lang/String;

JNI原理

Java語言的執(zhí)行環(huán)境是Java虛擬機(jī)(JVM),JVM其實是主機(jī)環(huán)境中的一個進(jìn)程,每個JVM虛擬機(jī)都在本地環(huán)境中有一個JavaVM結(jié)構(gòu)體,該結(jié)構(gòu)體在創(chuàng)建Java虛擬機(jī)時被返回,在JNI環(huán)境中創(chuàng)建JVM的函數(shù)為JNI_CreateJavaVM。

JNI 定義了兩個關(guān)鍵數(shù)據(jù)結(jié)構(gòu),即“JavaVM”和“JNIEnv”,兩者本質(zhì)上都是指向函數(shù)表的二級指針。

JavaVM

JavaVM是Java虛擬機(jī)在JNI層的代表,JavaVM 提供了“調(diào)用接口”函數(shù),您可以利用此類函數(shù)創(chuàng)建和銷毀 JavaVM。理論上,每個進(jìn)程可以包含多個JavaVM,但AnAndroid只允許每個進(jìn)程包含一個JavaVM。

JNIEnv

JNIEnv是一個線程相關(guān)的結(jié)構(gòu)體,該結(jié)構(gòu)體代表了Java在本線程的執(zhí)行環(huán)境。JNIEnv 提供了大多數(shù) JNI 函數(shù)。您的原生函數(shù)均會接收 JNIEnv 作為第一個參數(shù)。

JNIEnv作用:

  • 調(diào)用Java函數(shù)
  • 操作Java代碼

JNIEnv定義(jni.h):

libnativehelper/include/nativehelper/jni.h

#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM; 
#else
typedef const struct JNINativeInterface* JNIEnv;
typedef const struct JNIInvokeInterface* JavaVM;
#endif

定義中可以看到JavaVM,Android中一個進(jìn)程只會有一個JavaVM,一個JVM對應(yīng)一個JavaVM結(jié)構(gòu),而一個JVM中可能創(chuàng)建多個Java線程,每個線程對應(yīng)一個JNIEnv結(jié)構(gòu)

Android NDK-深入理解JNI

 

javavm.png

注冊JNI函數(shù)

Java世界和Native世界的方法是如何關(guān)聯(lián)的,就是通過JNI函數(shù)注冊來實現(xiàn)。JNI函數(shù)注冊有兩種方式:

靜態(tài)注冊

這種方法就是通過函數(shù)名來找對應(yīng)的JNI函數(shù),可以通過javah命令行來生成JNI頭文件

javah com.yeungeek.jnisample.NativeHelper

生成對應(yīng)的com_yeungeek_jnisample_NativeHelper.h文件,生成對應(yīng)的JNI函數(shù),然后在實現(xiàn)這個函數(shù)就可以了

/*
 * Class: com_yeungeek_jnisample_NativeHelper
 * Method: stringFromJNI
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_yeungeek_jnisample_NativeHelper_stringFromJNI
 (JNIEnv *, jclass);

靜態(tài)注冊方法中,Native是如何找到對應(yīng)的JNI函數(shù),在JNI查找方式中介紹系統(tǒng)的流程,并沒有詳細(xì)說明靜態(tài)注冊的查找。這里簡單說明下這個過程(以上面的聲明為例子s):

當(dāng)Java調(diào)用native stringFromJNI函數(shù)時,會從對應(yīng)JNI庫中查找Java_com_yeungeek_jnisample_NativeHelper_stringFromJNI函數(shù),如果沒有找到,就會報錯。

靜態(tài)注冊方法,就是根據(jù)函數(shù)名來關(guān)聯(lián)Java函數(shù)和JNI函數(shù),JNI函數(shù)需要遵循特定的格式,這其中就有一些缺點:

  • 聲明了native方法的Java類,需要通過javah來生成頭文件
  • JNI函數(shù)名稱非常長
  • 第一次調(diào)用native函數(shù),需要通過函數(shù)名來搜索關(guān)聯(lián)對應(yīng)的JNI函數(shù),效率比較低

如何解決這些問題,讓native函數(shù),提前知道JNI函數(shù),就可以解決這個問題,這個過程就是動態(tài)注冊。

動態(tài)注冊

動態(tài)注冊在前面的Camera例子中,已經(jīng)有涉及到,JNI函數(shù)classInit的聲明。

static const JNINativeMethod gCameraMetadataMethods[] = {
// static methods
 { "nativeClassInit",
 "()V",
 (void *)CameraMetadata_classInit }, //和Java層nativeClassInit()對應(yīng)
 ......
}

JNI中有一種結(jié)構(gòu)用來記錄Java的Native方法和JNI方法的關(guān)聯(lián)關(guān)系,它就是JNINativeMethod,它在jni.h中被定義:

typedef struct {
 const char* name; //Java層native函數(shù)名
 const char* signature; //Java函數(shù)簽名,記錄參數(shù)類型和個數(shù),以及返回值類型
 void* fnPtr; //Native層對應(yīng)的函數(shù)指針
} JNINativeMethod;

在JNI查找方式說到,JNI注冊的兩種時間,第一種已經(jīng)介紹過了,我們自定義的native函數(shù),基本都是會使用System.loadLibrary(“xxx”),來進(jìn)行JNI函數(shù)的關(guān)聯(lián)。

loadLibrary(Android7.0)

public static void loadLibrary(String libname) {
 Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname);
}

調(diào)用到Runtime(libcore/ojluni/src/main/java/java/lang/Runtime.java)的loadLibrary0方法:

synchronized void loadLibrary0(ClassLoader loader, String libname) {
 ......
 String libraryName = libname;
 if (loader != null) {
 String filename = loader.findLibrary(libraryName);
 if (filename == null) {
 // It's not necessarily true that the ClassLoader used
 // System.mapLibraryName, but the default setup does, and it's
 // misleading to say we didn't find "libMyLibrary.so" when we
 // actually searched for "liblibMyLibrary.so.so".
 throw new UnsatisfiedLinkError(loader + " couldn't find "" +
 System.mapLibraryName(libraryName) + """);
 }
 //doLoad
 String error = doLoad(filename, loader);
 if (error != null) {
 throw new UnsatisfiedLinkError(error);
 }
 return;
 }
 //loader 為 null
 ......
 for (String directory : getLibPaths()) {
 String candidate = directory + filename;
 candidates.add(candidate);
 if (IoUtils.canOpenReadOnly(candidate)) {
 String error = doLoad(candidate, loader);
 if (error == null) {
 return; // We successfully loaded the library. Job done.
 }
 lastError = error;
 }
 }
 ......
}

doLoad

private String doLoad(String name, ClassLoader loader) {
 //調(diào)用 native 方法
 synchronized (this) {
 return nativeLoad(name, loader, librarySearchPath);
 }
}

nativeLoad

進(jìn)入到虛擬機(jī)代碼/libcore/ojluni/src/main/native/Runtime.c

JNIEXPORT jstring JNICALL
Runtime_nativeLoad(JNIEnv* env, jclass ignored, jstring javaFilename,
 jobject javaLoader, jstring javaLibrarySearchPath)
{
 return JVM_NativeLoad(env, javaFilename, javaLoader, javaLibrarySearchPath);
}

然后調(diào)用JVM_NativeLoad,JVM_NativeLoad方法申明在jvm.h中,實現(xiàn)在OpenjdkJvm.cc(/art/runtime/openjdkjvm/OpenjdkJvm.cc)

JNIEXPORT jstring JVM_NativeLoad(JNIEnv* env,
 jstring javaFilename,
 jobject javaLoader,
 jstring javaLibrarySearchPath) {
 ScopedUtfChars filename(env, javaFilename);
 if (filename.c_str() == NULL) {
 return NULL;
 }
 std::string error_msg;
 {
 art::JavaVMExt* vm = art::Runtime::Current()->GetJavaVM();
 bool success = vm->LoadNativeLibrary(env,
 filename.c_str(),
 javaLoader,
 javaLibrarySearchPath,
 &error_msg);
 if (success) {
 return nullptr;
 }
 }
 // Don't let a pending exception from JNI_OnLoad cause a CheckJNI issue with NewStringUTF.
 env->ExceptionClear();
 return env->NewStringUTF(error_msg.c_str());
}

LoadNativeLibrary

調(diào)用JavaVMExt的LoadNativeLibrary方法,方法在(art/runtime/java_vm_ext.cc)中,這個方法代碼非常多,選取主要的部分進(jìn)行分析

bool JavaVMExt::LoadNativeLibrary(JNIEnv* env,
 const std::string& path,
 jobject class_loader,
 jstring library_path,
 std::string* error_msg) {
 ......
 bool was_successful = false;
 //加載so庫中查找JNI_OnLoad方法,如果沒有系統(tǒng)就認(rèn)為是靜態(tài)注冊方式進(jìn)行的,直接返回true,代表so庫加載成功,
 //如果找到JNI_OnLoad就會調(diào)用JNI_OnLoad方法,JNI_OnLoad方法中一般存放的是方法注冊的函數(shù),
 //所以如果采用動態(tài)注冊就必須要實現(xiàn)JNI_OnLoad方法,否則調(diào)用java中申明的native方法時會拋出異常
 void* sym = library->FindSymbol("JNI_OnLoad", nullptr);
 if (sym == nullptr) {
 VLOG(jni) << "[No JNI_OnLoad found in "" << path << ""]";
 was_successful = true;
 } else {
 // Call JNI_OnLoad. We have to override the current class
 // loader, which will always be "null" since the stuff at the
 // top of the stack is around Runtime.loadLibrary(). (See
 // the comments in the JNI FindClass function.)
 ScopedLocalRef<jobject> old_class_loader(env, env->NewLocalRef(self->GetClassLoaderOverride()));
 self->SetClassLoaderOverride(class_loader);
 VLOG(jni) << "[Calling JNI_OnLoad in "" << path << ""]";
 typedef int (*JNI_OnLoadFn)(JavaVM*, void*);
 JNI_OnLoadFn jni_on_load = reinterpret_cast<JNI_OnLoadFn>(sym);
 //調(diào)用JNI_OnLoad方法
 int version = (*jni_on_load)(this, nullptr);
 if (runtime_->GetTargetSdkVersion() != 0 && runtime_->GetTargetSdkVersion() <= 21) {
 // Make sure that sigchain owns SIGSEGV.
 EnsureFrontOfChain(SIGSEGV);
 }
 self->SetClassLoaderOverride(old_class_loader.get());
 }
 ......
}

代碼里的主要邏輯:

  • 加載so庫中查找JNI_OnLoad方法,如果沒有系統(tǒng)就認(rèn)為是靜態(tài)注冊方式進(jìn)行的,直接返回true,代表so庫加載成功
  • 如果找到JNI_OnLoad就會調(diào)用JNI_OnLoad方法,JNI_OnLoad方法中一般存放的是方法注冊的函數(shù)
  • 所以如果采用動態(tài)注冊就必須要實現(xiàn)JNI_OnLoad方法,否則調(diào)用Java中的native方法時會拋出異常

jclass、jmethodID和jfieldID

如果要通過原生代碼訪問對象的字段,需要執(zhí)行以下操作:

  1. 使用 FindClass 獲取類的類對象引用
  2. 使用 GetFieldID 獲取字段的字段 ID
  3. 使用適當(dāng)內(nèi)容獲取字段的內(nèi)容,例如 GetIntField

具體的使用,放在第二篇文章中講解

JNI的引用

JNI規(guī)范中定義了三種引用:

  • 局部引用(Local Reference)
  • 全局引用(Global Reference)
  • 弱全局引用(Weak Global Reference)

局部引用

也叫本地引用,在 JNI層函數(shù)使用的非全局引用對象都是Local Reference,最大的特點就是,JNI 函數(shù)返回后,這些聲明的引用可能就會被垃圾回收

全局引用

這種聲明的對象,不會主動釋放資源,不會被垃圾回收

弱全局引用

一種特殊的全局引用,在運行過程中可能被回收,使用之前需要判斷下是否為空

參考

  • Android:清晰講解JNI 與 NDK(含實例教學(xué))
  • Android JNI學(xué)習(xí)
  • Android JNI原理分析
  • Android深入理解JNI(一)JNI原理與靜態(tài)、動態(tài)注冊
  • JNI Tips

分享到:
標(biāo)簽:Android NDK
用戶無頭像

網(wǎng)友整理

注冊時間:

網(wǎng)站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網(wǎng)站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網(wǎng)站吧!
最新入駐小程序

數(shù)獨大挑戰(zhàn)2018-06-03

數(shù)獨一種數(shù)學(xué)游戲,玩家需要根據(jù)9

答題星2018-06-03

您可以通過答題星輕松地創(chuàng)建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學(xué)四六

運動步數(shù)有氧達(dá)人2018-06-03

記錄運動步數(shù),積累氧氣值。還可偷

每日養(yǎng)生app2018-06-03

每日養(yǎng)生,天天健康

體育訓(xùn)練成績評定2018-06-03

通用課目體育訓(xùn)練成績評定