我理解的RunLoop

本人小白,欢迎各位大佬补充指点
先说说我是咋理解runloop的吧?然后在专业的讲讲

本人理解:

  1. 从C语言的角度来看,整个app就是调用了一个main函数,只是这个main函数一直不返回值.
  2. main函数内部:
    常识:执行任务需要开启一个线程,因为任务必须在线程下执行
    一般的main函数执行流程:开启一个线程->添加许多任务(即写了很多代码)->任务完成(代码执行完毕)->线程关闭->返回main函数值程序结束
  3. runloop啥作用?
    Runloop就是一个死循环,把这个死循环放在上面开启的线程中,那么线程就永远不会结束.因此main永远不返回.但是这个死循环比较牛逼,下面讲讲他的牛逼之处:
    一般死循环特点:
    1>程序一旦进入死循环,一直循环执行,永不停歇,并且执行速度非常快
    2>只执行这段代码,其他任何代码都不会执行,也就是说,只专注在这跑圈,不管外界发生任何事
    Runloop特点:
    程序一旦进入runloop,当外界没事的时候,他就慢慢跑圈,(至于多慢我就不知道了),甚至休眠,一旦有事,他就快速跑到该事件,去执行该事件,执行完毕后再接着休眠

下面专业系统的讲一下:

RunLoop的传说

  1. 什么是runloop?
    字面理解:运行循环/跑圈
  2. 基本作用
    1>保持程序的持续运行
    打开app它一直不死,就是因为runloop
    2>处理App中的各种事件(比如触摸事件、定时器事件、Selector事件)
    3>节省CPU资源,提高程序性能:该做事时做事,该休息时休息
    能让我们的主线程,有事情就做事情,没事情就休息,比如点击事件:点击前主线程在休息,当点击事件处理完毕后,让主线程再次休息…
  3. 如果没有runloop
int main(int argc, char * argv[]) {
    NSLog(@"execute main function");
    return 0;
}

第3行后程序就结束了
如果有了runloop大致运行代码逻辑如下:

123.png

由于main函数里面( UIApplicationMain())启动了个RunLoop,所以程序并不会马上退出,保持持续运行状态
main函数中的RunLoop

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

代码的UIApplicationMain函数内部就启动了一个RunLoop
所以UIApplicationMain函数一直没有返回,保持了程序的持续运行
这个默认启动的RunLoop是跟主线程相关联的


既然runloop那么牛逼,那么我们就来认识他一下:

RunLoop对象

iOS中有2套API来访问和使用RunLoop:
Foundation(OC):NSRunLoop
Core Foundation(C):CFRunLoopRef
该框架有个特点:所有的类都已CF开头,以Ref(reference:引用)结尾
NSRunLoop和CFRunLoopRef都代表着RunLoop对象
NSRunLoop是基于CFRunLoopRef的一层OC包装,所以要了解RunLoop内部结构,需要多研究CFRunLoopRef层面的API(Core Foundation层面)

RunLoop与线程关系

上面说到,runloop通常是用来卡住线程的,那么他们必然有关系啦

  • 每条线程都有唯一的一个与之对应的RunLoop对象
    主线程的RunLoop已经自动创建好了,并且主动启动了,子线程的RunLoop需要主动创建,然后手动启动
  • RunLoop在第一次获取时创建,在线程结束时销毁

