热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

CFRunLoop源码学习笔记(CF1151.16)

1、CFRunLoopModeRef什么时候创建的?在调用__CFRunLoopFindMode(rl,modeName,create)1.1)首先通过

1、CFRunLoopModeRef 什么时候创建的?

在调用__CFRunLoopFindMode(rl, modeName, create)

1.1)首先通过modeName 在RunLoop 中的_modes/_commentModes 中查找,查找到直接返回,否则要进行 create 判断。

1.2)当 create 参数为true 的时候创建CFRunLoopModeRef, 并加入到rl(RunLoop)中。创建的过程默认创建了 timer端口(mk_timer(iOS & Mac OS X)/dispatch_timer(Mac OS X)), 并且将RunLoop 的wakeupPort 加入到 CFRunLoopModeRef的_portSet 中。注意: _portSet 实际调用了 mach_port_allocate 并分配了端口集合权限(MACH_PORT_RIGHT_PORT_SET),创建完成后 _portSet 至少有两个成员 一个是timer的端口,一个是RunLoop的wakup端口。

1.3)Mac OS下支持mk和dispatch timer,iOS下仅支持mk timer

2、CFRunLoop 何时创建的?

2.1) 当前的线程调用 CFRunLoopGetCurrent的时候创建,否则是没有任何创建的,创建的过程为, 首先判断pthread 的TSD 是否存储了CFRunLoopRef 对象,如果有直接提取,如果没有 则调用私有函数__CFRunLoopGet0, 并传入当前线程的 pthread_t 的值(pthread_self()取得)。

2.1.1) __CFRunLoopGet0(pthread_t) 概述

2.1.1.1) 首先判断 pthread_t 是否为空,如果为空,则赋值为主线程 pthread_main_thread_np取得

2.1.1.2) 然后创建一个全局字典,通过原子函数(OSAtomicCompareAndSwapPtrBarrier),确保这个RunLoop字典唯一创建,并在创建字典的同时通过调用 __CFRunLoopCreate 创建一个 主线程的RunLoop, 并以线程pthread_t.p 值为键值,存入全局字典

2.1.1.3) 从全局字典中取出以pthread_t.p 为键的RunLoop 对象,如果没有则调用__CFRunLoopCreate 创建一个 RunLoop 对象

2.1.1.4) 如果传入的线程pthread_t 与当前线程相同,则存入线程私有数据.(TSD),

2.1.1.5) 返回RunLoop 对象

 

2.1.2) __CFRunLoopCreate 概述

这里简单分配一个内存区域,然后返回这个内存区域,重点在于分配了一个wakeup port 端口,这个端口对于RunLoop 十分重要。

3、CFRunLoopRun 如何工作的?

3.1) CFRunLoopRun 内部是一个do{}while() 循环,调用了CFRunLoopRunSpecific, 通过判断 CFRunLoopRunSpecific 返回值确定是否继续执行, 传入modeName 为默认的模式(kCFRunLoopDefaultMode),返回值如果为kCFRunLoopRunStopped 或者 kCFRunLoopRunFinished 则退出循环, CFRunLoop 也就结束了

3.2) CFRunLoopRunSpecific 函数说明

3.2.1) 参数说明,

a) rl(CFRunLoopRef),

b) modeName(CFStringRef)

c) seconds(CFTimeInterval) 这个是设置超时时间

d) returnAfterSourceHandled(Boolean) 当source0处理后,是否还继续处理其余的source0,这个CFRunLoopRun 传入的是 false,意味这继续处理所有souce0

 

3.2.2) 执行过程

a) 首先判断 runloop 的deallocating的标志位,如果是销毁中状态,则直接返回kCFRunLoopRunFinished

b) 判断模式是否为空,为空也返回 kCFRunLoopRunFinished

c) 根据观察者掩码执行本次进入循环 观察者回调

d) 执行 __CFRunLoopRun 函数

