android so 加載過程源碼分析

Runtime.loadLibrary() 源碼分析

最近的用戶反饋威创,碰到一個(gè) loadLibrary() 失敗的問題病蛉,之前對(duì)這一個(gè)流程一直沒有進(jìn)行細(xì)致梳理杨帽,現(xiàn)在趁有空歹袁,梳理一下。

loadLibrary() 的流程

一般情況下靶瘸,通過 System.loadLibrary() 去加載你需要的 so 庫苫亦,如下:

System.loadLibrary("native-lib")

System 調(diào)用的代碼如下:

public static void loadLibrary(String libname) {
   Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname);
}

實(shí)際上毛肋,調(diào)用的是 Runtime 的 loadLibrary0() 函數(shù),Runtime 是每個(gè) Java 應(yīng)用的一個(gè)運(yùn)行時(shí)屋剑,并且getRutime() 獲得Runtime 對(duì)象是個(gè)單例:

public class Runtime {
    private static Runtime currentRuntime = new Runtime();
  
      public static Runtime getRuntime() {
        return currentRuntime;
    }
}

最終 loadLibrary0() 的源碼如下:

synchronized void loadLibrary0(ClassLoader loader, String libname) {
    if (libname.indexOf((int)File.separatorChar) != -1) {//判斷so名稱是否包含文件分隔符润匙,如果包含文件分隔符,則拋出異常
        throw new UnsatisfiedLinkError(
"Directory separator should not appear in library name: " + libname);
    }
    String libraryName = libname;
    if (loader != null) {
        String filename = loader.findLibrary(libraryName);//通過 ClassLoader 去 findLibrary()
        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 = nativeLoad(filename, loader);//調(diào)用 jni 方法唉匾,nativeLoad() 方法
        if (error != null) {
            throw new UnsatisfiedLinkError(error);
        }
        return;
    }

    String filename = System.mapLibraryName(libraryName);//使用 mapLibraryName() 方法查找實(shí)際的 so 庫的名稱
    List<String> candidates = new ArrayList<String>();
    String lastError = null;
    for (String directory : getLibPaths()) {//遍歷so 庫目錄孕讳,嘗試加載 so
        String candidate = directory + filename;
        candidates.add(candidate);

        if (IoUtils.canOpenReadOnly(candidate)) {
            String error = nativeLoad(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() 方法是 synchronize 的,所以如果多個(gè)線程調(diào)用巍膘,則需要等待厂财。 loadLibrary0() 方法主要分為以下幾步調(diào)用流程:

  1. 檢查 so 庫名稱,是否包含了文件分割符峡懈,如果包含則直接拋出異常璃饱。
  2. 如果 ClassLoader 不為空,調(diào)用 ClassLoader.findLibrary() 去查找so肪康,如果找不到則拋出 XXClassLoader couldn't find XXX.so.
  3. 如果 ClassLoader 不為空,調(diào)用 nativeLoad() 方法荚恶,去加載 so。源碼在libcore/ojluni/src/main/native/Runtime.c磷支,找到則返回谒撼。

后面的分支是 ClassLoader 為空的情況

  1. 如果 ClassLoader 為空,調(diào)用 System.mapLibraryName() 去獲取so 庫的完整名稱
  2. 如果 ClassLoader 為空,遍歷 getLibPaths()去查找 so齐唆,并且獲取路徑嗤栓。
  3. 調(diào)用 nativeLoad() 方法,使用完整 so 路徑去加載 so 庫

綜上箍邮,我們只要分析 ClassLoader.findLibrary() 和 nativeLoad() 方法即可茉帅。

loader.findLibrary() 方法

以包名為com.rockets.livedemo 為例子,對(duì)應(yīng)的 ClassLoader 為 PathClassLoader 如下:

dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.rockets.livedemo-ABxsabDzcYLzAaEA5wukSw==/base.apk"],nativeLibraryDirectories=[/data/app/com.rockets.livedemo-ABxsabDzcYLzAaEA5wukSw==/lib/arm, /data/app/com.rockets.livedemo-ABxsabDzcYLzAaEA5wukSw==/base.apk!/lib/armeabi-v7a, /system/lib]]]

其中锭弊,fileName 為:

/data/app/com.rockets.livedemo-ABxsabDzcYLzAaEA5wukSw==/lib/arm/libnative-lib.so

這里使用的 ClassLoader 是 PatchClassLoader ,其并沒有實(shí)現(xiàn) findLibrary() 方法堪澎,而是由其子類 BaseDexClassLoader 實(shí)現(xiàn)的,源碼如下:

this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted); 

@Override
 public String findLibrary(String name) {
   return pathList.findLibrary(name);
}

這里交由 DexPathList 的 findLibrary() 方法去實(shí)現(xiàn)味滞,如下:

public String findLibrary(String libraryName) {
    String fileName = System.mapLibraryName(libraryName);

    for (NativeLibraryElement element : nativeLibraryPathElements) {
        String path = element.findNativeLibrary(fileName);

        if (path != null) {
            return path;
        }
    }

    return null;
}

這里會(huì)先調(diào)用System.mapLibraryName() 去查找 so 庫的完整名稱樱蛤,具體的可以看后面針對(duì)這個(gè)函數(shù)的分析,總之這里會(huì)從你傳入的 native-lib 名稱剑鞍,變成 libnative-lib.so 這樣完整的名稱昨凡。

拿到完整的 so 庫的名稱之后,會(huì)通過遍歷 nativeLibraryPathElements 去查找 so 庫蚁署,那么 nativeLibraryPathElemts 是怎么初始化的便脊?如下:

NativeLibraryElement[] nativeLibraryPathElements;
/** List of application native library directories. */
private final List<File> nativeLibraryDirectories;
/** List of system native library directories. */
private final List<File> systemNativeLibraryDirectories;

this.nativeLibraryDirectories = splitPaths(librarySearchPath, false);
this.systemNativeLibraryDirectories =
          splitPaths(System.getProperty("java.library.path"), true);
     List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);
     allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);

this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories);

