Category、load、initialize、Associated源码解读

本文主要通过OC的源码剖析了与Category相关的原理。其中包括了Category的运行时方法属性管理、load方法原理、initialize方法原理、关联对象的原理。

先几个我们熟知的几个知识点

  • 查找一个方法的时候先从Category列表里找,依次找到主类的方法列表。
  • Category列表的方法顺序是有编译顺序决定的
  • 在程序运行的时候,runtime会将Category的数据,合并到类信息中(类对象、元类对象中)
  • Category的load方法在runtime加载类、分类的时候调用
  • 不能直接给Category添加成员变量,但是可以间接实现Category有成员变量的效果

以上这几个知识点是我们面试中常见到的,今天我们就通过源码去了解背后的原因。

在开始剖析之前需要大概了解一下objc_class的内存结构,附上一张图和一篇之前写的帖子Objective-C的内存结构

Category方法列表

编写一段oc代码

先定义一个Person类和一个Person+Test的分类,然后编译成C++代码,在程序启动之前,编译器会先将各个属性参数、方法列表等信息准备好。

// 定义一个Person类
#import "Person.h"
@implementation Person
- (void)abc {}
- (void)run {
    NSLog(@"Person - run");
}
+ (void)run2 {}
@end
// 定义一个Person+test分类
#import "Person+Test.h"
@implementation Person (Test)
- (void)run {
    NSLog(@"Person (Test) - run");
}
- (void)test {
    NSLog(@"test");
}
+ (void)test2 {}
@end

编译成C++代码

执行命令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Person.m -o Person.cpp 将person编译成c++文件。

  • Person的方法结构体
//实例方法abc 和 run 的结构体,包含了2个元素的方法数组
static struct /*_method_list_t*/ {
    unsigned int entsize;  // sizeof(struct _objc_method)
    unsigned int method_count;
    struct _objc_method method_list[2];
} _OBJC_$_INSTANCE_METHODS_Person __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    2,
    {{(struct objc_selector *)"abc", "v16@0:8", (void *)_I_Person_abc},
    {(struct objc_selector *)"run", "v16@0:8", (void *)_I_Person_run}}
};

// 类方法run2结构体,包含了1个元素的方法数组
static struct /*_method_list_t*/ {
    unsigned int entsize;  // sizeof(struct _objc_method)
    unsigned int method_count;
    struct _objc_method method_list[1];
} _OBJC_$_CLASS_METHODS_Person __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    1,
    {{(struct objc_selector *)"run2", "v16@0:8", (void *)_C_Person_run2}}
};

// _class_ro_t 类方法结构体,包含了上边的类方法列表数据
static struct _class_ro_t _OBJC_METACLASS_RO_$_Person __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    1, sizeof(struct _class_t), sizeof(struct _class_t), 
    0, 
    "Person",
    (const struct _method_list_t *)&_OBJC_$_CLASS_METHODS_Person,
    0, 
    0, 
    0, 
    0, 
};

// _class_ro_t 实例方法结构体,包含了上边实例方法列表数据
static struct _class_ro_t _OBJC_CLASS_RO_$_Person __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    0, sizeof(struct Person_IMPL), sizeof(struct Person_IMPL), 
    0, 
    "Person",
    (const struct _method_list_t *)&_OBJC_$_INSTANCE_METHODS_Person,
    0, 
    0, 
    0, 
    0, 
};

// 元类
extern "C" __declspec(dllimport) struct _class_t OBJC_METACLASS_$_NSObject;
extern "C" __declspec(dllexport) struct _class_t OBJC_METACLASS_$_Person __attribute__ ((used, section ("__DATA,__objc_data"))) = {
    0, // &OBJC_METACLASS_$_NSObject,
    0, // &OBJC_METACLASS_$_NSObject,
    0, // (void *)&_objc_empty_cache,
    0, // unused, was (void *)&_objc_empty_vtable,
    &_OBJC_METACLASS_RO_$_Person,
};

