Dalvik类的加载-源码阅读笔记

前言

本文主要研究Android dalvik虚拟机加载类的流程和机制。目的是了解Android中DEX文件结构,虚拟机如何从DEX文件中加载一个Java Class,以及到最终如何初始化这个类直至可被正常使用。

[Java]类的加载

在Java的世界里,所有类的加载,都由 java.lang.ClassLoader 来负责。ClassLoader是一个抽象类,它有多个实现类,例如 BootClassLoader , SystemClassLoader 以及虚拟机的具体实现,例如在dalvik虚拟机里的实现为 DexClassLoader

需要注意的每个虚拟机对于类的加载的逻辑并不十分相同,例如hotspot虚拟机和dalvik虚拟机加载一个类的过程基本上完全不同,hotspot主要是从class文件从加载类,而dalvik是从dex文件里去加载一个类,所以这里只讨论的是dalvik虚拟机里的实现机制。

双亲委派机制

不管虚拟机的具体实现,但虚拟机spec定义的关于类的加载规范必须被实现,例如最基础的双亲委派机制。

它规定每个ClassLoader都得有一个父亲ClassLoader,以此形成一个父子的多层级关系,利用这个层级关系实现了类的双亲委派机制。

在dalvik里的ClassLoader层级关系如下:

  • Bootstrap ClassLoader
  • System ClassLoader
  • Dex ClassLoader

在任何一个ClassLoader加载一个类的时候,都会先委托其父ClassLoader来负责加载这个类,一直递归到最顶层的ClassLoader,这个设计主要应该是为了安全考虑。以保障上层的类被上层的ClassLoader来加载,而避免系统类被下层的ClassLoader给替换掉了,而引发安全问题。

例如 DexClassLoader 加载类的时候,会先委托 SystemClassLoader 来加载该类,而 SystemClassLoader 又会先让它的父亲 BootClassLoader 先来加载,如果其所有祖父们都不加载该类,才会由这个ClassLoader去加载。

具体的代码实现为:

    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // First, check if the class has already been loaded
            Class c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                }
            }
            return c;
    }

源码来至 : /java/lang/ClassLoader.java

源码简单解读:

  • 先检查这个类是否被加载过,如果已经被加载过,则直接返回,不重新加载该类。(注意这里已经加过的类的列表并没有存储到Java层面,而是直接去问native层这个类是否被加载过,它维护了所有加载过的类)
  • 递归委派父ClassLoader去加载。
  • 如果所有父ClassLoader都不加载,自己才有权利去加载该类。
  • 先去找到该Class(具体的findClass逻辑由子类实现)

类加载器(ClassLoader)

ClassLoader作为基类,关键的方法都交给子类具体去实现了,但它定义了类的加载过程:

load -> find -> define -> resolve

Class<?> loadClass(String name, boolean resolve);

Class<?> findClass(String name);

Class<?> defineClass(String name, byte[] b, int off, int len, ProtectionDomain protectionDomain);

void resolveClass(Class<?> c);

SystemClassLoader

SystemClassLoader是一个ClassLoader默认的parent,即在创建一个ClassLoader时,不传入parent参数,则默认会使用这个SystemClassLoader作为其parent。

它其实是ClassLoader的一个静态内部类,里面包含了一个默认的ClassLoader,也是由ClassLoader的静态方法来创建的:

    static private class SystemClassLoader {
        public static ClassLoader loader = ClassLoader.createSystemClassLoader();
    }

     /**
     * Encapsulates the set of parallel capable loader types.
     */
    private static ClassLoader createSystemClassLoader() {
        String classPath = System.getProperty("java.class.path", ".");
        String librarySearchPath = System.getProperty("java.library.path", "");
        return new PathClassLoader(classPath, librarySearchPath, BootClassLoader.getInstance());
    }

    public static ClassLoader getSystemClassLoader() {
        return SystemClassLoader.loader;
    }

由代码可以看出 SystemClassLoader其实是一个全局静态的单例类,并且它的parent为 BootClassLoader。

BootClassLoader

