第六章 block与GCD(上)

第六章 block与GCD

“块”(block)是一种可在C、C++及Objective-C代码中使用的“词法闭包”(lexical closure),它极为有用,这主要是因为借由此机制,开发者可将代码像对象一样传递,令其在不同环境下运行。还有个关键的地方是,在定义“块”的范围内,它可以访问到其中的全部变量。
GCD是一种与“块”有关的技术,它提供了对线程的抽象,而这种抽象则基于“派发队列”(dispatch queue)。开发者可将块排入队列中,由GCD负责处理所有调度事宜。

37.理解“块”这一概念

块可以实现闭包。

1.块的基础知识

块与函数类似,只不过是直接定义在另一个函数里的,和定义它的那个函数共享同一个范围内的东西。块用“^”符号来表示,后面跟着一对花括号,括号里面是块的实现代码。例如:

^{
    //Block implementation here
}

块其实就是个值,而且自有其相关类型。与int、float或Objective-C对象一样,也可以把块赋给变量,然后像使用其他变量那样使用它。块类型的语法与函数指针近似。下面列出的这个块很简单,没有参数,也不返回值:

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

这段代码定义了一个名为someBlock的变量。由于变量名写在正中间,所以看上去也许有点怪,不过一旦理解了语法,很容易就能读懂。

块类型的语法结构如下:

return_type (^block_name)(parameters)

下面这种写法所定义的块,返回int值,并且接受两个int做参数:

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

定义好之后,就可以像函数那样使用了。比方说,addBlock块可以这样用:

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

块的强大之处是:在声明它的范围里,所有变量都可以为其所捕获。这也就是说,那个范围里的全部变量,在块里依然可用。比如,下面这段代码所定义的块,就使用了块以外的变量:

int addtional = 5;
int(^addBlock)(int a,int b) = ^(int a,int b){
    return a + b + addtional;
};
int add =  addBlock(2,5);///< add = 12

默认情况下,为块所捕获的变量,是不可以在块里修改的。在本例中,假如块内的代码改动了additional变量的值,那么编译器就会报错。不过,声明变量的时候可以加上__block修饰符,这样就可以在块内修改了。例如,可以用下面这个块来枚举数组中的元素(参见第48条),以判断其中有多少个小于2的数:

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

这段范例代码也演示了”内联块“(inline block)的用法。传给”enumerateObjectsUsingBlock:”方法的块并未先赋给局部变量,而是直接内联在函数调用里了。由这种常见的编码习惯也可以看出块为何如此有用。在Objective-C语言引入块的这一特性之前,想要编出与刚才那段代码相同的功能,就必须传入函数指针或选择子的名称,以供枚举方法调用。状态必须手工传入传出,这一版通过“不透明的void指针“实现,如此一来,就得再写几行代码了,而且还会令方法变得有些松散。与之相反,若声明内联形式的块,则可把所有业务逻辑都放在一处。

如果块所捕获的变量是对象类型,那么就会自动保留它。系统在释放这个块的时候,也会将其一并释放。这就引出了一个与块有关的重要问题。块本身可视为对象。实际上,在其他Objective-C对象所能响应的选择子中,有很多是块也可以响应的。而最重要之处则在于,块本身也和其他对象一样,有引用计数。当最后一个指向块的引用移走之后,块就回收了。回收时也会释放块所捕获的变量,以便平衡捕获时所执行的保留操作。

如果将块定义在Objective-C类的实例方法中,那么除了可以访问类的所有实例变量之外,还可以使用self变量。块总能修改实例变量,所以在声明时无须加block。不过,如果通过读取或写入操作捕获了实例变量,那么也会自动把self变量一并捕获了,因为实例变量是与self所指代的实例关联在一起的。例如,下面这个块声明在EOCClass类的方法中:

#import "EOCClass.h"

@implementation EOCClass
-(void)anInstanceMethod{
    void (^someBlock)() = ^{
        _anInstanceVariable = @"Something";
        NSLog(@"_anInstanceVariable = %@",_anInstanceVariable);
    };
}
@end

如果某个EOCClass实例正在执行anInstanceMethod方法,那么self变量就指向此实例。由于块里没有明确使用self变量,所以很容易就会忘记self变量其实也是为块所捕获了。直接访问实例变量和通过self来访问时等效的:

self-> _anInstanceVariable = @"Something";

之所以要捕获self变量,原因正在于此。我们经常通过属性访问实例变量,在这种情况下,就要指明self了:

self.aProperty = @“Something”;

然而,一定要记住:self也是个对象,因而块在捕获它时也会将其保留。如果self所指代的那个对象同时也保留了块,那么这种情况通常就会导致”循环引用“。