nativeLibraryDirectories 的獲取路徑是在安裝過程,賦值到 ApplicationInfo 中去的光戈,如下:

public class ApplicationInfo extends PackageItemInfo implements Parcelable {
/**
 * Full path to the directory where native JNI libraries are stored.
 */
public String nativeLibraryDir;
}

具體細(xì)節(jié)不分析哪痰,總的來說遂赠,如果 apk 是系統(tǒng)應(yīng)用,則 nativeLibraryDir 為 /system/lib/xxx 或者 system/app/xxx/lib晌杰。但是對(duì)于我們自己的應(yīng)用來說跷睦,這個(gè)值為 data/app/包名/lib。

以華為手機(jī)肋演,安裝抖音極速版為例:

抖音極速版的lib 目錄為 /data/data/com.ss.android.ugc.aweme.lite/lib抑诸,其中 com.ss.android.ugc.aweme.lite 為包名。

systemNativeLibraryDirectories 是通過靜態(tài)方法 getProperty() 去獲取惋啃,這里的代碼比較復(fù)雜哼鬓,直接跳過,最終我們獲取到的路徑是:

//http://androidxref.com/9.0.0_r3/xref/bionic/linker/linker.cpp
#if defined(__LP64__)
static const char* const kSystemLibDir     = "/system/lib64";
static const char* const kOdmLibDir        = "/odm/lib64";
static const char* const kVendorLibDir     = "/vendor/lib64";
static const char* const kAsanSystemLibDir = "/data/asan/system/lib64";
static const char* const kAsanOdmLibDir    = "/data/asan/odm/lib64";
static const char* const kAsanVendorLibDir = "/data/asan/vendor/lib64";
#else
static const char* const kSystemLibDir     = "/system/lib";
static const char* const kOdmLibDir        = "/odm/lib";
static const char* const kVendorLibDir     = "/vendor/lib";
static const char* const kAsanSystemLibDir = "/data/asan/system/lib";
static const char* const kAsanOdmLibDir    = "/data/asan/odm/lib";
static const char* const kAsanVendorLibDir = "/data/asan/vendor/lib";
#endif

static const char* const kAsanLibDirPrefix = "/data/asan";

static const char* const kDefaultLdPaths[] = {
  kSystemLibDir,
  kOdmLibDir,
  kVendorLibDir,
  nullptr
};