BootClassLoader也定义在ClassLoader类,但外部不能访问到。也是一个单例类。它是ClassLoader的子类。它是唯一一个没有parent的ClassLoader

class BootClassLoader extends ClassLoader {

    private static BootClassLoader instance;

    @FindBugsSuppressWarnings("DP_CREATE_CLASSLOADER_INSIDE_DO_PRIVILEGED")
    public static synchronized BootClassLoader getInstance() {
        if (instance == null) {
            instance = new BootClassLoader();
        }

        return instance;
    }

    public BootClassLoader() {
        super(null);
    }
    
    // ...
}

其中loadClass里,先查找已经加载的类,由以下方法来实现,该方法是native方法 :

VMClassLoader#findLoadedClass(ClassLoader cl, String name);

然后findClass是由 Class.classForName 来实现的,也是一个native方法。

[Android]类的加载

加载Dex(DexClassLoader)

在dalvik里面的ClassLoader主要是由 DexClassLoaderPathClassLoader 来完成,这两个类属于 android/platform/libcore 项目,源码可查看AOSP:

https://android.googlesource.com/platform/libcore/+/master/dalvik/src/main/java/dalvik/system/

DexClassLoader 继承 dalvik.system.BaseDexClassLoader ,只提供一个构造器,并没有实现代码,主要代码还是在父类里面。

我们现在看 BaseDexClassLoader ,它继承于 ClassLoader. 先来看它的构造器:

    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, librarySearchPath, null);
    }

这里需要传四个参数,也基本能看出这个类的主要结构。

  • dexPath: 这个参数的名字虽然是一个path结尾的,但不是传一个目录,而是传 .dex .zip,.apk,.jar的文件绝对路径,但可以一次传多个,用 : 作为分隔即可。
  • optimizedDirectory: 这个目录是dex的优化目录,必须是当前用户,并且可写.(也可以为 null,系统会使用默认目录,一般是 /data/app/dalvik-cache)
  • librarySearchPath: native库的搜索目录,可以用 : 作为分隔符传入多个目录,也可以传入 null .
  • parent: parent ClassLoader

这里面关键的参数都传给了 DexPathList 对象了,可见大多数逻辑都会交给它来处理。

findClass

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        Class c = pathList.findClass(name, suppressedExceptions);
        if (c == null) {
            ClassNotFoundException cnfe = new ClassNotFoundException(
                    "Didn't find class \"" + name + "\" on path: " + pathList);
            for (Throwable t : suppressedExceptions) {
                cnfe.addSuppressed(t);
            }
            throw cnfe;
        }
        return c;
    }

如推测的一样,具体的逻辑都交给 DexPathList 去实现了。那么接下来我们就来研究它。需要注意的是 BaseDexClassLoader 只重写了 findClass 这个方法,而没有重写 loadClass , defineClassresolveClass !

Dex列表(DexPathList)

这个类时传入的 dexPath 的抽象,因为dexPath可能会传入用 : 分隔的多个apk文件,而每个apk文件中又可能有多个dex文件,因此 DexPathList 包含了所有apk文件里面的所有dex文件的封装。并将每个Dex文件抽象成 DexFile 对象,包裹在 DexPathList#Element 列表中。

private Element[] dexElements;

在处理dexPath的时候,首先从 : 分隔符split成数组,然后遍历这些文件,将其解析成 DexFile 对象,并封装到 Element 中。

for (File file : files) {
    // ...
    if (name.endsWith(DEX_SUFFIX)) {
        // .dex
        DexFile dex = loadDexFile(file, optimizedDirectory, loader, elements);
        if (dex != null) {
            elements[elementsPos++] = new Element(dex, null);
        }
    } else { 
        // .zip .jar
        // ...
        DexFile dex = loadDexFile(file, optimizedDirectory, loader, elements);
        // ...
    }
    // ...
}

在调用 findClass 的时候,会遍历这些DexFile文件,从Dex中寻找具体的class:

for (Element element : dexElements) {
    Class<?> clazz = element.findClass(name, definingContext, suppressed);
    if (clazz != null) {
        return clazz;
    }
}

