IOS多线程开发其实很简单

概览

大家都知道,在开发过程中应该尽可能减少用户等待时间,让程序尽可能快的完成运算。可是无论是哪种语言开发的程序最终往往转换成汇编语言进而解释成机器码来执行。但是机器码是按顺序执行的,一个复杂的多步操作只能一步步按顺序逐个执行。改变这种状况可以从两个角度出发:对于单核处理器,可以将多个步骤放到不同的线程,这样一来用户完成UI操作后其他后续任务在其他线程中,当CPU空闲时会继续执行,而此时对于用户而言可以继续进行其他操作;对于多核处理器,如果用户在UI线程中完成某个操作之后,其他后续操作在别的线程中继续执行,用户同样可以继续进行其他UI操作,与此同时前一个操作的后续任务可以分散到多个空闲CPU中继续执行(当然具体调度顺序要根据程序设计而定),及解决了线程阻塞又提高了运行效率。苹果从iPad2 开始使用双核A5处理器(iPhone中从iPhone 4S开始使用),A7中还加入了协处理器,如何充分发挥这些处理器的性能确实值得思考。今天将重点分析iOS多线程开发:

多线程

简介

iOS多线程

NSThread

解决线程阻塞问题

多线程并发

线程状态

扩展-NSObject分类扩展

NSOperation

NSInvocationOperation

NSBlockOperation

线程执行顺序

GCD

串行队列

并发队列

其他任务执行方法

线程同步

NSLock同步锁

@synchronized代码块

扩展--使用GCD解决资源抢占问题

扩展--控制线程通信

总结

目录

多线程

简介

当用户播放音频、下载资源、进行图像处理时往往希望做这些事情的时候其他操作不会被中断或者希望这些操作过程中更加顺畅。在单线程中一个线程只能做一件事情,一件事情处理不完另一件事就不能开始,这样势必影响用户体验。早在单核处理器时期就有多线程,这个时候多线程更多的用于解决线程阻塞造成的用户等待(通常是操作完UI后用户不再干涉,其他线程在等待队列中,CPU一旦空闲就继续执行,不影响用户其他UI操作),其处理能力并没有明显的变化。如今无论是移动操作系统还是PC、服务器都是多核处理器,于是“并行运算”就更多的被提及。一件事情我们可以分成多个步骤,在没有顺序要求的情况下使用多线程既能解决线程阻塞又能充分利用多核处理器运行能力。

下图反映了一个包含8个操作的任务在一个有两核心的CPU中创建四个线程运行的情况。假设每个核心有两个线程,那么每个CPU中两个线程会交替执行,两个CPU之间的操作会并行运算。单就一个CPU而言两个线程可以解决线程阻塞造成的不流畅问题,其本身运行效率并没有提高,多CPU的并行运算才真正解决了运行效率问题,这也正是并发和并行的区别。当然,不管是多核还是单核开发人员不用过多的担心,因为任务具体分配给几个CPU运算是由系统调度的,开发人员不用过多关心系统有几个CPU。开发人员需要关心的是线程之间的依赖关系,因为有些操作必须在某个操作完成完才能执行,如果不能保证这个顺序势必会造成程序问题。

iOS多线程

在iOS中每个进程启动后都会建立一个主线程(UI线程),这个线程是其他线程的父线程。由于在iOS中除了主线程,其他子线程是独立于Cocoa Touch的,所以只有主线程可以更新UI界面(新版iOS中,使用其他线程更新UI可能也能成功,但是不推荐)。iOS中多线程使用并不复杂,关键是如何控制好各个线程的执行顺序、处理好资源竞争问题。常用的多线程开发有三种方式:

1.NSThread

2.NSOperation

3.GCD

三种方式是随着iOS的发展逐渐引入的,所以相比而言后者比前者更加简单易用,并且GCD也是目前苹果官方比较推荐的方式(它充分利用了多核处理器的运算性能)。做过.Net开发的朋友不难发现其实这三种开发方式 刚好对应.Net中的多线程、线程池和异步调用,因此在文章中也会对比讲解。

NSThread

