热修&插件 - Art加载Class流程

根据上篇文章了解,对应ClassLoader初始化时,会将对应的dex加载到内存。接下来再继续看Class的加载、链接和初始化过程。

一、整体过程

类加载、链接、初始化整体过程
任务 描述 最终状态
Load 从.odex/.dex中提取类信息到虚拟机内部。 kStatusLoaded
Verify 校验类信息是否合法。 kStatusVerified
Prepare 为该类和相关信息的存储分配一块存储空间。
Resolve 如果该类的成员还有引用其他类,可能还需要将其他类加载到虚拟机。 kStatusResolved
Initialize 初始化这个类(初始化静态成员变量、执行static语句块)。 kStatusInitialized

虽然如图所示,Class的加载、链接、初始化分别对应Load、Link、Initialize三个阶段,但ART代码中相关函数的调用顺序却不能(也没必要)完全与上述三个阶段相对应。

注:class.h中的完整的类状态

  enum Status {
    kStatusRetired = -3,  // Retired, should not be used. Use the newly cloned one instead.
    kStatusErrorResolved = -2,
    kStatusErrorUnresolved = -1,
    kStatusNotReady = 0,
    kStatusIdx = 1,  // Loaded, DEX idx in super_class_type_idx_ and interfaces_type_idx_.
    kStatusLoaded = 2,  // DEX idx values resolved.
    kStatusResolving = 3,  // Just cloned from temporary class object.
    kStatusResolved = 4,  // Part of linking.
    kStatusVerifying = 5,  // In the process of being verified.
    kStatusRetryVerificationAtRuntime = 6,  // Compile time verification failed, retry at runtime.
    kStatusVerifyingAtRuntime = 7,  // Retrying verification at runtime.
    kStatusVerified = 8,  // Logically part of linking; done pre-init.
    kStatusInitializing = 9,  // Class init in progress.
    kStatusInitialized = 10,  // Ready to go.
    kStatusMax = 11,
  };

二、从ClassLoader.loadClass()看类加载过程

2.1ClassLoader继承关系
ClassLoader继承关系类图
2.2 ClassLoader.loadClass()加载流程
libcore/ojluni/src/main/java/java/lang/ClassLoader.java

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException

{
        // 首先,判断当前类是否已经被加载过
       Class<?> c = findLoadedClass(name);
       if (c == null) {
            try {
                if (parent != null) {
                    //其次,递归去父类中查询是否加载到缓存
                    c = parent.loadClass(name, false);
               } else {
                    //均未缓存,去BootClassLoader中查找
                    c = findBootstrapClassOrNull(name);
               }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
               // from the non-null parent class loader
           }
            if (c == null) {
                //以上均未找到,则通过findClass去找
               c = findClass(name);
           }
        }
        return c;
}

以上即:双亲委派机制。目的是:避免重复加载以及核心累篡改。

接下来看findClass流程:

findClass调用栈

这里核心在于9。

art/runtime/class_linker.cc

