2020年iOS面试总结

🌸絮:2020年到来了,随着疫情的到来,我也失业了。一边忙着抵抗病毒,一边还得继续准备面试。又该准备“造火箭”了,去了继续“拧螺丝”,下面是自己最近的一些总结,也会在后面的面试中,遇到的面试问题,也一并记录一下。

如有编写有问题,可以留言。不可避免的有错别字,望见谅!
如果您也是个面试者,碰到面问题,可以留言、私信交流一下。

如果你还想看其他面试题,可以移步到2017年面试题

1. ARC帮我们做了什么?

  • 使用LLVM + Runtime 结合帮我管理对象的生命周期
  • LLVM 帮我们在代码合适的地方添加releaseretarnautorelease等添加计数器或者减少计数器操作
  • Runtime 帮我们像__weakcopy等关键字的操作

2.initializeload是如何调用的?它们会多次调用吗?

  • load方法说在应用加载的时候,Runtime直接拿到loadIMP直接去调用的,而不是像其他方式根据objc_msgSend(消息机制)来调用方法的
    • load方法调用的顺序是根据类的加载的前后进行调用的,但是每个类调用的顺序是superclass->class->category顺序调用的,每个load方法只会调用一次(手动调用不算)
    • 一下为Runtime源码的主要代码
    
    load_images(const char *path __unused, const struct mach_header *mh) {
      // 准备class 和category
      prepare_load_methods((const headerType *)mh);
      // 调用load方法
      call_load_methods();
    }
    
    void prepare_load_methods(const headerType *mhdr) {
      classref_t *classlist = 
          _getObjc2NonlazyClassList(mhdr, &count);
      for (i = 0; i < count; i++) {
          schedule_class_load(remapClass(classlist[i]));
      }
      category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
      for (i = 0; i < count; i++) {
          category_t *cat = categorylist[i];
          add_category_to_loadable_list(cat);
      }
    }
    
    static void schedule_class_load(Class cls) {
      // 开始递归,加载superclass
      schedule_class_load(cls->superclass);
      add_class_to_loadable_list(cls);
    }
    
    void call_load_methods(void) {
      do {
          while (loadable_classes_used > 0) {
              call_class_loads();
          }
          more_categories = call_category_loads();
      } while (loadable_classes_used > 0  ||  more_categories);
    }
    
    static void call_class_loads(void) {
      // 在此add_class_to_loadable_list 里面准备了所有重写load的方法的类
      struct loadable_class *classes = loadable_classes;
      // Call all +loads for the detached list.
      for ( int i = 0; i < used; i++) {
          Class cls = classes[i].cls;
          // 获取到load 方法的imp
          load_method_t load_method = (load_method_t)classes[i].method;
          // 调用laod 方法
          (*load_method)(cls, SEL_load);
      }
    }
    
    static bool call_category_loads(void) {
      // 在prepare_load_methods 方法里面准备了所有重新load方法的category
      struct loadable_category *cats = loadable_categories;
      for (int i = 0; i < used; i++) {
          // 获取到catgegory
          Category cat = cats[i].cat;
          // 获取category 的load 方法的IMP实现
          load_method_t load_method = (load_method_t)cats[i].method;
          cls = _category_getClass(cat);
          if (cls  &&  cls->isLoadable()) {
              // 调用load方法
              (*load_method)(cls, SEL_load);
          }
      }
    }
    
    • initialize方法的调用其实和其他方法调用一样的,objc_msgSend(消息机制)来调用的。调用的数序是:没有初始话的superclass -> 实现initializecategort 或者 实现了initializeclass,如果class没有实现initialize 方法,则会调用superclassinitialize,因为initialize的底层是使用了objc_msgSend
    • 看下Runtime底层调用_class_initialize的源码
      void _class_initialize(Class cls) {
        supercls = cls->superclass;
        if (supercls  &&  !supercls->isInitialized()) {
            // 又是个递归
            _class_initialize(supercls);
        }
        // 调用 initialize方法
        callInitialize(cls);
      }
      // objc_msgSend 调用 initialize 方法
      void callInitialize(Class cls) {
        // **注意:因为使用了objc_msgSend,有可能调用class的 initialize **
        objc_msgSend(cls, SEL_initialize);
      }
    

    总结:
    load方法一个类只会调用一次(除去手动调用),而调用的数序是,从superclass -> class -> category,category里面的顺序是先编译,先调用
    initialize方法,一个类可能会调用多次,如果子类没有实现initialize方法,当第一次使用此类的时候,会调用superclass。而调用的顺序是,superclass -> 实现initialize的category 或者 实现了initialize方法(没有category实现initialize) 或者 superclass的initialize (没有子类和category实现initialize方法)

