JNIEnv API

詳細(xì)探討了JNI調(diào)用如何使用宫盔,JNI的庫文件是如何加載的,下面來詳細(xì)探討下JNI API有额,這API是做什么的巍佑,有啥注意事項句狼,這是后續(xù)JNI開發(fā)的基礎(chǔ)胳螟。


image.png

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

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

Java的數(shù)據(jù)類型分為基本類型(primitive type秘遏,又稱原生類型或者原始類型)和引用類型(reference type),其中基本類型又分為數(shù)值類型邦危,boolean類型和returnAddress類型三類倦蚪。returnAddress類型在Java語言中沒有對應(yīng)的數(shù)據(jù)類型,由JVM使用表示指向某個字節(jié)碼的指針慕购。JVM定義了boolean類型沪悲,但是對boolean類型的支持非常有限贡珊,boolean類型沒有任何專供boolean值使用的字節(jié)碼指令飞崖,java語言表達(dá)式操作boolean值固歪,都是使用int類型對應(yīng)的字節(jié)碼指令完成的,boolean數(shù)組的訪問修改共用byte數(shù)組的baload和bstore指令蒲讯;JVM規(guī)范中明確了1表示true判帮,0表示false,但是未明確boolean類型的長度晌畅,Hotspot使用C++中無符號的char類型表示boolean類型,即boolean類型占8位连躏。數(shù)值類型分為整數(shù)類型和浮點數(shù)類型反粥,如下:

整數(shù)類型包含:

  • byte類型:值為8位有符號二進(jìn)制補(bǔ)碼整數(shù),默認(rèn)值為0
  • short類型:值為16位有符號二進(jìn)制補(bǔ)碼整數(shù)郑气,默認(rèn)值為0
  • int類型:值為32位有符號二進(jìn)制補(bǔ)碼整數(shù)忙芒,默認(rèn)值為0
  • long類型:值為64位有符號二進(jìn)制補(bǔ)碼整數(shù)呵萨,默認(rèn)值為0
  • char類型:值為16位無符號整數(shù),用于表示指向多文種平面的Unicode碼點忱嘹,默認(rèn)值是Unicode的null碼點(‘\u0000’)

浮點類型包括:

  • float類型:值為單精度浮點數(shù)集合中的元素,如果虛擬機(jī)支持的話是單精度擴(kuò)展指數(shù)集合中的元素础米,默認(rèn)值是正數(shù)0
  • double類型:值為雙精度浮點數(shù)集合中的元素椭盏,如果虛擬機(jī)支持的話是雙精度擴(kuò)展指數(shù)集合中的元素糟红,默認(rèn)值是正數(shù)0
  • float類型和double類型的長度在JVM規(guī)范中未明確說明柒爸,在Hotspot中float和double就是C++中對應(yīng)的float和double類型捎稚,所以長度分別是32位和64位今野,所謂雙精度是相對單精度而言的,即用于存儲小數(shù)部分的位數(shù)更多宰睡,可參考《C++ 名稱空間旋圆,wchar_t寬字符灵巧,浮點數(shù)精度,new/delete操作符》肄方。

引用類型

引用類型在JVM中有三種,類類型(class type)隅要,數(shù)組類型(array type)和接口類型(interface type),數(shù)組類型最外面的一維元素的類型稱為該數(shù)組的組件類型廓啊,組件類型也可以是數(shù)組類型,如果組件類型不是元素類型則稱為該數(shù)組的元素類型第步,引用類型其實就是C++中的指針。
JVM規(guī)范中并沒有強(qiáng)引用驯杜,軟引用滚局,弱引用和虛引用的概念,JVM定義的引用就是強(qiáng)引用嘁圈,軟引用,弱引用和虛引用是JDK結(jié)合垃圾回收機(jī)制提供的功能支持而已涨缚。
參考:Java 的強(qiáng)引用、弱引用茂翔、軟引用、虛引用

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

JNI數(shù)據(jù)類型其實就是Java數(shù)據(jù)類型在Hotspot中的具體表示或者對應(yīng)的C/C++類型俐末,類型的定義參考OpenJDK hotspot/src/share/prims/jni.h中载矿,如下圖:

image.png

部分類型跟CPU架構(gòu)相關(guān)的弯洗,通過宏定義jni_md.h引入,如下圖:

image.png

通常的服務(wù)器都是x86_64架構(gòu)的谣辞,其定義的類型如下:

image.png

從上面的定義可以得出,JVM中除基本數(shù)據(jù)類型外躯嫉,所有的引用類型都是指針,JVM這里只是定義了空白的類來區(qū)分不同的引用類型,具體處理指針時會將指針強(qiáng)轉(zhuǎn)成合適的數(shù)據(jù)類型舱痘,如jobject指針會強(qiáng)轉(zhuǎn)成Oop指針,詳情可以參考JNIEnv API的實現(xiàn)旬盯。

Java同JNI數(shù)據(jù)類型的對應(yīng)關(guān)系

怎么去驗證Java數(shù)據(jù)類型和JNI數(shù)據(jù)類型的對應(yīng)關(guān)系了?可以通過javah生成的本地方法頭文件,比對Java方法和對應(yīng)的本地方法的參數(shù)可以看出兩者的對應(yīng)關(guān)系培他,如下示例:

package jni;
 
import java.util.List;
 
public class JniTest{
 
    static
    {
        System.loadLibrary("HelloWorld");
    }
 
    public native static void say(boolean a, byte b, char c, short d, int e, long f, float g, double h, String s, List l,Throwable t,Class cl,
                                  boolean[] a2, byte[] b2, char[] c2, short[] d2, int[] e2, long[] f2, float[] g2, double[] h2,String[] s2);
 
}

生成的jni_JniTest.h頭文件如下:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class jni_JniTest */
 
