Android跨進程通信IPC之3——關于"JNI"的那些事

Android跨進程通信IPC整體內容如下

在分析IPC基于Android 6.0)的過程中,里面的核心部分是Native的,并且還包含一些linux kernel,而作為Android開發者看到的代碼大部分都是Java層,所以這就一定會存在Java與C/C++代碼的來回跳轉,那么久很有必要來先說一下JNI,本文主要內容如下:

  • 1、相關代碼
  • 2、JNI簡介
  • 3、Android中應用程序框架
  • 4、JNI查找方式
  • 5、loadLibrary源碼分析
  • 6、JNI資源
  • 7、總結

一、相關代碼

(一)、代碼位置

frameworks/base/core/jni/AndroidRuntime.cpp

libcore/luni/src/main/java/java/lang/System.java
libcore/luni/src/main/java/java/lang/Runtime.java
libnativehelper/JNIHelp.cpp
libnativehelper/include/nativehelper/jni.h

frameworks/base/core/java/android/os/MessageQueue.java
frameworks/base/core/jni/android_os_MessageQueue.cpp

frameworks/base/core/java/android/os/Binder.java
frameworks/base/core/jni/android_util_Binder.cpp

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

(二)、代碼鏈接

二、JNI簡介

(一)、JNI介紹

JNI(Java Native Interface,Java本地接口),用于打通Java層與Native(C/C++)層。這不是Android系統獨有的,而是Java所有。眾所周知,Java語言是是跨平臺的語言,而這跨平臺的背后都是一開Java虛擬機,虛擬機采用C/C++編寫,適配各個系統,通過JNI為上層Java提供各種服務,保證跨平臺性。

其實不少的Java的程序員,享受著其跨平臺性,可能全然不知JNI的存在。在Android平臺,讓JNI大放異彩,為更多的程序員所數值,往往為了提供效率或者其他功能需求,就需要在NDK上開發。本文的主要目的是介紹android上層中Java與Native的紐帶JNI。

JNI.png

(二)、Java/JNI/C的關系

1、C與Java的側重
  • C語言:C語言中重要的是函數 fuction
  • Java語言:Java中最重要的是JVM,class類,以及class中的方法
2、C與Java的"面向"
  • C語言:是面向函數的語言
  • Java語言:是面向對象的語言
3、C與Java如何交流
  • JNI規范:C語言與Java語言交流需要一個適配器,中間件,即JNI,JNI提供一共規范
  • C語言中調用Java的方法:可以讓我們在C代碼中找到Java代碼class的方法,并且調用該方法
  • Java語言中調用C語言方法:同時也可以在Java代碼中,將一個C語言的方法映射到Java的某個方法上
  • JNI的橋梁作用:JNI提供了一個橋梁,打通了C語言和Java語言的之間的障礙。
4、JNI中的一些概念
  • natvie:Java語言中修飾本地方法的修飾符(也可以理解為關鍵字),被該修飾符修飾的方法沒有方法體
  • Native方法:在Java語言中被native關鍵字修飾的方法是Native方法
  • JNI層:Java聲明Native方法的部分
  • JNI函數:JNIEnv提供的函數,這些函數在jni.h中進行定義
  • JNI方法:Native方法對應JNI實現的C/C++方法,即在jni目錄中實現的那些C語言代碼
5、JNI接口函數和指針

平臺相關代碼是通過調用JNI函數來訪問Java虛擬機功能的。JNI函數可以通過接口指針來獲得。接口指針是指針的指針,它指向一個指針數組,而指針數組中的每個元素又指向一個接口函數。每個接口函數都處在數組的某個預定偏移量中。下圖說明了接口指針的組織結構。

接口指針.png

JNI接口的組織類似于C++虛擬函數表或COM接口。使用接口表而不實用硬性編入的函數表的好處是使JNI名字空間與平臺代碼分開。虛擬機可以很容易地提供了多個版本的JNI寒暑表。例如,虛擬機 可以支持以下兩個JNI函數表:

  • 一個表對非法參數進行全面檢查,適用于調試程序
  • 另一個表只進行JNI規范所要求的最小程度的檢查,因此效率較高。

JNI接口指針只在當前線程中有效。因此,本地方法不能講接口指針從一個線程傳遞到另一個線程中。實現JNI虛擬機可能將本地線程的數據分配和儲存在JNI接口指針所指向區域中。本地方法將JNI接口指針當做參數來接受。虛擬機在從相同的Java線程中對本地方法進行多次調用時,保證傳遞給本地方法的接口指針是相同的。但是,一個本地方方可以被不同的Java線程所調用,因此可以接受不同的JNI接口指針。

本地方法將JNI接口指針當參數來接受。虛擬機在從相同的Java線程對本地方法進行多次調用時,保證傳遞給本地方法的接口指針是相同的。但是,一個本地方法可被不同的Java線程所調用,因此可以接受不同的JNI接口指針。

image.png
6、JavaVM和JNIEnv

####### (1)、JavaVM

代表Java虛擬機。所有的工作都是從獲取虛擬機接口開始的。有兩種方式:第一種方式,在加載動態鏈接庫時,JVM會調用JNI_OnLoad(JavaVM * jvm, void * reserved)(如果定了該函數)。第一個參數會傳入JavaVM指針;第二種方式,在native_code中調用JNI_CreateJavaVM(&jvm,(void*)&env,&vm_args) 可以得到JavaVM指針。兩種方式都可以用全局變量,比如JavaVM * g_jvm來保存獲取到的指針以便在任意上下文中使用。Android系統是利用第二種方式Invocation interface來創建JVM

####### (2)、JNIEnv

JNIEnv,即JNIEnvironment;字面意思就是JNI環境。其實它是一個與線程相關的JNI環境結構體。所以JNIEnv類型實際代表了Java環境。通過這個JNIEnv*指針,就可以對Java端代碼進行操作。與線程相關,不同線程的JNIEnv相互獨立。 JNIEnv只在當前線程中有效。Native方法不能將JNIEnv從一個線程傳遞到另一個線程中。相同的Java線程對Native方法多次調用時,傳遞給Native方法的JNIEnv是相同的。但是,一個本地方法可能會被不同的Java線程調用,因此可以接受不同的JNIEnv。