3.说下autoreleasepool

  • MRC下,当对象调用autorerelease方法时候,会将对象加入到对象前面的哪一个autoreleasepool里面,并且当autoreleasepool作用域释放的时候,会对里面的所有的对象进行一次release操作。

    • autoreleasepool底层是使用了AutoreleasePoolPage对象来管理的,AutoreleasePoolPage是一个双向的链表,每个AutoreleasePoolPage都有4096个字节,除了用来存放内部的成员变量,剩下的控件都会用来存放autorelease对象的地址
      /// AutoreleasePoolPage 的简化的结构
      class AutoreleasePoolPage {
        magic_t const magic;
        // 下一次可以存储对象的地址
        id *next;
        pthread_t const thread;
        // 标识上一个page对象
        AutoreleasePoolPage * const parent;
        // 标识下一个page对昂
        AutoreleasePoolPage *child;
        uint32_t const depth;
        uint32_t hiwat;
      }
      
    • autoreleasepool开始的时候,会调用AutorelasePoolPagepush方法,会讲一个标识POOL_BOUNDARY添加到AutoreleasePoolPage对象里面,并且返回POOL_BOUNDARY的地址r1(暂且这样叫)
    • 当对像进行relase的时候,会将对象的地址添加到当前AutorelasePoolPage里面,依次添加。
    • autoreleasepool作用域结束的时候,会调用AutorelasePoolPagepop(r1)方法(r1为当前aotoreleasepool开始的加入标识POOL_BOUNDARY的地址),AutorelasePoolPage则会将里面保存的对象的从左后一个开始进行release操作,当碰到r1时候,标识当前那个autoreleasepool里面所有的对象都进行了一次release操作。
    @autoreleasepool {
      // 此处会调用
       void *ctxt = AutoreleasePoolPage::push();
      // 添加到最近的一个autoreleasepool中
      [[[NSObject alloc]init] autorelease];
      //移除作用域的时候调用
      AutoreleasePoolPage:pop(ctxt)
    }
    // autoreleasepool 作用域开始会调用AutoreleasePoolPage::push()
    static inline void *push() {
      id *dest;
      if (DebugPoolAllocation) {
        // 创建一个心的page对象
        dest = autoreleaseNewPage(POOL_BOUNDARY);
      } else {
        // 已经有了page对象,讲`pool_boundary`添加进去
        dest = autoreleaseFast(POOL_BOUNDARY);
      }
    }
    static inline id *autoreleaseFast(id obj)
    {
      // 获取正在使用的page对昂
      AutoreleasePoolPage *page = hotPage();
      // page还没有装满
      if (page && !page->full()) {
        return page->add(obj);
      } else if (page) {
        // 已经添加满了
        return autoreleaseFullPage(obj, page);
      } else {
        // 没有page对象,创建心的page对象
        return autoreleaseNoPage(obj);
      }
    }
    // 对象调用release 的简介源码
    id objc_object::rootAutorelease2() {
      return AutoreleasePoolPage::autorelease((id)this);
    }
    static inline id autorelease(id obj) {
      // 同样也是添加进去
      id *dest = autoreleaseFast(obj);
      return obj;
    }
    // page调用pop简介源码 *token 表示结束的标识
    static inline void pop(void *token) {
      AutoreleasePoolPage *page;
      id *stop;
      page = pageForPointer(token);
      stop = (id *)token;
      page->releaseUntil(stop);
    }
    // 释放对象的源码
    void releaseUntil(id *stop) {
      // next 标识当前page可以存储对象的下一个地址
      while (this->next != stop) {
        AutoreleasePoolPage *page = hotPage();
        // 因为page是个双向链表,当page为空的时候,需要往上查找parent的page对象里面存储的睇相
        while (page->empty()) {
          page = page->parent;
          setHotPage(page);
        }
        id obj = *--page->next;
        if (obj != POOL_BOUNDARY) {// obj 不是刚开始传入的POOL_BOUNDARY及表示对象,所以需要调用一次操作
          objc_release(obj);
        }
      }
    }
    
    autoreleasepool.png
  • autoreleasepoolrunloop的关系

    • runloop里面会注册两个Observer来监听runloop的状态变化
      • 其中一个Observer监听的状态为kCFRunLoopEntry进入runloop的状态,则会调用AutoreleasePoolPage::push()方法
      • 另外中一个Observer监听的状态为kCFRunLoopBeforeWaiting、kCFRunLoopExit,即将休眠和退出当前的runloop。
        • kCFRunLoopBeforeWaiting的回掉里面会调用AutoreleasePoolPage::pop(ctxt)和AutoreleasePoolPage::(push)方法,释放上一个autoreleasepool里面添加的对象,并开启下一个autoreleasepool
        • kCFRunLoopExitObserver回掉里面会调用AutoreleasePoolPage::(push)释放autoreleasepool里面的对象