mirror::Class* ClassLinker::DefineClass(Thread* self,
                                        const char* descriptor,//目标类的字符串描述
                                        size_t hash,
                                        Handle<mirror::ClassLoader> class_loader,
                                        const DexFile& dex_file,//该类所在的dex文件对象
                                        const DexFile::ClassDef& dex_class_def//描述要加载的类在dex文件里的信息
) {
  StackHandleScope<3> hs(self);
  auto klass = hs.NewHandle<mirror::Class>(nullptr);
...
  if (klass == nullptr) {
    //分配一个class对象
   klass.Assign(AllocClass(self, SizeOfClassWithoutEmbeddedTables(dex_file, dex_class_def)));
  }
…
//获取DexCache
 ObjPtr<mirror::DexCache> dex_cache = RegisterDexFile(*new_dex_file, class_loader.Get());
if (dex_cache == nullptr) {
  self->AssertPendingException();
  return nullptr;
}
klass->SetDexCache(dex_cache);
 //kclass设置基本信息:
 //包括:AccessFlags、Status、DexClassDefIndex、DexTypeIndex等。
  SetupClass(*new_dex_file, *new_class_def, klass, class_loader.Get());
...
  // Add the newly loaded class to the loaded classes table.
  //将kclass加入到class_table中
  ObjPtr<mirror::Class> existing = InsertClass(descriptor, klass.Get(), hash);
  if (existing != nullptr) {
    // We failed to insert because we raced with another thread. Calling EnsureResolved may cause
   // this thread to block.
   //如果插入失败,则表示类已经被加载过
   return EnsureResolved(self, descriptor, existing);
  }
  // Load the fields and other things after we are inserted in the table. This is so that we don't
  // end up allocating unfree-able linear alloc resources and then lose the race condition. The
  // other reason is that the field roots are only visited from the class table. So we need to be
  // inserted before we allocate / fill in these fields.
  //加载来自dex中目标类的属性和其他信息
  LoadClass(self, *new_dex_file, *new_class_def, klass);
…
if (!LoadSuperAndInterfaces(klass, *new_dex_file)) {
  // Loading failed.
  if (!klass->IsErroneous()) {
    mirror::Class::SetStatus(klass, mirror::Class::kStatusErrorUnresolved, self);
  }
  return nullptr;
}
...
MutableHandle<mirror::Class> h_new_class = hs.NewHandle<mirror::Class>(nullptr);
if (!LinkClass(self, descriptor, klass, interfaces, &h_new_class)) {
  // Linking failed.
  if (!klass->IsErroneous()) {
    mirror::Class::SetStatus(klass, mirror::Class::kStatusErrorUnresolved, self);
  }
  return nullptr;
}
...
  return h_new_class.Get();
}

初始化一个mirror Class对象:kclass,优先从DexCache中获取对应的类信息,通过SetupClass设置基本信息,将kclass加入到class_table中,再通过LoadClass和LoadSuperAndInterfaces分别加载dex中目标类的相关信息以及其父类或者实现的接口的相关信息。

先看LoadClass