除了传入一个dexPath,还可以传入一个 ByteBuffer 数组,每个byteBuffer里面包含了一个dex文件的字节流。并且 optimizedDirectory 参数是可以为 NULL 的。

除了dex文件,DexPathList还负责管理所有的native库,并将其也维护在一个列表中:

/** List of native library path elements. */
private final NativeLibraryElement[] nativeLibraryPathElements;

nativeLibrary的searchPath可以是一个普通路径,也可以是一个zip的路径。

当查找一个library的时候,会去从这些目录下寻找文件,例如:findLibrary(String filename) ,那么会去遍历这些目录,直到找到 path/filename 存在(并可读)的时候,则返回该library文件(如so文件)

寻找Class文件(DexFile)

dalvik.system.DexFile 这个类是对 Dex文件的抽象,具体在Dex文件中寻找Class定义的工作,则是由这个类来处理。

首先它会将dex文件打开,并读取成VM cookie object对象(具体的读取dex逻辑是由native方法实现)。

public Class loadClass(String name, ClassLoader loader) {
    String slashName = name.replace('.', '/');
    return loadClassBinaryName(slashName, loader, null);
}

private static native Class defineClassNative(String name, ClassLoader loader, Object cookie,
                                              DexFile dexFile)
            throws ClassNotFoundException, NoClassDefFoundError;

至于真正的 loadClass() 逻辑其实还是在defineClassNative() 这个native方法里完成的。

到此为止,Java层的关于关于class的加载逻辑基本已经了解,具体的很多工作都是native层是去完成,因此接下来我们来研究native层的。

[c++]类的加载

读取DEX文件(openDexFile)

这里我们接着上面 DexFile 的 native 方法来看,c++的源码在:

\dalvik2\vm\native\dalvik_system_DexFile.cpp

首先在native层会将dex文件打开并映射到内存中:

static void Dalvik_dalvik_system_DexFile_openDexFile(const u4* args,
    JValue* pResult)
{
  // ...
  DexOrJar* pDexOrJar = NULL;
  // ...
  if (hasDexExtension(sourceName)
            && dvmRawDexFileOpen(sourceName, outputName, &pRawDexFile, false) == 0) {
        ALOGV("Opening DEX file '%s' (DEX)", sourceName);

        pDexOrJar = (DexOrJar*) malloc(sizeof(DexOrJar));
        pDexOrJar->isDex = true;
        pDexOrJar->pRawDexFile = pRawDexFile;
        pDexOrJar->pDexMemory = NULL;
  }
  // ...
  RETURN_PTR(pDexOrJar);
}

读取dex文件的逻辑在 dvmRawDexFileOpen 函数中,它会去读取dex文件,并将其内部数据映射到一块只读的共享内存中去。具体负责内存映射的逻辑在 dexFileParse 函数中。

加载类的流程

首先来看,虚拟机对于一个类的加载流程,分为如下几个状态,从中大概能看到整个流程都经过了什么。

类的加载状态 ClassStatus :

name value note
CLASS_ERROR -1
CLASS_NOTREADY 0
CLASS_IDX 1 loaded, DEX idx in super or interfaces
CLASS_LOADED 2 DEX idx values resolved
CLASS_RESOLVED 3 part of linking
CLASS_VERIFYING 4 in the process of being verified
CLASS_VERIFIED 5 logically part of linking, done pre-init
CLASS_INITIALIZING 6 class init in process
CLASS_INITIALIZED 7 ready to go

这个函数比较关键,它主要负责在dex文件去中查找和加载class,但也比较长,因此在此做省略操作,只保留关键部分,我们从中提取关键点进行分析:

static void Dalvik_dalvik_system_DexFile_defineClass(const u4* args,
    JValue* pResult)
{
    // ...

    if (pDexOrJar->isDex)
        pDvmDex = dvmGetRawDexFileDex(pDexOrJar->pRawDexFile);
    else
        pDvmDex = dvmGetJarFileDex(pDexOrJar->pJarFile);
    
    // ...

    clazz = dvmDefineClass(pDvmDex, descriptor, loader);
    // ...
}

