runloop

///以下方法可以理解成,给到NSRunloop对应RunMode下多长时间来执行属于该RunMode下的任务,如果执行完成了任务返回,如果超时了也返回,否则一直等待执行任务直到外部调用使之退出此次Runloop的RunMode
///指定了超时的OC方法也就是调用了下面这个方法
CFRunLoopRunInMode(RunMode类型, 时长, true); 最后一个固定为如果执行过就返回,如果我们改成C方法,该参数改为false,就算已经执行过Source未到超时时间也不会返回

///
启动一个runloop需要先设置计时器或者nsport,否则也没什么用 ///等待,超时或者是被唤醒并且执行完任务才会结束等待,返回结果。可以在使用中作为等待其它线程返回结果的用途,如果还等不到,那么就不会往下执行。(不超时的情况下,处理一次就往下执行,超时了也往下执行,返回结果都是YES) ///Model对启动成功或者失败是有影响的,port下测试到必须是NSDefaultRunLoopMode才能成功 ///这个是永不超时(启动后除非执行任务成功否则不会返回而是一直等待) [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]] ///这个是肯定超时(启动后执行当期待完成的任务,没有则直接略过,完成当前循环后立即过期返回) [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantPast]] ///启动运行,并且在设置时间之前一直处理事务 (时间段内可以处理多次,时间到了才往下执行) [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]]; ///只管启动,没有限制 [[NSRunLoop currentRunLoop] run];

dispatch  是线程调度  runloop是线程内任务调度 

runloop开启之后运行在某个模式下,但是可以在执行的过程中,调用APi使它进入到另一个模式下运行(上一层的Runloop并没有退出,而是记录了Runloop层级,那么是否就是在上一个Runloop中开启了新的Runloop,类似递归的代码嵌套?),同时它会记录上一个模式,在运行完恢复到上一个模式上去。在运行一个模式时,设置是否超时、是否执行完之后推出等信息,当前的runloop就根据实际的运行情况来决定是否退出当前运行循环,即退出当前层__CFRunloopRun函数,将模式回退到上一个模式继续执行,如果每一层的模式里面的内容都执行完成,那么线程也就可以结束了。开始的新运行时状态,如果不是执行完事件后就退出并且没有到达退出条件,它将进入休眠,同时监听一些内核端口消息,当它感兴趣的端口(计时器、主线程一步、port,perfrom等)收到消息时,它将从休眠中被唤醒,同时根据唤醒的端口选择继续工作下去的事件,直至满足退出条件结束。

 

/* rl, rlm are locked on entrance and exit */
//runloop进行循环
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
    //获取当前内核时间
    uint64_t startTSR = mach_absolute_time();
//如果当前runLoop或者runLoopMode为停止状态的话直接返回
    if (__CFRunLoopIsStopped(rl)) {
        __CFRunLoopUnsetStopped(rl);
    return kCFRunLoopRunStopped;
    } else if (rlm->_stopped) {
    rlm->_stopped = false;
    return kCFRunLoopRunStopped;
    }
    //判断是否是第一次在主线程中启动RunLoop,如果是且当前RunLoop为主线程的RunLoop,那么就给分发一个队列调度端口
    ////如果在主线程 && runloop是主线程的runloop && 该mode是commonMode,则给mach端口赋值为主线程收发消息的端口
    mach_port_name_t dispatchPort = MACH_PORT_NULL;//用来保存主队列(mainQueue)的端口。
    Boolean libdispatchQSafe = pthread_main_np() && ((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL == previousMode) || (!HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && 0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ)));
    if (libdispatchQSafe && (CFRunLoopGetMain() == rl) && CFSetContainsValue(rl->_commonModes, rlm->_name)) dispatchPort = _dispatch_get_main_queue_port_4CF();
    
#if USE_DISPATCH_SOURCE_FOR_TIMERS
    //在ios中 port 事件能勾起 该执行
    
    mach_port_name_t modeQueuePort = MACH_PORT_NULL;
    // 如果运行模式的队列存在
    if (rlm->_queue) {
        //给当前mode分发队列端口
        modeQueuePort = _dispatch_runloop_root_queue_get_port_4CF(rlm->_queue);
         // 如果分发不成功,提示原因
        if (!modeQueuePort) {
            CRASH("Unable to get port for run loop mode queue (%d)", -1);
        }
    }