NSThread是轻量级的多线程开发,使用起来也并不复杂,但是使用NSThread需要自己管理线程生命周期。可以使用对象方法+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(id)argument直接将操作添加到线程中并启动,也可以使用对象方法- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(id)argument创建一个线程对象,然后调用start方法启动线程。

解决线程阻塞问题

在资源下载过程中,由于网络原因有时候很难保证下载时间,如果不使用多线程可能用户完成一个下载操作需要长时间的等待,这个过程中无法进行其他操作。下面演示一个采用多线程下载图片的过程,在这个示例中点击按钮会启动一个线程去下载图片,下载完成后使用UIImageView将图片显示到界面中。可以看到用户点击完下载按钮后,不管图片是否下载完成都可以继续操作界面,不会造成阻塞。

//

//  NSThread实现多线程

//  MultiThread

//

//  Created by Kenshin Cui on 14-3-22.

//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.

//#import"KCMainViewController.h"@interfaceKCMainViewController (){

UIImageView *_imageView;

}

@end

@implementation KCMainViewController

- (void)viewDidLoad {

[super viewDidLoad];

[self layoutUI];

}#pragmamark 界面布局

-(void)layoutUI{

_imageView =[[UIImageView alloc]initWithFrame:[UIScreen mainScreen].applicationFrame];

_imageView.contentMode=UIViewContentModeScaleAspectFit;

[self.view addSubview:_imageView];

UIButton *button=[UIButton buttonWithType:UIButtonTypeRoundedRect];

button.frame=CGRectMake(50, 500, 220, 25);

[button setTitle:@"加载图片"forState:UIControlStateNormal];//添加方法[button addTarget:self action:@selector(loadImageWithMultiThread) forControlEvents:UIControlEventTouchUpInside];

[self.view addSubview:button];

}#pragmamark 将图片显示到界面

-(void)updateImage:(NSData *)imageData{

UIImage *image=[UIImage imageWithData:imageData];

_imageView.image=image;

}#pragmamark 请求图片数据

-(NSData *)requestData{//对于多线程操作建议把线程操作放到@autoreleasepool中@autoreleasepool {

NSURL *url=[NSURL URLWithString:@"http://images.apple.com/iphone-6/overview/images/biggest_right_large.png"];

NSData *data=[NSData dataWithContentsOfURL:url];returndata;

}

}#pragmamark 加载图片

-(void)loadImage{//请求数据NSData *data= [self requestData];/*将数据显示到UI控件,注意只能在主线程中更新UI,

另外performSelectorOnMainThread方法是NSObject的分类方法,每个NSObject对象都有此方法,

它调用的selector方法是当前调用控件的方法,例如使用UIImageView调用的时候selector就是UIImageView的方法

Object:代表调用方法的参数,不过只能传递一个参数(如果有多个参数请使用对象进行封装)

waitUntilDone:是否线程任务完成执行

*/[self performSelectorOnMainThread:@selector(updateImage:) withObject:data waitUntilDone:YES];

}#pragmamark 多线程下载图片

-(void)loadImageWithMultiThread{//方法1:使用对象方法

//创建一个线程,第一个参数是请求的操作,第二个参数是操作方法的参数

//    NSThread *thread=[[NSThread alloc]initWithTarget:self selector:@selector(loadImage) object:nil];

//    //启动一个线程,注意启动一个线程并非就一定立即执行,而是处于就绪状态,当系统调度时才真正执行

//    [thread start];

//方法2:使用类方法[NSThread detachNewThreadSelector:@selector(loadImage) toTarget:self withObject:nil];

}

@end

运行效果:

程序比较简单,但是需要注意执行步骤:当点击了“加载图片”按钮后启动一个新的线程,这个线程在演示中大概用了5s左右,在这5s内UI线程是不会阻塞的,用户可以进行其他操作,大约5s之后图片下载完成,此时调用UI线程将图片显示到界面中(这个过程瞬间完成)。另外前面也提到过,更新UI的时候使用UI线程,这里调用了NSObject的分类扩展方法,调用UI线程完成更新。

多个线程并发

上面这个演示并没有演示多个子线程操作之间的关系,现在不妨在界面中多加载几张图片,每个图片都来自远程请求。