这个函数前面做了一些转换工作就不分析了,首先它会去拿到可以映射到内存的dex文件,关键的一句在于调用 dvmDefineClass 函数,该函数会从dex里去查找和加载class。

该函数定义在 dalvik2/vm/oo/Class.cpp 文件中,具体的函数为:

static ClassObject* findClassNoInit(const char* descriptor, Object* loader,
    DvmDex* pDvmDex)
{
    // 在已经加载过的类中去寻找,如果已经加载过,则不用再进行加载了
    clazz = dvmLookupClass(descriptor, loader, true);
    if (clazz == null) {
        // 在Dex文件中寻找Class定义
        pClassDef = dexFindClass(pDvmDex->pDexFile, descriptor);
      
        // 找到了Class的定义后,将其加载成ClassObject对象
        ClassObject* clazz = loadClassFromDex(pDvmDex, pClassDef, loader);
        // ...
      
        // 记录该类到已加载过类的hash table中,便于下次检查
        dvmAddClassToHash(clazz);
        // ...
        
        // 链接这个Class
        dvmLinkClass(clazz);
    }
}

这个函数则是最为核心的逻辑,我们详细来分析:

首先,查找已经加载过的类,所有加载过的类都会将其classDescriptor(类的描述,类似于 Ljava.lang.Object; 这种字符串),将这个类的描述符进行hash,并作为加载的ClassObject的键丢到一个hash table里面去,每次加载一个类之前,都会先去这个hash table里面去找一下,看之前有没有加载过该类,如果加载过则不重复加载,直接从这个hash table里面返回ClassObject对象。否则才会去加载该类。

第一步:寻找

如果这个类从来没有加载过,则从Dex文件中去寻找Class的定义,这个过程是交给 DexFile.cpp 源码里的 dexFindClass 函数去完成的。

这个类也比较关键,也不太好理解,因为贴出完整的代码,我们仔细分析:

/*
 * 通过类描述符查找一个类的定义 
 *
 * 类描述符"descriptor"应该例如:"Landroid/debug/Stuff;".
 */
const DexClassDef* dexFindClass(const DexFile* pDexFile,
    const char* descriptor)
{
    const DexClassLookup* pLookup = pDexFile->pClassLookup;
    u4 hash;
    int idx, mask;

    hash = classDescriptorHash(descriptor);
    mask = pLookup->numEntries - 1;
    idx = hash & mask;

    /*
     * 遍历DexClassLookup,直到找到Class的定义
     */
    while (true) {
        int offset;

        offset = pLookup->table[idx].classDescriptorOffset;
        if (offset == 0)
            return NULL;

        // 先比对类描述符的hash值
        if (pLookup->table[idx].classDescriptorHash == hash) {
            const char* str;

            str = (const char*) (pDexFile->baseAddr + offset);
            // hash值匹配后,再比对类描述符的字符串
            if (strcmp(str, descriptor) == 0) {
                // 找到匹配指定描述符的类
                return (const DexClassDef*)
                    (pDexFile->baseAddr + pLookup->table[idx].classDefOffset);
            }
        }

        idx = (idx + 1) & mask;
    }
}

该函数主要描述了如何从一个dex文件中去寻找一个类的定义。

第二步:装载

根据找到的类的定义ClassDef,加载这个类的信息,去dex文件中去创建一个 ClassObject 对象,并将其存在已经加载过的类的hash table里面,下次find这个class的时候,就不会重复去加载这个class了,直接从hash table里面拿。

这个过程由 loadClassFromDex0 函数负责:

/*
 * Helper for loadClassFromDex, which takes a DexClassDataHeader and
 * encoded data pointer in addition to the other arguments.
 */
