阅读Effective Object-C 2.0 笔记(六)

还是要好好学习英文啊,笔者只能看中文版的,下载地址如下:
http://download.csdn.net/detail/m6830098/7977521
看书的时候还是困的不行不行的-

今天来学习学习本书的第六章。

第一条:理解"块"的概念。块可以实现闭包。这项语言特性是做为"扩展(extension)"而加入GCC编译器中的,在近期版本的Clang中都可以使用(Clang是开发Mac OS X及 iOS程序的编译器)。块与函数类似,只不过是直接定义在另一个函数里的,和定义它的那个函数共享同一个范围内的东西。块用" ^ "符号来表示,后面跟一对花括号,括号里面是块的实现代码。如:

 ^{
        //Block implementation here
   }

块其实就是个值,而且自有其相关类型。与int、float和Object-C对象一样,也可以吧块赋值给变量,然后像使用其他变量一样使用块。块类型的语法与函数指针类似。

void (^someBlock) () = ^{
     //Block implementation here
};

块类型的语法结构如下:

return_type (^block_name) (parameters)

例如:返回int值,有两个参数的块:

int (^addBlock) (int a, int b) = ^(int a, int b){ 
    return a+b;
}

定义好之后就可以像函数一样使用了。

int add = addBlock(2, 5);     // add =  7

快的强大之处在于:在声明它的范围里,所有变量都可以为其所捕获。即那个范围里的全局变量在块里依然可以使用。默认情况下,为块所捕获的变量,是不可以在块里修改的。如果修改了,编译器就会报错。不过在声明变量的时候可以加上 _ _block修饰符,这样就可以在块内修改了。

NSArray *array = @[@0,@1,@2,@3,@4];
__block int count = 0;
[array enumerateObjectsUsingBlock:^(NSNumber  *number, NSUInteger idx, BOOL * _Nonnull stop) {
    if ([number compare:@2] == NSOrderedAscending) {
        count ++;
    }
}];
//cout = 2

如果块所捕获的变量是对象类型,那么久会自动保留它。系统在释放这个块的时候,也会将其一并释放。如果将块定义在Object-C类的实例方法中,那么除了可以访问类的所有变量之外,还可以使用self变量。块总能修改实例变量,所以在声明是无须加_ _block。不过,如果通过读取或者写入操作捕获了实例变量,那么也会自动把self变量一并捕获了,因为实例变量是与self所指代的实例管理在一起的。

@interface WWClass

- (void)anInstanceMethod {
    //...
    void (^someBlock) () = ^{
        _anInstanceVariable = @"SomeThing";
        NSLog(@"_anInstanceVariable = %@", _anInstanceVariable);
    };
    //...
}
@end

如果某个WWClass实例正在执行anInstanceMethod方法,那么self变量就指向此实例。直接访问实例变量和通过self来访问是等效的:

self ->_anInstanceVariable = @"SomeThing";

注意:self也是个对象,因而块在捕获它时也会将其保留,如果self所指代的那个对象同时也保留了块,那么这种情况通常就会导致"保留环"。

块的内部结构:每个Object-C对象都占据着某个内存区域。因为实例变量的个数以及对象所包含的关联数据互不相同,所以每个对象所占的内存区域也有大有小。块本身也是对象,在存放块的对象内存区域中,首个变量是指向Class对象的指针,改指针叫做isa。其余内存里含有块对象正常运转所需的各种信息。


块对象的内存布局.png

在内存布局中,最重要的就是invoke变量,这是个函数指针,指向块的实现代码。函数原型至少要接受一个void *型的参数,此参数代表块。descriptor变量是指向结构体的指针,每个块里都包含此结构体,其中声明了块对象的总体大小,还声明了copy与dispose这两个辅助函数所对应的函数指针。

全局块、栈块以及堆块。定义块的时候,其所占的内存区域是分配在栈中的。也就是说,块只有在定义它的那个范围内有效。下面这段代码有危险:

void (^block) ();
if (/* some condition */ ) {
    block = ^ {
        NSLog(@"Block A");
    };
} else {
    block = ^ {
        NSLog(@"Block B");
    };
}
block();

定义在if以及else语句中的两个块都分配在栈内存中。若编译器未覆写待执行的块,则程序照常运行,若覆写,则程序崩溃。为解决此问题,可以给块对象发送copy消息以拷贝之。这样就能把块从栈复制到堆了。并且一旦复制到堆上,块就成了带引用计数的对象了,后续复制操作都不会真的执行复制,只是递增块对象的引用计数。下面这段代码就是安全的:

void (^block) ();
if (/* some condition */ ) {
    block = [^ {
        NSLog(@"Block A");
    } copy];
} else {
    block = [^ {
        NSLog(@"Block B");
    } copy];
}
block();

除了"栈块"和"堆块",还有一种"全局块"。这种块不会捕捉任何状态(比如外围的变量等)。运行时也无效有状态来参与。块所使用的整个内存区域,在编译期已经完全确定了,因此,全局块可以声明在全局内存里,而不需要再每次用到的时候于栈中创建。全局块的拷贝操作是个空操作,因为全局块决不可能为系统所回收。这种块实际上相当于单例。

第二条:为常见的快类型创建typedef。每个块都具备其"固有类型(inherent type)",因而可将其赋给适当类型的变量。这个类型由块所接受的参数及其返回值组成。与定义其他变量时一样,变量类型在左边,变量名在右边。

- (void)startWithCompletionHandler:(void (^) (NSData *data, NSError, *error))completion;

给参数定义一个别名,然后使用此名称来定义块:

typedef void (^WWCompletionHandler) (NSData *data, NSError, *error);
- (void)startWithCompletionHandler:(WWCompletionHandler)completion;

要点:1.以typedef重新定义块类型,可以令块变量用起来更加简单。2.定义新类型时应遵从现有的命名习惯,勿使其名称与别的类型相冲突。3.为同一个块签名定义多个类型别名。如果要重构的代码使用了块类型的某个别名,那么只需修改响应的typedef中的快签名既可,无须改动其他typedef。

第三条:用handler块降低代码分散程度。处理用户界面的显示及触摸操作所用的线程,不会应为要执行I/O或者网络通信这类耗时的任务而阻塞。这个线程通常称为主线程。假设把执行异步任务的方法做成同步的,那么在执行任务时,用户界面就变得无法响应用户输入了。某些情况下,如果应用程序在一定时间内无响应,那么就会自动终止,ios系统上的应用就是如此,"系统监控器(system watchdog)"在发现某个应用程序的主线程已经阻塞了一段时间之后,就会令其终止。委托模式有个缺点:如果类要分别使用多个获取器下载不同的数据,那么就得在delegate回调方法里根据传人的获取器参数来切换。这样不仅会令delegate回调方法变得很长,而且还要把网络数据获取器对象保存为实例变量,用来判断。改用块的好处:无须保存获取器,也无须在回调方法里面切换,每个completion handler的业务逻辑,都是和相关的获取器对象一起来定义的。这种写法还有其他的用途,比如:现在很多基于块的API都是用块来处理错误。可以分别用两个处理程序来处理操作失败的情况和操作成功的情况。也可以把处理失败情况所需的代码,与处理正常情况的的代码,都封装到同一个completion handler块中,该块多一个NSError类型的参数而已。

第四条:用块引用其所属对象是不要出现保留环。使用块时,很容易形成保留环。


形成保留环事例图.png

第五条:多用派发队列,少用同步锁。通常要使用锁来实现某种同步机制。在GCD出现之前,有两种办法,第一种是采用内置的"同步块(synchronization block)":

- (void)synchronizedMethod {
    @synchronized(self) {
        //safe
    }
}

这种方法会根据给定的对象,自动创建一个锁,并等待块中的代码执行完毕。执行到这段代码的结尾处,锁就释放了。另一个办法是直接使用NSLock对象:

_lock = [[NSLock alloc] init];

- (void)synchronizedMethod {
    [_lock  lock];
    //safe
    [_lock  unlock];
}

也可以使用NSRecursiveLock这种"递归所(recursive lock)",线程能够多次持有该锁,而不会出现死锁(deadlock)现象。有种简单而高效的办法可以代替同步块或者锁对象,那就是使用"传销同步列队(serial synchronization queue)"。将读取操作以及写入操作都安排在同一个列队中,既可包装数据同步。用法:

_syncQueue = dispatch_queue_create("com.effectiveobjectc.syncQueue", NULL);

- (NSString *)someString {
    __block NSString *localSomeString;
    dispatch_sync(_syncQueue, ^{
        localSomeString = _someString;
    });
    return  localSomeString;
}

- (void)setSomeString:(NSString *)someString {
    dispatch_sync(_syncQueue, ^{
        _someString = someString;
    });
}