// 类
extern "C" __declspec(dllimport) struct _class_t OBJC_CLASS_$_NSObject;
extern "C" __declspec(dllexport) struct _class_t OBJC_CLASS_$_Person __attribute__ ((used, section ("__DATA,__objc_data"))) = {
    0, // &OBJC_METACLASS_$_Person,
    0, // &OBJC_CLASS_$_NSObject,
    0, // (void *)&_objc_empty_cache,
    0, // unused, was (void *)&_objc_empty_vtable,
    &_OBJC_CLASS_RO_$_Person,
};
static void OBJC_CLASS_SETUP_$_Person(void ) {
// 元类的地址
    OBJC_METACLASS_$_Person.isa = &OBJC_METACLASS_$_NSObject;
    OBJC_METACLASS_$_Person.superclass = &OBJC_METACLASS_$_NSObject;
    OBJC_METACLASS_$_Person.cache = &_objc_empty_cache;
// 类的地址
    OBJC_CLASS_$_Person.isa = &OBJC_METACLASS_$_Person;
    OBJC_CLASS_$_Person.superclass = &OBJC_CLASS_$_NSObject;
    OBJC_CLASS_$_Person.cache = &_objc_empty_cache;
}

  • Person+Test.cpp
- 分类的结构体
struct _category_t {
    const char *name;
    struct _class_t *cls;
    const struct _method_list_t *instance_methods;
    const struct _method_list_t *class_methods;
    const struct _protocol_list_t *protocols;
    const struct _prop_list_t *properties;
};

// 分类实例方法结构体,包含实例方法列表
static struct /*_method_list_t*/ {
    unsigned int entsize;  // sizeof(struct _objc_method)
    unsigned int method_count;
    struct _objc_method method_list[2];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    2,
    {{(struct objc_selector *)"run", "v16@0:8", (void *)_I_Person_Test_run},
    {(struct objc_selector *)"test", "v16@0:8", (void *)_I_Person_Test_test}}
};

// 分类类方法结构体,包含类方法列表
static struct /*_method_list_t*/ {
    unsigned int entsize;  // sizeof(struct _objc_method)
    unsigned int method_count;
    struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    1,
    {{(struct objc_selector *)"test2", "v16@0:8", (void *)_C_Person_Test_test2}}
};

// category 结构体
static struct _category_t _OBJC_$_CATEGORY_Person_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
    "Person",
    0, // &OBJC_CLASS_$_Person,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Test,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Test,
    0,
    0,
};

通过C++文件,我们能大概看出编译后的类和分类的代码结构,这是一个静态编译的结果,为了之后分析运行时做的储备。

  • 方法是一个结构体指针,存放着方法列表
  • 实例方法和类方法存放在两个不同的结构体里
  • 存在两个结构体变量,一个是类的结构体,一个是元类的结构体
  • category的结构体里存放着实例方法和类方法的结构体地址

分析运行时

我们这里使用的是最新版本的oc代码来分析,下载代码时候,对应的iOS13.3版本,oc代码版本号objc4-756.2,可以去apple的opensource网站下载。

这是源码中category结构体,跟我们上边的编译后的C++代码是一样的

struct category_t {
    const char *name; // 类名 Psrson
    classref_t cls; // 类结构体地址
    struct method_list_t *instanceMethods; // 实例方法
    struct method_list_t *classMethods; // 类方法
    struct protocol_list_t *protocols; // 协议
    struct property_list_t *instanceProperties; // 实例属性
    // Fields below this point are not always present on disk.
    struct property_list_t *_classProperties; // 
   // 元类方法还是实例方法
    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }
    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};

源码解读顺序
objc-os.mm 调用顺序

  1. _objc_init
    是OC运行时的入口函数
  2. map_images
  3. map_images_nolock

objc-runtime-new.mm 调用顺序

  1. _read_images 加载镜像
  2. remethodizeClass 重新组装类方法
  3. attachCategories
  4. attachLists
  5. realloc、memmove、 memcpy
    // 下边的代码中,为了方便查看源代码有删减,只保留的关键逻辑