#ifndef _Included_jni_JniTest
#define _Included_jni_JniTest
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     jni_JniTest
 * Method:    say
 * Signature: (ZBCSIJFDLjava/lang/String;Ljava/util/List;Ljava/lang/Throwable;Ljava/lang/Class;[Z[B[C[S[I[J[F[D[Ljava/lang/String;)V
 */
JNIEXPORT void JNICALL Java_jni_JniTest_say
  (JNIEnv *, jclass, jboolean, jbyte, jchar, jshort, jint, jlong, jfloat, jdouble, jstring, jobject, jthrowable, jclass, jbooleanArray, jbyteArray, jcharArray, jshortArray, jintArray, jlongArray, jfloatArray, jdoubleArray, jobjectArray);
 
#ifdef __cplusplus
}
#endif
#endif

本地方法的第一個入?yún)⒍际荍NIEnv指針途蒋,第二個入?yún)⒏鶕?jù)本地方法是否是靜態(tài)方法區(qū)別處理懊烤,如果是靜態(tài)方法奸晴,第二個入?yún)⑹窃擃惖腃lass即jclass逮光,如果是普通方式則是當(dāng)前類實例的引用即jobject,其他的入?yún)⒏鶭ava方法的入?yún)⒁粯樱瑢ava的數(shù)據(jù)類型映射到JNI數(shù)據(jù)類型即可驾茴。在Java代碼調(diào)用本地方法同調(diào)用Java一樣都是值傳遞,基本類型參數(shù)傳遞的是參數(shù)值峡捡,對象類型參數(shù)傳遞的是對象引用,JVM必須跟蹤所有傳遞到本地方法的對象引用,確保所引用的對象沒有被垃圾回收掉装盯,本地代碼也需要及時通知JVM回收某個對象猖吴,垃圾回收器需要能夠回收本地代碼不再引用的對象共屈。

兩者的對應(yīng)關(guān)系具體如下表:

Java數(shù)據(jù)類型 JNI數(shù)據(jù)類型 x86 C++類型 長度 備注
boolean jboolean unsignedchar 8
byte jbyte signed char 8
char jchar unsigned short 16
short jshort short 16
int jint int 16
long jlong long 64
float jfloat float 32
double jdouble double 64
String jstring _jstring * 32/64 類指針,在64位機(jī)器上默認(rèn)開啟指針壓縮,指針長度是32位壤玫,否則是64位,不過被壓縮的指針僅限于指向堆對象的指針
Class jclass _jclass *
Throwable jthrowable _jthrowable *
boolean[] jbooleanArray _jbooleanArray *
byte[] jbyteArray _jbyteArray *
char[] jcharArray _jcharArray *
short[] jshortArray _jshortArray *
int[] jintArray _jintArray *
long[] jlongArray _jlongArray *
float[] jfloatArray _jfloatArray *
double[] jdoubleArray _jdoubleArray *
Object[] jobjectArray _jobjectArray *

API定義

JNI標(biāo)準(zhǔn)API的發(fā)展

早期不同廠商的JVM實現(xiàn)提供的JNI API接口有比較大的差異,這些差異導(dǎo)致開發(fā)者必須編寫不同的代碼適配不同的平臺她渴,簡單的介紹下這些API接口:

  • JDK 1.0 Native Method Interface
    該版本的API因為依賴保守的垃圾回收器且通過C結(jié)構(gòu)體的方式訪問Java對象的字段而被廢棄

  • Netscape's Java Runtime Interface
    Netscape就是大名鼎鼎的創(chuàng)造了JavaScript語言的網(wǎng)景公司,他制定了一套通用的API(簡稱JRI)蔑祟,并在設(shè)計之初就考慮了可移植性趁耗,但是存在諸多的爭議。

  • Microsoft's Raw Native Interface and Java/COM interface
    微軟的JVM實現(xiàn)提供兩種本地接口做瞪,Raw Native Interface (RNI)和Java/COM interface,前者很大程度了保持了對JDK JNI API接口的向后兼容装蓬,與之最大的區(qū)別是本地代碼必須通過RNI接口同垃圾回收器交互著拭;后者是一個語言獨立的本地接口,Java代碼可以同COM對象交互牍帚,一個Java類可以對外暴露成一個COM類儡遮。

這些API經(jīng)過各廠商充分討論,因為各種問題最終沒有成為標(biāo)準(zhǔn)API暗赶,詳情參考Java Native Interface Specification Contents Chapter 1: Introduction

JNIEnv和JavaVM定義

本地代碼調(diào)用JNI API的入口只有兩個JNIEnv和JavaVM類鄙币,這兩個都在jni.h中定義,如下:


image.png

部署后端應(yīng)用程序的服務(wù)器都具備C++運行時蹂随,所以只關(guān)注C++下的代碼即可十嘿,即#ifdef __cplusplus下的代碼。

JNIENV_和JavaVM_結(jié)構(gòu)體的定義如下:

image.png

兩者的實現(xiàn)其實是對結(jié)構(gòu)體JNINativeInterface_和JNIInvokeInterface_的簡單包裝而已岳锁,兩者定義如下:

image.png

從定義上可以看出兩者的結(jié)構(gòu)類似于C++中的虛函數(shù)表绩衷,結(jié)構(gòu)體中沒有定義方法而是方法指針,這樣一方面實現(xiàn)了C++下對C的兼容,C的結(jié)構(gòu)體中不能定義方法但是可以定義方法指針咳燕,C++的結(jié)構(gòu)體基本被擴(kuò)展成class勿决,可以定義方法和繼承;另一方面這種做法實現(xiàn)了接口與實現(xiàn)分離的效果招盲,調(diào)用API的本地代碼與JVM中具體的實現(xiàn)類解耦低缩,虛擬機(jī)可以基于此結(jié)構(gòu)輕松的提供兩種實現(xiàn)版本的JNI API,比如其中一個對入?yún)?yán)格校驗曹货,另一個只做關(guān)鍵參數(shù)最少的校驗咆繁,讓方法指針指向不同的實現(xiàn)即可,API調(diào)用方完全無感知控乾。官方文檔中提供了一張圖描述這種結(jié)構(gòu)么介,如下圖:

image.png