e) 根据观察者掩码执行本次退出循环 观察者回调

f) 返回 _CFRunLoopRun 的返回值

 

3.3) __CFRunLoopRun 函数说明

3.3.1) 参数说明

a) CFRunLoopRef rl

b) CFRunLoopModeRef rlm

c) CFTimeInterval seconds

d) Boolean stopAfterHandle 同 CFRunLoopRunSpecific 的 returnAfterSourceHandled

e) CFRunLoopModeRef previousMode 之前的模式

 

3.3.2) 执行过程

a) 首先判断了 runloop和runloopmode的停止状态,如果是停止状态则直接返回 kCFRunLoopRunStopped

b) 在主线程下取得主线程的 mach 端口(dispatchPort)

c) 根据seconds 小于 间隔限制 貌似一个常量,建立一个 dispatch_source 并加入timer,队列如果是主线程,则使用主线程队列,如果后台线程则使用后台线程,这个dispatch_source, 应该在CFRunLoopRunInMode 是传入的seconds 有效,CFRunLoop 传入的1.0e10 并不创建这个source。这个source 貌似仅仅为了唤醒 runloop

d) 进入 do{}while() 循环

  d.1) 定义基于 mach 消息机制的变量 

  d.2) 观察者beforeXXX回调(timer 和 source)

  d.3) 调用__CFRunLoopDoSources0 处理 source0, 仅仅处理通过 CFRunLoopSourceSignal 发信号的信号源,循环处理取决于 stopAfterHandle 这个变量的值, 如果处理过则返回true, 否则返回 false,

  d.4) 定义mach_msg 的超时时间设置的变量(布尔值 poll ),如果 seconds 为0 或者 __CFRunLoopDoSources0 返回 true 则证明拉取过,则设置 mach_msg 超时为0, mach_msg 不进行任何等待,直接返回,否则设置超时时间为无穷大,永远等待直到有消息返回给指定的端口

  d.5) 如果主线程端口不为空并且不是第一次进入循环则接收消息,如果没有消息,立即返回,如果接收到消息,跳转到处理消息地点,这里使用goto handlemsg;

  d.6) 处理观察者回调为afterXXX

  d.7) 如果可能将 主线程端口加入等待端口集合中portSet(初始值为runloop mode 的portSet)

  d.8) 继续接收消息,端口设置为集合端口 portSet , 根据poll值来设置超时时间,如果poll则为0, 否则无穷大时间。如果为无穷大超时时间,则线程处于休眠状态,直到向这个端口集合中的任意端口发送了 mach 消息

  d.9) 消息处理,通过判断 mach 消息发送给了那个端口进行处理

    i) 如果为空或者是runloop的wakup端口,则什么也不做,继续循环

    ii) 如果是timer时间端口,则处理timer时间回调(遍历回调,尽可能的处理一个timer事件)

    iii) 如果是主线程端口,则主线程处理msg,貌似回调GCD ????

    iv) 否则的情况下处理 source1

  d.10) 设置返回值

 

 __CFRunLoopRun 源码, 去掉windows、disaptch_timer 支持