4.category属性是存储在那里?

  • 我们都知道可以使用Runtimeobjc_setAssociatedObjectobjc_getAssociatedObject两个方法给category的属性重写getset方法,而此属性的值是存储在那里呢?
  • 其实此属性的值保存在一个AssociationsManager里面。
  • 我们也是可以根据源码看一下
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
   // 一下为精简的代码
    id new_value = value ? acquireValue(value, policy) : nil;
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        disguised_ptr_t disguised_object = DISGUISE(object);
        if (new_value) {
          ObjectAssociationMap *refs = new ObjectAssociationMap;
          associations[disguised_object] = refs;
          (*refs)[key] = ObjcAssociation(policy, new_value);
        }
    }
}
AssociationsManager.png

5.category方法是如何添加的?

  • 当我们给分类添加相同的方法的时候,会调用category里面的方法,而不是调用我们class里面的方法
  • 当编译器编译的时候,编译器会将category编译成category_t这样的结构体,等类初始化的时候,会将分类的信息同步到class_rw_t里面,包含:method、property、protocol等,同步的时候会将category里面的信息添加到class的前面(而不是替换掉class里面的方法),而方法调用的时候,而是遍历class_rw_t里面的方法,所以找到分类里面的IMP则返回。
    • 使用memmove,将类方法移动到后面
    • 使用memcpy,将分类的方法copy到前面
  • 当多个分类有相同的方法的时候,调用的顺序是后编译先调用
    • 当类初始化同步category的时候,会使用while(i--)的倒序循环,将后编译的category添加到最前面。
category.png

6. OC 的消息机制

  • 消息机制可以分为三个部分
    • 消息传递
      • 当我么调用方法的时候,方法的调用都会转化为objc_msgSend这样来传递。
      • 第一步会根据对象的isa指针找到所属的类(也就是类对象)
      • 第二步,会根据类对象里面的catch里面查找。catch是个散列表,是根据@selector(方法名)来获取对应的IMP,从而开始调用
      • 第三步,如果第二步没有找到,会继续查找到类对象里面的class_rw_t里面的methods(方法列表),从而遍历,找到方法所属的IMP,如果查找到则会添加到catch表里面
      • 第四步,如果第三部也没有找到,会根据类对象里面的superclass指针,查找supercatch,如果也是没有查找,会继续查找到superclass里面的class_rw_t里面的methods(方法列表),从而遍历,找到方法所属的IMP,如果查找到则会添加到catch表里面
      • 第五步,如果第四部还是没有查找到,此时会根据类的superclass,继续第四部操作
      • …………
      • 第六步。如果一直查找到基类都没有找到响应的方法,则会进入动态解析里面
    • 动态解析
      • 当消息传递,没有找到对应的IMP的时候,会进入的动态解析中
      • 此时会根据方法是类方法,还是实例方法分别调用+(BOOL)resolveClassMethod:(SEL)sel+(BOOL)resolveInstanceMethod:(SEL)sel
      • 我们可以实现这两个方法,使用Runtime的class_addMethod来添加对应的IMP
      • 如果添加后,返回true,没有添加则调用父类方法
      • 注意:其实返回true或者false,结果都是一样的,再次掉消息传递步骤
    • 消息转发
      • 如果我们没有实现动态解析方法,就会走到消息转发这里
      • 第一步,会调用-(id)forwardingTargetForSelector:(SEL)aSelector方法,我们可以在这里,返回一个响应aSelector的对象。当返回不为nil时候,系统会继续再次走消息转发,继续查找对应的IMP
      • 第二步,如果第一步返回nil或者self(自己),此时系统会继续走这里-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector,需要返回aSelector的一个签名
      • 第三步,如果返回了签名,就会到这里-(void)forwardInvocation:(NSInvocation *)anInvocation,相应的我们可以根据anInvocation,可以获取到参数、target、方法名等,再次操作的空间就很多了,看你需求喽。此时我们什么都不操作也是没问题的,
      • 注意:当我们是类方法的时候,其实我们可以将以上方法的-改为+,即可实现了类方法的转发
objc_msgSend.png

