类的加载(上)-- _objc_init&read_images

前言

上一篇文章主要分析dyld的整个流程以及dyld_objc_init之间的交互,_objc_initdyld注册了回调函数,所以_objc_initdyld中尤为关键,那么我们今天继续往下探讨。

准备工作

_objc_init

void _objc_init(void)
{
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    
    // fixme defer initialization until an objc-using image is found?
    //环境的初始化
    environ_init();
    //析构函数的绑定
    tls_init();
    //全局静态c++函数调用,这里只是调用objc自己的。在dyld调用之前,相当于objc的c++构造函数是自己调用的,不是dyld调用的。
    static_init();
    //runtime相关的两张表的初始化
    runtime_init();
    //初始化 libobjc 的异常处理系统。
    exception_init();
#if __OBJC2__
    // //缓存初始化
    cache_t::init();
#endif
    //启动回调机制
    _imp_implementationWithBlock_init();
    //map_images:管理文件中和动态库中所有的符号 (class Protocol selector category)
    //load_images:加载执行load方法
    //unmap_image:释放类相关资源。
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);

#if __OBJC2__
    didCallDyldNotifyRegister = true;
#endif
}
  • environ_init:读取影响运行时的环境变量,如果需要还可以打印环境变量帮助 export OBJC_HELP = 1
    -tls_init:关于线程key的绑定,比如每个线程数据的析构函数
  • static_init:运行C++静态构造函数。在dyld调用我们的静态构造函数之前,lib会调用_objc_init先调用自己的C++构造函数。
  • runtime_initruntime运行时环境初始化,里面主要是unattachedCategoriesallocatedClasses两张表。
  • exception_init:初始化libobjc库的异常处理系统。
  • cache_t::init缓存条件初始化。
  • _imp_implementationWithBlock_init启动回调机制。通常不会做什么,因为所有的初始化都是惰性的,但是对于某些进程,会迫不及待的加载trampolines dylib
  • _dyld_objc_notify_register: 向dyld的注册回调。

environ_init

void environ_init(void) 
{
    ……
    if (PrintHelp  ||  PrintOptions) {
        ……
        for (size_t i = 0; i < sizeof(Settings)/sizeof(Settings[0]); i++) {
            const option_t *opt = &Settings[i];            
            if (PrintHelp) _objc_inform("%s: %s", opt->env, opt->help);
            if (PrintOptions && *opt->var) _objc_inform("%s is set", opt->env);
        }
    }
}

由源码可以看出打印的条件是由PrintHelp || PrintOptions进行判断的。

日志输出
跳过PrintHelp || PrintOptions判断,去掉这个判断修改最后的代码成如下那样:

 for (size_t i = 0; i < sizeof(Settings)/sizeof(Settings[0]); i++) {
            const option_t *opt = &Settings[i];            
        _objc_inform("%s: %s", opt->env, opt->help);
        _objc_inform("%s is set", opt->env);
        }

运行结果:

运行结果

如果没有objc源码的话,那么我们可以通过终端输出日志。
终端输出环境

使用命令:

export OBJC_HELP=1

可以看出终端输出环境日志还是比较方便的,那么我们还可以通过xcode进行配置环境变量,操作如下:
Xcode中环境变量配置的位置:选中运行的target--> Edit scheme -->Run--> Arguments --> Environment Variables
在环境变量中有两个配置:OBJC_DISABLE_NONPOINTER_ISAOBJC_PRINT_LOAD_METHODS

在Xcode中添加环境变量

OBJC_DISABLE_NONPOINTER_ISA
OBJC_DISABLE_NONPOINTER_ISA就是判断是否是优化的指针。YES表示纯指针NO表示优化后的指针就是nonpointer isa

  • Xcode环境变量中不选择OBJC_DISABLE_NONPOINTER_ISA,通过lldb调试如下:
    优化过的isa

    isa的最低位是1,表示是优化后的isa,而且高位上也有数据。
  • Xcode环境变量中选择上OBJC_DISABLE_NONPOINTER_ISA,通过lldb调试如下:
    纯isa

    isa的最低位是0,表示是纯isa,而且高位上没有数据。