static ClassObject* loadClassFromDex0(DvmDex* pDvmDex,
        const DexClassDef* pClassDef, const DexClassDataHeader* pHeader,
        const u1* pEncodedData, Object* classLoader)
{
    newClass = (ClassObject*) dvmMalloc(size, ALLOC_NON_MOVING);
    dvmSetClassSerialNumber(newClass);
    
    // 类的签名
    newClass->descriptor = descriptor;
    // 类的状态 -> Loaded
    newClass->status = CLASS_IDX;

    // 父类
    newClass->super = (ClassObject*) pClassDef->superclassIdx;
  
    // 接口列表
    pInterfacesList = dexGetInterfacesList(pDexFile, pClassDef);
    
    // 静态变量(并设置为默认值0或null)
    newClass->sfieldCount = count;
    for (i = 0; i < count; i++) {
        dexReadClassDataField(&pEncodedData, &field, &lastIndex);
        loadSFieldFromDex(newClass, &field, &newClass->sfields[i]);
    }

    // 成员变量
    for (i = 0; i < count; i++) {
        dexReadClassDataField(&pEncodedData, &field, &lastIndex);
        loadIFieldFromDex(newClass, &field, &newClass->ifields[i]);
    }
    dvmLinearReadOnly(classLoader, newClass->ifields);

    // 成员方法
    newClass->directMethodCount = count;
    newClass->directMethods = (Method*) dvmLinearAlloc(classLoader,
            count * sizeof(Method));
    for (i = 0; i < count; i++) {
        dexReadClassDataMethod(&pEncodedData, &method, &lastIndex);
        loadMethodFromDex(newClass, &method, &newClass->directMethods[i]);
    }
    dvmLinearReadOnly(classLoader, newClass->directMethods);

    // 虚方法(父类方法)
    newClass->virtualMethodCount = count;
    newClass->virtualMethods = (Method*) dvmLinearAlloc(classLoader,
            count * sizeof(Method));
    for (i = 0; i < count; i++) {
        dexReadClassDataMethod(&pEncodedData, &method, &lastIndex);
        loadMethodFromDex(newClass, &method, &newClass->virtualMethods[i]);
    }
    dvmLinearReadOnly(classLoader, newClass->virtualMethods);

    // 字节码
    newClass->sourceFile = dexGetSourceFile(pDexFile, pClassDef);
}

注意这里的classLoader就是用来加载类的,一般都是java层传过来的。如果loader是null的话,则会去加载系统class,例如 java.lang.Class 类。

这里会在 heap 里分配一块内存(使用 dvmMalloc 函数),来创建一个 ClassObject 对象,用于放置类的信息。包括:

  • descriptor 类的签名
  • 类的字段对象 fieldObject
  • 类的状态为:CLASS_IDX
  • 类的父类
  • 类的接口列表
  • 类的静态成员变量信息(设置标准默认值:null和0)
  • 类的成员变量信息
  • 类的方法
  • 类的虚方法(父类方法)
  • 类的字节码

第三步:链接

bool dvmLinkClass(ClassObject* clazz) { }

link的过程又分为:

  • 加载 LOADED,父类和接口都为NULL
  • 链接 RESOLVED,将父类和接口的dex idx链接替换成真正的引用
  • 验证 VERIFIED,检查一个类的,例如:
    • 如果该类是 java.lang.Object,则不能再有父类
    • 如果不是java.lang.Object, 则必须有父类
    • 不能继承final类
    • 不能继承interface
    • 只能继承public的类或同一个包下的类
    • 等等

所有这些逻辑都在 java_lang_Class.cpp里的:

第四步:初始化

bool dvmInitClass(CLassObject* clazz) { }

初始化之前,必须确保该类已经被验证过(VERIFIED)如果没有,则立即先验证它。

如果没有优化过类,则先优化类(optimize),

然后做一些验证工作,和线程安全的操作(因为可能多个线程同时引发初始化某个类,所以会使用当前类锁对初始化过程加锁)。

接下来就开始初始化过程:

  • 先递归的初始化父类 super class,它的父类的父类,直到它们都先被初始化了。****
  • 初始化静态常量 static final,它会从dex里面将静态变量的值拿出去(根据偏移量),赋值到类的静态变量上。
  • 执行静态区的代码 static {},所有写在静态区的代码都会合并到静态方法里面,该方法的名字为 <clinit>,签名为 ()V。它被当做一个正常的静态方法被调用。dvmCallMethod()
  • 到此,如果没有遇到异常,则该类被视为初始化完毕,可以正常使用,状态也变为 CLASS_INITIALIZED.