7.weak表是如何存储__weak指针的

  • weak关键字,我们都知道,当对象销毁的时候,也会将指针赋值为nil,而weak的底层也是将指针和对象以键值对的形式存储在哈希表里面
  • 当使用__weak修饰的时候,底层会调用id objc_storeWeak(id *location, id newObj)传递两个参数
    • 第一个参数为指针,第二个参数为所指向的对象
  • 第二步,继续调用storeWeak(location, (objc_object *)newObj)
    • 第一个参数是指针,第二个参数是对象的地址
    • 再次方法里面会根据对象地址生成一个SideTables对象
  • 第三步,调用id weak_register_no_lock(weak_table_t *weak_table, id referent_id, id *referrer_id, bool crashIfDeallocating)
    • weak_table则为SideTables的一个属性referent_id为对象,referrer_id则为那个弱引用的指针
    • 在此里面会根据对象地址和指针生成一个weak_entry_t
  • 第四步,会继续调用static void weak_entry_insert(weak_table_t *weak_table, weak_entry_t *new_entry)
    • 重点:在此方法里面会根据对象 & weak_table->mask(表示weak表里面可以存储的大小减一,例如:表可以存储10个对象,那么mask就是9), 生成对应的index,如果index对应已经存储上对象,则会index++的方式找到未存储的对应,并将new_entry存储进去,储存在weak_table里的weak_entries属性里面
  • 注意:当一个对象多个weak指针指向的时候,生成的也是一个entry,多个指针时保存在entry里面referrers属性里面
  • 以下为简易的源码:
id
objc_storeWeak(id *location, id newObj)
{
    return storeWeak<DoHaveOld, DoHaveNew, DoCrashIfDeallocating>
        (location, (objc_object *)newObj);
}
static id 
storeWeak(id *location, objc_object *newObj) {
  // 根据对象生成新的SideTable
  SideTable *newTable = &SideTables()[newObj];
  newObj = (objc_object *)
  weak_register_no_lock(&newTable->weak_table, (id)newObj, location,  crashIfDeallocating);
}
id 
weak_register_no_lock(weak_table_t *weak_table, id referent_id, 
                      id *referrer_id, bool crashIfDeallocating){
  objc_object *referent = (objc_object *)referent_id;
  objc_object **referrer = (objc_object **)referrer_id;
  
  // 根据对象和指针生成一个entry
  weak_entry_t new_entry(referent, referrer);
  // 检查是是否该去扩容
  weak_grow_maybe(weak_table);
  // 将新的entry 插入到表里面
  weak_entry_insert(weak_table, &new_entry);
}
static void weak_entry_insert(weak_table_t *weak_table, weak_entry_t *new_entry)
{
    weak_entry_t *weak_entries = weak_table->weak_entries;

    size_t begin = hash_pointer(new_entry->referent) & (weak_table->mask);
    size_t index = begin;
    size_t hash_displacement = 0;
    while (weak_entries[index].referent != nil) {
        index = (index+1) & weak_table->mask;
        if (index == begin) bad_weak_table(weak_entries);
        hash_displacement++;
    }
    weak_entries[index] = *new_entry;
    weak_table->num_entries++;
}

weak_table的扩容,根据存储条数 >= 最大存储条数的3/4时,就会按照两倍的方式进行扩容,并且会将已经有的条目再次生成新的index(因为扩容后,weak_table的mask发生了改变)。进行保存

  • 以下为简易的源码:
static void weak_grow_maybe(weak_table_t *weak_table)
{
    size_t old_size = (weak_table->mask ? weak_table->mask + 1 : 0);
    if (weak_table->num_entries >= old_size * 3 / 4) {
        weak_resize(weak_table, old_size ? old_size*2 : 64);
    }
}
static void weak_resize(weak_table_t *weak_table, size_t new_size)
{
    size_t old_size = TABLE_SIZE(weak_table);
    weak_entry_t *old_entries = weak_table->weak_entries;
    // calloc 分配新的控件
    weak_entry_t *new_entries = (weak_entry_t *)
        calloc(new_size, sizeof(weak_entry_t));
    // mask 就是大小减一
    weak_table->mask = new_size - 1;
    weak_entry_t *entry;
    weak_entry_t *end = old_entries + old_size;
    for (entry = old_entries; entry < end; entry++) {
        if (entry->referent) {
           weak_entry_insert(weak_table, entry);
        }
    }
}

8. 方法catch表是如何存储方法的

  • 我们都是知道调用方法的时候,会根据对象的isa查找到对象类对象,并开始在catch表里面查询对应的IMP
  • 其实catch是个散列表,是根据方法的@selector(方法名) & catch->mask(catck表最大数量 - 1)得到index,如果index已经存储了新的方法,那么就会index++,如果index对应的值为nil时,将响应的方法,插入到catch表里面
  • 核心代码