大家应该注意到不管是使用+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(id)argument- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(id)argument方法还是使用- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait方法都只能传一个参数,由于更新图片需要传递UIImageView的索引和图片数据,因此这里不妨定义一个类保存图片索引和图片数据以供后面使用。

KCImageData.h

//

//  KCImageData.h

//  MultiThread

//

//  Created by Kenshin Cui on 14-3-22.

//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.

//#import@interfaceKCImageData : NSObject#pragmamark 索引

@property(nonatomic,assign)intindex;#pragmamark 图片数据

@property(nonatomic,strong) NSData *data;

@end

接下来将创建多个UIImageView并创建多个线程用于往UIImageView中填充图片。

KCMainViewController.m

//

//  NSThread实现多线程

//  MultiThread

//

//  Created by Kenshin Cui on 14-3-22.

//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.

//#import"KCMainViewController.h"#import"KCImageData.h"#defineROW_COUNT 5#defineCOLUMN_COUNT 3#defineROW_HEIGHT 100#defineROW_WIDTH ROW_HEIGHT#defineCELL_SPACING 10

@interfaceKCMainViewController (){

NSMutableArray *_imageViews;

}

@end

@implementation KCMainViewController

- (void)viewDidLoad {

[super viewDidLoad];

[self layoutUI];

}#pragmamark 界面布局

-(void)layoutUI{//创建多个图片控件用于显示图片_imageViews=[NSMutableArrayarray];for(intr=0; r

UIImageView *imageView=[[UIImageView alloc]initWithFrame:CGRectMake(c*ROW_WIDTH+(c*CELL_SPACING), r*ROW_HEIGHT+(r*CELL_SPACING                           ), ROW_WIDTH, ROW_HEIGHT)];

imageView.contentMode=UIViewContentModeScaleAspectFit;//            imageView.backgroundColor=[UIColor redColor];[self.view addSubview:imageView];

[_imageViews addObject:imageView];

}

}

UIButton *button=[UIButton buttonWithType:UIButtonTypeRoundedRect];

button.frame=CGRectMake(50, 500, 220, 25);

[button setTitle:@"加载图片"forState:UIControlStateNormal];//添加方法[button addTarget:self action:@selector(loadImageWithMultiThread) forControlEvents:UIControlEventTouchUpInside];

[self.view addSubview:button];

}#pragmamark 将图片显示到界面

-(void)updateImage:(KCImageData *)imageData{

UIImage *image=[UIImage imageWithData:imageData.data];

UIImageView *imageView= _imageViews[imageData.index];

imageView.image=image;

}#pragmamark 请求图片数据

-(NSData *)requestData:(int)index{//对于多线程操作建议把线程操作放到@autoreleasepool中@autoreleasepool {

NSURL *url=[NSURL URLWithString:@"http://images.apple.com/iphone-6/overview/images/biggest_right_large.png"];

NSData *data=[NSData dataWithContentsOfURL:url];returndata;

}

}#pragmamark 加载图片

-(void)loadImage:(NSNumber *)index{//    NSLog(@"%i",i);

//currentThread方法可以取得当前操作线程NSLog(@"current thread:%@",[NSThread currentThread]);inti=[index integerValue];//    NSLog(@"%i",i);//未必按顺序输出NSData *data= [self requestData:i];

KCImageData *imageData=[[KCImageData alloc]init];

imageData.index=i;

imageData.data=data;

[self performSelectorOnMainThread:@selector(updateImage:) withObject:imageData waitUntilDone:YES];

}#pragmamark 多线程下载图片

-(void)loadImageWithMultiThread{//创建多个线程用于填充图片for(inti=0; i

thread.name=[NSString stringWithFormat:@"myThread%i",i];//设置线程名称[thread start];

}

}

@end

通过NSThread的currentThread可以取得当前操作的线程,其中会记录线程名称name和编号number,需要注意主线程编号永远为1。多个线程虽然按顺序启动,但是实际执行未必按照顺序加载照片(loadImage:方法未必依次创建,可以通过在loadImage:中打印索引查看),因为线程启动后仅仅处于就绪状态,实际是否执行要由CPU根据当前状态调度。