2.块的内部结构

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

块对象的内存布局

在内存布局中,最重要的就是invoke变量,这是个函数指针,指向块的实现代码。函数原型至少要接受一个void*型的参数,此参数代表块。刚才说过,块其实就是一种代替函数指针的语法结构,原来使用函数指针时,需要用”不透明的void指针“来传递状态。而改用块之后,则可以把原来用标准C语言特性所编写的代码封装成简明且易用的接口。

descriptor变量是指向结构体的指针,每个块里都包含此结构体,其中声明了块对象的总体大小,还声明了copy与dispose这两个辅助函数所对应的函数指针。辅助函数在拷贝及丢弃块对象时运行,其中会执行一些操作,比方说,前者要保留捕获的对象,而后者则将之释放。

块还会把它所捕获的所有变量都拷贝一份。这些拷贝放在descriptor变量后面,捕获了多少个变量,就要占据多少内存空间。请注意,拷贝的并不是对象本身,而是指向这些对象的指针变量。invoke函数需要把块对象作为参数传进来是因为在执行块时,要从内存中把这些捕获到的变量读出来。

3.全局块、栈块及堆块

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

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

定义在if及else语句中的两个块都分配在栈内存中。编译器会给每个块分配好栈内存,然而等离开了相应的范围之后,编译器有可能把分配给块的内存覆写掉。于是,这两个块只能保证在对应的if或else语句范围内有效。这样写出来的代码可以编译,但是运行起来时而正确,时而错误。若编译器未覆写待执行的块,则程序照常运行,若覆写,则程序崩溃。

为解决此问题,可给块对象发送copy消息以拷贝之。这样的话,就可以把块从栈复制到堆了。拷贝后的块,可以在定义它的那个范围之外使用。而且,一旦复制到堆中,块就成了带引用计数的对象了。后续的复制操作都不会真的执行复制,只是递增块对象的引用计数。如果不再使用这个块,那就应该将其释放,在ARC下会自动释放,而手动管理引用计数时则需要自己来调用release方法。当引用计数降为0时,”分配在堆上的块“会像其他对象一样,为系统所回收。而”分配在栈上的块“则无须明确释放,因为栈内存本来就会自动回收。

我们只需给代码加上两个copy方法调用,就可令其变得安全了:

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

现在代码就安全了。如果手动管理引用计数,那么在用完块之后还需将其释放。

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

void (^block)() = ^{
    NSLog(@"This is a block");
};

由于运行该块所需的部信息都能在编译期确定,所以可把它做成全局块。这完全是种优化技术:若把如此简单的块当成复杂的块来处理,那就会在复制及丢弃该块时执行一些无谓的操作。

要点:

  • 块是C、C++、Objective-C中的词法闭包。
  • 块可接受参数,也可返回值。
  • 块可以分配在栈或堆上,也可以是全局的。分配在栈上的块可拷贝到堆里,这样的话,就和标准的Objective-C对象一样,具备引用计数了。

38.为常用的块类型创建typedef

每个块都具备其”固有类型“,因而可将其赋值给适当类型的变量。这个类型由块所接受的参数及其返回值组成。例如有如下这个块:

^(BOOL flag,int value){
    if(flag){
        return value * 5;
    }else{
        return value * 10;
    }
}

此块接受的两个类型分别为BOOL类型及int的参数,并返回类型为int的值。如果想把它赋值给变量,则需注意其类型。变量类型及相关赋值语句如下:

int (^variableName)(BOOL flag,int value) =
    ^(BOOL flag,int value){
        //Implementation
        return someInt;
    }

这个类型似乎和普通的类型大不相同,然而如果习惯函数指针的话,那么看上去就会觉得眼熟了。块类型的语法结构如下:

return_type (^block_name)(parameters)

与其他类型的变量不同,在定义块变量时,要把变量名放在类型之中,而不要放在右侧。这种语法非常难记,也非常难读。鉴于此,我们应该为常用的块类型起个别名,尤其是打算把代码发不成API供他人使用时,更应这样做。开发者可以起个更为易读的名字来表示块的用途,而把块的类型隐藏在其后面。

为了隐藏复杂的块类型,需要用到C语言中名为“类型定义”的特性。typedef关键字用于给类型起个易读的别名。比方说,想定义新类型,用以表示接受BOOL及int参数并返回int值的块,可通过下列语句来做:

typedef int(^EOCSomeBlock)(BOOL flag,int value);