static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver) {
  // 获取类对象的catch地址
  cache_t *cache = &cls->cache
  // 获取key
  cache_key_t key = (cache_key_t)sel;
  // 找到bucket
  bucket_t *bucket = cache->find(key, receiver);
}

bucket_t * cache_t::find(cache_key_t k, id receiver)
{
    // catch表的buckets属性
    bucket_t *b = buckets();
    // catch 表示的mask 最大值 - 1
    mask_t m = mask();
    
    mask_t begin = cache_hash(k, m);
    mask_t i = begin;
    do {
        if (b[i].key() == 0  ||  b[i].key() == k) {
            return &b[i];
        }
    } while ((i = cache_next(i, m)) != begin);
}
static inline mask_t cache_next(mask_t i, mask_t mask) {
    return (i+1) & mask;
}

注意:catch表的扩容,同样也是和weak_table一样按照2倍的方式进行扩容,但是注意:扩容后,以前缓存的方法则会被删除掉。

  • 简易代码
void cache_t::expand() {
    uint32_t oldCapacity = capacity();
    uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;
    reallocate(oldCapacity, newCapacity);
}

void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity)
{
    // 获取旧的oldBuckets
    bucket_t *oldBuckets = buckets();
    // 重新分配新的
    bucket_t *newBuckets = allocateBuckets(newCapacity);
    // free 掉旧的
    cache_collect_free(oldBuckets, oldCapacity);
}

9.优化后isa指针是什么样的?存储都有哪些内容?

  • 最新的Objective-C的对象里面的isa指针已经不是单单的指向所属类的地址了的指针了,而时变成了一个共用体,并且使用位域来存储更多的信息
isa.png

10.App启动流程,以及如何优化?

  • 启动顺序

    • dyld,Apple的动态连接器,可以用来装载Mach-O文件(可执行文件、动态库)
      • 装载App的可执行文件,同事递归加载所有依赖的动态库
      • 当dyld把可执行文件、动态库装载完毕后,会通知Runtime进行下一步的处理
    • Runtime
      • 调用map_images进行可执行文件内容的解析和处理
      • load_images里面调用call_load_methods,调用所有class和category的+load方法
      • 进行各种objc结构的初始化(注册Objc类,初始化类对象等等)
      • 到目前未知,可执行文件和动态库中所有的符号(Class,Protocol,Selector,IMP..)都已经按照格式成功加载到内存中,被runtime管理
    • main函数调用
      • 所有初始化工作结束后,dyld就会调用main函数
      • 截下来就是UIApplicationMan函数,AppDelegateapplication:didFinishLaunchingWithOptions:
  • App启动速度优化

    • dyld

      • 减少动态库,合并一些自定义的动态库,以及定期清理一些不需要的动态库
      • 较少Objc类、category的数量、以及定期清理一些不必要的类和分类
      • Swift尽量使用struct
    • Runtime

      • 使用+initializedispatch_once取代Objc的+load方法、C++的静态构造器
    • main

      • 再不印象用户体验的情况下面,尽可能的将一些操作延迟,不要全部放到finishLaunching
        • 一些网络请求
        • 一些第三方的注册
      • 以及window的rootViewControllerviewDidload方法,也别做耗时操作
    • 注意:我们可以添加环境变量可以打印出App的启动时间分析(Edit scheme -> Run -> Arguments)

      • DYLD_PRINT_STATISTICS设置为1,可以打印出来每个阶段的时间
      • 如果需要更详细的信息,那就设置DYLD_PRINT_STATISTICS_DETAILS为1
DYLD_PRINT_STATISTICS.png

11.App瘦身

  • 资源(图片、音频、视频等)

    • 可以采取无损压缩
    • 使用LSUnusedResources去除没有用的资源 LSUnusedResources
  • 可执行文件瘦身

    • Strip Linked ProductMake Strings Read-OnlySymbols Hidden by Default设置为true
    • 去掉一些异常支持 Enable C++ ExceptionsEnable Objective-C Exceptions设置为false
    • 使用AppCode检测未使用的代码:菜单栏 -> Code -> Inspect Code,等编译完成后,会看到未使用的类
  • 生成LinkMap文件,可以查看可执行文件的具体组成

    • 可借助第三方工具解析LinkMap文件LinkMap
Link Map.png

Link Map解析结果

LinkMap 解析结果.png

如果招聘者看到了,感觉我还可以,欢迎您的私信一下
如果招聘者看到了,感觉我还可以,欢迎您的私信一下
如果招聘者看到了,感觉我还可以,欢迎您的私信一下

持续更新中。。。。

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