也就是說边灭,如果是有宏定義 LP64 則定義為 /system/lib64,/odm/lib64健盒,/vendor/lib64绒瘦,如果沒有這個(gè)宏定義則為 /system/lib,/odm/lib扣癣,/odm/lib惰帽。這里沒去細(xì)究 LP64 的定義,從字面理解就是 64 位的系統(tǒng)父虑,會(huì)有這個(gè)宏定義该酗。

所以,如果是 64 位系統(tǒng)士嚎,systemNativeLibraryDirectories 的值為 /system/lib64呜魄,/odm/lib64,/vendor/lib64莱衩,接著會(huì)調(diào)用 makePathElements(this.systemNativeLibraryDirectories) 去構(gòu)造 nativeLibraryPathElements,最終構(gòu)造了一系列的 NativeLibaryElemt爵嗅,其實(shí) NativeLibraryElemt 就是包括了兩個(gè)字段 File zip, String zipDir,大概如下:

    public NativeLibraryElement(File dir) {
        this.path = dir;
        this.zipDir = null;
    }
  • NativeLibraryElemt(File(data/app/包名/lib));
  • NativeLibraryElemt(File(/system/lib64));
  • NativeLibraryElemt(File(/odm/lib64));
  • NativeLibraryElemt(File(/vendor/lib64));

最終,調(diào)用 NativeLibraryElemt 的 findLibrary()方法笨蚁,如下:

    public String findNativeLibrary(String name) {
        maybeInit();

        if (zipDir == null) {//這里為 null
            String entryPath = new File(path, name).getPath();
            if (IoUtils.canOpenReadOnly(entryPath)) {
                return entryPath;
            }
        } else if (urlHandler != null) {
            // Having a urlHandler means the element has a zip file.
            // In this case Android supports loading the library iff
            // it is stored in the zip uncompressed.
            String entryName = zipDir + '/' + name;
            if (urlHandler.isEntryStored(entryName)) {
              return path.getPath() + zipSeparator + entryName; 
            }
        }

        return null;
    }

由于 zipDir 為空睹晒,所以這里最后返回的就是 File(path, name).getPath(),也就是默認(rèn)的文件夾名+完整的so庫名括细,比如 /data/data/包名/libnative-lib.so 伪很。

nativeLoad() 方法

從上一步拿到的完整路徑之后。

nativeLoad() 方法位于 Runtime.c 文件中奋单,如下:

JNIEXPORT jstring JNICALL
Runtime_nativeLoad(JNIEnv* env, jclass ignored, jstring javaFilename,
                   jobject javaLoader)
{
    return JVM_NativeLoad(env, javaFilename, javaLoader);
}

最終锉试,會(huì)調(diào)用到 java_vm_ext.cc 中的,LoadNativeLibrary() 方法中