#endif
    //gcd 管理的定时器 用于实现runloop 超时机制
    dispatch_source_t timeout_timer = NULL;
    struct __timeout_context *timeout_context = (struct __timeout_context *)malloc(sizeof(*timeout_context));
    //立即超时
    if (seconds <= 0.0) { // instant timeout
        seconds = 0.0;
        timeout_context->termTSR = 0ULL;
    }
    //seconds为超时时间,超时时执行__CFRunLoopTimeout函数
    else if (seconds <= TIMER_INTERVAL_LIMIT) {
        // 分配任务队列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, DISPATCH_QUEUE_OVERCOMMIT);
        // 当前任务队列的超时定时器
    timeout_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
        dispatch_retain(timeout_timer);
        // 保存对应的数据到上下文
    timeout_context->ds = timeout_timer;
    timeout_context->rl = (CFRunLoopRef)CFRetain(rl);
    timeout_context->termTSR = startTSR + __CFTimeIntervalToTSR(seconds);
        // 设置定时器的上下文
    dispatch_set_context(timeout_timer, timeout_context); // source gets ownership of context
        
    dispatch_source_set_event_handler_f(timeout_timer, __CFRunLoopTimeout);
    dispatch_source_set_cancel_handler_f(timeout_timer, __CFRunLoopTimeoutCancel);
         // 根据seconds计算超时时间
    uint64_t ns_at = (uint64_t)((__CFTSRToTimeInterval(startTSR) + seconds) * 1000000000ULL);
        // 设置source的超时时间
    dispatch_source_set_timer(timeout_timer, dispatch_time(1, ns_at), DISPATCH_TIME_FOREVER, 1000ULL);
         // 开始计时
    dispatch_resume(timeout_timer);
    } else { // infinite timeout
        seconds = 9999999999.0;
        timeout_context->termTSR = UINT64_MAX;
    }
// 标记已经运行结束
    Boolean didDispatchPortLastTime = true;
    int32_t retVal = 0;
    do {
        //初始化一个存放内核消息的缓冲池
        uint8_t msg_buffer[3 * 1024];
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
        mach_msg_header_t *msg = NULL;
        mach_port_t livePort = MACH_PORT_NULL;
#elif DEPLOYMENT_TARGET_WINDOWS
        HANDLE livePort = NULL;
        Boolean windowsMessageReceived = false;
#endif
        //记录了所有当前mode中需要监听的port,作为调用监听消息函数的参数。
    __CFPortSet waitSet = rlm->_portSet;
//设置RunLoop为可以被唤醒状态
        __CFRunLoopUnsetIgnoreWakeUps(rl);
//2.通知Observers:通知即将处理timer
        if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
//通知Observers:通知即将处理Source0(非port)。
        if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
     //处理加入到runLoop中的block, 处理非延迟的主线程调用  由CFRunLoopPerformBlock加进来的block 可唤醒runloop  (回调函数里面可能给runloop增加了block函数)
    __CFRunLoopDoBlocks(rl, rlm);
     //执行source0中的源事件
        Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
        //有事件处理返回true,没有事件返回false
        if (sourceHandledThisLoop) {
            //处理加入到runLoop中的block
            __CFRunLoopDoBlocks(rl, rlm);
    }
        //如果没有Sources0事件处理 并且 没有超时,poll为false
        //如果有Sources0事件处理 或者 超时,poll都为true
        //0ULL == timeout_context->termTSR  代表超时判断
        // poll为true表示不需要等待端口消息
        Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
//如果主线程runloop 端口存在、didDispatchPortLastTime为假(首次执行不会进入判断,因为didDispatchPortLastTime为true)
        /// 是否存在异步到主线程的方法(是否已经处理好了这些方法)
        if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
            msg = (mach_msg_header_t *)msg_buffer;
            //__CFRunLoopServiceMachPort用于接受指定端口(一个也可以是多个)的消息,最后一个参数代表当端口无消息的时候是否休眠,0是立刻返回不休眠,TIMEOUT_INFINITY代表休眠
            //处理通过GCD派发到主线程的任务,这些任务优先级最高会被最先处理
            //如果有Source1,就直接跳转去处理消息。(文档说是检测source1,不过源码看来是检测dispatchPort---gcd端口事件)
            if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0)) {
                //如果端口有事件则跳转至handle_msg
                goto handle_msg;
            }