声明变量时,要把名称放在类型中间,并在前面加上“^”符号,而定义新类型时也得这么做。上面这条语句向系统中新增了一个名为EOCSomeBlock的类型。此后,不用再以复杂的块类型来创建变量了,直接使用新类型即可:

EOCSomeBlock block = ^(BOOL flag,int value){
    //Implementation
};

这次代码读起来就顺畅多了:与定义其他变量时一样,变量类型在左边,变量名在右边。

通过这项特性,可以把使用块的API做得更为易用些。类里面有些方法可能需要用块来做参数,比如执行异步任务时所用的“completion handler”参数就是块,凡遇到这种情况,都可以通过定义别名使代码变得更为易读。比方说,类里有个方法可以启动任务,它接受一个块作为处理程序,在完成任务之后执行这个块。若不定义别名,则方法签名会像下面这样:

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

注意,定义方法参数所用的块类型语法,又和定义变量时不同。若能把方法签名中的参数类型写成一个词,那读起来就顺口多了。于是,可以给参数类型起个别名,然后使用此名称来定义:

typedef void(^EOCCompletionHandler)(NSData *data,NSError *error);

-(void)startWithCompletionHandler:(EOCCompletionHandler)completion;

现在参数看上去就简单多了,而且易于理解。

使用类型定义还有个好处,就是当你打算重构块的类型签名时会很方便。比方说,要给原来的completion handler块再加一个参数,用以表示完成任务所花的时间,那么只需修改类型定义语句即可:

typedef void(^EOCCompletionHandler)(NSData *data,NSTimeInterval duration,NSError *error);

修改之后,凡是使用了这个类型定义的地方,比如方法签名等处,都会无法编译,而且报的是同一种错误,于是开发者可据此逐个修复。若不用类型定义,而直接写块类型,那么代码中要修改的地方就更多了。开发者很容易忘掉其中一两处,从而引发难于排查的bug。

最好在使用块类型的类中定义这些typedef,而且还应该把这个类的名字加在由typedef所定义的新类型名前面,这样可以阐明块的用途。还可以用typedef给同一个块签名类型创建数个别名。在这件事上,多多益善。

Mac OS X与iOS的Accounts框架就是个例子。在该框架中可以找到下面这两个类型定义语句:

typedef void(^ACAccountStoreSaveCompletionHandler)(BOOL success, NSError *error);
typedef void(^ACAccountStoreRequestAccessCompletionHandler)(BOOL granted, NSError *error);

这两个类型定义的签名相同,但用在不同的地方。开发者看到类型别名及签名中的参数之后,很容易就能理解此类型的用途。它们本来也可以合并成一个typedef,比如叫做ACAccountStorBooleanCompletionHandler,使用那两个别名的地方,都可以统一使用此名称。然后,这么做之后,块与参数的用途看上去就不那么明显了。

与此相似,如果有好几个类都要执行相似但各有区别的异步任务,而这几个类又不能放入同一个继承体系,那么,每个类就应该有自己的completion handler类型。这几个completion handler的前面也许完全相同,但最好还是在每个类里都各自定义一个别名,而不要共用同一个名称。反之,若这些类能纳入同一个继承中,则应该将类型定义语句放在超类中,以供各子类使用。

要点:

  • 以typedef重新定义块类型,可令块变量用起来更加简单。
  • 定义新类型时应遵从现有的命名规范,勿使其名称与别的类型相冲突。
  • 不妨为同一个块签名定义多个类型别名。如果要重构的代码使用了块类型的某个别名,那么只需修改相应的typedef中的块签名即可,无须改动其他typedef。

39.用handler块降低代码分散程度

与使用委托模式的代码相比,用块写出来的代码显得更为简洁。异步任务执行完毕后所需运行的业务逻辑,和启动异步任务所用的代码放在了一起。而且,由于块声明在创建获取器的范围内,所以它可以访问此范围内的全部变量。

委托模式有个缺点:如果类要分别使用多个获取器下载不同数据,那么就得在delegate回调方法里根据传入的获取器参数来切换。

把成功情况和失败情况放在同一个块中,缺点是:由于全部逻辑都写在一起,所以会令块变得比较长,切比较复杂。然而只用一个块的写法也有好处,那就是更为灵活。比方说,在传入错误信息时,可以把数据也传进来。有时数据正下载到一半,突然网络故障了。在这种情况下,可以把数据及相关的错误都传给块。这样的话,completion handler就能根据此判断问题并适当处理了,而且还可利用已下载好的这部分数据做些事情。还有个优点是:调用API的代码可能会在处理成功响应的过程中发现错误。这种情况需要和网络数据获取器所认定的失败情况按同一方式处理。此时,如果采用单一块的写法,那么就能把这种情况和获取器认定的失败情况统一处理了。要是把成功情况和失败情况交给两个不同的处理程序来负责,那么就没办法共享同一份错误处理代码了,除非把这段代码单独放在一个方法里,而这又违背了我们想把全部逻辑代码都放在一处的初衷。