OBJC_PRINT_LOAD_METHODS
环境变量OBJC_PRINT_LOAD_METHODS打印出程序中所有的load方法,在自定义类中添加load方法,配置环境变量OBJC_PRINT_LOAD_METHODS = YES

打印项目+load方法

XJLPerson类和LGPerson类中添加+load方法,已经在上图打印了出来。通过这样子可以检测哪位写的代码+load方法比较多,+load方法太多会影响程序的启动速度

tls_init

tls_init方法是关于线程key的绑定,比如每个线程数据的析构函数。

 void  tls_init(void)
{ 
#if SUPPORT_DIRECT_THREAD_KEYS
    //创建线程的缓存池
    pthread_key_init_np(TLS_DIRECT_KEY, &_objc_pthread_destroyspecific);
#else
    //析构函数
    _objc_pthread_key = tls_create(&_objc_pthread_destroyspecific);
#endif
}

static_init

运行C++静态构造函数,在dyld调用静态函数之前,libc会调用_objc_init方法先调用自己的C++构造函数,也就是说libobjc会调用自己的全局的C++函数,而且在dyld调用之前!

验证static_init

调试结果表明确实是libobjc系统库自己调用了内部的C++函数。

runtime_init

runtime运行时环境初始化,里面主要是unattachedCategoriesallocatedClasses两张表,实现代码如下:

void runtime_init(void)
{
    //对两张表进行初始化
    objc::unattachedCategories.init(32);
    objc::allocatedClasses.init();
}

exception_init

初始化libobjc库的异常处理系统,主要是注册异常回调。就像objcdyld中注册回调函数差不多,exception_init回调异常提供开发人员做异常处理

void exception_init(void)
{
    old_terminate = std::set_terminate(&_objc_terminate);
}

当程序出现崩溃现象或者用了不符合规则代码的时候,就会进入set_terminate方法,通过_objc_terminate方法来发出异常信息。

** _objc_terminate**

static void (*old_terminate)(void) = nil;
static void _objc_terminate(void)
{
    if (PrintExceptions) {
        _objc_inform("EXCEPTIONS: terminating");
    }

    if (! __cxa_current_exception_type()) {
        // No current exception.
        (*old_terminate)();
    }
    else {
        // There is a current exception. Check if it's an objc exception.
        @try {
            __cxa_rethrow();
        } @catch (id e) {
            // It's an objc object. Call Foundation's handler, if any.
            (*uncaught_handler)((id)e);
            (*old_terminate)();
        } @catch (...) {
            // It's not an objc object. Continue to C++ terminate.
            (*old_terminate)();
        }
    }
}

通过以上源码的分析,发现没有查询最后报异常处理也会走到_objc_terminate方法,在_objc_terminate方法发现了(*uncaught_handler)((id)e)它会把异常抛出去,全局搜索uncaught_handler

uncaught_handler

objc_uncaught_exception_handler 
objc_setUncaughtExceptionHandler(objc_uncaught_exception_handler fn)
{
    objc_uncaught_exception_handler result = uncaught_handler;
    uncaught_handler = fn;
    return result;
}

通过uncaught_handler = fn 可以知道我们可以自己传一个函数的句柄, fn可以是自己定义的函数,然后回调时,可以自己处理异常的信息。

cache_t::init

此方法是缓存条件的初始化。

void cache_t::init()
{
#if HAVE_TASK_RESTARTABLE_RANGES
    mach_msg_type_number_t count = 0;
    kern_return_t kr;

    while (objc_restartableRanges[count].location) {
        count++;
    }
   //开启缓存
    kr = task_restartable_ranges_register(mach_task_self(),
                                          objc_restartableRanges, count);
    if (kr == KERN_SUCCESS) return;
    _objc_fatal("task_restartable_ranges_register failed (result 0x%x: %s)",
                kr, mach_error_string(kr));
#endif // HAVE_TASK_RESTARTABLE_RANGES
}