此模式的思路是:把设置操作和获取操作都安排在序列化的列队里执行,这样的话,所有震动属性的访问操作就都同步了。多个的获取方法可以并发执行,而获取方法与设置方法之间不能并发执行,可以使用并发队列(concurrent queue):

_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,   NULL);

- (NSString *)someString {
    __block NSString *localSomeString;
    dispatch_sync(_syncQueue, ^{
        localSomeString = _someString;
    });
    return  localSomeString;
}

- (void)setSomeString:(NSString *)someString {
    dispatch_async(_syncQueue, ^{
        _someString = someString;
    });
}

像现在这样写代码,还是无法正确实现同步。所有读取操作与写入操作都会在同一个队列上执行,不过由于是并发队列,所有读取与写入挫折可以随时执行。而我们不想让这些操作随意执行。此时用一个简单的GCD功能即可,它就是栅栏(barrier)。下列函数可以向队列中派发块,将其作为栅栏使用:

void dispatch_barrier_async (dispatch_queue_t queue, dispatch_block_t block);
void dispatch_barrier_sync (dispatch_queue_t queue, dispatch_block_t block);

在队列中,栅栏块必须单独执行,不能与其他块并行。这只对并发队列有意义,因为串行队列中的快总是按顺序逐个来执行的。

_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,   NULL);

- (NSString *)someString {
    __block NSString *localSomeString;
    dispatch_sync(_syncQueue, ^{
        localSomeString = _someString;
    });
    return  localSomeString;
}

- (void)setSomeString:(NSString *)someString {
    dispatch_barrier_async(_syncQueue, ^{
        _someString = someString;
    });
}
栅栏块操作时序图.png

第六条:多用GCD,少用performSelector系列方法。performSelector系列函数中,最简单的是"performSelector:",它接受一个参数,就是要执行的那个选择子:

- (id)performSelector:(SEL)selector;

该方法与直接调用选择子方法等效。下面2行代码效果相同:

[object performSelector:@selector(selectorName)];
[object selectorName];

如果选择子是在运行期决定的,那么就能体现出此方式的强大之处了,这就等于在动态之上再次动态绑定。

SEL selector;
if (/* some condition */) {
    selector = @selector(foo);
} else if (/* some other condition */) {
    selector = @selector(bar);
} else {
    selector = @selector(baz);
}
[object performSelector:selector];

不够这样可能导致内存泄漏,改进如下:
SEL selector;
if (/* some condition /) {
selector = @selector(foo);
} else if (/
some other condition */) {
selector = @selector(bar);
} else {
selector = @selector(baz);
}
id ret = [object performSelector:selector];

performSelector:还有如下两个版本,可以在发消息时顺便传递参数:

- (id)performSelector:(SEL)selector  withObject:(id)object;
- (id)performSelector:(SEL)selector  withObject:(id)objectA  withObject:(id)objectB;

这个方法的局限比较多,由于参数类型是id,所以传人的参数必须是对象才行,如果选择子所介接受的参数是浮点数或者整数,就不能用该方法了,此外,该方法最多只能接受两个参数。performSelector系列方法还有个功能就是延迟执行选择子。或将其放在另一个线程执行。

- (id)performSelector:(SEL)selector  withObject:(id)argument  afterDelay:(NSTimeInterval)delay;
- (id)performSelector:(SEL)selector  onThread:(NSThread *)thread  withObject:(id)argument  waitUntilDone:(BOOL)wait;
- (id)performSelectorOnMainThread:(SEL)selector  withObject:(id)argument waitUntilDone:(BOOL)wait;

如果要延后执行某项任务,可以有两种方法,应该优先第二种:
//Using performSelector:withObject:afterDelay:
[self performSelector:@selector(doSomething) withObject:nil afterDelay:5.0];

//Using dispatch_after
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC));
dispatch_after(time, dispatch_get_main_queue(), ^(void){
    [self  doSomething];
});

把任务放在主线程上执行,同理:
//Using performSelectorOnMainThread:withObject:waitUntilDone
[self performSelectorOnMainThread:@selector(doSomething) withObject:nil waitUntilDone:NO];

//Using dispatch_async
//(or if waitUntilDone is YES,then dispatch_sync)
dispatch_async(dispatch_get_main_queue(), ^{
    [self  doSomething];
});

要点:performSelector系列方法在内存管理方面容易有疏失。它无法确定将要执行的选择子具体是什么,因而ARC编译器也就无法插入适当的内存管理方法。

