深入理解JNI

  1. JNI概述
  2. 学习JNI实例:MediaScanner
  3. 注册JNI函数
  4. 数据类型转换
  5. JNIEnv介绍

一、JNI概述

JNI全称,JavaNativeInterface——Java提供了Java层与Native层交互的桥梁。通过JNI技术我们可以做到如下:

  1. Java程序中的函数可以调用Native[C/C++]语言编写的函数。
  2. Native层中的函数可以调用Java层的函数,也就是说C/C++函数中可以调用Java函数。

二、JNI学习实例:MediaScanner类

MediaScanner类中的部分函数由Native层实现,JNI层中对应的是libmedia_jni.so,media_jni为JNI库的名字。libmedia.so库完成了实际功能。MediaScanner通过JNI库libmedia_jni.so和Native层的libmedia.so进行交互。

#2.1 Java层的MediaScanner分析
  • MediaScanner
class MediaScanner {
    ......
    static {
        System.loadLibrary("media_jni");
        native_init();
    }
    ......
    private static native final void native_init();
    ......
}

Media类中的静态代码块执行了两个操作:

  1. 加载media_jni库。
  2. 调用native_init()完成native层初始化操作。
    Java函数中调用native函数,必须通过位于JNI层的动态库来实现。一般采用的做法是在类的static代码块中,通过调用System.loadLibrary(String libraryName)来完成对动态库的加载。
  • 加载JNI库
    由System类的静态成员函数loadLibrary负责加载动态库。
  • System
public static void loadLibrary(String libname) {
    Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname);
}

loadLibrary方法中调用了Runtime类的成员函数loadLibrary0();

  • Runtime
synchronized void loadLibrary0(ClassLoader loader, String libname) {
    if (libname.indexOf((int)File.separatorChar) != -1) {
        throw new UnsatisfiedLinkError(
"Directory separator should not appear in library name: " + libname);
    }
    String libraryName = libname;

    //如果 loader不为空进入该分支
    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) + "\"");
        }
        //加载库
        String error = doLoad(filename, loader);
        if (error != null) {
            throw new UnsatisfiedLinkError(error);
        }
        return;
    }

    String filename = System.mapLibraryName(libraryName);
    List<String> candidates = new ArrayList<String>();
    String lastError = 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;
        }
    }

    if (lastError != null) {
        throw new UnsatisfiedLinkError(lastError);
    }
    throw new UnsatisfiedLinkError("Library " + libraryName + " not found; tried " + candidates);
}

loadLibrary0方法中所完成的操作是,如果ClassLoader不为空,则调用其成员函数findLibrary获取到库所在的路径,然后调用Runtime类的成员函数doLoad对库进行加载,doLoad函数则将具体的加载过程转发给Runtime类中定义的native层函数nativeLoad,进而完成后续加载过程。

  • ClassLoader.findLibrary

获取库所在的本地路径。

/**
 * Returns the absolute path name of a native library.  The VM invokes this
 * method to locate the native libraries that belong to classes loaded with
 * this class loader. If this method returns <tt>null</tt>, the VM
 * searches the library along the path specified as the
 * "<tt>java.library.path</tt>" property.
 *
 * @param  libname
 *         The library name
 *
 * @return  The absolute path of the native library
 *
 * @see  System#loadLibrary(String)
 * @see  System#mapLibraryName(String)
 *
 * @since  1.2
 */
protected String findLibrary(String libname) {
    return null;
}
  • Runtime.doLoad

调用native层函数nativeLoad完成对库加载。

private String doLoad(String name, ClassLoader loader) {
    ......
    // internal natives.
    synchronized (this) {
        return nativeLoad(name, loader, librarySearchPath);
    }
}

nativeLoad后续的执行步骤大致为:

  1. 调用dlopen函数,打开一个so文件并创建一个handle;
  2. 调用dlsym()函数,查看相应的so文件的JNI_OnLoad()函数指针,并执行相应函数。
#2.2 JNI层的MediaScanner分析

MediaScanner的JNI层代码在android_media_MediaScanner.cpp中,如下所示:

  • android_media_MediaScanner.cpp
static void android_media_MediaScanner_native_init(JNIEnv *env)
{
    ALOGV("native_init");
    jclass clazz = env->FindClass(kClassMediaScanner);
    if (clazz == NULL) {
        return;
    }

    fields.context = env->GetFieldID(clazz, "mNativeContext", "J");
    if (fields.context == NULL) {
        return;
    }
}