bool JavaVMExt::LoadNativeLibrary(JNIEnv* env,
                                  const std::string& path,
                                  jobject class_loader,
                                  std::string* error_msg) {
  error_msg->clear();

  // See if we've already loaded this library.  If we have, and the class loader
  // matches, return successfully without doing anything.
  // TODO: for better results we should canonicalize the pathname (or even compare
  // inodes). This implementation is fine if everybody is using System.loadLibrary.
  SharedLibrary* library;
  Thread* self = Thread::Current();
  {
    // TODO: move the locking (and more of this logic) into Libraries.
    MutexLock mu(self, *Locks::jni_libraries_lock_);
    library = libraries_->Get(path);
  }
  void* class_loader_allocator = nullptr;
  {
    ScopedObjectAccess soa(env);
    // As the incoming class loader is reachable/alive during the call of this function,
    // it's okay to decode it without worrying about unexpectedly marking it alive.
    ObjPtr<mirror::ClassLoader> loader = soa.Decode<mirror::ClassLoader>(class_loader);

    ClassLinker* class_linker = Runtime::Current()->GetClassLinker();
    if (class_linker->IsBootClassLoader(soa, loader.Ptr())) {
      loader = nullptr;
      class_loader = nullptr;
    }

    class_loader_allocator = class_linker->GetAllocatorForClassLoader(loader.Ptr());
    CHECK(class_loader_allocator != nullptr);
  }
  if (library != nullptr) {
    // Use the allocator pointers for class loader equality to avoid unnecessary weak root decode.
    if (library->GetClassLoaderAllocator() != class_loader_allocator) {
      // The library will be associated with class_loader. The JNI
      // spec says we can't load the same library into more than one
      // class loader.
      //
      // This isn't very common. So spend some time to get a readable message.
      auto call_to_string = [&](jobject obj) -> std::string {
        if (obj == nullptr) {
          return "null";
        }
        // Handle jweaks. Ignore double local-ref.
        ScopedLocalRef<jobject> local_ref(env, env->NewLocalRef(obj));
        if (local_ref != nullptr) {
          ScopedLocalRef<jclass> local_class(env, env->GetObjectClass(local_ref.get()));
          jmethodID to_string = env->GetMethodID(local_class.get(),
                                                 "toString",
                                                 "()Ljava/lang/String;");
          DCHECK(to_string != nullptr);
          ScopedLocalRef<jobject> local_string(env,
                                               env->CallObjectMethod(local_ref.get(), to_string));
          if (local_string != nullptr) {
            ScopedUtfChars utf(env, reinterpret_cast<jstring>(local_string.get()));
            if (utf.c_str() != nullptr) {
              return utf.c_str();
            }
          }
          env->ExceptionClear();
          return "(Error calling toString)";
        }
        return "null";
      };
      std::string old_class_loader = call_to_string(library->GetClassLoader());
      std::string new_class_loader = call_to_string(class_loader);
      StringAppendF(error_msg, "Shared library \"%s\" already opened by "
          "ClassLoader %p(%s); can't open in ClassLoader %p(%s)",
          path.c_str(),
          library->GetClassLoader(),
          old_class_loader.c_str(),
          class_loader,
          new_class_loader.c_str());
      LOG(WARNING) << *error_msg;
      return false;
    }
    VLOG(jni) << "[Shared library \"" << path << "\" already loaded in "
              << " ClassLoader " << class_loader << "]";
    if (!library->CheckOnLoadResult()) {
      StringAppendF(error_msg, "JNI_OnLoad failed on a previous attempt "
          "to load \"%s\"", path.c_str());
      return false;
    }
    return true;
  }

  // Open the shared library.  Because we're using a full path, the system
  // doesn't have to search through LD_LIBRARY_PATH.  (It may do so to
  // resolve this library's dependencies though.)

  // Failures here are expected when java.library.path has several entries
  // and we have to hunt for the lib.

  // Below we dlopen but there is no paired dlclose, this would be necessary if we supported
  // class unloading. Libraries will only be unloaded when the reference count (incremented by
  // dlopen) becomes zero from dlclose.

  // Retrieve the library path from the classloader, if necessary.
  ScopedLocalRef<jstring> library_path(env, GetLibrarySearchPath(env, class_loader));

  Locks::mutator_lock_->AssertNotHeld(self);
  const char* path_str = path.empty() ? nullptr : path.c_str();
  bool needs_native_bridge = false;
  void* handle = android::OpenNativeLibrary(env,
                                            runtime_->GetTargetSdkVersion(),
                                            path_str,
                                            class_loader,
                                            library_path.get(),
                                            &needs_native_bridge,
                                            error_msg);

  VLOG(jni) << "[Call to dlopen(\"" << path << "\", RTLD_NOW) returned " << handle << "]";

  if (handle == nullptr) {
    VLOG(jni) << "dlopen(\"" << path << "\", RTLD_NOW) failed: " << *error_msg;
    return false;
  }

  if (env->ExceptionCheck() == JNI_TRUE) {
    LOG(ERROR) << "Unexpected exception:";
    env->ExceptionDescribe();
    env->ExceptionClear();
  }
  // Create a new entry.
  // TODO: move the locking (and more of this logic) into Libraries.
  bool created_library = false;
  {
    // Create SharedLibrary ahead of taking the libraries lock to maintain lock ordering.
    std::unique_ptr<SharedLibrary> new_library(
        new SharedLibrary(env,
                          self,
                          path,
                          handle,
                          needs_native_bridge,
                          class_loader,
                          class_loader_allocator));

    MutexLock mu(self, *Locks::jni_libraries_lock_);
    library = libraries_->Get(path);
    if (library == nullptr) {  // We won race to get libraries_lock.
      library = new_library.release();
      libraries_->Put(path, library);
      created_library = true;
    }
  }
  if (!created_library) {
    LOG(INFO) << "WOW: we lost a race to add shared library: "
        << "\"" << path << "\" ClassLoader=" << class_loader;
    return library->CheckOnLoadResult();
  }
  VLOG(jni) << "[Added shared library \"" << path << "\" for ClassLoader " << class_loader << "]";

  bool was_successful = false;
  void* sym = library->FindSymbol("JNI_OnLoad", nullptr);
  if (sym == nullptr) {
    VLOG(jni) << "[No JNI_OnLoad found in \"" << path << "\"]";
    was_successful = true;
  } else {
    // Call JNI_OnLoad.  We have to override the current class
    // loader, which will always be "null" since the stuff at the
    // top of the stack is around Runtime.loadLibrary().  (See
    // the comments in the JNI FindClass function.)
    ScopedLocalRef<jobject> old_class_loader(env, env->NewLocalRef(self->GetClassLoaderOverride()));
    self->SetClassLoaderOverride(class_loader);

    VLOG(jni) << "[Calling JNI_OnLoad in \"" << path << "\"]";
    typedef int (*JNI_OnLoadFn)(JavaVM*, void*);
    JNI_OnLoadFn jni_on_load = reinterpret_cast<JNI_OnLoadFn>(sym);
    int version = (*jni_on_load)(this, nullptr);

    if (runtime_->GetTargetSdkVersion() != 0 && runtime_->GetTargetSdkVersion() <= 21) {
      // Make sure that sigchain owns SIGSEGV.
      EnsureFrontOfChain(SIGSEGV);
    }

    self->SetClassLoaderOverride(old_class_loader.get());

    if (version == JNI_ERR) {
      StringAppendF(error_msg, "JNI_ERR returned from JNI_OnLoad in \"%s\"", path.c_str());
    } else if (JavaVMExt::IsBadJniVersion(version)) {
      StringAppendF(error_msg, "Bad JNI version returned from JNI_OnLoad in \"%s\": %d",
                    path.c_str(), version);
      // It's unwise to call dlclose() here, but we can mark it
      // as bad and ensure that future load attempts will fail.
      // We don't know how far JNI_OnLoad got, so there could
      // be some partially-initialized stuff accessible through
      // newly-registered native method calls.  We could try to
      // unregister them, but that doesn't seem worthwhile.
    } else {
      was_successful = true;
    }
    VLOG(jni) << "[Returned " << (was_successful ? "successfully" : "failure")
              << " from JNI_OnLoad in \"" << path << "\"]";
  }

  library->SetResult(was_successful);
  return was_successful;
}