_imp_implementationWithBlock_init

启动回调机制。通常不会做什么,因为所有的初始化都是惰性的,但是对于某些进程,会迫不及待的加载 trampolines dylib

void
_imp_implementationWithBlock_init(void)
{
#if TARGET_OS_OSX
    // Eagerly load libobjc-trampolines.dylib in certain processes. Some
    // programs (most notably QtWebEngineProcess used by older versions of
    // embedded Chromium) enable a highly restrictive sandbox profile which
    // blocks access to that dylib. If anything calls
    // imp_implementationWithBlock (as AppKit has started doing) then we'll
    // crash trying to load it. Loading it here sets it up before the sandbox
    // profile is enabled and blocks it.
    //
    // This fixes EA Origin (rdar://problem/50813789)
    // and Steam (rdar://problem/55286131)
    if (__progname &&
        (strcmp(__progname, "QtWebEngineProcess") == 0 ||
         strcmp(__progname, "Steam Helper") == 0)) {
        Trampolines.Initialize();
    }
#endif
}

_dyld_objc_notify_register

dyld注册回调,_dyld_objc_notify_register仅供objc运行时调用并没有方法的实现,其方法的实现在dyld源码中。

// _dyld_objc_notify_register
void _dyld_objc_notify_register(_dyld_objc_notify_mapped    mapped,
                                _dyld_objc_notify_init      init,
                                _dyld_objc_notify_unmapped  unmapped)
{      
        //主要的方法是registerObjCNotifiers
    dyld::registerObjCNotifiers(mapped, init, unmapped);
}

registerObjCNotifiers

// _dyld_objc_notify_init
void registerObjCNotifiers(_dyld_objc_notify_mapped mapped, _dyld_objc_notify_init init, _dyld_objc_notify_unmapped unmapped)
{
    // record functions to call
    sNotifyObjCMapped   = mapped;
    sNotifyObjCInit     = init;
    sNotifyObjCUnmapped = unmapped;

    // call 'mapped' function with all images mapped so far
    try {
        notifyBatchPartial(dyld_image_state_bound, true, NULL, false, true);
    }
    catch (const char* msg) {
        // ignore request to abort during registration
    }

    // <rdar://problem/32209809> call 'init' function on all images already init'ed (below libSystem)
    for (std::vector<ImageLoader*>::iterator it=sAllImages.begin(); it != sAllImages.end(); it++) {
        ImageLoader* image = *it;
        if ( (image->getState() == dyld_image_state_initialized) && image->notifyObjC() ) {
            dyld3::ScopedTimer timer(DBG_DYLD_TIMING_OBJC_INIT, (uint64_t)image->machHeader(), 0, 0);
            (*sNotifyObjCInit)(image->getRealPath(), image->machHeader());
        }
    }
}

_dyld_objc_notify_register注册了map_imagesload_imagesunmap_image的回调。实现在dyld中,三个参数分别是:

  • map_images:管理文件中和动态库中所有的符号 (classProtocolselectorcategory)。(dyldimage加载到内存中会调用该函数)
  • load_images:加载执行load方法(dyld初始化所有的image文件会调用)。
  • unmap_image:释放类相关资源。

疑问:
在dyld加载过程中map_imagesload_images的调用没有区别,但是在这里传递的参数map_images&(取址)操作。map_images是指针拷贝,load_images值传递map_images需要同步变化,否则有可能发生错乱。而load_images比较简单只是load的调用,不需要同步变化。

补充:
在dyld中全局搜索sNotifyObjCMapped方法,发现如下:

sNotifyObjCMapped方法

sNotifyObjCMapped调用的地方是在notifyBatchPartial方法中,而notifyBatchPartial方法是在registerObjCNotifiers调用,在objc初始化注册通知时就调用了,所以是调用map_images后调用load_images

map_images分析

进入map_images源码:

void
map_images(unsigned count, const char * const paths[],
           const struct mach_header * const mhdrs[])
{
    mutex_locker_t lock(runtimeLock);
    return map_images_nolock(count, paths, mhdrs);

}

map_images只是对map_images_nolock的调用。
进入map_images_nolock源码:

void 
map_images_nolock(unsigned mhCount, const char * const mhPaths[],
                  const struct mach_header * const mhdrs[])
{
    ……

    if (hCount > 0) {
        //类的加载映射
        _read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
    }
    ……
}

ap_images_nolock的核心逻辑是要找镜像文件是怎么被加载的,也就是对classProtocolselectorcategory等相关的操作。在map_images_nolock中最终发现了_read_images的调用。那么_read_images就是核心的研究对象了!!请往下看。

_read_images

进入_read_images的源码,发现比较多,我们一步步来。
通过读取源码发现_read_images多算的判断都是输出日志,根据输出日志可以知道没段代码做的是什么,那么我们关闭这些判断得到以下明显的代码逻辑结构:

void _read_images(header_info **hList, uint32_t hCount, int 
totalClasses, int 
unoptimizedTotalClasses){
    ......
   // 条件控制进行一次的加载
    if (!doneOnce) { ... }
    ......
    //修复编译截断selector混乱问题
    //不同的类中有相同方法名的方法,但是他们的地址必须不一样
    static size_t UnfixedSelectors;
    ......
    ts.log("IMAGE TIMES: fix up selector references");
   //处理错误混乱的类(class)
    bool hasDyldRoots = dyld_shared_cache_some_image_overridden();
    for (EACH_HEADER) { ... }
    ts.log("IMAGE TIMES: discover classes");
    ......
    // Fix up remapped classes
    // Class list and nonlazy class list remain unremapped.
    // Class refs and super refs are remapped for message dispatching.
    //修复重映射一些没有被镜像文件加载进来的类
    if (!noClassesRemapped()) { ... }
    ts.log("IMAGE TIMES: remap classes");
    ......
   //修复一些消息
#if SUPPORT_FIXUP
    // Fix up old objc_msgSend_fixup call sites
    for (EACH_HEADER) {......}
    ts.log("IMAGE TIMES: fix up objc_msgSend_fixup");   
    .....
 #endif
    // Discover protocols. Fix up protocol refs.
    //当类中有协议的时候-->readProtocol
    for (EACH_HEADER) {......}
    ts.log("IMAGE TIMES: discover protocols");
    ......
   //修复没有被加载的协议
    for (EACH_HEADER) {......}
    ts.log("IMAGE TIMES: fix up @protocol references");
    //对分类的处理
    if (didInitialAttachCategories) {
        for (EACH_HEADER) {
            load_categories_nolock(hi);
        }
    }
    ts.log("IMAGE TIMES: discover categories");
    //类的加载处理
     for (EACH_HEADER) {
      classref_t const *classlist = hi->nlclslist(&count);
      ......
   }
     ts.log("IMAGE TIMES: realize non-lazy classes");
    // Realize newly-resolved future classes, in case CF manipulates them
    //对没有被处理的类进行优化。
     if (resolvedFutureClasses) {......}
     ts.log("IMAGE TIMES: realize future classes");
    ...
#undef EACH_HEADER
}

通过上面的源码分析,可以知道read_images方法主要做以下事情:

  • 条件控制进行一次加载
  • 修复预编译阶段的@selector的混乱的问题
  • 错误混乱的类处理
  • 修复重映射一些没有被镜像文件加载进来的类
  • 修复一些消息
  • 当类中有协议时:readProtocol
  • 修复没有被加载的协议
  • 分类的处理
  • 类的加载处理
  • 没有被处理的类,优化那些被侵犯的类

既然read_images做了那么多事情,那么就提取重点的在以下进行分析。