和JNIEnv相比,JavaVM可以在進程中各個線程間共享。理論上一個進程可以有多個JavaVM,但Android只允許一個JavaVM。需要強調在Android SDK中強調了額 " do not cache JNIEnv * ",要用的時候在不同的線程中通過JavaVM * jvm的方法來獲取與當前線程相關的JNIEnv *。

在Java里,每一個一個process可以產生多個JavaVM對象,但是在android上,每一個process只有一個Dalvik虛擬機對象,也就是在android進程中是通過有且只有一個虛擬機對象來服務所有Java和C/C++代碼。Java的dex字節碼和C/C++的 xxx.so 同時運行Dalvik虛擬機之內,共同使用一個進程空間。之所以可以相互調用,也是因為有Dalvik虛擬機。當Java代碼需要C/C++代碼時,Dalvik虛擬機加載xxx.so庫時,會先調用JNI_Onload(),此時會把Java對象的指針存儲于C層JNI組件的全局環境中,在Java層調用C層的Native函數時,調用Native函數線程必然通過Dalvik虛擬機來調用C層的Native函數。此時,虛擬機會為Native的C組件是實例化一個JNIEnv指針,該指針指向Dalvik虛擬機的具體函數列表,當JNI的C組件調用Java層的方法或屬性時,需要JNIEnv指針來進行調用。當本地C/C++想獲的當前線程所要使用的JNIEnv時,可以使用Dalvik虛擬機對象的JavaVM * jvm—>GetEnv()返回當前線程所在的JNIEnv*。

三、Android中應用程序框架

(一)、正常情況下的Android框架

最頂層是** Android應用程序代碼 **,上層的 ** 應用層 ** 和 ** 應用框架層 ** 主要是Java代碼,中間有一層的 ** Framework框架層代碼 ** 是C/C++代碼,通過Framework進行系統調用,調用底層的庫和 Linux內核

正常情況下的Android框架

(二)、使用JNI的Android框架

使用JNI時的Android框架:繞過Framework提供的底層代碼,直接調用自己的寫的C代碼,該代碼最終會編譯成一個庫,這個庫通過JNI提供的一個Stable的ABI 調用Linux kernel,ABI是二進制程序接口 application binary interface

使用JNI的Android框架.png

(三)、Android框架中的JNI

1、紐帶

JNI是連接框架層(Framework - C/C++) 和應用框架層(Application Framework - Java )的紐帶

2、JNI在Android中的作用

JNI可以調用本地代碼庫(即C/C++代碼),并通過Dalvik虛擬機與應用層和應用框架層進行交互,Android 中的JNI主要位于應用層和應用框架層之間

  • 應用層:該層是由JNI開發,主要使用標準的JNI編程模型
  • 應用框架層: 使用的是Android中自定義的一套JNI編程模型,該自定義的JNI編程模型彌補了標準的JNI編程模型的不足
3、NDK與JNI區別:
  • NDK:NDK是Google開發的一套開發和編譯工具集,主要用于AndroidJNI開發
  • JNI:JNI十套編程接口,用來實現Java代碼與本地C/C++代碼進行交互

四、JNI查找方式

Android系統在啟動的過程中,先啟動Kernel創建init進程,緊接著由init進程fork第一個橫穿Java和C/C++的進程,即Zygote進程。Zygote啟動過程會在 AndroidRuntime.cpp 中的startVM創建虛擬機,VM創建完成后,緊接著調用 startReg 完成虛擬機中的JNI方法注冊。

(一)、startReg

// frameworks/base/core/jni/AndroidRuntime.cpp      1440行
/*
 * Register android native functions with the VM.
 * 在虛擬機上注冊Android的native方法
 */
 int AndroidRuntime::startReg(JNIEnv*env) {
    /*
     * This hook causes all future threads created in this process to be
     * attached to the JavaVM.  (This needs to go away in favor of JNI
     * Attach calls.)
     * 此鉤子將導致在此過程中創建的所有未來線程 附加到JavaVM。 (這需要消除對
     * JNI的支持附加調用。)
     * 說白了就是設置線程的創建方法為 javaCreateThreadEtc
     */
        androidSetCreateThreadFunc((android_create_thread_fn) javaCreateThreadEtc);
        ALOGV("--- registering native functions ---\n");
    /*
     * Every "register" function calls one or more things that return
     * a local reference (e.g. FindClass).  Because we haven't really
     * started the VM yet, they're all getting stored in the base frame
     * and never released.  Use Push/Pop to manage the storage.
     * 每個“注冊”函數調用一個或多個返回的東西本地引用(例如FindClass)。
     * 因為我們沒有真的啟動虛擬機,它們都被存儲在基本框架中并沒有發布。 
     * 使用Push / Pop管理存儲。
     */
        env -> PushLocalFrame(200);
        // 進程JNI注冊函數
        if (register_jni_procs(gRegJNI, NELEM(gRegJNI), env) < 0) {
            env -> PopLocalFrame(NULL);
            return -1;
        }
        env -> PopLocalFrame(NULL);
        //createJavaThread("fubar", quickTest, (void*) "hello");
        return 0;
       }

startReg()函數里面調用了register_jni_procs()函數,這個函數是真正的注冊,那我們就來看下這個register_jni_procs()函數

1、register_jni_procs()
// frameworks/base/core/jni/AndroidRuntime.cpp      1283行
    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 load\n", array[i].mName)
            #endif
                return -1;
            }
        }
        return 0;
    }

發現上面的代碼很簡單,register_jni_procs(gRegJNI, NELEM(gRegJNI), env)函數的的作用就是循環調用gRegJNI數組成員所對應的方法

上面提到了gRegJNI數組,gRegJNI是什么,我們一起來看下