获得RunLoop对象

  • Foundation
    [NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
    [NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象
  • Core Foundation
    CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
    CFRunLoopGetMain(); // 获得主线程的RunLoop对象

上面讲了3条理论,下面代码证明一下:

- (void)viewDidLoad {
    [super viewDidLoad];
    //当前线程就是主线程
    NSLog(@"%p--%p",[NSRunLoop mainRunLoop],[NSRunLoop currentRunLoop]);
    //创建一个线程
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
    [thread start]; 
}
-(void)run{
    /*
     [NSRunLoop currentRunLoop]:这就是在子线程中创建了一个runloop,不是通过alloc/init,而且该创建是懒加载的
     通过分析runloop(C语言)源码过程如下:
     创建runloop函数只有一个参数t:线程(这也说明一个runloop只有一个线程)
     _CFRunLoopGet0(pthread_t t) {
     1.如果t==kNilPthreadT(0的意思),那么t就等于主线程
     __CFRunLoops:本身是一个可变字典,用于存储所有的runloop,key值为线程,value为runloop
     __CFRunLoops :{
     mainthread : mainloop,
     thread1:loop1,
     ...
     }
     2.如果发现__CFRunLoops为空值
       2.1先创一个可变字典(就是__CFRunLoops)
       2.2根据主线程创建一个主runloop,即mainloop
       2.3将mainloop存入可变字典,key值为主线程
     3.根据传进来的线程到字典中拿到相应的runloop
     4.没有创建一个newloop->将newloop存入可变字典->返回这个newloop
     5.有就直接返回loop
     }
     */
    NSLog(@"%p",[NSRunLoop currentRunLoop]);
}

RunLoop的相关类

学到对象,那么当然就有学到一些该对象相关的类了
Core Foundation中关于RunLoop的5个类
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
这几个类的关系图如下:

123.png

分析:
RunLoop就像一个汽车,要跑起来,那么就要装一些必备的东西

RunLoop里面有很多Mode对象->每个Mode对象里面又有三种集合分别存放:Source/Observer/Timer对象->有了这些Mode以及子内容runloop才能一直跑圈/休息/有事件触发
注意:runloop中的Mode中必须有source或者timer才能一直运行,否则跑一圈就停了

下面分别讲一讲这几个类:

CFRunLoopModeRef

  • CFRunLoopModeRef代表RunLoop的运行模式
  • 一个 RunLoop 包含若干个 Mode,每个Mode又包含若干个Source/Timer/Observer
  • 每次RunLoop启动时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode
    [NSRunLoop currentRunLoop] runMode: beforeDate:
  • 如果需要切换Mode,只能退出Loop,再重新指定一个Mode进入
    这样做主要是为了分隔开不同组的Source/Timer/Observer,让其互不影响
  • 系统默认注册了5个Mode:(前两种最重要)
    kCFRunLoopDefaultMode: App的默认Mode,通常主线程是在这个Mode下运行
    UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
    分析:当我们滚动图片时就会切换到这个模式,跟踪你的滚动,一旦停止滚动就会切换到Default模式
    举例:当我们滚动的时候,其他事件没反应,比如定时器,当滚动的时候定时器没反应,因为它在Default模式中
    UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用
    GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
    kCFRunLoopCommonModes: 这是一个占位用的Mode,不是一种真正的Mode

CFRunLoopTimerRef

  • CFRunLoopTimerRef是基于时间的触发器,基本上说的就是NSTimer
  • 它受runloop的Mode影响
  • GCD的定时器不受runLoop的Mode影响
    GCD定时器实现不一样
    代码举例:
- (void)viewDidLoad {
    [super viewDidLoad];
    //向sb中拖入一个textView
//情况一:没有滚动textView时,1秒打印一次,一旦滚动textView,定时器无用了,停止滚动,定时器有起作用了
//    [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(test) userInfo:nil repeats:YES];
    //上面一段代码做了很多事情,等价代码如下:
    //timer添加到defaultMode里面去,mode再添加到runloop中去,runloop在启动时候再指定这个mode->拿出timer来用
    NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(test) userInfo:nil repeats:YES];
    //也就说只能在Defaultmode下好使
//    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    
    //情况二:如果想要一滚动timer就有用,一停止滚动timer就失效,那么模式就用UITrackingRunLoopMode
//     [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
    //情况三:滚动不影响定时器
    //定时器只会跑在标记为CommonModes的模式下(或者用GCD定时器)
      [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    //通过打印runloop,我们发现kCFRunLoopDefaultMode/UITrackingRunLoopMode这两种模式都被标记为了commonmodes标签了,因此就可以做到了
    NSLog(@"%@",[NSRunLoop currentRunLoop]);  
}
- (void)test {
    NSLog(@"----");
}

CFRunLoopSourceRef

  • 系统定的我们用不着
  • CFRunLoopSourceRef是事件源(输入源)
  • 我们的一些触发事件,都是由source触发的
    1>以前的分法
    Port-Based Sources
    Custom Input Sources
    Cocoa Perform Selector Sources(处理performselector调用)
    2>根据函数调用栈分法
    Source0:非基于Port
    Source1:基于Port的,通过内核和其他线程通信,接收/分发系统事件

CFRunLoopObserverRef

  • CFRunLoopObserverRef是观察者,能够监听RunLoop的状态改变
    可以监听的时间点有以下几个:
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),//即将进入loop,左移动0->2的0次放= 1
    kCFRunLoopBeforeTimers = (1UL << 1),//即将处理Timer …2
    kCFRunLoopBeforeSources = (1UL << 2),//即将处理source...4
    kCFRunLoopBeforeWaiting = (1UL << 5),//即将进入休眠 …32
    kCFRunLoopAfterWaiting = (1UL << 6),//刚刚从休眠中唤醒...64
    kCFRunLoopExit = (1UL << 7),//即将推出loop 128
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

代码举例

  // 创建observer
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        NSLog(@"----监听到RunLoop状态发生改变---%zd", activity);
    });
    //添加观察者到当前线程的RunLoop的当前Mode中,监听runloop的状态
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
    // 释放Observer
    CFRelease(observer);
    /*
     //需要自己管理内存
     CF的内存管理(Core Foundation)
     1.凡是带有Create、Copy、Retain等字眼的函数,创建出来的对象,都需要在最后做一次release
     * 比如CFRunLoopObserverCreate
     2.release函数:CFRelease(对象);
     */