void ClassLinker::LoadClass(Thread* self,
                            const DexFile& dex_file,
                            const DexFile::ClassDef& dex_class_def,
                            Handle<mirror::Class> klass) {
  //获取classData
  const uint8_t* class_data = dex_file.GetClassData(dex_class_def);
  if (class_data == nullptr) {
    return;  // no fields or methods - for example a marker interface
  }

  //加载类的属性和方法
  LoadClassMembers(self, dex_file, class_data, klass);
}
void ClassLinker::LoadClassMembers(Thread* self,
                                   const DexFile& dex_file,
                                   const uint8_t* class_data,
                                   Handle<mirror::Class> klass) {
  {
    // Note: We cannot have thread suspension until the field and method arrays are setup or else
   // Class::VisitFieldRoots may miss some fields or methods.
//在没有加载完类成员之前,不允许当前线程挂起
   ScopedAssertNoThreadSuspension nts(__FUNCTION__);
    // Load static fields.
   // We allow duplicate definitions of the same field in a class_data_item
   // but ignore the repeated indexes here, b/21868015.
    //加载静态属性
   LinearAlloc* const allocator = GetAllocatorForClassLoader(klass->GetClassLoader());
    ClassDataItemIterator it(dex_file, class_data);
    LengthPrefixedArray<ArtField>* sfields = AllocArtFieldArray(self,
                                                                allocator,
                                                                it.NumStaticFields());
    size_t num_sfields = 0;
    uint32_t last_field_idx = 0u;
    for (; it.HasNextStaticField(); it.Next()) {
      uint32_t field_idx = it.GetMemberIndex();
      DCHECK_GE(field_idx, last_field_idx);  // Ordering enforced by DexFileVerifier.
     if (num_sfields == 0 || LIKELY(field_idx > last_field_idx)) {
        DCHECK_LT(num_sfields, it.NumStaticFields());
        //加载属性
        LoadField(it, klass, &sfields->At(num_sfields));
        ++num_sfields;
        last_field_idx = field_idx;
      }
    }
    // Load instance fields.
    //加载实例属性
   LengthPrefixedArray<ArtField>* ifields = AllocArtFieldArray(self,
                                                                allocator,
                                                                it.NumInstanceFields());
    size_t num_ifields = 0u;
    last_field_idx = 0u;
    for (; it.HasNextInstanceField(); it.Next()) {
      uint32_t field_idx = it.GetMemberIndex();
      DCHECK_GE(field_idx, last_field_idx);  // Ordering enforced by DexFileVerifier.
     if (num_ifields == 0 || LIKELY(field_idx > last_field_idx)) {
        DCHECK_LT(num_ifields, it.NumInstanceFields());
        LoadField(it, klass, &ifields->At(num_ifields));
        ++num_ifields;
        last_field_idx = field_idx;
      }
    }
...
    // Set the field arrays.
   klass->SetSFieldsPtr(sfields);
     DCHECK_EQ(klass->NumStaticFields(), num_sfields);
     klass->SetIFieldsPtr(ifields);
     DCHECK_EQ(klass->NumInstanceFields(), num_ifields);
     // Load methods.
    bool has_oat_class = false;
     //获取OatClass,通过OatClass可以获取类方法的本地机器指令
     const OatFile::OatClass oat_class =
         (Runtime::Current()->IsStarted() && !Runtime::Current()->IsAotCompiler())
             ? OatFile::FindOatClass(dex_file, klass->GetDexClassDefIndex(), &has_oat_class)
             : OatFile::OatClass::Invalid();
     const OatFile::OatClass* oat_class_ptr = has_oat_class ? &oat_class : nullptr;
     //加载方法
     klass->SetMethodsPtr(
         AllocArtMethodArray(self, allocator, it.NumDirectMethods() + it.NumVirtualMethods()),
         it.NumDirectMethods(),
         it.NumVirtualMethods());
     size_t class_def_method_index = 0;
     uint32_t last_dex_method_index = DexFile::kDexNoIndex;
     size_t last_class_def_method_index = 0;
     // TODO These should really use the iterators.
     //加载直接方法
    for (size_t i = 0; it.HasNextDirectMethod(); i++, it.Next()) {
       ArtMethod* method = klass->GetDirectMethodUnchecked(i, image_pointer_size_);
       LoadMethod(dex_file, it, klass, method);
       LinkCode(this, method, oat_class_ptr, class_def_method_index);
       uint32_t it_method_index = it.GetMemberIndex();
       if (last_dex_method_index == it_method_index) {
         // duplicate case
        method->SetMethodIndex(last_class_def_method_index);
       } else {
         method->SetMethodIndex(class_def_method_index);
         last_dex_method_index = it_method_index;
         last_class_def_method_index = class_def_method_index;
       }
       class_def_method_index++;
     }
     //加载虚拟方法
     for (size_t i = 0; it.HasNextVirtualMethod(); i++, it.Next()) {
       ArtMethod* method = klass->GetVirtualMethodUnchecked(i, image_pointer_size_);
       LoadMethod(dex_file, it, klass, method);
       DCHECK_EQ(class_def_method_index, it.NumDirectMethods() + i);
       //将对应方法关联上指令
       LinkCode(this, method, oat_class_ptr, class_def_method_index);
       class_def_method_index++;
     }
     DCHECK(!it.HasNext());
   }
   // Ensure that the card is marked so that remembered sets pick up native roots.
  Runtime::Current()->GetHeap()->WriteBarrierEveryFieldOf(klass.Get());
   self->AllowThreadSuspension();
}

加载类的属性和方法:
ArtField: 包括Static Field、Instance Field、Field
ArtMethod: 包括Direct Method、Virtual Method。

注:ArtField和ArtMethod在ART中分别用于描述一个类的成员变量和成员函数。
class.h
uint64_t ifields_ 和 uint64_t sfields_ 对应ArtField
uint64_t methods_; 对应ArtMethod

LinkCode 将对应方法关联上指令

