Android中的ClassLoader分析

本文基于Android5.0.0_r2分析

Java和Android中的classloader的区别

Java系统的ClassLoader:
  1. Bootstrap ClassLoader 最顶层加载类,主要加载核心类库 rt.jar, resources.jar, charset.jar等
  2. Extention ClassLoader 扩展的类加载器
  3. AppClassLoader 加载当前应用的classpath的所有类

普通类的类加载器是AppClassLoader,AppClassLoader的parent是ExtClassLoader,ExtClassLoader没有parent,BootstrapClassLoader是C++写的,无法在java代码中获取它的引用。JVM初始化sun.misc.Launcher并创建ExtClassLoader和AppClassLoader并将ExtClassLoader设置为AppClassLader的父加载器。
Java中的ClassLoader使用了双亲委托机制,一个类加载器寻找class和resource时,先判断这个class是否加载成功,没有的话不是自己加载而是交给父加载器,然后递归下去直到BootstrapClassLoader,如果 BootstrapClassLoader找到了就直接返回,如果没有找到就一级一级返回,最后到达自身去加载,这就叫做双亲委托。每次加载都是先查找缓存中是否存在,也就是有没有加载过,没有的话就到各自加载器负责加载的路径下查找。比如BootstrapClassLoader负责的rt.jar classes.jar等。

Android的ClassLoader

不同于Java的ClassLoader,应该说不同于JVM,Android不加载单独的class文件,而是去加载dex文件。我们知道dex其实就是很多个class集合在一起。因为毕竟class是有结构上的冗余的,而dex文件则消除了这些冗余。


区别

那这时候JVM的classloader自然就排不上用场了,所以Android有自己的classloader去加载dex。

  1. PathClassLoader只能加载系统中已经安装过的apk
  2. DexClassLoader可以加载jar/apk/dex,可以指定加载目录

至于为什么,我们去代码里面看:

源码分析

查看源码可以在http://androidxref.com/5.0.0_r2/网站上看,如果自己down下源码来的,也可以借助eclipse或者source insight查看。
我们定两个我们渴望知道的两个问题:

  1. DexClassLoader跟PathClassLoader的区别,是怎么体现的。
  2. findClass是怎么实现的。

我们带着问题来看源码:

DexClassLoader和PathClassLoader

//DexClassLoader

public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }
}

//PathClassLoader

public class PathClassLoader extends BaseDexClassLoader {
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }
    public PathClassLoader(String dexPath, String libraryPath,
            ClassLoader parent) {
        super(dexPath, null, libraryPath, parent);
    }
}

上面就是出掉注释后两个类的所有代码。什么都没干,而且没有实际loadClass方法,而且构造方法也只是调用了super,很明显逻辑代码都是在父类BaseDexClassLoader里面实现的。

其实这两个类的区别就是构造方法参数数量的区别:

  1. DexClassLoader调用的是四个参数的构造方法。
  2. PathClassLoader分别调用了两个参数的构造方法和三个参数的构造方法,这俩构造方法的区别就是传不传so库的地址。
谜团都在BaseDexClassloader里:
class BaseDexClassLoader extends ClassLoader {
    private final DexPathList pathList;

    /**
     * Constructs an instance. 故意没有删这个注释,可以看一下这四个参数的意思:
     *
     * @param dexPath            the list of jar/apk files containing classes and resources, delimited by {@code File.pathSeparator}, which defaults to {@code ":"} on Android
     * @param optimizedDirectory directory where optimized dex files should be written; may be {@code null} 
     * @param libraryPath        the list of directories containing native libraries, delimited by {@code File.pathSeparator}; may be {@code null}
     * @param parent             the parent class loader
     */
    public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }

    @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;
    }
...
}

BaseDexClassLoader还是没干什么具体的逻辑,我们看得出来其实真正做事情的其实是这个叫做DexPathList的类,整个BaseDexClassLoader其实是对DexPathList的功能做了一个包装,功能都是通过DexPathList来实现的。
我们看一下构造函数这四个参数的意思:

  1. dexPath:dex或者包含dex的jar/apk文件路径,多个需要用File.pathSeparator分隔开
  2. optimizedDirectory:odex的被存放的路径,可以为null,这里就能看出来区别,PathClassLoader设置的是null,DexClassLoader设置的是非null。
  3. libraryPath:本地库的文件路径,多个需要用File.pathSeparator分隔开
  4. parent:父classloader