2、gRegJNI數組
// frameworks/base/core/jni/AndroidRuntime.cpp      1296行
static const RegJNIRec gRegJNI[] = {
   REG_JNI(register_com_android_internal_os_RuntimeInit),
    REG_JNI(register_android_os_SystemClock),
    REG_JNI(register_android_util_EventLog),
    REG_JNI(register_android_util_Log),
    REG_JNI(register_android_content_AssetManager),
    REG_JNI(register_android_content_StringBlock),
    REG_JNI(register_android_content_XmlBlock),
    REG_JNI(register_android_emoji_EmojiFactory),
    REG_JNI(register_android_text_AndroidCharacter),
    REG_JNI(register_android_text_StaticLayout),
    REG_JNI(register_android_text_AndroidBidi),
    REG_JNI(register_android_view_InputDevice),
    REG_JNI(register_android_view_KeyCharacterMap),
    REG_JNI(register_android_os_Process),
    REG_JNI(register_android_os_SystemProperties),
    REG_JNI(register_android_os_Binder),
    REG_JNI(register_android_os_Parcel),
    REG_JNI(register_android_nio_utils),
    REG_JNI(register_android_graphics_Graphics),
    REG_JNI(register_android_view_DisplayEventReceiver),
    REG_JNI(register_android_view_RenderNode),
    REG_JNI(register_android_view_RenderNodeAnimator),
    REG_JNI(register_android_view_GraphicBuffer),
    REG_JNI(register_android_view_DisplayListCanvas),
    REG_JNI(register_android_view_HardwareLayer),
    REG_JNI(register_android_view_ThreadedRenderer),
    REG_JNI(register_android_view_Surface),
    REG_JNI(register_android_view_SurfaceControl),
    REG_JNI(register_android_view_SurfaceSession),
    REG_JNI(register_android_view_TextureView),
    REG_JNI(register_com_android_internal_view_animation_NativeInterpolatorFactoryHelper),
    REG_JNI(register_com_google_android_gles_jni_EGLImpl),
    REG_JNI(register_com_google_android_gles_jni_GLImpl),
    REG_JNI(register_android_opengl_jni_EGL14),
    REG_JNI(register_android_opengl_jni_EGLExt),
    REG_JNI(register_android_opengl_jni_GLES10),
    REG_JNI(register_android_opengl_jni_GLES10Ext),
    REG_JNI(register_android_opengl_jni_GLES11),
    REG_JNI(register_android_opengl_jni_GLES11Ext),
    REG_JNI(register_android_opengl_jni_GLES20),
    REG_JNI(register_android_opengl_jni_GLES30),
    REG_JNI(register_android_opengl_jni_GLES31),
    REG_JNI(register_android_opengl_jni_GLES31Ext),

    REG_JNI(register_android_graphics_Bitmap),
    REG_JNI(register_android_graphics_BitmapFactory),
    REG_JNI(register_android_graphics_BitmapRegionDecoder),
    REG_JNI(register_android_graphics_Camera),
    REG_JNI(register_android_graphics_CreateJavaOutputStreamAdaptor),
    REG_JNI(register_android_graphics_Canvas),
    REG_JNI(register_android_graphics_CanvasProperty),
    REG_JNI(register_android_graphics_ColorFilter),
    REG_JNI(register_android_graphics_DrawFilter),
    REG_JNI(register_android_graphics_FontFamily),
    REG_JNI(register_android_graphics_Interpolator),
    REG_JNI(register_android_graphics_LayerRasterizer),
    REG_JNI(register_android_graphics_MaskFilter),
    REG_JNI(register_android_graphics_Matrix),
    REG_JNI(register_android_graphics_Movie),
    REG_JNI(register_android_graphics_NinePatch),
    REG_JNI(register_android_graphics_Paint),
    REG_JNI(register_android_graphics_Path),
    REG_JNI(register_android_graphics_PathMeasure),
    REG_JNI(register_android_graphics_PathEffect),
    REG_JNI(register_android_graphics_Picture),
    REG_JNI(register_android_graphics_PorterDuff),
    REG_JNI(register_android_graphics_Rasterizer),
    REG_JNI(register_android_graphics_Region),
    REG_JNI(register_android_graphics_Shader),
    REG_JNI(register_android_graphics_SurfaceTexture),
    REG_JNI(register_android_graphics_Typeface),
    REG_JNI(register_android_graphics_Xfermode),
    REG_JNI(register_android_graphics_YuvImage),
    REG_JNI(register_android_graphics_pdf_PdfDocument),
    REG_JNI(register_android_graphics_pdf_PdfEditor),
    REG_JNI(register_android_graphics_pdf_PdfRenderer),

    REG_JNI(register_android_database_CursorWindow),
    REG_JNI(register_android_database_SQLiteConnection),
    REG_JNI(register_android_database_SQLiteGlobal),
    REG_JNI(register_android_database_SQLiteDebug),
    REG_JNI(register_android_os_Debug),
    REG_JNI(register_android_os_FileObserver),
    REG_JNI(register_android_os_MessageQueue),
    REG_JNI(register_android_os_SELinux),
    REG_JNI(register_android_os_Trace),
    REG_JNI(register_android_os_UEventObserver),
    REG_JNI(register_android_net_LocalSocketImpl),
    REG_JNI(register_android_net_NetworkUtils),
    REG_JNI(register_android_net_TrafficStats),
    REG_JNI(register_android_os_MemoryFile),
    REG_JNI(register_com_android_internal_os_Zygote),
    REG_JNI(register_com_android_internal_util_VirtualRefBasePtr),
    REG_JNI(register_android_hardware_Camera),
    REG_JNI(register_android_hardware_camera2_CameraMetadata),
    REG_JNI(register_android_hardware_camera2_legacy_LegacyCameraDevice),
    REG_JNI(register_android_hardware_camera2_legacy_PerfMeasurement),
    REG_JNI(register_android_hardware_camera2_DngCreator),
    REG_JNI(register_android_hardware_Radio),
    REG_JNI(register_android_hardware_SensorManager),
    REG_JNI(register_android_hardware_SerialPort),
    REG_JNI(register_android_hardware_SoundTrigger),
    REG_JNI(register_android_hardware_UsbDevice),
    REG_JNI(register_android_hardware_UsbDeviceConnection),
    REG_JNI(register_android_hardware_UsbRequest),
    REG_JNI(register_android_hardware_location_ActivityRecognitionHardware),
    REG_JNI(register_android_media_AudioRecord),
    REG_JNI(register_android_media_AudioSystem),
    REG_JNI(register_android_media_AudioTrack),
    REG_JNI(register_android_media_JetPlayer),
    REG_JNI(register_android_media_RemoteDisplay),
    REG_JNI(register_android_media_ToneGenerator),

    REG_JNI(register_android_opengl_classes),
    REG_JNI(register_android_server_NetworkManagementSocketTagger),
    REG_JNI(register_android_ddm_DdmHandleNativeHeap),
    REG_JNI(register_android_backup_BackupDataInput),
    REG_JNI(register_android_backup_BackupDataOutput),
    REG_JNI(register_android_backup_FileBackupHelperBase),
    REG_JNI(register_android_backup_BackupHelperDispatcher),
    REG_JNI(register_android_app_backup_FullBackup),
    REG_JNI(register_android_app_ActivityThread),
    REG_JNI(register_android_app_NativeActivity),
    REG_JNI(register_android_view_InputChannel),
    REG_JNI(register_android_view_InputEventReceiver),
    REG_JNI(register_android_view_InputEventSender),
    REG_JNI(register_android_view_InputQueue),
    REG_JNI(register_android_view_KeyEvent),
    REG_JNI(register_android_view_MotionEvent),
    REG_JNI(register_android_view_PointerIcon),
    REG_JNI(register_android_view_VelocityTracker),

    REG_JNI(register_android_content_res_ObbScanner),
    REG_JNI(register_android_content_res_Configuration),

    REG_JNI(register_android_animation_PropertyValuesHolder),
    REG_JNI(register_com_android_internal_content_NativeLibraryHelper),
    REG_JNI(register_com_android_internal_net_NetworkStatsFactory),
};