其中的JNI interface pointer就是傳入本地方法的參數(shù)JNIEnv指針, 該指針指向的JNIEnv對象本身包含了一個指向JNINativeInterface_結(jié)構(gòu)體的指針蜕衡,即圖中的Pointer壤短,JNINativeInterface_結(jié)構(gòu)體在內(nèi)存中相當(dāng)于一個指針數(shù)組,即圖中的Array of pointers to JNI functions慨仿,指針數(shù)組中的每個指針都是具體的方法實現(xiàn)的指針久脯。注意JVM規(guī)范要求同一個線程內(nèi)多次JNI調(diào)用接收的JNIEnv或者JavaVM指針都是同一個指針,且該指針只在該線程內(nèi)有效镰吆,因此本地代碼不能講該指針從當(dāng)前線程傳遞到另一個線程中帘撰。

JNINativeInterface_和JNIInvokeInterface_兩者的賦值如下:

image.png

前面的三個NULL都是為未來兼容COM對象保留的,JNINativeInterface_中第四個NULL是為未來的一個類相關(guān)的JNI操作保留的万皿。結(jié)構(gòu)體JNINativeInterface_和JNIInvokeInterface_包含的方法實現(xiàn)在跟jni.h同目錄的jni.cpp中摧找,JNIEnv和JavaVM類的初始化可以參考《Hotspot啟動和初始化源碼解析》。

三牢硅、異常處理
所有的JNI方法同大多數(shù)的C庫函數(shù)一樣不檢查傳入的參數(shù)的正確性蹬耘,這點由調(diào)用方負(fù)責(zé)檢查,如果參數(shù)錯誤可能導(dǎo)致JVM直接宕機(jī)减余。大多數(shù)情況下综苔,JNI方法通過返回一個特定的錯誤碼或者拋出一個Java異常的方式報錯,調(diào)用方可以通過ExceptionOccurred()方法判斷是否發(fā)生了異常位岔,如本地方法調(diào)用Java方法如筛,判斷Java方法執(zhí)行期間是否發(fā)生了異常,并通過該方法獲取異常的詳細(xì)信息抒抬。

JNI允許本地方法拋出或者捕獲Java異常杨刨,未被本地方法捕獲的異常會向上傳遞給方法的調(diào)用方。本地方法有兩種方式處理異常擦剑,一種是直接返回妖胀,導(dǎo)致異常在調(diào)用本地方法的Java方法中被拋出可免;一種是調(diào)用ExceptionClear()方法清除這個異常,然后繼續(xù)執(zhí)行本地方法的邏輯做粤。當(dāng)異常產(chǎn)生,本地方法必須先清除該異常才能調(diào)用其他的JNI方法捉撮,當(dāng)異常尚未處理時怕品,只有下列方法可以被安全調(diào)用:

ExceptionOccurred()
ExceptionDescribe()
ExceptionClear()
ExceptionCheck()
ReleaseStringChars()
ReleaseStringUTFChars()
ReleaseStringCritical()
Release<Type>ArrayElements()
ReleasePrimitiveArrayCritical()
DeleteLocalRef()
DeleteGlobalRef()
DeleteWeakGlobalRef()
MonitorExit()
PushLocalFrame()
PopLocalFrame()

異常處理相關(guān)API如下:

  • jint Throw(JNIEnv *env, jthrowable obj); 重新拋出Java異常
  • jjint ThrowNew(JNIEnv *env, jclass clazz,const char *message); 拋出一個指定類型和錯誤提示的異常
  • jjthrowable ExceptionOccurred(JNIEnv *env); 判斷當(dāng)前線程是否存在未捕獲的異常,如果存在則返回該異常對象巾遭,不存在返回NULL肉康,僅限于A方法調(diào)用B方法接受后,A方法調(diào)用此方法判斷B方法是否拋出異常的場景灼舍,B方法可以本地或者Java方法吼和。
  • jvoid ExceptionDescribe(JNIEnv *env); 將當(dāng)前線程的未捕獲異常打印到系統(tǒng)標(biāo)準(zhǔn)錯誤輸出流中如stderr,如果該異常是Throwable的子類則實際調(diào)用該類的printStackTrace方法打印骑素。注意執(zhí)行過程中獲取到異常實例jthrowable后就會調(diào)用ExceptionClear清除
  • jvoid ExceptionClear(JNIEnv *env); 清除當(dāng)前線程的未捕獲異常
  • jvoid FatalError(JNIEnv *env, const char *msg); 拋出一個致命異常炫乓,會直接導(dǎo)致JVM宕機(jī)
  • j jboolean ExceptionCheck(JNIEnv *env); 檢查當(dāng)前線程是否存在異常,不返回具體的異常對象献丑,若存在返回true

測試用例如下:

package jni;
 
public class ThrowTest {
 
    static {
        System.load("/home/openjdk/cppTest/ThrowTest.so");
    }
 
 
    public static native void rethrowException(Exception e);
 
    public static native void handlerException();
 
    public static void main(String[] args) {
        handlerException();
        rethrowException(new UnsupportedOperationException("Unsurpported ThrowTest"));
    }
 
 
}
#include "ThrowTest.h"
#include <stdio.h>
 
JNIEXPORT void JNICALL Java_jni_ThrowTest_rethrowException(JNIEnv * env,
        jclass cls, jthrowable e) {
    printf("Java_jni_ThrowTest_rethrowException\n");
    env->Throw(e);
}
 
void throwNewException(JNIEnv * env) {
    printf("throwNewException\n");
    jclass unsupportedExceptionCls = env->FindClass(
            "java/lang/UnsupportedOperationException");
    env->ThrowNew(unsupportedExceptionCls, "throwNewException Test\n");
}
 
JNIEXPORT void JNICALL Java_jni_ThrowTest_handlerException(JNIEnv * env,
        jclass cls) {
    throwNewException(env);
    jboolean result = env->ExceptionCheck();
    printf("ExceptionCheck result->%d\n", result);
    env->ExceptionDescribe();
    result = env->ExceptionCheck();
    printf("ExceptionCheck for ExceptionDescribe result->%d\n", result);
    throwNewException(env);
    jthrowable e = env->ExceptionOccurred();
    if (e) {
        printf("ExceptionOccurred not null\n");
    } else {
        printf("ExceptionOccurred null\n");
    }
    env->ExceptionClear();
    printf("ExceptionClear\n");
    e = env->ExceptionOccurred();
    if (e) {
        printf("ExceptionOccurred not null\n");
    } else {
        printf("ExceptionOccurred null\n");
    }
}