#elif DEPLOYMENT_TARGET_WINDOWS
            if (__CFRunLoopWaitForMultipleObjects(NULL, &dispatchPort, 0, 0, &livePort, NULL)) {
                goto handle_msg;
            }
#endif
        }

        didDispatchPortLastTime = false;
        //之前没有处理过source0或者没有超时,也没有source1消息,就让线程进入睡眠。
        //通知 Observers: RunLoop 的线程即将进入休眠
    if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
        // 标志当前runLoop为休眠状态
    __CFRunLoopSetSleeping(rl);
    // do not do any user callouts after this point (after notifying of sleeping)

        // Must push the local-to-this-activation ports in on every loop
        // iteration, as this mode could be run re-entrantly and we don't
        // want these ports to get serviced.

        __CFPortSetInsert(dispatchPort, waitSet);
        
    __CFRunLoopModeUnlock(rlm);
    __CFRunLoopUnlock(rl);

#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
#if USE_DISPATCH_SOURCE_FOR_TIMERS
        // 进入循环开始不断的读取端口信息,如果端口有唤醒信息则唤醒当前runLoop
        do {
            if (kCFUseCollectableAllocator) {
                objc_clear_stack(0);
                memset(msg_buffer, 0, sizeof(msg_buffer));
            }
            msg = (mach_msg_header_t *)msg_buffer;
            // 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。
            /// • 一个基于 port 的Source 的事件。
            /// • 一个 Timer 到时间了
            /// • RunLoop 自身的超时时间到了
            /// • 被其他什么调用者手动唤醒
            //如果poll为no,且waitset中无port有消息,线程进入休眠;否则唤醒
//            如果没有Sources0事件处理 并且 没有超时,poll为false
//            __CFRunLoopServiceMachPort用于接受指定端口(一个也可以是多个)的消息,最后一个参数代表当端口无消息的时候是否休眠,0是立刻返回不休眠,TIMEOUT_INFINITY代表休眠
            
            __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY);
            //livePort是modeQueuePort,则代表为当前mode队列的端口
            if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
                // Drain the internal queue. If one of the callout blocks sets the timerFired flag, break out and service the timer.
                while (_dispatch_runloop_root_queue_perform_4CF(rlm->_queue));
                //知道Timer被激活了才跳出二级循环继续循环一级循环
                if (rlm->_timerFired) {
                    // Leave livePort as the queue port, and service timers below
                    rlm->_timerFired = false;
                    break;
                } else {
                    if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);
                }
            }
            //如果livePort不为modeQueuePort,runLoop被唤醒。这代表__CFRunLoopServiceMachPort给出的livePort只有两种可能:一种情况为MACH_PORT_NULL,另一种为真正获取的消息的端口。
            else {
                // Go ahead and leave the inner loop.
                break;
            }
        } while (1);
#else
        if (kCFUseCollectableAllocator) {
            objc_clear_stack(0);
            memset(msg_buffer, 0, sizeof(msg_buffer));
        }
        msg = (mach_msg_header_t *)msg_buffer;
        __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY);
#endif
        
        
#elif DEPLOYMENT_TARGET_WINDOWS
        // Here, use the app-supplied message queue mask. They will set this if they are interested in having this run loop receive windows messages.
        __CFRunLoopWaitForMultipleObjects(waitSet, NULL, poll ? 0 : TIMEOUT_INFINITY, rlm->_msgQMask, &livePort, &windowsMessageReceived);
#endif
        
        __CFRunLoopLock(rl);
        __CFRunLoopModeLock(rlm);

        // Must remove the local-to-this-activation ports in on every loop
        // iteration, as this mode could be run re-entrantly and we don't
        // want these ports to get serviced. Also, we don't want them left
        // in there if this function returns.

        __CFPortSetRemove(dispatchPort, waitSet);
        //忽略端口唤醒
        __CFRunLoopSetIgnoreWakeUps(rl);

        // user callouts now OK again
        //取消runloop的休眠状态
    __CFRunLoopUnsetSleeping(rl);
        //通知 Observers:RunLoop的线程刚刚被唤醒了。
    if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