Java层的native_init对应JNI层的android_media_MediaScanner_native_init,下面详细分析其绑定过程。

当Java层调用native_init函数时,它会从对应的JNI库中寻找Java_android_media_Media_Scanner_native_init函数,如果没有,就会报错。如果找到,则会为这个native_init和Java_android_media_Media_Scanner_native_init建立一个关联关系,其实就是保存JNI函数的函数指针。以后调用native_init函数时,直接使用这个函数指针就可以了,这项工作是由虚拟机完成的。


三、注册JNI函数

“注册”是将Java层的native函数和JNI层对应的实现函数关联起来。JNI函数注册的方式有两种:

  • 静态注册
  • 动态注册
#3.1 静态注册

静态注册的大体流程如下:

  • 先编写Java代码,然后编译生成.class文件。
  • 使用Java的工具程序javah,如javah -o output packagename.classname,这样会生成一个叫output.h的JNI层头文件。其中packagename.classname是Java代码编译后的class文件,而在生成的output.h文件里,声明了对应的JNI层函数,只要实现里面的函数即可。
#3.1.1 :编写Java代码
package com.next.hhu.jnidemo;

public class StaticJNITest {
    public static native int add(int a, int b);
}
#3.1.2 编译生成.class文件

执行[1]操作进入到java目录下,然后执行[2]操作生成.class文件。

//[1] 进入到java目录下
cd app/src/main/java
//[2] 生成.class文件
javac com/next/hhu/jnidemo/StaticJNITest.java
#3.1.3 生成头文件

然后利用javah命令生成头文件,这样会在/java目录下生成com_next_hhu_jnidemo_StaticJNITest.h头文件。

javah com.next.hhu.jnidemo.StaticJNITest
#3.1.4 配置CMakeLists.text文件
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html

# Sets the minimum version of CMake required to build the native library.

cmake_minimum_required(VERSION 3.4.1)

# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.

add_library( # Sets the name of the library.
             native-lib

             # Sets the library as a shared library.
             SHARED

             # Provides a relative path to your source file(s).
             src/main/cpp/native-lib.cpp)

add_library( # Sets the name of the library.
             native-lib1

             # Sets the library as a shared library.
             SHARED

             # Provides a relative path to your source file(s).
             src/main/cpp/com_next_hhu_jnidemo_StaticJNITest.cpp)

# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.

find_library( # Sets the name of the path variable.
              log-lib

              # Specifies the name of the NDK library that
              # you want CMake to locate.
              log )

# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.

target_link_libraries( # Specifies the target library.
                       native-lib

                       # Links the target library to the log library
                       # included in the NDK.
                       ${log-lib} )
#3.2 动态注册

JNI中用JNINativeMethod结构来记录Java的Native方法和JNI方法的关联关系。

  • jni.h
typedef struct {
    const char* name; //Java方法名
    const char* signature; //Java方法的签名信息
    void*       fnPtr; //JNI中对应的函数指针
} JNINativeMethod;

MediaScanner JNI层中采用的是静态注册。

#3.2.1 定义JNINativeMethod[]

定义一个JNINativeMethod[],其成员就是MediaScanner中所有成员函数的一一对应关系。

android_media_MediaScanner.cpp

static JNINativeMethod gMethods[] = {
    {
        "processDirectory",
        "(Ljava/lang/String;Landroid/media/MediaScannerClient;)V",
        (void *)android_media_MediaScanner_processDirectory
    },

    {
        "processFile",
        "(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V",
        (void *)android_media_MediaScanner_processFile
    },

    {
        "setLocale",
        "(Ljava/lang/String;)V",
        (void *)android_media_MediaScanner_setLocale
    },

    {
        "extractAlbumArt",
        "(Ljava/io/FileDescriptor;)[B",
        (void *)android_media_MediaScanner_extractAlbumArt
    },

    {
        "native_init",
        "()V",
        (void *)android_media_MediaScanner_native_init
    },

    {
        "native_setup",
        "()V",
        (void *)android_media_MediaScanner_native_setup
    },

    {
        "native_finalize",
        "()V",
        (void *)android_media_MediaScanner_native_finalize
    },
};
#3.2.2 注册JNINativeMetod[]
  • android_media_MediaScanner.cpp