static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {uint64_t startTSR &#61; mach_absolute_time();/* 首先判断 CFRunLoopRef 是否停止&#xff0c;停止的话&#xff0c;重置停止状态并且返回停止状态 */if (__CFRunLoopIsStopped(rl)) {/* 是否设置了停止位 */__CFRunLoopUnsetStopped(rl);/* 重置停止位、并返回停止状态 */return kCFRunLoopRunStopped;} else if (rlm->_stopped) {/* 这个模式下是否停止了、并且重置模式停止状态 */rlm->_stopped &#61; false;/* 返回停止 */return kCFRunLoopRunStopped;}mach_port_name_t dispatchPort &#61; MACH_PORT_NULL; /* 主线程端口或者空 */Boolean libdispatchQSafe &#61; pthread_main_np() && ((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL &#61;&#61; previousMode) || (!HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && 0 &#61;&#61; _CFGetTSD(__CFTSDKeyIsInGCDMainQ)));if (libdispatchQSafe && (CFRunLoopGetMain() &#61;&#61; rl) && CFSetContainsValue(rl->_commonModes, rlm->_name)) dispatchPort &#61; _dispatch_get_main_queue_port_4CF(); /* libDispatch -> private.h *//* 定义一个 dispatch_source_t 为了CFRunLoopRunInMode 自定义时间时存在&#xff0c;为了做唤醒使用 */dispatch_source_t timeout_timer &#61; NULL;struct __timeout_context *timeout_context &#61; (struct __timeout_context *)malloc(sizeof(*timeout_context));if (seconds <&#61; 0.0) { // instant timeoutseconds &#61; 0.0;timeout_context->termTSR &#61; 0ULL;} else if (seconds <&#61; TIMER_INTERVAL_LIMIT/* TIMER_INTERVAL_LIMIT 504911232.0*/) {/* 尝试超时后唤醒&#xff0c;如果当前线程出于等待状态的情况下 */dispatch_queue_t queue &#61; pthread_main_np() ? __CFDispatchQueueGetGenericMatchingMain() : __CFDispatchQueueGetGenericBackground();/* 创建source */timeout_timer &#61; dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);dispatch_retain(timeout_timer);/* 写入source 的 上下文中 */timeout_context->ds &#61; timeout_timer;timeout_context->rl &#61; (CFRunLoopRef)CFRetain(rl);timeout_context->termTSR &#61; startTSR &#43; __CFTimeIntervalToTSR(seconds);/* 设置source的上下文 */dispatch_set_context(timeout_timer, timeout_context); // source gets ownership of context/* 设置时间事件的回调函数&#xff0c;调用了CFRunLoopWakeUp, 这个函数向等待端口发送了一个消息, 用来激活循环 */dispatch_source_set_event_handler_f(timeout_timer, __CFRunLoopTimeout);/* 设置时间事件的取消函数 */dispatch_source_set_cancel_handler_f(timeout_timer, __CFRunLoopTimeoutCancel);/* 计算触发的时间点(纳秒) */uint64_t ns_at &#61; (uint64_t)((__CFTSRToTimeInterval(startTSR) &#43; seconds) * 1000000000ULL);/* 设置source的timer, 仅仅延迟执行一次 */dispatch_source_set_timer(timeout_timer, dispatch_time(1, ns_at)/* start */, DISPATCH_TIME_FOREVER/* interval */, 1000ULL); /* 只执行一次 *//* 唤醒source */dispatch_resume(timeout_timer);} else { // infinite timeoutseconds &#61; 9999999999.0;timeout_context->termTSR &#61; UINT64_MAX;}Boolean didDispatchPortLastTime &#61; true;int32_t retVal &#61; 0;do {voucher_mach_msg_state_t voucherState &#61; VOUCHER_MACH_MSG_STATE_UNCHANGED;voucher_t voucherCopy &#61; NULL;/* 定义mach接收 msg 缓冲区 3k 大小 */uint8_t msg_buffer[3 * 1024]; /* 3K */mach_msg_header_t *msg &#61; NULL;mach_port_t livePort &#61; MACH_PORT_NULL;/* CFRunLoopModeRef->_portSet 实际上就是 mach_port_t -->>> __CFPort OR __CFPortSet */__CFPortSet waitSet &#61; rlm->_portSet; /* mac 下 就是 mach_port_t 等待端口集合 *//* 重置 CFRunLoopRef->_perRunData->ignoreWakeUps &#61; 0x57414B45; WAKUP 标识 */__CFRunLoopUnsetIgnoreWakeUps(rl);/* 回调 Before Timer && Source */if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);/* 回调block, 只能回到一次block&#xff1f; */__CFRunLoopDoBlocks(rl, rlm);/* source0 是否handled(回调) */Boolean sourceHandledThisLoop &#61; __CFRunLoopDoSources0(rl, rlm, stopAfterHandle/* __CFRunLoopRun 参数 */); //source 0 必须signedif (sourceHandledThisLoop) {/* 回调了Source0 */__CFRunLoopDoBlocks(rl, rlm); //source 0}/* 确定端口集合接收消息是否设置永远超时&#xff0c;如果poll为真&#xff0c;则立即返回&#xff0c;否则无限等待直到有消息过来 */Boolean poll &#61; sourceHandledThisLoop || (0ULL &#61;&#61; timeout_context->termTSR); /* 从CFRunLoopRun 过来的话 timeout_context->termTSR 永远不等于0*/if (MACH_PORT_NULL !&#61; dispatchPort && !didDispatchPortLastTime) {/* 首次循环不执行&#xff0c;因为外部定义 didDispatchPortLastTime 为 true */msg &#61; (mach_msg_header_t *)msg_buffer;if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0/*timeout &#61;&#61; 0*/, &voucherState, NULL)) {/* 主队列接收消息,接收消息成功则跳转到处理消息过程source1 */goto handle_msg;}}/* 执行到这里意味着 dispatchPort 没有成功接收到 mach message */didDispatchPortLastTime &#61; false; /* do while 循环外定义&#xff0c;初始化值 true *//* 没有回调并且timerTSR 大于 0 的情况下 并且 有等待掩码&#xff0c; poll 定义为 -> source0 处理了 或者 定义唤醒时间为0 */if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);/* 设置CFRunLoopRef 睡眠标志位。因为后面有进行一个 mach_msg 的等待操作 */__CFRunLoopSetSleeping(rl);__CFPortSetInsert(dispatchPort, waitSet); /* 将主端口 插入等待集合中 */ __CFRunLoopModeUnlock(rlm);__CFRunLoopUnlock(rl);CFAbsoluteTime sleepStart &#61; poll ? 0.0 : CFAbsoluteTimeGetCurrent();if (kCFUseCollectableAllocator) {memset(msg_buffer, 0, sizeof(msg_buffer));}msg &#61; (mach_msg_header_t *)msg_buffer;__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);__CFRunLoopLock(rl);__CFRunLoopModeLock(rlm);rl->_sleepTime &#43;&#61; (poll ? 0.0 : (CFAbsoluteTimeGetCurrent() - sleepStart));/* 从端口集合中移除 disaptchPort */__CFPortSetRemove(dispatchPort, waitSet); __CFRunLoopSetIgnoreWakeUps(rl);// user callouts now OK again __CFRunLoopUnsetSleeping(rl); //重置睡眠状态/* pool代表&#xff1a; 回调过 或者 触发时间为0&#xff0c; 根据掩码进行回调 */if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);/* 处理 mach 消息成功的标签*/handle_msg:;__CFRunLoopSetIgnoreWakeUps(rl);if (MACH_PORT_NULL &#61;&#61; livePort) {CFRUNLOOP_WAKEUP_FOR_NOTHING();} else if (livePort &#61;&#61; rl->_wakeUpPort) {CFRUNLOOP_WAKEUP_FOR_WAKEUP();} else if (rlm->_timerPort !&#61; MACH_PORT_NULL && livePort &#61;&#61; rlm->_timerPort) {CFRUNLOOP_WAKEUP_FOR_TIMER();if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {// Re-arm the next timer__CFArmNextTimerInMode(rlm, rl);}} else if (livePort &#61;&#61; dispatchPort) {CFRUNLOOP_WAKEUP_FOR_DISPATCH();__CFRunLoopModeUnlock(rlm);__CFRunLoopUnlock(rl);_CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)6, NULL);__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);_CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)0, NULL);__CFRunLoopLock(rl);__CFRunLoopModeLock(rlm);sourceHandledThisLoop &#61; true;didDispatchPortLastTime &#61; true;} else {/* 接收端口不为空&#xff0c;并且没有满足 timer、主队列端口情况下&#xff0c;处理 source1 */CFRUNLOOP_WAKEUP_FOR_SOURCE();voucher_t previousVoucher &#61; _CFSetTSD(__CFTSDKeyMachMessageHasVoucher, (void *)voucherCopy, os_release);// Despite the name, this works for windows handles as wellCFRunLoopSourceRef rls &#61; __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);if (rls) {mach_msg_header_t *reply &#61; NULL;/* source1 出现了 */sourceHandledThisLoop &#61; __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;if (NULL !&#61; reply) {/* 收到消息后&#xff0c;返回一个消息体&#xff0c;通知内核响应&#xff1f;&#xff1f;&#xff1f;&#xff1f; */(void)mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL);CFAllocatorDeallocate(kCFAllocatorSystemDefault, reply);}}// Restore the previous voucher_CFSetTSD(__CFTSDKeyMachMessageHasVoucher, previousVoucher, os_release);} if (msg && msg !&#61; (mach_msg_header_t *)msg_buffer) free(msg); __CFRunLoopDoBlocks(rl, rlm);if (sourceHandledThisLoop && stopAfterHandle/* CFRunLoopRun ->false */) {retVal &#61; kCFRunLoopRunHandledSource;} else if (timeout_context->termTSR _stopped) {rlm->_stopped &#61; false;retVal &#61; kCFRunLoopRunStopped;} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {retVal &#61; kCFRunLoopRunFinished;} voucher_mach_msg_revert(voucherState);os_release(voucherCopy);} while (0 &#61;&#61; retVal); // 循环结束&#xff0c; retVal 等于 0/* 释放超时timer */if (timeout_timer) {dispatch_source_cancel(timeout_timer);dispatch_release(timeout_timer);} else {free(timeout_context);}return retVal;
} //__CFRunLoopRun

  

 

 

