一、GCD Timer的创建和安放

尽管GCD Timer并不依赖于NSRunLoop,可是有没有可能在某种情况下,GCD Timer也失效了?就好比一開始我们也不知道NSTimer相应着一个runloop的某种mode。

先来看看GCD Timer的用法:

dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, aQueue);

dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, ti * NSEC_PER_SEC, ti * 0.1 * NSEC_PER_SEC);

dispatch_source_set_event_handler(timer, ^{
//...
}); dispatch_resume(timer);

考虑到NSTimer作为timerSource被放到一个runloop的某种mode所相应的集合中,那么我们自然而然会联想GCD Timer作为dispatch_source_t被放到哪里呢?

參考libdispatch的源代码dispatch_source_create这个API为一个dispatch_source_t类型的结构体ds做了分配内存和初始化操作。然后将其返回。

摘取当中代码片段来看:

    ds = _dispatch_alloc(DISPATCH_VTABLE(source),
sizeof(struct dispatch_source_s));
// Initialize as a queue first, then override some settings below.
_dispatch_queue_init((dispatch_queue_t)ds);
ds->dq_label = "source"; ds->do_ref_cnt++; // the reference the manager queue holds
ds->do_ref_cnt++; // since source is created suspended
ds->do_suspend_cnt = DISPATCH_OBJECT_SUSPEND_INTERVAL;
// The initial target queue is the manager queue, in order to get
// the source installed. <rdar://problem/8928171>
ds->do_targetq = &_dispatch_mgr_q;

从以上代码片段中能够得到几个信息:

  1. 在命名方面,dispatch_source_t变量命名为ds,从而能够判断dq_label成员应该是属于dispatch_queue_t的,而do_ref_cnt应该相应着dispatch_object_t这么一个类型,ref_cnt引用计数则显然是用来管理“对象”的生命周期;
  2. 考虑到出现了dispatch_object_t这么一个类型,我们能够自然而然地猜想dispatch_系列的结构体应该都“继承自”dispatch_object_t。尽管C语言中没有面向对象编程中的继承这个概念,但仅仅要将dispatch_object_t结构体放在内存布局的開始处(作为“基类”)。则实现了继承的概念。另外一个样例是Python的C实现,详细能够參考Python源代码剖析一书;
  3. 从最后三行的凝视来看,默认初始化do_targetq为_dispatch_mgr_q。这是为了保证source被安装,所以能够初步得到一个dispatch_source_t的安放信息。须要注意的是_dispatch_mgr_q在GCD中是个非常重要的角色,从命名也能够看出基本是作为单例管理队列来进行调度分发的;
  4. 进一步证明了即便 dispatch_source_create这个API不传入queue參数。timer也能够有效工作,由于这个參数仅仅是用来表明回调在哪里运行,假设没有传入,回调则交于root queue来分发;当然,假设有传入queue參数,则会将该參数作为targetq。

二、libdispatch的基本结构关系

上面提到了“基类”的概念,这里先看下“基类”的布局:

#define DISPATCH_STRUCT_HEADER(x) \
_OS_OBJECT_HEADER( \
const struct dispatch_##x##_vtable_s *do_vtable, \
do_ref_cnt, \
do_xref_cnt); \
struct dispatch_##x##_s *volatile do_next; \
struct dispatch_queue_s *do_targetq; \
void *do_ctxt; \
void *do_finalizer; \
unsigned int volatile do_suspend_cnt; struct dispatch_object_s {
DISPATCH_STRUCT_HEADER(object);
};

从命名上来看,dispatch_系列的结构体都应该有这么一个Header部分。

也就是说在libdispatch中,非常多结构体都继承自上述基类:

struct dispatch_queue_s {
DISPATCH_STRUCT_HEADER(queue);
DISPATCH_QUEUE_HEADER;
//...省略部分代码
}; struct dispatch_semaphore_s {
DISPATCH_STRUCT_HEADER(semaphore);
//...省略部分代码
} struct dispatch_source_s {
DISPATCH_STRUCT_HEADER(source);
//...省略部分代码
}; //...省略其他继承演示样例

三、再看dispatch_source_t

当中,dispatch_source_t作为我们眼下的重点讨论对象,做一下延伸:

struct dispatch_source_s {
DISPATCH_STRUCT_HEADER(source);
DISPATCH_QUEUE_HEADER;
DISPATCH_SOURCE_HEADER(source);
unsigned long ds_ident_hack;
unsigned long ds_data;
unsigned long ds_pending_data;
};