引用操作

引用的定義

jni.h中關(guān)于引用只定義了一個枚舉末捣,如下:

image.png

jobjectRefType表示引用類型,僅限于本地方法使用创橄,具體如下:

  • JNIInvalidRefType表示無效引用箩做;
  • JNILocalRefType表示本地引用,當(dāng)本地方法返回后這些引用會自動回收掉妥畏,JVM在開始執(zhí)行一個新的本地方法前會先執(zhí)行EnsureLocalCapacity評估當(dāng)前線程能否創(chuàng)建至少16個本地引用邦邦,然后調(diào)用PushLocalFrame創(chuàng)建一個新的用于保存該方法執(zhí)行過程中的本地引用的Frame,當(dāng)本地方法結(jié)束調(diào)用PopLocalFrame釋放調(diào)用該Frame醉蚁,同時釋放Frame中保存的即該本地方法執(zhí)行過程中創(chuàng)建的所有本地引用燃辖。本地方法接受的Java對象類型參數(shù)或者返回值都是JNILocalRefType類型,JNI允許開發(fā)者根據(jù)JNILocalRefType類型引用創(chuàng)建一個JNIGlobalRefType類型引用馍管。大部分情形開發(fā)者依賴程序自動回收掉本地引用即可郭赐,但是部分情形下需要開發(fā)者手動顯示釋放本地引用,比如本地方法中訪問了一個大的Java對象确沸,這時會創(chuàng)建一個指向該對象的本地引用捌锭,如果本地方法已經(jīng)不在使用該引用了,需要手動釋放該引用罗捎,否則該引用會阻止垃圾回收器回收掉該對象观谦;再比如通過本地方法遍歷某個包含大量Java對象的數(shù)組,遍歷的時候會為每個需要訪問的Java對象創(chuàng)建一個本地引用桨菜,如果這些引用不手動釋放而是等到方法結(jié)束才釋放就會導(dǎo)致內(nèi)存溢出問題豁状。JNI允許開發(fā)者在本地方法執(zhí)行的任意時點刪除本地引用從而允許垃圾回收器回收該引用指向的對象捉偏,為了確保這點,所有的JNI方法要求不能創(chuàng)建額外的本地引用泻红,除非它是作為返回值夭禽。創(chuàng)建的本地引用只在當(dāng)前線程內(nèi)有效,因此不能將一個本地引用傳遞到另一個線程中谊路。
  • JNIGlobalRefType表示全局引用讹躯,必須通過DeleteGlobalRef方法刪除才能釋放該全局引用,從而允許垃圾回收器回收該引用指向的對象
  • JNIWeakGlobalRefType表示全局弱應(yīng)用缠劝,只在JVM內(nèi)部使用潮梯,跟Java中的弱引用一樣,當(dāng)垃圾回收器運行的時候惨恭,如果一個對象僅被弱全局引用所引用秉馏,則這個對象將會被回收,如果指向的對象被回收掉了脱羡,全局弱引用會指向NULL萝究,開發(fā)者可以通過IsSameObject方法判斷弱引用是否等于NULL從而判斷指向的對象是否被回收掉了。全局弱引用比Java中的SoftReference 或者WeakReference都要弱锉罐,如果都指向了同一個對象糊肤,則只有SoftReference 或者WeakReference都被回收了,全局弱引用才會等于NULL氓鄙。全局弱應(yīng)用跟PhantomReferences的交互不明確馆揉,應(yīng)該避免兩者間的交互。

綜上抖拦,這里的引用其實就是Java中new一個對象返回的引用升酣,本地引用相當(dāng)于Java方法中的局部變量對Java對象實例的引用,全局引用相當(dāng)于Java類的靜態(tài)變量對Java對象實例的引用态罪,其本質(zhì)跟C++智能指針模板一樣噩茄,是對象指針的二次包裝,通過包裝避免了該指針指向的對象被垃圾回收器回收掉复颈,因此JNI中通過隱晦的引用訪問Java對象的消耗比通過指針直接訪問要高點绩聘,但是這是JVM對象和內(nèi)存管理所必須的。

引用API

相關(guān)API如下:

  • Jjobject NewGlobalRef(JNIEnv *env, jobject obj); 創(chuàng)建一個指向obj的全局引用耗啦,obj可以是本地或者全局引用凿菩,全局引用只能通過顯示調(diào)用DeleteGlobalRef()釋放。
  • Jvoid DeleteGlobalRef(JNIEnv *env, jobject globalRef); 刪除一個全局引用
  • Jjobject NewLocalRef(JNIEnv *env, jobject ref); 創(chuàng)建一個指向?qū)ο髍ef的本地引用
  • Jvoid DeleteLocalRef(JNIEnv *env, jobject localRef); 刪除一個本地引用
  • Jjint EnsureLocalCapacity(JNIEnv *env, jint capacity); 評估當(dāng)前線程是否能夠創(chuàng)建指定數(shù)量的本地引用帜讲,如果可以返回0衅谷,否則返回負(fù)數(shù)并拋出OutOfMemoryError異常。在執(zhí)行本地方法前JVM會自動評估當(dāng)前線程能否創(chuàng)建至少16個本地引用似将。JVM允許創(chuàng)建超過評估數(shù)量的本地引用获黔,如果創(chuàng)建過多導(dǎo)致JVM內(nèi)存不足JVM會拋出一個FatalError蚀苛。
  • Jjint PushLocalFrame(JNIEnv *env, jint capacity); 創(chuàng)建一個新的支持創(chuàng)建給定數(shù)量的本地引用的Frame,如果可以返回0玷氏,否則返回負(fù)數(shù)并拋出OutOfMemoryError異常堵未。注意在之前的Frame中創(chuàng)建的本地引用在新的Frame中依然有效。
  • Jjobject PopLocalFrame(JNIEnv *env, jobject result); POP掉當(dāng)前的本地引用Frame盏触,然后釋放其中的所有本地引用兴溜,如果result不為NULL,則返回該對象在前一個即當(dāng)前Frame之前被push的Frame中的本地引用
  • Jjweak NewWeakGlobalRef(JNIEnv *env, jobject obj); 創(chuàng)建一個指向?qū)ο髈bj的弱全局引用耻陕,jweak是jobject的別名,如果obj是null則返回NULL刨沦,如果內(nèi)存不足則拋出OutOfMemoryError異常
  • Jvoid DeleteWeakGlobalRef(JNIEnv *env, jweak obj); 刪除弱全局引用霜瘪。
  • JjobjectRefType GetObjectRefType(JNIEnv* env, jobject obj); 獲取某個對象引用的引用類型羞秤,JDK1.6引入的
  • JPushLocalFrame和PopLocalFrame兩個都是配合使用,常見于方法執(zhí)行過程中產(chǎn)生的本地引用需要盡快釋放掉,如下圖:
image.png

WITH_LOCAL_REFS和END_WITH_LOCAL_REFS兩個宏的定義如下:

image.png

NewGlobalRef的使用場景通常是初始化C/C++的全局屬性她君,需要通過全局引用確保該屬性指向的某個Java對象實例不被垃圾回收器回收掉,如下圖:

image.png

NewLocalRef的使用場景不多默辨,通常是用來檢測目標(biāo)對象是否已經(jīng)被回收掉了货岭,如果被回收了則該方法返回NULL,如下圖:


image.png

創(chuàng)建了一個新對象并返回該對象的本地引用通常直接調(diào)用JNIHandles::make_local實現(xiàn)徘禁,jni_NewLocalRef的實現(xiàn)也是通過該方法完成诅诱,所以NewLocalRef被直接使用的不多,如下:

image.png

類和對象操作

類操作API如下:

  • jint GetVersion(JNIEnv *env); 獲取當(dāng)前JVM的JNI接口版本送朱,該版本在jni.cpp中通過全局變量CurrentVersion指定娘荡。

  • jclass DefineClass(JNIEnv *env, const char *name, jobject loader,const jbyte *buf, jsize bufLen); 加載某個類,name表示類名驶沼,loader表示類加載器實例炮沐,buf表示class文件的字節(jié)數(shù)據(jù),bufLen表示具體的字節(jié)數(shù)回怜,此方法跟ClassLoader的實現(xiàn)基本一致大年。

  • jclass FindClass(JNIEnv *env, const char *name); 根據(jù)類名查找某個類,如果該類未加載則會調(diào)用合適的類加載器加載并鏈接該類玉雾。會優(yōu)先使用定義了調(diào)用FindClass的本地方法的類的類加載器加載指定類名的類翔试,如果是JDK標(biāo)準(zhǔn)類沒有對應(yīng)的類加載器則使用ClassLoader.getSystemClassLoader返回的系統(tǒng)類加載器加載。

  • jjlass GetSuperclass(JNIEnv *env, jclass clazz); 獲取父類复旬,如果該類是某個接口或者是Object則返回- jNULL

  • jboolean IsAssignableFrom(JNIEnv *env, jclass clazz1,jclass clazz2); 判斷clazz1能否安全的類型轉(zhuǎn)換成clazz2
    對象操作相關(guān)API:

  • jobject AllocObject(JNIEnv *env, jclass clazz); 分配一個Java對象并返回該對象的本地引用遏餐,注意該方法并未調(diào)用任何構(gòu)造方法

  • jobject NewObject(JNIEnv *env, jclass clazz,jmethodID methodID, ...); 調(diào)用指定的構(gòu)造方法創(chuàng)建一個Java對象并返回該對象的本地引用,最后的三個點表示該構(gòu)造方法的多個參數(shù)
    jobject NewObjectA(JNIEnv *env, jclass clazz,jmethodID methodID, const jvalue *args); 同上赢底,不過構(gòu)造方法的入?yún)⑹且粋€參數(shù)數(shù)組的指針

  • jobject NewObjectV(JNIEnv *env, jclass clazz,jmethodID methodID, va_list args);同上失都,不過構(gòu)造參數(shù)被放在va_list列表中

  • jclass GetObjectClass(JNIEnv *env, jobject obj);獲取Java對象所屬的Java類

  • jboolean IsInstanceOf(JNIEnv *env, jobject obj,jclass clazz); 判斷某個對象是否是某個類的實例

  • jboolean IsSameObject(JNIEnv *env, jobject ref1,jobject ref2); 判斷兩個引用是否引用相同的對象
    測試用例如下:

package jni;
 
class A{
 
}
 
public class ObjTest extends A {
 
    static {
        System.load("/home/openjdk/cppTest/ObjTest.so");
    }
 
    public ObjTest() {
        System.out.println("default");
    }
 
    public ObjTest(int age) {
        System.out.println("param Construtor,age->"+age);
    }
 
    public native static void test(Object a);
 
    public static void main(String[] args) {
        test(new ObjTest());
    }
}
#include "ObjTest.h"
#include <stdio.h>
 
 
JNIEXPORT void JNICALL Java_jni_ObjTest_test
  (JNIEnv * env, jclass jcl,jobject obj){
 
    jcl=env->GetObjectClass(obj);
    jclass objACls=env->FindClass("jni/A");
    jboolean result=env->IsAssignableFrom(jcl,objACls);
    printf("IsAssignableFrom result->%d\n",result);
 
    jobject objTest=env->AllocObject(jcl);
    printf("AllocObject succ \n");
 
    jmethodID defaultConst=env->GetMethodID(jcl,"<init>","()V");
    objTest=env->NewObject(jcl,defaultConst);
    printf("default construct new succ \n");
 
    jmethodID paramConst=env->GetMethodID(jcl,"<init>","(I)V");
    objTest=env->NewObject(jcl,paramConst,12);
    printf("param construct succ new \n");
 
    jclass superCls=env->GetSuperclass(jcl) jobject superObj=env->AllocObject(superCls);
    result=env->IsInstanceOf(superObj,objACls);
    printf("IsInstanceOf result->%d\n",result);
 
    jobject objTest2=env->NewLocalRef(objTest);
    result=env->IsSameObject(objTest2,objTest);
    printf("IsSameObject result->%d\n",result);
 
}