从上面的运行效果大家不难发现,图片并未按顺序加载,原因有两个:第一,每个线程的实际执行顺序并不一定按顺序执行(虽然是按顺序启动);第二,每个线程执行时实际网络状况很可能不一致。当然网络问题无法改变,只能尽可能让网速更快,但是可以改变线程的优先级,让15个线程优先执行某个线程。线程优先级范围为0~1,值越大优先级越高,每个线程的优先级默认为0.5。修改图片下载方法如下,改变最后一张图片加载的优先级,这样可以提高它被优先加载的几率,但是它也未必就第一个加载。因为首先其他线程是先启动的,其次网络状况我们没办法修改:

-(void)loadImageWithMultiThread{

NSMutableArray *threads=[NSMutableArrayarray];intcount=ROW_COUNT*COLUMN_COUNT;//创建多个线程用于填充图片for(inti=0; i

thread.name=[NSString stringWithFormat:@"myThread%i",i];//设置线程名称if(i==(count-1)){

thread.threadPriority=1.0;

}else{

thread.threadPriority=0.0;

}

[threads addObject:thread];

}for(inti=0; i

NSThread *thread=threads[i];

[thread start];

}

}

线程状态

在线程操作过程中可以让某个线程休眠等待,优先执行其他线程操作,而且在这个过程中还可以修改某个线程的状态或者终止某个指定线程。为了解决上面优先加载最后一张图片的问题,不妨让其他线程先休眠一会等待最后一个线程执行。修改图片加载方法如下即可:

-(NSData *)requestData:(int)index{//对于多线程操作建议把线程操作放到@autoreleasepool中@autoreleasepool {//对非最后一张图片加载线程休眠2秒if(index!=(ROW_COUNT*COLUMN_COUNT-1)) {

[NSThread sleepForTimeInterval:2.0];

}

NSURL *url=[NSURL URLWithString:_imageNames[index]];

NSData *data=[NSData dataWithContentsOfURL:url];returndata;

}

}

在这里让其他线程休眠2秒,此时你就会看到最后一张图片总是第一个加载(除非网速特别差)。

线程状态分为isExecuting(正在执行)、isFinished(已经完成)、isCancellled(已经取消)三种。其中取消状态程序可以干预设置,只要调用线程的cancel方法即可。但是需要注意在主线程中仅仅能设置线程状态,并不能真正停止当前线程,如果要终止线程必须在线程中调用exist方法,这是一个静态方法,调用该方法可以退出当前线程。

假设在图片加载过程中点击停止按钮让没有完成的线程停止加载,可以改造程序如下:

//

//  NSThread实现多线程

//  MultiThread

//

//  Created by Kenshin Cui on 14-3-22.

//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.

//#import"KCMainViewController.h"#import"KCImageData.h"#defineROW_COUNT 5#defineCOLUMN_COUNT 3#defineROW_HEIGHT 100#defineROW_WIDTH ROW_HEIGHT#defineCELL_SPACING 10

@interfaceKCMainViewController (){

NSMutableArray *_imageViews;

NSMutableArray *_imageNames;

NSMutableArray *_threads;

}

@end

@implementation KCMainViewController

- (void)viewDidLoad {

[super viewDidLoad];

[self layoutUI];

}#pragmamark 界面布局

-(void)layoutUI{//创建多个图片空间用于显示图片_imageViews=[NSMutableArrayarray];for(intr=0; r

UIImageView *imageView=[[UIImageView alloc]initWithFrame:CGRectMake(c*ROW_WIDTH+(c*CELL_SPACING), r*ROW_HEIGHT+(r*CELL_SPACING                           ), ROW_WIDTH, ROW_HEIGHT)];

imageView.contentMode=UIViewContentModeScaleAspectFit;//            imageView.backgroundColor=[UIColor redColor];[self.view addSubview:imageView];

[_imageViews addObject:imageView];

}

}//加载按钮UIButton *buttonStart=[UIButton buttonWithType:UIButtonTypeRoundedRect];

buttonStart.frame=CGRectMake(50, 500, 100, 25);

[buttonStart setTitle:@"加载图片"forState:UIControlStateNormal];