上述流程比較復(fù)雜,可以簡單概述為以下幾個(gè)步驟:

  1. 先判斷是否已經(jīng)加載過 so 庫辱匿,并且判斷加載 so 的 ClassLoader 是不是同一個(gè) ClassLoader键痛。
  2. 調(diào)用 android::OpenNativeLibrary() 去打開 so

上面的邏輯關(guān)注點(diǎn)在 OpenNativeLibrary() 這里炫彩,會(huì)將完整的路徑,ClassLoader 的引用傳遞進(jìn)去絮短,打開 so江兢。

  void* handle = android::OpenNativeLibrary(env,
                                            runtime_->GetTargetSdkVersion(),
                                            path_str,
                                            class_loader,
                                            library_path.get(),
                                            &needs_native_bridge,
                                            error_msg);

最終,經(jīng)過層層調(diào)用會(huì)進(jìn)去到 linker.cpp 中丁频,調(diào)用 dl_open() 方法杉允,將so 加載到內(nèi)存中,

mapLibrary()

這個(gè)函數(shù)主要作用是使用傳入的名稱席里,拼接完整的 so 庫的名稱(不包含路徑)叔磷,具體的實(shí)現(xiàn)源碼在 System.c 文件中,如下:

JNIEXPORT jstring JNICALL
System_mapLibraryName(JNIEnv *env, jclass ign, jstring libname)
{
    int len;
    int prefix_len = (int) strlen(JNI_LIB_PREFIX);//JNI_LIB_PREFIX 就是 lib
    int suffix_len = (int) strlen(JNI_LIB_SUFFIX);//JNI_LIB_SUFFIX 就是 .so

    jchar chars[256];
    if (libname == NULL) {
        JNU_ThrowNullPointerException(env, 0);
        return NULL;
    }
    len = (*env)->GetStringLength(env, libname);
    if (len > 240) {//so 名稱不能大于 240
        JNU_ThrowIllegalArgumentException(env, "name too long");
        return NULL;
    }
    cpchars(chars, JNI_LIB_PREFIX, prefix_len);
    (*env)->GetStringRegion(env, libname, 0, len, chars + prefix_len);
    len += prefix_len;
    cpchars(chars + len, JNI_LIB_SUFFIX, suffix_len);
    len += suffix_len;

    return (*env)->NewString(env, chars, len);
}