gRegJNI數組,有138個成員變量,定義在AndroidRuntime.cpp中,該數組中每一個成員都代表一類文件的jni映射,其中REG_JNI是一個宏定義,讓我們來看下

3、REG_JNI 宏定義
#ifdef NDEBUG
    #define REG_JNI(name)      { name }
    struct RegJNIRec {
        int (*mProc)(JNIEnv*);
    };
#else
    #define REG_JNI(name)      { name, #name }
    struct RegJNIRec {
        int (*mProc)(JNIEnv*);
        const char* mName;
    };
#endif

其中 mProc,就等價于調用其參數名所指向的函數,例如
REG_JNI(register_com_android_internal_os_RuntimeInit).mProc 也就是指進入 register_com_android_internal_os_RuntimeInit的方法,以此為例,看下面的代碼

int register_com_android_internal_os_RuntimeInit(JNIEnv* env)
{
    return jniRegisterNativeMethods(env, "com/android/internal/os/RuntimeInit",
        gMethods, NELEM(gMethods));
}

//gMethods:java層方法名與jni層的方法的一一映射關系
static JNINativeMethod gMethods[] = {
    { "nativeFinishInit", "()V",
        (void*) com_android_internal_os_RuntimeInit_nativeFinishInit },
    { "nativeZygoteInit", "()V",
        (void*) com_android_internal_os_RuntimeInit_nativeZygoteInit },
    { "nativeSetExitWithoutCleanup", "(Z)V",
        (void*) com_android_internal_os_RuntimeInit_nativeSetExitWithoutCleanup },
};

所以REG_JNI就是一個宏定義,該宏的作用就是調用相應的方法。

(二)、如何查找native方法

當大家在看framework層代碼時,經常會看到native方法,這往往需要查看所對應的C++方法在那個文件,對應那個方法?下面從一個實例出發帶大家如何查看Java層方法所對應的Native方法位置

1、實例(一)

在后面分析Android消息機制源碼,遇到MessageQueue.java中有多個native方法,比如:

private native void nativePollOnce(long ptr, int timeoutMillis);

這樣要怎么查找那?主要分為兩個步驟

  • 第一步:MessageQueue.java的全限定名為android.os.MessageQueue.java。方法名為android.os.MessageQueue.nativePollOnce(),而相對應的native層方法名只是將點號替換為下劃線,所以可得android_os_MessgaeQueue_nativePollOnce()。所以:nativePollOnce---->android_os_MessageQueue_nativePollOnce()
  • 第二步:有了對應的native方法,接下來就需要這個native方法在那個文件中,上面已經說了,Android系統啟動的時候已經注冊了大量的JNI方法。

在AndroidRumtime.cpp的gRegJNI數組。這些注冊方法命名方式如下:

register_[包名]_[類名]

那么MessageQueue.java所定義的JNI注冊方法名應該是
** register_android_os_MessageQueue ** ,的確存在于 ** gRegJNI ** 數組,說明這次JNI注冊過程是開機過程完成的。該方法是在 ** AndroidRuntime.cpp ** 申明為extern方法:

extern int register_android_os_MessageQueue(JNIEnv* env);

這些extern方法絕大多數位于 ** /framework/base/core/jni ** 目錄,大多數情況下 native命名方式為

[包名]_[類名].cpp
[包名]_[類名].h

所以 MessageQueue.java--->android_os_MessageQueue.cpp。
打開android_os_MessageQueue.cpp文件,搜索android_os_MessageQueue_nativePollOnce方法,這便找到了目標方法:

static void android_os_MessageQueue_nativePollOnce(JNIEnv* env, jobject obj,
        jlong ptr, jint timeoutMillis) {
    NativeMessageQueue* nativeMessageQueue = reinterpret_cast<NativeMessageQueue*>(ptr);
    nativeMessageQueue->pollOnce(env, obj, timeoutMillis);
}

到這里完成了一次從Java層方法搜索到所對應的C++方法的過程。

2、實例(二)

對于native文件命名方式,有時并非[包名]_[類名].cpp,比如Binder.java,Binder.java所對應的native文件:android_util_Binder.cpp。

比如:Binder.java的native方法,代碼如下:

public static final native int getCallingPid();

根據實例(一)的方式,找到getCallingPid--->android_os_Binder_getCallingPid(),并且在AndroidRumtime.cpp中的gRegJNI數組找到了register_android_os_Binder。按照實例(一)方式則native中的文件名為android_os_Binder.cpp,可是在/framwork/base/core/jni/目錄下找不到該文件,這是例外的情況。其實真正的文件名為android_util_Binder.cpp,這就是例外,這一點有些費勁,不明白為何google打破之前的規律。

//frameworks/base/core/jni/android_util_Binder.cpp    761行
static jint android_os_Binder_getCallingPid(JNIEnv* env, jobject clazz)
{
    return IPCThreadState::self()->getCallingPid();
}