除了开头的DISPATCH_STRUCT_HEADER,紧接着的是DISPATCH_QUEUE_HEADER,接下来才是DISPATCH_SOURCE_HEADER

也就是说,除了基类信息,一个dispatch_source_t还包括着queue的信息。而在DISPATCH_SOURCE_HEADER中,第一个成员例如以下:

#define DISPATCH_SOURCE_HEADER(refs) \
dispatch_kevent_t ds_dkev; \
//...省略部分代码 struct dispatch_kevent_s {
TAILQ_ENTRY(dispatch_kevent_s) dk_list;
TAILQ_HEAD(, dispatch_source_refs_s) dk_sources;
struct kevent64_s dk_kevent;
}; typedef struct dispatch_kevent_s *dispatch_kevent_t;

这个成员在dispatch_source_create方法中也会被初始化,以备用来兴许事件监听。

四、Timer类型dispatch_source_t的处理

以上讨论的基本是通用的dispatch_source_t相关处理。接下来讨论一个GCD Timer的真正处理流程,主要是dispatch_source_set_timer这个API:

void
dispatch_source_set_timer(dispatch_source_t source,
dispatch_time_t start,
uint64_t interval,
uint64_t leeway);

在这种方法中,会将定时器的相关信息封装在一个dispatch_set_timer_params结构体中作为上下文參数params,交由_dispatch_mgr_q来异步调用_dispatch_source_set_timer2方法:

// 不同版本号不一样。这里取了比較easy理解的版本号做演示样例
dispatch_barrier_async_f(&_dispatch_mgr_q, params, _dispatch_source_set_timer2);

这种方法也是作为GCD API暴露给开发人员的,在这种方法中做了进一步封装:

        // ...省略部分代码
dispatch_continuation_t dc = fastpath(_dispatch_continuation_alloc_cacheonly()); dc->do_vtable = (void *)(DISPATCH_OBJ_ASYNC_BIT | DISPATCH_OBJ_BARRIER_BIT);
dc->dc_func = func;
dc->dc_ctxt = context; _dispatch_queue_push(dq, dc);

这里将相关參数信息以及接下来要调用的方法名封装作为一个dispatch_continuation_t结构体。能够理解为一个队列任务块,然后push到队列中——这里的队列是_dispatch_mgr_q

到这里我们能够更清晰地了解到GCD内部是怎样对我们调用的API进行封装、进队,然后进一步分发运行。

五、熟悉又陌生的com.apple.libdispatch-manager

作为iOS开发,我们对com.apple.libdispatch-manager这个字符串应该非常熟悉,比方在crash日志中看过,也会在断点调试时遇到——它基本都是紧随在主线程之后。

这个字符串所相应的队列就是上文提到的_dispatch_mgr_q

static const struct dispatch_queue_vtable_s _dispatch_queue_mgr_vtable = {
.do_type = DISPATCH_QUEUE_MGR_TYPE,
.do_kind = "mgr-queue",
.do_invoke = _dispatch_mgr_invoke,
.do_debug = dispatch_queue_debug,
.do_probe = _dispatch_mgr_wakeup,
}; // 6618342 Contact the team that owns the Instrument DTrace probe before renaming this symbol
struct dispatch_queue_s _dispatch_mgr_q = {
.do_vtable = &_dispatch_queue_mgr_vtable,
.do_ref_cnt = DISPATCH_OBJECT_GLOBAL_REFCNT,
.do_xref_cnt = DISPATCH_OBJECT_GLOBAL_REFCNT,
.do_suspend_cnt = DISPATCH_OBJECT_SUSPEND_LOCK,
.do_targetq = &_dispatch_root_queues[DISPATCH_ROOT_QUEUE_COUNT - 1], .dq_label = "com.apple.libdispatch-manager",
.dq_width = 1,
.dq_serialnum = 2,
};

我们发现,就连_dispatch_mgr_q都有它相应的do_targetq,从命名上来看,能够初步判断_dispatch_mgr_q要做的事情终于都会丢到它的targetq上来完毕。

实际上,在libdispatch中,仅仅要有targetq,都会一层一层地往上扔。直到尽头。那么尽头在哪里呢?这里引用Concurrent Programming: APIs and Challenges里的一张图:

尽头在GCD的线程池。

六、GCD的尽头:root queue和线程池