static void LinkCode(ClassLinker* class_linker,
                     ArtMethod* method,
                     const OatFile::OatClass* oat_class,
                     uint32_t class_def_method_index) REQUIRES_SHARED(Locks::mutator_lock_) {
  Runtime* const runtime = Runtime::Current();
  if (runtime->IsAotCompiler()) {
    // The following code only applies to a non-compiler runtime.
   return;
  }

  // Method shouldn't have already been linked.
  DCHECK(method->GetEntryPointFromQuickCompiledCode() == nullptr);
  if (oat_class != nullptr) {
    // Every kind of method should at least get an invoke stub from the oat_method.
   // non-abstract methods also get their code pointers.
//从OatClass中获取对应的OatMethod,OatMethod中记录了方法的本地机器指令的偏移地址
   //通过OatMethod::LinkMethod将OatMethod中记录的信息设置到ArtMethod当中
   const OatFile::OatMethod oat_method = oat_class->GetOatMethod(class_def_method_index);
    oat_method.LinkMethod(method);
  }

  const void* quick_code = method->GetEntryPointFromQuickCompiledCode();
  bool enter_interpreter = class_linker->ShouldUseInterpreterEntrypoint(method, quick_code);
  if (!method->IsInvokable()) {
    EnsureThrowsInvocationError(class_linker, method);
    return;
  }
  if (method->IsStatic() && !method->IsConstructor()) {//静态非构造方法
    // For static methods excluding the class initializer, install the trampoline.
   // It will be replaced by the proper entry point by ClassLinker::FixupStaticTrampolines
   // after initializing class (see ClassLinker::InitializeClass method).
   method->SetEntryPointFromQuickCompiledCode(GetQuickResolutionStub());
  } else if (quick_code == nullptr && method->IsNative()) {//native方法且没有本地机器码
    method->SetEntryPointFromQuickCompiledCode(GetQuickGenericJniStub());
  } else if (enter_interpreter) {
    // Set entry point from compiled code if there's no code or in interpreter only mode.
   method->SetEntryPointFromQuickCompiledCode(GetQuickToInterpreterBridge());
  }

  if (method->IsNative()) {
    // Unregistering restores the dlsym lookup stub.
   method->UnregisterNative();
    if (enter_interpreter || quick_code == nullptr) {
      // We have a native method here without code. Then it should have either the generic JNI
     // trampoline as entrypoint (non-static), or the resolution trampoline (static).
     // TODO: this doesn't handle all the cases where trampolines may be installed.
     const void* entry_point = method->GetEntryPointFromQuickCompiledCode();
      DCHECK(class_linker->IsQuickGenericJniStub(entry_point) ||
             class_linker->IsQuickResolutionStub(entry_point));
    }
  }
}

这里通过method->SetEntryPointFromQuickCompiledCode来设置对应方法的入口函数:

  • 对于静态非构造方法,当有执行机器指令的方法调用这类函数时,入口函数是:GetQuickResolutionStub()。
  • 当一个执行本地机器指令的方法调用没有机器指令的native方法时,入口函数是GetQuickGenericJniStub()。
  • 抽象方法需要由被子类实现,所以抽象类是没有本地机器指令的,需要由解释器解释执行,当有机器指令的方法调用抽象方法时, 需要设置一个桥接函数,从而转到解释器,由解释器解释执行抽象方法, 这个桥接函数是GetQuickToInterpreterBridge()。

为什么每个类方法需要一个解释器入口点?

ART虚拟机执行Java方法主要有两种模式:

  • quick code:执行本地机器指令
  • interpreter:由解释器解释执行虚拟机字节码

当以解释器执行的类方法在执行的过程中调用了其它的类方法时,解释器就需要进一步知道被调用的类方法是应用以解释方式执行,还是本地机器指令方法执行。为了能够进行统一处理,就给每一个类方法都设置一个解释器入口点。

例如:
抽象方法声明类中是没有实现的,必须要由子类实现。因此抽象方法在声明类中是没有对应的本地机器指令的,它们必须要通过解释器来执行。

然后返回来看DefineClass最后一步:LinkClass