有人會問,以后遇到打破常規的文件命名的文件怎么辦?其實很簡單,首先,先嘗試在/framework/base/core/jni/中搜索,對于Binder.java,可以直接搜索Binder關鍵字,其他類似。如果這里找不到,可以通過grep全局搜索android_os_Binder_getCallingPid()這個方法在那個文件上。

jni存在的常見目錄:

  • /framework/base/core/jni
  • /framework/base/services/core/jni
  • /framework/base/media/jni
3、實例(三)

前面兩種都是在Android系統啟動之初,便已經注冊過JNI所對應的方法。那么如果程序自己定義的JNI方法,該如何查看JNI方法所在的位置?下面以MediaPlayer.java為例,其包名為android.media:

public class MediaPlayer{
    static {
        System.loadLibrary("media_jni");
        native_init();
    }

    private static native final void native_init();
}

通過static 靜態代碼塊中的System.loadLibrary()方法來加載動態庫,庫名為media_jni,Android平臺則會自動擴展成所對應的libmedia_jni.so庫。接著通過關鍵字native加載native_init方法之前,便可以在java層直接使用native層方法。

接下來便要查看libmedia_jni.so庫定義所在文件,一般都是通過android.mk文件中定義的LOCAL_MODULE:= libmedia_jni,可以采用grep或者mgrep來搜索包含libmedia_jni字段的Android.mk所在路徑。

搜索可知,libmedia_jni.so位于/framework/base/media/jni/Android.mk。用前面的實例(一)中的知識來查看相應的文件和方法分別為:

android_media_MediaPlayer.cpp
android_media_MediaPlayer_native_init()

再然后,你會發現果然在該Android.mk所在目錄/frameworks/base/media/jni中找到android_media_MediaPlayer.cpp文件。并在文件中存在相應的方法

//frameworks/base/media/jni/android_media_MediaPlayer.cpp     820行
    // This function gets some field IDs, which in turn causes class initialization.
    // It is called from a static block in MediaPlayer, which won't run until the
    // first time an instance of this class is used.
    // 當類初始化的時候此函數獲取一些字段ID。因為它是在MediaPlayer中的靜態塊中調用的,所以除非是第一次使用此類的實例,否則它將不會運行。
    static void android_media_MediaPlayer_native_init(JNIEnv *env)
    {
        jclass clazz;
        clazz = env->FindClass("android/media/MediaPlayer");
        if (clazz == NULL) {
            return;
        }
        fields.context = env->GetFieldID(clazz, "mNativeContext", "J");
        if (fields.context == NULL) {
            return;
        }
        fields.post_event = env->GetStaticMethodID(clazz, "postEventFromNative",
                "(Ljava/lang/Object;IIILjava/lang/Object;)V");
        if (fields.post_event == NULL) {
            return;
        }
        fields.surface_texture = env->GetFieldID(clazz, "mNativeSurfaceTexture", "J");
        if (fields.surface_texture == NULL) {
            return;
        }
        env->DeleteLocalRef(clazz);
        clazz = env->FindClass("android/net/ProxyInfo");
        if (clazz == NULL) {
            return;
        }
        fields.proxyConfigGetHost =
                env->GetMethodID(clazz, "getHost", "()Ljava/lang/String;");
        fields.proxyConfigGetPort =
                env->GetMethodID(clazz, "getPort", "()I");
        fields.proxyConfigGetExclusionList =
                env->GetMethodID(clazz, "getExclusionListAsString", "()Ljava/lang/String;");
        env->DeleteLocalRef(clazz);
        gPlaybackParamsFields.init(env);
        gSyncParamsFields.init(env);
    }

所以 MediaPlayer.java中的native_init()方法位于/frameworks/base/media/jni/目錄下的android_media_MediaPlayer.cpp文件中的android_media_MediaPlayer_native_init方法。

(三)、總結

JNI作為連接Java世界和C/C++世界的橋梁,很有必要掌握。看完本文后,一定要掌握在分析Android源碼過程匯總如何查找native方法。首先明白native方法名和文件名的命名規律,其次要懂得該如何去搜索代碼。JNI方式注冊無非是Android系統啟動中Zygote注冊以及通過System.loadLibrary方式注冊,對于系統啟動過程注冊的,可以通過查詢AndroidRuntime.cpp中的gRegJNI是否存在對應的register方法,如果不存在,則大多數通過LoadLibrary方式來注冊。

五、loadLibrary源碼分析

再來進一步分析,Java層與native層方法是如何注冊并映射的,繼續以MediaPlayer為例,進一步分析。

在MediaPlayer.java中調用System.loadLibrary("media_jni"),把libmedia_jni.so動態庫加載到內存。接下來以loadLibrary為起點展開JNI注冊流程的過程分析。

(一) loadLibrary() 流程

1、loadLibrary()方法
//libcore/luni/src/main/java/java/lang/System.java     1075行
    /**
     * Loads the system library specified by the <code>libname</code>
     * argument. The manner in which a library name is mapped to the
     * actual system library is system dependent.
     *
     * 加載由 libname 參數指定的系統庫, library庫名是通過系統依賴映射到實際系統庫的。
     *
     * <p>
     * The call <code>System.loadLibrary(name)</code> is effectively
     * equivalent to the call
     * <blockquote><pre>
     * Runtime.getRuntime().loadLibrary(name)
     * </pre></blockquote>
     *
     * 調用 System.loadLibrary(name)實際上等價于調用
     * Runtime.getRuntime().loadLibrary(name)
     *
     * @param      libname   the name of the library.      lib庫的名字
     * @exception  SecurityException  if a security manager exists and its
     *             <code>checkLink</code> method doesn't allow
     *             loading of the specified dynamic library
     * 如果存在安全管理員,并且其 checkLink 方法不允許 加載指定的動態庫,則會拋出SecurityException
     * @exception  UnsatisfiedLinkError  if the library does not exist.
     * 如果庫不存在則拋出UnsatisfiedLinkError
     * @exception  NullPointerException if <code>libname</code> is
     *             <code>null</code>
     * 如果libname是null則拋出NullPointerException
     * @see        java.lang.Runtime#loadLibrary(java.lang.String)
     * @see        java.lang.SecurityManager#checkLink(java.lang.String)
     */
    public static void loadLibrary(String libname) {
         Runtime.getRuntime().loadLibrary(libName, VMStack.getCallingClassLoader());
    }