// 加载镜像
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses) {
    // 开始处理category.
    for (EACH_HEADER) {
        //获取到所有的category列表,category放在二维数组里,每个数组存放着一个类的所有category
        //这个数据存放在代码段的__objc_catlist中,感兴趣的话可以查看Meth-O文件。
        category_t **catlist = 
            _getObjc2CategoryList(hi, &count);
        bool hasClassProperties = hi->info()->hasCategoryClassProperties();
        //遍历二维数组
        for (i = 0; i < count; i++) {
            category_t *cat = catlist[i];
            Class cls = remapClass(cat->cls);
            // 判断是否是实例方法,这个对应了我们上边编译好的C++代码
            bool classExists = NO;
            if (cat->instanceMethods ||  cat->protocols  
                ||  cat->instanceProperties) 
            {
                // 将对应的category的方法列表放入到NXMap里
                addUnattachedCategoryForClass(cat, cls, hi);
                if (cls->isRealized()) {
                    // 对类方法进行重组
                    remethodizeClass(cls);
                    classExists = YES;
                }
            }
            // 是否是类方法
            if (cat->classMethods  ||  cat->protocols  
                ||  (hasClassProperties && cat->_classProperties)) 
            {
                addUnattachedCategoryForClass(cat, cls->ISA(), hi);
                if (cls->ISA()->isRealized()) {
                      // 给元类对象重组方法 
                     remethodizeClass(cls->ISA());
                }
            }
        }
    }
}

//重组方法
static void remethodizeClass(Class cls)
{
    attachCategories(cls, cats, true /*flush caches*/);        
}

// 给类、元类附加方法
// 参数:比如上边的代码示例
// cls = Person.class
// cats = [category_t(Test)]
static void 
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
    if (!cats) return;
    if (PrintReplacedMethods) printReplacements(cls, cats);

    bool isMeta = cls->isMetaClass();

    // fixme rearrange to remove these intermediate allocations
    // 申请二维数组结构体空间,空间大小是一个类的所有分类的个数
    /**方法数组
     [
        [method_t, method_t],
        [method_t, method_t],
     ]
     */
    method_list_t **mlists = (method_list_t **)
        malloc(cats->count * sizeof(*mlists));
    /**属性数组
     [
        [property_t, property_t],
        [property_t, property_t],
     ]
     */
    property_list_t **proplists = (property_list_t **)
        malloc(cats->count * sizeof(*proplists));
    /**协议数组
     [
        [protocol_t, protocol_t],
        [protocol_t, protocol_t],
     ]
     */
    protocol_list_t **protolists = (protocol_list_t **)
        malloc(cats->count * sizeof(*protolists));

    // Count backwards through cats to get newest categories first
    int mcount = 0;
    int propcount = 0;
    int protocount = 0;
    int i = cats->count;
    bool fromBundle = NO;
    // 这里使用了i--,将方法列表倒序放入数组中,所以最后面编译的category在数组的最前边
    while (i--) {
        // 取出某个分类
        auto& entry = cats->list[i];
        // 取出分类中的类方法或者元类方法,插入数组中
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            mlists[mcount++] = mlist;
            fromBundle |= entry.hi->isBundle();
        }

        property_list_t *proplist = 
            entry.cat->propertiesForMeta(isMeta, entry.hi);
        if (proplist) {
            proplists[propcount++] = proplist;
        }

        protocol_list_t *protolist = entry.cat->protocols;
        if (protolist) {
            protolists[protocount++] = protolist;
        }
    }

    // 得到类对象数据
    auto rw = cls->data();

    // 将所有分类的对象方法,附加到类对象的方法列表中
    prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
    rw->methods.attachLists(mlists, mcount);
    free(mlists);
    if (flush_caches  &&  mcount > 0) flushCaches(cls);
    // 将所有分类的属性列表,附加到类对象的属性列表中
    rw->properties.attachLists(proplists, propcount);
    free(proplists);
    // 将所有分类的协议列表,附加到类对象的协议列表中
    rw->protocols.attachLists(protolists, protocount);
    free(protolists);
}

    /** 将一个数组的数据插入到另一个数组的前边
     addedLists
    [
       [method_t, method_t],
       [method_t, method_t],
    ]
     addedCount
    */
    void attachLists(List* const * addedLists, uint32_t addedCount) {
        if (addedCount == 0) return;

        // 如果原来的列表中有数据
        if (hasArray()) {
            // many lists -> many lists
            // 原来方法列表的数量
            uint32_t oldCount = array()->count;
            // 插入方法列表之后的数量
            uint32_t newCount = oldCount + addedCount;
            // 重新分配内存
            setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
            array()->count = newCount;
            // 先将老数据向后移动addedCount位置
            memmove(array()->lists + addedCount, array()->lists, 
                    oldCount * sizeof(array()->lists[0]));
            // 将新数据copy进来
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }
        // 如果原来的数据为空,并且新数据只有一个元素,那么直接复制
        else if (!list  &&  addedCount == 1) {
            // 0 lists -> 1 list
            list = addedLists[0];
        }
        
        else {
            // 1 list -> many lists
            List* oldList = list;
            uint32_t oldCount = oldList ? 1 : 0;
            uint32_t newCount = oldCount + addedCount;
            setArray((array_t *)malloc(array_t::byteSize(newCount)));
            array()->count = newCount;
            if (oldList) array()->lists[addedCount] = oldList;
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }
    }

