[{"content":"这篇文章回顾了HTTP协议的发展历程，从HTTP/0.9到HTTP/3，强调了其在性能和实时性方面的改进，同时提到了服务器系统性能指标，如QPS、TPS、并发数和RT，讨论了Nginx的流行原因，还列出了一些重要的网络调试命令，以及集线器、交换机和路由器在网络中的作用。\n超文本传输协议(HTTP)历经多年发展，已经从简单的文本传输演变成了为满足现代应用需求的高性能注重实时体验的协议。\nHTTP 演进历程 HTTP 的发展历程如下\nHTTP/0.9 设置用于通过单个 GET 获取简单的 HTML文档 HTTP/1.0 增加了头部信息和状态码以支持更丰富的交互，但每个请求任需要建立新的链接 (每次请求结束后关闭链接，再次请求时创建新链接) HTTP/1.1 引入了持久链接和更多的方法支持，让日常网页浏览变得更快、更高效 HTTP/2 通过多路复用技术解决了性能瓶颈，允许对个请求共享一个链接 HTTP/3 转向了基于 UDP 的 QUIC 协议，以降低延迟并提高可靠性，尤其是适用于移动和实时应用(移动的网络环境更加复杂) 系统性能指标 你的 API 运行缓慢。但究竟慢到什么程度？你需要具体数据。真实的指标能告诉你哪里出了问题，以及该从何处着手修复。\nQPS(Queries Per Second) 每秒请求数，指的是系统每秒处理的请求数量，你的服务器一秒收到 1000 个请求，那服务器的 QPS 就是 1000。 TPS(Transactions Per Second) 每秒事务数，指的是系统每秒能够处理完成交易的数量，一次交易包含完整的往返流程，即请求发出、访问数据库并返回响应。TPS反应的是实际完成的工作量而不仅仅是接收到的请求数。 Concurrency 并发数，指的是系统在任一给定时刻同时处理的活跃请求数量，即使每秒有 100 个请求，但是如果每个请求需要 5s 才能完成，那么实际上同时处理的并发请求数高达 500 个。高并发意味着需要更多的资源、更优的连接池管理以及更加智能的线程调度。 RT 响应时间，指的是从请求开始到收到响应所需要经过的时间，在客户端层面和服务器层面均可测量。 一个简单的公式将他们联系在一起： QPS = 并发数 / 平均响应时间 更高的并发数或者更低的响应时间 = 更高的吞吐量\nNginx 为什么如此流行 Nginx受欢迎原因如下:\n高性能服务器 反向代理与负载均衡器 缓存层 SSL 终端卸载 应该掌握的网络调试命令 帮助故障定位\nping：检查目标是否响应，并报告往返时间以确认基本可达性。 traceroute / tracert：显示路径上的每一跳，以便您查看数据包在何处变慢或停止。 mtr / pathping：持续测量每一跳的延迟和丢包，以发现间歇性问题。 ip addr, ip link / ipconfig /all：打印本地 IP 地址、MAC 地址和接口状态，以便您验证机器的网络身份。 ip route：显示路由表，以确认系统将使用哪个网关和下一跳。 ip neigh：显示 IP 到 MAC 的条目，以检测局域网中的重复或过时 ARP 记录。 ss -tulpn：列出监听套接字和进程 ID，以便确认服务是否确实绑定到预期端口。 dig：解析 DNS 记录以验证客户端将连接的确切 IP 地址。 curl -I：仅获取 HTTP(S)头信息，以检查状态码、重定向和缓存设置。 tcpdump / tshark：捕获数据包，以便您能检查真实流量，并验证发送与接收的内容。 iperf3：测量两台主机之间的端到端吞吐量，以区分带宽限制与应用问题。 ssh：在远程机器上打开一个安全外壳，以便直接运行检查并应用修复。 sftp：安全地传输文件，以便在事件期间拉取日志或推送工件。 nmap：扫描开放端口并探测版本，以确认哪些服务已暴露并正在响应。 上述命令有些 Mac 需要单独安装 brew 包\n集线器、交换机与路由 每个家庭或者办公网络都离不开这三种设备，集线器、交换机和路由器，但它们的角色常常被混淆。\n集线器工作在物理层（第一层）。它是三者中最简单的设备，无法识别地址或数据类型。当数据包到达时，它只是简单地将其广播给所有连接的设备，形成一个大的冲突域。这意味着所有设备会竞争相同的带宽，使得集线器在现代网络中效率低下。 交换机工作在第二层（数据链路层）。它能够知道MAC 地址，并将数据帧仅转发至正确的目标设备。交换机的每个端口都构成独立的冲突域，从而提升效率并加速局域网内的通信。 路由器工作在网络层（第三层）。它根据 IP 地址路由数据包，并将不同网络连接在一起，例如将家庭网络连接到互联网。每个路由器接口构成一个独立的广播域，从而隔离本地流量和外部流量。 ","permalink":"https://akashark.github.io/en/posts/tech/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/ep194-evolution-of-http%E7%BF%BB%E8%AF%91/","summary":"\u003cp\u003e这篇文章回顾了HTTP协议的发展历程，从HTTP/0.9到HTTP/3，强调了其在性能和实时性方面的改进，同时提到了服务器系统性能指标，如QPS、TPS、并发数和RT，讨论了Nginx的流行原因，还列出了一些重要的网络调试命令，以及集线器、交换机和路由器在网络中的作用。\u003c/p\u003e","title":"EP194: Evolution of HTTP[翻译]"},{"content":"背景 在App的开发过程中经常会遇到与其他App联动相互拉起的需求(也就是拉起其他App或者被其他App拉起)，那么在iOS中这种拉起操作应该如何实现呢，本文将主要介绍以下几个方面\n如何实现App拉起 如何实现App之间的相互拉起 背景知识 这里提前补充需要了解的知识点，下面是url的基本格式 protocol://hostname[:port]/path/[;parameters][?query]#fragment\nprotocol, 协议名称也叫做scheme，例如http、https当然这个地方也可以自定义 hostname：域名或ip地址，例如 www.baidu.com 就是域名；110.242.68.3就是IP地址。 port：端口号，例如 www.baidu.com:80后面的80就是端口号，80是默认端口号，一般不显示。 path：路径，表示主机上的目录或文件路径。例如：fanyi.baidu.com/translate。 query：可选项，用于传递参数，由?符号开始，\u0026amp;符号隔开，参数名和值用=符号相连。例如：www.baidu.com/s?ie=utf-8\u0026amp;… 在使用App拉起的过程中需要使用到url的基本格式定义，在拉起的过程中定义的url格式如下 customAppScheme://xxxxx/path?params=xxx App拉起 在iOS的系统中从A App打开B App，需要A App去找到B App，这就和我们邮寄信件一样我们需要写一个我们信件的目标地址，在系统中我们可以通过UIApplication中的API来完成这件事\n1 2 3 4 5 6 /** 通过应用程序打开一个资源路径 @param url 资源路径的地址 @return 返回成功失败的信息 */ - (BOOL)openURL:(NSURL*)url; openURL:通过url来标识我们要跳转的App的位置，一个简单的例子\n1 2 3 4 5 6 7 // 跳转到打电话 NSURL *url = [NSURL URLWithString:@\u0026#34;tel://10086\u0026#34;]; [[UIApplication sharedApplication] openURL:url]; //发送系统短信 NSURL *url = [NSURL URLWithString:@\u0026#34;sms://1383838438\u0026#34;]; [[UIApplication sharedApplication] openURL:url]; 从上面的例子可以产出来拉起其他App核心关键点在url，以上面的打电话为例url的定义为tel://10086结合上面的url格式定义，Scheme为tel，host为10086，可以才到10086就是要打的电话，那tel自然就是标识这打电话App，tel这个Scheme在系统的认知中就是标识着电话App，同样的如果我想要打开其他App只需要知道他的Scheme自然就可以打开该App。\nApp之间相互拉起 注册URL Scheme 根据上面的App拉起可以得出只要知道了App的scheme就可以拉起这个App，那么如果我们有两个App A和B，现在要求我用A App打开B App，自然解决方法就是在A中open B的scheme就可以了，可是B的scheme是什么呢，这就需要我们自己来定义啦。\n在Xcode的工程配置中点击info条目，在info下找到URL Types点击添加\n填写其中identifier和URL Schemes\nidentifier: URL Identifier是自定义的 URL scheme 的名字，一般采用反转域名的方法保证该名字的唯一性 URL Schemes: 为自己的app定义的schemes。使用另外的app调起自己的app时，使用这个参数 identifier是可选的，定义identifier后会变成url的host，scheme会变成url的protocol， 这么看来定义的时候identifier变成了打开url的host，scheme变成了协议，也就是说url的格式为 URL Scheme://URL identifier scheme 定义需要注意 url scheme必须能唯一标识一个App，如果你设置的scheme和其他的scheme相同，那么默认的表现是未知的 URL Scheme的命名应该是只能纯英文字符，而不能含有下划线或者数字 一个App可以同时拥有多个scheme\n通过上面的方式就定义好了Scheme啦，假设我们定义A App的scheme为customA B App的scheme为customB 当然实际上scheme对应的url protocol为customA://和customB://\n处理拉起请求 已经定义好了A和B的scheme，继续完成上面的需求，用A打开B，在A的任意位置我们调用\n1 2 NSURL *url = [NSURL URLWithString:@\u0026#34;customB://idetifierB\u0026#34;]; [[UIApplication sharedApplication] openURL:url]; 可以成功拉起B App那么如何将一些参数传递到B呢，如果使用的url为customB://idetifierB?params1=xx\u0026amp;params2=yy，B App如何获取到这参数。\n上面其实有提到过处理应用之间的跳转是通过UIApplication来完成的，所以可以在B App中通过重写- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary\u0026lt;UIApplicationOpenURLOptionsKey,id\u0026gt; *)options来获取到对应的请求来完成相应逻辑的处理。\n补充 注册URL Scheme 方式2 注册URL Scheme的方式还有一种，在Info.plist中间中右击添加URL types类型为Array,\n添加的每个item的内容，这和上面介绍的注册URL Scheme的方式类似。\n[[UIApplication sharedApplication] canOpenURL:url 使用 在调用[[UIApplication sharedApplication] openURL:url]前可以去判断下是否可以打开该应用[[UIApplication sharedApplication] canOpenURL:url，但是在iOS9后判断canOpenURL需要再info.plist文件中将目标Url定义进白名单，否则不能使用。 在Info.plist文件中添加LSApplicationQueriesSchemes字段，该字段对应的是数组类型，然后添加CustomA(要跳转的目标Scheme)\nLSApplicationQueriesSchemes对应的Array至多只能添加50个item，应该是Apple为了安全考虑不允许去扫描用户已经安装的App(可以跳转的前提是已经安装App且将scheme注册进系统)\n对于LSApplicationQueriesSchemes白名单的理解 Apple文档的介绍如下 说的是，在iOS9后，如果想要使用canOpenURL方法检查是否可以打这个URL或可以处理该URL的App，需要在info.plist里添加LSApplicationQueriesSchemes字段来预设url，否则是否安装都会返回NO。\n“白名单”的意义是要检查当前设备上是否安装了其他App，而不是打开其他App必须添加“白名单”，所以如果想要打开其他App，直接使用openURL即可。\n==canOpenURL：方法有限制，openURL:方法是没有限制的。==\n如何回跳 上面已经知道了如何跳转到另一个App中，完成了最开始说的从A App跳转到B App的需求，但是如果要求跳转到B App后完成调回App的操作呢，根据上面的介绍其实这个需求也很简单，B App条回A App重点是拿到A App的scheme即可，可以将A App的scheme在第一次跳转到B App的时候作为参数传递给B App这样在B App完成逻辑处理后就可以使用该Scheme完成回跳。\n具体来说就是 A-\u0026gt;B-\u0026gt;A\n首先要从B中的info.plist设置A注册的Scheme白名单 然后从A-\u0026gt;B的时候通过参数将A的scheme带过去 B处理完对应的业务后通过同样的方式返回A 其实在微信SDK中我们也是这样做的，在跳转到微信的url中添加一个backurl然后传递自己的App Scheme 微信执行完成后会拉起自己的App。\nRef iOS scheme跳转机制 关于iOS“白名单”的理解 ","permalink":"https://akashark.github.io/en/posts/tech/ios/%E5%B8%B8%E8%A7%81%E5%8A%9F%E8%83%BD/iosapp%E9%97%B4%E8%B7%B3%E8%BD%AC/","summary":"\u003ch2 id=\"背景\"\u003e背景\u003c/h2\u003e\n\u003cp\u003e在App的开发过程中经常会遇到与其他App联动相互拉起的需求(也就是拉起其他App或者被其他App拉起)，那么在iOS中这种拉起操作应该如何实现呢，本文将主要介绍以下几个方面\u003c/p\u003e","title":"iOS App间跳转"},{"content":"背景 早从19年 iOS 13开始Apple就建议我们将App中使用UIWebView的地方切换为WKWebView了。\nITMS-90809: Deprecated API Usage - Apple will stop accepting submissions of apps that use UIWebView APIs . See https://developer.apple.com/documentation/uikit/uiwebview for more information.\n按照Apple2019年12月13日的文档,20年4月，新应用必须使用WKWebView替代UIWebView，20年12月，应用更新必须使用WKWebView替换UIWebView。具体的时间点可以看下面这张图(有点像是找历史的感觉..) 不过好像Apple觉得可能这样太苛责了，或者商店改动的App比较少的原因，后面将这个时间放的宽松了些文档，会留给开发者更多的时间，后续具体的截止时间再通知，但是为了更多的时间兼容WKWebView，建议还是更早的替换，我们的App已经将内容展示的部分替换为了WKWebView，但是对于获取UserAgent的部分还使用的UIWebView的API，本篇文章就来讨论使用WKWebView获取并设置UA的实践。\n查找项目是否包含UIWebView的代码\n搜索项目代码，查找是否有UIWebView的代码 检查SDK是否使用UIWebView，在终端命令行下cd到项目目录，输入下面命令： grep -r UIWebView . 找到所有包含UIWebView的SDK UIWebView获取UA 1 2 3 UIWebView *webView = [[UIWebView alloc] initWithFrame:CGRectZero]; NSString *userAgent = [webView stringByEvaluatingJavaScriptFromString:@\u0026#34;navigator.userAgent\u0026#34;]; NSLog(@\u0026#34;userAgent :%@\u0026#34;, userAgent); WKWebView获取UA 1 2 3 4 WKWebView *webView = [[WKWebView alloc] initWithFrame:CGRectZero]; [wkWebView evaluateJavaScript:@\u0026#34;navigator.userAgent\u0026#34; completionHandler:^(id result, NSError *error) { NSLog(@\u0026#34;userAgent :%@\u0026#34;, result); }]; 如何修改UA 这种需求太常见了，App内嵌的H5页面有时候需要判断App的版本，App的类型等等信息，这些信息在H5页面初始化的时候作为参数，通过UA来进行获取，于是就有了App修改UA的操作，当然这样的修改大部分是在默认的UA上添加所需要的元素。\n网上常见的一种最佳实践方式是通过UIWebView获取UA并修改，然后通过WKWebView来进行页面的真实加载，这种的不符合我们说的将UIWebView全部替换的思路，所以这里不展开研究，这是提一下这一种方式确实不错，UIWebView获取UA是同步的，这样获取修改比较方便，同时也不影响WKWebView加载主题内容，但是使用UIWebView的API不知道啥时候就可能被Apple封杀\u0026hellip;\n通过UserDefaults修改 由于WKWebView有一个特性，在初始化时会获取UserDefaults中“UserAgent”这个key的值，这需要我们在真正使用的WKWebView之前要创建一个WKWebView获取它默认的UA\n1 2 3 4 5 6 7 8 9 webView = WKWebView(); webView?.evaluateJavaScript(\u0026#34;navigator.userAgent\u0026#34;, completionHandler: { (obj: Any?, error: Error?) in guard let ua = obj as? String else { return } let customUA = \u0026#34;\\(ua) Custom User Agent\u0026#34; UserDefaults.standard.register(defaults: [\u0026#34;UserAgent\u0026#34;: customUA]) UserDefaults.standard.synchronize() }) 这种方式在特定版本会有问题 后面会具体说下这个问题\n通过WKWebView.customUserAgent设置 这种方式其实和第一种方式并没有什么差别，而且还比较复杂，因为只对这一个WebView有效\n1 2 3 4 5 6 7 8 9 10 11 12 let fakeWebView = WKWebView(); fakeWebView.evaluateJavaScript(\u0026#34;navigator.userAgent\u0026#34;, completionHandler: { (obj: Any?, error: Error?) in guard let ua = obj as? String else { return } let customUA = \u0026#34;\\(ua) Custom User Agent\u0026#34; let realWebView = WKWebView() realWebView.customUserAgent = customUA }) let oldUserAgent = webView.value(forKey: \u0026#34;userAgent\u0026#34;) as? String ?? \u0026#34;\u0026#34; webView.customUserAgent = \u0026#34;\\(oldUserAgent) xxx\u0026#34; 这里获取UA的webView和最终加载内容的WebView并不是同一个后面会说明原因\n通过applicationNameForUserAgent设置 config的此属性与上一个属性(webview.customUserAgent)不同，不是将设置的字符串完全变成你所设置的值 它将字符串集添加到WebView的默认UserAgent并执行它。 这正好就是我们想要的，您所要做的就是设置要添加的字符串。 修改后的UserAgent，如：Mozilla/5.0 (iPhone; CPU iPhone OS 13_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Custom UserAgent 1 2 3 let config = WKWebViewConfiguration() config.applicationNameForUserAgent = \u0026#34;Custom User Agent\u0026#34; let webview = WKWebView(frame: .zero, configuration: config) 还可以在WebView的base类里面添加\n1 2 3 NSString *userAgent = [self valueForKey:@\u0026#34;applicationNameForUserAgent\u0026#34;]; userAgent = [userAgent stringByAppendingString:[NSString stringWithFormat:@\u0026#34;/myapp/%@\u0026#34;, CURRENTAPPVERSION]]; [self setValue:userAgent forKey:@\u0026#34;applicationNameForUserAgent\u0026#34;]; 在base类的初始化中添加上面的代码，通过applicationNameForUserAgent来获取的userAgent是不包含Mozilla/5.0 (iPhone; CPU iPhone OS 9_3 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko)的这是默认的一段，此时userAgent打印为Mobile/13E230，而H5获取到的UA打印出来为完整的Mozilla/5.0 (iPhone; CPU iPhone OS 9_3 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Mobile/13E230/myapp/1.0.0 但这个API是私有的使用的话要小心一些\n以上三种方式系统采用的优先级\n1 customUserAgent \u0026gt; UserDefault \u0026gt; applicationNameForUserAgent 左侧优先级高于右侧 如果设置了customUserAgent或UserDefaults方法，则applicationNameForUserAgent将被忽略。 applicationNameForUserAgent仅添加到了webview具有的默认UserAgent中。(applicationNameForUserAgent被赋得值就是UA自定义的部分) Xcode15 iOS 17 遇到的问题 上面的通过NSUserDefault的方式修改UA的在iOS17，Xcode15打包的版本上用不啦，会发现这样设置之后前端获取到的UA并不包含我们自定义的部分，下一篇文章将会针对这个问题深入源码进行详细的分析，这里先知道这个结论就好啦，可以参考这篇文章 遇到这个问题，我们就可以使用其他两种方式来设置修改UA。\n注意事项 修改UA的步骤一定要在loadRequest之前，不然的话可能会导致修改了但是第一次不生效的情况(使用WKWebView是异步的操作，一定要把这部分考虑进去) 获取并修改userAgent的webView对象跟加载网页的webView不是同一个对象 不然的话也会出现第一次设置不生效的情况，可以参考这片文章 参考了下网上的文章这个问题出现貌似是在iOS8以后，执行evaluateJavaScript后就设置不上了 WKWebView的创建以及执行JS的方法evaluateJavaScript都必须在主线程操作 WKWebView的渲染是在其他线程中，需要回调到主线程中进行相应的操作 Ref iOS修改WebView的UserAgent\nNSUserDefaults中的registerDefaults\n记使用WKWebView修改user-agent在iOS 12踩的一个坑\nWKWebView iOS17设置UserAgent\nUserAgent cannot be changed from UserDefaults only iOS 17 Device using Xcode 15\niOS 设置userAgent\n","permalink":"https://akashark.github.io/en/posts/tech/ios/webview/useragent%E8%8E%B7%E5%8F%96%E4%B8%8E%E4%BF%AE%E6%94%B9/","summary":"\u003ch2 id=\"背景\"\u003e背景\u003c/h2\u003e\n\u003cp\u003e早从19年 iOS 13开始Apple就建议我们将App中使用UIWebView的地方切换为WKWebView了。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eITMS-90809: Deprecated API Usage - Apple will stop accepting submissions of apps that use UIWebView APIs . See \u003ca href=\"https://developer.apple.com/documentation/uikit/uiwebview\"\u003ehttps://developer.apple.com/documentation/uikit/uiwebview\u003c/a\u003e for more information.\u003c/p\u003e","title":"UserAgent获取与修改"},{"content":"继续前面的整理，这将是最后一部分的整理内容，包括了goroutine channel的使用以及Go反射的使用。\nGo goroutine channel 进程 线程 并行 并发 进程 线程 进程就是程序在操作系统中的一次执行过程，是系统进行资源分配和调度的基本单位，进程是一个动态概念，是程序在执行过程中分配和管理资源的基本单位，每一个进程都有自己的地址空间，一个进程中至少有5个基本状态，初始态、执行态、等待状态、就绪状态、终止状态。通俗的讲进程就是一个正在执行的程序。\n线程是进程的一个执行实例，是程序执行的最小单元，它是比进程更小的能独立运行的基本单位，一个进程可以创建多个线程，同一个进程中的多个线程可以并发执行，一个程序要运行的话至少有一个进程\n并发 并行 并发 多个线程同时竞争一个位置，竞争到的才可以执行，同一个时间段内只有一个线程在执行 并行 多个线程可以同时执行，每个时间段内，可以有多个线程同时执行 通俗的讲多线程程序在单核CPU上面运行就是并发，多线程程序在多核CPU上运行就是并行，如果线程数大于CPU数，则多线程程序在多核CPU上面运行既有并行又有并发。\n区别在于同一时刻是否有多个任务正在被执行\nGo中的协程(goroutine)以及主线程 Go主线程，在主线程上可以起多个协程，Go中多协程可以实现并行或并发。 协程，可以理解为用户级别的线程，这是对于内核透明的，也就是系统并不知道有协程的存在，是完全由用户自己的程序进行调度的，Go的一个特色就是从语言层面原生支持协程，在函数或者方法前面加go关键字就可以创建一个协程，可以说go中的写成就是goroutine。\n多协程与多线程，Go中每个Goroutine默认占用内存远比java、c中的线程少的多，OS线程一般都固定了栈内存(2m)，一个Goroutine占用的内存非常小只有2k左右，多协程Goroutine切换调度开销方面远比线程少的多，这也就是go更加适合并发场景的原因。\n多协程切换调度开销远比线程要少\nGoroutine的使用以及sync.WaitGroup Goroutine的使用 在主线程中，开启一个Goroutine，该协程每隔50毫秒输出“你好go”在主线程中也没个50毫秒输出“你好go”，输出10次后退出程序，要求主线程和Goroutine同时执行\n主线程执行完毕后即使协程没有执行完毕程序也会退出，使用sync.WaitGroup来等待协程执行完毕\nsync.WaitGroup 实现等待协程执行完毕 var wg sync.WaitGroup 设置Go中并行运行的时候占用CPU的数目 Go运行时的调度器使用GOMAXPROCS参数来确定需要使用多少个OS线程来同时执行GO代码，默认值是机器上的CPU核心数，例如在一个8核心的机器上，调度器会把Go代码同时调度到8个OS线程上。\nGo语言中可以通过runtime.GOMAXPROCS()函数设置当前程序并发时占用的CPU核心数，Go1.5版本之前是单核心执行，1.5版本后默认使用全部的CPU核心数。\n1 2 3 4 5 6 7 8 func main() { // 获取当前计算机上面的cpu个数 cpuNum := runtime.NumCPU() fmt.Println(\u0026#34;cpuNum=\u0026#34;, cpuNum) runtime.GOMAXPROCS(cpuNum - 1) fmt.Println(\u0026#34;ok\u0026#34;) } Goroutine 例子 统计1-10000000的数字中的素数 传统方法 通过一个for循环判断各个数是不是素数 使用并发或者并行的方式，将统计素数的任务分配给多个goroutine去完成，这个时候就用到了goroutine start:(n-1)*30000+1 end: n*30000\n开启四个goroutine 进行循环\n协程之间的通信 \u0026ndash; Channel管道 管道是Go在语言级别上提供的goroutine间的通信方式，可以使用channel在多个goroutine之间传递消息，如果goroutine是Go程序并发的执行体，channel就是他们之间的连接，channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。\nGo并发模型是CSP，提倡通过通信共享内存而不是通过共享内存而实现通信。 Go语言中的管道是一种特殊的类型，管道像一个传送带或者队列，总是遵从先入先出的规则，保证收发数据的顺序。每一个管道都是一个具体类型的导管，也就是声明channel的时候需要为其指定元素类型。\nchannel 类型 channel是一种类型，一种引用类型，声明管道类型的格式如下\n1 2 3 4 5 var 变量 chan 元素类型 var ch1 chan int // 声明一个传递整型的管道 var ch2 chan bool // 声明一个传递bool类型的管道 var ch3 chan []int // 声明一个切片int切片的管道 创建channel 声明的管道后需要使用make函数初始化之后才能使用 make(chan 元素类型, 容量)\n1 2 3 4 5 6 // 创建一个能存储10个int类型数据的管道 ch1 := make(chan int, 10) // 创建一个能存储4个 bool类型数据的管道 ch2 := make(chan bool, 4) // 创建一个能存储3个[]int切片类型数据的管道 ch3 := make(chan []int, 3) channel操作 管道有发送、接受和关闭三种操作，发送和接收都是使用\u0026lt;-符号，现在我们先使用以下语句定义一个管道 ch := make(chan int, 3)\n发送 将数据放在管道内 将一个值发送到管道中 ch \u0026lt;- 10 把10发送到ch中\n接受 (从管道内取值) 从一个管道中接收值\n1 2 x := \u0026lt;- ch // 从ch中接收值并赋值给变量x \u0026lt;- ch // 从ch中接收值，忽略结果 管道是引用数据类型 使用make初始化\n放不下数据的时候会阻塞，已经取完了再取的时候会阻塞\n管道循环 当向管道中发送完数据时，我们可以通过close函数关闭管道，当管道被关闭时，再往管道发送值可能会引发panic，从改管道取值的操作会先取完管道中的值，然后取到的值一直都是对应类型的零值。那么该如何判断管道是否被关闭了呢？\n使用for range遍历管道，当管道被关闭的时候就会退出for range，如果没有关闭管道就会报个错误fatal error all Goroutines are asleep - deadlock\n通过for range来遍历管道数据，管道没有key\n1 2 3 for val := range ch1 { fmt.Println(val) } 要关闭管道不然的话会死锁 (写入完成后\n通过for循环循环遍历管道的时候可以不关闭管道，不知道啥时候管道关闭的时候使用for循环来遍历管道 但是要知道数据有多少不然的话就一直读取了零值\ngoroutine 结合Channel管道 需求: 定义两个方法一个方法给管道里面写数据，一个给管道里面读取数据，要求同步进行\n开启一个fn1的协程给管道inChan中写入100条数据 开启一个fn2的协程读取inChan中写入的数据 注意fn1和fn2同时操作一个管道 主线程必须等待操作完成后才可以退出 使用goroutine的时候管道没数据不会报错会等待 继续等待读取 多个goroutine可以共享channel中的数据\nGoruntine 与 Channel 统计素数例子 协程的个数 统计和打印并行执行 极致的并行\n//TODO: 完善写下这个代码 视频地址\n单向管道 有时候我们会将管道作为参数在多个任务函数间传递，很多时候我们在不同的任务函数中使用管道会对其进行限制，比如限制管道在函数中只能发送或者只能接收。\n1 2 3 4 5 6 7 8 9 10 11 12 13 // 在默认情况下管道是双向的 var chan1 chan int // 可读可写 // 声明只写的管道 var chan2 chan \u0026lt;- int chan2 = make(chan int, 3) chan2 \u0026lt;- 20 // num := \u0026lt;- chan2 // error // 声明为只读 var chan3 \u0026lt;- chan int num2 := \u0026lt;- chan3 // chan3 \u0026lt;- 30 // error ch := make(chan int, 2) 读写管道 ch := make(chan\u0026lt;- int, 2) 只写管道 ch := make(\u0026lt;-chan int, 2) 只读管道\nselect多路复用 在某些场景下我们需要同时从多个通道接受数据，这个时候可以使用go中给我们提供的select多路复用，通常情况下载通道接受数据时，如果没有数据可以接收将会发生阻塞，比如下面的代码来实现从多个通道接收数据的时候就会发生阻塞。\n使用select多路复用来获取chan的数据的时候不需要将chan关闭\ngoroutine recover 解决协程中出现的panic问题 Golang并发安全和锁 互斥锁 互斥锁是传统并发编程中对于共享资源进行访问控制的主要手段，他是由标准库sync中的Mutex结构体类型表示，sync.Mutex类型只有两个公开的指针方法，Lock和Unlock。Lock锁定当前共享资源，Unlock进行解锁。\n1 2 3 var mutex sync.Mutex mutex.Lock() mutex.Unlock() 顺序输出，不会由于加了go而数据错乱，因为添加了mutex.Lock()\n读写互斥锁 互斥锁的本质是当一个goroutine访问的时候，其他的goroutine都不能访问，这样在资源同步，避免竞争的同时也降低了程序的并发性能，程序由原本的并行执行变成了串行执行\n其实当对一个不会变化的数据只做读操作的话是不会存在资源竞争的问题，因为数据是不变的，不管怎么读取，多少goroutine同时读取，都是可以的。\n所以问题不是出在读上，而是写上，修改的数据要同步，这样其他的goroutine才可以感知的到，所以真正的互斥应该是读取和修改，修改与修改之间的，读和读是没有互斥的必要性的。\n所以对于互斥锁提出了一种性能更好的读写锁，读写锁可以让多个读操作并发执行同时读取，但是对于写操作是完全互斥的，也就是说，当一个goroutine进行写操作的时候，其他的goroutine既不能进行读操作，也不能进行写操作。\n读写锁是 读与读操作不互斥，读与写操作互斥，写与写操作互斥\n完整代码\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 var wg sync.WaitGroup var mutex sync.RWMutex func write() { mutex.Lock() fmt.Println(\u0026#34;执行写操作\u0026#34;) time.Sleep(time.Second * 2) mutex.Unlock() wg.Done() } func read() { mutex.RLock() fmt.Println(\u0026#34;执行读操作\u0026#34;) time.Sleep(time.Second * 1) mutex.RUnlock() wg.Done() } func main() { for i := 0; i \u0026lt; 10; i++ { wg.Add(1) go read() } for i := 0; i \u0026lt; 10; i++ { wg.Add(1) go write() } wg.Wait() } Go 反射 有时候我们需要一个函数，这个函数有能力统一处理各个值类型，而这些类型可能无法共享同一个接口，也可能是布局未知，也有可能这个类型在我们设计函数的时候还不存在，这个使用我们可以使用反射\n空接口可以存储任意类型的变量，那我们如何知道这个空接口保存数据的类型是什么，值是什么呢 可以使用类型断言 可以使用反射实现，也就是在程序运行时动态获取一个变量的类型信息和值信息 把结构体序列化为json字符串，自定义结构体tab标签的时候就用到了反射 后期我们会给大家讲ORM框架，这个ORM框架就用到了反射技术 ORM，对象关系映射是通过使用描述对象和数据库之间的映射的元数据，将面向对象语言中的对象自动持久化到关系数据库中。 反射的基本概念 反射是指在程序运行期间对于程序本身进行访问和修改的能力，正常情况程序在编译时，变量被转化为内存地址，变量名不会被编译器写入到可执行部分，在运行程序时，程序无法获取自身的信息，支持反射的语言可以在程序编译期将变量的反射信息，如字段名称，类型信息、结构体信息等整合到可执行文件中，并给程序提供接口访问反射信息，这样就可以在程序运行期间获取类型的反射信息，并且有能力修改他们。\n动态获取变量的能力\n使用反射可以实现的功能\n反射可以在程序运行期间动态的获取变量的各种信息，比如变量的类型 类别 如果是结构体，通过反射还可以获取结构体本身的信息，比如结构体的字段、结构体的方法等 通过反射，可以修改变量的值，可以调用关联的方法 reflect.typeOf 使用reflect.typeOf获取任意值的类型对象,Go语言中的变量分为两个部分\n类型信息 预先定义好的元信息 值信息 程序运行过程中可动态变化的 在Go语言中的反射机制中，任意接口值都是由一个具体类型和具体类型的值两个部分组成的，在Go语言中反射的相关功能由内置的reflect包提供，任意接口值在反射中都可以理解为由reflect.Type和reflect.value两部分组成。并且reflect包提供了reflect.TypeOf和reflect.ValueOf两个重要函数来获取任意对象的Value 和Type。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func reflectFn(x interface{}) { v := reflect.TypeOf(x) fmt.Println(v) } func main() { a := 10 b := 23.4 c := true d := \u0026#34;你好go\u0026#34; reflectFn(a) // int reflectFn(b) // float64 reflectFn(c) // bool reflectFn(d) // string } 通过反射回去对象的具体信息 typeOf类型信息\ntype Name 和 type Kind 在反射中关于类型还划分为类型Type和种类Kind，因为在go语言中我们可以使用type关键字构造很多自定义类型，而种类Kind就是指底层的类型，但是在反射中，当需要区分指针、结构体等大品种的类型时，就会用到种类Kind，举个例子，我们定义了两个指针类型和两个结构体类型，通过反射查看他们的类型和种类。 Go语言的反射中像数组、切片、map、指针等类型的变量，他们的.Name()都是返回空。\n返回的反射对象的有type和kind两种，其中种类kind是指向底层的类型\n指针、数组、切片 没有类型名称\nreflect.ValueOf reflect.ValueOf返回的是reflect.Value类型，其中包含了原始值的值信息，reflect.Value与原始值之间可以相互转换。 reflect.Value类型提供的获取原始值的方法如下 通过反射设置变量的值 Elem() 用来获取指针对应的值\n结构体反射 获取结构体中的属性与值 任意值通过reflect.TypeOf()获得反射对象信息后，如果他的类型是结构体，可以通过反射值对象的NumField()和Field()方法获得结构体成员的详细信息。\n获取结构体中的方法 获取结构体中的方法并执行 修改结构体属性 由于要修改结构体属性，所以要传入结构体指针类型\n类型判断\n","permalink":"https://akashark.github.io/en/posts/tech/go/%E8%AF%AD%E6%B3%95/%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E9%80%9F%E9%80%9A/","summary":"\u003cp\u003e继续前面的整理，这将是最后一部分的整理内容，包括了goroutine channel的使用以及Go反射的使用。\u003c/p\u003e\n\u003ch2 id=\"go-goroutine-channel\"\u003eGo goroutine channel\u003c/h2\u003e\n\u003ch3 id=\"进程-线程-并行-并发\"\u003e进程 线程 并行 并发\u003c/h3\u003e\n\u003ch4 id=\"进程-线程\"\u003e进程 线程\u003c/h4\u003e\n\u003cp\u003e进程就是程序在操作系统中的一次执行过程，是系统进行资源分配和调度的基本单位，进程是一个动态概念，是程序在执行过程中分配和管理资源的基本单位，每一个进程都有自己的地址空间，一个进程中至少有5个基本状态，初始态、执行态、等待状态、就绪状态、终止状态。通俗的讲进程就是一个正在执行的程序。\u003c/p\u003e","title":"基础语法速通三"},{"content":"上一篇我们根据大地老师的B站视频学习了一部分Go语言语法的基础部分，接下来让我继续开始学习，在这篇中将继续总结函数、接口、time包、 指针与结构体等重要的语法基础。\nGo 函数 函数定义 函数是组织好的、可重复使用的、用于执行指定任务的代码块。本文介绍了Go语言中函数的相关内容。 Go语言支持的函数类型包括: 函数、匿名函数、闭包 Go语言中定义函数使用func关键字，具体格式如下\n1 2 3 func 函数名(参数)(返回值){ 函数体 } 函数参数 从上面的定义可以看出参数的定义是使用形参名1 类型, 形参名2 类型，如果形参类型一样的话可以简写中间用逗号分隔 如果要定义的函数包含可变的参数则需要将函数的形参定义为可变形参，函数的可变参数是指函数的参数数量不固定，go语言中的可变参数通过在参数名后面(参数类型前面)加上...来表示可变参数(区别于针对于数组的拆解) 当可变参数与固定参数同时出现，可变参数放在最后一个形参的位置。\n1 2 3 4 5 6 7 8 9 10 // num 表示输入的参数个数， elements为参数元素 func sum(num int, elements ...int) { fmt.Printf(\u0026#34;%T\\n\u0026#34;, elements) // 传进来的参数类型为对应类型的切片 fmt.Println(num) sum := 0 for _, v := range elements { sum += v } fmt.Println(sum) } 返回值 1 2 3 4 5 6 7 8 9 10 11 12 13 14 func sum1(nums ...int) (int, int) { sum := 0 count := 0 for _, v := range nums { sum += v count += 1 } return sum, count // return 关键字一次可以返回多个值 } func main() { sum, count := sum1(1, 2, 3, 4, 5) fmt.Println(sum, count) } 函数定义时可以给返回值命名，并在函数体中直接使用这些变量，最后通过return关键字返回返回的时候直接写return就OK了 使用命名返回值和未命名返回值结合是不允许的\n函数变量作用域 和其他语言一样go语言中也包含了全局变量与局部变量，同时也有全局作用域和局部作用域\n全局变量\\作用域 全局变量是定义在函数外部的变量，在程序整个运行周期都是有效的 局部变量\\作用域 局部变量是函数内部定义的变量，函数内定义的变量无法在该函数外使用 函数类型与变量 和其他语言一样在Go语言中函数也是有类型与变量的概念的(函数是一等公民) 我们可以定义函数类型，通过type关键在来定义一个函数类型, 具体的格式如下 type calculation func(int, int) int 上面语句定义了一个calculation类型，他是一个函数类型，这种函数接受两个int类型的参数并且返回一个int类型的返回值，只要符合两个int参数，一个int返回值的函数都是calculation类型的函数\n整体来说就是形如type 函数名 func(int, int) int type关键字不光可以定义函数类型，也可以定义我们自己的数据类型，比如定义一个自己的int类型type myInt int\n方法作为返回值和参数 方法作为参数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 type calcType func(int, int) int func add(x, y int) int { return x + y } func sub(x, y int) int { return x - y } func calc(x, y int, fun calcType) int { return fun(x, y) } func main() { fmt.Println(calc(1,2, add)) } 函数作为返回值 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 type calcType func(int, int) int func add(x, y int) int { return x + y } func sub(x, y int) int { return x - y } func calc(x, y int, fun calcType) int { return fun(x, y) } func operation(op string) calcType { switch op { case \u0026#34;+\u0026#34;: return add case \u0026#34;-\u0026#34;: return sub case \u0026#34;*\u0026#34;: return func(i1, i2 int) int { // 匿名函数 return i1 * i2 } default: return nil } } func main() { f := operation(\u0026#34;*\u0026#34;) fmt.Println(f(2,2)) } 匿名函数 go语言中不支持函数的嵌套，但是可以定义匿名函数，匿名函数就是没有函数名的函数，匿名函数因为没有函数名，所以没法像普通函数那样调用，所以匿名函数必须要保存到某个变量中，或者设置为自执行在匿名函数后直接加()\n匿名函数的定义格式如下\n1 2 3 func(参数)(返回值) { 函数体 } 自执行函数格式如下\n1 2 3 4 5 6 func main() { // 匿名函数 匿名自执行函数 func(形参) { 函数体 }(实参) } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 type calcType func(int, int) int func operation(op string) calcType { switch op { case \u0026#34;+\u0026#34;: return add case \u0026#34;-\u0026#34;: return sub case \u0026#34;*\u0026#34;: return func(i1, i2 int) int { // 匿名函数 return i1 * i2 } default: return nil } } func main() { f := calc(3, 4, func(i1, i2 int) int { return i1 * i2 }) fmt.Println(f) var fn = func(x, y int) int { return x * y } res := calc(3,4, fn) fmt.Println(res) // 匿名自执行函数 形参 实参 函数体 fmt.Println(func (x, y int) int { return x * y }(3, 4)) } 函数递归调用 函数调用函数本身为递归，由于函数调用函数没有结束条件，所以要注意设置递归的出口，否则将会一直递归的调用下去。\n最经典的例子就是实现阶乘\n1 2 3 4 5 6 7 func fn(n int) int { if n \u0026gt; 1 { // 递归调用出口 return n * fn(n - 1) } else { return 1 } } 闭包 闭包可以理解为定义在一个函数内部的函数，在本质上闭包是将函数内部和函数外部连接起来的桥梁，或者说是函数和其引用环境的组合体。\n闭包是指有权访问另一个函数作用域中的变量的函数 创建闭包的常见的方式就是在一个函数内部创建另一个函数，通过另一个函数访问这个函数中的局部变量 由于闭包里作用域返回的局部变量资源不会被立刻销毁回收，所以可能会占用更多的内存，过渡的使用闭包会导致性能的下降，建议在非常有必要的时候才使用闭包 ps: 这里通过闭包创建的局部变量的回收时机 \u0026ndash; Go的GC处理\n1 2 3 4 5 6 7 func adder() func(int) int { var x int return func(y int) int { x += y return x } } 这里补充下前面提到的全局变量和局部变量的特点 全局变量\n常驻内存 污染全局 局部变量\n不常驻内存 不污染全局 Go语言中的闭包包含如下特点\n可以让一个常量常驻内存 可以让一个变量不污染全局 在返回的闭包中创建的局部变量是否被立即释放要看看闭包中是否对于局部变量有操作(引用)，如果有操作不会立即释放否则会释放 \u0026mdash; 要了解下Go中对于闭包的GC\n闭包可以解决将变量常驻内存且不污染全局\ndefer 语句 Go语言中的defer语句会将其后面跟随的语句进行延迟处理，在defer归属的函数即将返回时，将延迟处理的语句按照defer定义的逆序顺序进行执行，也就是说先被defer的语句最后被执行，最后被defer的语句最先被执行。\n命名返回值与匿名返回值在defer的表现有不同，具体可以参照上面的demo，对于这种表现是由于defer执行时机导致的。 下面再看看这个例子 由于defer在注册时要确定所有的参数，所以会先执行作为子函数的calc(\u0026quot;A\u0026quot;, x, y), calc(\u0026quot;B\u0026quot;, x, y)\npanic/recover Go语言中目前是没有异常机制，但是使用``panic/recover`模式来处理错误，panic可以在任何地方引发，但是recover只有在defer调用的函数中有效。类似于其他函数中的try/throw\nfn1 error: 抛出异常 结束\nerror: runtime error: integer divide by zero 结束 5\ndefer panic recover 结合使用 给管理员发送邮件\nGo 结构体 Go中没有类的概念，Go中结构体和其他语言中的类有些相似，和其他面向对象的语言中的类相比，Go中的结构体具有更高的扩展性和灵活性\nGo中基础数据类型可以表示一些事物的基本属性，但是当我们想飙到一个事物的全面或者部分属性时，这个时候再用单一的基本数据类型就无法满足要求了，Go提供了一种自定义数据类型，可以封装多个基本数据类型，这种数据类型叫结构体，英文名叫struct也就是我们可以通过struct来定义自己的类型\n结构体的首字母可以大写也可以小写，大写表示这个结构体是公有的，在其他的包里面也可以使用，小写表示私有的只能在本包中使用\nstruct 定义 Go 中通过type关键字定义一个结构体，在讲解结构体之前，首先介绍下通过type关键字自定义类型以及定义类型别名。\n自定义类型 在Go语言中有一些基本的数据类型，如string、 int、float、bool等，Go中可以使用type关键字来定义自定义类型 type myInt int\n上面代码表示将myInt定义为int类型，通过type关键字的定义，myInt就是一种新的类型，他具有int的特性\n类型别名 TypeAlias只是Type的别名，本质上TypeAlias与Type是同一个类型，就像一个孩子有大名小名英文名但这些名字都是指他本人 type TypeAlias = Type\n在go语言中有两个类型别名，rune和byte他们的底层定义如下 type byte = uint8 // 表示占一个字节的字符 字母 type rune = int32 // 表示占4个字节的字符 中文 特殊符号\n自定义类型打印出来的类型就是自定义的类型，类型别名定义的打印出来的类型就是原本的类型\n结构体定义与初始化 定义 使用type和struct关键字来定义结构体，具体代码如下\n1 2 3 4 type 类型名 struct { 字段名 字段类型 字段名 字段类型 } 类型名 表示自定义结构体的名称，在同一个包中不可以重复 字段名 表示结构体字段名，结构体中的字段名必须唯一 字段类型 表示结构体字段的具体类型 任意类型 实例化 只有当结构体实例化后才会真正的分配内存，也就是必须实例化后才能使用结构体的字段\n结构体的字段可以是: 基本数据类型、也可以是切片、Map以及结构体，如果结构体的字段类型是指针，slice和map的零值都是nil，即没有分配空间，如果需要使用这样的字段需要先make后才能使用\n方式一 结构体本身也是一种类型，可以像声明内置类型一样使用var关键字声明结构体类型 var 结构体实例 结构体类型 方式二 可以通过new关键字对结构体进行实例化，得到结构体的地址 1 2 3 4 5 6 7 func main() { var p2 = new(person) p2.name = \u0026#34;张三\u0026#34; p2.sex = \u0026#34;男\u0026#34; p2.age = 20 fmt.Pirntf(\u0026#34;值:%v 类型:%T\\n\u0026#34;, p2, p2) // 值:{张三 20 男} 类型main.Person } 通过new返回的是指针类型的实例\n在go中支持对结构体指针直接使用，来访问结构图id成员，在底层 p2.name = \u0026quot;张三\u0026quot;会转化为(*p2).name = \u0026quot;张三\u0026quot;\n方式三 使用\u0026amp;对结构体进行取地址操作相当于对该结构体类型进行一次new实例化操作 核心在于p3 := \u0026amp;Person{} 等同于 new一次\n和方式二基本一样，可以理解为对于new的简写 (得到的也是对应类型的实例的指针)\n方式四 类似于对于map直接赋值，直接对于结构体中的字段进行赋值即可 得到的是值类型\n方式五 得到的是指针类型 其实相当于对于方法4的取地址\n方式六 可以部分赋值部分不赋值，不赋值的是类型的默认值\n方式七 加不加取地址都可以加了是指针类型，不加是值类型 顺序要和定义的对齐\n结构体方法和接收者 在go语言中，没有类的概念但是可以给行(结构体 自定义类型)定义方法，所谓方法就是定义了接受者的函数，接受者的概念就类似于其他语言中的this或者self。 方法的定义格式如下\n1 2 3 func(接收者变量 接收者类型) 方法名(参数列表)(返回参数) { 函数体 } 接受者变量 接收者中的参数变量名在命名时，官方建议使用接受者类型名的第一个小写字母，而不是self或者this之类的命名，例如 Person类型的接受者变量应该命名为p，Connector类型的接受者变量应该命名为c等。 接收者类型和参数类型相似，可以是指针类型也可以是非指针类型 方法名、参数列表、返回参数具体格式于参数定义相同 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 type Person struct { name string age int sex string } func (p Person) printInfo() { fmt.Printf(\u0026#34;姓名: %v 年龄: %v\u0026#34;, p.name, p.age) } func main() { p := \u0026amp;Person{ name: \u0026#34;Sharker\u0026#34;, age: 10, sex: \u0026#34;男\u0026#34;, } p.printInfo() } 任意类型添加方法 在go中，接受者的类型可以是任意类型，不静静是结构体，任意类型都可以拥有方法举个例子在基于int类型使用type关键字可以定义新的自定义类型，然后为新定义自定义添加方法\n1 2 3 4 5 6 7 8 9 type myInt int func (m myInt) sayHello(){ fmt.Println(\u0026#34;Hello 我是一个int\u0026#34;) } func main() { var m1 myInt m1.sayHello() } 非本地类型不能定义犯法，也就是说我们不能给别的包的类型定义方法 通过给新类型增加方法其实就和iOS开发中的扩展有点类似，增加方法不增加成员变量\n结构体的嵌套与继承 结构体的匿名字段 结构体允许其他成员字段在声明时没有字段名而只有类型，这种没有名字的字段就成为匿名字段\n1 2 3 4 5 6 7 8 9 10 11 12 type Person struct { string int } func main() { p := Person{ \u0026#34;小王子\u0026#34;, 18 } fmt.Println(p.string) } 匿名字段默认采用类型名作为字段名，结构体要求字段名必须是唯一的因此一个结构体中同种类型的匿名字段只能有一个, 结构体中命名字段和匿名字段不可以混合定义\n结构体的嵌套 1 2 3 4 5 6 7 8 9 10 type User struct { Username string Password string Address Address // 表示User结构体嵌套Address结构体 } type Address struct { Name string Phone string City string } 还可以使用匿名嵌套的方式\n1 2 3 4 5 6 7 8 9 10 11 type User struct { Username string Password string Address } type Address struct { Name string Phone string City string } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 type UserInfo struct { Username string Password string Address } type Address struct { Name string Phone string City string } func main() { u := UserInfo { Username: \u0026#34;Sharker\u0026#34;, Password: \u0026#34;xxxx\u0026#34;, // 嵌套结构体 可以定义匿名字段和命名字段混用 Address: Address{ Name: \u0026#34;北京\u0026#34;, Phone: \u0026#34;13263235040\u0026#34;, City: \u0026#34;北京\u0026#34;, }, } fmt.Println(u) } 赋值和调用和命名的一样\n当访问结构体成员时会去查找结构体中查找该字段，如果找不到再去匿名结构体中查找 简单点说就是内部嵌套的结构体字段也可以在外层直接访问到\n结构体命名访问冲突 优先访问的是嵌套其他结构体中的字段(外层的字段)，访问被嵌套的需要加上结构体前缀 二义性了 需要加上结构提的前缀\n结构体的继承 结构体的继承是通过结构体的嵌套来实现的\n因为dog没有Name属性但是在调用Name的时候发现本结构体中没有的话就会去子结构体中寻找从而实现了dog继承了Animal相关的属性与方法 结构体中可以嵌套另一个结构体或者结构体指针\n结构体与json相互转化 当时用Go语言写一些RESTFul接口的时候就需要涉及到结构体和Json之间的相互转化，Go Json序列化是指把结构体数据转化为Json格式的字符串，Go json反序列化是指把json数据转化为Golang中的结构体对象 Go 中的序列化与反序列化主要通过\u0026quot;encoding/json\u0026quot;包中的json.Marshal()和json.Unmarshal()方法\n结构体标签 Tag是结构体的元信息，可以在运行时的时候通过反射机制读取出来，Tag在结构体字段的后方定义，由一对反引号包裹起来，具体的格式如下 key1:\u0026quot;value1\u0026quot; key2:value2 可以定义多个tag标签 结构体tag由一个或者多个键值对组成，键与值使用冒号分割，值使用双引号括起来，同一个结构体字段可以设置多个键值对tag，不同的键值对tag之间使用空格分隔。\n为结构体添加Tag时，必须严格遵守键值对的规则，结构体标签的解析代码的容错能力很差，一旦格式写错，编译和运行时都不会提示任何错误，通过反射也无法正确取值，例如不要在key和value之间添加空格\n对于嵌套结构体序列化与反序列化与正常的非嵌套的结构体的处理是一样的按照正常的Marshal搞就可以了\nGo 接口 Go中的接口是一种抽象数据类型,Go中接口定义了对象的行为规范，只定义规范不进行实现，接口中定义的规范需要由具体的对象来实现。 可以通俗的理解接口就是一个标准，他是对一个对象的行为和规范进行约定，约定实现接口的对象必须得按照接口的规范。\nGo 接口定义 在go中定义接口(interface)是一种类型，一种抽象的类型，接口是一组函数method的集合，go中的接口不能包含任何变量。 在go中接口中的所有方法都没有方法体，接口定义了一个对象的行为规范，只定义规范不实现，接口体现了程序设计的多态和高内聚低耦合的思想。 Go中的接口也是一种数据类型，不需要显示实现，只需要一个变量含有接口类型中的所有方法，那么这个变量就实现了这个接口。\n1 2 3 4 type 接口名 interface { 方法名1(参数列表) 返回值列表 方法名2(参数列表) 返回值列表 } 接口名 使用type将接口定义为自定义的类型名，GO语言的接口在命名时，一般在单词名后面添加er，如有写操作的接口叫Writer，有字符串功能的接口叫Stringer等，接口名最好要能突出该接口的类型含义。 方法名 当方法名首字母是大写且这个接口名首字母也是大写时，这个方法可以被接口所在的包之外的代码访问。 参数列表、返回值列表 参数列表和返回值列表中的参数变量名可以省略 接口定义的方法没有函数体，不需要给出具体的实现 如果接口里面有方法的话，必须要通过结构体或者自定义类型实现这个接口，要实现这个接口的话必须要实现接口中的所有方法(实现这 结构体或者自定义类型) go 中接口就是一种数据类型 (interface)\n外部调用是 computer.work(camera) 等价于 var usb Usber = camera 接口是规范 实现接口需要实现接口中的所有方法\n空接口 GO中的接口可以不定义任何方法，没有任何方法的接口就是空接口，空接口表示没有任何约束，因此任何类型变量都可以实现空接口，空接口在实际使用的非常多，用空接口可以代表任意数据类型\n空接口表示任意类型Any\ngo中空接口是可以直接当做类型来使用的， 可以表示任意类型\n作为函数参数 使用空接口实现可以接受任意类型的函数参数\n1 2 3 4 // 空接口作为函数参数 func show(a interface{}) { 函数体 } map的值实现空接口 使用空接口实现可以保存任意值的字典\n1 2 3 4 5 var studentInfo = make(map[string]interface{}) studentInfo[\u0026#34;name\u0026#34;] = \u0026#34;张三\u0026#34; studentInfo[\u0026#34;age\u0026#34;] = 18 studentInfo[\u0026#34;married\u0026#34;] = false fmt.Println(studentInfo) 切片实现空接口 1 2 var slice = []interface{}{\u0026#34;张三\u0026#34;, 20, true, 32.9} fmt.Println(slice) 类型断言 一个接口的值是由一个具体类型和具体类型的值两个部分组成的，这两部分分别被称为接口的动态类型和动态值。 如果我们想要判断空接口中的值的类型，那么这个时候就可以使用类型断言 x.(T)\nx 表示类型为interface{}的变量 T 表示断言x可能的类型 定义一个方法可以传入任意数据类型，然后根据不同的类型实现不同的功能\n只能在switch使用 x.(type)\n结构体值接收者和指针接收者实现接口的区别 值接收者 如果结构体中的方法是值接收者，那么实例化后的结构体值类型和结构体指针都可以赋值给接口类型变量\n指针接收者 如果结构体中的方法是指针接收者，那么实例化后结构体指针类型可以赋值给接口变量 \u0026ndash; 只能是指针类型变量 值类型的变量不可以\n一个结构体实现多个接口 两个接口Animal1， Animal2 接口嵌套 接口与接口之间可以通过嵌套创建出新的接口\n空接口与类型断言的细节 1 2 3 4 5 6 7 8 9 10 func main() { var userinfo = make(map[string]interface{}) userinfo[\u0026#34;username\u0026#34;] = \u0026#34;张三\u0026#34; userinfo[\u0026#34;age\u0026#34;] = 20 userinfo[\u0026#34;hobby\u0026#34;] = []string{\u0026#34;睡觉\u0026#34;, \u0026#34;吃饭\u0026#34;} fmt.Println(userinfo[\u0026#34;age\u0026#34;]) fmt.Println(userinfo[\u0026#34;hobby\u0026#34;]) fmt.Println(userinfo[\u0026#34;hobby\u0026#34;][1]) } interface{} dose not support indexing 空接口不支持idnex\n获取struct里面的属性获取不到 因为是interface{}\n对于第一个切片的问题 使用类型断言来给空接口一个具体的类型就可以实现访问了 对于struct是一样的 Go 指针 通过前面的教程我们知道变量是用来存储数据的，变量的本质是给存储数据的内存地址起了一个好记的名字，比如我们定义了一个变量a := 10, 这个时候可以直接通过a这个变量来读取内存中保存的10这个值，在计算机底层a这个变量其实对应了一个内存地址。\n指针也是一个变量，但是他是一种特殊的变量，他存储的数据不是一个普通的值，而是另一个变量的内存地址\n通过地址取值 1 2 3 4 5 func main() { a := 10 p := \u0026amp;a // p是指针变量 类型为*int // *p 表示取出p这个变量对应的内存地址的值 } 将值类型变成了引用类型数据 取地址\nnew和make 指针需要创建内存后使用\n1 2 3 var userinfo = make(map[string]string) userinfo[\u0026#34;username\u0026#34;] = \u0026#34;张三\u0026#34; fmt.Println(userinfo) map是引用类型数据 必须分配内存空间后才可以初始化\n1 2 3 var a *int // 指针也是引用类型数据 *a = 100 fmt.Println(*a) 执行上面的代码会引发panic，这是因为在Go语言中对于引用类型的变量，在使用的时候不仅声明它，还要为他分配内存空间，否则值就没法鵆，对于值类型的声明不需要分配内存空间，是因为他在声明的时候已经默认分配好了存储空间，要分配内存就引出了new和make，Go语言中的new和make是内建的两个函数，主要用于分配内存。\nnew new是一个内置的函数，它的签名如下 func new(Type) *Type\nType 表示类型，new函数只接受一个参数，这个参数是一个类型 *Type表示类型指针，new函数返回一个指向该类型内存地址的指针 实际开发中new并不常用，使用new函数得到的是一个类型指针，并且该指针对应的值为该类型的零值 make var a = make(type) 创建并分配内存空间, a的默认值为对应type的零值\nnew 与 make 区别 两者都是用来做内存分配的 make只能用于slice、map以及channel的初始化，返回的还是这三个引用类型本身 而new用于类型的内存分配，并且内存对应的值为类型零值，返回的是指向类型的指针 Go time包以及日期函数 可以使用Day Hour等来访问时间的具体字段\nFormat格式化 时间类型有一个自带的方法Format进行格式化，需要注意的是Go语言中格式化时间模版不是常见的Y-m-d H:M:S而是使用了Go的诞生圣剑2006年1月2号15点04分(2006 1 2 3 4)\n2006 年 01 月 02 日 03 时 04 分 05 秒 12进制和24进制的区别在于 15表示24小时制 03表示12小时制\n时间戳 时间戳转日期 日期转化时间戳 上面的两种转化\n时间戳转日期 先将时间戳转化为时间对象，再将时间对象通过Format转化为string 日期转化时间戳 先将日期通过ParseInLocation转化为时间对象，在通过Unix转化为时间戳 时间间隔 单位是纳秒\n时间操作函数 定时器 Go mod 与 Go包详解 包的定义 包是多个Go源码的集合，是一种高级的代码复用方案，Go语言为我们提供了很多内置包，如fmt、strconv、strings、sort、error等 Go中的包可以分为三种\n系统内置包 Go语言给我们提供了内置包，引用后可以直接使用，如fmt、strconv等 自定义包 开发者自己写的包 第三方包 属于自定义包的一种，需要下载安装到本地后才可以使用，如decimal包解决float精度丢失问题 包管理工具 go mod 在Go1.11版本之前如果我们要自定义包的话必须把项目放在GOPATH目录下，GO1.11版本之后无需要手动配置环境变量，使用go mod管理项目，也不需要非得把项目放到GOPATH指定目录下，可以在磁盘的任意位置新建项目，GO1.13以后彻底不要GOPATH了。\ninit 在实际项目开发中我们首先需要在项目中用go mod命令生成一个go.mod文件管理我们的项目依赖 go mod init 包名\n自定义包 package是多个go源码的集合，一个包可以简单理解为一个存放多个.go文件的文件夹，该文件夹下面的所有go文件都要在代码的第一行添加如下代码，声明文件归属的包 package 包名\n一个文件夹下面直接包含的文件只能归属一个package，同样一个package的文件不能再多个文件夹下面 包名可以不和文件夹的名字一样，包名不能包含-符号 包名为main的包为程序的入口包，这种包编译后会得到一个可执行文件，而编译不包含main的包的源码文件则不会得到可执行文件 自定义包名 在导入包名的时候，可以为导入的包设置别名，通常用于导入的包名太长或者导入的包名冲突的情况 import 别名 \u0026quot;包的路径\u0026quot;\n1 2 3 import ( 别名 \u0026#34;包的路径\u0026#34; ) Go init函数 在Go程序执行时导入包语句会自动触发包内部的init函数，需要注意的是init()函数没有参数也没有返回值，init函数在程序运行时被自动调用执行，不能在代码中主动调用。 包初始化的顺序如下图所示 程序初始化流程\n全局声明 init() main() init函数执行顺序 Go语言包会从main包开始检查其导入的所有包，每个包中又可能导入其他的包，Go编译器由此构建出一个树状的包引用关系，再根据引用顺序决定编译顺序，依次编译这些包的代码，在运行时被左后导入的包会在最初的时候调用其init函数\n栈结构 先入后出\n使用第三方包 可以在包管理查找常见的go第三方包 比如去上面的网址找到float精度丢失的包decimal 然后查看其安装命令 使用命令 安装第三方包 方式一 go get github.com/xxxx 全局\n方式二 go mod download 全局\n方式三 依赖包会自动下载到GOPath/pkg/mod中多个项目可以共享缓存的mod注意使用go mod download的时候首先需要再你的项目中引入第三方包\n补充 Go mod 常见命令\nGo 文件 目录操作 文件操作 读取文件 方式一 file.Read()\n只读方式打开文件 file, err := os.Open() 读取文件 file.Read() 关闭文件流 defer file.Close() 一次读取128个字节，但是可能最后一个切片读取不满128个字节打印就会出现问题\nfile.Read返回的第一个参数为读取到的字节数，在将每个切片合入的时候通过tempSlice[:n] 在截取读取到的切片\n方式二 bufio读取文件\n只读方式打开文件 file, err := os.Open() 创建reader对象 reader := bufio.NewReader(file) ReadString读取文件 line, err := reader.ReadString('\\n') 关闭文件流 defer file.Close() bufio读取方式 reader.ReadString 返回的第一个参数是读取到的字符串\n方式三 读取文件 os.ReadFile 打开关闭文件的方法都封装好了只需要一句话就可以读取 os.ReadFile\n1 2 3 4 5 6 7 8 9 func main() { str, err := os.ReadFile(\u0026#34;./main.go\u0026#34;) if err == nil { fmt.Println(string(str)) } else { fmt.Println(err) } } 写入文件 打开文件的权限设置 方式一 file.Write\n打开文件 file, err := os.OpenFile(\u0026quot;文件地址\u0026quot;, os.O_CREATE|os.O_RDWR, 0666) 写入文件 1 2 file.Write([]byte(str)) // 写入字节切片数据 file.WriteString(\u0026#34;直接写入的字符串数据\u0026#34;) // 直接写入字符串数据 关闭文件流 defer file.Close() 方式二 bufio 创建写入对象\n打开文件 file, err := os.OPenFile(\u0026quot;文件地址\u0026quot;, os.O_CREATE|os.O_RDWR, 0666) 创建write对象 write := bufio.NewWriter(file) 将数据先写入缓存 write.WriteString(\u0026quot;Hello\\r\\n\u0026quot;) 将缓存中的内容写入文件 write.Flush() 关闭文件流 defer file.Close() 记得要Flush写入数据\n方式三 os.WriteFile() 直接封装好的可以直接调用 os.WriteFile()\n1 2 3 4 func main() { str := \u0026#34;xxx\u0026#34; os.WriteFile(\u0026#34;./main.go\u0026#34;, []byte(str), 0666) } 文件重命名 复制文件 使用os.ReadFile与os.WriteFile 完成文件的转移复制\n使用file对象 注意要记得 关闭文件流！\n删除文件 remove 不但可以删除文件也可以删除目录 删除多级目录使用removeAll 删除文件remove 递归删除removeAll\n目录操作 创建目录 一次性创建多级目录 ","permalink":"https://akashark.github.io/en/posts/tech/go/%E8%AF%AD%E6%B3%95/%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E9%80%9F%E9%80%9A/","summary":"\u003cp\u003e上一篇我们根据大地老师的\u003ca href=\"https://www.bilibili.com/video/BV1XY4y1t76G/?p=3\u0026amp;spm_id_from=pageDriver\u0026amp;vd_source=1ce8f381eab5d06dd966abe30310ea9a\"\u003eB站视频\u003c/a\u003e学习了一部分Go语言语法的基础部分，接下来让我继续开始学习，在这篇中将继续总结函数、接口、time包、\n指针与结构体等重要的语法基础。\u003c/p\u003e\n\u003ch2 id=\"go-函数\"\u003eGo 函数\u003c/h2\u003e\n\u003ch3 id=\"函数定义\"\u003e函数定义\u003c/h3\u003e\n\u003cp\u003e函数是组织好的、可重复使用的、用于执行指定任务的代码块。本文介绍了Go语言中函数的相关内容。\nGo语言支持的函数类型包括: 函数、匿名函数、闭包\nGo语言中定义函数使用func关键字，具体格式如下\u003c/p\u003e","title":"基础语法速通 二"},{"content":"Go 语法速通 以下为学习B站大地Go课程Go语言基础部分的的笔记输出，作为平时查阅的资料，希望也可以帮助读者快速熟悉Go语言\n经典Hello World 首先是经典的输出Hello World，Go语言的fmt包中包含了输出函数Print、Println、Printf，如下代码输出Hello World\n1 2 3 4 5 6 package main import \u0026#34;fmt\u0026#34; func main() { fmt.Println(\u0026#34;Hello World\u0026#34;) } 使用go run main.go来执行该go文件，输出Hello World。 这里补充一个go程序执行的顺序 知识点 package标识了go文件所属的包，在go中只有main函数，且main函数所处的包必须是main包，包中的元素（函数、变量）通过大小写来确定其访问权限，具体细节可以参考 Go 语言包管理（Package）必知必会 彻底理解 Go 的包概念 Go语言中的包和库：一次全面的理解 理解Go语言包(package) go module 与 package\nfmt包是Go的标准库之一，其有很多强大的功能具体可以参考 深入理解 fmt 包 golang fmt格式“占位符”\nGo 变量 常量 与 命名规则 命名规则 Go语言的变量命名规则和大多数的语言是一样的，以数字字母或者下划线且首字母不能为数字都可以定义为变量名，但是不可以使用关键字作为变量名。具体的规则如下\n变量名必须有数字、字母下划线组成 标识符开头不能为数字 标识符不能是保留字和关键字 变量的名字是区分大小写的 标识符一定要见名思意，变量名称建议使用名词，方法名建议使用动词 变量命名一般采用驼峰式，当遇到特有名词如DNS等的时候，特有名词根据是否私有全部大写或者小写(变量的公有还是私有根据变量名的大小写来决定) 常量变量也可以使用_作为开头 代码风格 代码每一行结束后不写分号 运算符左右建议加一个空格 推荐使用驼峰命名 左括号不分行如if { 在一行 if 判断条件的() 不用写 不同于其他语言 变量 使用var定义变量，在Go中定义完变量后必须去使用，使用var声明后不对其进行赋值的化该变量值为空(其对应类型的空值)。\n声明初始化变量有两种方式\nvar 变量名 类型 = 表达式 类型可省略 变量名 := 表达式 生命并初始化 短变量声明法 只能局部变量 对于第一种声明赋值方式可以看下面的例子\n1 2 var username string = \u0026#34;xxx\u0026#34; var username = \u0026#34;xxx\u0026#34; 上面的代码都是为定义username为字符串变量同时为其赋值，可以通过上面的代码看出go的编译器具有类型推导的能力。\ngo语言中变量需要声明后使用，同时同一作用域内不支持重复声明相同的变量\n变量还可以一次定义多个，但是前提是类型必须相同如\n1 2 3 var a, b string a = \u0026#34;xxx\u0026#34; b = \u0026#34;xxx\u0026#34; 对于类型不一致的变量如果想要一次定义多个的化可以使用如下方法\n1 2 3 4 var ( 变量名1 类型1 变量名2 类型2 ) 短变量声明法 变量名 := 表达式 只能作为局部变量，不能为全局变量 一次声明多个变量并初始化可以声明相同类型与不同类型的变量 匿名变量 匿名变量在使用多重赋值时，如果想要忽略某个值，可以使用匿名变量，匿名变量使用_表示和Swift中使用一样如下所示\n1 2 3 4 func getUserInfo() (string, int) { return \u0026#34;Sharker\u0026#34;, 10 } var username, _ = getUserInfo() 匿名变量不占用命名空间，不会分配内存，所以匿名变量之间不存在重复声明\n变量使用前必须初始化，声明后必须使用\n常量 常量的值是不可以改变的，使用const关键字定义常量，定义常量时必须要赋值如下所示\n1 const a = \u0026#34;A\u0026#34; 多个常量也可以一起声明如\n1 2 3 4 const ( a = \u0026#34;A\u0026#34; b = \u0026#34;B\u0026#34; ) const 同时声明多个常量的时候，如果只赋值了第一个值则后面的值都是一样的\n1 2 3 4 5 6 7 const ( n1 = 100 n2 n3 n4 ) // 则n2 - n4其值都是100 iota iota是go语言中的常量计数器，const中每新增一行常量声明将会使得iota计数一次 iota默认值是0，直接定义iota的值为0\n1 2 3 const a = iota fmt.Println(a) // 0 在一次定义多个const变量的时候,iota初始化为0且会自增\n1 2 3 4 5 6 7 8 9 10 11 12 const ( b = iota // b = 0 c // c = 1 ) const ( n1 = iota _ n3 n4 ) fmt.Println(n1, n3, n4) // 0 2 3 在iota生命中插队\n1 2 3 4 5 6 const ( n1 = iota // 0 n2 = 100 // 100 n3 = iota // 2 n4 // 3 ) iota多个定义到一行\n1 2 3 4 5 const ( n1, n2 = iota + 1, iota + 2 // 1 2 n3, n4 // 2 3 n5, n6 // 3 4 ) 上面的是由于iota每新增一行定义+1,同时定义了一行中的两数的规则 对应规则很容易推断出每个变量对应的值\n数据类型 int类型 整型分为以下两个大类\n有符号整型按照长度分为 int8 int16 int32 int64 无符号整型 uint8 uint16 uint32 uint64 使用unsafe.Sizeof可以查看不同长度的整型 在内存中占用的存储空间 单位是字节数 补充:\nunsafe.Sizeof 1 2 var num int = 10 fmt.Println(\u0026#34;num = %v 类型%T\u0026#34;, num, num) 整型类型转化 1 int64(a) //将a强制转化为64位 在强转的时候要注意高位向低位转化的时候的溢出问题 int 属于简写目的是为了兼容 在64位系统上其为int64 在32位系统上其为int32\n字面量输出语法 1 2 3 4 5 6 num := 9 fmt.Printf(\u0026#34;num=%v\\n\u0026#34;, num) // %v 原样输出 fmt.Printf(\u0026#34;num=%d\\n\u0026#34;, num) // %d 表示10进制输出 fmt.Printf(\u0026#34;num=%b\\n\u0026#34;, num) // %b 表示二进制输出 fmt.Printf(\u0026#34;num=%o\\n\u0026#34;, num) // %o 表示八进制输出 fmt.Printf(\u0026#34;num=%x\\n\u0026#34;, num) // %x 表示16进制输出 float浮点型 Go语言支持两种浮点类型,float32和float64，这两种浮点类型数据格式遵循IEEE754标准，float32的浮点数最大范围为3.4e38, 可以使用常量定义math.MaxFloat32,float64的浮点数的最大范围约为1.8e308,可以使用一个常量定义math.MaxFloat64\n打印浮点数的时候可以使用fmt包配合%f占位符来打印\n为啥浮点类型就没有个float在不同系统上表现为float32与float64\nfloat精度丢失问题 几乎所有的编程语言都有精度丢失这个问题，这是典型的二进制浮点数精度丢失问题，在定长条件下，二进制小数和十进制小数互转可能存在精度丢失\nint与float的转化 直接使用类型()来强转，但是高位转化为低位的时候注意溢出的问题，float转化为int的时候直接截取小数部分\n科学计数法 bool类型 Go语言中以bool类型进行声明布尔型数据，布尔型数据只有true和false两种值。\n布尔类型变量的默认值为false Go语言中不允许将整型强制转化为布尔类型 布尔型无法参与数值运算，也无法与其他类型进行转化 字符串类型 字符串转义 输出多行字符串 和其他语言类似也是使用两个反引号\n1 2 s1 := `第一行 第二行` 字符串常用方法 字符串长度 输出的是字节数 汉字占用四个字节\ncontains 是否包含子串 str2是否包含在str1 同样的HasPrefix 和 HasSuffix 以及 index lastIndex也一样第一个参数都是全集的字符串 str2为子集 lastIndex表示最后出现的位置 index与lastindex查找不到的话返回-1\nbyte和rune类型 组成每个字符串的元素叫做”字符“,可以通过遍历字符串元素获得字符，字符用单引号包裹起来 Go中的字符属于int类型，默认输出为ASCII码值(使用%v 如果想要原样输出使用%c)\nbyte 占一个字节 byte -\u0026gt; uint8 rune 占4个字节 rune -\u0026gt; int32\nrune细节\n获取字符串中的字符 直接使用下标获取 go中汉字占用四个字节，一个字母占用一个字节 unsafe.sizeOf无法获取字符串占用的大小(获取的是结构体的大小)，只能使用len来查看string类型占用的存储空间 但是如果包含了中文的话len的长度并不是字符串的实际长度\n打印字符 字符是汉字 类型是int32 循环字符串里面的字符 包含汉字的使用for循环有问题 汉字占4个字节 使用range循环 表示一个utf8类型 字符串直接使用for的话是使用byte表示一个字符，使用range循环表示使用rune表示一个字符 如果要循环的字符串只有英文字母的话可以使用for循环 不然的应该使用for range循环\n修改字符串 要修改字符串，需要先将其转化为[]rune或者[]byte，完成后在转化为string，无论哪种转化，都会重新分配内存，并复制字节数组 直接赋值的话没法修改字符串的 应该先去转化 至于要转化成哪种数组主要看是否包含除了英文字母以外的其他字符\n数据类型 int 整型 1 2 3 4 5 6 7 8 9 1. 整型和整形之间的转化 var a int8 = 20 var b int16 = 40 fmt.Println(int16(a) + b) 2. 整型与浮点型之间的转化 var a float32 = 29.23 var b int = 40 fmt.Println(a + float32(b)) 这种数值的转化建议从低位转化为高位, 防止溢出情况的发生\nstring类型 其他类型转化为string类型 使用fmt包中的Sprintf将其他类型转化为string类型\n1 2 3 4 5 6 7 8 strs = fmt.Sprintf(\u0026#34;%f\u0026#34;, f) // 浮点型 fmt.Printf(\u0026#34;str type %T, strs = %v \\n\u0026#34;, strs, strs) strs = fmt.Sprintf(\u0026#34;%t\u0026#34;, t) // bool型 fmt.Printf(\u0026#34;str type %T, strs = %v \\n\u0026#34;, strs, strs) strs = fmt.Sprint(\u0026#34;%c\u0026#34;, b) // 相应Unicode码点所表示的字符 fmt.Printf(\u0026#34;str type %T, strs = %v \\n\u0026#34;, strs, strs) 使用strconv包来进行类型转化 strconv.FormatInt两个参数\nint64的数值 传值int类型的进制 1 2 3 var i int = 20 str1 := strconv.FormatInt(int64(i), 10) fmt.Printf(\u0026#34;值: %v 类型: %T \\n\u0026#34;, str1, str1) 同理还可以使用strconv.FormatFloat转化 string类型转化为数值类型 使用strconv.ParaseInt转化 同理可以使用strconv.ParaseFloat转化 这里补充下使用fmt输出的占位符 其他基本数据类型，如bool float等与其他语言的基础数据类型相似\nGo 复合数据类型 - 数组 数组的定义 数组的定义方式为 var 数组变量名 [元素数量] T 通过%T打印数组的类型可以发现，数组的长度也是属于数组的类型\nint类型的数组声明后为初始化其元素值为0，string为空字符串\n数组声明为未初始化的时候数组中的元素为对应类型的空值\n数组的初始化方式有很多种，下面将一一介绍\n方式一 1 2 3 4 5 var arr1 [3]int arr1[0] = 23 arr1[1] = 24 arr1[2] = 25 fmt.Println(arr1) 方式二 1 2 3 4 5 var arr1 = [3]int{23, 34, 5} fmt.Println(arr1) arr1 := [3]string{\u0026#34;php\u0026#34;, \u0026#34;nodejs\u0026#34;, \u0026#34;golang\u0026#34;} fmt.Println(arr1) 方式三 按照上面的方法每次都要确保提供的初始值和数组长度一致，一般情况下我们可以让编译器根据初始值的个数自行推断数组的长度，例如 1 2 3 4 5 6 7 8 9 func main() { var testArray [3]int var numArray = [...]int{1, 2} var cityArray = [...]string{\u0026#34;北京\u0026#34;, \u0026#34;上海\u0026#34;, \u0026#34;深圳\u0026#34;} fmt.Println(testArray) fmt.Println(numArray) fmt.Println(\u0026#34;%T\u0026#34;, numsArray) // [2]int fmt.Println(\u0026#34;%T\u0026#34;, cityArray) // [3]string } 可以自动根据初始化列表的值来初始化数组的长度 使用len()来查看数组的长度，数组的长度初始化后不可以改变\n方式四 可以使用指定索引值的方式来初始化数组 1 2 3 4 5 func main() { a := [...]int{1:1, 3:5} fmt.Println(a) // 0, 1, 0, 5 fmt.Printf(\u0026#34;%f\\n\u0026#34;, a) [4]int } 使用指定索引的方式来初始化数组，按照最大下标的值来初始化数组的长度，没有声明的值为对应类型的空值\n数组的类型 基本数据类型与数组均为值类型\n下面要说到的切片为引用类型\n值类型，改变副本的值，不会改变本身的值 引用类型，改变副本的值，会改变本身的值 本质上是改变引用指向的原本的位置的值 多维数组 多维数组的定义 var 数组变量名 [元素数量][元素数量] T\n1 2 3 4 5 6 7 8 9 10 11 12 var arr = [3][2]string { {\u0026#34;北京\u0026#34;, \u0026#34;上海\u0026#34;}, {\u0026#34;广州\u0026#34;, \u0026#34;深圳\u0026#34;}, {\u0026#34;成都\u0026#34;, \u0026#34;重庆\u0026#34;}, } // 打印二维数组 for _, v1 := range arr { for _, v2 : range v1 { fmt.Println(v2) } } 同时多维数组的定义还支持通过列表元素的数量推断数组的长度 这个种写法仅支持外层(只有第一层)的数组的使用\nGo 复合数据类型 - 切片 切片的定义 切片-Slice是一个拥有相同类型元素的可变长度的序列，他是基于数组类型做的一层封装，他十分的灵活可以支持自动扩容，切片是一个引用数据类型，他的内部结构包含了地址、长度和容量 切片的声明如下格式 var name []T\nname 为变量名 T 表示切片中的元素类型 Slice 拥有相同的类型元素的可变长序列，切片是引用数据类型 与数组定义的区别在于不写长度\n声明与初始化 同样的切片也具有多种的声明与初始化方式\n方式一 1 2 var arr1 []int fmt.Printf(\u0026#34;%v -- %T 长度 %v\u0026#34;, arr1, arr1, len(arr1)) // [] []int 0 方式二 1 2 var arr2 = []int{1, 2, 34, 45} fmt.Println(\u0026#34;%v - %T - 长度: %v\u0026#34;, arr2, arr2, len(arr2)) 方式三 1 2 var arr3 = []int{1:2, 2:4, 5:6} fmt.Println(\u0026#34;%v - %T - 长度: %v\u0026#34;, arr3, arr3, len(arr3)) // 0 2 4 0 0 6 []int 6 切片的默认值是nil 切片的循环遍历 与数组的方式一样\n1 2 3 4 5 6 7 8 9 10 11 1. 使用for循环的方式 var strSlice = []string{\u0026#34;php\u0026#34;, \u0026#34;java\u0026#34;, \u0026#34;nodejs\u0026#34;, \u0026#34;golang\u0026#34;} for i :=0; i \u0026lt; len(strSlice); i++ { fmt.Println(strSlice[i]) } 2. for range 循环 var strSlice = []string{\u0026#34;php\u0026#34;, \u0026#34;java\u0026#34;, \u0026#34;nodejs\u0026#34;, \u0026#34;golang\u0026#34;} for index, value := range strSlice { fmt.Println(index, value) } 基于数组定义切片 切片可以从原本存在的数组中定义\n1 2 3 4 5 6 func main() { // 基于数组定义切片 a := [5]int{1,2,3,4,5} b := a[:] // 获取数组里面的所有值 fmt.Println(\u0026#34;%T\u0026#34;, b) // b的类型为切片 } 也可以获取数组部分\n1 2 3 4 a := [5]int{55, 56, 57, 58, 59} b := a[:] // 获取数组里面的所有值 fmt.Println(\u0026#34;%v-%T\u0026#34;, b, b) c := a[1:4] // 获取数组中的部分获取的是56 57 58 左包含 右不包含 基于切片的切片 与基于数组的切片相同\n切片的长度和容量 切片拥有自己的长度和容量，可以通过内置的len()函数求长度，使用内置的cap()函数求切片的容量 切片的长度就是它包含的元素个数 切片的容量是从它的第一个元素开始数，到其底层数组元素末尾的个数 切片s的长度和容量可通过表达式len(s)和cap(s)来获取。 切片的本质 切片的本质就是对于底层数组的封装，他包含了三个信息:底层数组的指针，切片的长度，切片的容量 切片本身是数组的封装，指针指向切片的开头，长度为切片的长度，容量为切片开始位置到数组末尾\nmake() 常见切片 上面对于数组的赋值都是采用既有的数据，如果需要动态的创建一个切片，我们就需要使用make()函数来创建切片具体的格式如下 make([]T, size, cap)\nT 切片的元素类型 size 切片中元素的数量 cap 切片的容量 创建的Slice中元素的值为对应类型的零值\nAppend方法的使用 切片扩容 对于切片的扩容需要用到append方法\ngolang中没法通过下边的方式给切片扩容，指的是直接在arr[x] = 0，x为当前切片的最大长度+1，这样的操作会引起越界错误 使用append()方法来进行扩容如下\n1 2 3 arr := []int{1,2,3} arr = append(arr, 4) fmt.Println(arr) 合并切片 使用append()方法可以将两个切片合成一个切片\n1 2 3 4 5 sliceA := []string{\u0026#34;php\u0026#34;, \u0026#34;java\u0026#34;} sliceB := []string{\u0026#34;nodejs\u0026#34;, \u0026#34;go\u0026#34;} sliceA = append(sliceA, sliceB...) fmt.Println(sliceA) // [php, java, nodejs, go] 其中...表示拆包,将sliceB中的元素打平\n切片的扩容策略 首先判断，如果申请容量大于2倍的旧容量，最终容量就是申请的容量 否则判断，如果旧切片长度小于1024，则最终容量就是旧容量的两倍 否则判断，如果旧切片长度大于等于1024，则最终容量从旧容量开始循环增加原来的1/4，直到最终容量大于等于新申请的容量，即1/4的步长增加 如果容量计算值溢出，则最终容量就是新申请容量 需要注意的是，切片扩容还会根据切片中元素的类型不同而做出不同的处理，比如int和string类型的处理方式就是不同的\n对应源码的位置在slice扩容策略\n使用copy()函数复制切片 make创建sliceB切片，copy拷贝A到B值复制避免引用类型影响 代码如下\n1 2 3 4 5 6 7 8 sliceA := []int{1, 2, 3, 45} sliceB := make([]int, 4, 4) copy(sliceB, sliceA) sliceB = append(sliceB, 3) // 值拷贝 fmt.Println(sliceA) // 1, 2, 3, 45 fmt.Println(sliceB) // 1, 2, 3, 45, 3 从切片中删除元素 go 语言中并没有删除切片元素的专用方法，我们可以使用切片本身的特性来删除元素\n1 2 3 4 5 6 7 func main() { // 从切片中删除元素 a := []int{30, 31, 32, 33, 34, 35, 36, 37} // 要删除索引为2的元素 a = append(a[:2], a[3:]...) // 跳过index为2的元素 fmt.Println(a) // [30 31 33 34 35 36 37] } 左包右不包 append合并切片的时候最后添加的切片要加\u0026hellip;因为参数类型为element\u0026hellip; 其实\u0026hellip;操作符表示将元素打开成为单独的element\n切片排序算法以及sort算法包 选择排序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 func main() { // 选择排序 var numsSlice = []int{9, 6, 5, 4, 8, 7} for i := 0; i \u0026lt; len(numsSlice); i++ { for j := i + 1; j \u0026lt; len(numsSlice); j++ { if numsSlice[i] \u0026gt; numsSlice[j] { // 升序排序 temp := numsSlice[i] numsSlice[i] = numsSlice[j] numsSlice[j] = temp } } } fmt.Println(numsSlice) } 冒泡排序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 func main() { // 冒泡排序 var numsSlice = []int{9, 6, 5, 4, 8} for i := 0; i \u0026lt; len(numsSlice); i++ { for j := 0; j \u0026lt; len(numsSlice) - 1 - i; j++ { // 区别与选择排序的问题在于start = 0 if numsSlice[j] \u0026gt; numsSlice[j + 1] { temp := numsSlice[j] numsSlice[j] = numsSlice[j + 1] numsSlice[j + 1] = temp } } } fmt.Println(numsSlice) } sort算法包 对于int float64和string数组或是切片的排序，go分别提供了sort.Ints()、sort.Float64s() 和 sort.Strings()函数，默认都是从小到大排序\n1 2 3 4 5 intList := []int{2, 4, 3, 5, 7, 6, 9, 1, 0} float8List := []float64{4.2, 5.9, 12.4, 10.2, 50.7, 99.9, 31.4, 3.14} stringList := []string{\u0026#34;a\u0026#34;, \u0026#34;c\u0026#34;, \u0026#34;b\u0026#34;, \u0026#34;z\u0026#34;, \u0026#34;x\u0026#34;, \u0026#34;w\u0026#34;, \u0026#34;y\u0026#34;, \u0026#34;d\u0026#34;} sort.Ints(intList) // 升序排序 go的sort包也可以使用sort.Reverse(slice)来调换slice.Interface.Less，也就是比较函数，所以int、float64和string的逆序排序函数可这样写\n1 2 3 4 5 6 7 8 func main() { intList := []int{2, 4, 3, 5, 7, 6, 9, 8, 1, 0} // float8List := []float64{4.2, 5.9, 12.4, 10.2, 50.7, 99.9} // stringList := []string{\u0026#34;a\u0026#34;, \u0026#34;c\u0026#34;, \u0026#34;b\u0026#34;, \u0026#34;z\u0026#34;, \u0026#34;x\u0026#34;, \u0026#34;w\u0026#34;} sort.Sort(sort.Reverse(sort.IntSlice(intList))) // sort 逆序排序 fmt.Println(intList) } Go 复合数据类型 - map map是一种无序的基于key-value的数据结构，Go语言中的map是引用数据类型，必须初始化后才能使用，Go语言中的map定义语法如下 map[KeyType]ValueType 其中\nKeyType: 表示键的类型 ValueType: 表示键对应的值的类型 map类型的变量默认初始化为nil，需要使用make()函数来分配内存，语法为 maps := make(map[string]string) make 用于slice map 和 channel的初始化\n创建与初始化 make创建 1 2 3 4 5 6 7 var userinfo = make(map[string]string) userinfo[\u0026#34;username\u0026#34;] = \u0026#34;张三\u0026#34; userinfo[\u0026#34;age\u0026#34;] = \u0026#34;20\u0026#34; userinfo[\u0026#34;sex\u0026#34;] = \u0026#34;男\u0026#34; fmt.Println(userinfo[\u0026#34;username\u0026#34;]) 初始化的时候赋值 1 2 3 4 5 6 var userinfo = map[string]string { \u0026#34;usarname\u0026#34;: \u0026#34;张三\u0026#34;, \u0026#34;age\u0026#34;: \u0026#34;20\u0026#34;, \u0026#34;sex\u0026#34;: \u0026#34;男\u0026#34; } fmt.Println(userinfo) 循环遍历 1 2 3 4 5 6 7 8 9 10 var userinfo = map[string]string { \u0026#34;username\u0026#34;: \u0026#34;张三\u0026#34;, \u0026#34;age\u0026#34;: \u0026#34;20\u0026#34;, \u0026#34;sex\u0026#34;: \u0026#34;男\u0026#34; } for k, v := range userinfo { fmt.Println(\u0026#34;key:%v value:%v\\n\u0026#34;, k, v) // key username value: 张三 .... } map类型的CURD 创建map类型的数据 1 2 3 4 5 // 创建 map类型的数据 var userinfo = make(map[string]string) userinfo[\u0026#34;username\u0026#34;] = \u0026#34;张三\u0026#34; userinfo[\u0026#34;age\u0026#34;] = \u0026#34;20\u0026#34; fmt.println(userinfo) 修改map类型的数据 1 2 3 4 5 6 var userinfo = map[string]string { \u0026#34;username\u0026#34;: \u0026#34;张三\u0026#34;, \u0026#34;age\u0026#34;: \u0026#34;20\u0026#34;, } userinfo[\u0026#34;username\u0026#34;] = \u0026#34;李四\u0026#34; fmt.Println(userinfo) 获取 查找map类型的数据 1 2 3 4 5 6 var userinfo = map[stirng]string { \u0026#34;username\u0026#34;: \u0026#34;张三\u0026#34;, \u0026#34;age\u0026#34;: \u0026#34;20\u0026#34;, } v, ok := userinfo[\u0026#34;age\u0026#34;] // 获取map中key为age的值 如果存在ok为true 否则为false fmt.Println(v, ok) 使用delete() 函数删除键值对 使用delete()内建函数从map中删除一组键值对，delete()函数的格式如下 delete(map对象, key) 其中\nmap 对象表示要删除键值对的map对象 key 表示要删除的键值对的键 map与切片的结合 当我们想在切片里面放一些列用户的信息，这时我们可以顶一个元素为map的切片\n1 2 3 4 5 6 7 8 var userinfo = make([]map[string]string, 2, 2) if userinfo[0] == nil { userinfo[0] = make(map[string]string) userinfo[0][\u0026#34;username\u0026#34;] = \u0026#34;张三\u0026#34; userinfo[0][\u0026#34;age\u0026#34;] = \u0026#34;20\u0026#34; userinfo[0][\u0026#34;height\u0026#34;] = \u0026#34;180cm\u0026#34; } fmt.Println(userinfo) 1 2 3 4 5 6 7 8 9 10 11 userinfos := []map[string]string { { \u0026#34;name\u0026#34;: \u0026#34;Sharker\u0026#34;, \u0026#34;age\u0026#34;: \u0026#34;20\u0026#34;, }, { \u0026#34;name\u0026#34;: \u0026#34;Alice\u0026#34;, \u0026#34;age\u0026#34;: \u0026#34;2\u0026#34;, }, } fmt.Println(userinfos) map类型的排序 key升序排序\n遍历key放在切片里面，对于切片进行排序，然后再输出\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // 按照key升序输出map的key=\u0026gt;value // 1. 把mao的key放在切片里面 var keySlice []int for key, _ := range map1 { keySlice = append(keySlice, key) } fmt.Println(keySlice) // 2.让key进行升序排序 sort.Ints(keySlice) fmt.Println(keySlice) // 3. 循环遍历key输出map的值 for _, v := range keySlice { fmt.Println(\u0026#34;key=%v value=%n\\n\u0026#34;, v, map1[v]) } Go 运算符 golang ++ \u0026ndash; 只能单独使用且只能写在变量后面 也就是 var a = 10 a = a++ //错误 a++ //正确 Go 流程控制 条件判断 go的条件判断语句和其他语言的不同点在于不需要再if后面加(), 现在很多语言都不需要了比如Swift if的{}不能省略 { 左括号必须紧挨着if的条件判断或者else\n循环语句 for 1 2 3 for 初始化语句; 条件表达式; 结束语句 { 循环体结构 } 同样的go语言中的for也不需要写()\n1 2 3 for { // 无限循环 代替while } 对于go来说没有while语句 可以使用for无限循环来代替\nfor range 键值循环 1 2 3 for index, value in range(可迭代的数据结构) { // 角标 \u0026amp; 值 } switch case 1 2 3 4 5 6 7 8 9 10 11 func main() { var extname = \u0026#34;.html\u0026#34; switch extname { case \u0026#34;.html\u0026#34;: fmt.Println(\u0026#34;text/html\u0026#34;) case \u0026#34;.css\u0026#34;: fmt.Println(\u0026#34;text/css\u0026#34;) default: fmt.Println(\u0026#34;找不到此后缀\u0026#34;) } } go语言的switch case如上所示，和很多比较新的语言一样，go语言中的switch case 不需要在每个case中单独的添加break, 每个语句执行会自动的break，如果想要执行穿透操作需要增加 fallthrough关键字\nfallthrough语法可以执行满足条件的case的下一个case，是为了兼容C语言中的case设计的\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 func main() { var age = 30 switch { case age \u0026lt; 24: fmt.Println(\u0026#34;好好学习\u0026#34;) case age \u0026gt;= 24 \u0026amp;\u0026amp; age \u0026lt;= 60: fmt.Println(\u0026#34;好好赚钱\u0026#34;) fallthrough case age \u0026gt; 60: fmt.Println(\u0026#34;注意身体\u0026#34;) default: fmt.Println(\u0026#34;输入错误\u0026#34;) } } 可以在switch case判断条件上写表达式\n1 2 3 4 5 6 7 8 switch extname := \u0026#34;.css\u0026#34;; extname { case \u0026#34;.html\u0026#34;: fmt.Println(\u0026#34;text/html\u0026#34;) case \u0026#34;.css\u0026#34;: fmt.Println(\u0026#34;text/css\u0026#34;) default: fmt.Println(\u0026#34;找不到此后缀\u0026#34;) } switch case 多个分支\n1 2 3 4 5 6 7 8 9 var n = 5 switch n { case 1, 3, 5, 7, 9: fmt.Println(\u0026#34;奇数\u0026#34;) case: 2, 4, 6, 8, 10: fmt.Println(\u0026#34;偶数\u0026#34;) default: fmt.Println(\u0026#34;不认识\u0026#34;) } 可以在switch case 中的case语句中添加表达式这时就不需要再switch语句后面再判断变量 注意看switch 后面并没有跟任何的判断，这是因为在case中添加了判断条件\ncontinue goto break break go语言中break语句用于以下几个方面:\n用于循环语句中跳出循环，并开始执行循环之后的语句 break在switch中执行一条case后跳出语句的作用 在多重循环中，可以用标号label标出想break的循环 label跳出多层循环\n1 2 3 4 5 6 7 8 9 label1: for i := 0; i \u0026lt; 2; i++ { for j := 0; j \u0026lt; 10; j++ { if j == 3 { break label1 // 跳出循环到label1的位置 } fmt.Println(i, j) } } continue 跳过本次循环，但不跳过整体的循环，在continue语句后使用标签时，表示开始标签对应的循环 goto goto 语句通过标签进行代码间的无条件跳转，goto语句可以快速跳出循环，避免循环重复\n1 2 3 4 5 6 7 8 9 10 func main() { var n = 30 if n \u0026gt; 24 { fmt.Println(\u0026#34;成年人\u0026#34;) goto label1 } fmt.Println(\u0026#34;111\u0026#34;) label1: fmt.Println(\u0026#34;到这里了\u0026#34;) } ","permalink":"https://akashark.github.io/en/posts/tech/go/%E8%AF%AD%E6%B3%95/%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E9%80%9F%E9%80%9A/","summary":"\u003ch1 id=\"go-语法速通\"\u003eGo 语法速通\u003c/h1\u003e\n\u003cp\u003e以下为学习\u003ca href=\"https://www.bilibili.com/video/BV1XY4y1t76G/?p=3\u0026amp;spm_id_from=pageDriver\u0026amp;vd_source=1ce8f381eab5d06dd966abe30310ea9a\"\u003eB站大地Go课程\u003c/a\u003eGo语言基础部分的的笔记输出，作为平时查阅的资料，希望也可以帮助读者快速熟悉Go语言\u003c/p\u003e\n\u003ch2 id=\"经典hello-world\"\u003e经典Hello World\u003c/h2\u003e\n\u003cp\u003e首先是经典的输出Hello World，Go语言的fmt包中包含了输出函数Print、Println、Printf，如下代码输出Hello World\u003c/p\u003e","title":"基础语法速通 一"},{"content":"","permalink":"https://akashark.github.io/en/posts/read/%E5%9B%BE%E8%A7%A3http/%E5%9B%BE%E8%A7%A3http%E8%AF%BB%E5%90%8E%E6%84%9F/","summary":"","title":"图解Http读后感"},{"content":"测试\n","permalink":"https://akashark.github.io/en/posts/tech/ios/foundation/kvo-kvc%E5%A4%8D%E4%B9%A0/","summary":"\u003cp\u003e测试\u003c/p\u003e","title":"KVO KVC复习"},{"content":"文本及样式 Flutter.. 两个点语法含义 Dart中两个点..和三个点\u0026hellip;的用法\n文本及样式 常见属性 textAlign: 文本的对齐方式；可以选择左对齐、右对齐还是居中。注意，对齐的参考系是Text widget 本身, 只有 Text 宽度大于文本内容长度时指定此属性才有意义 maxLines、overflow: 指定文本显示的最大行数，默认情况下，文本是自动折行的，如果指定此参数，则文本最多不会超过指定的行。如果有多余的文本，可以通过overflow来指定截断方式，默认是直接截断，本例中指定的截断方式TextOverflow.ellipsis，它会将多余文本截断后以省略符“\u0026hellip;”表示；TextOverflow 的其他截断方式请参考 SDK 文档。 textScaleFactor: 代表文本相对于当前字体大小的缩放因子，相对于去设置文本的样式style属性的fontSize，它是调整字体大小的一个快捷方式。该属性的默认值可以通过MediaQueryData.textScaleFactor获得，如果没有MediaQuery，那么会默认值将为1.0。 TextStyle 1 2 3 4 5 6 7 8 9 10 11 Text(\u0026#34;Hello world\u0026#34;, style: TextStyle( color: Colors.blue, fontSize: 18.0, height: 1.2, fontFamily: \u0026#34;Courier\u0026#34;, background: Paint()..color=Colors.yellow, decoration:TextDecoration.underline, decorationStyle: TextDecorationStyle.dashed ), ); height：该属性用于指定行高，但它并不是一个绝对值，而是一个因子，具体的行高等于fontSize*height。 fontSize：该属性和 Text 的textScaleFactor都用于控制字体大小。但是有两个主要区别： fontSize可以精确指定字体大小，而textScaleFactor只能通过缩放比例来控制。 textScaleFactor主要是用于系统字体大小设置改变时对 Flutter 应用字体进行全局调整，而fontSize通常用于单个文本，字体大小不会跟随系统字体大小变化。 TextSpan 有点像富文本的展示方式\n1 2 3 4 5 6 const TextSpan({ TextStyle style, Sting text, List\u0026lt;TextSpan\u0026gt; children, GestureRecognizer recognizer, }); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 Text.rich(TextSpan( children: [ TextSpan( text: \u0026#34;Home: \u0026#34; ), TextSpan( text: \u0026#34;https://flutterchina.club\u0026#34;, style: TextStyle( color: Colors.blue ), recognizer: _tapRecognizer ), ] )) 我们通过 TextSpan 实现了一个基础文本片段和一个链接片段，然后通过Text.rich 方法将TextSpan 添加到 Text 中，之所以可以这样做，是因为 Text 其实就是 RichText 的一个包装，而RichText 是可以显示多种样式(富文本)的 widget ps: 在Flutter中经常会用用到..的语法糖 如下:\n1 2 3 state.clone() ..splashImg = action.img ..famousSentence = action.famousSentence; 等价于\n1 2 3 state.clone() state.splashImg = action.img state.famousSentence = action.famousSentence; 可以看成链式调用，但是和OC与java的链式调用不太一样 在OC/Java中链式调用有个规律，谁调用就返回谁，但是在dart中\u0026quot;..\u0026ldquo;不用在方法中返回调用主体，景观源码的实现方式也是通过set进去的，但是我们看到的就是Dart给我们提供的语法糖，因为Dart本身就是把成员变量的getter setter方法改成隐式的了\n三个点(\u0026hellip;) 是用来拼接集合 如list Map等\n1 2 3 4 5 6 7 8 9 10 class Test { Test() { //这里组合后 list就变成[ \u0026#39;a\u0026#39;, \u0026#39;b\u0026#39;, \u0026#39;c\u0026#39;,\u0026#39;d\u0026#39;, \u0026#39;e\u0026#39;, \u0026#39;f\u0026#39;] var list2 = [\u0026#39;d\u0026#39;, \u0026#39;e\u0026#39;, \u0026#39;f\u0026#39;]; var list = [\u0026#39;a\u0026#39;, \u0026#39;b\u0026#39;, \u0026#39;c\u0026#39;, ...list2]; //这里组合后map就变成{\u0026#39;a\u0026#39;: \u0026#39;a\u0026#39;, \u0026#39;b\u0026#39;: \u0026#39;b\u0026#39;,\u0026#39;c\u0026#39;: \u0026#39;c\u0026#39;, \u0026#39;d\u0026#39;: \u0026#39;d\u0026#39;} var map2 = {\u0026#39;a\u0026#39;: \u0026#39;a\u0026#39;, \u0026#39;b\u0026#39;: \u0026#39;b\u0026#39;}; var map = {...map2, \u0026#39;c\u0026#39;: \u0026#39;c\u0026#39;, \u0026#39;d\u0026#39;: \u0026#39;d\u0026#39;}; } } DefaultTextStyle 在 Widget 树中，文本的样式默认是可以被继承的（子类文本类组件未指定具体样式时可以使用 Widget 树中父级设置的默认样式），因此，如果在 Widget 树的某一个节点处设置一个默认的文本样式，那么该节点的子树中所有文本都会默认使用这个样式，而DefaultTextStyle正是用于设置默认文本样式的 设置Widget树中子Widget的文本的样式, 如果这子Widget中设指定了对应文本样式的话(设置inherit: false, 则全部都不使用继承的默认样式)，子widget的优先级会更高\n字体 在Flutter中使用字体分两步完成，首先在pubspec.yaml中声明他们，以确保会打包到应用中，然后通过TextStyle 属性使用字体\n在asset中声明 1 2 3 4 5 6 7 8 9 10 11 12 flutter: fonts: - family: Raleway fonts: - asset: assets/fonts/Raleway-Regular.ttf - asset: assets/fonts/Raleway-Medium.ttf weight: 500 - asset: assets/fonts/Raleway-SemiBold.ttf weight: 600 - family: AbrilFatface fonts: - asset: assets/fonts/abrilfatface/AbrilFatface-Regular.ttf 使用字体 1 2 3 4 5 6 7 8 9 10 // 声明文本样式 const textStyle = const TextStyle( fontFamily: \u0026#39;Raleway\u0026#39;, ); // 使用文本样式 var buttonText = const Text( \u0026#34;Use the font for this text\u0026#34;, style: textStyle, ); package中的字体 要使用 Package 中定义的字体，必须提供package参数。例如，假设上面的字体声明位于 my_package包中。然后创建 TextStyle 的过程如下,如果在 package 包内部使用它自己定义 的字体，也应该在创建文本样式时指定package参数 1 2 3 4 const textStyle = const TextStyle( fontFamily: \u0026#39;Raleway\u0026#39;, package: \u0026#39;my_package\u0026#39;, //指定包名 ); 一个包也可以只提供字体文件而不需要在 pubspec.yaml 中声明。 这些文件应该存放在包的lib/文件夹中。字体文件不会自动绑定到应用程序中，应用程序可以在声明字体时有选择地使用这些字体。假设一个名为my_package的包中有一个字体文件： lib/fonts/Raleway-Medium.ttf 然后再声明中声明\n1 2 3 4 5 6 7 flutter: fonts: - family: Raleway fonts: - asset: assets/fonts/Raleway-Regular.ttf - asset: packages/my_package/fonts/Raleway-Medium.ttf weight: 500 ps: lib/是隐含的，所以它不应该包含在 asset 路径中。\n在这种情况下，由于应用程序本地定义了字体，所以在创建TextStyle时可以不指定package参数：\n1 2 3 const textStyle = const TextStyle( fontFamily: \u0026#39;Raleway\u0026#39;, ); ","permalink":"https://akashark.github.io/en/posts/read/flutter%E5%AE%9E%E6%88%98/%E7%AC%AC%E4%B8%89%E7%AB%A0%E5%9F%BA%E7%A1%80%E7%BB%84%E4%BB%B6-%E4%B8%80/","summary":"\u003cp\u003e\u003ca href=\"https://book.flutterchina.club/chapter3/text.html\"\u003e文本及样式\u003c/a\u003e\n\u003ca href=\"https://www.cnblogs.com/gloryhope/p/13367585.html\"\u003eFlutter.. 两个点语法含义\u003c/a\u003e\n\u003ca href=\"https://www.jianshu.com/p/35063261c583\"\u003eDart中两个点..和三个点\u0026hellip;的用法\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"文本及样式\"\u003e文本及样式\u003c/h2\u003e\n\u003ch3 id=\"常见属性\"\u003e常见属性\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003etextAlign: 文本的对齐方式；可以选择左对齐、右对齐还是居中。注意，对齐的参考系是Text widget 本身, 只有 Text 宽度大于文本内容长度时指定此属性才有意义\u003c/li\u003e\n\u003cli\u003emaxLines、overflow: 指定文本显示的最大行数，默认情况下，文本是自动折行的，如果指定此参数，则文本最多不会超过指定的行。如果有多余的文本，可以通过overflow来指定截断方式，默认是直接截断，本例中指定的截断方式TextOverflow.ellipsis，它会将多余文本截断后以省略符“\u0026hellip;”表示；TextOverflow 的其他截断方式请参考 SDK 文档。\u003c/li\u003e\n\u003cli\u003etextScaleFactor: 代表文本相对于当前字体大小的缩放因子，相对于去设置文本的样式style属性的fontSize，它是调整字体大小的一个快捷方式。该属性的默认值可以通过MediaQueryData.textScaleFactor获得，如果没有MediaQuery，那么会默认值将为1.0。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"textstyle\"\u003eTextStyle\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv style=\"color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\n\u003ctable style=\"border-spacing:0;padding:0;margin:0;border:0;\"\u003e\u003ctr\u003e\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e 1\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e 2\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e 3\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e 4\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e 5\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e 6\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e 7\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e 8\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e 9\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e10\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e11\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;;width:100%\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-dart\" data-lang=\"dart\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eText(\u003cspan style=\"color:#0ff;font-weight:bold\"\u003e\u0026#34;Hello world\u0026#34;\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  style: TextStyle(\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    color: Colors.blue,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    fontSize: \u003cspan style=\"color:#ff0;font-weight:bold\"\u003e18.0\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    height: \u003cspan style=\"color:#ff0;font-weight:bold\"\u003e1.2\u003c/span\u003e,  \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    fontFamily: \u003cspan style=\"color:#0ff;font-weight:bold\"\u003e\u0026#34;Courier\u0026#34;\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    background: Paint()..color=Colors.yellow,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    decoration:TextDecoration.underline,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    decorationStyle: TextDecorationStyle.dashed\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  ),\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cul\u003e\n\u003cli\u003eheight：该属性用于指定行高，但它并不是一个绝对值，而是一个因子，具体的行高等于fontSize*height。\u003c/li\u003e\n\u003cli\u003efontSize：该属性和 Text 的textScaleFactor都用于控制字体大小。但是有两个主要区别：\n\u003cul\u003e\n\u003cli\u003efontSize可以精确指定字体大小，而textScaleFactor只能通过缩放比例来控制。\u003c/li\u003e\n\u003cli\u003etextScaleFactor主要是用于系统字体大小设置改变时对 Flutter 应用字体进行全局调整，而fontSize通常用于单个文本，字体大小不会跟随系统字体大小变化。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"textspan\"\u003eTextSpan\u003c/h2\u003e\n\u003cp\u003e有点像富文本的展示方式\u003c/p\u003e","title":"第三章基础组件 一"},{"content":"第二章\n调试Flutter应用 日志与断点 debugger() 声明 1 2 3 4 void someFunction(double offset) { debugger(when: offset \u0026gt; 30.0); // ... } print、debugPrint、flutter logs Dart print()功能将输出到系统控制台，我们可以使用flutter logs来查看它（基本上是一个包装adb logcat）。\n如果你一次输出太多，那么Android有时会丢弃一些日志行。为了避免这种情况，我们可以使用Flutter的foundation库中的debugPrint() (opens new window)。 这是一个封装print，它将输出限制在一个级别，避免被Android内核丢弃。\nFlutter框架中的许多类都有toString实现。按照惯例，这些输出通常包括对象的runtimeType单行输出，通常在表单中ClassName(more information about this instance…)。 树中使用的一些类也具有toStringDeep，从该点返回整个子树的多行描述。已一些具有详细信息toString的类会实现一个toStringShort，它只返回对象的类型或其他非常简短的（一个或两个单词）描述。\n调试模式断言 在Flutter应用调试过程中，Dart assert语句被启用，并且 Flutter 框架使用它来执行许多运行时检查来验证是否违反一些不可变的规则。当一个某个规则被违反时，就会在控制台打印错误日志，并带上一些上下文信息来帮助追踪问题的根源。\n要关闭调试模式并使用发布模式，请使用flutter run \u0026ndash;release运行我们的应用程序。 这也关闭了Observatory调试器。一个中间模式可以关闭除Observatory之外所有调试辅助工具的，称为“profile mode”，用\u0026ndash;profile替代\u0026ndash;release即可。\n断点 Vscode 或者 AS上自带的\n调试应用程序层 widget树 渲染树 Layer树 文档写的太少了而且没有实操，这个地方再找找资料补充下 官网上有对于DevTools的相关教程 教程\n异常捕获 Dart单线程模型 Dart线程运行过程，如上图中所示，入口函数 main() 执行完后，消息循环机制便启动了。首先会按照先进先出的顺序逐个执行微任务队列中的任务，事件任务执行完毕后程序便会退出，但是，在事件任务执行的过程中也可以插入新的微任务和事件任务，在这种情况下，整个线程的执行过程便是一直在循环，不会退出，而Flutter中，主线程的执行过程正是如此，永不终止。 在Dart中，所有的外部事件任务都在事件队列中，如IO、计时器、点击、以及绘制事件等，而微任务通常来源于Dart内部，并且微任务非常少，之所以如此，是因为微任务队列优先级高，如果微任务太多，执行时间总和就越久，事件队列任务的延迟也就越久，对于GUI应用来说最直观的表现就是比较卡，所以必须得保证微任务队列不会太长。值得注意的是，我们可以通过Future.microtask(…)方法向微任务队列插入一个任务。 在事件循环中，当某个任务发生异常并没有被捕获时，程序并不会退出，而直接导致的结果是当前任务的后续代码就不会被执行了，也就是说一个任务中的异常是不会影响其他任务执行的。\nFlutter框架异常捕获 onError是FlutterError的一个静态属性，它有一个默认的处理方法 dumpErrorToConsole，到这里就清晰了，如果我们想自己上报异常，只需要提供一个自定义的错误处理回调即可，如：\n1 2 3 4 5 6 void main() { FlutterError.onError = (FlutterErrorDetails details) { reportError(details); }; ... } 这样我们就可以处理那些Flutter为我们捕获的异常了\n其他异常捕获与日志收集 在Flutter中，还有一些Flutter没有为我们捕获的异常，如调用空对象方法异常、Future中的异常。在Dart中，异常分两类：同步异常和异步异常，同步异常可以通过try/catch捕获，而异步异常则比较麻烦，如下面的代码是捕获不了Future的异常的：\n1 2 3 4 5 try{ Future.delayed(Duration(seconds: 1)).then((e) =\u0026gt; Future.error(\u0026#34;xxx\u0026#34;)); }catch (e){ print(e) } Dart中有一个runZoned(\u0026hellip;) 方法，可以给执行对象指定一个Zone。Zone表示一个代码执行的环境范围，为了方便理解，读者可以将Zone类比为一个代码执行沙箱，不同沙箱的之间是隔离的，沙箱可以捕获、拦截或修改一些代码行为，如Zone中可以捕获日志输出、Timer创建、微任务调度的行为，同时Zone也可以捕获所有未处理的异常。下面我们看看runZoned(\u0026hellip;)方法定义：\n1 2 3 4 R runZoned\u0026lt;R\u0026gt;(R body(), { Map zoneValues, ZoneSpecification zoneSpecification, }) zoneValues: Zone 的私有数据，可以通过实例zone[key]获取，可以理解为每个“沙箱”的私有数据。 zoneSpecification：Zone的一些配置，可以自定义一些代码行为，比如拦截日志输出和错误等，举个例子： 1 2 3 4 5 6 7 8 9 10 11 12 13 14 runZoned( () =\u0026gt; runApp(MyApp()), zoneSpecification: ZoneSpecification( // 拦截print 蜀西湖 print: (Zone self, ZoneDelegate parent, Zone zone, String line) { parent.print(zone, \u0026#34;Interceptor: $line\u0026#34;); }, // 拦截未处理的异步错误 handleUncaughtError: (Zone self, ZoneDelegate parent, Zone zone, Object error, StackTrace stackTrace) { parent.print(zone, \u0026#39;${error.toString()} $stackTrace\u0026#39;); }, ), ); 这样一来，我们 APP 中所有调用print方法输出日志的行为都会被拦截，通过这种方式，我们也可以在应用中记录日志，等到应用触发未捕获的异常时，将异常信息和日志统一上报。 另外我们还拦截了未被捕获的异步错误，这样一来，结合上面的 FlutterError.onError 我们就可以捕获我们Flutter应用错误了并进行上报了！如下\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 void collectLog(String line){ ... //收集日志 } void reportErrorAndLog(FlutterErrorDetails details){ ... //上报错误和日志逻辑 } FlutterErrorDetails makeDetails(Object obj, StackTrace stack){ ...// 构建错误信息 } void main() { // 已经捕获的异常 var onError = FlutterError.onError; //先将 onerror 保存起来 FlutterError.onError = (FlutterErrorDetails details) { onError?.call(details); //调用默认的onError reportErrorAndLog(details); //上报 }; runZoned( () =\u0026gt; runApp(MyApp()), zoneSpecification: ZoneSpecification( // 拦截print print: (Zone self, ZoneDelegate parent, Zone zone, String line) { collectLog(line); parent.print(zone, \u0026#34;Interceptor: $line\u0026#34;); }, // 拦截未处理的异步错误 handleUncaughtError: (Zone self, ZoneDelegate parent, Zone zone, Object error, StackTrace stackTrace) { reportErrorAndLog(details); parent.print(zone, \u0026#39;${error.toString()} $stackTrace\u0026#39;); }, ), ); } ","permalink":"https://akashark.github.io/en/posts/read/flutter%E5%AE%9E%E6%88%98/%E7%AC%AC%E4%B8%80%E4%B8%AAflutter%E5%BA%94%E7%94%A8-%E5%9B%9B/","summary":"\u003cp\u003e\u003ca href=\"https://book.flutterchina.club/chapter2/first_flutter_app.html#_2-1-1-%E5%88%9B%E5%BB%BAflutter%E5%BA%94%E7%94%A8%E6%A8%A1%E6%9D%BF\"\u003e第二章\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"调试flutter应用\"\u003e调试Flutter应用\u003c/h2\u003e\n\u003ch3 id=\"日志与断点\"\u003e日志与断点\u003c/h3\u003e\n\u003ch4 id=\"debugger-声明\"\u003edebugger() 声明\u003c/h4\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv style=\"color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\n\u003ctable style=\"border-spacing:0;padding:0;margin:0;border:0;\"\u003e\u003ctr\u003e\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e1\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e2\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e3\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e4\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;;width:100%\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-dart\" data-lang=\"dart\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff;font-weight:bold\"\u003evoid\u003c/span\u003e someFunction(\u003cspan style=\"color:#fff;font-weight:bold\"\u003edouble\u003c/span\u003e offset) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  debugger(when: offset \u0026gt; \u003cspan style=\"color:#ff0;font-weight:bold\"\u003e30.0\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#007f7f\"\u003e// ...\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch4 id=\"printdebugprintflutter-logs\"\u003eprint、debugPrint、flutter logs\u003c/h4\u003e\n\u003cp\u003eDart print()功能将输出到系统控制台，我们可以使用flutter logs来查看它（基本上是一个包装adb logcat）。\u003c/p\u003e","title":"第一个Flutter应用 四"},{"content":"第二章\n路由管理 MaterialPageRoute 1 2 3 4 5 6 7 // 路由跳转 Navigator.push( context, MaterialPageRoute(builder: (context){ return const NewRoute(); }) ); MaterialPageRoute继承自PageRoute类，PageRoute类是一个抽象类，表示占有整个屏幕空间的一个模态路由页面，它还定义了路由构建及切换时过渡动画的相关接口及属性。MaterialPageRoute 是 Material组件库提供的组件，它可以针对不同平台，实现与平台页面切换动画风格一致的路由切换动画\n1 2 3 4 5 6 MaterialPageRoute({ WidgetBuilder builder, RouteSettings settings, bool maintainState = true, bool fullscreenDialog = false, }) MaterialPageRoute构造函数 (可以点进去看注释，注释写的也很清楚)\nbuilder 是一个WidgetBuilder类型的回调函数，它的作用是构建路由页面的具体内容，返回值是一个widget。我们通常要实现此回调，返回新路由的实例。 settings 包含路由的配置信息，如路由名称、是否初始路由（首页）。 maintainState：默认情况下，当入栈一个新路由时，原来的路由仍然会被保存在内存中，如果想在路由没用的时候释放其所占用的所有资源，可以设置maintainState为 false。 fullscreenDialog表示新的路由页面是否是一个全屏的模态对话框，在 iOS 中，如果fullscreenDialog为true，新页面将会从屏幕底部滑入（而不是水平方向）。 Navigator Navigator是一个路由管理的组件，它提供了打开和退出路由页方法。Navigator通过一个栈来管理活动路由集合。通常当前屏幕显示的页面就是栈顶的路由。 常用方法\nFuture push(BuildContext context, Route route) 将给定的路由入栈（即打开新的页面），返回值是一个Future对象，用以接收新路由出栈（即关闭）时的 返回数据。 bool pop(BuildContext context, [ result ]) 将栈顶路由出栈，result 为页面关闭时返回给上一个页面的数据。 实例方法 Navigator类第一个参数为context的静态方法都对应一个Navigator的实例方法，比如 Navigator.push(BuildContext context, Route route)等价于 Navigator.of(context).push(Route route) ，下面命名路由相关的方法也是一样的。 Navigator 还有很多其他方法，如Navigator.replace、Navigator.popUntil等，详情请参考 API文档或SDK 源码注释，在此不再赘述。\n路由传值(非命名路由) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 class TipRoute extends StatelessWidget { TipRoute({ Key key, required this.text, // 接收一个text参数 }) : super(key: key); final String text; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(\u0026#34;提示\u0026#34;), ), body: Padding( padding: EdgeInsets.all(18), child: Center( child: Column( children: \u0026lt;Widget\u0026gt;[ Text(text), ElevatedButton( onPressed: () =\u0026gt; Navigator.pop(context, \u0026#34;我是返回值\u0026#34;), child: Text(\u0026#34;返回\u0026#34;), ) ], ), ), ), ); } } class RouterTestRoute extends StatelessWidget { @override Widget build(BuildContext context) { return Center( child: ElevatedButton( onPressed: () async { // 打开`TipRoute`，并等待返回结果 var result = await Navigator.push( context, MaterialPageRoute( builder: (context) { return TipRoute( // 路由参数 text: \u0026#34;我是提示xxxx\u0026#34;, ); }, ), ); //输出`TipRoute`路由返回结果 print(\u0026#34;路由返回值: $result\u0026#34;); }, child: Text(\u0026#34;打开提示页\u0026#34;), ), ); } } 命名路由 所谓命名路由，即有名字的路由，我们可以先给路由起一个名字，然后就可以通过路由名字直接打开新路由了，这为路由管理带来了一种直观、简单的方式。\n路由表 Map\u0026lt;String, WidgetBuilder\u0026gt; routes;他是一个Map，key为路由的名字，是一个字符串，value是个builder回调函数，用于生成相应的路由widget。\n注册路由表\n1 2 3 4 5 6 7 8 9 10 11 12 MaterialApp( title: \u0026#39;Flutter Demo\u0026#39;, initialRoute:\u0026#34;/\u0026#34;, //名为\u0026#34;/\u0026#34;的路由作为应用的home(首页) theme: ThemeData( primarySwatch: Colors.blue, ), //注册路由表 routes:{ \u0026#34;new_page\u0026#34;:(context) =\u0026gt; NewRoute(), \u0026#34;/\u0026#34;:(context) =\u0026gt; MyHomePage(title: \u0026#39;Flutter Demo Home Page\u0026#39;), //注册首页路由 } ); 跳转要通过路由名称来打开新路由，可以使用Navigator 的pushNamed方法： Future pushNamed(BuildContext context, String routeName,{Object arguments})\n传递参数 1 2 3 4 5 6 7 8 9 10 11 class EchoRoute extends StatelessWidget { @override Widget build(BuildContext context) { //获取路由参数 var args=ModalRoute.of(context).settings.arguments; //...省略无关代码 } } Navigator.of(context).pushNamed(\u0026#34;new_page\u0026#34;, arguments: \u0026#34;hi\u0026#34;); 对于有构造函数，并且构造函数需要传递参数的Widget我们可以使用下面的方式进行适配\n1 2 3 4 5 6 7 8 MaterialApp( ... //省略无关代码 routes: { \u0026#34;tip2\u0026#34;: (context){ return TipRoute(text: ModalRoute.of(context)!.settings.arguments); }, }, ); 路由生成钩子 MaterialApp有一个onGenerateRoute属性，它在打开命名路由时可能会被调用，之所以说可能，是因为当调用Navigator.pushNamed(\u0026hellip;)打开命名路由时，如果指定的路由名在路由表中已注册，则会调用路由表中的builder函数来生成路由组件；如果路由表中没有注册，才会调用onGenerateRoute来生成路由。onGenerateRoute回调签名如下： Route\u0026lt;dynamic\u0026gt; Function(RouteSettings settings)\n有了onGenerateRoute回调，要实现上面控制页面权限的功能就非常容易：我们放弃使用路由表，取而代之的是提供一个onGenerateRoute回调，然后在该回调中进行统一的权限控制，如：\n1 2 3 4 5 6 7 8 9 10 11 12 13 onGenerateRoute: (settings) { return MaterialPageRoute(builder: (context) { String? routeName = settings.name; switch (routeName) { case \u0026#34;/\u0026#34;: { return MyHomePage(title: \u0026#34;title\u0026#34;); } case \u0026#34;new\u0026#34;: { return NewRoute(titleStr: \u0026#34;titleStr\u0026#34;); } } return Scaffold(); }); 其中MaterialPageRoute\n1 2 3 4 5 6 7 8 9 10 MaterialPageRoute({ required this.builder, super.settings, this.maintainState = true, super.fullscreenDialog, }) : assert(builder != null), assert(maintainState != null), assert(fullscreenDialog != null) { assert(opaque); } 传入widgetBuild返回一个Route的子类\n总结 建议使用命名路由的形式，这将会带来如下好处：\n语义化更明确。 代码更好维护；如果使用匿名路由，则必须在调用Navigator.push的地方创建新路由页，这样不仅需要import新路由页的dart文件，而且这样的代码将会非常分散。 可以通过onGenerateRoute做一些全局的路由跳转前置处理逻辑。 包管理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 name: flutter_in_action description: First Flutter Application. version: 1.0.0+1 dependencies: flutter: sdk: flutter cupertino_icons: ^0.1.2 dev_dependencies: flutter_test: sdk: flutter flutter: uses-material-design: true name: 应用或者包名 description: 应用或包的描述、简介。 version：应用或包的版本号。 dependencies：应用或包依赖的其他包或插件。 dev_dependencies：开发环境依赖的工具包（而不是flutter应用本身依赖的包）。 flutter：flutter相关的配置选项。 如果我们的Flutter应用本身依赖某个包，我们需要将所依赖的包添加到dependencies下就可以了\nPub仓库 Pub（https://pub.dev/ ）是 Google 官方的 Dart Packages 仓库，类似于 node 中的 npm仓库、Android中的 jcenter。我们可以在 Pub 上面查找我们需要的包和插件，也可以向 Pub 发布我们的包和插件。我们将在后面的章节中介绍如何向 Pub 发布我们的包和插件。\n我们可以使用IDE的功能或者手动运行flutter packages get 命令来下载依赖包。另外，需要注意dependencies和dev_dependencies的区别，前者的依赖包将作为App的源码的一部分参与编译，生成最终的安装包。而后者的依赖包只是作为开发阶段的一些工具包，主要是用于帮助我们提高开发、测试效率，比如 flutter 的自动化测试包等。\n本地依赖 如果我们正在本地开发一个包，包名为pkg1，我们可以通过下面方式依赖：\n1 2 3 dependencies: pkg1: path: ../../code/pkg1 git依赖 1 2 3 4 dependencies: pkg1: git: url: git://github.com/xxx/pkg1.git 1 2 3 4 5 dependencies: package1: git: url: git://github.com/flutter/packages.git path: packages/package1 问题他们这个不应该也有一个对于包的描述文件么，比如podspec之类的\n资源管理 指定 assets 1 2 3 4 flutter: assets: - assets/my_icon.png - assets/background.png assets指定应包含在应用程序中的文件， 每个 asset 都通过相对于pubspec.yaml文件所在的文件系统路径来标识自身的路径。asset 的声明顺序是无关紧要的，asset的实际目录可以是任意文件夹（在本示例中是assets 文件夹）\n加载文本assets 通过rootBundle (opens new window)对象加载：每个Flutter应用程序都有一个rootBundle (opens new window)对象， 通过它可以轻松访问主资源包，直接使用package:flutter/services.dart中全局静态的rootBundle对象来加载asset即可。 通过 DefaultAssetBundle (opens new window)加载：建议使用 DefaultAssetBundle (opens new window)来获取当前 BuildContext 的AssetBundle。 这种方法不是使用应用程序构建的默认 asset bundle，而是使父级 widget 在运行时动态替换的不同的 AssetBundle，这对于本地化或测试场景很有用。 通常，可以使用DefaultAssetBundle.of()在应用运行时来间接加载 asset（例如JSON文件），而在widget 上下文之外，或其他AssetBundle句柄不可用时，可以使用rootBundle直接加载这些 asset，例如：\n加载图片 主资源默认对应于1.0倍的分辨率图片。看一个例子：\n…/my_icon.png …/2.0x/my_icon.png …/3.0x/my_icon.png 在设备像素比率为1.8的设备上，\u0026hellip;/2.0x/my_icon.png 将被选择。对于2.7的设备像素比 率，\u0026hellip;/3.0x/my_icon.png将被选择。 如果未在Image widget上指定渲染图像的宽度和高度，那么Image widget将占用与主资源相同 的屏幕空间大小。 也就是说，如果\u0026hellip;/my_icon.png是72px乘72px，那么\u0026hellip;/3.0x/ my_icon.png应该是216px乘216px; 但如果未指定宽度和高度，它们都将渲染为72像素×72像素 （以逻辑像素为单位）。\npubspec.yaml中asset部分中的每一项都应与实际文件相对应，但主资源项除外。当主资源缺少某个资源时，会按分辨率从低到高的顺序去选择 ，也就是说1x中没有的话会在2x中找，2x中还没有的话就在3x中找。(可以不放1x的)\n1 2 3 4 5 6 7 8 9 Widget build(BuildContext context) { return DecoratedBox( decoration: BoxDecoration( image: DecorationImage( image: AssetImage(\u0026#39;graphics/background.png\u0026#39;), ), ), ); } 注意，AssetImage 并非是一个widget， 它实际上是一个ImageProvider，有些时候你可能期望直接得到一个显示图片的widget，那么你可以使用Image.asset()方法，如：\n1 2 3 Widget build(BuildContext context) { return Image.asset(\u0026#39;graphics/background.png\u0026#39;); } 使用默认的 asset bundle 加载资源时，内部会自动处理分辨率等，这些处理对开发者来说是无感知的。 (如果使用一些更低级别的类，如 ImageStream (opens new window)或 ImageCache (opens new window)时你会注意到有与缩放相关的参数)\n要加载依赖包中的图像，必须给AssetImage提供package参数。 例如，假设您的应用程序依赖于一个名为“my_icons”的包，它具有如下目录结构：\n…/pubspec.yaml …/icons/heart.png …/icons/1.5x/heart.png …/icons/2.0x/heart.png …etc. 然后加载图像，使用: AssetImage('icons/heart.png', package: 'my_icons') Image.asset('icons/heart.png', package: 'my_icons') 注意:包在使用本身的资源时也应该加上package参数来获取。 ps:\n与iOS中的类似，有Bundle类型，读取图片默认是处理scale，但是底层的API在处理图片的时候需要使用对应的scale处理 对于启动页和图标图片的使用均是在原生平台下进行使用的 多平台共享assets ","permalink":"https://akashark.github.io/en/posts/read/flutter%E5%AE%9E%E6%88%98/%E7%AC%AC%E4%B8%80%E4%B8%AAflutter%E5%BA%94%E7%94%A8-%E4%B8%89/","summary":"\u003cp\u003e\u003ca href=\"https://book.flutterchina.club/chapter2/first_flutter_app.html#_2-1-1-%E5%88%9B%E5%BB%BAflutter%E5%BA%94%E7%94%A8%E6%A8%A1%E6%9D%BF\"\u003e第二章\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"路由管理\"\u003e路由管理\u003c/h2\u003e\n\u003ch3 id=\"materialpageroute\"\u003eMaterialPageRoute\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv style=\"color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\n\u003ctable style=\"border-spacing:0;padding:0;margin:0;border:0;\"\u003e\u003ctr\u003e\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e1\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e2\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e3\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e4\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e5\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e6\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e7\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;;width:100%\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-dart\" data-lang=\"dart\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#007f7f\"\u003e// 路由跳转\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    Navigator.push(\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      context,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      MaterialPageRoute(builder: (context){\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#fff;font-weight:bold\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#fff;font-weight:bold\"\u003econst\u003c/span\u003e NewRoute();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      })\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    );\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003eMaterialPageRoute继承自PageRoute类，PageRoute类是一个抽象类，表示占有整个屏幕空间的一个模态路由页面，它还定义了路由构建及切换时过渡动画的相关接口及属性。MaterialPageRoute 是 Material组件库提供的组件，它可以针对不同平台，实现与平台页面切换动画风格一致的路由切换动画\u003c/p\u003e","title":"第一个Flutter应用 三"},{"content":"第二章\n有状态与无状态组件 Stateful widget 可以拥有状态，这些状态在 widget 生命周期中是可以变的，而 Stateless widget 是不可变的。 Stateful widget 至少由两个类组成： 一个StatefulWidget类。 一个 State类； StatefulWidget类本身是不变的，但是State类中持有的状态在 widget 生命周期中可能会发生变化。 Widget 接口 Widget定义\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 @immutable // 不可变的 abstract class Widget extends DiagnosticableTree { const Widget({ this.key }); final Key? key; @protected @factory Element createElement(); @override String toStringShort() { final String type = objectRuntimeType(this, \u0026#39;Widget\u0026#39;); return key == null ? type : \u0026#39;$type-$key\u0026#39;; } @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.dense; } @override @nonVirtual bool operator ==(Object other) =\u0026gt; super == other; @override @nonVirtual int get hashCode =\u0026gt; super.hashCode; static bool canUpdate(Widget oldWidget, Widget newWidget) { return oldWidget.runtimeType == newWidget.runtimeType \u0026amp;\u0026amp; oldWidget.key == newWidget.key; } ... } @immutable 代表 Widget 是不可变的，这会限制 Widget 中定义的属性（即配置信息）必须是不可变的（final），为什么不允许 Widget 中定义的属性变化呢？这是因为，Flutter 中如果属性发生变化则会重新构建Widget树，即重新创建新的 Widget 实例来替换旧的 Widget 实例，所以允许 Widget 的属性变化是没有意义的，因为一旦 Widget 自己的属性变了自己就会被替换。这也是为什么 Widget 中定义的属性必须是 final 的原因。 widget类继承自DiagnosticableTree，DiagnosticableTree即“诊断树”，主要作用是提供调试信息。 Key: 这个key属性类似于 React/Vue 中的key，主要的作用是决定是否在下一次build时复用旧的 widget ，决定的条件在canUpdate()方法中。 createElement()：正如前文所述“一个 widget 可以对应多个Element”；Flutter 框架在构建UI树时，会先调用此方法生成对应节点的Element对象。此方法是 Flutter 框架隐式调用的，在我们开发过程中基本不会调用到。 debugFillProperties(\u0026hellip;) 复写父类的方法，主要是设置诊断树的一些特性。 canUpdate(\u0026hellip;)是一个静态方法，它主要用于在 widget 树重新build时复用旧的 widget ，其实具体来说，应该是：是否用新的 widget 对象去更新旧UI树上所对应的Element对象的配置；通过其源码我们可以看到，只要newWidget与oldWidget的runtimeType和key同时相等时就会用new widget去更新Element对象的配置，否则就会创建新的Element。 Flutter 中的四棵树 Flutter渲染流程\n根据 Widget 树生成一个 Element 树，Element 树中的节点都继承自 Element 类。 根据 Element 树生成 Render 树（渲染树），渲染树中的节点都继承自RenderObject 类。 根据渲染树生成 Layer 树，然后上屏显示，Layer 树中的节点都继承自 Layer 类。 负责渲染和布局的是Render Tree\n1 2 3 4 5 6 7 8 9 Container( // 一个容器 widget color: Colors.blue, // 设置容器背景色 child: Row( // 可以将子widget沿水平方向排列 children: [ Image.network(\u0026#39;https://www.example.com/1.png\u0026#39;), // 显示图片的 widget const Text(\u0026#39;A\u0026#39;), ], ), ); 对于上面的代码会变成如下三棵树，其中对于容器设置back会变成cloredBox 三棵树中，Widget 和 Element 是一一对应的，但并不和 RenderObject 一一对应。比如 StatelessWidget 和 StatefulWidget 都没有对应的 RenderObject。 StatelessWidget Context statelessWidget是不需要记录状态的Widget，主要是方法是在build中构建UI配置，build方法有一个context参数，它是BuildContext类的一个实例，表示当前 widget 在 widget 树中的上下文，每一个 widget 都会对应一个 context 对象（因为每一个 widget 都是 widget 树上的一个节点）。实际上，context是当前 widget 在 widget 树中位置中执行”相关操作“的一个句柄(handle)，比如它提供了从当前 widget 开始向上遍历 widget 树以及按照 widget 类型查找父级 widget 的方法。下面是在子树中获取父级 widget 的一个示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class ContextRoute extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(\u0026#34;Context测试\u0026#34;), ), body: Container( child: Builder(builder: (context) { // 在 widget 树中向上查找最近的父级`Scaffold` widget Scaffold scaffold = context.findAncestorWidgetOfExactType\u0026lt;Scaffold\u0026gt;(); // 直接返回 AppBar的title， 此处实际上是Text(\u0026#34;Context测试\u0026#34;) return (scaffold.appBar as AppBar).title; }), ), ); } } StatefulWidget createState() 用于创建和 StatefulWidget 相关的状态，它在StatefulWidget 的生命周期中可能会被多次调用。例如，当一个 StatefulWidget 同时插入到 widget 树的多个位置时，Flutter 框架就会调用该方法为每一个位置生成一个独立的State实例，其实，本质上就是一个StatefulElement对应一个State实例。\nState 一个StatefulWidget类会对应一个State类，State表示与其对应的StatefulWidget要维护的状态，State中的保存的状态信息可以:\n在widget构建时被同步读取 在widget生命周期中可以被改变，当State被改变时，可以手动调用其setState()方法通知Flutter框架状态发生改变，Flutter框架收到状态改变的消息后，会重新调用其build方法构建widget树，从而更新UI State 中有两个常用的属性:\nwidget，它表示与该 State 实例关联的 widget 实例，由Flutter 框架动态设置。注意，这种关联并非永久的，因为在应用生命周期中，UI树上的某一个节点的 widget 实例在重新构建时可能会变化，但State实例只会在第一次插入到树中时被创建，当在重新构建时，如果 widget 被修改了，Flutter 框架会动态设置State. widget 为新的 widget 实例。 context。StatefulWidget对应的 BuildContext，作用同StatelessWidget 的BuildContext。 State 生命周期 initState: 当 widget 第一次插入到 widget 树时会被调用，对于每一个State对象，Flutter 框 架只会调用一次该回调，所以，通常在该回调中做一些一次性的操作，如状态初始化、订阅 子树的事件通知等。不能在该回调中调用 BuildContext.dependOnInheritedWidgetOfExactType（该方法用于在 widget 树上获取离当前 widget 最近的一个父级InheritedWidget，关于InheritedWidget 我们将在后面章节介绍），原因是在初始化完成后， widget 树中的InheritFrom widget也可能会发生变化，所以正确的做法应该在在build（）方法或 didChangeDependencies()中调用它。 didChangeDependencies: 当State对象的依赖发生变化时会被调用；例如：在之前build() 中包含了一个 InheritedWidget （第七章介绍），然后在之后的build() 中Inherited widget发 生了变化，那么此时InheritedWidget的子 widget 的didChangeDependencies() 回调都会被调用。典型的场景是当系统语言 Locale 或应用主题改变时，Flutter 框架 会通知 widget 调用此回调。需要注意，组件第一次被创建后挂载的时候（包括重创建） 对应的didChangeDependencies也会被调用。 build: 此回调读者现在应该已经相当熟悉了，它主要是用于构建 widget 子树的，会在如下场景 被调用： 在调用initState()之后 在调用didUpdateWidget()之后 在调用setState()之后 在调用didChangeDependencies()之后 在State对象从树中一个位置移除后(会调用deactivate)又重新插入到树的其他位置 reassemble: 此回调是专门为了开发调试而提供的，在热重载(hot reload)时会被调用，此回调在Release模式下永远不会被调用。 didUpdateWidget: 在 widget 重新构建时，Flutter 框架会调用widget.canUpdate来检测 widget 树 中同一位置的新旧节点，然后决定是否需要更新，如果widget.canUpdate返回true则会 调用此回调。正如之前所述，widget.canUpdate会在新旧 widget 的 key 和 runtimeType 同时相等时会返回true，也就是说在在新旧 widget 的key和 runtimeType同时相等时didUpdateWidget()就会被调用。 deactivate: 当 State 对象从树中被移除时，会调用此回调。在一些场景下，Flutter 框架会将 State 对象重新插到树中，如包含此 State 对象的子树在树的一个位置移动到另一个位 置时（可以通过GlobalKey 来实现）。如果移除后没有重新插入到树中则紧接着会调用 dispose()方法。 dispose: 当 State 对象从树中被永久移除时调用；通常在此回调中释放资源。 ps: 在继承StatefulWidget重写其方法时，对于包含@mustCallSuper标注的父类方法，都要在子类方法中调用父类方法。\n在Widget树中获取State对象 context获取 context对象有一个findAncestorStateOfType()方法，该方法可以从当前节点沿着 widget 树向上查找指定类型的 StatefulWidget 对应的 State 对象。下面是实现打 开 SnackBar 的示例： 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 class GetStateObjectRoute extends StatefulWidget { const GetStateObjectRoute({Key? key}) : super(key: key); @override State\u0026lt;GetStateObjectRoute\u0026gt; createState() =\u0026gt; _GetStateObjectRouteState(); } class _GetStateObjectRouteState extends State\u0026lt;GetStateObjectRoute\u0026gt; { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(\u0026#34;子树中获取State对象\u0026#34;), ), body: Center( child: Column( children: [ Builder(builder: (context) { return ElevatedButton( onPressed: () { // 查找父级最近的Scaffold对应的ScaffoldState对象 ScaffoldState _state = context.findAncestorStateOfType\u0026lt;ScaffoldState\u0026gt;()!; // 打开抽屉菜单 _state.openDrawer(); }, child: Text(\u0026#39;打开抽屉菜单1\u0026#39;), ); }), ], ), ), drawer: Drawer(), ); } } 在 Flutter 开发中便有了一个默认的约定：如果 StatefulWidget 的状态是希望暴露出的，应当在 StatefulWidget 中提供一个of 静态方法来获取其 State 对象，开发者便可直接通过该方法来获取 1 2 3 4 5 6 7 8 9 10 11 Builder(builder: (context) { return ElevatedButton( onPressed: () { // 直接通过of静态方法来获取ScaffoldState ScaffoldState _state=Scaffold.of(context); // 打开抽屉菜单 _state.openDrawer(); }, child: Text(\u0026#39;打开抽屉菜单2\u0026#39;), ); }), 通过GolbalKey GlobalKey 是 Flutter 提供的一种在整个 App 中引用 element 的机制。如果一个 widget 设置了GlobalKey，那么我们便可以通过globalKey.currentWidget获得该 widget 对象、globalKey.currentElement来获得 widget 对应的element对象， 如果当前 widget 是StatefulWidget，则可以通过globalKey.currentState来获得 该 widget 对应的state对象。 ps: 使用 GlobalKey 开销较大，如果有其他可选方案，应尽量避免使用它。另外，同一 个 GlobalKey 在整个 widget 树中必须是唯一的，不能重复。 使用RenderObject定义Widget 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class CustomWidget extends LeafRenderObjectWidget{ @override RenderObject createRenderObject(BuildContext context) { // 创建 RenderObject return RenderCustomObject(); } @override void updateRenderObject(BuildContext context, RenderCustomObject renderObject) { // 更新 RenderObject super.updateRenderObject(context, renderObject); } } class RenderCustomObject extends RenderBox{ @override void performLayout() { // 实现布局逻辑 } @override void paint(PaintingContext context, Offset offset) { // 实现绘制 } } 常见组件 Text (opens new window)：该组件可让您创建一个带格式的文本。 Row (opens new window)、 Column (opens new window)： 这些具有弹性空间的布局类 widget 可让您在水平（Row）和垂直（Column）方向上创建灵活的布局。其设计是基于 Web 开发中的 Flexbox 布局模型。 Stack (opens new window)： 取代线性布局 (译者语：和 Android 中的FrameLayout相似)，[Stack](https://docs.flutter.dev/flutter/ widgets/Stack-class.html)允许子 widget 堆叠， 你可以使用 Positioned (opens new window)来定位他们相对于Stack的上下左右四条边的位置。Stacks是基于Web开发中的绝对定位（absolute positioning )布局模型设计的。 Container (opens new window)： Container (opens new window)可让您创建矩形视觉元素。Container 可以装饰一个BoxDecoration (opens new window), 如 background、一个边框、或者一个阴影。 Container (opens new window)也可以具有边距（margins）、填充(padding)和应用于其大小的约束(constraints)。另外， Container (opens new window)可以使用矩阵在三维空间中对其进行变换。 Material组件 Cupertino组件 在Widget之上的两个库，是Flutter提供的两种风格的组件库\n","permalink":"https://akashark.github.io/en/posts/read/flutter%E5%AE%9E%E6%88%98/%E7%AC%AC%E4%B8%80%E4%B8%AAflutter%E5%BA%94%E7%94%A8-%E4%B8%80/","summary":"\u003cp\u003e\u003ca href=\"https://book.flutterchina.club/chapter2/first_flutter_app.html#_2-1-1-%E5%88%9B%E5%BB%BAflutter%E5%BA%94%E7%94%A8%E6%A8%A1%E6%9D%BF\"\u003e第二章\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"有状态与无状态组件\"\u003e有状态与无状态组件\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eStateful widget 可以拥有状态，这些状态在 widget 生命周期中是可以变的，而 Stateless widget 是不可变的。\u003c/li\u003e\n\u003cli\u003eStateful widget 至少由两个类组成：\n\u003cul\u003e\n\u003cli\u003e一个StatefulWidget类。\u003c/li\u003e\n\u003cli\u003e一个 State类； StatefulWidget类本身是不变的，但是State类中持有的状态在 widget 生命周期中可能会发生变化。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"widget-接口\"\u003eWidget 接口\u003c/h2\u003e\n\u003cp\u003eWidget定义\u003c/p\u003e","title":"第一个Flutter应用 一"},{"content":"第二章\n状态管理 StatefulWidget的状态管理视情况被管理，通常有一下几种方式\nWidget 管理自己的状态 Widget 管理子widget状态 混合管理 (父Widget和子Widget都管理状态) 如何决定使用哪种管理方式 如果状态是用户数据，如复选框的选中状态、滑块的位置，则该状态最好由父 Widget 管理。 如果状态是有关界面外观效果的，例如颜色、动画，那么状态最好由 Widget 本身来管理。 如果某一个状态是不同 Widget 共享的则最好由它们共同的父 Widget 管理。 在 Widget 内部管理状态封装性会好一些，而在父 Widget 中管理会比较灵活。有些时 候，如果不确定到底该怎么管理状态，那么推荐的首选是在父 Widget 中管理（灵活会显 得更重要一些）。 Widget管理自身状态 _TapboxAState 类:\n管理TapboxA的状态。 定义_active：确定盒子的当前颜色的布尔值。 定义_handleTap()函数，该函数在点击该盒子时更新_active，并调用setState()更新UI。 实现widget的所有交互式行为 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 // TapboxA 管理自身状态. //------------------------- TapboxA ---------------------------------- class TapboxA extends StatefulWidget { TapboxA({Key? key}) : super(key: key); @override _TapboxAState createState() =\u0026gt; _TapboxAState(); } class _TapboxAState extends State\u0026lt;TapboxA\u0026gt; { bool _active = false; void _handleTap() { setState(() { _active = !_active; }); } Widget build(BuildContext context) { return GestureDetector( onTap: _handleTap, child: Container( child: Center( child: Text( _active ? \u0026#39;Active\u0026#39; : \u0026#39;Inactive\u0026#39;, style: TextStyle(fontSize: 32.0, color: Colors.white), ), ), width: 200.0, height: 200.0, decoration: BoxDecoration( color: _active ? Colors.lightGreen[700] : Colors.grey[600], ), ), ); } } 父Widget管理子Widget的状态 在以下示例中，TapboxB通过回调将其状态导出到其父组件，状态由父组件管理，因此它的父组件为StatefulWidget。但是由于TapboxB不管理任何状态，所以TapboxB为StatelessWidget。 ParentWidgetState 类:\n为TapboxB 管理_active状态。 实现_handleTapboxChanged()，当盒子被点击时调用的方法。 当状态改变时，调用setState()更新UI。 TapboxB 类: 继承StatelessWidget类，因为所有状态都由其父组件处理。 当检测到点击时，它会通知父组件。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 // ParentWidget 为 TapboxB 管理状态. //------------------------ ParentWidget -------------------------------- class ParentWidget extends StatefulWidget { @override _ParentWidgetState createState() =\u0026gt; _ParentWidgetState(); } class _ParentWidgetState extends State\u0026lt;ParentWidget\u0026gt; { bool _active = false; void _handleTapboxChanged(bool newValue) { setState(() { _active = newValue; }); } @override Widget build(BuildContext context) { return Container( child: TapboxB( active: _active, onChanged: _handleTapboxChanged, ), ); } } //------------------------- TapboxB ---------------------------------- class TapboxB extends StatelessWidget { TapboxB({Key? key, this.active: false, required this.onChanged}) : super(key: key); final bool active; final ValueChanged\u0026lt;bool\u0026gt; onChanged; void _handleTap() { onChanged(!active); } Widget build(BuildContext context) { return GestureDetector( onTap: _handleTap, child: Container( child: Center( child: Text( active ? \u0026#39;Active\u0026#39; : \u0026#39;Inactive\u0026#39;, style: TextStyle(fontSize: 32.0, color: Colors.white), ), ), width: 200.0, height: 200.0, decoration: BoxDecoration( color: active ? Colors.lightGreen[700] : Colors.grey[600], ), ), ); } } 混合状态管理 对于一些组件来说，混合管理的方式会非常有用。在这种情况下，组件自身管理一些内部状态，而父组件管理一些其他外部状态。 在下面 TapboxC 示例中，手指按下时，盒子的周围会出现一个深绿色的边框，抬起时，边框消失。点击完成后，盒子的颜色改变。 TapboxC 将其_active状态导出到其父组件中，但在内部管理其_highlight状态。这个例子有两个状态对象_ParentWidgetState和_TapboxCState。 _ParentWidgetStateC类:\n管理_active 状态。 实现 _handleTapboxChanged() ，当盒子被点击时调用。 当点击盒子并且_active状态改变时调用setState()更新UI。 _TapboxCState 对象:\n管理_highlight 状态。 GestureDetector监听所有tap事件。当用户点下时，它添加高亮（深绿色边框）；当用户释放时，会移除高亮。 当按下、抬起、或者取消点击时更新_highlight状态，调用setState()更新UI。 当点击时，将状态的改变传递给父组件。 整体Demo代码如下\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 //---------------------------- ParentWidget ---------------------------- class ParentWidgetC extends StatefulWidget { @override _ParentWidgetCState createState() =\u0026gt; _ParentWidgetCState(); } class _ParentWidgetCState extends State\u0026lt;ParentWidgetC\u0026gt; { bool _active = false; void _handleTapboxChanged(bool newValue) { setState(() { _active = newValue; }); } @override Widget build(BuildContext context) { return Container( child: TapboxC( active: _active, onChanged: _handleTapboxChanged, ), ); } } //----------------------------- TapboxC ------------------------------ class TapboxC extends StatefulWidget { TapboxC({Key? key, this.active: false, required this.onChanged}) : super(key: key); final bool active; final ValueChanged\u0026lt;bool\u0026gt; onChanged; @override _TapboxCState createState() =\u0026gt; _TapboxCState(); } class _TapboxCState extends State\u0026lt;TapboxC\u0026gt; { bool _highlight = false; void _handleTapDown(TapDownDetails details) { setState(() { _highlight = true; }); } void _handleTapUp(TapUpDetails details) { setState(() { _highlight = false; }); } void _handleTapCancel() { setState(() { _highlight = false; }); } void _handleTap() { widget.onChanged(!widget.active); } @override Widget build(BuildContext context) { // 在按下时添加绿色边框，当抬起时，取消高亮 return GestureDetector( onTapDown: _handleTapDown, // 处理按下事件 onTapUp: _handleTapUp, // 处理抬起事件 onTap: _handleTap, onTapCancel: _handleTapCancel, child: Container( child: Center( child: Text( widget.active ? \u0026#39;Active\u0026#39; : \u0026#39;Inactive\u0026#39;, style: TextStyle(fontSize: 32.0, color: Colors.white), ), ), width: 200.0, height: 200.0, decoration: BoxDecoration( color: widget.active ? Colors.lightGreen[700] : Colors.grey[600], border: _highlight ? Border.all( color: Colors.teal[700], width: 10.0, ) : null, ), ), ); } } 全局状态管理 实现一个全局的事件总线，将语言状态改变对应为一个事件，然后在APP中依赖应用语言的组件的initState 方法中订阅语言改变的事件。当用户在设置页切换语言后，我们发布语言改变事件，而订阅了此事件的组件就会收到通知，收到通知后调用setState(\u0026hellip;)方法重新build一下自身即可。 使用一些专门用于状态管理的包，如 Provider、Redux，读者可以在 pub 上查看其详细信息。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 import \u0026#39;package:flutter/material.dart\u0026#39;; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: \u0026#39;Flutter Demo\u0026#39;, theme: ThemeData( // This is the theme of your application. // // Try running your application with \u0026#34;flutter run\u0026#34;. You\u0026#39;ll see the // application has a blue toolbar. Then, without quitting the app, try // changing the primarySwatch below to Colors.green and then invoke // \u0026#34;hot reload\u0026#34; (press \u0026#34;r\u0026#34; in the console where you ran \u0026#34;flutter run\u0026#34;, // or simply save your changes to \u0026#34;hot reload\u0026#34; in a Flutter IDE). // Notice that the counter didn\u0026#39;t reset back to zero; the application // is not restarted. primarySwatch: Colors.blue, ), home: const MyHomePage(title: \u0026#39;Flutter Demo Home Page\u0026#39;), ); } } class MyHomePage extends StatefulWidget { const MyHomePage({super.key, required this.title}); // This widget is the home page of your application. It is stateful, meaning // that it has a State object (defined below) that contains fields that affect // how it looks. // This class is the configuration for the state. It holds the values (in this // case the title) provided by the parent (in this case the App widget) and // used by the build method of the State. Fields in a Widget subclass are // always marked \u0026#34;final\u0026#34;. final String title; // 对于StatefulWidget自动会执行creatState 但在生命周期的那个时刻呢 @override State\u0026lt;MyHomePage\u0026gt; createState() =\u0026gt; _MyHomePageState(); } class _MyHomePageState extends State\u0026lt;MyHomePage\u0026gt; { int _counter = 0; void _incrementCounter() { setState(() { // This call to setState tells the Flutter framework that something has // changed in this State, which causes it to rerun the build method below // so that the display can reflect the updated values. If we changed // _counter without calling setState(), then the build method would not be // called again, and so nothing would appear to happen. _counter++; }); } @override Widget build(BuildContext context) { // This method is rerun every time setState is called, for instance as done // by the _incrementCounter method above. // // The Flutter framework has been optimized to make rerunning build methods // fast, so that you can just rebuild anything that needs updating rather // than having to individually change instances of widgets. return Scaffold( appBar: AppBar( // Here we take the value from the MyHomePage object that was created by // the App.build method, and use it to set our appbar title. title: Text(widget.title), ), body: Center( // Center is a layout widget. It takes a single child and positions it // in the middle of the parent. child: Column( // Column is also a layout widget. It takes a list of children and // arranges them vertically. By default, it sizes itself to fit its // children horizontally, and tries to be as tall as its parent. // // Invoke \u0026#34;debug painting\u0026#34; (press \u0026#34;p\u0026#34; in the console, choose the // \u0026#34;Toggle Debug Paint\u0026#34; action from the Flutter Inspector in Android // Studio, or the \u0026#34;Toggle Debug Paint\u0026#34; command in Visual Studio Code) // to see the wireframe for each widget. // // Column has various properties to control how it sizes itself and // how it positions its children. Here we use mainAxisAlignment to // center the children vertically; the main axis here is the vertical // axis because Columns are vertical (the cross axis would be // horizontal). mainAxisAlignment: MainAxisAlignment.center, children: \u0026lt;Widget\u0026gt;[ TaboxA(), ParentWidget(), ParentWidgetC(), ], ), ), floatingActionButton: FloatingActionButton( onPressed: _incrementCounter, tooltip: \u0026#39;Increment\u0026#39;, child: const Icon(Icons.add), ), ); } } class TaboxA extends StatefulWidget { const TaboxA({Key? key}) : super(key: key); @override State\u0026lt;TaboxA\u0026gt; createState() =\u0026gt; _TaboxAState(); } class _TaboxAState extends State\u0026lt;TaboxA\u0026gt; { bool _active = false; // Widget自己管理状态 _handleTap() { setState(() { _active = !_active; }); } @override Widget build(BuildContext context) { return GestureDetector( onTap: _handleTap, child: Container( width: 200.0, height: 200.0, decoration: BoxDecoration( color: _active ? Colors.lightGreen[700] : Colors.grey[600], ), child: Center( child: Text( _active ? \u0026#34;Active\u0026#34; : \u0026#34;Inactive\u0026#34;, style: const TextStyle( fontSize: 32.0, color: Colors.white, ), ), ), ), ); } } // 交给父类管理状态 class ParentWidget extends StatefulWidget { const ParentWidget({Key? key}) : super(key: key); @override State\u0026lt;ParentWidget\u0026gt; createState() =\u0026gt; _ParentWidgetState(); } class _ParentWidgetState extends State\u0026lt;ParentWidget\u0026gt; { bool _active = false; _handleTapboxChanged(bool newValue) { if (newValue == _active) return; setState(() { _active = newValue; }); } @override Widget build(BuildContext context) { return Container( child: TapBoxB( active: _active, onChanged: _handleTapboxChanged, ), ); } } class TapBoxB extends StatelessWidget { const TapBoxB({Key? key, required this.active, required this.onChanged}) : super(key: key); final bool active; final ValueChanged\u0026lt;bool\u0026gt; onChanged; _handleTap() { onChanged(!active); } @override Widget build(BuildContext context) { return GestureDetector( onTap: _handleTap, child: Container( width: 200.0, height: 200.0, decoration: BoxDecoration( color: active ? Colors.lightGreen[700] : Colors.grey[600], ), child: Center( child: Text( active ? \u0026#34;Active\u0026#34; : \u0026#34;Inactive\u0026#34;, style: const TextStyle( fontSize: 32.0, color: Colors.white, ), ), ), ), ); } } // 混合管理模式 class ParentWidgetC extends StatefulWidget { @override State\u0026lt;ParentWidgetC\u0026gt; createState() =\u0026gt; _ParentWidgetCState(); } // 管理active状态 class _ParentWidgetCState extends State\u0026lt;ParentWidgetC\u0026gt; { bool _active = false; _handleTapboxChanged(bool newValue) { if (newValue == _active) return ; setState(() { _active = newValue; }); } @override Widget build(BuildContext context) { return Container( child: TapBoxC( active: _active, activeOnChanged: _handleTapboxChanged, ), ); } } class TapBoxC extends StatefulWidget { const TapBoxC({Key? key, required this.active, required this.activeOnChanged}) : super(key: key); // 父widget管理的状态 final bool active; final ValueChanged\u0026lt;bool\u0026gt; activeOnChanged; @override State\u0026lt;TapBoxC\u0026gt; createState() =\u0026gt; _TapBoxCState(); } // 管理highlight状态 class _TapBoxCState extends State\u0026lt;TapBoxC\u0026gt; { bool _highlight = false; void _handleTapDown(TapDownDetails details) { setState(() { _highlight = true; }); } void _handleTapUp(TapUpDetails details) { setState(() { _highlight = false; }); } void _handleTapCancel() { setState(() { _highlight = false; }); } void _handleTap() { // 回调active状态 widget.activeOnChanged(!widget.active); } @override Widget build(BuildContext context) { return GestureDetector( onTapDown: _handleTapDown, // 处理按下事件 onTapUp: _handleTapUp, // 处理抬起事件 onTap: _handleTap, onTapCancel: _handleTapCancel, child: Container( width: 200.0, height: 200.0, decoration: BoxDecoration( color: widget.active ? Colors.lightGreen[700] : Colors.grey[600], border: _highlight ? Border.all( color: Colors.teal, width: 10.0 ): null, ), child: Center( child: Text( widget.active ? \u0026#39;Active\u0026#39; : \u0026#39;Inactive\u0026#39;, style: const TextStyle(fontSize: 32.0, color: Colors.white), ), ), ), ); } } ","permalink":"https://akashark.github.io/en/posts/read/flutter%E5%AE%9E%E6%88%98/%E7%AC%AC%E4%B8%80%E4%B8%AAflutter%E5%BA%94%E7%94%A8-%E4%BA%8C/","summary":"\u003cp\u003e\u003ca href=\"https://book.flutterchina.club/chapter2/first_flutter_app.html#_2-1-1-%E5%88%9B%E5%BB%BAflutter%E5%BA%94%E7%94%A8%E6%A8%A1%E6%9D%BF\"\u003e第二章\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"状态管理\"\u003e状态管理\u003c/h2\u003e\n\u003cp\u003eStatefulWidget的状态管理视情况被管理，通常有一下几种方式\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eWidget 管理自己的状态\u003c/li\u003e\n\u003cli\u003eWidget 管理子widget状态\u003c/li\u003e\n\u003cli\u003e混合管理 (父Widget和子Widget都管理状态)\n如何决定使用哪种管理方式\u003c/li\u003e\n\u003cli\u003e如果状态是用户数据，如复选框的选中状态、滑块的位置，则该状态最好由父 Widget 管理。\u003c/li\u003e\n\u003cli\u003e如果状态是有关界面外观效果的，例如颜色、动画，那么状态最好由 Widget 本身来管理。\u003c/li\u003e\n\u003cli\u003e如果某一个状态是不同 Widget 共享的则最好由它们共同的父 Widget 管理。\n在 Widget 内部管理状态封装性会好一些，而在父 Widget 中管理会比较灵活。有些时\n候，如果不确定到底该怎么管理状态，那么推荐的首选是在父 Widget 中管理（灵活会显\n得更重要一些）。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"widget管理自身状态\"\u003eWidget管理自身状态\u003c/h3\u003e\n\u003cp\u003e_TapboxAState 类:\u003c/p\u003e","title":"第一个Flutter应用 二"},{"content":"永澄：任务、目标和计划都是啥？它们之间啥关系？\n行动(Action) 行动，指的是通过一步操作即可完成的、不可细分的最小事项，活动，也可以理解为执行的最小单元。 对于任务规划目标来说只有行动才是可以被执行的，这是任务管理中的基本原则，任何任务，目标，计划都需要转化成行动才能被执行。 行动概念的用法，通常有两种:\n规范描述 当你提到行动时，其中必须包含动词 落实任务 用话术的方式来帮助自己思考某任务是否可以落地，话术为:\u0026ldquo;现在已经分解到行动层级了 么? 可以执行了么?\u0026rdquo; 在拆分好行动后问下自己，是否每个拆分出来的小任务都是可以被执 行的 ps: 一件事是否是行动，你需要真诚地对待自己，不需要假装自己很厉害，把所有事情都看的太简单，也不需要严格分解所有的事项，那样管理成本就太高了。让这件事处于一个合理的管理成本区间就够了。也就是说，管理成本区间决定着一件事对自己来说究竟是不是行动。\n任务(Task) 任务是由若干行动有机组合而形成的事件/活动集合，任务可以通过一定方式分解成行动。 只要不能一步完成的事项/活动都是任务\n分解任务\n若干行动，若干指的是不定量，一个或者多个，所以这里隐藏这一点，存在由一个行动构成的任务 有机组合的集合，不是说把任务放在一起就是任务了，任务有其整体性，我们用最简单的集合来理解: 把两条行动“吃饭”、“买票”放在一起组成一个任务“去餐厅吃饭”，那这个任务本身也是可以被理解的。如果行动组合成的任务不能被理解，那就不叫任务，比如说“一边在家吃饭，一边在电影院排队买票”就超出认知范围，这就不是“有机”组合。也可以这么说，任务是由行动构成的有机整体。正如上图展示的那样，组合在一起。 一定方式分解。任务是可以被分解成行动的，但是必须要通过一定方式来分解，不同人掌握的“方式”不同，把任务分解成行动（就是所谓的“任务分解”）的过程也是不同的，所以，即便同样的任务，不同人的处理方式也是不同的。另外，这一条还说明，如果你不具备“方式”，很可能有些任务是无法分解的，比如说“先赚一个亿”，不能分解就不能执行，就只能卡在那。引申一下：如果你发现自己卡住了，不要抱怨/低落/挫败，而是要去找到有效的“方式” 总结下: 从整体上看，任务是一个整体，打开他的表面，他还是若干任务构成的有机集合，这个集合可以通过某种方式分解成一个个的行动，之所以有任务，就是因为通过实施任务，可以得到结果，而那个结果是我们想要的目的(Purpose)，任务只是稻城结果的执行载体。\n目标 目标是任务的固有属性 我理解就是一个任务的指定是有一个或者多个目标的，比如我吃饭目标就是晚上吃饱了 即便是执行同一件任务(行为), 因为目的(Purpose)不同，所以需要达成的目标也是不同的。\n目标是眼睛中可以看到的标准，用这个定义可以解释SMART原则； 目标很复杂，为降低管理成本，要从一个点切入理解，这个点是“任务和目标的关系”； 目标是任务的属性，任务和目标的关系是：任务是对象、目标是属性； 任务的目标属性有不同的值，任务之所以体现出不同的目标属性，是由执行任务的人背后的目的所决定的，目的不同，即便同一件任务，目标也不同。 PORT模型中的O-目标，其实蕴含在任务之中，根据目的不同，目标也不同。这就是“做任何事前必须要澄清目的”的底层逻辑。 ps: 任务和目标的关系，必须是现有目的，后有任务，再有目标，按照细分流程刻意联系，只有练习多了，就能提出整体目标。\n计划 计划是为了达成任务的整体目标而对任务分解出的行动进行排序的方式和结果。\n为什么要有计划？为了实现任务的整体目标。 计划是什么？是方法，作为动词使用，计划（安排）某某事情；也是结果，作为名词使用，做出一份计划。当它是动词使用时，主要对行动进行排序，排序之后的结果，就是名词形式。 排序是什么意思？通常来说要对行动的先后关系、重要程度、执行方式进行考虑，最终排列行动的串并行执行关系，就是排序。 总结 每个人都有自己想要的（想得到-期待导向、想逃避-问题导向），但是水平不同，效能不同：水平低的就会瞎做事情，以为自己可以得到想要的结果，这种无意识、无方法的人效能通常很低。\n来看看高水平的人——他是既知道自己要什么、还知道怎么去得到的人（做对的事，把事做对），他的做法是：先去分析如果想要实现目的，需要得到哪些结果；通过结果推导出任务以及任务所需要达成的目标；之后把任务分解至可以执行的行动；再对行动进行排序形成计划；按照计划去执行最终达成目的。\n这是一个完整的流程，具体执行的时候要根据目的和任务的大小来选择相应的标准，哪个环节要做成什么样的，哪个环节可以省略，这些标准的形成来自于刻意训练。这是所有自我效能提升类学科（目标、时间、任务、行动管理）的底层逻辑，对于上图标准的把握、对流程的应用能力，直接体现出一个人的自我管理能力，也决定了一个人的效能水平。\n","permalink":"https://akashark.github.io/en/posts/life/%E8%87%AA%E6%88%91%E6%88%90%E9%95%BF/%E4%BB%BB%E5%8A%A1%E7%9B%AE%E6%A0%87%E8%AE%A1%E5%88%92/","summary":"\u003cp\u003e\u003ca href=\"https://mp.weixin.qq.com/s/AkkLJvfYq8T8N-UWkBJY_w?\"\u003e永澄：任务、目标和计划都是啥？它们之间啥关系？\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"行动action\"\u003e行动(Action)\u003c/h2\u003e\n\u003cp\u003e行动，指的是通过一步操作即可完成的、不可细分的最小事项，活动，也可以理解为执行的最小单元。\n对于任务规划目标来说只有行动才是可以被执行的，这是任务管理中的基本原则，任何任务，目标，计划都需要转化成行动才能被执行。\n行动概念的用法，通常有两种:\u003c/p\u003e","title":"任务目标计划"},{"content":"第一章 起步 弹射起步 变量声明 var 声明变量，但是类型后面不可以改变了，根据第一次赋值数据的类型来推断其类型，编译结束后其类型就已经被确定 dynamic 和 Object 声明动态变量，后面类型也可以改变，不同点在于dynamic可以调用可以用的属性(有运行时风险)，Object 只能调用Object提供的属性 final const final是运行时常量，const是编译时常量，同时用final或者const修饰的变量可以不加类型 空类型 安全 1 2 3 4 5 6 7 int i = 8; //默认为不可空，必须在定义时初始化。 int? j; // 定义为可空类型，对于可空变量，我们在使用前必须判空。 // 如果我们预期变量不能为空，但在定义时不能确定其初始值，则可以加上late关键字， // 表示会稍后初始化，但是在正式使用它之前必须得保证初始化过了，否则会报错 late int k; k=9; 函数 函数式编程 Dart函数声明如果没有显式声明返回值类型时会默认当做dynamic处理，返回值没有类型推断 支持箭头函数，只有一个语句的函数，使用箭头函数简写 函数做为变量 1 2 3 4 var say = (str){ print(str); }; say(\u0026#34;hi world\u0026#34;); 函数作为参数传递 1 2 3 4 void execute(var callback) { callback(); } execute(() =\u0026gt; print(\u0026#34;xxx\u0026#34;)) 可选的位置参数 包装一组函数参数，用[]标记为可选的位置参数，并放在参数列表的最后面： 1 2 3 4 5 6 7 String say(String from, String msg, [String? device]) { var result = \u0026#39;$from says $msg\u0026#39;; if (device != null) { result = \u0026#39;$result with a $device\u0026#39;; } return result; } 可选的命名参数 定义函数时，使用{param1, param2, …}，放在参数列表的最后面，用于指定命名参数。例如： 1 2 3 4 //设置[bold]和[hidden]标志 void enableFlags({bool bold, bool hidden}) { // ... } mixin Dart 是不支持多继承的，但是它支持 mixin，简单来讲 mixin 可以 “组合” 多个类，我们通过一个例子来理解。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 class Person { say() { print(\u0026#39;say\u0026#39;); } } mixin Eat { eat() { print(\u0026#39;eat\u0026#39;); } } mixin Walk { walk() { print(\u0026#39;walk\u0026#39;); } } mixin Code { code() { print(\u0026#39;key\u0026#39;); } } class Dog with Eat, Walk{} class Man extends Person with Eat, Walk, Code{} ps：\n不能同时使用位置参数和命名参数 我们定义了几个 mixin，然后通过 with 关键字将它们组合成不同的类。有一点需要注意：如果多个mixin 中有同名方法，with 时，会默认使用最后面的 mixin 的，mixin 方法中可以通过 super 关键字调用之前 mixin 或类中的方法。 异步支持 Future Future.then 为了方便示例，在本例中我们使用Future.delayed 创建了一个延时任务（实际场景会是一个真正的耗时任务，比如一次网络请求），即2秒后返回结果字符串\u0026quot;hi world!\u0026quot;，然后我们在then中接收异步结果并打印结果，代码如下：\n1 2 3 4 5 Future.delayed(Duration(seconds: 2),(){ return \u0026#34;hi world!\u0026#34;; }).then((data){ print(data); }); Future.catchError 1 2 3 4 5 6 7 8 9 10 Future.delayed(Duration(seconds: 2),(){ //return \u0026#34;hi world!\u0026#34;; throw AssertionError(\u0026#34;Error\u0026#34;); }).then((data){ //执行成功会走到这里 print(\u0026#34;success\u0026#34;); }).catchError((e){ //执行失败会走到这里 print(e); }); 或者使用then的onError可选参数\n1 2 3 4 5 6 7 8 Future.delayed(Duration(seconds: 2), () { //return \u0026#34;hi world!\u0026#34;; throw AssertionError(\u0026#34;Error\u0026#34;); }).then((data) { print(\u0026#34;success\u0026#34;); }, onError: (e) { print(e); }); Future.whenComplete 无论成功失败都走的块\n1 2 3 4 5 6 7 8 9 10 11 12 Future.delayed(Duration(seconds: 2),(){ //return \u0026#34;hi world!\u0026#34;; throw AssertionError(\u0026#34;Error\u0026#34;); }).then((data){ //执行成功会走到这里 print(data); }).catchError((e){ //执行失败会走到这里 print(e); }).whenComplete((){ //无论成功或失败都会走到这里 }); Future.wait 类似于GCD group\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 Future.wait([ // 2秒后返回结果 Future.delayed(Duration(seconds: 2), () { return \u0026#34;hello\u0026#34;; }), // 4秒后返回结果 Future.delayed(Duration(seconds: 4), () { return \u0026#34; world\u0026#34;; }) ]).then((results){ print(results[0]+results[1]); }).catchError((e){ print(e); }); 执行上面代码，4秒后你会在控制台中看到“hello world”。 里面的两个任务都执行完成后才会走到then\nasync/await 避免回调低于，用同步的写法写出异步的代码 回调地狱\n1 2 3 4 5 6 7 8 9 10 login(\u0026#34;alice\u0026#34;,\u0026#34;******\u0026#34;).then((id){ //登录成功后通过，id获取用户信息 getUserInfo(id).then((userInfo){ //获取用户信息后保存 saveUserInfo(userInfo).then((){ //保存用户信息，接下来执行其他操作 ... }); }); }) 使用Future写出 Callback Hell 1 2 3 4 5 6 7 8 9 10 login(\u0026#34;alice\u0026#34;,\u0026#34;******\u0026#34;).then((id){ return getUserInfo(id); }).then((userInfo){ return saveUserInfo(userInfo); }).then((e){ //执行接下来的操作 }).catchError((e){ //错误处理 print(e); }); Future 的所有API的返回值仍然是一个Future对象，所以可以很方便的进行链式调用” ，如果在then 中返回的是一个Future的话，该future会执行，执行结束后会触发后面的then回调，这样依次向下，就避免了层层嵌套。\n使用 async/await 消除callback hell 1 2 3 4 5 6 7 8 9 10 task() async { try{ String id = await login(\u0026#34;alice\u0026#34;,\u0026#34;******\u0026#34;); String userInfo = await getUserInfo(id); await saveUserInfo(userInfo); //执行接下来的操作 } catch(e){ //错误处理 print(e); } async用来表示函数是异步的，定义的函数会返回一个Future对象，可以使用 then 方法添加回调函数。 await 后面是一个Future，表示等待该异步任务完成，异步完成后才会往下走；await必须出现在 async 函数内部。 其实，无论是在 JavaScript 还是 Dart 中，async/await 都只是一个语法糖， 译器或解释器最终都会将其转化为一个 Promise（Future）的调用链。\nStream Stream 也是用于接收异步事件数据，和 Future 不同的是，它可以接收多个异步操作的结果（成功或失败）。 也就是说，在执行异步任务时，可以通过多次触发成功或失败事件来传递结果数据或错误异常。 Stream 常用于会多次读取数据的异步任务场景，如网络内容下载、文件读写等。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Stream.fromFutures([ // 1秒后返回结果 Future.delayed(Duration(seconds: 1), () { return \u0026#34;hello 1\u0026#34;; }), // 抛出一个异常 Future.delayed(Duration(seconds: 2),(){ throw AssertionError(\u0026#34;Error\u0026#34;); }), // 3秒后返回结果 Future.delayed(Duration(seconds: 3), () { return \u0026#34;hello 3\u0026#34;; }) ]).listen((data){ print(data); }, onError: (e){ print(e.message); },onDone: (){ }); 输出如下: I/flutter (17666): hello 1 I/flutter (17666): Error I/flutter (17666): hello 3\n","permalink":"https://akashark.github.io/en/posts/read/flutter%E5%AE%9E%E6%88%98/%E8%B5%B7%E6%AD%A5/","summary":"\u003ch1 id=\"第一章-起步\"\u003e第一章 起步\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://book.flutterchina.club/chapter1/mobile_development_intro.html\"\u003e弹射起步\u003c/a\u003e\n\u003cimg loading=\"lazy\" src=\"http://media.wjbbf.cn/mweb/16647787422077.jpg\" alt=\"\"  /\u003e\n\u003c/p\u003e\n\u003ch2 id=\"变量声明\"\u003e变量声明\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003evar 声明变量，但是类型后面不可以改变了，根据第一次赋值数据的类型来推断其类型，编译结束后其类型就已经被确定\u003c/li\u003e\n\u003cli\u003edynamic 和 Object 声明动态变量，后面类型也可以改变，不同点在于dynamic可以调用可以用的属性(有运行时风险)，Object 只能调用Object提供的属性\u003c/li\u003e\n\u003cli\u003efinal const final是运行时常量，const是编译时常量，同时用final或者const修饰的变量可以不加类型\u003c/li\u003e\n\u003cli\u003e空类型 安全\u003c/li\u003e\n\u003c/ul\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv style=\"color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\n\u003ctable style=\"border-spacing:0;padding:0;margin:0;border:0;\"\u003e\u003ctr\u003e\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e1\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e2\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e3\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e4\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e5\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e6\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e7\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;;width:100%\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-dart\" data-lang=\"dart\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff;font-weight:bold\"\u003eint\u003c/span\u003e i = \u003cspan style=\"color:#ff0;font-weight:bold\"\u003e8\u003c/span\u003e; \u003cspan style=\"color:#007f7f\"\u003e//默认为不可空，必须在定义时初始化。\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff;font-weight:bold\"\u003eint\u003c/span\u003e? j; \u003cspan style=\"color:#007f7f\"\u003e// 定义为可空类型，对于可空变量，我们在使用前必须判空。\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#007f7f\"\u003e// 如果我们预期变量不能为空，但在定义时不能确定其初始值，则可以加上late关键字，\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#007f7f\"\u003e// 表示会稍后初始化，但是在正式使用它之前必须得保证初始化过了，否则会报错\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003elate \u003cspan style=\"color:#fff;font-weight:bold\"\u003eint\u003c/span\u003e k;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ek=\u003cspan style=\"color:#ff0;font-weight:bold\"\u003e9\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch2 id=\"函数\"\u003e函数\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e函数式编程\u003c/li\u003e\n\u003cli\u003eDart函数声明如果没有显式声明返回值类型时会默认当做dynamic处理，返回值没有类型推断\u003c/li\u003e\n\u003cli\u003e支持箭头函数，只有一个语句的函数，使用箭头函数简写\u003c/li\u003e\n\u003cli\u003e函数做为变量\u003c/li\u003e\n\u003c/ul\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv style=\"color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\n\u003ctable style=\"border-spacing:0;padding:0;margin:0;border:0;\"\u003e\u003ctr\u003e\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e1\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e2\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e3\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e4\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;;width:100%\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-dart\" data-lang=\"dart\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff;font-weight:bold\"\u003evar\u003c/span\u003e say = (str){\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  print(str);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e};\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003esay(\u003cspan style=\"color:#0ff;font-weight:bold\"\u003e\u0026#34;hi world\u0026#34;\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cul\u003e\n\u003cli\u003e函数作为参数传递\u003c/li\u003e\n\u003c/ul\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv style=\"color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\n\u003ctable style=\"border-spacing:0;padding:0;margin:0;border:0;\"\u003e\u003ctr\u003e\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e1\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e2\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e3\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e4\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;;width:100%\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-gdscript3\" data-lang=\"gdscript3\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003evoid execute(\u003cspan style=\"color:#fff;font-weight:bold\"\u003evar\u003c/span\u003e callback) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    callback();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eexecute(() =\u0026gt; \u003cspan style=\"color:#fff;font-weight:bold\"\u003eprint\u003c/span\u003e(\u003cspan style=\"color:#0ff;font-weight:bold\"\u003e\u0026#34;xxx\u0026#34;\u003c/span\u003e))\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cul\u003e\n\u003cli\u003e可选的位置参数 包装一组函数参数，用[]标记为可选的位置参数，并放在参数列表的最后面：\u003c/li\u003e\n\u003c/ul\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv style=\"color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\n\u003ctable style=\"border-spacing:0;padding:0;margin:0;border:0;\"\u003e\u003ctr\u003e\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e1\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e2\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e3\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e4\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e5\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e6\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e7\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;;width:100%\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-c\" data-lang=\"c\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eString say(String from, String msg, [String? device]) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  var result = \u003cspan style=\"color:#f00\"\u003e\u0026#39;$\u003c/span\u003efrom says \u003cspan style=\"color:#f00\"\u003e$\u003c/span\u003emsg\u003cspan style=\"color:#f00\"\u003e\u0026#39;\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#fff;font-weight:bold\"\u003eif\u003c/span\u003e (device != null) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    result = \u003cspan style=\"color:#f00\"\u003e\u0026#39;$\u003c/span\u003eresult with a \u003cspan style=\"color:#f00\"\u003e$\u003c/span\u003edevice\u003cspan style=\"color:#f00\"\u003e\u0026#39;\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#fff;font-weight:bold\"\u003ereturn\u003c/span\u003e result;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cul\u003e\n\u003cli\u003e可选的命名参数 定义函数时，使用{param1, param2, …}，放在参数列表的最后面，用于指定命名参数。例如：\u003c/li\u003e\n\u003c/ul\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv style=\"color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\n\u003ctable style=\"border-spacing:0;padding:0;margin:0;border:0;\"\u003e\u003ctr\u003e\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e1\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e2\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e3\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e4\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;;width:100%\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-dart\" data-lang=\"dart\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#007f7f\"\u003e//设置[bold]和[hidden]标志\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff;font-weight:bold\"\u003evoid\u003c/span\u003e enableFlags({\u003cspan style=\"color:#fff;font-weight:bold\"\u003ebool\u003c/span\u003e bold, \u003cspan style=\"color:#fff;font-weight:bold\"\u003ebool\u003c/span\u003e hidden}) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#007f7f\"\u003e// ... \n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cul\u003e\n\u003cli\u003emixin Dart 是不支持多继承的，但是它支持 mixin，简单来讲 mixin 可以 “组合” 多个类，我们通过一个例子来理解。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv style=\"color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\n\u003ctable style=\"border-spacing:0;padding:0;margin:0;border:0;\"\u003e\u003ctr\u003e\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e 1\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e 2\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e 3\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e 4\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e 5\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e 6\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e 7\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e 8\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e 9\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e10\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e11\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e12\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e13\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e14\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e15\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e16\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e17\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e18\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e19\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e20\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e21\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e22\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e23\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e24\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e25\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272\"\u003e26\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;;width:100%\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;\"\u003e\u003ccode class=\"language-dart\" data-lang=\"dart\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff;font-weight:bold\"\u003eclass\u003c/span\u003e Person {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  say() {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    print(\u003cspan style=\"color:#0ff;font-weight:bold\"\u003e\u0026#39;say\u0026#39;\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003emixin Eat {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  eat() {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    print(\u003cspan style=\"color:#0ff;font-weight:bold\"\u003e\u0026#39;eat\u0026#39;\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003emixin Walk {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  walk() {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    print(\u003cspan style=\"color:#0ff;font-weight:bold\"\u003e\u0026#39;walk\u0026#39;\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003emixin Code {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  code() {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    print(\u003cspan style=\"color:#0ff;font-weight:bold\"\u003e\u0026#39;key\u0026#39;\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff;font-weight:bold\"\u003eclass\u003c/span\u003e Dog \u003cspan style=\"color:#fff;font-weight:bold\"\u003ewith\u003c/span\u003e Eat, Walk{}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#fff;font-weight:bold\"\u003eclass\u003c/span\u003e Man \u003cspan style=\"color:#fff;font-weight:bold\"\u003eextends\u003c/span\u003e Person \u003cspan style=\"color:#fff;font-weight:bold\"\u003ewith\u003c/span\u003e Eat, Walk, Code{}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003eps：\u003c/p\u003e","title":"起步"},{"content":"背景 最近有两件事情对我感触还是挺大的，第一件是在字节的转正实习转正，第二件是转正后2天的研究生导师组会。为啥这两件事情会对我感触比较大呢，总的来说这两件事情的本质其实是相同的，都是完成一部分任务，对于现在完成的情况遇到的问题细节等作输出汇报，汇报的对象一个是+1 +2的领导，一个是自己的研究生导师，在汇报的工程中，发现他们的思考方式竟然是一致的。这是有点颠覆我的认知的，我之前一直以为，大学的教授大部分是学院派，搞理论的那一套会比较厉害，企业中的领导更多的思考点是如何创造更大的价值。\n一些想法 他们的思考方式是一直的，在听我汇报的过程中问的问题也是相似的，基本上从几个维度，现在正在做的事情是什么，做的怎么样了，遇到了什么问题，预期的结果是什么，为什么这么做，衡量收益的指标是什么，下一步打算怎么做，预期的结果是什么，为什么这么做，打算怎么去规避一些问题。 一般问到现在正在做什么事情的时候我还能回答的很好，到了预期以及下一步怎么做的时候，我经常就有点拉胯了，说实在的之前并没有细想过，问到衡量指标我就彻底歇菜，但是我发现对于衡量指标老板们问的最多。\n现在回想起来这些问题其实他们想问的点也很清楚\n正在做的事情， 其实想了解当前事情的背景是什么 做的怎么样了， 遇到了什么问题， 其实想了解当前事件的进度如何，到了哪一步了 预期结果是什么， 其实是想了解当前这件事你自己搞清楚没有，自己想要完成什么样的目标 一下步打算怎么做，其实是想了解你对这件事情的规划，下一步的计划 为什么这么做，如何规避问题， 其实就是想看看你对整件事情的一个思考程度 衡量指标，你对这件事情的认知程度，事件的清晰程度 老板们可能并不清楚我做的事情，但这个简简单单的几个问题，就能确定清楚我的一些思考，以及这件事情的完成程度，老板们的思路是如果这件事，我思考的很全面了，并且清楚自己的每一步在干什么，其实八九不离十就可以做好，毕竟已经是研究生(或者通过面试进入公司)。\n后续的一些做法 但我确实对自己做的事情，没想的很细。。。我经常做事情就是一股脑的冲进去，但是这样缺少思考，感觉大部分的时间都是白白荒废掉了。 觉得老板们的思考还是很到位的，以后的思考方式也应该向老板们看起，做事情的时候首先将这几件事情想清楚\n为什么做这件事 (评估下这件事情的优先级，成本，大概的收益) 怎么做这件事情 (现状是什么，可能遇到的问题，卡点在哪里，现阶段要做什么) 对这件事情预期，目标 (目标是什么，做到什么程度，做完这件事情对于现状的提升，对于现状以及以后的影响) 5w2h分析法 5w2h 发明者以五个w开头的英语单词和两个以h开头的英语单词进行设问。发现解决问题的线索，寻找出创新和发明新项目的思路，更进一步进行设计构思，从而搞出新的发明项目，这就叫做5w2h法。\nWhy What When Who Where How to do How much 在发明和设计中，对问题不敏感，看不出毛病是与平时不善于提问有密切关系的。对一个问题追根刨底，才有可能发现新的知识和新的疑问。所以从根本上说， 会发明首先要学会提问，善于提问。阻碍提问的因素：一是怕提问多，被别人看成什么也不懂的傻瓜。二是随着年龄和知识的增长，提问欲望渐渐淡薄。如果提 得不到答复和鼓励，反而遭人讥讽，结果在人的潜意识中就形成了这种看法：好提问、好挑毛病的人事扰乱别人的讨厌鬼，最好紧闭嘴唇，不看、不闻、不问， 是这恰恰阻碍了人的创造性的发挥。\n在对5w2h模式进行思考的时候可以善用下思维导图，遇到问题多追问自己两个问题没准可以发现问题的本质原因.\n","permalink":"https://akashark.github.io/en/posts/life/%E6%80%9D%E8%80%83/20221001/","summary":"\u003ch2 id=\"背景\"\u003e背景\u003c/h2\u003e\n\u003cp\u003e最近有两件事情对我感触还是挺大的，第一件是在字节的转正实习转正，第二件是转正后2天的研究生导师组会。为啥这两件事情会对我感触比较大呢，总的来说这两件事情的本质其实是相同的，都是完成一部分任务，对于现在完成的情况遇到的问题细节等作输出汇报，汇报的对象一个是+1 +2的领导，一个是自己的研究生导师，在汇报的工程中，发现他们的思考方式竟然是一致的。这是有点颠覆我的认知的，我之前一直以为，大学的教授大部分是学院派，搞理论的那一套会比较厉害，企业中的领导更多的思考点是如何创造更大的价值。\u003c/p\u003e","title":"最近的一些感悟 (20221001)"},{"content":"What’s New in Xcode 14 What’s New in Xcode 14\nSingle Size App Icon 在WWDC22, Apple发布了最新的Xcode14，Xcode14中带来了大量的功能和性能的提高，包括了在源码编辑器和其他方面上很多比较Cool的东西，我将和你一起分享下你要知道的。\n让我们来一起过下这些主题\nCode Structure When Scrolling 单尺寸App icon对于开发者是一个非常好的功能，你再也不需要App Icon生成工具，你能使用单一的一个1024x1024px的App Icon 资源。 不但如此，这将减少你App的尺寸，你可以尝试下使用1024x1024px的App Icon资源，然后导入到Xcode的Assets中从右边的工具条上选择单尺寸，一旦你导出IPA，你将看出来不同 下面的图是一个app归档后的结果，他展示了选择App icon 单图片和多图片的不同 Code Structure When Scrolling 我第一次看到这个功能很是吃惊，我想起来了成组的table View的样式，我确信这个是一个有价值的功能，为我们在编辑器滑动的时候展示我们在那个方法。 你当然可以禁止或者开启这个功能在Setting中如下展示 同样的对于标记导航也有修改，你能通过标记导航看到你在代码前写的标记标签。 Memberwise Initializer Completion 完成成员变量的初始化，在之前版本的Xcode，你需要去右击成员变量初始化 在Xcode 14 提供了高效的方式, 创建成员变量初始化，你只需要写下init，然后你将看到最接近的成员变量初始化的方法 还有更多代码补全的提高，这些补全也更加精确 Optiuonal Unwrapping 我看到的一大进步是你不需要通过 if let 设置来创建不可变变量来检查可选变量。 Xcode Build TimeLine 在之前版本的 Xcode 中，我们将构建日志视为一个列表，就像您在左侧看到的以下列表一样。我们还知道正在构建哪个步骤以及需要多少时间。现在，使用 Xcode 14，我们可以将这些日志视为时间线。 Highlighted Other Features and Improvements in the Release Notes 还有很多高光的其他能力和提高在release文档中被提到\nSimulator now supports remote notifications in iOS 16 when running in macOS 13 on Mac computers with Apple silicon or T2 processors. Xcode 14 can now compile targets in parallel with their Swift target dependencies. You can now enable sandboxing for shell script build phases using the ENABLE_USER_SCRIPT_SANDBOXING build setting. Xcode now provides RECOMMENDED_MACOSX_DEPLOYMENT_TARGET, RECOMMENDED_IPHONEOS_DEPLOYMENT_TARGET, RECOMMENDED_TVOS_DEPLOYMENT_TARGET, RECOMMENDED_WATCHOS_DEPLOYMENT_TARGET, and RECOMMENDED_DRIVERKIT_DEPLOYMENT_TARGET build settings that indicate the recommended minimum deployment versions for each supported Xcode platform. Because bitcode is now deprecated, builds for iOS, tvOS, and watchOS no longer include bitcode by default. The Thread Performance Checker shows runtime performance issues in the Issue Navigator and the source editor while debugging an app. Interface Builder now updates scenes asynchronously. Wrapping code with an if statement now automatically reindents the block. ","permalink":"https://akashark.github.io/en/posts/tech/%E7%BF%BB%E8%AF%91/xcode14%E6%96%B0%E5%8A%9F%E8%83%BD/","summary":"\u003ch1 id=\"whats-new-in-xcode-14\"\u003eWhat’s New in Xcode 14\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://medium.com/better-programming/whats-new-in-xcode-14-f6b56c33a8b3\"\u003eWhat’s New in Xcode 14\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"single-size-app-icon\"\u003eSingle Size App Icon\u003c/h2\u003e\n\u003cp\u003e在WWDC22, Apple发布了最新的Xcode14，Xcode14中带来了大量的功能和性能的提高，包括了在源码编辑器和其他方面上很多比较Cool的东西，我将和你一起分享下你要知道的。\u003c/p\u003e","title":"Xcode14新功能"},{"content":"工厂模式 浅析设计模式1 —— 工厂模式\n设计模式分类 创建型模式是对垒的实例化过程进行抽象，从而将对象的穿件和使用分离开，工厂模式属于创建型模式的范畴\n基本概念 工厂模式的核心思想就是创建对象和使用对象的解耦，由工厂负责对象的创建，而用户只能通过接口来使用对象， 这样就可以灵活应对变化的业务需求，方便代码管理，避免代码重复\n简单工厂模式 顾名思义，简单工厂模式是最简单的一种工厂模式，它定义了一个负责生产对象的工厂类，使用者可以根据不同参数来创建并返回不同子类，这些子类都共用一个接口（即父类）。\n结构 简单工厂模式包含三种类，分别是抽象产品类、具体产品类、工厂类，下面分别对各类及它们之间的关系作进一步说明。 使用 有了上述的基本概念，我们将简单工厂模式的使用步骤概括为：\nstep1: 创建抽象产品类，并为具体产品定义好一个接口 step2: 创建具体产品类，其通过接口来集成抽象产品类，同时也要定义计划生产的每一个具体产品 step3: 创建工厂类，器创建的静态方法可以对传入的不同参数做出响应 step4: 外界使用这就能调用工厂类的静态方法了，通过传入不同参数来创建不同具体产品实例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 //step1:创建抽象产品类，定义具体产品的公共接口 public abstract class Shirt{ public abstract void Show(); } //step2:创建具体产品类（继承抽象产品类），定义生产的具体产品 //具体产品类A，女款衬衫 public class WomenShirt extends Shirt{ @Override public void Show(){ System.out.println(\u0026#34;展示女款衬衫\u0026#34;); } } //具体产品类B，男款 public class MenShirt extends Shirt{ @Overside public void Show(){ System.out.println(\u0026#34;展示男款衬衫\u0026#34;)； } } //step3:创建工厂类，通过静态方法处理不同传入参数，从而创建不同具体产品类的实例 public class Factory{ public static Shirt Exhibit(String ShirtName){ switch(ShirtName){ case \u0026#34;女款衬衫\u0026#34;: return new WomenShirt(); case \u0026#34;男款衬衫\u0026#34;: return new MenShirt(); default: return null; } } } //step4:外界调用工厂类的静态方法，传入不同参数创建不同具体产品类的实例 public class SimpleFactoryPattern{ public static void main(String[] args){ Factory exhibitFactory = new Factory(); //用户搜索女款衬衫 try{ //调用工厂类的静态方法，传入参数并创建实例 exhibitFactory.Exhibit(\u0026#34;女款衬衫\u0026#34;).Show(); }catch(NullPointerException e){ System.out.println(\u0026#34;没有找到商品\u0026#34;); } //用户搜索男款裤子 try{ exhibitFactory.Exhibit(\u0026#34;男款裤子\u0026#34;).Show(); }catch(NullPointerException e){ System.out.println(\u0026#34;没有找到商品\u0026#34;); } //用户搜索男款衬衫 try{ exhibitFactory.Exhibit(\u0026#34;男款衬衫\u0026#34;).Show(); }catch(NullPointerException e){ System.out.println(\u0026#34;没有找到商品\u0026#34;); } } } 优点 将对象的使用和创建过程分离开，实现解藕。客户端不需要关注对象是谁创建的、怎么创建的，只要通过工厂中的静态方法就可以直接获取其需要的对象。 将初始化实例的工作放到工厂里执行，代码易维护， 更符合面向对象的原则，做到面向接口编程，而不是面向实现编程。 缺点 工厂类中需要选择创建具体某个对象，所以一旦添加新产品则必须要对工厂中的选择逻辑进行修改，导致工厂逻辑过于复杂，违背开闭原则。 工厂类集合了所有实例（具体产品）的创建逻辑，一旦这个工厂不能正常工作，整个系统都会受到影响。 静态方法不能被继承和重写，会造成工厂角色无法形成基于继承的等级结构。 场景 具体产品类比较少，使用简单工厂模式可以实现生产者与消费者的分离，而且也不会在工厂中定义太复杂的判断逻辑 使用者只需要知道工厂类的参数，不关系如何创建对象的逻辑时 工厂方法模式 工厂方法模式包含四种类，分别是抽象产品类，具体产品类，抽象工厂类，具体工厂类 使用 step1: 创建抽象工厂类，定义具体工厂的公共接口 step2: 创建抽象产品类，定义具体产品的公共接口 step3: 创建具体产品类(继承抽象产品类), 定义生产的具体产品 step4: 创建具体工厂类(集成抽象工厂类), 定义创建相应具体产品实例的方法 step5: 外界调用具体工厂类的方法，创建不同产品类的实例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 //step1:创建抽象工厂类，定义具体工厂的公共接口 public abstract class Factory{ public abstract Shirt Exhibit(); } //step2:创建抽象产品类，定义具体产品的公共接口 public abstract class Shirt{ public abstract void Show(); } //step3:创建具体产品类（继承抽象产品类），定义生产的具体产品 //具体产品类A，女款衬衫 public class WomenShirt extends Shirt{ @Override public void Show(){ System.out.println(\u0026#34;展示女款衬衫\u0026#34;); } } //具体产品类B，男款衬衫 public class MenShirt extends Shirt{ @Overside public void Show(){ System.out.println(\u0026#34;展示男款衬衫\u0026#34;)； } } //step4:创建具体工厂类，定义创建具体产品实例的方法 //具体工厂类A，展示女款衬衫类商品 public class WomenShirtFactory extends Factory{ @Overside public Shirt Exhibit(){ return new WomenShirt()； } } //具体工厂类B，展示男款衬衫类商品 public class MenShirtFactory extends Factory{ @Overside public Shirt Exhibit(){ return new MenShirt()； } } //step5:外界调用具体工厂类的方法，创建不同具体产品类的实例 public class FactoryPattern{ public static void main(String[] args){ //用户在店铺搜索女款衬衫 Factory exhibitWomenShirtFactory = new WomenShirtFactory(); exhibitWomenShirtFactory.Exhibit().Show(); //用户在店铺搜索男款衬衫 Factory exhibitMenShirtFactory = new MenShirtFactory(); exhibitMenShirtFactory.Exhibit().Show(); } } ps: 其实把if else 判断具体创建哪种产品交给了外部调用者来区分\n优点 符合开闭原则，新增一种产品时，只需要增加相应的具体产品类和工厂子类即可，可发方便的生产或者切换产品 符合单一原则，每个具体工厂类只负责创建对应的具体产品，而简单工厂中工厂类可能存在比较复杂的逻辑 相对于简单工厂模式，可形成基于继承的等级结构 缺点 一个具体工厂只能创建一种具体产品，添加新产品的时, 除增加新产品类外，还要提供与之对应的工厂类，类的个数成对增加，在一定程度是哪个增加了系统的复杂度，同时有更多的类需要加载与编译，给系统带来了额外的开销 由于考虑到系统的可扩展行，引入了抽象层，在客户端代码中均使用抽象层进行定义，增加了系统的抽象性和理解难度，增加了系统的实现难度 虽然保证了工厂方法内的对修改的关闭，但对于使用工厂的类，如果需要更改另一种产品，仍需要修改实例化的具体工厂类 难以对于父类接口进行修改，因为一旦修改接口，就必须要对众多的子类进行修改 适用场景 一个类不确定他所必须创建的对象的类，在工厂方法模式长胖呢个，客户端不需要知道具体产品类的类名，只需要知道对应的工厂即可 你期望获得较高的扩展性 一个类希望由它的子类来指定它所创建的对象。在工厂方法模式中，对于抽象工厂类只需提供一个创建产品的接口，而由其子类来确定具体要创建的对象，利用面向对象的多态性和里氏替换原则，在程序运行时，子类对象将覆盖父类对象，从而使系统更容易扩展 当类将创建对象的职责委托给多个工厂子类中的一个，而且用户知道要使用那个一个子类(判断放在了客户端这里) 抽象工厂模式 抽象工厂模式，提供一个创建一系列相关或相互依赖对象的接口，而无须指定它们的具体类。抽象工厂模式与工厂方法模式最大的区别：抽象工厂中每个具体工厂可以创建多类具体产品；而工厂方法每个具体工厂只能创建一类具体产品。\n结构 抽象工厂模式包含五种类，分别是抽象产品族类、抽象产品类、具体产品类、抽象工厂类、具体工厂类 感觉就是又增加了一层抽象层 抽象产品族 使用 step1：创建抽象工厂类，定义具体工厂的公共接口； step2：创建抽象产品族类，定义抽象产品的公共接口； step3：创建抽象产品类（继承抽象产品族类），定义具体产品的公共接口； step4：创建具体产品类（继承抽象产品类），定义生产的具体产品； step5：创建具体工厂类（继承抽象工厂类），定义创建相应具体产品实例的方法； step6：外界调用具体工厂类的方法，创建不同具体产品类的实例。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 //step1:创建抽象工厂类，定义具体工厂的公共接口 public abstract class Factory{ public abstract Clothing ExhibitShirt(); public abstract Clothing ExhibitTrousers(); } //step2:创建抽象产品族类，定义抽象产品的公共接口 public abstract class Clothing{ public abstract void Show(); } //step3:创建抽象产品类，定义具体产品的公共接口 // 短袖抽象类 public abstract class Shirt extends Clothing{ @Override public abstract void Show(); } // 裤子抽象类 public abstract class Trousers extends Clothing{ @Override public abstract void Show(); } //step4:创建具体产品类（继承抽象产品类），定义生产的具体产品 //衬衫产品类A，淘宝衬衫 public class TBShirt extends Shirt{ @Override public void Show(){ System.out.println(\u0026#34;展示淘宝店铺衬衫\u0026#34;); } //衬衫产品类B，淘特衬衫 public class TTShirt extends Shirt{ @Overside public void Show(){ System.out.println(\u0026#34;展示淘特店铺衬衫\u0026#34;)； } } //裤子产品类A，淘宝裤子 public class TBTrousers extends Trousers{ @Override public void Show(){ System.out.println(\u0026#34;展示淘宝店铺裤子\u0026#34;); } } //裤子产品类B，淘特裤子 public class TTTrousers extends Trousers{ @Overside public void Show(){ System.out.println(\u0026#34;展示陶特店铺裤子\u0026#34;)； } } //step5:创建具体工厂类，定义创建具体产品实例的方法 //淘宝工厂类A，展示衬衫+裤子 public class TBFactory extends Factory{ @Overside public Clothing ExhibitShirt(){ return new TBShirt()； } @Overside public Clothing ExhibitTrousers(){ return new TBTrousers()； } } //淘特工厂类B，展示裤子+衬衫 public class TTFactory extends Factory{ @Overside public Clothing ExhibitShirt(){ return new TTShirt()； } @Overside public Clothing ExhibitTrousers(){ return new TTTrousers()； } } //step6:外界实例化具体工厂类，调用工厂类中创建不同目标产品的方法，创建不同具体产品类的实例 public class AbstractFactoryPattern{ public static void main(String[] args){ TBFactory exhibitTBFactory = new TBFactory(); TTFactory exhibitTTFactory = new TTFactory(); //淘宝用户搜索衬衫 exhibitTBFactory.ExhibitShirt().Show(); //淘宝用户搜索衬衫 exhibitTBFactory.ExhibitTrousers().Show(); //淘特用户搜索衬衫 exhibitTTFactory.ExhibitShirt().Show(); //淘特用户搜索衬衫 exhibitTTFactory.ExhibitTrousers().Show(); } } 优点 降低耦合度。抽象工厂模式将具体产品的创建延迟到具体工厂类中，这样将对象的创建封装起来，可以减少客户端与具体产品类之间的依赖，从而降低系统耦合度，有利于后期的维护和扩展。 符合开闭原则。新增一种产品类时，只需增加相应的具体产品类和工厂子类即可，简单工厂模式需要修改工厂类的判断逻辑。 符合单一职责原则。每个具体工厂类只负责创建对应的产品，简单工厂模式中的工厂类需要进行复杂的 switch 逻辑判断。 不使用静态工厂方法，可以形成基于继承的等级结构。 便于添加更换产品族。因为具体产品都是由具体工厂创建的，所以在更换产品族的时候只要简单修改具体工厂即可。 具体产品的创建过程和客户端隔离。客户端通过操作抽象产品接口实现操作具体产品实例，具体产品的类名不会出现在客户端中。 缺点 难以支持新种类产品的变化。这是因为抽象工厂接口中已经确定了可被创建的产品集合，如果需要添加新产品，此时就必须去添加抽象产品接口，还要在抽象工厂接口中添加新方法，并在所有具体工厂中实现该新方法。这样就会改变抽象工厂类以及所有具体工厂子类的改变，违背开闭原则。 类图有点复杂，可读性没有工厂方法模式高。 场景 系统不要求依赖产品类实例如何被创建、组合和表达，这点也是所有工厂模式应用的前提。 系统要求提供一个产品类的库，所有产品以同样的接口出现，客户端不需要依赖具体实现。 系统中有多个产品族，但每次只使用其中某一族产品。（切换产品族只需修改具体工厂对象即可） 总结 简单工厂模式：让一个工厂类负责创建所有对象；但没有考虑后期扩展和维护，修改违背开闭原则，静态方法不能被继承。 工厂方法模式：主要思想是继承，修改符合开闭原则；但每个工厂只能创建一种类型的产品。 抽象工厂模式：主要思想是组合，本质是产品族，实际包含了很多工厂方法，修改符合开闭原则；但只适用于增加同类工厂这种横向扩展需求，不适合新增功能方法这种纵向扩展。 ﻿其实这三种工厂模式在形式和特点上都非常相似，甚至存在一定的内在联系，而且最终目的都是解耦。在使用时，我们不必去在意这个模式到底工厂方法模式还是抽象工厂模式，因为它们之间也是可以灵活转变的。比如你原本使用的是工厂方法模式，加入一个新方法后就可能会让具体产品类构成不同等级结构中的产品族，代码结构就变成抽象工厂模式了；而对于抽象工厂模式，当减少一个或多个具体产品类时，使原有产品族只剩下一个产品后，代码结构也就转变成了工厂方法模式。\n","permalink":"https://akashark.github.io/en/posts/tech/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/%E5%B7%A5%E5%8E%82%E6%A8%A1%E5%BC%8F/","summary":"\u003ch1 id=\"工厂模式\"\u003e工厂模式\u003c/h1\u003e\n\u003cp\u003e\u003ca href=\"https://mp.weixin.qq.com/s/PEDR88G30w4rvlV66zElYw\"\u003e浅析设计模式1 —— 工厂模式\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"设计模式分类\"\u003e设计模式分类\u003c/h2\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"http://media.wjbbf.cn/mweb/16639916193524.jpg\" alt=\"\"  /\u003e\n\n创建型模式是对垒的实例化过程进行抽象，从而将对象的穿件和使用分离开，工厂模式属于创建型模式的范畴\u003c/p\u003e\n\u003ch2 id=\"基本概念\"\u003e基本概念\u003c/h2\u003e\n\u003cp\u003e工厂模式的核心思想就是创建对象和使用对象的解耦，由工厂负责对象的创建，而用户只能通过接口来使用对象， 这样就可以灵活应对变化的业务需求，方便代码管理，避免代码重复\u003c/p\u003e","title":"工厂模式"},{"content":"HTTP发展历程 [TOC]\nHTTP协议版本 HTTP/0.9 HTTP/1.0 HTTP/1.1 HTTP/2.0 HTTP/3.0 HTTP/0.9 0.9版本的HTTP协议他不涉及数据包传输(仅仅是HTLM通过ASCII编码后传递)，主要是规定了客户端和服务端之间的通信格式，默认使用80端口，这个版本只有一条命令GET，而且不支持处理HTML以外任何的文件格式。 缺点\n只支持简单的文本传输，并没有更多丰富的元素 支持的行为比较少，只支持GET HTTP/1.0 相对于0.9版本增加了如下\n支持响应支持HTTP头(HTTP Header)， 支持响应含状态行，增加状态码 支持HEAD，POST等方法 支持HTML以外的文件格式 包括了图片，视频，二进制文件等 相对于0.9版本1.0版本，为了支持互联网的变化增加了许多内容，丰富了网络提供的基础服务 缺点 客户端必须为每一个待请求的对象建立并维护一个新的连接，也就是说当页面存在多个对象，HTTP1.0建立非持久连接，使得一个页面下载十分缓慢，增加了网络传输负担 HTTP/1.1 相对于之前的版本1.1版本增加了如下\n长连接与管道化: HTTP1.1支持长连接和请求的流水线处理，在一个TCP连接上可以传送很多个HTTP请求和响应，减少了建立和关闭连接的消耗和延迟，在HTPP1.1中默认开启Connection: keey-alive 缓存处理: 在 HTTP1.0 中主要使用 header 里的If-Modified-Since,Expires来做为缓存判断的标准，HTTP1.1 则引入了更多的缓存控制策略例如Entity tag，If-Unmodified-Since, If-Match, If-None-Match等更多可供选择的缓存头来控制缓存策略。 带宽优化: HTTP1.0 中，存在一些浪费带宽的现象，例如客户端只是需要某个对象的一部分，而服务器却将整个对象送过来了，并且不支持断点续传功能，HTTP1.1 则在请求头引入了range 头域，它允许只请求资源的某个部分，即返回码是206（Partial Content），这样就方便了开发者自由的选择以便于充分利用带宽和连接。 错误状态码: 在 HTTP1.1 中新增了24 个错误状态响应码，如 409（Conflict）表示请求的资源与资源的当前状态发生冲突；410（Gone）表示服务器上的某个资源被永久性的删除。 Host头处理: 在 HTTP1.0 中认为每台服务器都绑定一个唯一的 IP 地址，因此，请求消息中的 URL 并没有传递主机名（hostname）。但随着虚拟主机技术的发展，在一台物理服务器上可以存在多个虚拟主机（Multi-homed Web Servers），并且它们共享一个 IP 地址。HTTP1.1 的请求消息和响应消息都应支持 Host 头域，且请求消息中如果没有 Host 头域会报告一个错误（400 Bad Request）。 缺点 虽然 1.1 版允许复用 TCP 连接，但是同一个 TCP 连接里面，所有的数据通信是按次序进行的。服务器只有处理完一个回应，才会进行下一个回应。要是前面的回应特别慢，后面就会有许多请求排队等着。 HTTP1.x 在传输数据时，所有传输的内容都是明文，客户端和服务器端都无法验证对方的身份，这在一定程度上无法保证数据的安全性。 HTTP1.x 在使用时，header 里携带的内容过大，在一定程度上增加了传输的成本，并且每次请求 header 基本不怎么变化，尤其在移动端增加用户流量。 虽然 HTTP1.x 支持了 keep-alive，来弥补多次创建连接产生的延迟，但是 keep-alive 使用多了同样会给服务端带来大量的性能压力，并且对于单个文件被不断请求的服务(例如图片存放网站)，keep-alive 可能会极大的影响性能，因为它在文件被请求之后还保持了不必要的连接很长时间。 SPDY协议 2009 年，谷歌公开了自行研发的 SPDY 协议，主要解决 HTTP/1.1 效率不高的问题。 这个协议在 Chrome 浏览器上证明可行以后，就被当作 HTTP/2 的基础，主要特性都在 HTTP/2 之中得到继承。SPDY 可以说是综合了 HTTPS 和 HTTP 两者有点于一体的传输协议，主要解决：\n降低延迟 SPDY采用了多路复用，多路复用通过多个请求Stream共享一个TCP连接的方式降低延迟的同时提高了带宽的利用率 请求优先级 多路复用带来了一个新的问题是，在连接共享的基础上可能会导致关键请求被阻塞，SPYD运行给每个request设置优先级，这样重要的请求会优先得到响应 Header压缩 SYPD采用了DEFLATE压缩算法对于HTTP的Header部分进行了压缩处理 服务端推送 采用了SPDY设计的网页可以接受服务端主动的推送，比如在请求css的时候服务端将js作为推送内容推送给浏览器，在网页请求js的时候可以直接使用浏览器的换从不用再次发起请求 默认增加SSL/LTS层来保证数据传递的安全性问题 HTTP/2.0 HTTP/2.0可以说是SPDY的升级版，主要与SPDY的不同在于\nHTTP2.0 支持明文传输HTTP传输，而SPDY强制使用HTTPS HTTP2.0 消息头的压缩算法采用了HPACK，而非SPDY采用的DEFLATE HTTP2.0的新特性包括了 二进制分帧 HTTP2.0的所有数据帧都采用二进制编码 多路复用 请求优先级 header压缩 服务端推送 二进制分帧 帧: 客户端与服务器通过交换帧来通信，帧是基于这个新协议通信的最小单元 消息: 逻辑上的HTTP消息。比如请求，响应等，由一个或者多个帧组成 流: 流是连接中的一个虚拟信道，可以承载双向消息(全双工)，每个流都有一个唯一的整数标识符 HTTP/2 采用二进制格式传输数据，而非 HTTP 1.x 的文本格式，二进制协议解析起来更高效。 HTTP/1.x 的请求和响应报文，都是由起始行，首部和实体正文（可选）组成，各部分之间以文本换行 符分隔。HTTP/2 将请求和响应数据分割为更小的帧，并且它们采用二进制编码。 帧、流、消息的关系 每个数据流都以消息的形式发送，而消息又由一个或者多个帧组成，帧是流中的数据单元，一个数据报的header帧可以分成多个header帧，data帧可以分成多个data帧。 多路复用 多路复用运行同时通过单一的HTTP/2.0连接发起多重的请求-响应消息，每个request都可以复用共享的连接，每一个request对应一个id，这样一个连接上可以有多个request，每个连接的request可以随机的混合在一起，接受房可以根据request的id将request再归属到各自不同的服务端请求请求里 请求优先级 把 HTTP 消息分解为很多独立的帧之后，就可以通过优化这些帧的交错和传输顺序，每个流都可以带有一个 31 比特的优先值：0 表示最高优先级；2 的 31 次方-1 表示最低优先级。 服务器可以根据流的优先级，控制资源分配（CPU、内存、带宽），而在响应数据准备好之后，优先将最高优先级的帧发送给客户端。 header压缩 HTTP1.X的header带有大量信息，而且每次都要重复发送，HTTP/2.0使用encoder来减少需要传输的header的大小，并且通讯双方各自cache一份header field表，避免了重复header的传输问题，又减少了需要传输的大小，为了减少这部分的资源消耗并提升性能，HTTP/2.0对一下首部采取了压缩策略\nHTTP/2.0在客户端和服务端使用首部表来跟踪和存储之前发送的键值对(HTTP Header)，不再重复发送Header 首部表在HTTP/2.0的连接存续期内始终存在，由客户端和服务端共同渐进地更新 针对Header的更新，如下图所示，根据已经缓存的Header Fields表来更新键值对 服务器推送 Server Push 即服务端能通过 push 的方式将客户端需要的内容预先推送过去，也叫“cache push”。 服务器可以对一个客户端请求发送多个响应。服务器向客户端推送资源无需客户端明确地请求，服务端可以提前给客户端推送必要的资源，这样可以减少请求延迟时间，例如服务端可以主动把 JS 和 CSS 文件推送给客户端，而不是等到 HTML 解析到资源时发送请求，大致过程如下图所示： HTTP/3.0 HTTP3.0可以看做HTTP协议的一次大版本的更新，HTTP3.0将传输层的协议由TCP更换成了QUIC协议 为什么会更换掉传输层的协议呢，因为在HTTP2.0中大家已经将HTTP队头阻塞的问题通过多路复用解决了，应用层的优化做尽了，没地方卷了，就开上了TCP队头阻塞的问题，打算从传输层下手。 哪有为什么选择UDP呢，传输层的协议替换是需要硬件层面运营商支持的，大规模的普及人力物力成本较大ROI较低，就很IPV6一样24年前的东西到现在也没普及多少，所以这帮人就盯上了UDP，UDP和TCP一样被大部分硬件支持，而且基于UDP的QUIC协议提供了可靠性的保证。 那么QUIC是怎么解决TCP队头问题的呢， 为了解决传输级别的队头阻塞问题，通过 QUIC 连接传输的数据被分为一些流。流是持久性 QUIC 连接中短暂、独立的“子连接”。每个流都处理自己的错误纠正和传递保证，但使用连接全局压缩和加密属性。每个客户端发起的 HTTP 请求都在单独的流上运行，因此丢失数据包不会影响其他流/请求的数据传输。\n补充 数字证书 证书生成规则\nCA机构拥有非对称加密的私钥和公钥。 CA机构对证书明文数据T进行hash。 对hash后的值用私钥加密，得到数字签名S。 明文和数字签名共同组成了数字证书，这样一份数字证书就可以颁发给网站了。 从上面的图也可以看出数字证书就是证书的明文信息+证书明文信息用hash签名再用CA的私钥加密后的签名\n证书验证规则 拿到证书，得到明文T1，数字签名S1。 用CA机构的公钥对S1解密，得到S2。 用证书里说明的hash算法对明文T1进行hash得到T2。 比较S2是否等于T2，等于则表明证书可信。 队头阻塞 TCP队头阻塞 TCP队头阻塞是因为TCP协议要求我们在每收到一个SYN包的时候就需要发送一个ACK作为回复，但是在 网络不好的情况下有可能发生丢包的现象，TCP的做法是在发送方将丢失的数据包重传之前，接受方是不 能将已经接受的数据包做回复的(已经接受的数据包将丢弃)。TCP的队头阻塞问题在移动网络上更加明 显。 HTTP队头阻塞 HTTP在1.1后默认开启了管线化，HTTP 管线化的意思就是客户端无需在发送后续 HTTP 请求之前等待 服务器响应请求。此功能可以更有效地利用带宽并减少延迟，但它的改进空间甚至更大。HTTP 管线化 仍要求服务器按照接收到的请求顺序进行响应，因此，如果管线化中的单个请求执行得很慢，则对客户 端的所有后续响应都将相应地延迟下去。这个问题被称为队头阻塞。 总结 从 http/0.9 到 http/2 的发展，有了很多的优化点如下：\n二进制分帧：HTTP/2 的所有帧都采用二进制编码 多路复用 (Multiplexing) 请求优先级 header 压缩 服务端推送 到了HTTP3.0更是有了长足的进步直接把传输层的协议改了不再依靠应用层去做一些花活了 ","permalink":"https://akashark.github.io/en/posts/tech/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/http%E5%8F%91%E5%B1%95%E5%8F%B2%E6%B5%85%E6%9E%90/","summary":"\u003ch1 id=\"http发展历程\"\u003eHTTP发展历程\u003c/h1\u003e\n\u003cp\u003e[TOC]\u003c/p\u003e\n\u003ch2 id=\"http协议版本\"\u003eHTTP协议版本\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eHTTP/0.9\u003c/li\u003e\n\u003cli\u003eHTTP/1.0\u003c/li\u003e\n\u003cli\u003eHTTP/1.1\u003c/li\u003e\n\u003cli\u003eHTTP/2.0\u003c/li\u003e\n\u003cli\u003eHTTP/3.0\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"http://media.wjbbf.cn/mweb/16631349733000.jpg\" alt=\"\"  /\u003e\n\u003c/p\u003e\n\u003ch2 id=\"http09\"\u003eHTTP/0.9\u003c/h2\u003e\n\u003cp\u003e0.9版本的HTTP协议他不涉及数据包传输(仅仅是HTLM通过ASCII编码后传递)，主要是规定了客户端和服务端之间的通信格式，默认使用80端口，这个版本只有一条命令GET，而且不支持处理HTML以外任何的文件格式。\n缺点\u003c/p\u003e","title":"HTTP发展史浅析"},{"content":"","permalink":"https://akashark.github.io/en/docs/network/http%E5%8F%91%E5%B1%95%E5%8F%B2%E6%B5%85%E6%9E%90/","summary":"","title":"HTTP发展史浅析"},{"content":"为什么要有虚拟内存？ 虚拟内存 单道程序的操作系统直接引用的物理地址，无法同时运行两个程序，程序会因为内存地址错乱而崩溃，单道程序的操作系统使用队列，做完一个任务开始下一个任务。\n现代操作系统的做法是把进程所使用的地址「隔离」开来，即让操作系统为每个进程分配独立的一套「虚拟地址」，人人都有，大家自己玩自己的地址就行，互不干涉。但是有个前提每个进程都不能访问物理地址，至于虚拟地址最终怎么落到物理内存里，对进程来说是透明的，操作系统已经把这些都安排的明明白白了。 操作系统会提供一种机制，将不同进程的虚拟地址和不同内存的物理地址映射起来。\n程序中使用的地址: 虚拟内存地址 硬盘中使用的地址: 物理内存地址\n操作系统引入了虚拟内存，进程持有的虚拟地址会通过 CPU 芯片中的内存管理单元（MMU）的映射关系，来转换变成物理地址，然后再通过物理地址访问内存，如下图所示： 操作系统使用内存分段和内存分页来管理虚拟地址与物理地址之间的关系\n补充: 虚拟内存有什么作用\n提高内存使用率: 虚拟内存可以使得进程运行内存超过物理内存大小，因为程序运行符合局部性原则，cpu访问内存会有很明显的重复访问的倾向性，对于那些没有被经常使用到的的内存，我们可以将他们换出物理内存外，到磁盘上的swap区域 进程隔离: 每个进程都有自己的页表，所以每个进程的虚拟内存相互独立，一个进程没有办法访问其他进程的页表，这些页表都是私有的，这就解决了多进程之间地址冲突的问题 安全性保证: 页表里面的页表项除了物理地址之外，还有一些标志属性，比如控制一个页的读写权限标记该页是否存在，在内存访问方面，操作系统为操作系统提供了更好的安全支持。 内存分段 程序是由若干个逻辑分段组成的，如可由代码分段、数据分段、栈段、堆段组成。不同的段是有不同的属性的，所以就用分段（Segmentation）的形式把这些段分离出来。 分段机制下的虚拟地址由两部分组成，段选择因子和段内偏移量。\n段选择因子和段内偏移量：\n段选择子就保存在段寄存器里面。段选择子里面最重要的是段号，用作段表的索引。段表里面保存的是这个段的基地址、段的界限和特权等级等。 虚拟地址中的段内偏移量应该位于 0 和段界限之间，如果段内偏移量是合法的，就将段基地址加上段内偏移量得到物理内存地址。 问题\n内存碎片化问题 内存交换效率低 碎片化 外部内存碎片，产生了多个不连续的小物理内存，导致新的程序无法被装载 内部内存碎片，程序所有的内存都被装载到了物理内存，但是这个程序有部分的内存可能并不是很常使用，这也会导致内存的浪费； 内存交换，将部分内存写到磁盘上再次读取进来紧挨着已经被占用的防止出现外部碎片，这个内存交换空间，在 Linux 系统里，也就是我们常看到的 Swap 空间，这块空间是从硬盘划分出来的，用于内存与硬盘的空间交换。\n交换率低 对于多进程的系统来说，用分段的方式，内存碎片是很容易产生的，产生了内存碎片，那不得不重新 Swap 内存区域，这个过程会产生性能瓶颈。 因为硬盘的访问速度要比内存慢太多了，每一次内存交换，我们都需要把一大段连续的内存数据写到硬盘上。所以，如果内存交换的时候，交换的是一个占内存空间很大的程序，这样整个机器都会显得卡顿。\n内存分页 分段的好处就是能产生连续的内存空间，但是会出现内存碎片和内存交换的空间太大的问题。 要解决这些问题，那么就要想出能少出现一些内存碎片的办法。另外，当需要进行内存交换的时候，让需要交换写入或者从磁盘装载的数据更少一点，这样就可以解决问题了。这个办法，也就是内存分页（Paging）。\n分页是把整个虚拟和物理空间切成一段段固定尺寸的大小,这样一个连续的并且尺寸固定的空间我们叫做页。 虚拟地址和物理地址之间通过页表来映射 和分段管理的段表类似，分页管理的页表也存储在MMU(内存管理单元)中\n当进程访问的虚拟地址在页表查不到的时候，系统会产生一个缺页中断，进入系统内核空间分配物理内存，更新进程页表，最后返回用户空间，恢复进程的运行\n由于内存控件都是预先划分好的，也就不会像分段会产生非常细小的内存，这正是分段会产生内存碎片的原因，而采用了分页, 那么释放的内存都是以页为释放单元，也就不会产生无法给进程使用的小内存了\n如果内存空间不够，操作系统会把其他正在运行的进程中的「最近没被使用」的内存页面给释放掉，也就是暂时写在硬盘上，称为换出（Swap Out）。一旦需要的时候，再加载进来，称为换入（Swap In）。所以，一次性写入磁盘的也只有少数的一个页或者几个页，不会花太多时间，内存交换的效率就相对比较高。 在分页机制下，虚拟地址分为两部分，页号和页内偏移。页号作为页表的索引，页表包含物理页每页所在物理内存的基地址，这个基地址与页内偏移的组合就形成了物理内存地址，见下图。 内存地址转化三个步骤\n把虚拟内存地址，切分成页号和偏移量 根据页号去MMU中查找对应的物理页号 直接拿到物理页的页号(物理内存的基地址), 加上前面的偏移量, 就得到了物理内存地址 有空间上的缺陷。 因为操作系统是可以同时运行非常多的进程的，那这不就意味着页表会非常的庞大。 在 32 位的环境下，虚拟地址空间共有 4GB，假设一个页的大小是 4KB（2^12），那么就需要大约 100 万 （2^20） 个页，每个「页表项」需要 4 个字节大小来存储，那么整个 4GB 空间的映射就需要有 4MB 的内存来存储页表。 这 4MB 大小的页表，看起来也不是很大。但是要知道每个进程都是有自己的虚拟地址空间的，也就说都有自己的页表。 那么，100 个进程的话，就需要 400MB 的内存来存储页表，这是非常大的内存了，更别说 64 位的环境了。 ps: 内存分页和内存分段的逻辑和链表和数组有点相似，分页采用的不是连续的内存区域，分段采用的是连续的内存区域\n多级页表 如果使用了二级分页，一级页表就可以覆盖整个 4GB 虚拟地址空间，但如果某个一级页表的页表项没有被用到，也就不需要创建这个页表项对应的二级页表了，即可以在需要时才创建二级页表。做个简单的计算，假设只有 20% 的一级页表项被用到了，那么页表占用的内存空间就只有 4KB（一级页表） + 20% * 4MB（二级页表）= 0.804MB，这对比单级页表的 4MB 是不是一个巨大的节约？\n64位的系统二级分页不够使用,变成了四级\n全局页目录项 PGD（Page Global Directory）； 上层页目录项 PUD（Page Upper Directory）； 中间页目录项 PMD（Page Middle Directory）； 页表项 PTE（Page Table Entry）； 内存地址局部性原则 我们就可以利用这一特性，把最常访问的几个页表项存储到访问速度更快的硬件，于是计算机科学家们，就在 CPU 芯片中，加入了一个专门存放程序最常访问的页表项的 Cache，这个 Cache 就是 TLB（Translation Lookaside Buffer） ，通常称为页表缓存、转址旁路缓存、快表等。 段页式内存管理 段页式内存管理的实现方式\n先将程序划分为多个有逻辑意义的段，也就是前面提到的分段机制 接着再将每个段划分为多个页，也就是对分段划分出来的连续空间，再划分固定大小的页 地址结构由段号，段内页号，页内偏移三个部分组成 段页式地址转化中要得到物理地址需要进过三次内存访问\n段表得到页表的起始地址 访问页表得到物理页号 物理页号与页内偏移组合得到物理地址 ps: 逻辑地址和线性地址\n程序中所使用的地址，通常是没有被段式内存管理映射的地址，称为逻辑地址 通过段式内存管理映射的地址，称为线性地址，也叫做虚拟地址 逻辑地址是「段式内存管理」转换前的地址，线性地址则是「页式内存管理」转换前的地址。 程序文件段（.text），包括二进制可执行代码； 已初始化数据段（.data），包括静态常量； 未初始化数据段（.bss），包括未初始化的静态变量； 堆段，包括动态分配的内存，从低地址开始向上增长； 文件映射段，包括动态库、共享内存等，从低地址开始向上增长（跟硬件和内核版本有关 (opens new window)）； 栈段，包括局部变量和函数调用的上下文等。栈的大小是固定的，一般是 8 MB。当然系统也提供了参数，以便我们自定义大小； 总结 为了在多进程环境下，使得进程之间的内存地址不受影响，相互隔离，于是操作系统就为每个进程独立分配一套虚拟地址空间，每个程序只关心自己的虚拟地址就可以，实际上大家的虚拟地址都是一样的，但分布到物理地址内存是不一样的。作为程序，也不用关心物理地址的事情。\n每个进程都有自己的虚拟空间，而物理内存只有一个，所以当启用了大量的进程，物理内存必然会很紧张，于是操作系统会通过内存交换技术，把不常使用的内存暂时存放到硬盘（换出），在需要的时候再装载回物理内存（换入）。\n那既然有了虚拟地址空间，那必然要把虚拟地址「映射」到物理地址，这个事情通常由操作系统来维护。\n那么对于虚拟地址与物理地址的映射关系，可以有分段和分页的方式，同时两者结合都是可以的。\n内存分段是根据程序的逻辑角度，分成了栈段、堆段、数据段、代码段等，这样可以分离出不同属性的段，同时是一块连续的空间。但是每个段的大小都不是统一的，这就会导致内存碎片和内存交换效率低的问题。\n于是，就出现了内存分页，把虚拟空间和物理空间分成大小固定的页，如在 Linux 系统中，每一页的大小为 4KB。由于分了页后，就不会产生细小的内存碎片。同时在内存交换的时候，写入硬盘也就一个页或几个页，这就大大提高了内存交换的效率。\n再来，为了解决简单分页产生的页表过大的问题，就有了多级页表，它解决了空间上的问题，但这就会导致 CPU 在寻址的过程中，需要有很多层表参与，加大了时间上的开销。于是根据程序的局部性原理，在 CPU 芯片中加入了 TLB，负责缓存最近常被访问的页表项，大大提高了地址的转换速度。\n","permalink":"https://akashark.github.io/en/posts/read/%E5%9B%BE%E8%A7%A3%E7%B3%BB%E7%BB%9F/%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E6%9C%89%E8%99%9A%E6%8B%9F%E5%86%85%E5%AD%98/","summary":"\u003ch1 id=\"为什么要有虚拟内存\"\u003e为什么要有虚拟内存？\u003c/h1\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"http://media.wjbbf.cn/mweb/16598818822755.jpg\" alt=\"\"  /\u003e\n\u003c/p\u003e\n\u003ch2 id=\"虚拟内存\"\u003e虚拟内存\u003c/h2\u003e\n\u003cp\u003e单道程序的操作系统直接引用的物理地址，无法同时运行两个程序，程序会因为内存地址错乱而崩溃，单道程序的操作系统使用队列，做完一个任务开始下一个任务。\u003c/p\u003e","title":"为什么要有虚拟内存"},{"content":"关于我\n英文名: Sharker 职业: 学生/程序员 喜好: 摆烂 ","permalink":"https://akashark.github.io/en/about/","summary":"\u003cp style=\"font-size: 25px;\"\u003e关于我\u003c/p\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e\u003c/th\u003e\n          \u003cth\u003e\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e英文名:\u003c/td\u003e\n          \u003ctd\u003eSharker\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e职业:\u003c/td\u003e\n          \u003ctd\u003e学生/程序员\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e喜好:\u003c/td\u003e\n          \u003ctd\u003e摆烂\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e","title":"🙋🏻‍♂️关于"}]