//处理端口消息
        handle_msg:
        //设置此时runLoop忽略端口唤醒(保证线程安全)
        __CFRunLoopSetIgnoreWakeUps(rl);

#if DEPLOYMENT_TARGET_WINDOWS
        if (windowsMessageReceived) {
            // These Win32 APIs cause a callout, so make sure we're unlocked first and relocked after
            __CFRunLoopModeUnlock(rlm);
        __CFRunLoopUnlock(rl);

            if (rlm->_msgPump) {
                rlm->_msgPump();
            } else {
                MSG msg;
                if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE | PM_NOYIELD)) {
                    TranslateMessage(&msg);
                    DispatchMessage(&msg);
                }
            }
            
            __CFRunLoopLock(rl);
        __CFRunLoopModeLock(rlm);
         sourceHandledThisLoop = true;
            
            // To prevent starvation of sources other than the message queue, we check again to see if any other sources need to be serviced
            // Use 0 for the mask so windows messages are ignored this time. Also use 0 for the timeout, because we're just checking to see if the things are signalled right now -- we will wait on them again later.
            // NOTE: Ignore the dispatch source (it's not in the wait set anymore) and also don't run the observers here since we are polling.
            __CFRunLoopSetSleeping(rl);
            __CFRunLoopModeUnlock(rlm);
            __CFRunLoopUnlock(rl);
            
            __CFRunLoopWaitForMultipleObjects(waitSet, NULL, 0, 0, &livePort, NULL);
            
            __CFRunLoopLock(rl);
            __CFRunLoopModeLock(rlm);            
            __CFRunLoopUnsetSleeping(rl);
            // If we have a new live port then it will be handled below as normal
        }
        
        
#endif
        // 处理待处理的事件
        if (MACH_PORT_NULL == livePort) {
            CFRUNLOOP_WAKEUP_FOR_NOTHING();
            // handle nothing
        }
        //struct __CFRunLoop中有这么一项:__CFPort _wakeUpPort,用于手动将当前runloop线程唤醒,通过调用CFRunLoopWakeUp完成,CFRunLoopWakeUp会向_wakeUpPort发送一条消息
        else if (livePort == rl->_wakeUpPort) {
            //只有外界调用CFRunLoopWakeUp才会进入此分支,这是外部主动唤醒runLoop的接口
            
            CFRUNLOOP_WAKEUP_FOR_WAKEUP();//返回2重新循环
            // do nothing on Mac OS
#if DEPLOYMENT_TARGET_WINDOWS
            // Always reset the wake up port, or risk spinning forever
            ResetEvent(rl->_wakeUpPort);
#endif
        }
#if USE_DISPATCH_SOURCE_FOR_TIMERS
        //如果是定时器事件
        else if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
            CFRUNLOOP_WAKEUP_FOR_TIMER();
            if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
                // Re-arm the next timer, because we apparently fired early
                __CFArmNextTimerInMode(rlm, rl);
            }
        }
#endif
#if USE_MK_TIMER_TOO
        //处理因timer的唤醒。
//        如果一个 Timer 到时间了,触发这个Timer的回调。
        else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
            CFRUNLOOP_WAKEUP_FOR_TIMER();
            // On Windows, we have observed an issue where the timer port is set before the time which we requested it to be set. For example, we set the fire time to be TSR 167646765860, but it is actually observed firing at TSR 167646764145, which is 1715 ticks early. The result is that, when __CFRunLoopDoTimers checks to see if any of the run loop timers should be firing, it appears to be 'too early' for the next timer, and no timers are handled.
            // In this case, the timer port has been automatically reset (since it was returned from MsgWaitForMultipleObjectsEx), and if we do not re-arm it, then no timers will ever be serviced again unless something adjusts the timer list (e.g. adding or removing timers). The fix for the issue is to reset the timer here if CFRunLoopDoTimers did not handle a timer itself. 9308754
            if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
                // Re-arm the next timer
                __CFArmNextTimerInMode(rlm, rl);
            }
        }