第七条:掌握GCD及操作列队的使用时机。对于那些只需执行一次的代码,使用GCD的dispatch_once最为方便。然而在执行后台任务时,GCD并不一定是最佳方法。还有一种叫做NSOperationQueue,它虽然与GCD不同,但却与之相关,开发者可以把操作以NSOperation子类的形式放在列队中,而这些操作也能够并发执行。两者的差别,首先要注意:GCD是纯C的API,而操作列队则是Object-C的对象。在GCD中,任务用块来表示,而块是个轻量级的数据结构。与之相反,"操作(operation)"则是个重量级的Object-C对象。用NSOperationQueue类的"addOperationWithBlock:"方法搭配NSBlockOperation类来使用操作列队,其语法与纯GCD方式非常类似。好处:1.取消某个操作。如果使用操作列队,那么想要取消操作是很容易的。运行任务之前,可以在NSOperation对象上调用cancel方法,该方法会设置对象内的标志位,用以表明此任务不需要执行,不过,已经启动的任务无法取消。如果不使用操作列队,而是把块安排到GCD队列,那就无法取消了。2.指定操作间的依赖关系。一个操作可以依赖其他多个操作。开发者能够指定操作之间的依赖体系,使特定的操作必须在另一个操作顺利执行完毕后方可执行。3.通过键值观测机制监控NSOperation对象的属性。NSOperation对象有许多属性都适合通过键值观测机制(KVO)来监听,比如可以通过isCancelled属性来判断任务是否已取消,通过isFinished属性来判断任务是否已完成。4.指定操作的优先级。操作的优先级表示此操作与队列中的其他操作之间的优先级关系,优先级高的操作先执行。GCD没有直接实现此功能的办法,GCD的队列确实有优先级,但那是针对整个列队的。5.重用NSOperation对象。系统内置了一下NSOperation的子类(NSBlockOperation),也可以自己创建。有一个API选用了操作队列而非派发列队,那就是NSNotificationCenter,开发者通过其中的方法来注册监听器,以便在发生相关的事件时得到通知,而这个方法的接受参数是块,不是选择子:

- (id)addObserverForName:(NSString *)name object:(id)object queue:(NSOperationQueue *)queue usingBlock:(void (^) (NSNotification *))block;

第八条:通过Dispatch Group机制,根据系统资源状况来执行任务。dispatch group 是GCD的一项特性,能够把任务分组。调用者可以等待这组任务执行完毕,也可以在提供回调函数之后继续往下执行。dispatch_group的创建方法:
dispatch_group_t group = dispatch_group_create();
把任务编组的两种方法:
1.使用下面的函数:

void  dispatch_group_async (dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);

它是普通dispatch_async函数的变体,比原来的多一个参数,用于表示待执行的块的所归属的组。
2.使用下面的一对函数:

void  dispatch_group_enter (dispatch_group_t group);
void  dispatch_group_leave (dispatch_group_t group);

前者能够使分组里正要执行的任务数递增,而后者则使之递减。故调用了dispatch_group_enter以后,必须有与之对应的dispatch_group_leave才行。
下面这个函数用于等待dispatch group执行完毕:

  long  dispatch_group_wait (dispatch_group_t group,dispatch_time_t timeout);

此函数接受二个参数,一个是要等待的group,另一个是代表等待时间的timeout值,timeout参数表示函数在等待dispatch group执行完毕时,应该阻塞多久。如果执行dispatch group所需的时间小于timeout,则返回0;否则返回非0值。次参数也可以取常量DISPATCH_TIME_FOREVER,这表示函数会一直等着dispatch group执行完,而不会超时。
还有一个函数:

void  dispatch_group_notify (dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);

与wait函数不同的是:可以向此函数传人块,等dispatch group执行完之后,块会在特定的线程上执行。

也可以用GCD来实现循环。函数

void  dispatch_apply (size_t iterations dispatch_queue_t queue, void(^block)(size_t));

此函数会反复执行一定的次数,每次传给块的参数值都会递增,从0开始,直到"iterations - 1",其用法如下:

dispatch_queue_t queue = dispatch_queue_create("com.effectiveobjectc.syncQueue", NULL);
dispatch_apply (10, queue, ^(size_t i){
        //perform task
});

需要注意的是:dispatch_apply所用的队列可以是并发队列。如果采用并发队列,循环处理数组:

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,   NULL);
dispatch_apply (array.count, queue, ^(size_t i){
    //perform task
    id object = array[i];
    [object  performTask];    
});