RunLoop的逻辑流程图

456.png

RunLoop的应用

  1. performSelector的使用
  //延迟3秒设置图片并且值只在NSDefaultRunLoopMode模式下,也就是说,在滚动的时候,不显示图片,滚动停止,显示图片
    [self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"123"] afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]
  1. 常驻线程
#import "ViewController.h"
#import "ZHThread.h"
@interface ViewController ()
@property (nonatomic,strong) ZHThread *thread;
@end

@implementation ViewController
/*
 常驻线程:让一个线程永远不死
 通过继承NSThread并且从写delloc监听线程,可以看到,开启线程后执行完任务立马死掉
 注意:不能简单搞个属性强引用,因为线程一旦执行完毕,即使不释放掉,当前的线程也处于消亡状态,从新开启([self.thread start])会崩溃
 当然不通过start调用,通过performselector调用虽然不会崩溃,但是不会调用run2,因为线程处于消亡状态
 //解决办法:线程中添加runloop
 好处:可以随时让拿到这个线程让他做一些事情.没有做事情之前,这个runloop处于休眠状态,一旦调用该线程处理事件,就回唤醒runloop去执行该线程的事件
 使用:搞一个常驻线程监控联网状态
 原理:
 1.创建一个子线程并开启线程
 2.在开启方法(run)中添加一个runloop,并开启runloop
 3.runloop会一直跑圈不会执行完毕,阻止了开启方法的执行完毕即卡住子线程不让他执行完毕
 4.一旦子线程中有事件,runloop会被唤醒,让改事件在该线程中执行,一旦执行完毕,继续跑圈卡住该线程
 如果用一个死循环代替runloop是不可以的,因为死循环一直在处理这个事件,不会停下来先去执行触发事件
 */
- (void)viewDidLoad {
    [super viewDidLoad];
   self.thread = [[ZHThread alloc] initWithTarget:self selector:@selector(run) object:nil];
    //执行线程
     [self.thread start];
}

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [super touchesBegan:touches withEvent:event];
    //随时拿到这个子线程执行事件
    [self performSelector:@selector(run2) onThread:self.thread withObject:nil waitUntilDone:nil];
}
-(void)run{
    NSLog(@"=====run====");
    //run:默认为NSDefaultRunLoopMode模式,时间是永远,下面三行代码等价
    //往里面添加source(mode中啥都没有,runloop没用)
    [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
   //启动runloop
    [[NSRunLoop currentRunLoop] run];
//    [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
//    [[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture]];
    NSLog(@"永远不会调用======");
}
-(void)run2{
     NSLog(@"=====run2====");
}

上面代码用图解如下:

111.png
  1. 在子线程搞个定时器一直调用
//创建子线程初始化调用这个方法(将上面viewdidload方法中run改为runx即可)
-(void)runx{
@autoreleasepool {
    //创建定时器
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run2) userInfo:nil repeats:YES];
    //将定时器添加到runloop
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];
    //或者
//    [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run2) userInfo:nil repeats:YES];
    //放在主线程中就不会用到这句,因为主线程一直在run,不用手动run
//    [[NSRunLoop currentRunLoop] run];
}
}
  1. 自动释放池
  • 将所有对象都放到这个池子中去,到池子释放的时候给池子中的每一个对象release
  • 自动释放池什么时候释放?(看源码得到)
    runloop休眠之前会释放一次,因为休眠可能会很长,如果不释放,会堆积很多
    释放后会同时创建一个新的释放池,处理新一轮事件
    因此,在创建runloop的代码外层最好包装一层自动释放池
    比如上面的代码

GCD的定时器使用

  • GCD的定时器不受runLoop的Mode影响(滚动界面时,定时器不受影响)
    GCD定时器实现不一样
  • GCD比NSTimer准确
    代码举例:
#import "ViewController.h"
@interface ViewController ()
/** 定时器(这里不用带*,因为dispatch_source_t就是个类,内部已经包含了*) */
@property (nonatomic, strong) dispatch_source_t timer;
@end
@implementation ViewController
int count = 0;
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    // 获得队列
    dispatch_queue_t queue = dispatch_get_main_queue();
    // 创建一个定时器(dispatch_source_t本质还是个OC对象)
    self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    // 设置定时器的各种属性(几时开始任务,每隔多长时间执行一次)
    // GCD的时间参数,一般是纳秒(1秒 == 10的9次方纳秒)NSEC_PER_SEC = 1s
    // dispatch_time(DISPATCH_TIME_NOW, 3.0 * NSEC_PER_SEC) 比当前时间晚3秒
   //当前时间直接写:DISPATCH_TIME_NOW
    dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC));
    uint64_t interval = (uint64_t)(1.0 * NSEC_PER_SEC);
    dispatch_source_set_timer(self.timer, start, interval, 0);
    // 设置回调
    dispatch_source_set_event_handler(self.timer, ^{
        NSLog(@"------------%@", [NSThread currentThread]);
        count++;
                if (count == 4) {
                    // 取消定时器
                    dispatch_cancel(self.timer);
                    self.timer = nil;
                }
    });
    // 启动定时器
    dispatch_resume(self.timer);
}
@end

IOS程序启动与运转

注:这段是copy谁的我忘了,总结的很好,借用一下哈


从Xcode的线程函数调用栈(注意看看)可以看到一些方法调用顺序。
(主线程开启)start->(加载framework,动态静态链接库,启动图片,Info.plist,pch等)->main函数->UIApplicationMain函数:{

  • 初始化UIApplication单例对象
  • 初始化AppDelegate对象,并设为UIApplication对象的代理
  • 检查Info.plist设置的xib文件是否有效,如果有则解冻Nib文件并设置outlets,创建显示key window、rootViewController、与rootViewController关联的根view(没有关联则看rootViewController同名的xib),否则launch之后由程序员手动加载。
  • 建立一个主事件循环,其中包含UIApplication的Runloop来开始处理事件。
    }

UIApplication:

  1. 通过window管理视图;
  2. 发送Runloop封装好的control消息给target;
  3. 处理URL,应用图标警告,联网状态,状态栏,远程事件等。

AppDelegate:

管理UIApplication生命周期和应用的五种状态(notRunning/inactive/active/background/suspend)。

Key Window:

  1. 显示view;
  2. 管理rootViewcontroller生命周期;
  3. 发送UIApplication传来的事件消息给view。
    rootViewController:
  4. 管理view(view生命周期;view的数据源/代理;view与superView之间事件响应nextResponder的“备胎”);
  5. 界面跳转与传值;
  6. 状态栏,屏幕旋转。

view:

  1. 通过作为CALayer的代理,管理layer的渲染(顺序大概是先更新约束,再layout再display)和动画(默认layer的属性可动画,view默认禁止,在UIView的block分类方法里才打开动画)。layer是RGBA纹理,通过和mask位图(含alpha属性)关联将合成后的layer纹理填充在像素点内,GPU每1/60秒将计算出的纹理display在像素点中。
  2. 布局子控件(屏幕旋转或者子视图布局变动时,view会重新布局)。
  3. 事件响应:event和guesture。

runloop:

  1. (要让马儿跑)通过do-while死循环让程序持续运行:接收用户输入,调度处理事件时间。
  2. (要让马儿少吃草)通过mach_msg()让runloop没事时进入trap状态,节省CPU资源。

RunLoop面试题

  1. 什么是RunLoop?
    1>从字面意思看:运行循环/跑圈
    2>其实它内部就是do-while循环,在这个循环内部不断地处理各种任务(比如:source/Timer/Observer)
    3>一个线程对应一个runloop,主线程的runloop默认已经启动,子线程的runloop需要自己手动启动(调用run方法)
    4>RunLoop只能选择一个Mode启动,如果当Mode中没有任何source(source0/source1),Timer,那么就直接退出RunLoop
  2. 自动释放池什么时候释放?
    通过Observer监听RunLoop的状态,一旦监听到RunLoop即将进入休眠等待状态,就释放自动释放池(KCFRunLoopBeforeWaiting)
  3. 在开发中如何使用RunLoop?什么应用场景?
    1>开启一个常驻线程(让一个子线程不进入消亡状态,等待其他的线程发来消息,处理其他事件)
    在子线程中开启一个定时器
    在子线程中进行一些长期监控
    2>可以控制定时器在特定模式下运行
    3>可以让某些事件(行为/任务)在特定模式下执行
    4>可以添加Observer监听runloop的状态,比如监听点击事件的处理(在所有点击事件之前做一些事情)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容