建议使用同一个块来处理成功与失败情况。

基于handler来设计API还有个原因,就是某些代码必须运行在特定的线程上。比方说,Cocoa与Cocoa Touch中的UI操作必须在主线程上执行。这就相当于GCD中的“主队列”。因此,最好能由调用API的人来决定handler应该运行在哪个线程上。NSNotificationCenter就属于这种API,它提供了一个方法,调用者可以经由此方法来注册想要接收的通知,等到相关事件发生时,通知中心就会执行注册好的那个块。调用者可以指定某个块应该安排在哪个执行队列里,然而这不是必需的。若没有指定队列,则按默认方式执行,也就是说,将由投递通知的那个线程来执行。下列方法可用来新增观察者:

- (id <NSObject>)addObserverForName:(nullable NSString *)name 
                             object:(nullable id)obj 
                             queue:(nullable NSOperationQueue *)queue 
                        usingBlock:(void (^)(NSNotification *note))block

此处传入的NSOperationQueue参数就表示触发通知时用来执行块代码的那个队列。这是个“操作队列”,而非“底层GCD队列”,不过两者语义相同。

要点:

  • 要创建对象时,可以使用内联的handler块将相关业务逻辑一并声明。
  • 在有多个实例需要监控时,如果采用委托模式,那么经常需要根据传入的对象来切换,而若改用handler块来实现,则可直接将块与相关对象放在一起。
  • 设计API时如果用到了handler块,那么可以增加一个参数,使调用者可通过此参数来决定应该把块安排在哪个队列上执行。

40.用Block引用其所属对象时不要出现循环引用

使用块时,若不仔细思量,则很容易导致循环引用。比方说,下面这个类就提供了一套接口,调用者可由此从某个URL中下载数据。在启动获取器时,可设置completion handler,这个块会在下载结束之后以回调方式执行。为了能在p_requestCompleted方法执行调用者所指定的块,这段代码需要把completion handler保存到实例变量里面。

//EOCNetworkFetcher.h
typedef void(^EOCNetworkFetcherCompletionHandler)(NSData *data);

@interface EOCNetworkFetcher : NSObject
@property(nonatomic,strong,readonly)NSURL *url;

-(instancetype)initWithURL:(NSURL *)url;
-(void)startWithCompletionHandler:
(EOCNetworkFetcherCompletionHandler)completion;

@end


//EOCNetworkFetcher.m

@interface EOCNetworkFetcher ()
@property(nonatomic,strong,readwrite)NSURL *url;
@property(nonatomic,copy)EOCNetworkFetcherCompletionHandler completionHandler;
@property(nonatomic,strong)NSData *downloadedData;

@end


@implementation EOCNetworkFetcher

-(instancetype)initWithURL:(NSURL *)url{
    if(self = [super init]){
        _url = url;
    }
    return self;
}