总结一下,以Person为例:

  • 程序启动时,将Person的category分类列表载入内存,顺序为编译顺序,[method_t(test01),method_t(test02)]
  • 处理数组顺序,将最后编译的category数据,放在数组的最前边。
  • 处理类对象、元类对象数据,将category数据列表,插入到类对象、元类对象数据列表的最前边。

+load方法的调用时机

上边我们分析源码的时候,提到了_objc_init 入口函数。
这个函数里调用了load_images方法,接下来调用了call_load_methods方法,我们看下源码

void call_load_methods(void)
{
    do {
        // 1. 加载类的load方法
        while (loadable_classes_used > 0) {
            call_class_loads();
        }
        // 2.加载分类的load方法
        more_categories = call_category_loads();
    } while (loadable_classes_used > 0  ||  more_categories);
}
//1.加载类的load方法
static void call_class_loads(void)
{
    // 遍历所有class,通过函数指针直接调用load方法
    for (i = 0; i < used; i++) {
        Class cls = classes[i].cls;
        (*load_method)(cls, SEL_load);
    }
}
// 2.加载分类的load方法
static bool call_category_loads(void)
{
// 遍历所有category
    for (i = 0; i < used; i++) {
        Category cat = cats[i].cat;
        // 获取分类的class,通过函数指针直接调用load方法
        cls = _category_getClass(cat);
    }
    return new_categories_added;
}

通过源码可以看到

  • +load方法会在runtime加载类、分类时调用
  • 每个类、分类的+load,在程序运行过程中只调用一次
  • 调用顺序
    先调用类的+load,按照编译先后顺序调用(先编译,先调用,调用子类的+load之前会先调用父类的+load
    再调用分类的+load,按照编译先后顺序调用(先编译,先调用)

+ initialize方法的调用时机

initialize调用过程
class_getInstanceMethod
lookUpImpOrNil
lookUpImpOrForward
initializeAndLeaveLocked
initializeAndMaybeRelock
initializeNonMetaClass
callInitialize
objc_msgSend(cls, SEL_initialize)

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
// 没有初始化过,才进行初始化
    if (initialize && !cls->isInitialized()) {
        cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
    }

//初始化class
void initializeNonMetaClass(Class cls)
{
    supercls = cls->superclass;
//    递归调用父类的初始化
    if (supercls  &&  !supercls->isInitialized()) {
        initializeNonMetaClass(supercls);
    }
    if (reallyInitialize) {
//            初始化函数
            callInitialize(cls); //    ((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
           if (PrintInitializing) {
                _objc_inform("INITIALIZE: thread %p: finished +[%s initialize]",
                             pthread_self(), cls->nameForLogging());
            }
    }
}

总结一下

load、initialize方法的区别
  1. 调用方式
    1、load是根据函数地址直接调用
    2、initialize是通过objc_msgSend调用

  2. 调用时刻
    1、load是runtime加载类、分类的时候调用(只会调用1次)
    2、initialize是类第一次接收到消息的时候调用,每一个类只会initialize一次(父类的initialize方法可能会被调用多次)

load、initialize的调用顺序
  • load
    1、 先调用类的load
    a) 先编译的类,优先调用load
    b) 调用子类的load之前,会先调用父类的load
    2、再调用分类的load
    a) 先编译的分类,优先调用load
  1. initialize
    1、 先初始化父类
    2、再初始化子类(可能最终调用的是父类的initialize方法)