// This function only registers the native methods, and is called from
// JNI_OnLoad in android_media_MediaPlayer.cpp
int register_android_media_MediaScanner(JNIEnv *env)
{
    return AndroidRuntime::registerNativeMethods(env,
                kClassMediaScanner, gMethods, NELEM(gMethods));
}

调用AndroidRuntime的registerNativeMethods函数来完成注册工作。

  • AndroidRuntime.cpp
/*
 * Register native methods using JNI.
 */
/*static*/ int AndroidRuntime::registerNativeMethods(JNIEnv* env,
    const char* className, const JNINativeMethod* gMethods, int numMethods)
{
    return jniRegisterNativeMethods(env, className, gMethods, numMethods);
}

调用JNIHelp.c中的jniRegisterNativeMethods方法来完成注册。

  • JNIHelp.c
int jniRegisterNativeMethods(JNIENV *env,const char *className,
                            const JNINativeMethod *gMethods,int numMethods) 
{
    jclass clazz;
    clazz = (*env)->findClass(env,className);
    ......
    //实际上调用JNIEnv的RegisterNatives函数完成注册的
    if((*env)->RegisterNatives(env,clazz,gMethods,numMethods) < 0) {
        return -1;
    }
    return 0;
}
#3.2.3 JNI_OnLoad函数中调用注册函数

当Java层通过System.loadLibrary加载完成JNI动态库后,紧接着会查找该库中一个叫JNI_OnLoad的函数。如果有,就调用它,动态注册工作在这里完成。

//该函数的第一个参数类型为JavaVM,代表JNI层的Java虚拟机,每一个Java进程有且仅有一个
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserve) {
    JNIEnv *env = NULL;
    jint result = -1;
    
    if (vm->GetEnv((void **) &env, JNI_VERSION_1_4) != JNI_OK) {
        goto bail;
    }
    if (env == NULL) {
        return -1;
    }
    jclass clazz = env->FindClass("com/next/hhu/jnidemo/JNIHelper");
    if (clazz == NULL) {
        return -1;
    }
    ......
    //动态注册MediaScanner的JNI函数
    if(register_android_media_MediaScanner(env) < 0) {
        goto bail;
    }
    .....
    return JNI_VERSION_1_4;//必须返回这个值,否则会报错。
}

四、数据类型转换

#4.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
#4.2 数组数据类型
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
#4.3 引用数据类型转换
Signature格式 Java Native
Ljava/lang/String; String jstring
L+classname+; 所有对象 jobject
[L+classname+; Object[] jobjectArray
Ljava.lang.Class; Class jclass
Ljava.lang.Throwable; Throwable jthrowable
#4.4 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;

五、JNIEnv介绍

JNIEnv是一个与线程相关的代表JNI环境的结构体,JNIEnv提供了一些JNI系统函数。通过这些函数可以做到:

  • 调用JAVA函数。
  • 操作jobject对象等很多事情。

JNIEnv是一个线程相关的变量。由于线程相关,所以不能在一个线程中使用另一个线程的JNIEnv结构体,有一种情况当后台线程收到一个网络消息,而又需要由Native层函数主动回调Java层函数时,JNIEnv该如何处理?

//全进程只有一个JavaVM对象,所以可以保存,并且在任何地方使用都没有问题。
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved);

JavaVM和JNIEnv之间的关系:

  • 调用JavaVM的AttachCurrentThread函数,就可以得到这个线程的JNIEnv结构体。这样就可以在后台线程中回调Java函数。
  • 另外,在后台线程退出前,需要调用JavaVM的DetachCurrentThread的函数来释放对应的资源。
/*
 * C++ object wrapper.
 *
 * This is usually overlaid on a C struct whose first element is a
 * JNINativeInterface*.  We rely somewhat on compiler behavior.
 */
struct _JNIEnv {
   /* do not rename this; it does not seem to be entirely opaque */
    const struct JNINativeInterface* functions;

#if defined(__cplusplus)
......
#endif /*__cplusplus*/
};
#5.1 通过JNIEnv操作jobject

Java的引用类型除了少数几个外,最终在JNI层都会用jobject来表示对象的数据类型,操作jobject步骤:

#5.1.1 jfieldID和jmethodID介绍
    jfieldID    (*GetFieldID)(JNIEnv*, jclass clazz, const char *name, const char *sig);
    jmethodID   (*GetMethodID)(JNIEnv*, jclass clazz, const char *name, const char *sig);

其中,jclass代表Java类,name表示成员函数或成员变量的名字,sig为这个函数和变量的签名信息。

public:
    MyMediaScannerClient(JNIEnv *env, jobject client)
        :   mEnv(env),
            mClient(env->NewGlobalRef(client)),
            mScanFileMethodID(0),
            mHandleStringTagMethodID(0),
            mSetMimeTypeMethodID(0)
    {
        ALOGV("MyMediaScannerClient constructor");
        jclass mediaScannerClientInterface =
                env->FindClass(kClassMediaScannerClient);

        if (mediaScannerClientInterface == NULL) {
            ALOGE("Class %s not found", kClassMediaScannerClient);
        } else {
            mScanFileMethodID = env->GetMethodID(
                                    mediaScannerClientInterface,
                                    "scanFile",
                                    "(Ljava/lang/String;JJZZ)V");

            mHandleStringTagMethodID = env->GetMethodID(
                                    mediaScannerClientInterface,
                                    "handleStringTag",
                                    "(Ljava/lang/String;Ljava/lang/String;)V");

            mSetMimeTypeMethodID = env->GetMethodID(
                                    mediaScannerClientInterface,
                                    "setMimeType",
                                    "(Ljava/lang/String;)V");
        }
    }

将scanFile和handleStringTag函数的jmethodId保存为MyMediaScannerClient的成员变量,供后续使用。

#5.1.2 使用jfieldID和jmethodID
    virtual status_t scanFile(const char* path, long long lastModified,
            long long fileSize, bool isDirectory, bool noMedia)
    {
        ALOGV("scanFile: path(%s), time(%lld), size(%lld) and isDir(%d)",
            path, lastModified, fileSize, isDirectory);

        jstring pathStr;
        if ((pathStr = mEnv->NewStringUTF(path)) == NULL) {
            mEnv->ExceptionClear();
            return NO_MEMORY;
        }
        
        //调用JNIEnv的CallVoidMethod函数,注意CallVoidMethod的参数:
        //第一个参数代表MediaScannerClient的jobject对象,
        //第二个参数是函数scanFile的jmethodID,后面是Java中scanFile的参数。
        mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified,
                fileSize, isDirectory, noMedia);

        mEnv->DeleteLocalRef(pathStr);
        return checkAndClearExceptionFromCallback(mEnv, "scanFile");
    }

通过JNIEnv输出CallVoidMethod,再把jobject、jmethodID和对应的参数传进去,JNI层就能够调用Java对象的函数了。

#5.2 jstring介绍

Java中String为引用类型,JNI规范中单独创建一个jstring类型来表示Java中的String类型。

  • 调用JNIEnv的NewString(JNIEnv *env,const jchar *unicodeChars,jsize len),可以从Native字符串得到一个jstring对象。可以把jstring对象看作是Java中String对象在JNI层的代表,也就是说jstring是一个Java String。由于Java String中存储的是Unicode字符串,所以NewString函数的参数也必须是Unicode字符串。
  • 调用JNIEnv的NewStringUTF将根据Native的一个UTF-8字符串得到一个jstring对象。
  • JNIEnv中GetStringChars和GetStringUTFChars函数,它们可以将Java String对象转换成本地字符串。
  • 如果在代码中调用了上面几个函数,在做完相关工作后,就需要调用ReleaseStringChars或ReleaseStringUTFChars函数来对应地释放资源,否则会导致JVM内存泄漏。