-(void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion{
    self.completionHandler = completion;
    //Start the request
    //Request sets downloadedData property
    //When request is finished,p_requestCompleted is called
}

-(void)p_requestCompleted{
    if(_completionHandler){
        _completionHandler(_downloadedData);
    }
}
@end

某个类可能会创建这种网络数据获取器对象,并用其从URL中下载数据:

#import "EOCClass.h"
#import "EOCNetworkFetcher.h"

@interface EOCClass ()
{
    EOCNetworkFetcher *_networkFetcher;
    NSData *_fetchedData;
}
@end

@implementation EOCClass

-(void)downloadData{
    NSURL *url = [[NSURL alloc]initWithString:@"http://www.example.com/something.dat"];
    _networkFetcher = [[EOCNetworkFetcher alloc]initWithURL:url];
    [_networkFetcher startWithCompletionHandler:^(NSData *data) {
        NSLog(@"Request URL %@ finished",_networkFetcher.url);
        _fetchedData = data;
    }];
    
}
@end

这里就造成了一个循环引用。因为completion handler块要设置_fetchedData实例变量,所以它必须捕获self变量(第37条)。这就是说,handler块保留了创建网络数据获取器的那个EOCClass实例。而EOCClass实例则通过strong实例变量保留了获取器,最后,获取器对象又保留了handler块。下图描述了这个环:

用块引用其所属对象时出现的循环引用

要打破循环引用也很容易:要么令_networkFetcher实例变量不要引用获取器,要么令获取器的completionHandler属性不再持有handler块。在网络数据获取器这个例子中,应该等completion handler块执行完毕后,再去打破引用环,以便使获取器对象在handler块执行期间保持存活状态。比方说,completion handler块的代码可以这么修改:

[_networkFetcher startWithCompletionHandler:^(NSData *data) {
        NSLog(@"Request URL %@ finished",_networkFetcher.url);
        _fetchedData = data;
        _networkFetcher = nil;
    }];

如果设计API时用到了completion handler这样的回调块,那么很容易形成循环引用,所以必须意识到这个重要问题。一般来说,只要适时清理掉环中的某个引用,即可解决此问题,然而,未必总有这种机会。在本例中,唯有completion handler运行过后,方能解除引用环。若是completion handler一直不运行,那么引用环就无法打破,于是内存就会泄露。

像completion handler块这种写法,还可能引入另外一种形式的引用环。如果completion handler块所引用的对象最终又引用了这个块本身,那么就会出现引用环。比方说,我们修改下这个例子,使调用API的那段代码无须在执行期间保留指向网络数据获取器的引用,而是设定一套机制,令获取器对象自己设法保持存活。要想保持存活,获取器对象可以在启动任务时把自己加到全局的collection中(比如用set来实现这个collection),待任务完成后,再移除。而调用方则需将其代码修改如下:

-(void)downloadData{
    NSURL *url = [[NSURL alloc]initWithString:@"http://www.example.com/something.dat"];
    EOCNetworkFetcher *networkFetcher = [[EOCNetworkFetcher alloc]initWithURL:url];
    [networkFetcher startWithCompletionHandler:^(NSData *data) {
        NSLog(@"Request URL %@ finished",_networkFetcher.url);
        _fetchedData = data;
    }];
}

大部分网络通信库都采用这种方法,因为假如令调用者自己来将获取器对象保持存活的话,他们会觉得麻烦。Twitter框架的TWRequest对象也用的这个办法。然后,本例这样做会引入引用环。completion handler块其实要通过获取器对象来引用其中的URL(引用了EOCNetworkFetcher的url属性)。于是,块就要保留获取器,而获取器反过来又经由其completion handler属性保留了这个块。所幸要修复这个问题也不难。回想一下,获取器对象之所以要把completion handler块保存在属性里面,其唯一目的就是想稍后使用这个块。于是,获取器一旦运行过completion handler之后,就没必要再保留它了。所以,只需将p_requestCompleted方法按照如下方式修改即可:

-(void)p_requestCompleted{
    if(_completionHandler){
        _completionHandler(_downloadedData);
    }
    self.completionHandler = nil;
}

这样一来,只要下载请求执行完毕,引用环就解除了,而获取器对象也将会在必要时为系统所回收。请注意,之所以要在start方法中把completion handler作为参数传进去,这也是一条重要原因。假如把completion handler暴露为获取器对象的公共属性,那么就不便在执行完下载请求之后直接将其清理掉了。因为既然已经把handler作为属性公布了,那就意味着调用者可以自由使用它,若是此时又在内部将其清理掉的话,则会破坏“封装语义”。在这种情况下要想打破引用环,只有一个办法可用,那就是强迫调用者在handler代码里自己把completionHandler属性清理干净。可这并不是十分合理,因为你无法假定调用者一定会这么做。

这两种引用环都很容易发生。使用块来编程时,一不小心就会出现这种bug,反过来说,只要小心谨慎,这种问题也很容易解决。关键在于,要想清楚块可能会捕获并保留哪些对象。如果这些对象又直接或间接保留了块,那么就要考虑怎样在适当的时机解除引用环。

要点:

  • 如果块所捕获的对象直接或间接地保留了块本身,那么就得当心循环引用问题。
  • 一定要找个适当的时机解除循环引用,而不能把责任推给API的调用者。

转载请注明出处:第六章 block与GCD(上)

参考:《Effective Objective-C 2.0》

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,418评论 25 707
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,567评论 18 399
  • 凡是涉及到小猫小狗小动物题材的电影,如果不是让人发笑的喜剧,便一定是能把人虐哭的正剧。 《一条狗的使命》便是这样一...
    我的杨桐去哪儿啦阅读 188评论 0 0
  • 纳尼?30分钟看完一本书?有没有搞错? 如果是以前,我也不信。看书这么神圣的事情,不是应该打扫完房间,沏一壶茶,换...
    潇洒君阅读 396评论 0 0
  • 染发膏
    Yule0411阅读 115评论 0 1