只加载一次

 if(!doneOnce) {
        //控制进行只进行一次加载,进来之后修改状态
        doneOnce = YES; 
        launchTime = YES;
         ...
        // Preoptimized classes don't go in this table.
        // 4/3 is NXMapTable's load factor
       //计算所需class表的大小,负载因子是3/4
        int namedClassesSize = 
        (isPreoptimized() ? unoptimizedTotalClasses : totalClasses) * 4 / 3;  
        //创建哈希表 存放所有的类
        gdb_objc_realized_classes =
            NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize);
        ts.log("IMAGE TIMES: first time tasks");
  }
  • 加载一次后控制判断的doneOnce条件等于YES,确保只加载一次,下次来的时候直接到创建表的操作。
  • gdb_objc_realized_classes,表里存放所有的不管是实现的还是没有实现。

修复@selector的混乱

//修复编译截断selector混乱问题
    //不同的类中有相同方法名的方法,但是他们的地址必须不一样
    static size_t UnfixedSelectors;
    {
        mutex_locker_t lock(selLock);
        for (EACH_HEADER) {
            if (hi->hasPreoptimizedSelectors()) continue;

            bool isBundle = hi->isBundle();
            //从macho文件中获取方法名列表
            SEL *sels = _getObjc2SelectorRefs(hi, &count);
            UnfixedSelectors += count;
            for (i = 0; i < count; i++) {
                const char *name = sel_cname(sels[i]);
                SEL sel = sel_registerNameNoLock(name, isBundle);
                if (sels[i] != sel) {
                    sels[i] = sel;
                }
            }
        }
    }
    ts.log("IMAGE TIMES: fix up selector references");

不同的下面可以创建相同的方法的,区分这些方法的话必须是让他们都有各自独立方法地址。因为方法是存放在中的,所以即使方法名相同但是存放在的不一样,那么方法地址肯定不一样

错误混乱的类处理

//处理错误混乱的类(class)
    bool hasDyldRoots = dyld_shared_cache_some_image_overridden();

    for (EACH_HEADER) {
        if (! mustReadClasses(hi, hasDyldRoots)) {
            // Image is sufficiently optimized that we need not call readClass()
            continue;
        }
          //从mach-o文件中获取class信息
        classref_t const *classlist = _getObjc2ClassList(hi, &count);

        bool headerIsBundle = hi->isBundle();
        bool headerIsPreoptimized = hi->hasPreoptimizedClasses();

        for (i = 0; i < count; i++) {
            Class cls = (Class)classlist[i];
            Class newCls = readClass(cls, headerIsBundle, headerIsPreoptimized);
            //野指针的判断
            if (newCls != cls  &&  newCls) {
                // Class was moved but not deleted. Currently this occurs 
                // only when the new class resolved a future class.
                // Non-lazily realize the class below.
                resolvedFutureClasses = (Class *)
                    realloc(resolvedFutureClasses, 
                            (resolvedFutureClassCount+1) * sizeof(Class));
                resolvedFutureClasses[resolvedFutureClassCount++] = newCls;
            }
        }
    }

    ts.log("IMAGE TIMES: discover classes");

运行代码,在创建newCls的地方断点,看看cls与newCls的地址是怎样子的

断点运行代码

看以看出cls地址与newCls的地址是不一样的,在newCls没有被赋值的时候,系统已经给它分配了一个内存地址(脏地址),所以newCls有数据。

让newCls赋值,然后断点查看情况

让newCls赋值,断点查看

得出结论:read_class是将地址关联起来的。

自定义两个类分别是XJLPersonLGPerson,编译之后查看mach-o文件

mach-o文件查看

可以看出XJLPerson的地址为:0000000100004780LGPerson的地址为:0000000100004730

在代码中加上拦截代码,看看编译之后的地址是否对应

加上拦截代码断点调试

macho源码对应起来的,XJLPersonLGPerson的地址跟在mach-o记录的地址一样

readClass
这是绑定clsnewCls关系的核心代码,那么先看它的代码实现逻辑是怎么样子的。