其中奖磁,涉及到的宏定義如下,在源碼 jvm_md.h 中:

#define JNI_LIB_PREFIX "lib"
#ifdef __APPLE__
#define JNI_LIB_SUFFIX ".dylib"
#define VERSIONED_JNI_LIB_NAME(NAME, VERSION) JNI_LIB_PREFIX NAME "." VERSION JNI_LIB_SUFFIX
#else
#define JNI_LIB_SUFFIX ".so"
#define VERSIONED_JNI_LIB_NAME(NAME, VERSION) JNI_LIB_PREFIX NAME JNI_LIB_SUFFIX "." VERSION
#endif
#define JNI_LIB_NAME(NAME) JNI_LIB_PREFIX NAME JNI_LIB_SUFFIX

這里判斷 so 名稱不能多于 240 個(gè)字符改基。

上述方法將傳入的 so 名稱進(jìn)行拼接,例如傳入 audio_shared 名稱咖为,最后會(huì)生成 libaudio_shared.so 這個(gè)完整名稱秕狰。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市躁染,隨后出現(xiàn)的幾起案子鸣哀,更是在濱河造成了極大的恐慌,老刑警劉巖吞彤,帶你破解...
    沈念sama閱讀 222,000評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件我衬,死亡現(xiàn)場離奇詭異,居然都是意外死亡饰恕,警方通過查閱死者的電腦和手機(jī)挠羔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,745評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來懂盐,“玉大人褥赊,你說我怎么就攤上這事±蚰眨” “怎么了拌喉?”我有些...
    開封第一講書人閱讀 168,561評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長俐银。 經(jīng)常有香客問我尿背,道長,這世上最難降的妖魔是什么捶惜? 我笑而不...
    開封第一講書人閱讀 59,782評(píng)論 1 298
  • 正文 為了忘掉前任田藐,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘汽久。我一直安慰自己鹤竭,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,798評(píng)論 6 397
  • 文/花漫 我一把揭開白布景醇。 她就那樣靜靜地躺著臀稚,像睡著了一般。 火紅的嫁衣襯著肌膚如雪三痰。 梳的紋絲不亂的頭發(fā)上吧寺,一...
    開封第一講書人閱讀 52,394評(píng)論 1 310
  • 那天,我揣著相機(jī)與錄音散劫,去河邊找鬼稚机。 笑死,一個(gè)胖子當(dāng)著我的面吹牛获搏,可吹牛的內(nèi)容都是我干的赖条。 我是一名探鬼主播,決...
    沈念sama閱讀 40,952評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼常熙,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼谋币!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起症概,我...
    開封第一講書人閱讀 39,852評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎早芭,沒想到半個(gè)月后彼城,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,409評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡退个,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,483評(píng)論 3 341
  • 正文 我和宋清朗相戀三年募壕,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片语盈。...
    茶點(diǎn)故事閱讀 40,615評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡舱馅,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出刀荒,到底是詐尸還是另有隱情代嗤,我是刑警寧澤,帶...
    沈念sama閱讀 36,303評(píng)論 5 350
  • 正文 年R本政府宣布缠借,位于F島的核電站干毅,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏泼返。R本人自食惡果不足惜硝逢,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,979評(píng)論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧渠鸽,春花似錦叫乌、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,470評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至猎拨,卻和暖如春膀藐,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背红省。 一陣腳步聲響...
    開封第一講書人閱讀 33,571評(píng)論 1 272
  • 我被黑心中介騙來泰國打工额各, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人吧恃。 一個(gè)月前我還...
    沈念sama閱讀 49,041評(píng)論 3 377
  • 正文 我出身青樓虾啦,卻偏偏與公主長得像,于是被迫代替她去往敵國和親痕寓。 傳聞我的和親對(duì)象是個(gè)殘疾皇子傲醉,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,630評(píng)論 2 359

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