标签:
int main(int argc, char * argv[]) { //程序一直运行状态 while (AppIsRunning) { //睡眠状态,等待唤醒事件 id whoWakesMe = SleepForWakingUp(); //得到唤醒事件 id event = GetEvent(whoWakesMe); //开始处理事件 HandleEvent(event); } return 0; }
-----------------------------runloop 与Cocoa息息相关---------------------------------------
1、相关知识:
自动释放池、延迟回调、触摸事件、屏幕刷新、定时器(NSTimer)、GCD,mach kernel,block,pthread,NSObject(NSThreadPerformAddition),CADisplayLink,CATransition,CAAnimation,dispatch_get_main_queue(),NSPort,NSURLConnection,AFNetworking,这么说来跟很多知识都有关联。
2、CFRunLoopRef构造:数据结构;创建与退出;mode切换和item依赖;Runloop启动
3、Runloop内部逻辑:关键在两个判断点(是否睡觉,是否退出)
4、Runloop本质:mach port和mach_msg()
5、如何处理界面刷新
6、如何处理UI事件响应
7、如何处理手势
8、如何处理GCD任务
9、如何处理timer(与CADisplayLink)
10、如何处理performSelector
11、如何处理网络请求
12、Runloop 常见应用
---------------------------------------------------------------------------------------------------------
先简单介绍runloop的功能,以及涉及到的模块
【线程】 Runloop的寄生于线程:一个线程只能有唯一对应的runloop;但这个根runloop里可以嵌套子runloops;线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。在任何一个Cocoa程序的线程中,都可以通过:NSRunLoop *runloop = [NSRunLoop currentRunLoop];
线程(创建)-->runloop将进入-->最高优先级OB创建释放池-->runloop将睡-->最低优先级OB销毁旧池创建新池-->runloop将退出-->最低优先级OB销毁新池-->线程(销毁)
主线程 (有 RunLoop 的线程) 几乎所有函数都从以下六个之一的函数调起(可以通过xcode端点调试,看函数栈):
CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION CFRunloop is calling out to an abserver callback function 用于向外部报告 RunLoop 当前状态的更改,框架中很多机制都由 RunLoopObserver 触发,如 CAAnimation CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK CFRunloop is calling out to a block 消息通知、非延迟的perform、dispatch调用、block回调、KVO CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE CFRunloop is servicing the main desipatch queue CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION CFRunloop is calling out to a timer callback function 延迟的perform, 延迟dispatch调用 CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION CFRunloop is calling out to a source 0 perform function 处理App内部事件、App自己负责管理(触发),如UIEvent、CFSocket。普通函数调用,系统调用 CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION CFRunloop is calling out to a source 1 perform function 由RunLoop和内核管理,Mach port驱动,如CFMachPort、CFMessagePort
【自动释放池】自动释放池寄生于Runloop:程序启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler(),一是,监测Entry(即将进入Loop)状态,其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池,其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前;另外一个,Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。
下面将介绍runloop的结构,然后继续介绍【触摸事件】,【屏幕刷新】,【定时器】,【屏幕刷新】等。
// runloop数据结构 struct __CFRunLoopMode { CFStringRef _name; // Mode名字, CFMutableSetRef _sources0; // Set<CFRunLoopSourceRef> CFMutableSetRef _sources1; // Set<CFRunLoopSourceRef> CFMutableArrayRef _observers; // Array<CFRunLoopObserverRef> CFMutableArrayRef _timers; // Array<CFRunLoopTimerRef> ... }; // mode数据结构 struct __CFRunLoop { CFMutableSetRef _commonModes; // Set<CFStringRef> CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer> CFRunLoopModeRef _currentMode; // Current Runloop Mode CFMutableSetRef _modes; // Set<CFRunLoopModeRef> ... };创建与退出:mode切换和item依赖
a 主线程的runloop自动创建,子线程的runloop默认不创建(在子线程中调用NSRunLoop *runloop = [NSRunLoop currentRunLoop];获取RunLoop对象的时候,就会创建RunLoop);
b runloop退出的条件:app退出;线程关闭;设置最大时间到期;modeItem为空;
c 同一时间一个runloop只能在一个mode,切换mode只能退出runloop,再重进指定mode(隔离modeItems使之互不干扰);
d 一个item可以加到不同mode;一个mode被标记到commonModes里(这样runloop不用切换mode)。
启动Runloop:
// 用DefaultMode启动 void CFRunLoopRun(void) { CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false); } // 用指定的Mode启动,允许设置RunLoop最大时间(假无限循环),执行完毕是否退出 int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) { return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled); }CFRunLoopModeRef:
// 添加mode
CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
类型:
1. kCFRunLoopDefaultMode: 默认 mode,通常主线程在这个 Mode 下运行。
2. UITrackingRunLoopMode: 追踪mode,保证Scrollview滑动顺畅不受其他 mode 影响。
3. UIInitializationRunLoopMode: 启动程序后的过渡mode,启动完成后就不再使用。
4: GSEventReceiveRunLoopMode: Graphic相关事件的mode,通常用不到。
5: kCFRunLoopCommonModes: 占位mode,作为标记DefaultMode和CommonMode用。
modeItems
// 添加移除item的函数(参数:添加/移除哪个item到哪个runloop的哪个mode下) CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName); CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName); CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode); CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName); CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName); CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);CFRunLoopSourceRef:事件来源
数据结构(source0/source1);
// source0 (manual): order(优先级),callout(回调函数) CFRunLoopSource {order =..., {callout =... }} // source1 (mach port):order(优先级),port:(端口), callout(回调函数) CFRunLoopSource {order = ..., {port = ..., callout =...}source0:event事件,只含有回调,需要标记待处理(signal),然后手动将runloop唤醒(wakeup);
// Observer:order(优先级),ativity(监听状态),callout(回调函数) CFRunLoopObserver {order = ..., activities = ..., callout = ...}创建与添加;
// 第一个参数用于分配该observer对象的内存空间 // 第二个参数用以设置该observer监听什么状态 // 第三个参数用于标识该observer是在第一次进入run loop时执行还是每次进入run loop处理时均执行 // 第四个参数用于设置该observer的优先级,一般为0 // 第五个参数用于设置该observer的回调函数 // 第六个参数observer的运行状态 CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { // 执行代码 }监听的状态:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { kCFRunLoopEntry = (1UL << 0), // 即将进入Loop kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠 kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒 kCFRunLoopExit = (1UL << 7), // 即将退出Loop };
// RunLoop的实现 int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) { // 0.1 根据modeName找到对应mode CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false); // 0.2 如果mode里没有source/timer/observer, 直接返回。 if (__CFRunLoopModeIsEmpty(currentMode)) return; // 1.1 通知 Observers: RunLoop 即将进入 loop。---(OB会创建释放池) __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry); // 1.2 内部函数,进入loop __CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) { Boolean sourceHandledThisLoop = NO; int retVal = 0; do { // 2.1 通知 Observers: RunLoop 即将触发 Timer 回调。 __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers); // 2.2 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。 __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources); // 执行被加入的block __CFRunLoopDoBlocks(runloop, currentMode); // 2.3 RunLoop 触发 Source0 (非port) 回调。 sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle); // 执行被加入的block __CFRunLoopDoBlocks(runloop, currentMode); // 2.4 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。 if (__Source0DidDispatchPortLastTime) { Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg) if (hasMsg) goto handle_msg; } // 3.1 如果没有待处理消息,通知 Observers: RunLoop 的线程即将进入休眠(sleep)。--- (OB会销毁释放池并建立新释放池) if (!sourceHandledThisLoop) { __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting); } // 3.2. 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。 // - 一个基于 port 的Source1 的事件。 // - 一个 Timer 到时间了 // - RunLoop 启动时设置的最大超时时间到了 // - 被手动唤醒 __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) { mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg } // 3.3. 被唤醒,通知 Observers: RunLoop 的线程刚刚被唤醒了。 __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting); // 4.0 处理消息。 handle_msg: // 4.1 如果消息是Timer类型,触发这个Timer的回调。 if (msg_is_timer) { __CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time()) } // 4.2 如果消息是dispatch到main_queue的block,执行block。 else if (msg_is_dispatch) { __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg); } // 4.3 如果消息是Source1类型,处理这个事件 else { CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort); sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg); if (sourceHandledThisLoop) { mach_msg(reply, MACH_SEND_MSG, reply); } } // 执行加入到Loop的block __CFRunLoopDoBlocks(runloop, currentMode); // 5.1 如果处理事件完毕,启动Runloop时设置参数为一次性执行,设置while参数退出Runloop if (sourceHandledThisLoop && stopAfterHandle) { retVal = kCFRunLoopRunHandledSource; // 5.2 如果启动Runloop时设置的最大运转时间到期,设置while参数退出Runloop } else if (timeout) { retVal = kCFRunLoopRunTimedOut; // 5.3 如果启动Runloop被外部调用强制停止,设置while参数退出Runloop } else if (__CFRunLoopIsStopped(runloop)) { retVal = kCFRunLoopRunStopped; // 5.4 如果启动Runloop的modeItems为空,设置while参数退出Runloop } else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) { retVal = kCFRunLoopRunFinished; } // 5.5 如果没超时,mode里没空,loop也没被停止,那继续loop,回到第2步循环。 } while (retVal == 0); } // 6. 如果第6步判断后loop退出,通知 Observers: RunLoop 退出。--- (OB会销毁新释放池) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit); }
函数作用栈显示:
{ // 1.1 通知Observers,即将进入RunLoop // 此处有Observer会创建AutoreleasePool: _objc_autoreleasePoolPush(); __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry); do { // 2.1 通知 Observers: 即将触发 Timer 回调。 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers); // 2.2 通知 Observers: 即将触发 Source (非基于port的,Source0) 回调。 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources); // 执行Block __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block); // 2.3 触发 Source0 (非基于port的) 回调。 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0); // 执行Block __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block); // 3.1 通知Observers,即将进入休眠 // 此处有Observer释放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush(); __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting); // 3.2 sleep to wait msg. mach_msg() -> mach_msg_trap(); // 3.3 通知Observers,线程被唤醒 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting); // 4.1 如果是被Timer唤醒的,回调Timer __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer); // 4.2 如果是被dispatch唤醒的,执行所有调用 dispatch_async 等方法放入main queue 的 block __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block); // 4.3 如果如果Runloop是被 Source1 (基于port的) 的事件唤醒了,处理这个事件 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1); // 5. 退出判断函数调用栈无显示 } while (...); // 6. 通知Observers,即将退出RunLoop // 此处有Observer释放AutoreleasePool: _objc_autoreleasePoolPop(); __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit); }一步一步写具体的实现逻辑过于繁琐不便理解,按Runloop状态大致分为:
这个函数内部的调用栈大概是这样的:
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv() QuartzCore:CA::Transaction::observer_callback: CA::Transaction::commit(); CA::Context::commit_transaction(); CA::Layer::layout_and_display_if_needed(); CA::Layer::layout_if_needed(); [CALayer layoutSublayers]; [UIView layoutSubviews]; CA::Layer::display_if_needed(); [CALayer display]; [UIView drawRect];
注意看runloop的方法:CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION
注意看runloop的方法:CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE
如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。
CFRunLoopTimerRef:系统内“定时闹钟”
NSTimer和performSEL方法实际上是对CFRunloopTimerRef的封装;runloop启动时设置的最大超时时间实际上是GCD的dispatch_source_t类型。
数据结构:
// Timer:interval:(闹钟间隔), tolerance:(延期时间容忍度),callout(回调函数) CFRunLoopTimer {firing =..., interval = ...,tolerance = ...,next fire date = ...,callout = ...}创建与生效;
//NSTimer: // 创建一个定时器(需要手动加到runloop的mode中) + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo; // 默认已经添加到主线程的runLoop的DefaultMode中 + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo; // performSEL方法 // 内部会创建一个Timer到当前线程的runloop中(如果当前线程没runloop则方法无效;performSelector:onThread: 方法放到指定线程runloop中) - (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay相关类型(GCD的timer与CADisplayLink)
dispatch_source_set_timer(dispatch_source_t source, // 定时器对象 dispatch_time_t start, // 定时器开始执行的时间 uint64_t interval, // 定时器的间隔时间 uint64_t leeway // 定时器的精度 );
CADisplayLink 是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和 NSTimer 并不一样,其内部实际是操作了一个 Source)。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer 相似),造成界面卡顿的感觉。在快速滑动TableView时,即使一帧的卡顿也会让用户有所察觉。Facebook 开源的 AsyncDisplayLink 就是为了解决界面卡顿的问题,其内部也用到了 RunLoop。
CADisplayLink :
Timer的tolerance表示最大延期时间,如果因为阻塞错过了这个时间精度,这个时间点的回调也会跳过去,不会延后执行。
CADisplayLink 是一个和屏幕刷新率一致的定时器,如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer 相似,只是没有tolerance容忍时间),造成界面卡顿的感觉。
除了基于端口的源,Cocoa定义了自定义输入源,允许你在任何线程执行selector方法。和基于端口的源一样,执行selector请求会在目标线程上序列化,减缓许多在线程上允许多个方法容易引起的同步问题。不像基于端口的源,一个selector执行完后会自动从run loop里面移除。
当在其他线程上面执行selector时,目标线程须有一个活动的run loop。对于你创建的线程,这意味着线程在你显式的启动run loop之前是不会执行selector方法的,而是一直处于休眠状态。
NSObject类提供了类似如下的selector方法:
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)argwaitUntilDone:(BOOL)wait modes:(NSArray *)array;
注意看runloop的方法:cfrunloop_is_calling_out_to_a_timer_callback_function
根据前面介绍过的跟runloop先关的模块,那么每个模块都有应用。这里列举常见应用:
滑动与图片刷新
当tableview的cell上有需要从网络获取的图片的时候,滚动tableView,异步线程会去加载图片,加载完成后主线程就会设置cell的图片,但是会造成卡顿。可以让设置图片的任务在CFRunLoopDefaultMode下进行,当滚动tableView的时候,RunLoop是在 UITrackingRunLoopMode 下进行,不去设置图片,而是当停止的时候,再去设置图片。- (void)viewDidLoad { [super viewDidLoad]; // 只在NSDefaultRunLoopMode下执行(刷新图片) [self.myImageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@""] afterDelay:ti inModes:@[NSDefaultRunLoopMode]]; }常驻子线程,保持子线程一直处理事件
+ (void)networkRequestThreadEntryPoint:(id)__unused object { @autoreleasepool { [[NSThread currentThread] setName:@"AFNetworking"]; NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; [runLoop run]; } } + (NSThread *)networkRequestThread { static NSThread *_networkRequestThread = nil; static dispatch_once_t oncePredicate; dispatch_once(&oncePredicate, ^{ _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil]; [_networkRequestThread start]; }); return _networkRequestThread; } - (void)start { [self.lock lock]; if ([self isCancelled]) { [self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]]; } else if ([self isReady]) { self.state = AFOperationExecutingState; [self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]]; } [self.lock unlock]; }
接到程序崩溃时的信号进行自主处理例如弹出提示等
CFRunLoopRef runLoop = CFRunLoopGetCurrent(); NSArray *allModes = CFBridgingRelease(CFRunLoopCopyAllModes(runLoop)); while (1) { for (NSString *mode in allModes) { CFRunLoopRunInMode((CFStringRef)mode, 0.001, false); } }
- (BOOL)runUntilBlock:(BOOL(^)())block timeout:(NSTimeInterval)timeout { __block Boolean fulfilled = NO; void (^beforeWaiting) (CFRunLoopObserverRef observer, CFRunLoopActivity activity) = ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { fulfilled = block(); if (fulfilled) { CFRunLoopStop(CFRunLoopGetCurrent()); } }; CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(NULL, kCFRunLoopBeforeWaiting, true, 0, beforeWaiting); CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode); // Run! CFRunLoopRunInMode(kCFRunLoopDefaultMode, timeout, false); CFRunLoopRemoveObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode); CFRelease(observer); return fulfilled; }
参考http://www.starming.com/index.php?v=index&view=74
http://www.jianshu.com/p/37ab0397fec7
http://blog.csdn.net/ztp800201/article/details/9240913
http://www.cnblogs.com/zy1987/p/4582466.html
标签:
原文地址:http://blog.csdn.net/hherima/article/details/51746125