转:https://www.cnblogs.com/chengsh/p/8629605.html



推荐阅读
  • 重入锁(ReentrantLock)学习及实现原理
    本文介绍了重入锁(ReentrantLock)的学习及实现原理。在学习synchronized的基础上,重入锁提供了更多的灵活性和功能。文章详细介绍了重入锁的特性、使用方法和实现原理,并提供了类图和测试代码供读者参考。重入锁支持重入和公平与非公平两种实现方式,通过对比和分析,读者可以更好地理解和应用重入锁。 ... [详细]
  • 深入理解Kafka服务端请求队列中请求的处理
    本文深入分析了Kafka服务端请求队列中请求的处理过程,详细介绍了请求的封装和放入请求队列的过程,以及处理请求的线程池的创建和容量设置。通过场景分析、图示说明和源码分析,帮助读者更好地理解Kafka服务端的工作原理。 ... [详细]
  • 本文分享了一个关于在C#中使用异步代码的问题,作者在控制台中运行时代码正常工作,但在Windows窗体中却无法正常工作。作者尝试搜索局域网上的主机,但在窗体中计数器没有减少。文章提供了相关的代码和解决思路。 ... [详细]
  • 李逍遥寻找仙药的迷阵之旅
    本文讲述了少年李逍遥为了救治婶婶的病情,前往仙灵岛寻找仙药的故事。他需要穿越一个由M×N个方格组成的迷阵,有些方格内有怪物,有些方格是安全的。李逍遥需要避开有怪物的方格,并经过最少的方格,找到仙药。在寻找的过程中,他还会遇到神秘人物。本文提供了一个迷阵样例及李逍遥找到仙药的路线。 ... [详细]
  • java线程池的实现原理源码分析
    这篇文章主要介绍“java线程池的实现原理源码分析”,在日常操作中,相信很多人在java线程池的实现原理源码分析问题上存在疑惑,小编查阅了各式资 ... [详细]
  • 本文讨论了如何优化解决hdu 1003 java题目的动态规划方法,通过分析加法规则和最大和的性质,提出了一种优化的思路。具体方法是,当从1加到n为负时,即sum(1,n)sum(n,s),可以继续加法计算。同时,还考虑了两种特殊情况:都是负数的情况和有0的情况。最后,通过使用Scanner类来获取输入数据。 ... [详细]
  • 本文讨论了使用差分约束系统求解House Man跳跃问题的思路与方法。给定一组不同高度,要求从最低点跳跃到最高点,每次跳跃的距离不超过D,并且不能改变给定的顺序。通过建立差分约束系统,将问题转化为图的建立和查询距离的问题。文章详细介绍了建立约束条件的方法,并使用SPFA算法判环并输出结果。同时还讨论了建边方向和跳跃顺序的关系。 ... [详细]
  • 本文介绍了解决二叉树层序创建问题的方法。通过使用队列结构体和二叉树结构体,实现了入队和出队操作,并提供了判断队列是否为空的函数。详细介绍了解决该问题的步骤和流程。 ... [详细]
  • 本文详细介绍了Java中vector的使用方法和相关知识,包括vector类的功能、构造方法和使用注意事项。通过使用vector类,可以方便地实现动态数组的功能,并且可以随意插入不同类型的对象,进行查找、插入和删除操作。这篇文章对于需要频繁进行查找、插入和删除操作的情况下,使用vector类是一个很好的选择。 ... [详细]
  • 本文讨论了在openwrt-17.01版本中,mt7628设备上初始化启动时eth0的mac地址总是随机生成的问题。每次随机生成的eth0的mac地址都会写到/sys/class/net/eth0/address目录下,而openwrt-17.01原版的SDK会根据随机生成的eth0的mac地址再生成eth0.1、eth0.2等,生成后的mac地址会保存在/etc/config/network下。 ... [详细]
  • 本文介绍了在Android开发中使用软引用和弱引用的应用。如果一个对象只具有软引用,那么只有在内存不够的情况下才会被回收,可以用来实现内存敏感的高速缓存;而如果一个对象只具有弱引用,不管内存是否足够,都会被垃圾回收器回收。软引用和弱引用还可以与引用队列联合使用,当被引用的对象被回收时,会将引用加入到关联的引用队列中。软引用和弱引用的根本区别在于生命周期的长短,弱引用的对象可能随时被回收,而软引用的对象只有在内存不够时才会被回收。 ... [详细]
  • 本文介绍了一道经典的状态压缩题目——关灯问题2,并提供了解决该问题的算法思路。通过使用二进制表示灯的状态,并枚举所有可能的状态,可以求解出最少按按钮的次数,从而将所有灯关掉。本文还对状压和位运算进行了解释,并指出了该方法的适用性和局限性。 ... [详细]
  • 广度优先遍历(BFS)算法的概述、代码实现和应用
    本文介绍了广度优先遍历(BFS)算法的概述、邻接矩阵和邻接表的代码实现,并讨论了BFS在求解最短路径或最短步数问题上的应用。以LeetCode中的934.最短的桥为例,详细阐述了BFS的具体思路和代码实现。最后,推荐了一些相关的BFS算法题目供大家练习。 ... [详细]
  • 在开发中,有时候一个业务上要求的原子操作不仅仅包括数据库,还可能涉及外部接口或者消息队列。此时,传统的数据库事务无法满足需求。本文介绍了Java中如何利用java.lang.Runtime.addShutdownHook方法来保证业务线程的完整性。通过添加钩子,在程序退出时触发钩子,可以执行一些操作,如循环检查某个线程的状态,直到业务线程正常退出,再结束钩子程序。例子程序展示了如何利用钩子来保证业务线程的完整性。 ... [详细]
  • A-Score UVA-1585水 ... [详细]
author-avatar
ahhylwjj
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有