关联对象

开发过程中,给category属性赋值,通常会用到objc_setAssociatedObject 和 objc_getAssociatedObject方法,接下来看看关联对象在源码中是怎么实现的。
大概的调用流程是这样的


//objc_setAssociatedObject 
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
    if (!object && !value) return;
    
    assert(object);
    
    if (object->getIsa()->forbidsAssociatedObjects())
        _objc_fatal("objc_setAssociatedObject called on instance (%p) of class %s which does not allow associated objects", object, object_getClassName(object));
    
    // retain the new value (if any) outside the lock.
    ObjcAssociation old_association(0, nil);
    id new_value = value ? acquireValue(value, policy) : nil;
    {
        // 单利,管理所有的关联数据
        AssociationsManager manager;
        // 获取所有的关联对象的map
        AssociationsHashMap &associations(manager.associations());
        // 通过传进来的对象生成一个key
        disguised_ptr_t disguised_object = DISGUISE(object);
        if (new_value) {
            // break any existing association.
            // 通过key获取一个value,这个value是这个对象的所有关联属性的map
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i != associations.end()) {
                // secondary table exists
                ObjectAssociationMap *refs = i->second;
                // 通过外边传进来的key查找到与这个对象相关联的属性map
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    old_association = j->second;
                    // 更新map中的值,包含了内存管理方式和具体的value
                    j->second = ObjcAssociation(policy, new_value);
                } else {
                    (*refs)[key] = ObjcAssociation(policy, new_value);
                }
            } else {
                // 第一次赋值
                ObjectAssociationMap *refs = new ObjectAssociationMap;
                associations[disguised_object] = refs;
                (*refs)[key] = ObjcAssociation(policy, new_value);
                object->setHasAssociatedObjects();
            }
        } else {
            // setting the association to nil breaks the association.
            // 如果传进来的value是nil,则擦除key对应的value
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i !=  associations.end()) {
                ObjectAssociationMap *refs = i->second;
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    old_association = j->second;
                    refs->erase(j);
                }
            }
        }
    }
    // release the old value (outside of the lock).
    if (old_association.hasValue()) ReleaseValue()(old_association);
}

id _object_get_associative_reference(id object, void *key) {
    id value = nil;
    uintptr_t policy = OBJC_ASSOCIATION_ASSIGN;
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        disguised_ptr_t disguised_object = DISGUISE(object);
        AssociationsHashMap::iterator i = associations.find(disguised_object);
        if (i != associations.end()) {
            ObjectAssociationMap *refs = i->second;
            ObjectAssociationMap::iterator j = refs->find(key);
            if (j != refs->end()) {
                ObjcAssociation &entry = j->second;
                value = entry.value();
                policy = entry.policy();
                if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) {
                    objc_retain(value);
                }
            }
        }
    }
    if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
        objc_autorelease(value);
    }
    return value;
}

总结

通过category我们了解到了类的方法列表相关的内存结构,load、initialize方法的调用时机,关联对象的调用原理,再结合之前的一篇oc的Objective-C的内存结构文章,我们大概整体了解了OC对象的内存结构。
其实关于OC语法的相关内容,源码中都能找到答案。只要静下心来,用心读源码,很多问题都会迎刃而解。

如果你觉得有用,给个点赞吧。

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