bool ClassLinker::LinkClass(Thread* self,
                            const char* descriptor,
                            Handle<mirror::Class> klass,
                            Handle<mirror::ObjectArray<mirror::Class>> interfaces,
                            MutableHandle<mirror::Class>* h_new_class_out) {
  CHECK_EQ(mirror::Class::kStatusLoaded, klass->GetStatus());
  if (!LinkSuperClass(klass)) {//link超类
    return false;
  }

  ArtMethod* imt_data[ImTable::kSize];
  // If there are any new conflicts compared to super class.
  bool new_conflict = false;
  std::fill_n(imt_data, arraysize(imt_data), Runtime::Current()->GetImtUnimplementedMethod());
  if (!LinkMethods(self, klass, interfaces, &new_conflict, imt_data)) {//link方法
    return false;
  }

  if (!LinkInstanceFields(self, klass)) {//link对象属性
    return false;
  }

  size_t class_size;
  if (!LinkStaticFields(self, klass, &class_size)) {//link静态属性
    return false;
  }

  CreateReferenceInstanceOffsets(klass);
  CHECK_EQ(mirror::Class::kStatusLoaded, klass->GetStatus());
  ImTable* imt = nullptr;
  if (klass->ShouldHaveImt()) {
    // If there are any new conflicts compared to the super class we can not make a copy. There
   // can be cases where both will have a conflict method at the same slot without having the same
   // set of conflicts. In this case, we can not share the IMT since the conflict table slow path
   // will possibly create a table that is incorrect for either of the classes.
   // Same IMT with new_conflict does not happen very often.
   if (!new_conflict) {
      ImTable* super_imt = FindSuperImt(klass.Get(), image_pointer_size_);
      if (super_imt != nullptr) {
        bool imt_equals = true;
        for (size_t i = 0; i < ImTable::kSize && imt_equals; ++i) {
          imt_equals = imt_equals && (super_imt->Get(i, image_pointer_size_) == imt_data[i]);
        }
        if (imt_equals) {
          imt = super_imt;
        }
      }
    }
    if (imt == nullptr) {
      LinearAlloc* allocator = GetAllocatorForClassLoader(klass->GetClassLoader());
      imt = reinterpret_cast<ImTable*>(
          allocator->Alloc(self, ImTable::SizeInBytes(image_pointer_size_)));
      if (imt == nullptr) {
        return false;
      }
      imt->Populate(imt_data, image_pointer_size_);
    }
  }
  if (!klass->IsTemp() || (!init_done_ && klass->GetClassSize() == class_size)) {
    // We don't need to retire this class as it has no embedded tables or it was created the
   // correct size during class linker initialization.
   CHECK_EQ(klass->GetClassSize(), class_size) << klass->PrettyDescriptor();
    if (klass->ShouldHaveEmbeddedVTable()) {
      klass->PopulateEmbeddedVTable(image_pointer_size_);
    }
    if (klass->ShouldHaveImt()) {
      klass->SetImt(imt, image_pointer_size_);
    }
    // Update CHA info based on whether we override methods.
   // Have to do this before setting the class as resolved which allows
   // instantiation of klass.
   Runtime::Current()->GetClassHierarchyAnalysis()->UpdateAfterLoadingOf(klass);
    // This will notify waiters on klass that saw the not yet resolved
   // class in the class_table_ during EnsureResolved.
    //linkClass 输入状态为kStatusLoaded,输出状态为kStatusResolved
   mirror::Class::SetStatus(klass, mirror::Class::kStatusResolved, self);
    h_new_class_out->Assign(klass.Get());
  } else {
    CHECK(!klass->IsResolved());
    // Retire the temporary class and create the correctly sized resolved class.
   StackHandleScope<1> hs(self);
    auto h_new_class = hs.NewHandle(klass->CopyOf(self, class_size, imt, image_pointer_size_));
    // Set arrays to null since we don't want to have multiple classes with the same ArtField or
   // ArtMethod array pointers. If this occurs, it causes bugs in remembered sets since the GC
   // may not see any references to the target space and clean the card for a class if another
   // class had the same array pointer.
   klass->SetMethodsPtrUnchecked(nullptr, 0, 0);
    klass->SetSFieldsPtrUnchecked(nullptr);
    klass->SetIFieldsPtrUnchecked(nullptr);
    if (UNLIKELY(h_new_class == nullptr)) {
      self->AssertPendingOOMException();
      mirror::Class::SetStatus(klass, mirror::Class::kStatusErrorUnresolved, self);
      return false;
    }

    CHECK_EQ(h_new_class->GetClassSize(), class_size);
    ObjectLock<mirror::Class> lock(self, h_new_class);
    FixupTemporaryDeclaringClass(klass.Get(), h_new_class.Get());
    {
      WriterMutexLock mu(self, *Locks::classlinker_classes_lock_);
      ObjPtr<mirror::ClassLoader> const class_loader = h_new_class.Get()->GetClassLoader();
      ClassTable* const table = InsertClassTableForClassLoader(class_loader);
      ObjPtr<mirror::Class> existing = table->UpdateClass(descriptor, h_new_class.Get(),
                                                   ComputeModifiedUtf8Hash(descriptor));
      if (class_loader != nullptr) {
        // We updated the class in the class table, perform the write barrier so that the GC knows
       // about the change.
       Runtime::Current()->GetHeap()->WriteBarrierEveryFieldOf(class_loader);
      }
      CHECK_EQ(existing, klass.Get());
      if (log_new_roots_) {
        new_class_roots_.push_back(GcRoot<mirror::Class>(h_new_class.Get()));
      }
    }
    // Update CHA info based on whether we override methods.
   // Have to do this before setting the class as resolved which allows
   // instantiation of klass.
   Runtime::Current()->GetClassHierarchyAnalysis()->UpdateAfterLoadingOf(h_new_class);
    // This will notify waiters on temp class that saw the not yet resolved class in the
   // class_table_ during EnsureResolved.
   mirror::Class::SetStatus(klass, mirror::Class::kStatusRetired, self);
    CHECK_EQ(h_new_class->GetStatus(), mirror::Class::kStatusResolving);
    // This will notify waiters on new_class that saw the not yet resolved
   // class in the class_table_ during EnsureResolved.
   mirror::Class::SetStatus(h_new_class, mirror::Class::kStatusResolved, self);
    // Return the new class.
   h_new_class_out->Assign(h_new_class.Get());
  }
  return true;
}