通過代碼和上面的注釋,我們知道了,loadLibrary(String)其本質是調用了 Runtime.getRuntime().loadLibrary(String,ClassLoader)
那我們就來跟蹤一下

2、Runtime.getRuntime().loadLibrary(String,ClassLoader)方法
     /*
      * Searches for and loads the given shared library using the given ClassLoader.
      * 使用指定的ClassLoader搜索并加載給定的共享庫
      */
    void loadLibrary(String libraryName, ClassLoader loader) {
         //如果load不為null 則進入該分支 
        if (loader != null) {
            //查找庫所在的路徑
            String filename = loader.findLibrary(libraryName);
            // 如果路徑為null則說明找不到庫
            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".
                // 當我們搜索liblibMyLibrary.so.so的時候,可能會提示我們沒有找
                // 到"ibMyLibrary.so"。是因為ClassClassLoader不一定使用System,
                 // 但是默認設置又是這樣的,所以會有一定的誤導性
                throw new UnsatisfiedLinkError(loader + " couldn't find \"" +
                        System.mapLibraryName(libraryName) + "\"");
            }
            //找到路徑,則加載庫
            String error = doLoad(filename, loader);
            if (error != null) {
                throw new UnsatisfiedLinkError(error);
            }
            return;
        }
        // 如果loader為null,則執行下面的代碼
        // 其中 System.mapLibraryName(String) 是"根據庫名返回本地的庫"
        String filename = System.mapLibraryName(libraryName);
        List<String> candidates = new ArrayList<String>();
        String lastError = null;
        for (String directory : mLibPaths) {
            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;
            }
        }
        if (lastError != null) {
            throw new UnsatisfiedLinkError(lastError);
        }
        throw new UnsatisfiedLinkError("Library " + libraryName + " not found; tried " + candidates);
    }

通過上面的代碼,我們知道,無論loader是否為null,最后都是通過doLoad(String, ClassLoader)來真正的加載。那我們來一起看一下

3、doLoad(String name, ClassLoader loader) 方法
    //libcore/luni/src/main/java/java/lang/Runtime.java            401行
    private String doLoad(String name, ClassLoader loader) {
        // Android apps are forked from the zygote, so they can't have a custom LD_LIBRARY_PATH,
        // which means that by default an app's shared library directory isn't on LD_LIBRARY_PATH.
       // Android應用程序從zygote分支fork出來的,所以他們無法自定義
       // LD_LIBRARY_PATH,這意味著默認情況下,應用程序的共享庫目錄不在
       // LD_LIBRARY_PATH上。

        // The PathClassLoader set up by frameworks/base knows the appropriate path, so we can load
        // libraries with no dependencies just fine, but an app that has multiple libraries that
        // depend on each other needed to load them in most-dependent-first order.

        // We added API to Android's dynamic linker so we can update the library path used for
        // the currently-running process. We pull the desired path out of the ClassLoader here
        // and pass it to nativeLoad so that it can call the private dynamic linker API.

        // We didn't just change frameworks/base to update the LD_LIBRARY_PATH once at the
        // beginning because multiple apks can run in the same process and third party code can
        // use its own BaseDexClassLoader.

        // We didn't just add a dlopen_with_custom_LD_LIBRARY_PATH call because we wanted any
        // dlopen(3) calls made from a .so's JNI_OnLoad to work too.

        // So, find out what the native library search path is for the ClassLoader in question...
        // 由framework / base設置的PathClassLoader知道適具體的路徑,因此我們可以
        // 加載沒有依賴關系的庫,但是具有依賴于彼此的多個庫的應用程序需要以大多
        // 數依賴的順序加載它們。 
        // 為了讓我們可以在正在運行的進程中更新庫路徑,所以我們向Android的動態鏈
        // 接器添加了API。
        // 我們將所需的路徑從ClassLoader中拉出,并將其傳遞給nativeLoad,便可以  
        // 調用私有動態鏈接器API。
        // 我們不僅僅是更改框架/基礎來更新LD_LIBRARY_PATH一次,因為多個apk
        // 可以在同一進程中運行,第三方代碼可以使用自己的BaseDexClassLoader。
        // 我們沒有添加dlopen_with_custom_LD_LIBRARY_PATH調用,因為我們希望
        // 使用.so的JNI_OnLoad進行的任何dlopen(3)調用也可以工作。
        //因此,找出本機庫搜索路徑對于有問題的ClassLoader

        String ldLibraryPath = null;
        String dexPath = null;
        if (loader == null) {
            // We use the given library path for the boot class loader. This is the path
            // also used in loadLibraryName if loader is null.
            ldLibraryPath = System.getProperty("java.library.path");
        } else if (loader instanceof BaseDexClassLoader) {
            BaseDexClassLoader dexClassLoader = (BaseDexClassLoader) loader;
            ldLibraryPath = dexClassLoader.getLdLibraryPath();
        }
        // nativeLoad should be synchronized so there's only one LD_LIBRARY_PATH in use regardless
        // of how many ClassLoaders are in the system, but dalvik doesn't support synchronized
        // internal natives.
        // nativeLoad應該同步,所以使用中只有一個LD_LIBRARY_PATH,無論系統中有多少個ClassLoaders,dalvik不支持native的同步。
        synchronized (this) {
            return nativeLoad(name, loader, ldLibraryPath);
        }
    }

nativeLoad()這是一個native方法,再進去ART虛擬機java_lang_Runtime.cc,再細講就要深入剖析虛擬機內部,這里就不往下深入了。后續有時間單獨講解下虛擬機。這里直接說結論

  • 調用dlopen()函數,打開一個so文件并創建一個handle;
  • 調用dlsym()函數,查看相應so文件的JNIOnLoad()函數指針,并執行相應函數。
4、總結

所以說,System.loadLibrary()的作用就是調用相應庫中的JNI_OnLoad()方法。那我們就來看下JNI_OnLoad()過程

(二) JNI_OnLoad流程

1、JNI_OnLoad()
jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
    JNIEnv* env = NULL;
    //frameworks/base/media/jni/android_media_MediaPlayer.cpp    1132行
    // 注冊JNI
    if (register_android_media_MediaPlayer(env) < 0) {
        goto bail;
    }
    ...
}