我们的两个问题:

  1. 区别是有没有传optimizedDirectory,但是具体区别还是在DexPathList的构造方法里面,这里看不出来。
  2. findClass还是调用了DexPathList的findClass,这里找不到的时候抛了个异常。
DexPathList
class DexPathList {
    private static final String DEX_SUFFIX = ".dex";
    private final ClassLoader definingContext;

    /**
     * List of dex/resource (class path) elements.
     * Should be called pathElements, but the Facebook app uses reflection
     * to modify 'dexElements' (http://b/7726934). 看到这段注释的时候,真的是直白,直接写到源码里。。。
     */
    private final Element[] dexElements;
    private final File[] nativeLibraryDirectories;

    /**
     * @param definingContext    the context in which any as-yet unresolved classes should be defined
     * @param dexPath            list of dex/resource path elements, separated by {@code File.pathSeparator}
     * @param libraryPath        list of native library directory path elements, separated by {@code File.pathSeparator}
     * @param optimizedDirectory directory where optimized {@code .dex} files should be found and written to, or {@code null} to use the default system directory for same
     */
    public DexPathList(ClassLoader definingContext, String dexPath,
                       String libraryPath, File optimizedDirectory) {
        //definingContext和dexPath不能为空
        if (definingContext == null) {
            throw new NullPointerException("definingContext == null");
        }
        if (dexPath == null) {
            throw new NullPointerException("dexPath == null");
        }
//如果设置了optimizedDirectory,持续判空
        if (optimizedDirectory != null) {
            if (!optimizedDirectory.exists()) {
                throw new IllegalArgumentException("optimizedDirectory doesn't exist: " + optimizedDirectory);
            }
            if (!(optimizedDirectory.canRead() && optimizedDirectory.canWrite())) {
                throw new IllegalArgumentException( "optimizedDirectory not readable/writable: "  + optimizedDirectory);
            }
        }

        this.definingContext = definingContext;
        ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
        //":"分隔开的path 
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions);
        if (suppressedExceptions.size() > 0) {
            this.dexElementsSuppressedExceptions = suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
        } else {
            dexElementsSuppressedExceptions = null;
        }
        this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
    }
...
}

最重要的一句

this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions);

看splitDexPath这一串调用

//DexPathList.java
    private static ArrayList<File> splitDexPath(String path) {
        return splitPaths(path, null, false);
    }
//DexPathList.java
     private static ArrayList<File> splitPaths(String path1, String path2,
                                              boolean wantDirectories) {
        ArrayList<File> result = new ArrayList<File>();
        splitAndAdd(path1, wantDirectories, result);
        splitAndAdd(path2, wantDirectories, result);
        return result;
    }
//DexPathList.java
    private static void splitAndAdd(String searchPath, boolean directoriesOnly,
                                    ArrayList<File> resultList) {
        if (searchPath == null) {
            return;
        }
        for (String path : searchPath.split(":")) {
            try {
                StructStat sb = Libcore.os.stat(path);
                if (!directoriesOnly || S_ISDIR(sb.st_mode)) {
                    resultList.add(new File(path));
                }
            } catch (ErrnoException ignored) {
            }
        }
    }

其实就是把冒号:分隔的dex路径add到一个ArrayList返回回来。我们看makeDexElements做了什么,这里也就是PathClassLoader跟DexClassLoader区分的地方:

      private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory,
                                             ArrayList<IOException> suppressedExceptions) {
        ArrayList<Element> elements = new ArrayList<Element>();
          for (File file : files) {
            File zip = null;
            DexFile dex = null;
            String name = file.getName();

            if (file.isDirectory()) {
                // We support directories for looking up resources.
                // This is only useful for running libcore tests. 支持目录,但只是做test的时候
                elements.add(new Element(file, true, null, null));
            } else if (file.isFile()) {
                if (name.endsWith(DEX_SUFFIX)) { //路径下是一个dex文件
                    // Raw dex file (not inside a zip/jar).
                    try {
                        dex = loadDexFile(file, optimizedDirectory);
                    } catch (IOException ex) {
                        System.logE("Unable to load dex file: " + file, ex);
                    }
                } else {
                    zip = file;
                    try {
                        dex = loadDexFile(file, optimizedDirectory);
                    } catch (IOException suppressed) {
                       suppressedExceptions.add(suppressed);
                    }
                }
            } else {
                System.logW("ClassLoader referenced unknown path: " + file);
            }

            if ((zip != null) || (dex != null)) {
                elements.add(new Element(file, false, zip, dex));
            }
        }

        return elements.toArray(new Element[elements.size()]);
    }