字段和方法操作

jfieldID和jmethodID定義

JNI中使用jfieldID來標(biāo)識一個某個類的字段柏蘑,jmethodID來標(biāo)識一個某個類的方法,jfieldID和jmethodID都是根據(jù)他們的字段名(方法名)和描述符確定的粹庞,通過jfieldID讀寫字段或者通過jmethodID調(diào)用方法都會避免二次查找咳焚,但是這兩個ID不能阻止該字段或者方法所屬的類被卸載,如果類被卸載了則jfieldID和jmethodID失效了需要重新計算庞溜,因此如果希望長期使用jfieldID和jmethodID則需要保持對該類的持續(xù)引用革半,即建立對該類的全局引用,兩者的定義在jni.h中流码,如下:

image.png

這兩個并非常規(guī)的字符串或者數(shù)字形式的ID又官,而是一個指針,指向?qū)嶋H保存字段信息和方法的該類的Klass漫试。

API說明

相關(guān)的API如下:

  • jfieldID GetFieldID(JNIEnv *env, jclass clazz,const char *name, const char *sig); 獲取某個類的指定字段的jfieldID六敬,如果該類未初始化則GetFieldID會觸發(fā)其完成初始化,注意不能用該方法獲取數(shù)組長度對應(yīng)的jfieldID進(jìn)而獲取數(shù)組長度而應(yīng)該直接使用GetArrayLength()方法獲取驾荣。

  • NativeType Get<type>Field(JNIEnv *env, jobject obj,jfieldID fieldID); 讀取某個實例指定字段的值外构,NativeType和type是一一對應(yīng)的,具體如下圖:


    image.png
  • void Set<type>Field(JNIEnv *env, jobject obj, jfieldID fieldID,NativeType value); 改寫某個實例指定字段的值播掷,type與NativeType的對應(yīng)關(guān)系同上审编。

  • jmethodID GetMethodID(JNIEnv *env, jclass clazz,const char *name, const char *sig); 獲取某個類的指定方法的jmethodID,如果該類未初始化則此方法會觸發(fā)該類完成初始化歧匈,獲取的方法可以是該類從父類繼承的方法垒酬。獲取構(gòu)造方法時,方法名必須是<init>件炉,返回類型必須是void (V)伤溉。

  • NativeType Call<type>Method(JNIEnv *env, jobject obj,jmethodID methodID, ...);

  • NativeType Call<type>MethodA(JNIEnv *env, jobject obj,jmethodID methodID, const jvalue *args);

  • NativeType Call<type>MethodV(JNIEnv *env, jobject obj,jmethodID methodID, va_list args); 上述三個都是調(diào)用指定實例對象的指定方法,不同的是方法入?yún)⒌膫鬟f方法妻率,同之前的NewObject系列方法乱顾。注意如果調(diào)用的方法是從父類繼承的,無論是構(gòu)造方法還是普通方法宫静,使用的jmethodID都必須是從obj獲取的走净,而不是從父類實例獲取的。NativeType和type是一一對應(yīng)的孤里,如下:


    image.png
  • NativeType CallNonvirtual<type>Method(JNIEnv *env, jobject obj,jclass clazz, jmethodID methodID, ...);

  • NativeType CallNonvirtual<type>MethodA(JNIEnv *env, jobject obj,jclass clazz, jmethodID methodID, const jvalue *args);

  • NativeType CallNonvirtual<type>MethodV(JNIEnv *env, jobject obj,jclass clazz, jmethodID methodID, va_list args); 同Call<type>Method都是調(diào)用指定類實例的指定方法伏伯,jmethodID必須是從類實例的jclass獲取,CallNonvirtual<type>Method多了一個參數(shù)jclass捌袜,但是jclass不一定是obj的jclass说搅,也可以是obj的父類的jclass,無論哪一個虏等,jmethodID都是從指定的jclass clazz獲取的弄唧,最終調(diào)用的方法實現(xiàn)取決于jmethodID适肠。簡單的說如果子類繼承并改寫了父類的方法,Call<type>Method只能調(diào)用改寫后的方法候引,而CallNonvirtual<type>Method即可以調(diào)用父類的原始方法侯养,也可以調(diào)用子類的改寫方法。這里的虛方法是C++中的概念澄干,C++中如果一個方法不是虛方法逛揩,子類繼承并改寫了該方法,則當(dāng)子類實例向上強(qiáng)轉(zhuǎn)成父類實例時調(diào)用的就是父類而非子類的改寫方法麸俘。如果子類繼承并改寫的方法被定義成虛方法辩稽,則會通過虛函數(shù)表的方式保證當(dāng)子類實例向上強(qiáng)轉(zhuǎn)成父類實例時調(diào)用的依然是子類的改寫方法,即所謂的多態(tài)从媚,參考《C++ 類公有繼承逞泄,多態(tài),虛函數(shù)静檬,抽象基類》。NativeType和type的對應(yīng)關(guān)系同Call<type>Method并级。
    注意上述方法和字段操作都是針對非靜態(tài)的方法和字段的拂檩,對靜態(tài)的方法和字段操作的API如下:

  • jfieldID GetStaticFieldID(JNIEnv *env, jclass clazz,const char *name, const char *sig); 獲取指定靜態(tài)方法的ID,同上會觸發(fā)未初始化類的初始化

  • NativeType GetStatic<type>Field(JNIEnv *env, jclass clazz,jfieldID fieldID);讀取指定靜態(tài)字段的值嘲碧,NativeType和type的對應(yīng)關(guān)系同Get<type>Field

  • void SetStatic<type>Field(JNIEnv *env, jclass clazz,jfieldID fieldID, NativeType value); 改寫指定靜態(tài)字段的值稻励,NativeType和type的對應(yīng)關(guān)系同Set<type>Field

  • jmethodID GetStaticMethodID(JNIEnv *env, jclass clazz,const char *name, const char *sig); 獲取指定靜態(tài)方法的ID,會導(dǎo)致未初始化類的初始化

  • NativeType CallStatic<type>Method(JNIEnv *env, jclass clazz,jmethodID methodID, ...);

  • NativeType CallStatic<type>MethodA(JNIEnv *env, jclass clazz,jmethodID methodID, jvalue *args);

  • NativeType CallStatic<type>MethodV(JNIEnv *env, jclass clazz,jmethodID methodID, va_list args); 上述三個方法都是調(diào)用指定靜態(tài)方法