Class readClass(Class cls, bool headerIsBundle, bool headerIsPreoptimized)
{
    // 获取类名
    const char *mangledName = cls->nonlazyMangledName();
    if (missingWeakSuperclass(cls)) { ... }
    cls->fixupBackwardDeployingStableSwift();
    Class replacing = nil;

    if (mangledName != nullptr) { ... }

    if (headerIsPreoptimized  &&  !replacing) {...
    } else {
        if (mangledName) { 
        //some Swift generic classes can lazily generate their names
            //将类名和地址关联起来
            addNamedClass(cls, mangledName, replacing);
        } else { ...}
        //将关联的类插入到另一张哈希表中
        addClassTableEntry(cls);
    }
    // for future reference: shared cache never contains MH_BUNDLEs
    if (headerIsBundle) { ... }
    return cls;
}
  • nonlazyMangledName获取类名
  • rw的赋值和ro的获取并不在readClass里面
  • addNamedClass类名地址关联绑定起来
    -addClassTableEntry将关联的类插入哈希表中,这张表中都是初始化过的

nonlazyMangledName获取类名

const char *nonlazyMangledName() const {
    return bits.safe_ro()->getName();
}

进入safe_ro方法

const class_ro_t *safe_ro() const {
    class_rw_t *maybe_rw = data();
    if (maybe_rw->flags & RW_REALIZED) {
        // maybe_rw is rw
        // rw有值 直接从rw中的ro获取
        return maybe_rw->ro();
    } else 
        // maybe_rw is actually ro
        // 直接从ro中获取,ro是macho中的数据
        return (class_ro_t *)maybe_rw;、
    }
}

进入addNamedClass,关联地址跟类名

static void addNamedClass(Class cls, const char *name, Class replacing = nil)
{
    runtimeLock.assertLocked();
    Class old;
    if ((old = getClassExceptSomeSwift(name))  &&  old != replacing) {
        inform_duplicate(name, old, cls);
        // getMaybeUnrealizedNonMetaClass uses name lookups.
        // Classes not found by name lookup must be in the
        // secondary meta->nonmeta table.
        addNonMetaClass(cls);
    } else {
        //更新gdb_objc_realized_classes表,将key设置为 name value 设置为cls
        NXMapInsert(gdb_objc_realized_classes, name, cls);
    }
    ASSERT(!(cls->data()->flags & RO_META));
    // wrong: constructed classes are already realized when they get here
    // ASSERT(!cls->isRealized());
}

最后更新gdb_objc_realized_classes哈希表,keynamevalue是cls`,这样子类和地址就关联起来了。

进入addClassTableEntry方法----插入另一张表

static void
addClassTableEntry(Class cls, bool addMeta = true)
{
    runtimeLock.assertLocked();
    // This class is allowed to be a known class via the shared cache or via
    // data segments, but it is not allowed to be in the dynamic 
    //table already.
    // allocatedClasses
    auto &set = objc::allocatedClasses.get();
    ASSERT(set.find(cls) == set.end());
    if (!isKnownClass(cls))
        set.insert(cls);
    if (addMeta)
        //将元类插入哈希表中
        addClassTableEntry(cls->ISA(), false);
}
  • allocatedClasses_objc_initruntime_init运行时环境初始化,里面主要是unattachedCategoriesallocatedClasses两张表,此时插入allocatedClasses表中。
  • addMeta = true 将元类添加allocatedClasses表中。

注意:rw的赋值和ro的获取并不在readClass里面!!

类的加载

类的加载是比较复杂的,需要单独一个篇章进行分析探索,这里只是简单分析以下类加载的一些流程。

XJLPerson非懒加载断点情况

注释很明显的提示初始化非懒加载类,什么是非懒加载类?其实就是实现了load方法或者静态的实例方法,图中添加断点地方没有断住,就是因为XJLPerson是懒加载类。现在给XJLPerson添加load方法:
XJLPerson添加load方法

XJLPerson添加+load方法时候,能够进入断点。

疑点:realizeClassWithoutSwift(cls, nil);方法明显就是类加载的核心点,那么它是怎么样子实现的呢?那就要在下篇文章详细分解了哦。

补充map_image与load_image流程图

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

推荐阅读更多精彩内容