[buttonStart addTarget:self action:@selector(loadImageWithMultiThread) forControlEvents:UIControlEventTouchUpInside];

[self.view addSubview:buttonStart];//停止按钮UIButton *buttonStop=[UIButton buttonWithType:UIButtonTypeRoundedRect];

buttonStop.frame=CGRectMake(160, 500, 100, 25);

[buttonStop setTitle:@"停止加载"forState:UIControlStateNormal];

[buttonStop addTarget:self action:@selector(stopLoadImage) forControlEvents:UIControlEventTouchUpInside];

[self.view addSubview:buttonStop];//创建图片链接_imageNames=[NSMutableArrayarray];

[_imageNames addObject:@for(inti=0; i

[_imageNames addObject:[NSString stringWithFormat:@"http://images.cnblogs.com/cnblogs_com/kenshincui/613474/o_%i.jpg",i]];

}}#pragmamark 将图片显示到界面

-(void)updateImage:(KCImageData *)imageData{

UIImage *image=[UIImage imageWithData:imageData.data];

UIImageView *imageView= _imageViews[imageData.index];

imageView.image=image;

}#pragmamark 请求图片数据

-(NSData *)requestData:(int)index{//对于多线程操作建议把线程操作放到@autoreleasepool中@autoreleasepool {

NSURL *url=[NSURL URLWithString:_imageNames[index]];

NSData *data=[NSData dataWithContentsOfURL:url];returndata;

}

}#pragmamark 加载图片

-(void)loadImage:(NSNumber *)index{inti=[index integerValue];

NSData *data= [self requestData:i];

NSThread *currentThread=[NSThread currentThread];//    如果当前线程处于取消状态,则退出当前线程if(currentThread.isCancelled) {

NSLog(@"thread(%@) will be cancelled!",currentThread);

[NSThread exit];//取消当前线程}

KCImageData *imageData=[[KCImageData alloc]init];

imageData.index=i;

imageData.data=data;

[self performSelectorOnMainThread:@selector(updateImage:) withObject:imageData waitUntilDone:YES];

}#pragmamark 多线程下载图片

-(void)loadImageWithMultiThread{intcount=ROW_COUNT*COLUMN_COUNT;

_threads=[NSMutableArray arrayWithCapacity:count];//创建多个线程用于填充图片for(inti=0; i

NSThread *thread=[[NSThread alloc]initWithTarget:self selector:@selector(loadImage:) object:[NSNumber numberWithInt:i]];

thread.name=[NSString stringWithFormat:@"myThread%i",i];//设置线程名称[_threads addObject:thread];

}//循环启动线程for(inti=0; i

NSThread *thread= _threads[i];

[thread start];

}

}#pragmamark 停止加载图片

-(void)stopLoadImage{for(inti=0; i

NSThread *thread= _threads[i];//判断线程是否完成,如果没有完成则设置为取消状态

//注意设置为取消状态仅仅是改变了线程状态而言,并不能终止线程if(!thread.isFinished) {

[thread cancel];

}

}

}

@end

运行效果(点击加载大概1秒后点击停止加载):

使用NSThread在进行多线程开发过程中操作比较简单,但是要控制线程执行顺序并不容易(前面万不得已采用了休眠的方法),另外在这个过程中如果打印线程会发现循环几次就创建了几个线程,这在实际开发过程中是不得不考虑的问题,因为每个线程的创建也是相当占用系统开销的。

扩展--NSObject分类扩展方法

为了简化多线程开发过程,苹果官方对NSObject进行分类扩展(本质还是创建NSThread),对于简单的多线程操作可以直接使用这些扩展方法。

- (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg:在后台执行一个操作,本质就是重新创建一个线程执行当前方法。

- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait:在指定的线程上执行一个方法,需要用户创建一个线程对象。

- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait:在主线程上执行一个方法(前面已经使用过)。

例如前面加载图多个图片的方法,可以改为后台线程执行:

-(void)loadImageWithMultiThread{intcount=ROW_COUNT*COLUMN_COUNT;for(inti=0; i

[self performSelectorInBackground:@selector(loadImage:) withObject:[NSNumber numberWithInt:i]];

}

}

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

推荐阅读更多精彩内容