這里面主要通過調用register_android_media_MediaPlayer()函數來進行注冊的。

2、register_android_media_MediaPlayer()
//frameworks/base/media/jni/android_media_MediaPlayer.cpp 1086行
// This function only registers the native method
// 這個函數僅僅是用來注冊native方法的
static int register_android_media_MediaPlayer(JNIEnv *env)
{
    return AndroidRuntime::registerNativeMethods(env,
                "android/media/MediaPlayer", gMethods, NELEM(gMethods));
}

我們看到register_android_media_MediaPlayer()函數里面,實際上是調用的AndroidRuntime的registerNativeMethods()函數。

在看AndroidRuntime的registerNativeMethods()函數之前,先說下gMethods

3、gMethods
//frameworks/base/media/jni/android_media_MediaPlayer.cpp    1036行
static JNINativeMethod gMethods[] = {
    {
        "nativeSetDataSource",
        "(Landroid/os/IBinder;Ljava/lang/String;[Ljava/lang/String;"
        "[Ljava/lang/String;)V",
        (void *)android_media_MediaPlayer_setDataSourceAndHeaders
            
    },
    {"_setDataSource",      "(Ljava/io/FileDescriptor;JJ)V",    (void *)android_media_MediaPlayer_setDataSourceFD},
    {"_setDataSource",      "(Landroid/media/MediaDataSource;)V",(void *)android_media_MediaPlayer_setDataSourceCallback },
    {"_setVideoSurface",    "(Landroid/view/Surface;)V",        (void *)android_media_MediaPlayer_setVideoSurface},
    {"_prepare",            "()V",                              (void *)android_media_MediaPlayer_prepare},
    {"prepareAsync",        "()V",                              (void *)android_media_MediaPlayer_prepareAsync},
    {"_start",              "()V",                              (void *)android_media_MediaPlayer_start},
    {"_stop",               "()V",                              (void *)android_media_MediaPlayer_stop},
    {"getVideoWidth",       "()I",                              (void *)android_media_MediaPlayer_getVideoWidth},
    {"getVideoHeight",      "()I",                              (void *)android_media_MediaPlayer_getVideoHeight},
    {"setPlaybackParams", "(Landroid/media/PlaybackParams;)V", (void *)android_media_MediaPlayer_setPlaybackParams},
    {"getPlaybackParams", "()Landroid/media/PlaybackParams;", (void *)android_media_MediaPlayer_getPlaybackParams},
    {"setSyncParams",     "(Landroid/media/SyncParams;)V",  (void *)android_media_MediaPlayer_setSyncParams},
    {"getSyncParams",     "()Landroid/media/SyncParams;",   (void *)android_media_MediaPlayer_getSyncParams},
    {"seekTo",              "(I)V",                             (void *)android_media_MediaPlayer_seekTo},
    {"_pause",              "()V",                              (void *)android_media_MediaPlayer_pause},
    {"isPlaying",           "()Z",                              (void *)android_media_MediaPlayer_isPlaying},
    {"getCurrentPosition",  "()I",                              (void *)android_media_MediaPlayer_getCurrentPosition},
    {"getDuration",         "()I",                              (void *)android_media_MediaPlayer_getDuration},
    {"_release",            "()V",                              (void *)android_media_MediaPlayer_release},
    {"_reset",              "()V",                              (void *)android_media_MediaPlayer_reset},
    {"_setAudioStreamType", "(I)V",                             (void *)android_media_MediaPlayer_setAudioStreamType},
    {"_getAudioStreamType", "()I",                              (void *)android_media_MediaPlayer_getAudioStreamType},
    {"setParameter",        "(ILandroid/os/Parcel;)Z",          (void *)android_media_MediaPlayer_setParameter},
    {"setLooping",          "(Z)V",                             (void *)android_media_MediaPlayer_setLooping},
    {"isLooping",           "()Z",                              (void *)android_media_MediaPlayer_isLooping},
    {"_setVolume",          "(FF)V",                            (void *)android_media_MediaPlayer_setVolume},
    {"native_invoke",       "(Landroid/os/Parcel;Landroid/os/Parcel;)I",(void *)android_media_MediaPlayer_invoke},
    {"native_setMetadataFilter", "(Landroid/os/Parcel;)I",      (void *)android_media_MediaPlayer_setMetadataFilter},
    {"native_getMetadata", "(ZZLandroid/os/Parcel;)Z",          (void *)android_media_MediaPlayer_getMetadata},
    {"native_init",         "()V",                              (void *)android_media_MediaPlayer_native_init},
    {"native_setup",        "(Ljava/lang/Object;)V",            (void *)android_media_MediaPlayer_native_setup},
    {"native_finalize",     "()V",                              (void *)android_media_MediaPlayer_native_finalize},
    {"getAudioSessionId",   "()I",                              (void *)android_media_MediaPlayer_get_audio_session_id},
    {"setAudioSessionId",   "(I)V",                             (void *)android_media_MediaPlayer_set_audio_session_id},
    {"_setAuxEffectSendLevel", "(F)V",                          (void *)android_media_MediaPlayer_setAuxEffectSendLevel},
    {"attachAuxEffect",     "(I)V",                             (void *)android_media_MediaPlayer_attachAuxEffect},
    {"native_pullBatteryData", "(Landroid/os/Parcel;)I",        (void *)android_media_MediaPlayer_pullBatteryData},
    {"native_setRetransmitEndpoint", "(Ljava/lang/String;I)I",  (void *)android_media_MediaPlayer_setRetransmitEndpoint},
    {"setNextMediaPlayer",  "(Landroid/media/MediaPlayer;)V",   (void *)android_media_MediaPlayer_setNextMediaPlayer},
};

gMethods,記錄java層和C/C++層方法的一一映射關系。這里涉及到結構體JNINativeMethod,其定義在jni.h文件:

/、libnativehelper/include/nativehelper/jni.h    129行
typedef struct {
    const char* name;  //Java層native函數名
    const char* signature;   //Java函數簽名,記錄參數類型和個數,以及返回值類型
    void*       fnPtr;  //Native層對應的函數指針
} JNINativeMethod;

下面讓我們看一下AndroidRuntime的registerNativeMethods()函數