//PathDexList.java
    private static DexFile loadDexFile(File file, File optimizedDirectory) throws IOException {
        if (optimizedDirectory == null) {//PathClassLoader的分支
            return new DexFile(file);//构造一个DexFile来表示Dex
        } else {//DexClassLoader的分支,创建一个path来缓存dex
            String optimizedPath = optimizedPathFor(file, optimizedDirectory);
//这里file.getPath():需要load的dex路径;optimizedPath:load进来的dex要存在哪儿。
            return DexFile.loadDex(file.getPath(), optimizedPath, 0);
        }
    }
//PathDexList.java
    private static String optimizedPathFor(File path,
                                           File optimizedDirectory) {
        /*
         * Get the filename component of the path, and replace the
         * suffix with ".dex" if that's not already the suffix.
         *
         * We don't want to use ".odex", because the build system uses
         * that for files that are paired with resource-only jar
         * files. If the VM can assume that there's no classes.dex in
         * the matching jar, it doesn't need to open the jar to check
         * for updated dependencies, providing a slight performance
         * boost at startup. The use of ".dex" here matches the use on
         * files in /data/dalvik-cache.
         */
        String fileName = path.getName();
        if (!fileName.endsWith(DEX_SUFFIX)) {
            int lastDot = fileName.lastIndexOf(".");
            if (lastDot < 0) {
                fileName += DEX_SUFFIX;
            } else {
                StringBuilder sb = new StringBuilder(lastDot + 4);
                sb.append(fileName, 0, lastDot);
                sb.append(DEX_SUFFIX);
                fileName = sb.toString();
            }
        }
 //在optimizedDirectory目录下创建一个叫fileName的文件
        File result = new File(optimizedDirectory, fileName);
        return result.getPath();
    }

上面的代码中我们看到了PathClassLoader跟DexClassLoader的区分,因为PathClassLoader加载的是系统已经安装好了的,所以直接用就好了。
而DexClassLoader加载的是用户指定的目录下的dex或者包含dex的jar和apk,所以需要重新load。这里创建了一个dex,并且用DexFile.loadDex(file.getPath(), optimizedPath, 0);来把dex加载进来。
这里我们发现又多了个类叫做DexFile,这是存储了Dex文件一些属性的类,而loadDex又做了什么我们DexFile.java里面怎么做的。

final class DexFile {
    private long mCookie;
    private final String mFileName;
    private final CloseGuard guard = CloseGuard.get();

    public DexFile(File file) throws IOException {
        this(file.getPath());
    }

    public DexFile(String fileName) throws IOException {
        mCookie = openDexFile(fileName, null, 0);
        mFileName = fileName;
        guard.open("close");
    }
//指定了name的dex,也就是DexClassLoader需要调用的地方
    private DexFile(String sourceName, String outputName, int flags) throws IOException {
        if (outputName != null) {
            try {
                String parent = new File(outputName).getParent();
                if (Libcore.os.getuid() != Libcore.os.stat(parent).st_uid) {
                    throw new IllegalArgumentException("Optimized data directory " + parent
                            + " is not owned by the current user. Shared storage cannot protect"
                            + " your application from code injection attacks.");
                }
            } catch (ErrnoException ignored) {
                // assume we'll fail with a more contextual error later
            }
        }

        mCookie = openDexFile(sourceName, outputName, flags);
        mFileName = sourceName;
        guard.open("close");
        //System.out.println("DEX FILE cookie is " + mCookie + " sourceName=" + sourceName + " outputName=" + outputName);
    }

/**
     * Open a DEX file, specifying the file in which the optimized DEX
     * data should be written. If the optimized form exists and appears
     * to be current, it will be used; if not, the VM will attempt to
     * regenerate it.
     * This is intended for use by applications that wish to download
     * and execute DEX files outside the usual application installation
     * mechanism. This function should not be called directly by an
     * application; instead, use a class loader such as
     * dalvik.system.DexClassLoader. //注释没删,这里能看到说这个方法应该只会被DexClassLoader调用到。
     *
     * @param sourcePathName Jar or APK file with "classes.dex". (May expand this to include
     *                       "raw DEX" in the future.) dex文件或者包含dex的jar或者apk
     * @param outputPathName File that will hold the optimized form of the DEX data. dex需要存放的路径
     * @param flags          Enable optional features. (Currently none defined.)
     * @return A new or previously-opened DexFile.
     * @throws IOException If unable to open the source or output file.
     */
    static public DexFile loadDex(String sourcePathName, String outputPathName,
                                  int flags) throws IOException {
        //调用了上面三个参数的构造方法
        return new DexFile(sourcePathName, outputPathName, flags);
    }
...
    private static long openDexFile(String sourceName, String outputName, int flags) throws IOException {
        // Use absolute paths to enable the use of relative paths when testing on host.
        return openDexFileNative(new File(sourceName).getAbsolutePath(),
                (outputName == null) ? null : new File(outputName).getAbsolutePath(),
                flags);
    }
...
    private static native long openDexFileNative(String sourceName, String outputName, int flags);

}