DexFile文件结构

我们对虚拟机加载一个类的整个过程基本有了一定了解,因为在加载的过程中,基本都是在和 DexFile 文件在打交道,因此作为扩展知识,我们也顺便了解 DexFile 的文件结构。

首先要明确 Android 之所以使用 Dex 文件来代替Java里的Jar包文件,主要是为了解决在手机这种存储空间有限的设备上能更进一步的压缩空间的考虑。

传统的jar包文件,里面存储了一个个分散的class文件,而dex文件其实是将一个个分散的class文件合并成一个文件。

因此dex的文件结构基本上和class的文件结构非常相关或相似,所有先来看见看class文件的结构:

一个class文件基本上可以分为几块内容:

  • 基本信息(如magic,minor version, major version, access flags, this class, super class等信息)
  • 常量池 Constant Pool,它包含了所有基本类型,string,类名,字段名,方法名,类型等常量
  • 接口列表 Interfaces ,它是该类的所有实现接口的列表,包含了所有接口的class,其中className也是指向常量池里的一个classInfo
  • 字段列表 fields, 它包括该类所有字段的列表,每个字段包括三个信息:Name字段名,Descriptor字段类型签名,access flags访问权限(public/private/protected/static/final等)
  • 方法列表 methods, 它包括该类的所有定义的方法的列表,每个方法包括:Name方法名,Descriptor方法签名,access flags访问权限,以及具体的字节码code.

接下来我们先来看 DexFile 的定义:

struct DexFile {
    /* directly-mapped "opt" header */
    const DexOptHeader* pOptHeader;

    // DEX文件头指针,包含所有指针的偏移量和长度
    const DexHeader*    pHeader;
    // 字符串列表指针,UTF-16编码
    const DexStringId*  pStringIds;
    // 类型列表指针
    const DexTypeId*    pTypeIds;
    // 字段列表指针
    const DexFieldId*   pFieldIds;
    // 方法列表指针
    const DexMethodId*  pMethodIds;
    // 函数原型数据指针,方法声明的字符串,返回类型和参数列表
    const DexProtoId*   pProtoIds;
    // 类的定义列表指针,类的信息,包括接口,超类,类信息,静态变量偏移量等
    const DexClassDef*  pClassDefs;
    // 静态连接数据
    const DexLink*      pLinkData;

    /*
     * These are mapped out of the "auxillary" section, and may not be
     * included in the file.
     */
    const DexClassLookup* pClassLookup;
    const void*         pRegisterMapPool;       // RegisterMapClassPool

    /* points to start of DEX file data */
    const u1*           baseAddr;

    /* track memory overhead for auxillary structures */
    int                 overhead;

    /* additional app-specific data structures associated with the DEX */
    //void*               auxData;
};

可以看出来基本一个Dex就是将多个Class的信息整合到一起,这样的好处是,例如常量池这些都可以共享,从而减少了整体的存储空间。

结语

看完dalvik虚拟机对于类的加载流程的相关源码以后,对一个类是如何被加载到虚拟机有了一个新的认知,并从中看到Android dalvik是如何实现Java虚拟机spec的等一些细节,和hotspot的实现还是有比较大的区别的,对Dex文件,Class文件的格式也有了一个更直观的了解。下一步需要了解的应该是虚拟机内存相关的部分。

源码索引:

java/lang/ClassLoader.java

    loadClass(findClass)

    defineClass

    resolveClass
dalvik.system.BaseDexClassLoader.java

    findClass
dalvik2/vm/oo/class.cpp

    loadMethodFromDex
dalvik2/vm/native.cpp

    dvmResolveNativeMethod

    unregisterJNINativeMethods
dalvik2/vm/jni.cpp

    dvmRegisterJNIMethod

参考资料:

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

推荐阅读更多精彩内容