注意在調(diào)用方法或者設(shè)置屬性傳參數(shù)時愈涩,需要密切關(guān)注參數(shù)類型望抽,尤其是基本類型,只有字段或者方法聲明明確使用了基本類型傳參才能使用基本類型履婉,否則必須通過Integer等包裝類的構(gòu)造方法將基本類型轉(zhuǎn)換為對應(yīng)包裝類的對象煤篙;另一個需要注意的就是可變參數(shù)類型,Java中可以傳入數(shù)量可變的參數(shù)毁腿,這些參數(shù)最終會被編譯器轉(zhuǎn)換成一個數(shù)組辑奈,即這類參數(shù)類型實際是一個數(shù)組,因此傳參時不能跟Java一樣已烤,而需要顯示的傳入一個數(shù)組類型鸠窗。示例如下:

package jni;
 
import java.util.Arrays;
import java.util.List;
 
class SuperA{
    public void say(){
        System.out.println("say SuperA");
    }
 
    public void add(int a,int b){
        System.out.println("SuperA add a->"+a+",b->"+b+",result->"+(a+b));
    }
}
 
public class FiledMethodTest extends SuperA {
 
    static {
        System.load("/home/openjdk/cppTest/FiledMethodTest.so");
    }
 
    private List<Integer> list;
 
    private boolean boolField;
 
    private byte byteField;
 
    private char charField;
 
    private short shortField;
 
    private int intField;
 
    private long longField;
 
    private float floatField;
 
    private double doubleField;
 
    private static int staticFiled;
 
    public FiledMethodTest() {
        list= Arrays.asList(1,2);
        boolField=false;
        byteField=11;
        charField='c';
        shortField=12;
        intField=13;
        longField=14;
        floatField=15.0f;
        doubleField=16.0;
        staticFiled=17;
    }
 
    @Override
    public void say() {
        System.out.println("say FiledMethodTest");
    }
 
 
    public List<Integer> getList() {
        return list;
    }
 
    private static void printList(List list){
        if(list==null){
            System.out.println("list is null");
        }else {
            System.out.println("list->" + list);
        }
    }
 
    private void printObj() {
        System.out.println("FiledMethodTest{" +
                "list=" + list +
                ", boolField=" + boolField +
                ", byteField=" + byteField +
                ", charField=" + charField +
                ", shortField=" + shortField +
                ", intField=" + intField +
                ", longField=" + longField +
                ", floatField=" + floatField +
                ", doubleField=" + doubleField +
                ", staticFiled=" + staticFiled +
                '}');
    }
 
    public native static void test(FiledMethodTest a);
 
    public static void main(String[] args) throws Exception {
        FiledMethodTest a=new FiledMethodTest();
        System.out.println("start test");
        test(a);
 
    }
}
#include "FiledMethodTest.h"
#include <stdio.h>
 