LinkClass整体过程是Prepare + Resolve,为该类和相关信息的存储分配一块存储空间,如果该类的成员还有引用其他类,可能还需要将其他类加载到虚拟机。输入状态为kStatusLoaded,输出状态为kStatusResolved。

这里我们发现,link过程并没有做Verify,那么Verify是在什么时候做的呢?

这里简单梳理下《深入理解Android Java虚拟机Art》中的结论:
类的Verify可以在dex2oat阶段进行,这叫预校验。如果出现错误,状态则置为:kStatusRetryVerificationAtRutime,如果是这个状态,此类在之后的虚拟机运行时还会再继续做校验。,如果dex2oat校验成功,状态则置为:kStatusVerified。该类在虚拟机运行时,如果类的初始状态为kStatusVerified,则将该类中methods_数组中所有的ArtMethod对象设置kAccSkipAccessChecks标志,同时还设置kAccVerificationAttempted标记(该标记表示该类已经校验过了,不需要再做校验)。如果类的初始状态为kStatusRetryVerificationAtRutime,则在虚拟机运行时会触发MethodVerifier对该类进行校验。

回顾下ClassLoader.loadClass()整个过程:

先通过双亲委派机制判断当前类是否被加载过、是否需要上层的父加载器去加载。如果都没有则走findClass流程,而findClass核心是ClassLinker::DefineClass:
该方法将类的信息从Dex文件中转换为对应的mirror Class对象。构建过程包括基本参数设置,属性、方法加载,其中方法还会设置对应的入口函数,通过linkClass完成准备和解析流程。最终加入到class_table中(它被ClassLoader用于管理该ClassLoader所加载的类)。这个过程虽然返回了Class对象,但是目前还只完成了加载操作,离目标类最终可用还差:验证工作和初始化相关工作。

那么findClass的数据结构转换过程总结如下:


类加载过程数据结构转换

数据获取流程:
class_table有表白已经被加载过,直接从loadClass流程就获取了,否则走DefineClass,优先从DexCache中获取dex信息,否则从当前DexFile中获取。然后将dex中的信息转换为对应的mirror Class对象,最后将class加入到class_table中,缓存当前类已经被指定的classLoader加载了。

参考:
《深入理解Android Java虚拟机Art》
https://www.jianshu.com/p/86f9db3ab430
https://www.jianshu.com/go-wild?ac=2&url=http%3A%2F%2Fblog.csdn.net%2Fluoshengyang%2Farticle%2Fdetails%2F39533503

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

推荐阅读更多精彩内容