4、AndroidRuntime的registerNativeMethods()函數
//frameworks/base/core/jni/AndroidRuntime.cpp  262行
/*
 * Register native methods using JNI.
 * 使用JNI注冊native方法
 */
int AndroidRuntime::registerNativeMethods(JNIEnv* env,
    const char* className, const JNINativeMethod* gMethods, int numMethods)
{
    return jniRegisterNativeMethods(env, className, gMethods, numMethods);
}

jniRegisterNativeMethods該方法是由Android JNI幫助類JNIHelp.cpp來完成。

5、jniRegisterNativeMethods()函數
//libnativehelper/JNIHelp.cpp     73行
extern "C" int jniRegisterNativeMethods(C_JNIEnv* env, const char* className,
    const JNINativeMethod* gMethods, int numMethods)
{
    JNIEnv* e = reinterpret_cast<JNIEnv*>(env);

    ALOGV("Registering %s's %d native methods...", className, numMethods);
    scoped_local_ref<jclass> c(env, findClass(env, className));
    //找不到native注冊方法
    if (c.get() == NULL) {
        char* msg;
        asprintf(&msg, "Native registration unable to find class '%s'; aborting...", className);
        e->FatalError(msg);
    }
     //調用JNIEnv結構體的成員變量
    if ((*env)->RegisterNatives(e, c.get(), gMethods, numMethods) < 0) {
        //如果native方法注冊失敗
        char* msg;
        asprintf(&msg, "RegisterNatives failed for '%s'; aborting...", className);
        e->FatalError(msg);
    }

    return 0;
}

通過上面的代碼我們發現jniRegisterNativeMethods()內部真正的注冊函數是RegisterNatives()函數,那我們繼續跟蹤

6、RegisterNatives()函數
// libnativehelper/include/nativehelper/jni.h         976行
    jint RegisterNatives(jclass clazz, const JNINativeMethod* methods,
        jint nMethods)
    { return functions->RegisterNatives(this, clazz, methods, nMethods); }
}

其中functions是指向JNINativeInterface結構體指針,也就是將調用下面的方法:

struct JNINativeInterface {
    jint (*RegisterNatives)(JNIEnv*, jclass, const JNINativeMethod*,jint);
}

再往下深入就到了虛擬機內部了,就不在深入了。

(三) 總結

總之,這個過程完成了gMethods數組中的方法的映射關系,比如java層的native_init()方法,映射到native層的android_media_MediaPlayer_native_init()方法。

虛擬機相關的變量中有兩個非常重要的變量JavaVM和JNIEnv:

  • JavaVM:是指進程虛擬機環境,每個進程有且只有一個JavaVM實例。
  • JNIEnv:是指線程上下文環境,每個線程有且只有一個JNIEnv實例。

六、JNI資源

JNINativeMethod結構體中有一個字段為signature(簽名),在介紹signature格式之前需要掌握各種數據類型在Java層、Native層以及簽名所采用的簽名格式。

(一)、數據類型

1、基本數據類型
Signature Java Native
B byte jbyte
C char jchar
D double jdouble
F float jfloat
I int jint
S short jshort
J long jlong
Z boolean jboolean
V void void
2、數組數據類型

數組簡稱則是在前面添加" **[ ** " :

Signature格式 Java Native
[B byte[] jbyteArray
[C char[] jcharArray
[D double[] jdoubleArray
[F float[] jfloatArray
[I int[] jintArray
[S shor[] jshortArray
[ J long[] jlongArray
[ Z boolean[] jbooleanArray
3、復雜數據類型

對象類型簡稱:L+ ** classname **+;

Signature格式 Java Native
Ljava/lang/String String jstring
L+class+ ; 所有對象 jobject
[ L + classname + ; Object[] jobjectArray
Ljava.lang.Class Class jclass
Ljava.lang.Throwable Throwable jthrowable
4、Signature

有了前面的講解,我們通過案例來說說函數簽名:** (入參) 返回值參數 **,這里用到的便是前面介紹的Signature格式

Java格式 對應的簽名
void foo() ()V
float foo(int i) (I) F
long foo(int[] i) ([ i) J
double foo (Class c) (Ljava/lang/Class;) D
boolean foo (int [] i,String s) ([ILjava/lang/String ;) Z
String foo(int i) (I) Ljava/lang/String ;

(二)、其他注意事項

1、垃圾回收

對于Java而言,開發者是無需關心垃圾回收的,因為這完全由虛擬機GC來負責垃圾回收,而對于JNI開發人員,由于內存釋放需要謹慎處理,需要的時候申請,使用完后記得釋放內存,以免發生內存泄露。

所以JNI提供了了三種Reference類型:

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

其中Global Reference 如果不主動釋放,則一直不會釋放;對于其他兩個類型的引用都是釋放的可能性,那是不是意味著不需要手動釋放? 答案是否定的,不管這三種類型的那種引用,都盡可能在某個內存不需要時,立即釋放,這對系統更為安全可靠,以減少不可預知的性能與穩定性問題。

另外,ART虛擬機在GC算法有所優化,為了減少內存碎片化問題,在GC之后有可能會移動對象內存的位置,對于Java層程序并沒有影響,但是對于JNI程序要注意了,對于通過指針來直接訪問內存對象時,Dalvik能正確運行的程序,ART下未必能正常運行。
2、異常處理

Java層出現異常,虛擬機會直接拋出異常,這是需要try... catch 或者繼續向外throw。但是對于JNI出現異常時,即執行到JNIEnv 中某個函數異常時,并不會立即拋出異常來中斷程序的執行,還可以繼續執行內存之類的清理工作,知道返回Java層才會拋出相應的異常。

另外,Dalvik虛擬機有些情況下JNI函數出錯可能會返回NULL,但ATR虛擬機在出錯時更多是拋出異常。這樣導致的問題就可能是在Dalvik版本能正常運行的成員,在ART虛擬機上并沒正確處理異常而崩潰。

七、總結

本文主要是通過實例,基于Android 6.0源碼分析 JNI原理,講述JNI核心功能:

  • 介紹了JNI的概念及如何查找JNI方法,讓大家明白如何從Java層跳轉到Native層
  • 分了JNI函數注冊流程,進異步加深對JNI的理解
  • 列舉Java與Native一級函數簽名方式。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容