JNIEXPORT void JNICALL Java_jni_FiledMethodTest_test(JNIEnv * env, jclass jcl,
        jobject obj) {
 
    //注意字段和方法描述符中如果是其他的類,必須帶上后面的分號
    jfieldID listId = env->GetFieldID(jcl, "list", "Ljava/util/List;");
    jfieldID boolFieldId = env->GetFieldID(jcl, "boolField", "Z");
    jfieldID byteFieldId = env->GetFieldID(jcl, "byteField", "B");
    jfieldID charFieldId = env->GetFieldID(jcl, "charField", "C");
    jfieldID shortFieldId = env->GetFieldID(jcl, "shortField", "S");
    jfieldID intFieldId = env->GetFieldID(jcl, "intField", "I");
    jfieldID longFieldId = env->GetFieldID(jcl, "longField", "J");
    jfieldID floatFieldId = env->GetFieldID(jcl, "floatField", "F");
    jfieldID doubleFieldId = env->GetFieldID(jcl, "doubleField", "D");
    jfieldID staticFiledId = env->GetStaticFieldID(jcl, "staticFiled", "I");
 
    jmethodID printListId = env->GetStaticMethodID(jcl, "printList",
            "(Ljava/util/List;)V");
    jmethodID printObjId = env->GetMethodID(jcl, "printObj", "()V");
    jmethodID getListId = env->GetMethodID(jcl, "getList",
            "()Ljava/util/List;");
    jmethodID sayId=env->GetMethodID(jcl,"say","()V");
    jmethodID addId=env->GetMethodID(jcl,"add","(II)V");
 
    jclass arrayListCls = env->FindClass("java/util/ArrayList");
    jmethodID list_costruct = env->GetMethodID(arrayListCls, "<init>", "()V");
    jmethodID listAddId = env->GetMethodID(arrayListCls, "add",
            "(Ljava/lang/Object;)Z");
 
    jclass arraysCls=env->FindClass("java/util/Arrays");
    jmethodID asListId=env->GetStaticMethodID(arraysCls,"asList","([Ljava/lang/Object;)Ljava/util/List;");
 
    jclass intergerCls = env->FindClass("java/lang/Integer");
    jmethodID interger_costruct = env->GetMethodID(intergerCls, "<init>",
            "(I)V");
 
    //如果找不到方法或者字段不會直接報錯胯究,需要手動執(zhí)行異常檢查
    if (env->ExceptionCheck()) {
        jthrowable err = env->ExceptionOccurred();
        env->Throw(err);
    }
 
    jobject listObj = env->GetObjectField(obj, listId);
    env->CallStaticVoidMethod(jcl, printListId, listObj);
 
    jobject listObj2 = env->CallObjectMethod(obj, getListId);
    jboolean issame = env->IsSameObject(listObj, listObj2);
    printf("issame->%d\n", issame);
 
    jboolean boolField = env->GetBooleanField(obj, boolFieldId);
    printf("boolField->%d\n", boolField);
 
    jbyte byteField = env->GetByteField(obj, byteFieldId);
    printf("byteField->%d\n", byteField);
 
    jchar charField = env->GetCharField(obj, charFieldId);
    printf("charField->%d\n", charField);
 
    jshort shortField = env->GetShortField(obj, shortFieldId);
    printf("shortField->%d\n", shortField);
 
    jint intField = env->GetIntField(obj, intFieldId);
    printf("intField->%d\n", intField);
 
    jlong longField = env->GetLongField(obj, longFieldId);
    printf("longField->%d\n", longField);
 
    jfloat floatField = env->GetFloatField(obj, floatFieldId);
    printf("floatField->%f\n", floatField);
 
    jdouble doubleField = env->GetDoubleField(obj, doubleFieldId);
    printf("doubleField->%f\n", doubleField);
 
    jint staticFiled = env->GetStaticIntField(jcl, staticFiledId);
    printf("staticFiled->%d\n", staticFiled);
 
    //JNI中沒有對基本類型的自動裝箱拆箱機(jī)制稍计,必要時需要手動包裝
    jobject intObj = env->NewObject(intergerCls, interger_costruct, 3);
    jobject intObj2 = env->NewObject(intergerCls, interger_costruct, 4);
 
//  jobject newList = env->NewObject(arrayListCls, list_costruct);
    //add方法接受的參數(shù)實際是一個對象,因此需要手動包裝
//  env->CallBooleanMethod(newList, listAddId, intObj);
//  env->CallBooleanMethod(newList, listAddId, intObj2);
 
    //Arrays.asList方法在Java中是不可變參數(shù)裕循,實際多個參數(shù)最終會被轉(zhuǎn)變成數(shù)組臣嚣,因此這里的入?yún)⒈仨毷菙?shù)組
    jobjectArray objArray=env->NewObjectArray(2,intergerCls,intObj);
    env->SetObjectArrayElement(objArray,1,intObj2);
    jobject newList=env->CallStaticObjectMethod(arraysCls,asListId,objArray);
 
    env->SetObjectField(obj, listId, newList);
    env->SetBooleanField(obj, boolFieldId, 1);
    env->SetByteField(obj, byteFieldId, 21);
    env->SetCharField(obj, charFieldId, 'd');
    env->SetShortField(obj, shortFieldId, 22);
    env->SetIntField(obj, intFieldId, 23);
    env->SetLongField(obj, longFieldId, 24);
    env->SetFloatField(obj, floatFieldId, 25.0);
    env->SetDoubleField(obj, doubleFieldId, 26.0);
    env->SetStaticIntField(jcl, staticFiledId, 27);
 
    env->CallVoidMethod(obj, printObjId);
 
    jclass superCls=env->GetSuperclass(jcl);
    jmethodID superSayId=env->GetMethodID(superCls,"say","()V");
    //如果子類沒有覆寫則使用父類的實現(xiàn)净刮,否則使用子類覆寫的實現(xiàn)
    env->CallVoidMethod(obj,sayId);
    env->CallVoidMethod(obj,addId,3,4);
    //使用jclass的方法實現(xiàn),可以是子類的也可以是父類的,取決于后面的methodId
    env->CallNonvirtualVoidMethod(obj,jcl,sayId);
    env->CallNonvirtualVoidMethod(obj,jcl,superSayId);
    env->CallNonvirtualVoidMethod(obj,superCls,sayId);
    env->CallNonvirtualVoidMethod(obj,superCls,superSayId);
 
}

注意上述示例中printObj和printList方法都是私有方法茧球,但是通過JNI接口一樣可以正常調(diào)用庭瑰,說明JNI無視Java的訪問權(quán)限控制,可以訪問任何方法和字段抢埋。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末弹灭,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子揪垄,更是在濱河造成了極大的恐慌穷吮,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,695評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件饥努,死亡現(xiàn)場離奇詭異捡鱼,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)酷愧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,569評論 3 399
  • 文/潘曉璐 我一進(jìn)店門驾诈,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人溶浴,你說我怎么就攤上這事乍迄。” “怎么了士败?”我有些...
    開封第一講書人閱讀 168,130評論 0 360
  • 文/不壞的土叔 我叫張陵闯两,是天一觀的道長。 經(jīng)常有香客問我谅将,道長漾狼,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,648評論 1 297
  • 正文 為了忘掉前任饥臂,我火速辦了婚禮逊躁,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘隅熙。我一直安慰自己志衣,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 68,655評論 6 397
  • 文/花漫 我一把揭開白布猛们。 她就那樣靜靜地躺著念脯,像睡著了一般。 火紅的嫁衣襯著肌膚如雪弯淘。 梳的紋絲不亂的頭發(fā)上绿店,一...
    開封第一講書人閱讀 52,268評論 1 309
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼假勿。 笑死借嗽,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的转培。 我是一名探鬼主播恶导,決...
    沈念sama閱讀 40,835評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼浸须!你這毒婦竟也來了惨寿?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,740評論 0 276
  • 序言:老撾萬榮一對情侶失蹤删窒,失蹤者是張志新(化名)和其女友劉穎裂垦,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體肌索,經(jīng)...
    沈念sama閱讀 46,286評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡蕉拢,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,375評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了诚亚。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片晕换。...
    茶點故事閱讀 40,505評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖站宗,靈堂內(nèi)的尸體忽然破棺而出闸准,到底是詐尸還是另有隱情,我是刑警寧澤份乒,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布恕汇,位于F島的核電站腕唧,受9級特大地震影響或辖,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜枣接,卻給世界環(huán)境...
    茶點故事閱讀 41,873評論 3 333
  • 文/蒙蒙 一颂暇、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧但惶,春花似錦耳鸯、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,357評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至添谊,卻和暖如春财喳,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,466評論 1 272
  • 我被黑心中介騙來泰國打工耳高, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留扎瓶,地道東北人。 一個月前我還...
    沈念sama閱讀 48,921評論 3 376
  • 正文 我出身青樓泌枪,卻偏偏與公主長得像概荷,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子碌燕,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,515評論 2 359

推薦閱讀更多精彩內(nèi)容