第九条:使用dispatch_once来执行只需运行一次的线程安全代码。对于单例模式(singleton)而言,在类中编写sharedInstance的方法,该方法只会返回全类共用的单例实例,而不会在每次调用的时候都创建新的实例。没有GCD的时候:

+ (id)sharedInstance {
      static  WWClass *sharedInstance = nil;
      @synchronized(self) {
            if (! sharedInstance) {
                sharedInstance = [[self alloc]  init];
            }
         }
         return sharedInstance;
    }

有了GCD之后,使用下面的函数重新改写单例函数:

void  dispatch_once (dispatch_once_t *token,  dispatch_block_t block);

此函数的接受类型为dispatch_once_t的特殊参数,我们可以称之为"标记"(token),此外还有块参数。对于给定的标记来说,该函数保证相关的块必定会执行,且仅执行一次。首次调用该函数时,必然会执行块中的代码,最重要的是,此操作完全是线程安全的。注意:对于只需执行一次的块来说,每次调用函数是传人的标记(token)必须是完全相同的。改写后的单例模式:

+ (id)sharedInstance {
     static  WWClass *sharedInstance = nil;
     static dispatch_once_t onceToken;
     dispatch_once ( &onceToken, ^{
                sharedInstance = [[self alloc]  init];
      });
      return sharedInstance;
 }

使用dispatch_once 可以简化代码并且彻底保证线程安全。也更高效。

第十条:不要使用dispatch_get_current_queue。 文档中说此函数返回当前正在执行代码的队列。该函数有种典型的错误用法(antipattern,"反模式"),就是用它检测当前列队是不是某个特定的列队,试图以此方法来避免执行同步派发时可能遭遇的死锁问题。但有时候并不能解决死锁问题。使用这种API的开发者可能误以为:在回调块里调用dispatch_get_current_queue所返回的"当前队列",总是起调用API时指定的那个,但实际上返回的却是API内部的那个同步队列。解决这个问题最好的办法就是通过GCD所提供的功能来设定"队列特有数据"(queue_specific data),此功能可以吧任意数据以键值对的形式关联到队列里。最重要在于,假如根基指定的键获取不到关联数据,那么系统就会沿着体系向上查找,直到找到数据或达根列队为止。

dispatch_queue_t queueA =     dispatch_queue_create("com.effectiveobjectivec.quequeA", NULL);

dispatch_queue_t queueB = dispatch_queue_create("com.effectiveobjectivec.quequeB", NULL);
dispatch_set_target_queue(queueB, queueA);

static int kQueueSpecific;
CFStringRef queueSpecificValue = CFSTR("queueA");
dispatch_queue_set_specific(queueA, &kQueueSpecific, (void *)queueSpecificValue, (dispatch_function_t)CFRelease);

dispatch_sync(queueB, ^{
    dispatch_block_t block = ^{
        CFStringRef retrievedValue = dispatch_get_specific(&kQueueSpecific);
        if (retrievedValue) {
            block();
        } else {
            dispatch_sync(queueA, block);
        }
    };
});

这样就可以通过"队列特有数据"(queue_specific data)来获取某个队列,处理死锁问题。这个方法:

void dispatch_queue_set_specific(dispatch_queue_t queue, const void *key, void *context, dispatch_function_t destructor)

此函数的首个参数表示待设置的数据的队列,其后两个参数是键与值。键与值都是不透明的void指针。对于键来说,有一个问题要注意:函数是按指针值来比较键的,而不是按照其内容。故"队列特有数据"(queue_specific data)的行为与NSDictionary对象不同,后者是比较键的"对象等同性"。"队列特有数据"更像是"关联引用"。值(函数原型里叫做"context")也是不透明的void指针,于是可以在其中存放任意数据。函数的最后一个参数是"析构函数(destructor function)",对于给定的键来说,当队列所占的内存为系统所回收,或者有新的值与键相关联时,原有的值对象就会移除,而析构函数会于此时运行。dispatch_function_t 类型的定义如下:

typedef void (*dispatch_function_t ) (void *)

由此可知,析构函数只能带一个指针参数且返回值必须为void。

注意:dispatch_get_current_queue函数用于解决由不可重入的代码所引发的死锁,然而能用此函数解决的问题,也能用"队列特有数据"队列来解决,所以还是少用dispatch_get_current_queue。

最后,本书一共7个章节,此为第六章节:块与大中枢派发。

共勉!一步一个巴掌印。。。。。

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

推荐阅读更多精彩内容