#endif
        //如果是dispatch到main queue的block
        else if (livePort == dispatchPort) {
            CFRUNLOOP_WAKEUP_FOR_DISPATCH();
            __CFRunLoopModeUnlock(rlm);
            __CFRunLoopUnlock(rl);
            _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)6, NULL);
            
#if DEPLOYMENT_TARGET_WINDOWS
            void *msg = 0;
#endif
            //处理异步方法唤醒。处理gcd dispatch到main_queue的block,执行block。
            /*有判断是否是在MainRunLoop,有获取Main_Queue 的port,并且有调用 Main_Queue 上的回调,这只能是是 GCD 主队列上的异步任务。即:dispatch_async(dispatch_get_main_queue(), block)产生的任务。
             */
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
            _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)0, NULL);
            __CFRunLoopLock(rl);
            __CFRunLoopModeLock(rlm);
             sourceHandledThisLoop = true;
            didDispatchPortLastTime = true;
        } else {
            //处理Source1 (基于port)
            CFRUNLOOP_WAKEUP_FOR_SOURCE();
            // Despite the name, this works for windows handles as well
            //过滤macPort消息,有一些消息不一定是runloop中注册的,这里只处理runloop中注册的消息,在rlm->_portToV1SourceMap通过macPort找有没有对应的runloopMode
            CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);
            // 有source1事件待处理
            if (rls) {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
        mach_msg_header_t *reply = NULL;
                // 处理Source1
        sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
        if (NULL != reply) {
            //当前线程处理完source1,给发消息的线程反馈消息
            (void)mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL);
            CFAllocatorDeallocate(kCFAllocatorSystemDefault, reply);
        }
#elif DEPLOYMENT_TARGET_WINDOWS
                sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls) || sourceHandledThisLoop;
#endif
        }
        } 
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
        if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);
#endif
        
    __CFRunLoopDoBlocks(rl, rlm);
        
//进入run loop时传入的参数,处理完事件就返回
    if (sourceHandledThisLoop && stopAfterHandle) {
        retVal = kCFRunLoopRunHandledSource;
        }
    // 超出传入参数标记的超时时间了
    else if (timeout_context->termTSR < mach_absolute_time()) {
            retVal = kCFRunLoopRunTimedOut;
    }
        // 被外部调用者强制停止了
    else if (__CFRunLoopIsStopped(rl)) {
            __CFRunLoopUnsetStopped(rl);
        retVal = kCFRunLoopRunStopped;
    } else if (rlm->_stopped) {
        rlm->_stopped = false;
        retVal = kCFRunLoopRunStopped;
    }
        // source/timer/observer一个都没有了
    else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
        retVal = kCFRunLoopRunFinished;
    }
        // 如果没超时,mode里没空,loop也没被停止,那继续loop
    } while (0 == retVal);

    if (timeout_timer) {
        dispatch_source_cancel(timeout_timer);
        dispatch_release(timeout_timer);
    } else {
        free(timeout_context);
    }

    return retVal;
}

 

https://blog.ibireme.com/2015/05/18/runloop/

 

- (void)testRunloop {
    [NSThread detachNewThreadWithBlock:^{
        [NSThread sleepForTimeInterval:1.2];
        dispatch_async(dispatch_get_main_queue(), ^{
            [self otherRunloopWork];
        });
        [NSThread sleepForTimeInterval:4.0];
        isFinsh = YES;
    }];
// 以下的timer类型,则不会触发从当前runloop返回
// [self performSelector:@selector(otherRunloopWork) withObject:nil afterDelay:1.2]; while (!isFinsh) { CFTimeInterval duration = CFAbsoluteTimeGetCurrent(); // [[NSRunLoop currentRunLoop] runMode:[NSRunLoop currentRunLoop].currentMode beforeDate:[NSDate distantFuture]]; CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10.0, true); NSLog(@"Runloop执行时长:%.7f",CFAbsoluteTimeGetCurrent() - duration); } NSLog(@"Runloop等待结束"); } - (void)otherRunloopWork { NSLog(@"Runloop的其他工作"); }

 

posted @ 2020-05-19 19:48  雨筱逸悠  阅读(217)  评论(0编辑  收藏  举报