回过头来看_dispatch_mgr_qdo_targetq,是_dispatch_root_queues中的最后一个元素。而root queue数组中按优先级升序排列:

// 老版本号libdispatch的代码,新版本号不同
static struct dispatch_queue_s _dispatch_root_queues[] = {
{
.do_vtable = &_dispatch_queue_root_vtable,
.do_ref_cnt = DISPATCH_OBJECT_GLOBAL_REFCNT,
.do_xref_cnt = DISPATCH_OBJECT_GLOBAL_REFCNT,
.do_suspend_cnt = DISPATCH_OBJECT_SUSPEND_LOCK,
.do_ctxt = &_dispatch_root_queue_contexts[0], .dq_label = "com.apple.root.low-priority",
.dq_running = 2,
.dq_width = UINT32_MAX,
.dq_serialnum = 4,
},
{
// ... 省略部分代码
.dq_label = "com.apple.root.low-overcommit-priority",
},
{
// ... 省略部分代码
.dq_label = "com.apple.root.default-priority",
},
{
// ... 省略部分代码
.dq_label = "com.apple.root.default-overcommit-priority",
},
{
// ... 省略部分代码
.dq_label = "com.apple.root.high-priority",
},
{
// ... 省略部分代码
.dq_label = "com.apple.root.high-overcommit-priority",
},
};

能够看到,在老版本号的libdispatch中,_dispatch_mgr_q是取最高优先级的root queue来作为do_targetq的。而在新版本号中,则是有专门为其服务的root queue:

static struct dispatch_queue_s _dispatch_mgr_root_queue = {
.do_vtable = DISPATCH_VTABLE(queue_root),
.do_ref_cnt = DISPATCH_OBJECT_GLOBAL_REFCNT,
.do_xref_cnt = DISPATCH_OBJECT_GLOBAL_REFCNT,
.do_suspend_cnt = DISPATCH_OBJECT_SUSPEND_LOCK,
.do_ctxt = &_dispatch_mgr_root_queue_context,
.dq_label = "com.apple.root.libdispatch-manager",
.dq_running = 2,
.dq_width = DISPATCH_QUEUE_WIDTH_MAX,
.dq_serialnum = 3,
}; static struct dispatch_queue_s _dispatch_mgr_root_queue = {
.do_vtable = DISPATCH_VTABLE(queue_root),
.do_ref_cnt = DISPATCH_OBJECT_GLOBAL_REFCNT,
.do_xref_cnt = DISPATCH_OBJECT_GLOBAL_REFCNT,
.do_suspend_cnt = DISPATCH_OBJECT_SUSPEND_LOCK,
.do_ctxt = &_dispatch_mgr_root_queue_context,
.dq_label = "com.apple.root.libdispatch-manager",
.dq_running = 2,
.dq_width = DISPATCH_QUEUE_WIDTH_MAX,
.dq_serialnum = 3,
};

只是不管是老版本号还是新版本号。_dispatch_mgr_qdo_targetq——最好还是称作_dispatch_mgr_root_queue——的VTABLE中,终于指向的方法都是_dispatch_queue_wakeup_global

// 老版本号
.do_probe = _dispatch_queue_wakeup_global, // 新版本号
unsigned long
_dispatch_root_queue_probe(dispatch_queue_t dq)
{
_dispatch_queue_wakeup_global(dq);
return false;
}

也就是说,当任务一层一层终于丢到root queue上,触发的是_dispatch_queue_wakeup_global这种方法。在这种方法中。则是线程池的相关维护,比方调用pthread_create创建线程来运行_dispatch_worker_thread方法。

到眼下为止,我们跳过了一些过程讨论到了GCD的线程池,接下来我们会先回过头来看怎样一步步走到线程的创建和运行的,再讨论线程创建后要运行些什么。

七、从任务安排到分发

我们在第四部分讨论到了_dispatch_queue_push(dq, dc);,将定时器相关信息以及下一步要调用的方法封装成dispatch_continuation_t结构放到队列_dispatch_mgr_q中。

那么,_dispatch_mgr_q是做什么的呢?能够先简单直接地看看它通常在做什么:

能够看到,它通常都是没事干等事来。先来看看它怎么处于等事干的状态,也就是它怎么被创建出来并初始化完毕的。

我们从上图调用栈能够看到线程入口是_dispatch_mgr_thread,它是作为_dispatch_mgr_q的.do_invoke的:

DISPATCH_VTABLE_SUBCLASS_INSTANCE(queue_mgr, queue,
.do_type = DISPATCH_QUEUE_MGR_TYPE,
.do_kind = "mgr-queue",
.do_invoke = _dispatch_mgr_thread,
.do_probe = _dispatch_mgr_queue_probe,
.do_debug = dispatch_queue_debug,
);

什么时候会触发.do_invoke调用呢?在整个libdispatch中,仅仅有在元素出队的时候才会触发:

static inline void
_dispatch_continuation_pop(dispatch_object_t dou)
{
dispatch_continuation_t dc = dou._dc, dc1;
dispatch_group_t dg; _dispatch_trace_continuation_pop(_dispatch_queue_get_current(), dou);
if (DISPATCH_OBJ_IS_VTABLE(dou._do)) {
return dx_invoke(dou._do);
}

那就是说_dispatch_mgr_q从root queue出队时会进入等事干的状态,那么它是什么时候进队的?当我们要push任务块进入队列时。会唤醒该队列并调用其.do_probe成员,而_dispatch_mgr_q相应的.do_probe_dispatch_mgr_wakeup

unsigned long
_dispatch_mgr_wakeup(dispatch_queue_t dq DISPATCH_UNUSED)
{
if (_dispatch_queue_get_current() == &_dispatch_mgr_q) {
return false;
} static const struct kevent64_s kev = {
.ident = 1,
.filter = EVFILT_USER,
.fflags = NOTE_TRIGGER,
}; #if DISPATCH_DEBUG && DISPATCH_MGR_QUEUE_DEBUG
_dispatch_debug("waking up the dispatch manager queue: %p", dq);
#endif _dispatch_kq_update(&kev); return false;
}

_dispatch_kq_update里面会做一次性的初始化:dispatch_once_f(&pred, NULL, _dispatch_kq_init);,当中有运行到:

_dispatch_queue_push(_dispatch_mgr_q.do_targetq, &_dispatch_mgr_q);

也就是将_dispatch_mgr_q进队并wakeup它的targetq。由于它的targetq是root queue。所以就会调用到_dispatch_queue_wakeup_global,就到了我们在第六部分讲的GCD尽头,创建或从线程池中获取一个线程来运行_dispatch_worker_thread

static void *
_dispatch_worker_thread(void *context)
{
dispatch_queue_t dq = context;
// ... 省略部分代码 const int64_t timeout = 5ull * NSEC_PER_SEC;
do {
_dispatch_root_queue_drain(dq);
} while (dispatch_semaphore_wait(&pqc->dpq_thread_mediator,
dispatch_time(0, timeout)) == 0); // ... 省略部分代码
return NULL;
}

在drain一个queue的过程,就是尽可能地将队列里面的任务块一个个出队,出队时就会触发出队元素的.do_invoke,相应于_dispatch_mgr_q就是_dispatch_mgr_thread

void
_dispatch_mgr_thread(dispatch_queue_t dq DISPATCH_UNUSED)
{
_dispatch_mgr_init();
// never returns, so burn bridges behind us & clear stack 2k ahead
_dispatch_clear_stack(2048);
_dispatch_mgr_invoke();
} static void
_dispatch_mgr_invoke(void)
{
static const struct timespec timeout_immediately = { 0, 0 };
struct kevent64_s kev;
bool poll;
int r; for (;;) {
_dispatch_mgr_queue_drain();
poll = _dispatch_mgr_timers();
if (slowpath(_dispatch_select_workaround)) {
poll = _dispatch_mgr_select(poll);
if (!poll) continue;
}
poll = poll || _dispatch_queue_class_probe(&_dispatch_mgr_q);
r = kevent64(_dispatch_kq, _dispatch_kevent_enable,
_dispatch_kevent_enable ? 1 : 0, &kev, 1, 0,
poll ? &timeout_immediately : NULL);
_dispatch_kevent_enable = NULL;
if (slowpath(r == -1)) {
int err = errno;
switch (err) {
case EINTR:
break;
case EBADF:
DISPATCH_CLIENT_CRASH("Do not close random Unix descriptors");
break;
default:
(void)dispatch_assume_zero(err);
break;
}
} else if (r) {
_dispatch_kevent_drain(&kev);
}
}
}

一旦进入_dispatch_mgr_invoke。这个线程就进入了等事干的状态。

八、GCD Timer到期时的任务分发

上面讲了_dispatch_mgr_q的初始化和工作过程。如今回过头来继续看GCD Timer的处理过程。

和第七部分开头一样:我们在第四部分讨论到了_dispatch_queue_push(dq, dc);,将定时器相关信息以及下一步要调用的方法封装成dispatch_continuation_t结构放到队列_dispatch_mgr_q中。

这时候我们push了任务块进入_dispatch_mgr_q,就会wakeup to drain,将任务块pop出来:

static inline void
_dispatch_continuation_pop(dispatch_object_t dou)
{
dispatch_continuation_t dc = dou._dc, dc1;
dispatch_group_t dg; _dispatch_trace_continuation_pop(_dispatch_queue_get_current(), dou);
if (DISPATCH_OBJ_IS_VTABLE(dou._do)) {
return dx_invoke(dou._do);
} // ... 省略部分代码
_dispatch_client_callout(dc->dc_ctxt, dc->dc_func);
// ... 省略部分代码
}

回头看下我们之前进队时封装的信息:

    dispatch_continuation_t dc = _dispatch_continuation_alloc_from_heap();

    dc->do_vtable = (void *)(DISPATCH_OBJ_ASYNC_BIT | DISPATCH_OBJ_BARRIER_BIT);
dc->dc_func = func;
dc->dc_ctxt = ctxt;

而在pop过程中的判断条件是if (DISPATCH_OBJ_IS_VTABLE(dou._do)),相关代码例如以下:

#define DISPATCH_OBJ_ASYNC_BIT  0x1
#define DISPATCH_OBJ_BARRIER_BIT 0x2
#define DISPATCH_OBJ_GROUP_BIT 0x4
// vtables are pointers far away from the low page in memory
#define DISPATCH_OBJ_IS_VTABLE(x) ((unsigned long)(x)->do_vtable > 127ul)

条件不满足。所以我们运行了方法调用,一步步先进入了_dispatch_source_set_timer2再进入_dispatch_source_set_timer3,然后更新timer链表:

// Updates the ordered list of timers based on next fire date for changes to ds.
// Should only be called from the context of _dispatch_mgr_q.
static void
_dispatch_timers_update(dispatch_source_t ds)

这里值得一提的是,假设定时器採用的是wall clock。那么会做下额外的处理:

    if (params->values.flags & DISPATCH_TIMER_WALL_CLOCK) {
_dispatch_mach_host_calendar_change_register();
}

当定时器到期时就会运行_dispatch_wakeup(ds),然后一路push & wakeup直到root queue。通常我们创建的queue所相应的targetq是default优先级的root queue,所以终于还是走到了_dispatch_queue_wakeup_global来分配线程运行drain queue的pop动作:

终于回调出去。

九、GCD Timer的失效性

讨论了那么多。那么GCD Timer是不是也有可能在某种情况下失效呢?

关于定时器的有效工作,有两个关键环节,一个是mgr queue。还有一个是root queue。

能够看到mgr queue仅仅是负责事件监听和分发,能够理解是非常轻量级的、不应该也不同意存在失效的;而root queue则负责从线程池分配线程运行任务。线程池的大小眼下来看是255,而且有高低优先级之分。

我们创建的GCD Timer的优先级是继承自它的targetq的,而我们正常创建的queue所相应的root queue优先级是default。所以说假设存在大量高优先级的任务派发。或者255个线程都卡住了。那么GCD Timer是会被影响到的。

从NSTimer的失效性谈起(二):关于GCD Timer和libdispatch的更多相关文章

  1. 二:多线程--GCD

    一.简单介绍 1.GCD全称是Grand Central Dispatch,可译为“牛逼的中枢调度器”,纯C语言,提供了非常多强大的函数 2.GCD的优势 GCD是苹果公司为多核的并行运算提出的解决方 ...

  2. iOS应用架构谈(二):View层的组织和调用方案(上)

    OS客户端应用架构看似简单,但实际上要考虑的事情不少.本文作者将以系列文章的形式来回答iOS应用架构中的种种问题,本文是其中的第二篇,主要讲View层的组织和调用方案.上篇主要讲View层的代码结构. ...

  3. Java随谈(二)对空指针异常的碎碎念

    本文适合对 Java 空指针痛彻心扉的人阅读,推荐阅读时间25分钟. 若有一些Java8 函数式编程的基础可以当成对基础知识的巩固. 一.万恶的null 今天,我们简单谈谈null的问题.因为null ...

  4. iOS应用架构谈(二):View层的组织和调用方案(中)

    iOS客户端应用架构看似简单,但实际上要考虑的事情不少.本文作者将以系列文章的形式来回答iOS应用架构中的种种问题,本文是其中的第二篇,主要讲View层的组织和调用方案.中篇主要讨论MVC.MVCS. ...

  5. Qt浅谈之二十七进程间通信之QtDBus

    一.简介 DBus的出现,使得Linux进程间通信更加便捷,不仅可以和用户空间应用程序进行通信,而且还可以和内核的程序进行通信,DBus使得Linux变得更加智能,更加具有交互性.        DB ...

  6. Qt浅谈之二十App自动重启及关闭子窗口

    一.简介 最近因项目需求,Qt程序一旦检测到错误,要重新启动,自己是每次关闭主窗口的所有子窗口但有些模态框会出现问题,因此从网上总结了一些知识点,以备以后的应用. 二.详解 1.Qt结构 int ma ...

  7. LDA-线性判别分析(二)

    本来是要调研 Latent Dirichlet Allocation 的那个 LDA 的, 没想到查到很多关于 Linear Discriminant Analysis 这个 LDA 的资料.初步看了 ...

  8. Qt浅谈之二十App自动重启及关闭子窗口(六种方法)

    一.简介 最近因项目需求,Qt程序一旦检测到错误,要重新启动,自己是每次关闭主窗口的所有子窗口但有些模态框会出现问题,因此从网上总结了一些知识点,以备以后的应用. 二.详解 1.Qt结构 int ma ...

  9. 浅谈Struts2(二)

    一.struts2的跳转 1.action跳转JSP a.默认为forward <action name="action1" class="com.liquidxu ...

随机推荐

  1. 大数据小视角5:探究SSD写放大的成因与解决思路

    笔者目前开发运维的存储系统的服务器都跑在SSD之上,目前单机服务器最大的SSD容量有4T之多.(公司好有钱,以前在实验室都只有机械硬盘用的~~)但SSD本身的特性与机械硬盘差距较大,虽然说在性能上有诸 ...

  2. OutputStreramWriter和InputStreamReader类

    整个IO类中除了字节流和字符流还包括字节和字符转换流. OutputStreramWriter将输出的字符流转化为字节流 InputStreamReader将输入的字节流转换为字符流 但是不管如何操作 ...

  3. 13,EasyNetQ-错误条件

    在本节中,我们将看看任何消息系统中可能出现的各种错误情况,并查看EasyNetQ如何处理它们. 1,我的订阅服务死亡 你已经写了一个订阅了我的NewCustomerMessage的windows服务. ...

  4. Outlook错误代码

    一般错误1 0x80004005 MISC The operation failed Virus Scanner Integration Issue Usually Related To Script ...

  5. Mac电脑 阿里云ECS(ContentOS) Apache+vsftpd+nodejs+mongodb建站过程总结

    简介:我这里采用的阿里云免费提供的6个月ECS服务器:制作了一个简单的爬虫程序:里面很多功能还么做:搜索里面功能回去的数据未做处理会崩溃(大家不要点搜索功能):地址:http://loldragon. ...

  6. BZOJ.3238.[AHOI2013]差异(后缀自动机 树形DP/后缀数组 单调栈)

    题目链接 \(Description\) \(Solution\) len(Ti)+len(Tj)可以直接算出来,每个小于n的长度会被计算n-1次. \[\sum_{i=1}^n\sum_{j=i+1 ...

  7. pwcrack--一款集合多种md5解密的工具

    项目开源地址:https://github.com/L-codes/pwcrack-framework Ruby2.5+ (tested with Ruby2.5.3 & Ruby 2.6.3 ...

  8. 潭州课堂25班:Ph201805201 WEB 之 页面编写 第一课 (课堂笔记)

    index.html <!DOCTYPE html> <html lang="en"> <head> <meta charset=&quo ...

  9. 走进java

    Java 技术体系 1.java技术语言 2.各种硬件平台上的java虚拟机 3.Class文件格式 4.Java API类库 5.来自商业机构和开源社区的第三方Java类库 我们把Java程序设计语 ...

  10. 流程控制语句 if

    格式: if 条件: 结果 第一种: >: print() 第二种: <: print() else: print() 第三种: num = input("请输入你猜的数字:&q ...