static void
android_media_MediaScanner_processFile(
        JNIEnv *env, jobject thiz, jstring path,
        jstring mimeType, jobject client)
{
    ALOGV("processFile");

    // Lock already hold by processDirectory
    MediaScanner *mp = getNativeScanner_l(env, thiz);
    if (mp == NULL) {
        jniThrowException(env, kRunTimeException, "No scanner available");
        return;
    }

    if (path == NULL) {
        jniThrowException(env, kIllegalArgumentException, NULL);
        return;
    }

    const char *pathStr = env->GetStringUTFChars(path, NULL);
    if (pathStr == NULL) {  // Out of memory
        return;
    }

    const char *mimeTypeStr =
        (mimeType ? env->GetStringUTFChars(mimeType, NULL) : NULL);
    if (mimeType && mimeTypeStr == NULL) {  // Out of memory
        // ReleaseStringUTFChars can be called with an exception pending.
        env->ReleaseStringUTFChars(path, pathStr);
        return;
    }

    MyMediaScannerClient myClient(env, client);
    MediaScanResult result = mp->processFile(pathStr, mimeTypeStr, myClient);
    if (result == MEDIA_SCAN_RESULT_ERROR) {
        ALOGE("An error occurred while scanning file '%s'.", pathStr);
    }
    env->ReleaseStringUTFChars(path, pathStr);
    if (mimeType) {
        env->ReleaseStringUTFChars(mimeType, mimeTypeStr);
    }
}
#5.3 JNI类型签名介绍

Java中支持函数重载,也就是说,可以定义同名但不同参数的函数。因此,仅仅靠函数名没办法找到具体的函数。JNI技术中将参数类型和返回值类型的组合作为一个函数的签名信息,有了签名信息和函数名,就能找到Java中的函数了。
Java中提供了javap的工具来帮助生成函数或变量的签名信息。

//XXX是编译后的.class文件
javap -s -p XXX
C:\Users\hhu\Desktop\11\JNIDemo\app\src\main\java>javap -s -p com.next.hhu.jnidemo.StaticJNITest
Compiled from "StaticJNITest.java"
public class com.next.hhu.jnidemo.StaticJNITest {
  public com.next.hhu.jnidemo.StaticJNITest();
    descriptor: ()V

  public static native int add(int, int);
    descriptor: (II)I

  static {};
    descriptor: ()V
}
#5.3 垃圾回收

JNI提供了三种引用类型:

  • Local Reference:本地引用。在JNI层函数中使用的非全局引用对象都是Local Reference,它包括函数调用时传入的jobject和在JNI层函数中创建的jobject。Local Reference最大特点是,一旦JNI层函数返回,这些object就可能被垃圾回收。
  • Global Reference:全局引用,这种对象如不主动释放,它永远不会被垃圾回收。
  • Weak Global Reference:弱全局引用,一种特殊的Global Reference,在运行过程中可能会被垃圾回收,因此在使用它之前,需要调用JNIEnv的IsSameObject判断它是否被回收了。
public:
    MyMediaScannerClient(JNIEnv *env, jobject client)
        :   mEnv(env),
            mClient(env->NewGlobalRef(client)),
    
    {
       ......
    }

    virtual ~MyMediaScannerClient()
    {
        ALOGV("MyMediaScannerClient destructor");
        mEnv->DeleteGlobalRef(mClient);
    }
#5.4 JNI中的异常处理

如果调用JNIEnv的某些函数出错了,则会产生一个异常,但这个异常不会中断本地函数的执行,直到从JNI层返回到Java层后,虚拟机才会抛出这个异常。虽然在JNI层中产生的异常不会中断本地函数的运行,但一旦产生异常后,就只能做一些资源清理工作。所以安全编码显得十分重要。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,539评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,911评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,337评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,723评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,795评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,762评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,742评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,508评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,954评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,247评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,404评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,104评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,736评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,352评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,557评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,371评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,292评论 2 352

推荐阅读更多精彩内容

  • 此文章为《深入理解Android卷 I》的读书笔记,笔者已经完成了卷一的第一遍阅读,第一遍时写下了一些笔记,现在开...
    pokerWu阅读 1,364评论 0 6
  • 什么是JNI? JNI 是java本地开发接口.JNI 是一个协议,这个协议用来沟通java代码和外部的本地代码(...
    a_tomcat阅读 2,819评论 0 54
  • 我不管你用什么甜言蜜语 我不会被你吸引 爱不是做作的副产 它需要你用心去培育 我没有什么高智商 去领会你的心意 我...
    啊Ben阅读 220评论 0 2
  • 失恋了,你可以伤心难过,但千万别作践自己,别总是痛哭流涕,动不动呼天抢地,毫无原则地低到尘埃里,让自己显得廉价和低...
    不哭不闹衒耀阅读 162评论 0 0
  • 历史上每次的金融变革都离不开技术的创新,从传统金融到互联网金融的发展,都是基于安全的条件下,以金融业经营成...
    煎饼果籽阅读 268评论 0 0