我列出了构造方法用到的一些方法,我们看到其实最后是调用到了一个native方法得到了一个long值并存到了一个叫做mCookie的变量里,这个操作很眼熟,我之前写过一篇文章JNI中用long传递指针到java,JNI编程里面很多时候我们会把在C++中开辟的地址的指针传到java中存成long,后续的调用中我们再拿着这个指针去C++中就能定位到我们之前操作的那块地址。这里是不是也是这样子的呢?
我们在dalvik_system_DexFile.cc找到了这个方法
怎么找到的?

AndroidXRef

直接搜就完了,记得如果不知道在哪个包下面记得右边选择select all

//dalvik_system_DexFile.cc
static jlong DexFile_openDexFileNative(JNIEnv* env, jclass, jstring javaSourceName, jstring javaOutputName, jint) {
  ScopedUtfChars sourceName(env, javaSourceName);
  if (sourceName.c_str() == NULL) {
    return 0;
  }
  NullableScopedUtfChars outputName(env, javaOutputName);
  if (env->ExceptionCheck()) {
    return 0;
  }

  ClassLinker* linker = Runtime::Current()->GetClassLinker();
  std::unique_ptr<std::vector<const DexFile*>> dex_files(new std::vector<const DexFile*>());
  std::vector<std::string> error_msgs;

  bool success = linker->OpenDexFilesFromOat(sourceName.c_str(), outputName.c_str(), &error_msgs,
                                             dex_files.get());

  if (success || !dex_files->empty()) {
    // In the case of non-success, we have not found or could not generate the oat file.
    // But we may still have found a dex file that we can use. 返回了dex_files.release()的指针
    return static_cast<jlong>(reinterpret_cast<uintptr_t>(dex_files.release()));
  } else {
    // The vector should be empty after a failed loading attempt.
    DCHECK_EQ(0U, dex_files->size());

    ScopedObjectAccess soa(env);
    CHECK(!error_msgs.empty());
    // The most important message is at the end. So set up nesting by going forward, which will
    // wrap the existing exception as a cause for the following one.
    auto it = error_msgs.begin();
    auto itEnd = error_msgs.end();
    for ( ; it != itEnd; ++it) {
      ThrowWrappedIOException("%s", it->c_str());
    }

    return 0;
  }
}

跟我们想的一样,就是把std::vector<const DexFile>得指针给返回回来了,也就是说这个long指向了一个std::vector<const DexFile>,后面用到mCache的时候我们确认一下。
到这里我们看完了PathClassLoader跟DexClassLoader的构造方法。
回到PathDexList.java继续看findClass

//PathDexList.java
    public Class findClass(String name, List<Throwable> suppressed) {
        for (Element element : dexElements) {
            DexFile dex = element.dexFile;

            if (dex != null) {
                Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
                if (clazz != null) {
                    return clazz;
                }
            }
        }
        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }

//DexFile.java
    public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
        return defineClass(name, loader, mCookie, suppressed);
    }

    private static Class defineClass(String name, ClassLoader loader, long cookie,
                                     List<Throwable> suppressed) {
        Class result = null;
        try {
            result = defineClassNative(name, loader, cookie);
        } catch (NoClassDefFoundError e) {
            if (suppressed != null) {
                suppressed.add(e);
            }
        } catch (ClassNotFoundException e) {
            if (suppressed != null) {
                suppressed.add(e);
            }
        }
        return result;
    }

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

我们其实PathDexList.findClass还是调用了DexFile方法,而DexFile最终还是调用了一个native方法去获取Class文件,当然去获取的时候带上了之前初始化时候得到的mCache的JNI的指针。我们同样去dalvik_system_DexFile.cc确认一下是不是跟我们想的一样。

//dalvik_system_DexFile.cc
static jclass DexFile_defineClassNative(JNIEnv* env, jclass, jstring javaName, jobject javaLoader,
                                        jlong cookie) {
  std::vector<const DexFile*>* dex_files = toDexFiles(cookie, env);
  if (dex_files == NULL) {
    VLOG(class_linker) << "Failed to find dex_file";
    return NULL;
  }
  ScopedUtfChars class_name(env, javaName);
  if (class_name.c_str() == NULL) {
    VLOG(class_linker) << "Failed to find class_name";
    return NULL;
  }
  const std::string descriptor(DotToDescriptor(class_name.c_str()));

  for (const DexFile* dex_file : *dex_files) {
    const DexFile::ClassDef* dex_class_def = dex_file->FindClassDef(descriptor.c_str());
    if (dex_class_def != nullptr) {
      ScopedObjectAccess soa(env);
      ClassLinker* class_linker = Runtime::Current()->GetClassLinker();
      class_linker->RegisterDexFile(*dex_file);
      StackHandleScope<1> hs(soa.Self());
      Handle<mirror::ClassLoader> class_loader(
          hs.NewHandle(soa.Decode<mirror::ClassLoader*>(javaLoader)));
      mirror::Class* result = class_linker->DefineClass(descriptor.c_str(), class_loader, *dex_file,
                                                        *dex_class_def);
      if (result != nullptr) {
        VLOG(class_linker) << "DexFile_defineClassNative returning " << result;
        return soa.AddLocalReference<jclass>(result);
      }
    }
  }
  VLOG(class_linker) << "Failed to find dex_class_def";
  return nullptr;
}

static std::vector<const DexFile*>* toDexFiles(jlong dex_file_address, JNIEnv* env) {
  std::vector<const DexFile*>* dex_files = reinterpret_cast<std::vector<const DexFile*>*>(
      static_cast<uintptr_t>(dex_file_address));
  if (UNLIKELY(dex_files == nullptr)) {
    ScopedObjectAccess soa(env);
    ThrowNullPointerException(NULL, "dex_file == null");
  }
  return dex_files;
}

跟我们想的一样,传进来的mCache指针被转成了一个std::vector<const DexFile * >,然后得到遍历vector得到一个个的DexFile,然后用这个ClassLinker玩意儿找到class,然后包装成jclass返回回来。C的代码不了解也没看过,有兴趣的可以继续探索。然后DexFile大致长这样,可以根据数据结构看出来Dex文件的大致结构。

DexFile

好像很长了,总结一下。

  1. Java中使用双亲委托加载class文件,有AppClassLoader, ExtClassLoader, BootstrapClassLoader三个系统的classloader,另外还可以继承classloader实现自定义的ClassLoader
  2. Android有PathClassLoader和DexClassLoader,PathClassLoader加载系统中已经安装过的apk,DexClassLoader可以加载自定义目录。
  3. DexClassLoader和PathClassLoader,其实包括BasePathClassLoader都没什么逻辑代码,都是依靠DexPathList实现加载操作
  4. DexClassLoader和PathClassLoader的区别是调用BasePathClassLoader构造方法的时候传没传optimizedDirectory,这是他俩的特性决定的:PathClassLoader加载的是已经加载过的dex,DexClassLoader则是指定目录的dex,所以DexClassLoader需要做的要更多一步,就是把dex给load进来。
  5. 加载Dex和加载Class都是在native做的,其中会把native开辟的空间传到java存起来,后边用的时候再带过去。

参考文章:
热修复——深入浅出原理与实现

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