├── .gitignore ├── README.md ├── articles ├── 2015-conclusion.md ├── 2016-conclusion.md ├── 2017-conclusion.md ├── 2018-conclusion.md ├── advanced-swift-chinese.md ├── alfred-workflows.md ├── android-drawble-reuse.md ├── android-splash.md ├── android-xml.md ├── appium.md ├── async-dsym.md ├── bat-offer.md ├── cocoapods-debug.md ├── cocoapods-xcode.md ├── compile-and-language.md ├── crack-paw.md ├── design-pattern-factory.md ├── dsl-ruby-ios.md ├── efficient-mac.md ├── fe-history.md ├── fight-against-anxiety.md ├── fq-2019.md ├── fq.md ├── hashtable.md ├── how-does-git-work.md ├── http-encoding.md ├── http-proxy-tools.md ├── https-9-questions.md ├── ios-compile-speed.md ├── ios-custom-transition-animation.md ├── ios-lock.md ├── ios-rounded-corner.md ├── ios-runloop.md ├── ios-scrollview-optimize.md ├── ios-tableview.md ├── javascript-async.md ├── javascript-modules.md ├── jian_zhi_ru_men.md ├── main-thread-ui.md ├── multi-thread-conclusion.md ├── number-more-than-half.md ├── objc-gcd.md ├── objc-load-initialize.md ├── objc-runtime-story.md ├── objc-runtime.md ├── objc-strong-weak-dance.md ├── objc-swift-block.md ├── objc-swift-copy-mutable.md ├── objc-thread-backtrace.md ├── pointer-and-reference.md ├── pop.md ├── quic.md ├── react-native.md ├── string-encoding.md ├── swift-array-append.md ├── swift-array.md ├── swift-assembly-1-pwt.md ├── swift-code-coverage.md ├── swift-dictionary-impl.md ├── swift-dictionary.md ├── swift-interface.md ├── swift-object-compare.md ├── swift-print.md ├── swift-thread-safe-map.md ├── swift-tips.md ├── swift-uicolor.md ├── tcp-ip-1.md ├── tcp-ip-2.md ├── tcp-ip-3.md ├── tcp-ip-4.md ├── tcp-ip-5.md ├── tcp-ip-6.md ├── uikit-optimization.md ├── uiscrollview-with-autolayout.md ├── uiview-life-time.md ├── vue-express-conclusion.md ├── weibo-short-url.md ├── why-nodejs.md ├── wireshark.md ├── xiaomi-router.md └── zi-wo-xiu-yang.md └── pictures ├── cocoapods-debug ├── gem-env.png ├── rubymine-config-bundle.png ├── rubymine-config-main.png ├── rubymine-debug-cocoapods-binary.png ├── rubymine-debug-cocoapods-core.png └── rubymine-debug-main.png ├── jianzhi ├── bmi.jpg ├── compare.jpg └── energy.png ├── swift-coverage ├── 16289505462244.jpg ├── 16289950183691.jpg ├── 16289952692264.jpg ├── 16289964552519.jpg ├── 16289972039432.jpg ├── 16289987785206.jpg ├── 16290003214898.jpg ├── 16290004371805.jpg ├── 16290004530676.jpg ├── 16290144521686.jpg ├── 16290157144891.jpg ├── carbon -1-.png ├── carbon -2-.png ├── carbon -3-.png ├── carbon -4-.png ├── carbon -5-.png └── swift-coverage.png ├── swift-dictionary ├── call-find.png ├── debug.png └── find-out-impl.png └── swift-interface ├── error.png ├── inlinable-age-setter.png ├── inlinable-none-stability.png └── inlinable-stability.png /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | .DS_Store 3 | .Ulysses-Group.plist 4 | .MWebMetaData 5 | -------------------------------------------------------------------------------- /articles/2015-conclusion.md: -------------------------------------------------------------------------------- 1 | 我一直认为,对于程序员来说,写一天代码不算什么,也许这只是解决了一个bug,或者干脆就没能搞定。一周的时间学习内容页有限,可能是一两个困扰已久的知识点终于悟透了,也可能是新学了某个知识点。但是和一个月前的自己相比,总是感觉自己进步颇大,和一年前的自己相比,就有一种判若两人的感觉了。2015就要过去了,我想有必要总结一下这一年来的收获得失。 2 | 3 | # 从小白开始 4 | 5 | 从去年年底正式开始写iOS开始,到大约三四月份,一直在开发自己的第一个iOS应用,现在回顾那段时间的博客,主要是关于UI方面的总结,以及一些常见错误的解决方法。在这个过程中了解了怎么处理HTTP请求和Json解析(没错,当时还不知道HTTP请求是什么),图片的异步加载与缓存,`UIScrollview`和`UITableView`这些稍微“高端”一些的UI控件。也开始接触了定时器、内存管理等知识。APP上架以后我就天真的以为自己已经把iOS学得差不多了。 6 | 7 | # 第一次失败 8 | 9 | 五月份的时候做过两个小的项目,一个APP上架(广告赚了将近200),一个Xcode插件在Github,然后兴冲冲的去面了一家公司。待遇是12K+包吃住,虽然也没觉得自己达到这个水平,不过真正到了第二轮被拒的时候还是有一些小伤感。面试虽然没过,却还是有些收获: 10 | 11 | 1. 逐渐开始过渡到用Google查资料,平时关注Objc.io并RSS订阅各个大牛的技术博客,偶尔在StackOverflow提个问题。总的来说就是层次提高了,获取知识的来源拓宽了。这一点至今让我受益匪浅。 12 | 13 | 2. 关于UI方面的一些细节,以前做项目,只想着效果OK就可以,面试官问我怎么处理图片的拉伸问题我就一问三不知。还有一些比较底层的知识,比如`CoreGraphis`、`CoreAnimation`等也是之前欠缺的。 14 | 15 | 3. 以前只会嘴上喊MVC模式,实际代码中VC负责一切。被面试官一下子问个正着。现在严格遵循MVC模式,而且学习了MVVM模式,写了几篇分析ReactiveCocoa源码的文章:[ReactiveCocoa详解](http://blog.csdn.net/column/details/reactivecocoa.html)。可惜对函数式编程的思想还是不够熟练,在项目中用了一两次之后就暂时搁置了,听说美团在用RAC。 16 | 17 | # 第二次失败 18 | 19 | 面试结束后,把面试过程中遇到的问题总结了一下,又投了一份简历。可能是运气不错,我准备的问题被问到了好几个,也有可能是那个创业公司比较缺人,所以成功的拿到了Offer。当时谈了400一天,后来因为晚上加班,最后实际给了440一天(大二就月入过万了,当时还是有点小骄傲)。六月份开始入职,但是问题来的比工资快。现在想来主要是这几点: 20 | 21 | 1. 不适应身份的转变。企业是企业,学校是学校,在学校的团队里,自己是技术leader同时还身兼产品经理。自己定好需求再实现。但是在公司里,产品经理的需求一日三变,写完的代码leader看技术实现,产品经理看效果,美工检查UI布局。由于以前基础太不扎实,态度也比较敷衍,所以这么一来漏洞百出,顾此失彼。 22 | 23 | 2. 心态不够好。刚开始干劲十足,但总是被PM改需求,被leader批评。后来慢慢的也有些失望,甚至是抵触。其中的过程比较复杂,至今也不太能理清楚, 24 | 25 | 总的来说,第二次失败的原因有两个。一是自己技术水平不够,这里有代码规范问题也有iOS开发上的问题,二是太以自我为中心,如果当时能多站在公司的角度考虑问题,也许情况就不是那样。后来和公司的关系越来越僵,最后九月初选择了提前离职,回到学校继续钻研技术。 26 | 27 | 回到学校之后的整整两个月都没有看技术。半主动离职让我开始怀疑自己,虽然对公司的领导、氛围小有愤懑,但毕竟自己的问题更加严重。于是选择了逃避,每天玩游戏、看电影,甚至去大连旅游了一趟。回家呆了一段时间,再回学校已经是11月,感觉逃避也不是办法,只是还是要一点点学。 28 | 29 | # 读书 30 | 31 | 15年读了不少好书,收益颇丰。实习期间看完了《Effective Objective-C 2.0》和《Objective-C高级编程》,对不少基础知识,比如ARC、block和GCD都有了更加深入的了解。 32 | 33 | 实习的时候还看了一本《老码说编程之玩转Swift江湖》,当时Swift还是1.x版本。从这一点来说,我是个不合格的实习生。实习期间路上、晚上都在看书,导致白天精力不济。因为当时的思想还是利用实习期间多学技术,但创业公司要求的其实是实习生为他创造效益。 34 | 35 | 11月读了《程序员的自我修养》,对程序的运行和操作系统有了更透彻的理解。读完之后写了一篇[读后感](http://www.jianshu.com/p/47156b4259ed)。12月的Swift 2开源,感觉Swift是大势所趋,所以开始学习objc出版的"Advanced Swift",同时自己也写了它的[中文翻译版](http://www.jianshu.com/p/18744b078508),后来有人提醒我版权问题,所以已经暂停原文翻译了,后面的几篇文章都是对原文进行加工和总结后得出的。 36 | 37 | 阅读和翻译英文书的收获非常大,对很多的知识的理解比直接阅读中文教程要深刻的多,同时也大幅度提高了自己阅读英文教程的能力。 38 | 39 | # 收获 40 | 41 | 最大的收获,莫过于Swift的学习了,了解基本语法的同时,有机会也会自己看一下已经开源的部分的实现原理。 42 | 43 | 磨刀不误砍柴工,我减少了很多写项目的时间,希望把基础知识弄扎实,因为很多bug往往来自于对某个概念的错误理解。与其一知半解的去解决bug,不如先掌握知识。所以在读书的同时,我也开始深入的思考iOS开发的一些基础知识: 44 | 45 | * AutoLayout和UIScrollview的联合使用 46 | * UIScrollview性能优化 47 | * GCD和NSOperationQueue 48 | * UIView的生命周期 49 | * Swift与OC在复制对象时的异同 50 | * Swift与OC闭包的异同 51 | 52 | 感谢Google上的各种资料,我完成了自己的[Xcode插件](https://github.com/bestswifter/XcodeCareer-Plugin),用来统计在Xcode中所有写过的代码行数和写代码时间,不过好像有隐藏的bug,后来也就不维护了。因为看过《程序员的自我修养》,对一些底层的知识有了浅显的了解,所以在Google的帮助下,自己破解了一款收费应用:[一个数字的魔法——破解Mac软件之旅](http://bestswifter.com/blog/2015/11/24/%5B%3F%5D-ge-shu-zi-de-mo-fa-po-jie-macruan-jian-zhi-lu/) 53 | 54 | 仔细想想,其他乱七八糟的东西也学了不少,实习期间相当长的时间在做C++项目,写了一个静态库分别给OC和Java调用,所以也稍微了解了一下C++。发现了CSDN博客的一个bug,又去了解了一下Python,写了一个脚本把自己积分刷到了第一。学校有安卓的课,所以不得不写了个[安卓应用](https://github.com/bestswifter/MathModeling-Android),也算是了解了一些简单的Java和安卓开发。年底的时候注册了bestswifter域名,搭建了[个人博客](http://bestswifter.com/),虽然注定要被打脸,但也希望能够鞭策自己。 55 | 56 | # 2016年的计划 57 | 58 | 1. 文章要继续阅读和翻译英文书籍。目前Advanced Swift翻译了三分之二,一月份估计可以结束。下一个目标或许还是Objc的书:Core Data。事实上总是有读不完的好书的。 59 | 60 | 2. 阅读优秀的博文。实际上今年2015年的很多时间浪费在一些低质量的文章上,不仅学不到知识,还把自己弄得晕头转向。NSHispter和Objc.io有非常多优秀的文章,足够在2016年好好拜读了。 61 | 62 | 3. 技术与基础。有些知识点还没来得及学习,有些学过但是长期不用已经忘了,目前想到的涉及这几点:响应者链与事件处理、KVO、几种消息传递机制的比较、Runtime、NSUrlSession、CALayer等等等等。虽然要学的太多,不过一直很喜欢一句话:“怕什么真理无穷,进一寸有一寸的欢喜”。 63 | 64 | 4. 大学期间就该做大学里该做的事,比如读书。目前想到的是《代码整洁之道》、《图解TCP/IP协议》,如果有空希望可以深入了解Mac与iOS操作系统。 65 | 66 | 5. 实习。目前对创业公司有了一定了解,希望能够到一家大公司实习几个月。 67 | 68 | 虽然任务浩繁,不过总得尽力完成,希望一年后的自己与现在判若两人! -------------------------------------------------------------------------------- /articles/2017-conclusion.md: -------------------------------------------------------------------------------- 1 | # 我的 2017 年度总结 2 | 3 | ## 我的技术积累 4 | 5 | 作为一名还在靠技术吃饭的程序员,最关心的当然还是每年技术的成长。17 年写的文章并不多,我数了下大概在十篇左右。大概是有三方面的原因: 6 | 7 | 1. 杂事较多:毕业,各种论文、旅游、游戏占用了相当多的时间。 8 | 2. 开通了自己的 **[小专栏](https://xiaozhuanlan.com/bestswifter)** 和 **[知识星球](https://wx.zsxq.com/dweb/#/index/8481441222)**,有些比较短的心得总结就没有分享出来。 9 | 3. 很欣喜的看到自己文章更有深度、更有质量了。学了很多,写的比较少,不再把一些不成熟、不成体系、没有创新的文章发出来。 10 | 11 | 17 年学习的 iOS 相对比较少了。一方面很多基础知识已经掌握了,对于新的框架只要简单知道个原理就行了,不用再扣细节。主要是对自动化测试和 iOS 工程化有了简单的了解。 12 | 13 | ### 工作效率 14 | 15 | 最大的收获莫过于整理出来了自己的脚本库:[macbootstrap](https://github.com/bestswifter/macbootstrap),主要用于配置新电脑的开发环境。会自定义系统的一些配置,安装 Homebrew 以及常用的包并同步配置,整理并汇总了以前常用的 Zsh 脚本(还在持续开发中),添加了 Git 的 alias 与用法,配置好了 Vim 的插件。 16 | 17 | 这个库不仅仅是脚本的整理,也像是一份教程和备忘录,在忘记的时候可以速查。有新的功能时,我都会在[我的微博话题](https://weibo.com/p/100808c3e3501a6ae1093e3f86879bd2783130/super_index) 发布,欢迎关注。 18 | 19 | 翻墙和代理是每个程序员必备的技能,我也花了相当长的时间,算是整理出了一份最佳实践吧:[全自动科学上网方案分享](./fq.md)。 20 | 21 | 这里最核心的概念就是可配置,比如它可以做到: 22 | 23 | 1. 电脑访问不同的网页时采用不同的策略,比如 google.com 会翻墙而 baidu.com 则是直连。这样可以加快速度并节省 ss 流量。 24 | 2. 电脑上不同 app 可以采用不同的策略,比如 微信 直连 而 Telegram 则是翻墙,好处同上。 25 | 3. 手机也可以根据不同的网页采用不同的策略,除了 可以配置为翻墙和直连外,还可以在请求特点地址(比如公司的内网)时自动代理到电脑的 Charles 上,做到翻墙、直连、调试三不误。 26 | 4. 上述配置可以汇总为一个策略,在不同场景(比如 公司 WiFi、家庭 WiFi、4G)下**自动**选择不同策略,因为回到家以后就没有必要再连 Charles 代理了。 27 | 28 | ### Appium 与自动化测试 29 | 30 | 其实毕设就写了基于 Appium 的自动化测试系统,在公司也把一个小项目给落地了,对整个自动化测试的原理和实践有了更深的理解。但由于人力的问题没能把项目做大做完美,留了一个很小的遗憾。现有的、基于图像比较的自动化测试系统基本可以处理绝大多数情况,但由于操作精度的问题,并不适合极度复杂的场景。另外由于视频画面是动态变化的,也不太适合此类测试。 31 | 32 | 总的来说,自动化测试的方向有三个:提高工具稳定性、提高操作精度、降低 Case 维护成本。自动化测试是属于锦上添花的那种工具,希望在新的团队里有机会能够落实并推广吧。 33 | 34 | 文章整理在这里:[Appium 从入门到原理](./appium.md),欢迎交流。 35 | 36 | ### 网络 37 | 38 | 今年对网络层的研究比较多,因为我们做广告的,主要还是侧重于和后端的数据传输,至于客户端的绘制,一般倒不会太难。 39 | 40 | 由于去年准备春招的时候已经把 TCP/IP 看的差不多了,今年基于 TCP/IP 拓展了不少知识。 41 | 42 | 比如 [试图取代 TCP 的 QUIC 协议到底是什么](./quic.md) 这篇文章看起来是写 QUIC 协议,但其实新协议的出现一定是要解决现有问题的。所以如果不对 TCP 协议有比较深刻的理解,是无法读懂这篇文章的。 43 | 44 | 此外还把 HTTPS 的相关问题整理出了一篇文章:[九个问题从入门到熟悉HTTPS](./https-9-questions.md)。 45 | 46 | 在工作过程中,经常会与 HTTP 规范打交道,由于通过互联网只能传输由 0、1 组成的二进制数据,所以难免要和各种编码方式打交道,比如 JSON、URLEncode 和 Base64 等,对于他们的输入内容、输出内容、使用场景和鉴别方式应该有最基本的了解,于是写了一篇总结:[小谈 HTTP 中的编码](./http-encoding.md)。 47 | 48 | 除了上述理论整理外,在追查性能问题时还使用了 WireShark 这一复杂但是功能强大的工具。想当初还是在计算机网络科上用它来具体分析各层协议的细节,再次使用它时又对 IP/DNS/TCP/HTTP 等协议有了更深入的理解。通过分析具体的流量和表现,与那些曾经只是停留于纸上的概念能够吻合时,收获是非常巨大的。因此也写了一篇 [利用 WireShark 深入调试网络请求](./wireshark.md) 作为总结。 49 | 50 | ### Xcode 编译与打包 51 | 52 | 年初的时候把 Cocoapods 的工作原理简单的整理了一下,对 Xcode 工程配置有了更深一些的理解。后来又承担了团队内部 CI 的优化工作,中间不可避免的用 Ruby 写了一些脚本,主要借助 Xcodeproj 这个库,对项目做一些编译时的改造。总结了一篇 [细聊 Cocoapods 与 Xcode 工程配置](./cocoapods-xcode.md)。 53 | 54 | ### 全栈化 55 | 56 | 年初的时候学习了一些 JS,主要是对 JS 中的模块化有了一些原理上的认识,总结了一篇博客:[JavaScript 模块化简析](./javascript-modules.md)。个人对这种学习方式是相当满意的,因为不仅仅是阅读别人的博客,而是自己从零开始去感受 JS 的语法,找到问题的所在(JS 原生没有 import/include 这类机制),并且对 CMD/AMD 这类规范的实现原理有了一些了解。对于我来说,这些规范的名字和特性并不重要(反正也不写),但了解它们解决了什么问题、如何解决的,则非常有意义。 57 | 58 | 因为对 Promise、Async/Await 比较感兴趣,后来也自行学习过一些。深入思考了导致回调地狱的原因,查阅了 Promise 的实现源码,并思考了 Async 的实现原理,总结了一篇博客:[异步与回调的设计哲学](./javascript-async.md)。虽然自己尝试写 Demo 失败了,但至少知道是基于协程来做的,后来在学 Python 的时候也印证了这一点。想尝试在 OC 里面造一个轮子,但碍于时间的限制,也一直没有动工。唯一的遗憾是没有想清楚在 Promise 和 Async 中,处理异常的思路,先在写代码还在瞎猜。 59 | 60 | 后来利用业余时间,用 Vue 把公司一个小的配置管理平台重写了,UI 更加美观会吸引更多的用户。这个过程其实没啥说道的,无非就是选择脚手架、选择 IDE、选择 UI 框架、开始撸码。后来因为时间问题,只产出了第一版,一个勉强能用的模型,就被拉去做业务需求了。不过一整套流程下来,对前端开发还是有了初步的认识,其实做一个中后台的页面还是比较简单的。 61 | 62 | 期间遇到了各种问题,也基本上通过查资料和 XJB 乱改,自行解决了。不过对于 Vue/React 里面的组件以及 Redux 库有了基本的了解和使用经验,但纯理论的理解还不够深入,希望新的一年里把这一层面的内容再补一补。自己还使用过 Webpack 的插件来完成一些特定的需求,然而现在又出了 Parcel,连 Webpack 都升级了。。。真的是三个月不看前端,技术就被淘汰了。 63 | 64 | 除了前端和 JS 领域的学习,对一些更加基础的知识也略有研究。比如了解了一点设计模式([我眼中的工厂模式](./design-pattern-factory.md)),了解了一些基本术语([指针和引用的区别](./pointer-and-reference.md)),基本上搞定了 Python 的编码问题([字符串编码入门科普](./string-encoding.md))。 65 | 66 | 在准备毕设期间,我写了一篇非常长的文章,比较全面的介绍了语言的编译原理:[大前端开发者需要了解的基础编译原理和语言知识](./compile-and-language.md),在查阅资料和写作的过程中收获非常大。 67 | 68 | 趁着七月份刚入职的功夫,还深入系统的学习了 Python,读完了一本非常棒的数:《流畅的 Python》,把书中很多例子都自己写了一遍,印象比较深刻,后来也整理出了一篇 55K 字的文章:[Python 入门指北](https://xiaozhuanlan.com/topic/1053427869)。 69 | 70 | ## 工作积累 71 | 72 | 因为涉及到公司业务,也没啥能详细说的说的,总的来说就是继续踩了很多广告方面的坑。做了一些样式和性能方面的优化。 73 | 74 | 年初的时候做过一些安卓方面的开发,收获颇丰,有技术方面的积累也有业务上的思考。原本计划年底学习一下贴吧的安卓代码,没想到后来去做了手百,年底又做了好看,年后干脆都离职了。。。真是计划赶不上变化啊。 75 | 76 | 平时也会督促自己多听一听别的团队的分享,对不少广告术语有了更多的认识,但暂时也感觉不到什么作用。 77 | 78 | 比较有意思的一个技术项目是做 UI 的动态化展示,接触到了 FlexBox 布局模型。因为写过一些 demo,所以对里面的字段勉强说得上熟悉吧,然后自己又删掉了一些客户端不常用的字段,加上了一些和业务相关的字段,最终的效果是像乐高积木一样,通过端上定义元控件样式 + 服务端下发组合方案,实现了布局的动态化,算是一个轻量级的 RN 吧(没有业务逻辑)。 79 | 80 | 还做过一个 Feed 预加载的优化项目,因为落地页都是广告主页面,所以相当比例的网页是动态改变的,并不适合预加载。于是又设计了一套极为复杂的监控系统,用来识别哪些网页可以预加载,哪些不可以。。。 81 | 82 | 年底的时候开始做好看视频,被视频的开发和打点统计折磨得痛不欲生,后来和竞品团队交流了一下,发现大家都非常痛苦。 83 | 84 | 由于工作需要,也写过一段时间 SQL 语句,各种 join 了解过一点吧,但再深入的知识就接触不到了。个人的计划是先用两三年的时间把大前端融会贯通,再向后端发展。 85 | 86 | ## 总结与计划 87 | 88 | 回望我在 [2016 年总结](./2016-conclusion.md) 中给自己提出的几个小目标,其实完成的不算很理想。 89 | 90 | 1. 业务:曾计划 17 年深入的了解一下广告业务。回过头来看,知道的肯定是更多了,但是距离预期还有些差距。不过广告业务确实是极为复杂,也不是一两年能完全搞明白的。这一块给自己打 60 分,及格吧。 91 | 2. 阅读:16 年的计划是阅读 《七周七并发模型》和 《改善 Python 程序的 91 个建议》,前面一本书读了好几遍,无奈并不能读懂,只能放弃。后者没有读,但是读了更好的 《流畅的 Python》。因为只写了一段时间安卓,所以 Java 的学习笔记少,《Java 编程思想》 几乎没读几页,《深入理解 Java 虚拟机》倒是基本上读完了。因为回学校准备毕设期间时间相当充沛,不仅读完了 《Unix 编程艺术》 和 《编译系统透视》这两本非常大部头的数,还读了一半《领域特定语言》,可惜没有机会实践。总的来说因为时间充沛,所以对自己的读书成果还是相当满意的,给自己打 120 分吧。做得好就是应该表扬! 92 | 3. 技术:继续维护了用 Python 开发的网络中间人平台,并且深入的学习了 Python。虽然没有机会再写 Java,但积累了一些前端的开发经验,所以基本上算是完成了任务吧,可以给自己打 95 分。 93 | 94 | 17 年的行动基本上完成了 16 年的计划,有超额完成的,也有不足的。安卓没有机会去写了,但收获了前端开发技能。总的来说对自己是基本满意的。 95 | 96 | 新的一年,我又给自己订了几个小目标: 97 | 98 | 1. 业务:对 iOS 平台的一到两个点进行深入研究,比如组件化与热修复。不仅要懂原理,更要把事情落地,做出成绩来。 99 | 2. 阅读:今年打算读一些偏向原理类的书,而不再是具体的技术。比如《重构,改善既有代码的设计》和《代码简洁之道》(英文名叫 Clean Code,注意不是 Coder)。 100 | 3. 技术:接触到更多的前端和后端开发。前端开发方面,不仅仅满足于页面的实现,要对 Webpack、单向数据流、UI 组件、JS 语法有更深入的理解。如果有机会做一些后端方面的开发的话,先了解语法和框架就可以了。 -------------------------------------------------------------------------------- /articles/2018-conclusion.md: -------------------------------------------------------------------------------- 1 | # 2018 年总结 2 | 3 | ## 回顾 4 | 5 | 第四篇年终总结了,照例回顾一下[去年的计划](./2017-conclusion.md): 6 | 7 | > 新的一年,我又给自己订了几个小目标: 8 | 9 | > 业务:对 iOS 平台的一到两个点进行深入研究,比如组件化与热修复。不仅要懂原理,更要把事情落地,做出成绩来。 10 | > 11 | > 阅读:今年打算读一些偏向原理类的书,而不再是具体的技术。比如《重构,改善既有代码的设计》和《代码简洁之道》(英文名叫 Clean Code,注意不是 Coder)。 12 | > 13 | > 技术:接触到更多的前端和后端开发。前端开发方面,不仅仅满足于页面的实现,要对 Webpack、单向数据流、UI 组件、JS 语法有更深入的理解。如果有机会做一些后端方面的开发的话,先了解语法和框架就可以了。 14 | 15 | 虽然今年感觉碌碌无为的混过去了,不过对比了一下计划,貌似完成度还可以接受: 16 | 17 | 1. 业务:了解了基于 LLVM 的热修复原理,参与了一部分和 Clang 相关的开发。了解了一些组件化的方案,积累了一些踩坑的经验。重度参与了公司组件平台的开发工作,并且在公司内上线,也算是落地了。这一项给自己打 0.8 分, 18 | 2. 阅读:这方面完成的很不好,断断续续的学习了一点点设计模式,没什么值得说的收获,后面做具体的分析和总结。这一项给自己打 0.3 分。 19 | 3. 技术:基于 Vue 实现了前端 SPA 的开发,基于 Express 实现了后端的开发,略懂一点 Webpack 的配置,JS 语法,单向数据流什么的也算是接触一些。比较满意的是技术在实战中得到了锻炼,不太满意的是缺少沉淀和总结。这一项给自己打 0.7 分。 20 | 21 | 总的来说 18 年计划的完成度还可以,除了上述计划,还有额外的收获,给自己 60 分的及格成绩吧。 22 | 23 | ## 工作 24 | 25 | 三月初入职了字节跳动。最主要的原因还是因为算上实习,在上家公司已经呆了两年,积累了很多理论知识,希望能有实践的机会。字节跳动这边是新成立的部门,刚好能提供比较好的机会。当然说实在的,字节跳动提供的薪水也很难拒绝,18 年的生活水平有了明显提高。 26 | 27 | 入职以后的第一个月,做的事情还比较琐碎,主要是解决 Xcode 工程与编译方面的问题以及 Clang 方面的插件开发,当时还准备基于 JSPatch 的方案来做热修复,所以开发了基于语法分析的 OC 转 JS 工具。后来团队调研了基于 LLVM 的热修复方案并且决定放弃 JSPatch 的过渡做法,我也就没有参与后续的开发了。 28 | 29 | 那段时间相对比较空闲,压力也不大,闲暇之余还学习了 [libextobjc](https://github.com/jspahrsummers/libextobjc) 的源码。这也是 18 年最后和 iOS 相关的研发、学习。 30 | 31 | 大约一个月后就全人力开始了组件化平台的前后端开发工作。最初的一两个月主要是新项目启动的问题,包括前后端脚手架的搭建,技术方案选型,登录、用户体系等业务。这一阶段主要是遇到各种技术问题,好在通过自己的研究都解决了,并且很快的在公司内上线。 32 | 33 | 从下半年开始,随着接入平台的业务越来越多,需求也越来越定制化,一方面要为之前的零经验买单,补上欠下的技术债,另一方面也要完成业务方提出的需求,同时还要参与平台未来业务的规划,但是人力却没有任何增加。因此在相当长的一段时间里,陷入了比较焦虑和低迷的状态,具体表现为每天大量的加班,但效率降低,产出并不明显。最终形成了比较恶性的循环。 34 | 35 | ### 收获 36 | 37 | 虽然压力很大,工作也很辛苦,但收获也是比较可观的,主要从三个方面来总结吧: 38 | 39 | 1. 技术:技术方面的东西比较好衡量,也很直观。主要就是前后端知识的学习,积累了完整的项目经验,对研发流程有了比较深入的了解,虽然不是专业的前后端开发,但在后来和相关同学对接时,发现自己除了项目经验有所欠缺,但在设计、规范和效率上并不逊色,甚至优点还很明显。此外,在 shell/git 脚本,正则表达式方面也有了不小的进步。由于也涉及到 iOS 和 安卓 的组件化任务,对相关的技术也有初步的涉猎。 40 | 2. 效率:逐渐认识到,一个优秀的程序员,绝对会遇到任务太多,但时间不够的情况,想要做完所有的事情是不现实的。因此对任务制定合理的优先级,规划排期,是工作的常态,也是必备的技能。同样的两个任务,一个先做一个后做,效率和收益可能是完全不同的。这方面的收获主要在于对 “重要-紧急-四象限” 概念的认知和实践,以及建立了一定的任务排期,周报总结的概念并付诸实践。 41 | 3. 成长:因为公司还比较小,处于高速成长的过程中。因此部门在推动工作进行时,难免遇到和其它部门的合作,甚至是竞争。这方面我还相当不成熟,犯了很多错。这方面的技巧相对来说不容易总结,只能说多多积累,不断打磨心性吧。公司越大,做人就越比做事重要。 42 | 43 | ## 生活 44 | 45 | 18 年的生活比较精彩,变动也很多,客观的讲,占用了很多学习技术的时间。 46 | 47 | 年初的时候,随着工作调动也搬了家,从十几平米的合租主卧搬入了六十多平的整租小两居: 48 | 49 | ![](http://blog.bestswifter.com/WechatIMG5.jpeg) 50 | 51 | 年底提前续约,竟然租金还小幅度下降了一些,明年的生活压力也不会太大了。 52 | 53 | 五月份的时候天津发布了海河英才计划,奔波十几趟以后拿到了户口,随后掏空父母的积蓄在郊区接盘了一套房子,并且一直后悔到现在。 54 | 55 | ![](http://blog.bestswifter.com/zhunqianzheng.jpeg) 56 | 57 | 十月份迎来了一位新的家庭成员:一只可爱的渐层猫猫 58 | 59 | ![](http://blog.bestswifter.com/cat.jpeg) 60 | 61 | 年底借着出差的机会,第一次来到深圳,参观了世界之窗,腾讯滨海大厦等地标性建筑。 62 | 63 | ![](http://blog.bestswifter.com/tencent.jpeg) 64 | 65 | ## 中产? 66 | 67 | 18 年家里购买了大尺寸的电视、空气净化器,配置了六千多的床垫,以及配套床上用品。生活质量有了明显的提高。 68 | 69 | 饮食方面,从成都苍蝇巷子里的小吃、人均不到一百的火锅,再到几百块的日料自助、元旦接近一千的盘古七星,几乎不会限制饮食方面的任何预算。美食成为了对抗工作压力的唯一方式。 70 | 71 | 下半年跟着公司团办了浦发的 AE 白,开始研究起了信用卡权益,至今不知道是谁薅了谁的羊毛。年底办理了香港的银行卡,方便后续美股和港股的投资。 72 | 73 | 除了美食以外,最大的消费其就是几乎配齐了全套的苹果设备,好在公司提供了最新款的电脑,省下了一大笔开销。 74 | 75 | 总的来说 18 年的收入和消费水平都有大幅度提高,同样提高的还有生活质量和幸福感。不过算上股票大跌带来的亏损,18 年的财务情况并不是特别符合预期,需要警惕陷入中产生活的消费陷阱中。 76 | 77 | ## 计划 78 | 79 | 和往年不同的是,随着工作经验的丰富,技术不再成为唯一关注的对象,往后的年度计划会变更为技术和生活两大部分。 80 | 81 | ### 技术: 82 | 83 | 1. 在现有业务的基础上寻求技术突破,尝试利用新技术带来业务的收益,目前看到的方向包括 GraphQL、后端技术栈标准化,页面和网络性能优化等。 84 | 2. 对组件化和基于组件化的研发流程有更深入的理解,对 Android 组件化有最基本的了解,注重业务经验的积累,争取业务上的突破 85 | 3. 对设计模式、正则表达式等计算机基础知识有更深入的理解 86 | 87 | ### 生活: 88 | 89 | 1. 注重身体健康,完成体检一次,重疾险购买一次,体重控制在 72kg 内,恢复运动,身体素质有明显提升 90 | 2. 控制非必要消费,与国家共克时艰,但对具有长期受益的投入(比如床垫/空气净化器)不设限制。 91 | 3. 国内旅游 2 次,继续享受美食 92 | -------------------------------------------------------------------------------- /articles/advanced-swift-chinese.md: -------------------------------------------------------------------------------- 1 | # Advanced Swift 中文版 2 | -------------------------------------------------------------------------------- /articles/alfred-workflows.md: -------------------------------------------------------------------------------- 1 | 之前曾写过一篇文章:[如何大幅度提高 Mac 开发效率](https://bestswifter.com/efficient-mac/),主要介绍了各种提高 Mac 效率的方法,美中不足的是忘记了介绍神器 Alfred。 2 | 3 | 更大的问题在于,不管是 Alfred 还是其他的应用,我们都是被动的使用者。也就是说我们只能学习如何使用现有的工具,却不能利用自己的编程技能去提高效率,而所谓的工具,唯一的目的和价值就是提高自己的工作效率,解决痛点,而非被人学习如何使用。 4 | 5 | 这篇文章的主要目的就是介绍如何在 Alfred 打造自己的 workflow,从而能自己发现、并解决那些可以被自动化脚本取代的、无聊的工作。作为示范,我提供了两个插件: 6 | 7 | 1. 快速打开&关闭 shadowsocks 连接 8 | 2. 截图后快速上传到七牛 9 | 10 | 他们使用不同的技术制作而成,但宗旨只有一个:**“找到一切可以被自动化的流程并且用脚本来完成”**,都开源在[我的Github](https://github.com/bestswifter/my-workflow)。 11 | 12 | # Workflow 13 | 14 | workflow 是 Alfred 中最强大的部分,它能把常见工作转变成一个工作流 15 | 16 | # AppleScript 17 | 18 | 如果你使用 GoAgentX 这个软件,并且用 shadowsocks 翻墙,那么频繁的关闭和打开连接应该是无法避免的。 19 | 20 | 为了避免频繁的移动鼠标和点击,我希望用命令的方式来打开和关闭 shadowsocks 连接,如下图所示: 21 | 22 | ![演示](https://o8ouygf5v.qnssl.com/alfred/GoAgentX-Alfred.gif ) 23 | 24 | 这其实类似于按键精灵的思想,因为我们希望有人能自动点击那个连接按钮或者关闭按钮。这种情况下可以考虑使用 AppleScript。它的作用是方便我们和程序进行交流,从而执行一系列程序内置的操作指令。 25 | 26 | 这里我不想自己介绍它的语法,因为实在是太白话了,文末的参考资料中也有详细的解释。来看一下第一个插件的脚本: 27 | 28 | ```applescript 29 | on alfred_script(q) 30 | -- 根据自己的需要修改profileName 31 | set profileName to "shadowsocks" 32 | tell application "GoAgentX" 33 | if q is "c" then 34 | toggle status of profileName to running 35 | else if q is "d" then 36 | toggle status of profileName to stopped 37 | end if 38 | end tell 39 | end alfred_script 40 | ``` 41 | 42 | 如果你略具备编程经验,你一定会好奇这行代码是怎么写出来的,以及他们的语法是什么: 43 | 44 | ```applescript 45 | toggle status of profileName to running 46 | ``` 47 | 48 | 首先打开系统应用:脚本编辑器(Script),然后选择 文件->打开字典,找到 GoAgentX 这个应用,你就会发现所有的语法都在这里了: 49 | 50 | ![GoAgentX支持的所有脚本命令](https://o8ouygf5v.qnssl.com/1468480517.png ) 51 | 52 | 所以,有心的开发者日后在自己的应用中也可以考虑添加对 AppleScript 的支持。 53 | 54 | # Python 55 | 56 | 如果是和外部程序交互,比如网络请求,文件读写,执行 bash 脚本等等,就不再适合使用 AppleScript 这么简单的脚本了,推荐使用 Python 进行编程。 57 | 58 | 长久以来,写博客时把图片上传到图床一直是一个繁琐的任务: 59 | 60 | 1. 截图 61 | 2. 打开截图保存目录,复制图片 62 | 3. 去七牛上传 63 | 4. 复制 url 64 | 5. 粘贴到 markdown 文本中 65 | 66 | 通过 workflow,我们可以把上述步骤简化为简单的三步,省略大量时间: 67 | 68 | 1. Command+Ctrl+A 截图(图片不要保存在本地,存在剪贴板中即可) 69 | 2. 在 Alfred 中输入 `gn` 70 | 3. 图片会自动上传,并且把 Url 复制到剪贴板中,你只要按下 Command + V 即可 71 | 72 | 具体的实现也不复杂,由于已经开源所以就不分析了,简单介绍一下用法: 73 | 74 | 1. 首先运行在 Alfred 中输入 `qnconf`,后面要加上 AK 和 SK,这是你的密钥,可以从官网获取,输入如下: 75 | 76 | qnconf xxx xxx 77 | 78 | 2. 在 workflow 插件的文件夹中,有一个 `conf.txt` 文件,你需要打开它,设置上传到哪个 bucket,以及你的图床前缀,可以根据我现有的文件做修改。 79 | 3. 使用时,确保剪贴板中有图片,在 Alfred 中输入 `qn` 后面也可以指定一个参数名,表示上传图片的名称。如果不写则默认是当前时间戳。 80 | 4. 运行后,图片的 url 已经复制在你的剪贴板中了,尽情使用吧。 81 | 82 | # 写在最后 83 | 84 | 这篇文章的目的不是介绍 workflow 的开发,也不是介绍 Python 的语法,这些资料网上应有尽有。 85 | 86 | **学习一门脚本语言,结合一个伟大的应用,大幅度提升自己的效率**,这种一举两得的事情实在是再美好不过了。 87 | 88 | # 参考资料 89 | 90 | 1. [AppleScript的终极入门手册](http://www.jayz.me/?p=267) 91 | 2. [Alfred workflow 开发指南](http://myg0u.com/python/2015/05/23/tutorial-alfred-workflow.html) 92 | 3. [pngpaste](https://github.com/jcsalterego/pngpaste) -------------------------------------------------------------------------------- /articles/android-drawble-reuse.md: -------------------------------------------------------------------------------- 1 | # 复用 Drawable 2 | 3 | **Drawable** 表示了一类通用的,可被绘制的资源。与View 的主要区别就在于,Drawable 并不会响应事件。 4 | 5 | ## Drawable 的复用机制 6 | 7 | Drawable 的使用非常方便,系统框架内部有 700 多种默认的 Drawable。当我们新建一个 button 时,实际上它的背景就是一个默认的 Drawable。 8 | 9 | 每个 Drawable 都有一个 `constant state`,这个 state 中保存了Drawable 所有的关键信息。比如对于 Button 来说,其中就保存了用于展示的 Bitmap。 10 | 11 | 由于 Drawable 非常常用,为了优化性能(其实主要就是节省内存),所有的 Drawable 都共享同一个 `constant state`。 12 | 13 | ## 重用 state 14 | 15 | 这种优化有时候也会导致一些问题,比如: 16 | 17 | ```java 18 | Drawable star = context.getResources().getDrawable(R.drawable.star); 19 | if (book.isFavorite()) { 20 | star.setAlpha(255); // opaque 21 | } else { 22 | star.setAlpha(70); // translucent 23 | } 24 | ``` 25 | 26 | 假设有多个 book 对象构成一个 `listview`,我们希望的效果是喜欢的图书,星星是亮的,否则是灭的。但如果使用上述代码就会发现,所有星星的颜色都是一样的。 27 | 28 | 这是因为 alpha 信息保存在 constant state 中,所有的星星都共享这个 state,对任何一个的修改都会影响其他所有的。 29 | 30 | 解决方案是使用 `mutate` 方法。这个方法会返回同一个 Drawable 对象,但是其中的 state 被复制了,这样对 state 的修改就互不干扰: 31 | 32 | ```java 33 | Drawable star = context.getResources().getDrawable(R.drawable.star); 34 | if (book.isFavorite()) { 35 | star.mutate().setAlpha(255); // opaque 36 | } else { 37 | star.mutate().setAlpha(70); // translucent 38 | } 39 | ``` 40 | 41 | ## 重用 bitmap 42 | 43 | 不过有时候仅仅复制 state 还不够,因为所有的 state 还会共享同一个 Bitmap,也就是说调用 `mutate()` 方法并不会复制 Bitmap。 44 | 45 | 假设我们有两个 TextView 需要设置圆角,我们可以首先创建一个 `GradientDrawable` 对象并设置圆角: 46 | 47 | ```java 48 | GradientDrawable gd = new GradientDrawable(); 49 | gd.setColor(Color.parseColor("#000000")); 50 | gd.setCornerRadius(context.getResources().getDimension(R.dimen.ds4)); 51 | gd.setStroke(1, Color.parseColor("#000000")); 52 | ``` 53 | 54 | 接下来,任何需要设置圆角背景的 TextView 都可以调用: 55 | 56 | ```java 57 | textview.setBackgroundDrawable(gd); 58 | ``` 59 | 60 | 然而由于 Drawable 对象的 Bitmap 会被复用,所以即使我们调用了 `mutate()` 方法,所有的 TextView 的圆角背景区域依然都会以最后一个 TextView 的大小为准。 61 | 62 | 在这种情况下,我们可以通过 `constant state` 创建一个新的 Drawable 对象,此时这两个完全不同的对象会使用不用的 Bitmap,也就避免了上述问题: 63 | 64 | ```java 65 | textview.setBackgroundDrawable(gd.getConstantState().newDrawable()); 66 | ``` 67 | -------------------------------------------------------------------------------- /articles/android-xml.md: -------------------------------------------------------------------------------- 1 | 虽然大三曾经短暂的接触 Android,但都只是囫囵吞枣,仅仅停留在调用 API 和开源库完成效果的程度上。既然真的要开始搞 Android,还是有必要刨根问底一下的。 2 | 3 | 作为入门,最近开始看 Google 的 [Android Training](https://developer.android.com/training/index.html)。最简单的肯定是创建一个 Hello World 工程,不过在写 LinearLayout 的时候,我发现一个比较奇怪的问题: 4 | 5 | ```xml 6 | 7 | 13 | 14 | ``` 15 | 16 | XML 文件中的前两行代码似乎很啰嗦,定义了两个 `xmlns` 和一长串没有意义的 URL。 17 | 18 | 实际上这里的 `xmlns` 指的是 XML 中的命名空间(namespace)概念。比如这里的 `android:layout_width` 属性,它就是 `android` 命名空间下的属性。如果没有命名空间的约束,整个 XML 中就不能出现重复的属性,事情就会很麻烦。 19 | 20 | 除了安卓默认提供的命名空间和控制 UI 样式的属性外,有时候,我们还可以自定义命名空间和属性,比如对于某个颜色来说,我希望它在普通模式和夜间模式下具有不同的样式,但对使用者完全透明(即对外只有一个颜色名)。 21 | 22 | 此时,就可以自定义一个 `app:bg_color`。要做到这一点,我们需要实现 `LayoutInflater.Factory` 接口并实现 `onCreateView` 方法。在将 XML 转化(inflate)为 View 的时候,实际上就是读取 XML 树中的各种属性和值,而 `onCreateView` 方法可以理解为这一过程的 Hook。 23 | 24 | 除此以外,我们也可以简单的添加几个常用的属性,[这篇文章](http://stackoverflow.com/questions/2695646/declaring-a-custom-android-ui-element-using-xml) 详细讲述了实现过程。 25 | -------------------------------------------------------------------------------- /articles/appium.md: -------------------------------------------------------------------------------- 1 | 因为业务需求和准备毕设,最近开始研究自动化测试的内容。由于同时要做 iOS、安卓和 Web 测试,我们最终选择了 Appium 这个开源工具并基于它做一些封装,从而能够使用一套公共 API 完成移动端的双端测试。本文主要会基于一些开源代码和个人实践,对 iOS 端的自动化测试原理做一个简单介绍,Android 略有区别但也大致同理。 2 | 3 | 其实文章没有很长,也没有太多技术含量,驱使我写这篇文章的主要原因是 Google 上能搜到的绝大多数博客都是错的,大多是根据一篇老旧过时的文章抄抄改改。所以真的很想问问这些文章的作者,你真的搞懂 Appium 的原理么?对这一些错误的知识有可能搞懂么?自己不先搞懂,怎么能昧着良心写进博客里? 4 | 5 | # Appium 6 | 7 | 我假设读者完全没有了解过自动化测试以及相关的概念,那么首先就要搞明白 Appium 是什么,大概由几个步骤组成,接下来才是对每个部分的深入了解。 8 | 9 | 简单来说,Appium 是一个测试工具,可以进行 iOS、Android 和 Web 测试,同时还允许使用多种语言来编写测试用例。那么问题就变成了为什么 Appium 支持多种语言来写测试用例,以及这些测试用例是如何运行在具体的平台(比如 iOS )上的。为了回答这个问题,我们需要把 Appium 分成三个部分来看,分别是 Appium 客户端、Appium 服务端和设备端。既然是自动化测试,那么就先从设备端说起。 10 | 11 | # 设备端 12 | 13 | 如果你按照[官网的教程](http://appium.io/slate/en/tutorial/ios.html?java#native-ios-automation)成功的运行了 iOS 真机测试,你会看到手机上多了一个名为 **WebDriverAgentRunner** 的应用,以后简称 **WDA**,这个应用的作用就是对你的目标 App 进行测试。 14 | 15 | 好吧,是不是觉得事情有点神奇了?安装了一个别人的 app,居然能唤起你自己的应用,还能执行测试,是不是存在什么黑魔法? 16 | 17 | 首先需要介绍一下苹果的 UI 自动化测试框架,在 Xcode 7 以前使用了`UI Automation` 框架,利用 JS 脚本去做应用测试。而在 Xcode 7 中苹果提供了新的框架 `UI Testing`,在 Xcode 8 中干脆直接移除了对 `UI Automation` 的支持。所以毫无疑问,在 iOS 9 或者更高的系统版本中,Appium 也是利用了 `UI Testing` 框架来做测试而不是`UI Automation`。 18 | 19 | 很多程序员应对 `UI Testing` 框架并不陌生,在新建项目的时候就有机会勾选上这个选项,或者后期通过 `Add target` 的方式补上。默认情况下,一个测试用例就是一个 `.m` 文件,模板代码如下: 20 | 21 | ```objc 22 | #import 23 | 24 | @interface Test : XCTestCase 25 | 26 | @end 27 | 28 | @implementation Test 29 | 30 | - (void)setUp { 31 | [super setUp]; 32 | 33 | // Put setup code here. This method is called before the invocation of each test method in the class. 34 | 35 | // In UI tests it is usually best to stop immediately when a failure occurs. 36 | self.continueAfterFailure = NO; 37 | // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. 38 | [[[XCUIApplication alloc] init] launch]; 39 | 40 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 41 | } 42 | 43 | - (void)tearDown { 44 | // Put teardown code here. This method is called after the invocation of each test method in the class. 45 | [super tearDown]; 46 | } 47 | 48 | - (void)testExample { 49 | // Use recording to get started writing UI tests. 50 | // Use XCTAssert and related functions to verify your tests produce the correct results. 51 | } 52 | @end 53 | ``` 54 | 55 | 可以看到一共只有三个方法, `setUp` 方法中主要做一些测试前的准备,比如这里的 `[[[XCUIApplication alloc] init] launch];` 就创建了一个被测试应用的实例并唤起它。`tearDown` 方法是测试结束后的清理工作。 56 | 57 | 所有的测试函数都必须以 `test` 开头,比如这里的 `- (void)testExample`。好吧,不得不承认 OC 这们语言还是有缺陷的,缺少了 Annotation 以后就只能用变量名来做标记,这种业务对应的 Java 表示应该是: 58 | 59 | ```java 60 | @test 61 | public void example() { 62 | // do some test 63 | } 64 | ``` 65 | 66 | 言归正传,有了这样的测试代码后,只要 `Command + U` 就可以运行测试。不过这还是没有解决之前的疑惑,为什么 Appium 可以用一个第三方 app 唤起待测试的应用并进行调试?当然,这个问题等价于,上面代码中的 `[[XCUIApplication alloc] init]` 到底会创建一个什么样的 app 实例?它怎么知道这个对象的 `launch` 方法会打开手机上的哪个 app? 67 | 68 | 这个问题似乎没有搜到比较明确的答案,不过经过实践分析以后发现,`XCUIApplication` 类存在一个私有方法,可以传入目标应用的 BundleID: 69 | 70 | ```objc 71 | [[XCUIApplication alloc] initPrivateWithPath:nil bundleID:@"com.bestswifte.targetapp"]; 72 | ``` 73 | 74 | 我们知道手机上一个 BundleID 唯一对应了一个应用,后装的应用会替换掉之前相同 ID 的应用,所以通过 BundleID 总是可以正确的唤起待测试应用。唯一要注意的是,为了顺利通过编译器的语法检测,我们在调用私有方法之前需要先构造一份 `XCUIApplication` 的头文件,声明一下将要调用的私有方法。 75 | 76 | 当我们用这个私有的初始化方法替换掉默认的 `init` 方法后,就可以正常唤起待测试应用了,不过你会发现被测试的应用刚一打开就会退出,这是因为我们的测试代码内容为空,所以很快就会进入到销毁流程。 77 | 78 | 解决问题也很简单,我们可以在 `testExample` 里面跑一个死循环,模拟 Runloop 的操作。只不过这次不是监听用户事件,而是监听某个 TCP 端口,等待网络传输过来的消息。 79 | 80 | 我们以 Facebook 开源的 [WDA](https://github.com/facebook/WebDriverAgent) 为例,看看它的 `FBScreenshotCommands.m` 文件: 81 | 82 | ```objc 83 | #import "FBScreenshotCommands.h" 84 | 85 | #import "XCUIDevice+FBHelpers.h" 86 | 87 | @implementation FBScreenshotCommands 88 | 89 | #pragma mark - 90 | 91 | + (NSArray *)routes 92 | { 93 | return 94 | @[ 95 | [[FBRoute GET:@"/screenshot"].withoutSession respondWithTarget:self action:@selector(handleGetScreenshot:)], 96 | [[FBRoute GET:@"/screenshot"] respondWithTarget:self action:@selector(handleGetScreenshot:)], 97 | ]; 98 | } 99 | 100 | 101 | #pragma mark - Commands 102 | 103 | + (id)handleGetScreenshot:(FBRouteRequest *)request 104 | { 105 | NSString *screenshot = [[XCUIDevice sharedDevice].fb_screenshot base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength]; 106 | return FBResponseWithObject(screenshot); 107 | } 108 | 109 | @end 110 | ``` 111 | 112 | 这里首先会注册一个 `/screenshot` 的路由,并且制定处理函数为 `handleGetScreenshot`,然后函数内部调用 `XCUIDevice` 的截图方法。 113 | 114 | 所以分析到这里,WDA 的套路就很清晰了,它能根据被测试应用的 BundleID 将它唤起,然后自己进入死循环保证测试用例一直不退出。此时等待服务器传来 URL 数据,然后解析 URL,分发到对应模块,各个模块根据 URL 指令执行对应的测试操作,最终再把测试结果返回。 115 | 116 | # Appium 服务端 117 | 118 | 简单来说,Appium 服务端是一个 Node.js 应用,这个应用跑在电脑上,用于和 WDA 进行通信。刚刚我们看到了截图命令的 URL 是 `/screenshot`,可以想见还有别的类似的测试操作,所以 WDA 有必要和 Appium 服务端约定一套通信协议。考虑到 Appium 还支持 Android 测试,所以在安卓手机上也有类似的东西需要和 Appium 服务端进行交互。这样一来,约定一套通用的协议就显得非常重要。 119 | 120 | Appium 采用的是 WebDriver 协议。在 [w3.org](https://www.w3.org/TR/webdriver/) 上有一个对该协议的详细描述,而 [Selenim 的官网](http://www.seleniumhq.org/docs/03_webdriver.jsp) 也介绍了 WebDriver 协议。目前我尚不知道这两处介绍的关系,是互为补充 or 两套规范,但可以肯定的是下面这段话的介绍: 121 | 122 | > WebDriver’s goal is to supply a well-designed object-oriented API that provides improved support for modern advanced web-app testing problems. 123 | 124 | 所以简单的把 WebDriver 理解成一套通用的测试协议即可。 125 | 126 | # Appium 客户端 127 | 128 | Appium 客户端就是指我们写的那些测试代码了。Appium 支持多种测试语言的根本原因在于,WebDriver 协议为各种主流语言提供了一个第三方库,能够方便的把测试脚本转化成符合 WebDriver 规范的 URL。比如 [https://www.w3.org/TR/webdriver/#list-of-endpoints](https://www.w3.org/TR/webdriver/#list-of-endpoints) 就规定了包括截图、寻找元素、点击元素等操作对应的 URL 格式。 129 | 130 | 我们的项目目前使用 Java 语言来编写测试脚本,这样的好处是 Android 工程师就可以承担起维护和编写 Android、iOS 两个平台下测试代码的重任了。 131 | 132 | # 总结 133 | 134 | 其实 Appium 的原理非常简单,一句话就能概括: 135 | 136 | > 提供各个语言的第三方库,将测试脚本转化成 WebDriver 协议下的 URL,通过 Node 服务发送到各个平台上的代理工具,代理工具在运行过程中不断接收 URL,根据 WebDriver 协议解析出要执行的操作,然后调用各个平台上的原生测试框架完成测试,再将测试结果返回给 Node 服务器。 137 | 138 | 最后再友情提醒一句: 139 | 140 | > iOS 上的真机测试环境很难配置,如果一天搞不定,不要气馁,多试几次,也许明天就好了 141 | 142 | # 参考资料: 143 | 144 | 1. [[iOS9 UIAutomation] What is Appium approach to UIAutomation deprecation by Apple](https://discuss.appium.io/t/ios9-uiautomation-what-is-appium-approach-to-uiautomation-deprecation-by-apple/7319) 145 | 2. [现在开始把UI Testing用起来!](http://www.jianshu.com/p/31367c97c67d) 146 | 3. [A Beginner’s Guide to Automated UI Testing in iOS](http://www.appcoda.com/automated-ui-test/) 147 | 148 | 学习的过程中走了不少弯路,如果你看到下面这两篇文章,建议立刻按下 `Command + w`,不要浪费时间去学习错误知识了: 149 | 150 | 1. [How Appium Works?](http://executeautomation.com/blog/how-appium-works/) 151 | 2. [Appium简介及工作原理](http://blog.sina.com.cn/s/blog_c2c7d41b0102xd47.html) 152 | 3. [用实例告诉你,如何利用Appium实现移动终端UI自动化测试](http://rdc.hundsun.com/portal/article/584.mhtml) -------------------------------------------------------------------------------- /articles/async-dsym.md: -------------------------------------------------------------------------------- 1 | # 通过异步生成 dSYM 实现极速打包 2 | 3 | ## 背景 4 | 5 | 对于头条这种百万行级别的大型应用来说,即使使用 Mac Pro 进行编译打包,耗时也接近一小时。公司搭建了组件化平台后,组件得以提前编译为二进制,大大降低的应用的 CI 编译时间,目前耗时大约为八分钟左右。 6 | 7 | 通过对编译时间的进一步分析发现,大约有两分钟的时间用于生成 dSYM 文件,这个文件是 Release 模式下应用的符号表,由于 CI 打出的包不管是用于灰度测试还是内部研发人员测试,均有发生 crash 的风险和追查 crash 的需求,因此生成 dSYM 文件的步骤是不可省略的。 8 | 9 | 既然这一步不可省略,直觉告诉我们可以通过异步的方式去生成,从而避免阻塞编译构建。具体的做法为: 10 | 11 | 1. 将一次编译构建拆分为两次 12 | 2. 第一次编译不生成 dSYM 文件 13 | 3. 第二次编译再生成 dSYM 文件,由于使用了相同的代码和缓存,因此速度非常快。 14 | 15 | ## 符号化流程 16 | 17 | ### UUID 关联 18 | 19 | 经过实际测试后发现,这种做法并不可行。这里首先介绍一下 crash 日志的解析流程。 20 | 21 | 当安装在手机上的 App 发生崩溃后,系统会生成一份崩溃日志,其中记录了每个线程的调用堆栈,但是只有进程地址,没有函数名称。将进程地址转换为函数名称依赖于 dSYM 文件,这个过程也称为符号化。 22 | 23 | 一份崩溃日志必须要有对应的 dSYM 才能解析,它们通过一个叫做 UUID 的标志关联。具体流程如下: 24 | 25 | 1. 通过 xcodebuild 命令编译产物时,会生成一个 .app 文件和对应的 dSYM 文件,它们都是 Mach-O 可执行文件,都有自己的唯一标示,即 UUID。且两者的 UUID 相同。 26 | 2. App 崩溃后,系统生成一份崩溃日志,并且在其中记录下 UUID 27 | 3. 通过 UUID 找到关联的 dSYM 文件完成符号化。 28 | 29 | 崩溃日志中的 UUID 一般在 `Binary Images` 中的下一行: 30 | 31 |

32 | 33 | dSYM 文件的 UUID 可以通过 `dwarfdump --uuid` 命令获取: 34 | 35 |

36 | 37 | ### 符号化方式 38 | 39 | 崩溃日志的符号化一般有两种方式: 40 | 41 | 1. 使用 `symbolicatecrash` 命令,传入 dSYM 文件和崩溃日志,可以生成符号化以后的崩溃日志。 42 | 2. 使用 `atos` 命令,传入 dSYM 文件和崩溃日志中的具体地址,可以得到这个地址对应的函数符号。 43 | 44 | 经过测试验证,我们发现: 45 | 46 | 1. 第一种符号化方式,要求 dSYM 文件和崩溃日志中的 UUID 相同才能解析,一般个人用户会使用这种方案。 47 | 2. 第二种符号化方式,由于不涉及崩溃日志,表面上看不需要关联 UUID。著名的 Fabric 平台,和公司内的 Slardar 平台采用这种方式,并且单独存储 dSYM 文件。但在解析崩溃日志时,依然依赖 UUID 字段去找到对应 dSYM 文件。 48 | 49 | 需要说明的是,UUID **仅用于两个文件之间的关联**,苹果并不对它们的值有任何限制。以 `symbolicatecrash` 命令为例,崩溃日志和 dSYM 文件的 UUID 只要一致,**不管值是什么,均可以成功符号化**。 50 | 51 | ### 异步导出 dSYM 的方案 52 | 53 | 至此,我们已经摸清楚了最初异步导出 dSYM 方案失败的原因。在两次打包中,即使代码和缓存都一样,系统依然会产生两个 UUID。 54 | 55 | 假设第一次编译产生的 `.app` 文件的 UUID 为 A,第二次编译产生的 dSYM 文件的 UUID 为 B。在 Slardar 解析时,崩溃日志中的 UUID 为 A,但是平台只存储了 UUID 为 B 的 dSYM,虽然两者实际上可以通用,但是无法在平台上正确的关联上,导致解析失败。 56 | 57 | 因此,解决方案有以下几种: 58 | 59 | 1. 保存一份 A -> B 的 UUID 映射表,Slardar 平台根据这个关联 60 | 2. 保存一份 A -> B 的 UUID 映射表,hook 系统生成 crash 日志的流程,将其中的 UUID 从 A 改成 B。 61 | 3. 修改第二次编译生成的 dSYM 文件,将它的 UUID 改成和第一次生成的 `.app` 文件的 UUID 一致。 62 | 63 | 显然第三种方案操作更简单,并且对已有系统完全透明,无侵入和耦合。 64 | 65 | ## Mach-O 文件 66 | 67 | ### 结构 68 | 69 | Mach-O 文件由三个部分组成,分别是 Header、Load Commands 和 Data。借用比较知名的图片来展示下: 70 | 71 |

72 | 73 | 而 UUID 就是其中一个 Load Command,名字叫 `LC_UUID`。可以通过 MachOViewer 看下其中的结构: 74 | 75 |

76 | 77 | 可以看到这个 Load Command 的结构: 78 | 79 | - 前四个字节 `0000001B` 中的 `1B` 表示这是 `LC_UUID` 段。(通过 `#import ` 并输入 `LC_UUID` 可以验证,并且可以看到所有 Load Command 的枚举) 80 | - 接下来四个字节 `00000018` 中的 `18` 是 16 进制,对应到 10 进制表示这个 Load Command 的大小为 24 字节 81 | - 因此可以推算出来,剩下 `24 - 4 - 4 = 16` 个字节就是实际存储的 UUID 的数据 82 | 83 | ### 修改 84 | 85 | 借助开源工具 [LIEF](https://github.com/lief-project/LIEF/) 可以获对 Mach-O 文件做解析,分析 Header、Load Commands 等各个部分的数据。 86 | 87 | 然而这个库当前发布的所有 Release 版本均有严重的 Bug,它的解析结果是正确的,但是写入结果有问题。而最新的 master 分支虽然写入没问题,但是处理大文件时会卡死(也可能是笔者姿势不对)。因此无奈之下,仅用这个库进行解析,获取必要的数据。 88 | 89 | 写入部分其实也很简单,通过 `mmap` 把文件映射到内存中,借助 `LIEF` 的分析结果,找到 `LC_UUID` 的偏移量,手动修改指针并写回文件即可。 90 | 91 | 部分核心逻辑如下(有删减): 92 | 93 | ```c++ 94 | // mmap 读取文件 95 | int fileDescriptor = open(argv[1], O_RDONLY, 0); 96 | size_t size = _GetFileSize(fileDescriptor); 97 | char *contents = mmap(0, size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fileDescriptor, 0); 98 | ``` 99 | 100 | 主要的修改部分: 101 | 102 | ```c++ 103 | void modifyDsymUUID(char *contents, FatBinary *macho, string instructionSet, string UUID) { 104 | // 先找到对应的指令集 105 | for (Binary &binary :*macho) { 106 | Header header = binary.header(); 107 | // 根据 header 判断是不是当前需要处理的指令集,如果不是的话就略过 108 | if (!isCurrentInstructionSet(header, instructionSet)) { 109 | continue; 110 | } 111 | 112 | // 开始处理,找到 LC_UUID 段,以及这一段的偏移量和大小 113 | UUIDCommand uuidCommand = binary.uuid(); 114 | uint64_t binaryFatOffset = binary.fat_offset(); 115 | uint64_t commandOffset = uuidCommand.command_offset(); 116 | uint32_t commandSize = uuidCommand.size(); 117 | 118 | // 生成新的 uuid 数据并逐个替换 119 | std::vector newUUID = rawUUID(UUID); 120 | for (int i = 8; i < commandSize; ++i) { 121 | contents[binaryFatOffset + commandOffset + i] = newUUID[i - 8]; 122 | } 123 | } 124 | } 125 | ``` 126 | 127 | ## 后记 128 | 129 | 感谢公司内外各位大神的指教,然而受限于时间和笔者的能力,目前这个工具仅用于修改 UUID,不区分是否是 dSYM 文件,且**仅支持 armv7 和 arm64 架构**。因此项目开源在:https://github.com/bestswifter/bsUUIDModifier 130 | 131 | 欢迎修改订制与交流指正。 132 | -------------------------------------------------------------------------------- /articles/cocoapods-debug.md: -------------------------------------------------------------------------------- 1 | # Cocoapods 源码调试 2 | 3 | ## 综述 4 | 5 | 对于任何大型工程来说,硬着头皮直接看源码,往往不是最高效的选择。如果能直接从源码运行起来,并且打上断点,可以说就已经解决 80% 的问题了。 6 | 因为函数间的调用关系已经不再是瓶颈,最多就是有一些语法上的规则不了解,可能会影响阅读了。 7 | 8 | 在开始调试之前,也参考了一些网上的资料: 9 | 10 | - [使用 VSCode debug CocoaPods 源码和插件](https://github.com/X140Yu/debug_cocoapods_plugins_in_vscode/blob/master/duwo.md):我司大佬同事写的文章,但是我个人还是不太喜欢用 VSCode。 11 | 尤其是这类脚本语言,JetBrains 家的 IDE 总感觉更加专业一些。而且插件的调试流程不太自然,过于 hack 了。 12 | - [Cocoapods 插件调试环境配置](http://dreamtracer.top/cocoapods-cha-jian-diao-shi-huan-jing-pei-zhi/):和这个类似的还有几篇文章,但基本上大同小异,我没有成功跑起来,里面的逻辑也感觉比较奇怪。 13 | 14 | 本文主要介绍,如何**基于 RubyMine** 这个 IDE,创建一个**隔离的环境**,来调试 Cocoapods 和相关的插件。 15 | 16 | ## 背景知识介绍 17 | 18 | 在折腾源码调试之前,我对 Ruby 的工具链并不熟悉(现在也是一只半解)。但是这部分知识的确实,会比较影响后续排查问题,以及对整体流程的理解。所以有必要提前解释下。 19 | 20 | 我会尽量从基础到实际,用简单的语言来描述下相关工具的作用。不少概念参考自御姐的文章:[为什么我们要使用 RVM / Bundler ?](https://juejin.im/post/5c1fb3696fb9a049af6d4132) 21 | 22 | ### RVM 23 | 24 | rvm 的全程是 `ruby version manager`,用来在命令行下,提供多 Ruby 版本的管理和切换能力。它不仅可以管理多个 ruby 版本, 也可以用来替换系统自带的 Ruby。那个 Ruby 配置有很多问题,比如最典型的, 25 | `gem install` 安装依赖报错,没有权限。 26 | 27 | 安装: 28 | 29 | ```bash 30 | # 可能需要用 gpg 命令先进行验证,根据命令行提示来操作一般都没问题 31 | curl -sSL https://get.rvm.io | bash 32 | ``` 33 | 34 | 使用 `2.6.2` 版本的 ruby 35 | 36 | ```bash 37 | rvm install '2.6.2' 38 | rvm use 2.6.2 39 | ``` 40 | 41 | ### gem 42 | 43 | 这是 Ruby 的包管理工具,类似于 iOS `cocoapods`,JS 的 `npm`,macOS 的 `homebrew` 44 | 45 | 当我们使用 rvm 来管理 ruby 版本后,和 `ruby` 命令一样,`gem` 命令也不再是系统自带的,而是由 `rvm` 统一提供了。 46 | 47 | 以通过 rvm 安装的 ruby 2.6.2 为例,这个可执行的 `gem` 二进制,位于目录 `$HOME/.rvm/rubies/ruby-2.6.2/bin/gem` 下 48 | 49 | 可以通过 `gem env` 命令来查看更详细的配置: 50 | 51 | ![gem env 详情](../pictures/cocoapods-debug/gem-env.png) 52 | 53 | 如果需要安装 Ruby 的包,可以通过下面命令: 54 | 55 | ```bash 56 | gem install bundler 57 | ``` 58 | 59 | 大部分 gem 包的安装路径都在上图中的第二个路径,即 `$HOME/.rvm/rubies/ruby-2.6.2/lib/ruby/gems/2.6.0` 下。也有少数被安装在第一个路径 `$HEOME/.rvm/gems/ruby-2.6.2` 下。不过都不影响使用(有了解的同学可以指教一下这俩路径的区别)。 60 | 61 | ### bundler 62 | 63 | 这是一个特殊的 gem 包,安装后会得到一个叫做 `bundle` 的可执行文件。主要是用来管理所有 `gem` 包的版本的。 64 | 65 | 举个例子,如果没有 `bundler` 这个包,每个项目都需要手动执行多个 `gem install` 命令。由于语义化版本号的范围依赖问题,不同时候执行 `gem install` 命令得到的结果可能是不一样的(比如开发者又发布了更新的版本,默认就用上了)。 66 | 67 | `bundler` 的解决方案是,它定义了一个 `Gemfile` 文件,用来描述一个项目所有的 `gem` 包依赖情况。 68 | 69 | 然后通过 `bundle install` 命令一件安装,并且生成 `Gemfile.lock` 文件来锁定版本号。由于本文的读者至少是对 Cocoapods 有一定了解的开发者,这个文件和机制的作用,就不需要再解释了。 70 | 71 | `bundler` 还提供了 `bundle exec` 命令,来确保依赖的准确性。 72 | 73 | 上文介绍了,默认情况下 `gem install` 安装的 `gem` 包,都会存储到一个全局统一的路径下。如果我们安装了同一个 `gem` 包的不同版本, `bundle exec` 命令就可以根据 Gemfile 中的依赖,自动选择正确版本的 `gem` 包了。 74 | 75 | > 对于新人来说,bundler 这个库确实比较怪,一句话总结就是:安装了 `bundler` 这个包后,可以用 `bundle install` 命令来读取 `Gemfile` 文件中定义的 `gem` 包依赖,自动批量安装,并且生成 `Gemfile.lock` 文件锁定版本号 76 | 77 | ## 源码调试 78 | 79 | ### 原理 80 | 81 | 经过一番摸(cai)索(keng),我整理了一下使用 RubyMine 调试的原理。由于都是经验性的结论,就不上分析过程了。 82 | 83 | 要想调试 `pod install` 等命令的流程,我们至少需要两个目录: 84 | 85 | - 源码目录:这里面要存放 cocoapods 的源码,如果要调试插件,插件的源码也要在这里。 86 | - iOS Demo 工程目录:这里面要有 Demo 工程和用于测试的 Podfile,并且使用上面源码版本的 Pod 87 | 88 | 对于 RubyMine 这个 IDE 来说,它的调试依赖于 `ruby-debug-ide` 这个 IDE,并且需要进入到 Demo 工程目录去执行源码版本的 Pod。 89 | 90 | 理解这个原理非常重要,因为它看起来会带来一些矛盾: 91 | 92 | - 我们日常需要基于源码目录进行开发, 查看函数的定义,写代码等等。Gemfile 也需要在这个工程下配置,这样才能源码依赖和调试插件。 93 | - Demo 工程理论上应该和源码目录完全解耦,因为一份源码工程完全可以对应多个 Demo 工程,不能叫接收两者耦合 94 | - 实际的 `Pod Install` 是发生在 Demo 工程目录下的,需要在这里引用到 Cocoapods 源码,并且正确的配置相关依赖 95 | 96 | 上述三个需求是预期达到的目的,再基于之前的原理分析,初步可以形成一个思路:“主体配置放在源码目录中进行日常开发和管理,Demo 工程中通过软连接的方式获取到 Gemfile 配置”。 97 | 98 | 下面分别介绍 Cocoapods 源码目录和 Demo 工程目录的配置。 99 | 100 | ### Cocoapods 源码目录配置 101 | 102 | 首先创建一个空的目录,然后把我们要调试的代码下载下来: 103 | 104 | ```bash 105 | git clone https://github.com/CocoaPods/CocoaPods.git cocoapods 106 | git clone https://github.com/CocoaPods/Core.git cocoapods-core 107 | git clone https://github.com/leavez/cocoapods-binary.git 108 | ``` 109 | 110 | 这里把 cocoapods-core 模块也以源码的方式引入,原因有两个: 111 | 112 | 1. cocoapods 工程是由若干个子模块共同组合的,尝试一下 cocoapods-core 这个子模块的调试体验 113 | 2. 目前(2020.7) 最新的 cocoapods 仓库中,依赖于 cocoapods-core 模块提供的 `post_integrate` 钩子函数。但是 这个函数的实现,还在 cocoapods-core 的 master 分支上,并没有跟随最新的版本(1.9.3) 发布。而 cocoapods 默认是通过版本号来依赖 cocoapods-core 模块的,所以需要手动改为源码依赖。 114 | 115 | 然后创建一个 Gemfile,内容如下: 116 | 117 | ```ruby 118 | dir = File.dirname(Pathname.new(__FILE__).realpath) 119 | 120 | gem 'cocoapods', path: "#{dir}/cocoapods" 121 | gem 'cocoapods-core', path: "#{dir}/cocoapods-core" 122 | gem 'cocoapods-binary', path: "#{dir}/cocoapods-binary" 123 | 124 | group :debug do 125 | gem 'debase' 126 | gem 'ruby-debug-ide' 127 | end 128 | ``` 129 | 130 | 这里 `dir` 变量的定义稍后会解释,除此以外没有太多需要解释的地方,三个需要调试的库切换为源码,附带 `debase` 和 `ruby-debug-ide` 这俩 gem 包用于调试。 131 | 132 | 最后执行命令: 133 | 134 | ```bash 135 | bundle config --local path $PWD"/vendor/bundle" 136 | ``` 137 | 138 | 这行命令会在工程中创建一个 `.bundle/config` 文件,内容如下: 139 | 140 | ```ruby 141 | --- 142 | BUNDLE_PATH: "当前目录/vendor/bundle" 143 | ``` 144 | 145 | 表示把相关的 gem 包依赖,都安装到当前目录下的 `vendor/bundle` 目录下。虽然 `bundle exec` 已经提供了包管理工具不同版本之间的隔离,但是个人总是感觉,把所有的依赖都放在一个固定的地方统一管理,会更加优雅一些。 146 | 147 | ### Demo 工程配置 148 | 149 | 创建一个普通的 iOS 工程,或者任何已有工程也行。 150 | 151 | 最核心的一步是,把源码工程的 `.bundle` 配置目录,和 `Gemfile` 文件通过软链接的方式引入: 152 | 153 | ```bash 154 | cocoapods_debug_path="这里写源码工程的目录" 155 | ln -s $cocoapods_debug_path/.bundle . 156 | ln -s $cocoapods_debug_path/Gemfile . 157 | ``` 158 | 159 | 这样的效果是,在这个 Demo 工程目录下执行 `bundle install` 的效果,和在 cocoapods 源码目录下执行的效果是完全一致的。 160 | 161 | 回到上一小节的 `Gemfile` 配置,现在应该可以理解用 `dir` 变量的原因了。因为这个 Gemfile 不止在源码目录会被用到,为了确保即使以软链接的形式在其它目录被引用时,也能正确的找到依赖,所以不能简单的用 `./cocoapods` 这种写法,而是需要获取到文件真实所在的目录。 162 | 163 | 这样配置的好处是,对于任何已有的工程,只要软连接两个文件,就可以使用本地的 `cocoapods` 源码进行调试了。 164 | 165 | ### RubyMine 工程配置 166 | 167 | 准备完代码后,还需要在 IDE 中配置下工程,如下图所示: 168 | 169 | ![RubyMine Configuration 配置](../pictures/cocoapods-debug/rubymine-config-main.png) 170 | 171 | 有几个点注意下: 172 | 173 | 1. 这里的类型,选择最普通的 Ruby 工程就行了,不需要什么额外的配置 174 | 2. 这里填写 cocoapods 源码中的 `bin/pod` 这个可执行脚本,也就是 `pod install` 的入口 175 | 3. 参数和上面的 script 是对应的。如果要执行的命令是 `pod install`,那么 `pod` 之后的部分都算是 script。 176 | 4. 工作目录,这里要填写 Demo 工程的目录,因为 `pod install` 实际上是发生在 Demo 工程中,而不是源码目录下。 177 | 5. Ruby SDK,选择 rvm 提供的 ruby 版本即可,不要选 global。 178 | 179 | 最后不要忘了勾选使用 `bundle`,否则无法正确识别依赖 180 | 181 | ![使用 bundle exec](../pictures/cocoapods-debug/rubymine-config-bundle.png) 182 | 183 | ## 效果展示 184 | 185 | 只要配置了本地依赖的模块,都可以被正确的调试。 186 | 187 | ### cocoapods 源码 188 | 189 | ![cocoapods 主工程调试](../pictures/cocoapods-debug/rubymine-debug-main.png) 190 | 191 | ### cocoapods-core 插件 192 | 193 | ![cocoapods-core 模块调试](../pictures/cocoapods-debug/rubymine-debug-cocoapods-core.png) 194 | 195 | ### cocoapods-binary 插件 196 | 197 | 首先需要简单改造下 Podfile,引用这个插件: 198 | 199 | ```ruby 200 | platform :ios, '9.0' 201 | 202 | plugin 'cocoapods-binary' 203 | 204 | use_frameworks! 205 | 206 | target 'DemoProject' do 207 | pod 'AFNetworking', '~> 4.0', :binary => true 208 | end 209 | ``` 210 | 211 | 由于我们的 Gemfile 中已经配置了 cocoapods-binary 是通过源码依赖的,所以 Podfile 里无需其他的配置,只要声明对 `cocoapods-binary` 的依赖,就可以走到断点了 212 | 213 | ![cocoapods 插件调试](../pictures/cocoapods-debug/rubymine-debug-cocoapods-binary.png) 214 | 215 | 得益于 RubyMine 提供的支持,整个调试和源码阅读体验,还是比较理想的。现在可以随意把玩 Cocoapods 了。Enjoy yourself~ 216 | -------------------------------------------------------------------------------- /articles/design-pattern-factory.md: -------------------------------------------------------------------------------- 1 | > 仅是一家之言,欢迎交流讨论、指正错误。 2 | 3 | 本科有一门课程是设计模式,上课的时候读完了《Head First 设计模式》,这是一本很好的书,可惜当时的我不是一个好读者,囫囵吞枣看了几百页却没有吸收精华。工作以后,有了一些代码量的积累,打算补一补设计模式相关的内容。 4 | 5 | 这篇文章讲工厂模式,在开始分析之前,我想谈谈我对设计模式的看法。以前我之所以学不好设计模式,一方面是自己代码量不足,只能纸上谈兵,另一方面我想也和介绍设计模式的文章有关。大多数文章重点在于介绍 xxx 模式是什么,然后配上千篇一律的类图(Class Diagram) 和 Demo。好一点的文章,类图和 Demo 容易理解点,有可能还会谈谈某两个模式之间的异同。 6 | 7 | 但设计模式是什么?是一门必须掌握的课程,一些必须背下来的概念,然后放到实际工程里面套用的么?我不敢苟同,在我看来涉及模式其实描述了一些 Coding 的技巧。所谓的技巧,有的是能够节省冗余代码,更重要的则是开闭原则,也就是“**对拓展开放,对修改关闭**”,或者说得再直白点,就是方便开发者后期维护的。 8 | 9 | 这些技巧是有限的、反复出现的,为了便于交流和沟通,我们给这些技巧起上名字,否则每个人对这些技巧都有自己的理解,就不方便沟通了。既然是技巧,那么一定有它**“巧”**的一面,对比的则是原来不巧的代码。所以理解某个设计模式的实现是次要的,重点是理解它巧在哪里,因为要理解巧在哪里,所以顺便要看看如何实现。 10 | 11 | # 工厂模式 12 | 13 | 上面这些话可能有点虚、有点绕,没关系,我举个具体例子来说。这篇文章介绍的是工厂模式,工厂模式根据“教科书”,分为三种: 14 | 15 | 1. 简单工厂模式 16 | 2. 工厂方法模式 17 | 3. 抽象工厂模式 18 | 19 | 既然是工厂,那么肯定是用来生产东西的,所以有的教材或者书籍把它归类于“创建型模式”,我是坚决反对的。还有很多文章,动不动就是工厂、材料的举例,如果你以为只有创建东西,还需要材料时才用得着工厂模式,那么设计模式这门学问基本上就算失败了。 20 | 21 | ## 简单工厂模式 22 | 23 | 以那个经典的披萨的例子来说吧,父类叫 `Pizza`,子类有很多,什么 `GoodPizza`、`BadPizza`、`LargePizza`、`SmallPizza` 之类的随便写。 24 | 25 | 问题来了,这么多可能的披萨,怎么选择呢?当然是用参数来标记了。比如订披萨的时候: 26 | 27 | ```java 28 | // Snippet 1 29 | public Pizza orderPizza(String type){ 30 | Pizza pizza = null; 31 | if(type.equals("g")){ 32 | pizza = new GoodPizza(); 33 | }else if(type.equals("b")){ 34 | pizza = new BadPizza(); 35 | }else if(type.equals("l")){ 36 | pizza = new LargePizza(); 37 | }else { 38 | pizza = new SmallPizza(); 39 | } 40 | return pizza; 41 | } 42 | ``` 43 | 44 | 第一段代码是原始场景,它只是实现了需求,我给它起个名字叫学校代码(School Code),也就是那些学校里的学生写出来的,仅仅是可以运行的代码。问题很明显,一方面你不能保证只有在订披萨的时候才会创建披萨的实例对象,如果别的地方也要创建披萨对象,相同的代码就要重写一遍。另一方面这样写会导致 `orderPizza` 所在的类依赖于 `Pizza` 的四个子类,而实际上的需求仅仅是创建披萨实例而已,它的调用者并不应该依赖于具体的子类。 45 | 46 | 因此在订披萨的地方处理这样的字符串判断就显得不合理,解决方案也很简单,用一个专门的类来处理创建披萨的逻辑就行了: 47 | 48 | ```java 49 | //Snippet 2 50 | public class SimplePizzaFactory { 51 | public Pizza createPizza(String type) { 52 | Pizza pizza = null; 53 | if(type.equals("g")){ 54 | pizza = new GoodPizza(); 55 | }else if(type.equals("b")){ 56 | pizza = new BadPizza(); 57 | }else if(type.equals("l")){ 58 | pizza = new LargePizza(); 59 | }else { 60 | pizza = new SmallPizza(); 61 | } 62 | return pizza; 63 | } 64 | } 65 | 66 | public Pizza orderPizza(String type) { 67 | SimplePizzaFactory simplePizzaFactory = new SimplePizzaFactory(); 68 | Pizza pizza= simplePizzaFactory.createPizza(type); 69 | } 70 | ``` 71 | 72 | 这就是简单工厂方法了,它说的是两个概念: 73 | 74 | 1. 一个类只做和自己相关的事,不依赖的别瞎依赖 75 | 2. 有可能复用的代码抽出去,独立成类,不要到处重复 76 | 77 | 这个模式和什么所谓的 **工厂** 一点关系都没有。假如这里调用的不是 `new` 关键字来新建对象,而是用类的静态方法,一样会有依赖问题。这种大段的细节逻辑,不管是不是创建对象,还是可以被独立出去。 78 | 79 | 采用了简单工厂模式以后,n 个披萨需要 n+1 个类,额外的那个是工厂类,处理创建披萨对象的具体逻辑。 80 | 81 | ## 工厂方法模式 82 | 83 | 我们考虑一下新增披萨种类的情况。在第二段代码的实现中,如果要新增一个子类,需要在 `SimplePizzaFactory` 中新增对子类的依赖和解析方法,也就是多一个 `else if` 的分支。如果是删除一种披萨,就需要删掉一个子类的依赖和 `else if` 分支。 84 | 85 | 这个操作看起来并不复杂,虽然要增改代码,但还是可以接受。不过如果子类无法修改 `SimplePizzaFactory` 代码呢?父类和工厂类有可能是基础团队在维护,而披萨子类可能是业务团队维护,不同的业务团队还有可能维护不同的子类。且不说不一定有代码的写权限,就算大家一起写,代码冲突了怎么办,忘记删除逻辑了怎么办? 86 | 87 | 这时候就看出问题所在了,它违反了开闭原则,也就是说并没有做到对修改关闭。怎么对修改关闭呢,这就需要借助 OOP 编程时的一个小技巧。 88 | 89 | 直接上代码吧,为了偷懒,我直接把 [深入浅出工厂设计模式](https://segmentfault.com/p/1210000009074890/read) 这篇文章的里的代码搬过来改改了: 90 | 91 | ```java 92 | //snippet 3 93 | public abstract class APizzaStore { 94 | public Pizza orderPizza(String type) { 95 | Pizza pizza= createPizza(type); 96 | return pizza; 97 | } 98 | abstract Pizza createPizza(String type); 99 | } 100 | 101 | public class GoodPizzaStore extends APizzaStore{ 102 | @Override 103 | Pizza createPizza(String type) { 104 | Pizza pizza = new GoodPizza(); 105 | return pizza; 106 | } 107 | } 108 | ``` 109 | 110 | 采用工厂方法模式以后,工厂不再负责具体的业务细节。它变成了一个抽象类,规定了一个**抽象方法** `createPizza` **强迫** 子类实现。同时它在 `orderPizza` 函数中调用了这个方法,但方法的实现者并不是自己。因为实际使用的并不是抽象工厂类 `APizzaStore` 而是具体的 `GoodPizzaStore`,利用**多态性**,实际调用的也是子类的 `createPizza` 方法。 111 | 112 | 所以归根结底,抽象工厂方法只是使用了一个小技巧,我称之为: 113 | 114 | > 父类定框架,子类做填充,依赖多态性 115 | 116 | 这种技巧的好处在于将具体实现下降到各个子类中实现,父类仅仅指定这些方法何时被调用,从而不再关心有多少子类,实现了“对拓展开放、对修改关闭”。当然你也会发现方法的调用者不能再偷懒,传递字符串就能拿到合适的披萨类型了,现在 117 | 118 | 当然,使用工厂方法模式也有代价,对于 n 个披萨类型,现在我们需要 2n+1 个类了。其中 n 个 `Pizza` 的子类,1 个 `APizzaStore` 的抽象类负责制定流程框架,n 个 `APizzaStore` 的子类负责实现细节。 119 | 120 | 以还是那句话,工厂方法模式和 **工厂** 半毛钱关系都没有。它只是一种 OOP 下的编程技巧,在任何场景下都有可能使用。但这个技巧和语言有点关系,比如这里的例子是 Java 语言。我们知道 Java 可以把一个类标记为 `abstract`,虚拟类有一个虚拟方法。子类如果想变成具体类就必须实现这个虚拟方法。但是在 OC 中并没有虚拟类的概念,父类的空方法子类完全可以不实现,那么就无法在编译时作出这些规定,只能靠文档和 Code Review 来督促(父类方法抛错是运行时)。 121 | 122 | ## 抽象工厂模式 123 | 124 | 最后聊聊抽象工厂模式,依我愚见,抽象工厂模式和 **工厂** 更没关系。 125 | 126 | 在某些极端情况下,披萨的种类可能会特别多,但并不是毫无规律的多。可能会出现可以归类的情况。比如我们考虑两个维度,一个是披萨的产地,可以是中国、美国、印度、日本等等,另一个是披萨的口味,它的数量有限,只有麻辣、微辣和不辣三种: 127 | 128 | ![](http://images.bestswifter.com/1492518813.png) 129 | 130 | 这里画了个很简单图表,一共有 15 种披萨。我们可以用中国不辣、日本不辣、印度不辣来描述三种披萨,也可以先建立三个披萨工场,分别用来生产不辣、微辣、麻辣的披萨,然后用不辣工厂的中国披萨、日本披萨、印度披萨来描述上述三种披萨。这有点类似于数学里面提取公因数的概念。 131 | 132 | 假设我们建立了三个工厂,分别生产不辣、微辣、麻辣的披萨,以微辣工厂为例: 133 | 134 | ```java 135 | //snippet 4 136 | abstract class APizzaFactory{ 137 | public abstract ChinesePizza createChinesePizza(); 138 | public abstract JapanesePizza createJapanesePizza(); 139 | public abstract AmericanPizza createAmericanPizza(); 140 | // ... 141 | } 142 | 143 | class HotFactory extends APizzaFactory{ 144 | public abstract ChinesePizza createChinesePizza() { 145 | return new HotChinesePizza(); 146 | } 147 | public abstract JapanesePizza createJapanesePizza() { 148 | return new HotJapanesePizza(); 149 | } 150 | public abstract AmericanPizza createAmericanPizza() { 151 | return new HotAmericanPizza(); 152 | } 153 | } 154 | 155 | HotFactory hotFactory = new HotFactory(); 156 | ChinesePizza pizza = hotFactory.createChinesePizza(); 157 | ``` 158 | 159 | 这样我们在创建披萨的时候就不用了解 15 个具体子类了,只要了解三种工厂和五个种类。换句话说我们把一个很长的一维数组(1 x 15)转化了二维数组,每个维度的长度都不大(3 x 5)。我们还可以换一个思路,比如建立五个工厂,分别表示不同国家,然后每个工厂可以生产三种不同口味的披萨。 160 | 161 | 那么到底是以口味为标准建立工厂,还是以国家为标准呢?我的建议是尽量减少新增工厂的可能性。比如上图中可以看到,新增口味的成本是添加五个国家披萨在这个新口味下的实现,而新增国家的成本仅仅是在已有的三个工厂中各增加一个方法。可见新增工厂(口味)比新增产品(国家)更麻烦一些。所以在上述例子中,个人建议针对不同的口味建立工厂,在实际项目中作出正确的选择应该也不会太困难。 162 | 163 | 考虑到披萨子类过多,而且大部分可以分类,在实际项目中为了节省代码量,我们还可以用反射的方式来动态获取类并生成实例。 164 | 165 | 总之,抽象工厂模式名字很玄乎,但是概念也很简单,就是用二维数组的思想来简化数量多、但可以分类的数据。 166 | 167 | # 总结 168 | 169 | 这是学习设计模式的第一篇文章,从比较简单的工厂方法开始讲起。重点是忽略设计模式的表象,挖掘背后的原理。比如工厂模式就和工厂、创建对象没啥关系: 170 | 171 | 1. 简单工厂模式: 具体逻辑由具体类处理,减少不必要依赖,方便代码复用 172 | 2. 工厂方法模式: 父类定框架,子类做实现,利用多态的特点实现对拓展开放,对修改关闭 173 | 3. 抽象工厂模式: 借用二维数组的概念对复杂的子类做分类,简化业务逻辑。 174 | 175 | # 参考文档 176 | 177 | 本文写作过程中参考了以下两篇文章,但他们并不权威: 178 | 179 | 1. [深入浅出工厂设计模式](https://segmentfault.com/p/1210000009074890/read) 180 | 2. [简单工厂、工厂方法、抽象工厂、策略模式、策略与工厂的区别](http://lh-kevin.iteye.com/blog/1981574) 181 | -------------------------------------------------------------------------------- /articles/efficient-mac.md: -------------------------------------------------------------------------------- 1 | > 本文是视频直播的文字整理,录像可以在:[优酷](http://v.youku.com/v_show/id_XMTU4Nzg3NjAwMA==.html) 上看到 2 | 3 | 关于 Mac 工作效率的文章一直层出不穷,然而并非所有内容都适合程序员,比如某些 Unix 命令,其实使用频率非常低。作为一名初级 iOS 程序员,我尝试着和大家分享一些能够切实提高我们开发效率的小技巧。 4 | 5 | 我是无鼠标主义者,任何需要鼠标的操作在我看来都是极为低效的。Mac 的触摸板非常好用,但是我依然在尝试避免使用触摸板。因为双手保持在键盘区域更适合编程。虽然触摸板不可能被避免(比如浏览网页),但我希望至少在 Xcode 中不使用它。 6 | 7 | 所以,本文会和大家分享一些系统级快捷键,Xcode、Chrome、iTerm 等应用中的快捷键,以及常用的工具,比如 Vim 和 Git 的使用。这里面除了 Xcode,其他都是通用的,如果你不是 iOS 开发者,建议自行查阅相关 IDE 的快捷键。 8 | 9 | ## 综述 10 | 11 | 一部分人可能认为,快捷键用起来很别扭,还不如自己用触摸板(鼠标)来得方便。然而你应该意识到,使用触摸板的效率是有上限的,当你熟悉快捷键后,速度远比现在快得多。 12 | 13 | 这一点,在学习 vim 时尤其重要。你不应该关注完成一个命令需要多久,而应该关注需要多少个按键,你可以认为在形成肌肉记忆后,按键的思考时间为0。所以我们得出一个结论: 14 | 15 | 总时间 = 按键数 * 一个常数(表示单次按键时间)。 16 | 17 | 因此,评价 vim 中一个操作的优劣,通常用**高尔夫分数**来表示,它表示完整这个操作需要几次按键。 18 | 19 | **但是!!!快捷键是提高效率的手段,但它不会提高代码质量。既要坚持学习,也要适可而止,万万不可主次颠倒。** 20 | 21 | 关键不在于你学会了多少快捷键,而是你有多少工作是可以通过快捷键来完成的,目的在于提高效率,仅此而已。 22 | 23 | 一种很强大,通用的的方法是 设置->键盘->快捷键->应用快捷键 然后精确匹配应用中的快捷键名,这个通常需要配合 CheatSheet 来实现。当你觉得某个快捷键不好用的时候,也可以通过这种方式去修改。 24 | 25 | ![应用内快捷键替换](http://images.bestswifter.com/shortcut/1@2x.png) 26 | 27 | 在设置快捷键时,需要避免全局快捷键和应用快捷键冲突,同时也要注意一些常用操作在多个应用内保持统一。 28 | 29 | 我建议将 Caps Lock 与 Ctrl 键对调,因为大小写切换键的使用频率非常低,而 Ctrl 的使用频率显然高于他,因此有必要将大小写切换键放到最不容易触碰到的地方。 30 | 31 | 下面我会介绍一些我常用的快捷键,它们大部分是系统自带的,也有少部分是我自己定义的。 32 | 33 | ## 入门 34 | 35 | 1. 绝大多数应用的 preference 页面都是通过 `Command + ,` 打开的。 36 | 2. 剪切,复制,粘贴,撤销,重做,光标移动到行首和行尾,这些基础操作必须掌握。 37 | 38 | ## Snap 39 | 40 | 相信很多人都有这样的烦恼:如果应用不全屏,那么桌面上显示的窗口太多,每个窗口的显示内容不够多。如果应用全屏,那么切换应用是很麻烦的。要么用 `Command + Tab`,要么手势滑动,但无论哪一种,时间复杂度都是 O(n)。有没有 O(1) 的方法呢?答案是使用神器:**snap** 41 | 42 | 我主要是以应用首字母或者关键字母作为标识,配合 `Command + Shift` 前缀: 43 | 44 | 1. **Xcode:`J`** 45 | 2. **Chrome:`K`** 46 | 3. **iTerm:`L`** 47 | 4. Markdown 相关:`M` 48 | 5. QQ:`Y` 49 | 6. 微信:`U` 50 | 7. SoureceTree:`S` 51 | 8. MacVim:`V` 52 | 9. Evernote:`E` 53 | 10. **Dock:`1/2/3/4`:因工作需要,我常用的是备忘录,邮件,日历,设置** 54 | 11. `;` 这个键我没有启用,但它实际上是一个非常方便的快捷键。 55 | 56 | Dock 栏应用的选择需要一定的权衡。显然最快的方式是只按 `Command`,但是这种全局快捷键会导致大量冲突。而 `Controll` 和 `Option` 键又非常难以触摸,所以我选择了 `Command + Shift` 作为所有应用的快捷键前缀。 57 | 58 | **注意避免字母 `o` 和 `f`,它们在 Xcode 中有特殊的用处。** 59 | 60 | ![Snap](http://images.bestswifter.com/shortcut/2.png) 61 | 62 | ## Xcode 快捷键 63 | 64 | 编译、运行,Instruments,单元测试,暂停这些基本操作就不解释了。我把一些自认为比较有用的命令加粗表示: 65 | 66 | ### 文件编辑 67 | 68 | 1. `Command + [` 和 `Command + ]` 左右缩进 69 | 2. **`Command + Option + [` 和 `Command + Option + ]` 当前行上下移动** 70 | 3. `Command + Option + Left/Right` 折叠、展开当前代码段 71 | 72 | ### 文件跳转 73 | 74 | 1. **`Command + Control + Up/Down` .h 和 .m 文件切换** 75 | 2. **`Command + Control + Left/Right` 浏览历史切换** 76 | 3. `Command + Control + j` 跳转到定义处 77 | 3. `Command + Option + j` 跳转到目录搜索 78 | 4. `Command + 1/2/3/4/5` 跳转到左侧不同的栏目 79 | 5. **`Comannd + Shift + o` 文件搜索** 80 | 81 | ### 搜索 82 | 83 | 1. `Comannd + Shift + f` 全局搜索 84 | 2. `Command + e` 搜索当前选中单词 85 | 3. `Command + g` 搜索下一个 86 | 87 | ### tab 88 | 89 | 1. `Command + t` 新建一个 tab 90 | 2. `Command + w` 关闭当前 tab 91 | 3. **`Command + Shift + [` 和 `Command + Shift + ]` 左右切换 tab** 92 | 93 | ### Scheme 94 | 95 | 1. `Command + shift + ,` 编辑 scheme,选择 debug 或 release 96 | 97 | ### 调试 98 | 99 | `F6`:跳到下一条指令 100 | `F7`:跳进下一条指令(它会跳进内部函数,具体效果自测) 101 | `Control + Command + y` 继续运行 102 | 103 | ### 其他 104 | 105 | 1. `Command + k` 删除 Console 中的内容 106 | 2. `Command + d` 打开/关闭 控制台(修改系统快捷键:Show/Hide Debug Area) 107 | 108 | 获得更全面的快捷键介绍,请参考:[这篇文章](http://stackoverflow.com/questions/10296138/xcode-debug-shortcuts) 109 | 110 | ## Vim 常用快捷键 111 | 112 | 入门指南:[简明 Vim 练级攻略](http://coolshell.cn/articles/5426.html) 113 | 在我的 git 上有一份 Vim 的配置,先[下载](https://github.com/bestswifter/.vim)到 `~/` 目录下,然后建立软连接: 114 | 115 | ```bash 116 | rm .vimrc 117 | ln -s .vim/.vimrc .vimrc 118 | ``` 119 | 120 | 推荐一个 Mac 上的 Vim 软件:MacVim,它比在终端中看 Vim 更好一些。打开 MacVim 后,输入以下命令安装插件: 121 | 122 | ```vim 123 | :BundleInstall 124 | ``` 125 | 126 | ### 进入输入模式 127 | 128 | 1. `i` 在光标前面进入输入模式,`a` 在光标后面进入输入模式 129 | 2. `I` 在行首进入输入模式,`A` 在行尾进入输入模式 130 | 3. `o` 在下一行行首进入输入模式,`O` 在上一行行首进入输入模式 131 | 132 | ### 文本操作 133 | 134 | 1. `yy` 复制当前行,`dd` 剪切当前行,`p` 复制。注意这里用的都是 Vim 自带的剪贴板。 135 | 2. `U` 撤销,**`Ctrl + r` 重做 136 | 3. `x` 删除光标所在的字母 137 | 4. `cae` 或 `bce` 删除当前光标所在的单词,并进入编辑模式 138 | 5. `数字+命令` 重复命令 n 次,比如 `3dd` 139 | 140 | ### 光标移动 141 | 142 | 1. `^` 到本行开头,`$` 到本行末尾 143 | 2. `:111` 或 `111G` 跳转到 111 行,`gg` 第一行,`G` 最后一行。 144 | 3. `e` 移动到本单词的结尾, `w` 移动到下一个单词的开头。 145 | 4. `%` 匹配当前光标所在的括号(小括号,中括号,大括号) 146 | 5. `*` 查找与光标所在单词相同的下一个单词 147 | 6. `f + 字母` 跳转到字母第一次出现的位置,`2fb` 跳转到字母 b 第二次出现的位置 148 | 7. `t + 字母` 跳转到字母第一次出现的前一个位置,`3ta` 跳转到字母 a 第三次出现的前一个位置 149 | 8. f 和 t 换成大写,表示反方向移动查找。`dt + 字母` 表示删除字母前的所有内容。 150 | 151 | ### 举一反三 152 | 153 | 1. `` 比如 `0y$`,从行首复制到行尾,`ye` 表示 154 | 从当前位置复制到本单词结尾。 155 | 2. `a` 或 `i` 156 | 157 | `action` 可以是任何的命令,比如 `d`,`y`,`v` 等 158 | `object` 可以是 `w` 单词,`p` 段落,或者是一个具体的字母 159 | `a` 和 `i` 的区别在于 `i` 表示 inner,只作用于内部,不含两端。 160 | 161 | 思考一下,有多少种方法可以删除光标当前所在单词? 162 | 163 | 答案:`diw`,`daw`,`caw`,`ciw`,`bce`,`bde`。 164 | 165 | 思考一下他们的原理,后两者不太推荐(有可能跳到前一个单词)。 166 | 167 | 如果是选中当前单词呢? 168 | 169 | 除了以上基本语法,我还在整理一套 《Vim 基础练习题》,等完成之后会与大家分享。 170 | 171 | ### 实战 172 | 173 | 1. 给多行添加注释: 174 | 175 | 1. `v`:进入可视状态 176 | 2. `nj`: 向下选择n行, 或者输入 `Shift ]` 跳到段尾 177 | 3. `Command + /` 添加注释 178 | 179 | 2. 在 MacVim 中,`git blame` 无比清晰: 180 | 181 | ![Snap](http://images.bestswifter.com/shortcut/3.png) 182 | 183 | ## Chrome 184 | 185 | 1. **`Command + l` 焦点移动到地址栏** 186 | 2. **`Shift + Option + Delete/Left`** 向左删除/选中一个单词(可以自定义为 `Ctrl-w`) 187 | 3. `Command + y` 搜索历史 188 | 4. **`Command + 数字` 快速切换 tab** 189 | 5. `Command + shift + []` 左右切换 tab 190 | 6. `Command + t/w` 新建/关闭 tab 191 | 7. `Command + e/g` 搜索选中,前往下一个,或者用 `Command + f` 和回车。 192 | 193 | 可以看到,Chrome 中涉及到 tab 的操作应该与 Xcode 尽量保持一致。 194 | 195 | ## iTerm2 196 | 197 | 1. **`Ctrl w` 删除前一个单词** 198 | 2. `Command + r` 清除屏幕上的内容 199 | 3. `Command + t/w` 打开/关闭 tab 200 | 4. **`Command + 数字` 切换到第 n 个 tab** 201 | 5. `双击` 选中一个单词,自动复制 202 | 203 | iTerm 可以通过 `Command + shift + []` 来左右切换 tab,也可以通过 `Command + Left/Right` 切换,后者其实是多余的,而且不符合习惯。 204 | 205 | 所以参考 [这篇文章](http://www.michael-noll.com/blog/2007/01/04/word-movement-shortcuts-for-iterm-on-mac-os-x/) 或者自行查阅 Google,在 Preference->Keys->Global Shortcut Keys 中,设置好 `Command` 加上左右键,和删除键的对应操作。 206 | 207 | ## Git 208 | 209 | [git](http://gitbook.liuhui998.com/index.html) 的本质是对指针的操作。 210 | 211 | 掌握git的 `add`、`commit`、`stash`、`pull`、`fetch` 这些基本操作 212 | 213 | 理解什么是本地仓库,什么是远程仓库,理解多人开发时的 `merge` 和 `conflict` 的概念 214 | 215 | 掌握分支的使用,掌握 `checkout` 命令的使用 216 | 217 | 熟练掌握 [`git rebase`](http://gitbook.liuhui998.com/4_2.html) 操作,包括 [`git rebase -i`](http://gitbook.liuhui998.com/4_3.html) 和 `git rebase --onto`,掌握一种 git 工作流 218 | 219 | ## Oh my zsh 220 | 221 | 首先[下载 oh-my-zsh 的配置](https://github.com/bestswifter/.sys.config)到 `~/` 目录下,然后在命令行中执行以下操作: 222 | 223 | ```bash 224 | rm .zshrc 225 | ln -s .sys.config/.zshrc .zshrc 226 | ``` 227 | 228 | 然后重启 iTerm。你可以根据自己的喜好,前往 `~/.sys.config/setting/git.zh` 配置 git 命令的别名,比如; 229 | 230 | ```bash 231 | alias gcm='git commit -m' 232 | alias gignore='git update-index --assume-unchanged' 233 | alias gpush='git push origin HEAD:dev;' 234 | alias go='git checkout' 235 | ``` 236 | 237 | ## More 238 | 239 | 据说 Alfred 是效率神器,鉴于我除了写代码,一般不怎么玩 mac,所以也就没有去了解。如果有更多好的快捷键和应用,欢迎与我交流。 -------------------------------------------------------------------------------- /articles/fight-against-anxiety.md: -------------------------------------------------------------------------------- 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 | * A 技术和 B 技术都是新的技术,时间有限,选择哪一个学习 30 | * 现在这家公司给的钱不少,但是做的技术没有前景,害怕被市场淘汰。老板画饼说提名晋升/加薪,但又怕耽误一年时间还没收获,要不要看看外面的机会 31 | * 二线城市房价低、有户口,但是薪资低、选择少,要不要去 32 | * 35 岁危机怎么度过,要不要降薪去稳定的公司或者搞点什么副业 33 | 34 | 可以看出,这些都是 35 | 36 | ### 焦虑是客观存在 37 | 38 | ## 如何化解焦虑 39 | 40 | ### 坚持长期正确的事情 41 | 42 | ### 有效的休息 -------------------------------------------------------------------------------- /articles/fq-2019.md: -------------------------------------------------------------------------------- 1 | ## 背景 2 | 3 | > 本文首发与 2019.9.21,最后更新于 2019.11.2 4 | 5 | 两年之前写过一篇 [爱国上网的教程](./fq.md),介绍了各种基础知识和工具使用,无背景的读者可以看下。其中部分工具在本文得到了更新。 6 | 7 | 如果觉得文章太长,可以直接参考使用我的解决方案后得到的效果,和几个服务的评测。 8 | 9 | ## 我的解决方案 10 | 11 | 我使用华硕路由器的梅林固件,通过在路由器上配置代理,做到对内网所有设备提供透明的科学上网服务。 12 | 13 | > ac88u 虽然看起来数字比 ac86u 高,但其实 CPU 很落后,5G 频道信号也不稳定,因此推荐买 ac86u,双十一价格不到 800. 14 | 15 | ### 路由器配置 16 | 17 | 首先安装科学上网插件。由于插件中心下架,所以需要自行[下载 zip 包](https://github.com/hq450/fancyss)进行离线安装 18 | 19 | 由于我选择下文介绍的 [IPLC 节点](http://n3ro.host/register?ref=8731),配置相当稳定,所以没有使用服务订阅,而是手动导入了一些速度还不错的节点: 20 | 21 | ![](http://images.bestswifter.com/iplc-nodes.jpg) 22 | 23 | 路由器上的科学上网,最重要的就是稳定。但任何节点都不可能永远稳定,好在服务商一次会提供很多节点可供选择,所以我们只要在节点宕机以后进行自动切换即可。我没有采用故障转移的方案,这种方案无法控制备用节点的范围,会导致可能切换到性能很差的节点。 24 | 25 | 我的解决方案是使用 Haproxy 进行自动恢复。Haproxy 是一个类似于 Nginx 的负载均衡服务。配置方式如下,我挑选了一个最稳定高速的节点作为主节点,以及一些性能和稳定性都不错的节点作为备用节点: 26 | 27 | ![](http://images.bestswifter.com/haproxy.png) 28 | 29 | 配置成功后,会自动生成一个 IP 地址为 0.0.0.0,端口 1181 的 SS 节点。好处是,可以一次性订阅很多节点,然后选择其中质量好的做为主、备用节点。 30 | 31 | 在路由器上,我还搭建了一个 socks5 代理: 32 | 33 | ![](http://images.bestswifter.com/merlin-socks5.png) 34 | 35 | 首先需要填写 ss 服务的信息。由于上面我已经通过 haproxy 搭建了具备容灾功能的节点,这里就可以直接用上了,避免出现直接写服务商的节点可能导致的偶尔不可用问题。配置好后,就对外提供一个 14179 端口作为 socks5 代理,这样接入的电脑不再需要安装/启动 GoAgent/Clash 等软件,可以在需要科学上网的地方使用路由器的 socks5 代理,常见的有: 36 | 37 | 1. Chrome 的 SwitchyOmega 插件: 38 | 39 | ![](http://images.bestswifter.com/merlin-Switchyomega.png) 40 | 41 | 2. 终端需要科学上网时,使用 `export ALL_PROXY=socks5://192.168.50.1:14179` 即可。 42 | 43 | 总体来看,服务的结构大概是这样的: 44 | 45 | ![](http://images.bestswifter.com/merlin-topologic.png) 46 | 47 | ### 效果展示 48 | 49 | > 以下效果基于华硕路由器(硬件)、梅林(操作系统)、科学上网插件(软件)和 [N3RO](http://n3ro.host/register?ref=8731) (科学上网服务提供商)实现 50 | 51 | 总结来看,这套方案配置下来,有以下特点: 52 | 53 | 1. 路由器上的配置对接入路由器的所有设备透明,无需任何配置即可科学上网。 54 | 2. 服务稳定,本身采用 IPLC 专线,国庆/大会期间无影响,再通过 haproxy 进行多节点容灾,几乎不会存在服务不可用的情况。 55 | 3. 主要用香港节点,低延迟,高带宽。如果不是刻意观察,很难意识到自己在使用代理。 56 | 4. 自动 + 手动调度流量,路由器上默认根据域名黑名单或者 IP 地址,使用代理服务访问被墙的网站。通过 socks5 代理,支持浏览器上手动细化调整规则。 57 | 58 | 比如以搜索 `Golang` 为例,下面分别是我百度和 Google 的搜索耗时: 59 | 60 | 下面是百度的: 61 | 62 | ![](http://images.bestswifter.com/test_baidu.png) 63 | 64 | 对比谷歌的: 65 | 66 | ![](http://images.bestswifter.com/test_google2.png) 67 | 68 | 可以看到两者差距不到 100ms,基本上无法感知,从而实现了对百度的替换。 69 | 70 | ## 服务评测 71 | 72 | 1. 自己购买 VPS 并搭建服务,有一定技术成本并且容易造成资源浪费。相比之下,直接购买服务,可以以更低的价格获得更高的配置。 73 | 2. 按流量付费是比较合理的策略,可以让用户得到非常好的网络服务。由于国内流量是直接转发,并不会走科学上网,大部分人对流量的需求其实远低于预期,以我自己使用的观察来看,一个月 30G 流量就足够,60G 流量基本上是绰绰有余了。 74 | 75 | 在踩(浪)了(废)好多坑(钱)以后,我总结了几个还不错的服务推荐出来: 76 | 77 | 1. [N3RO](https://n3ro.host/register?ref=8731) 是我购买了众多服务并多方比较以后,最后发现性能和稳定性比较好,价格也比较适中,因此重点推荐一下。 78 | 2. [JustMySocks](https://justmysocks.net/members/aff.php?aff=407) 是搬瓦工推出的服务,使用了 ShadowSocks 服务,解决了 IP 容易被封的问题,且使用 CN2 GIA 专线,速度很快。 79 | 3. [Duang Cloud](https://duangcloud.org/aff.php?aff=99):使用了新的 v2ray 协议,支持节点订阅,技术更加先进,但配置方式几乎不变。价格比第一种方案便宜,但胜在节点多,而且即使在敏感时期也不受任何影响。 80 | 81 | ### N3RO 82 | 83 | [N3RO](http://n3ro.host/register?ref=8731) 主打高速稳定,非常壕无人性的提供了 44 条 IPLC 节点,提供服务国家&地区包括香港、日本、韩国、新加坡、美国、德国、台湾。 84 | 85 | 说到 IPLC,也是我在这次调研过程中新发现的东西。全名叫:国际私人租赁线路(International Private Leased Circuit),最大的特点就是:不经过 GFW,所以稳定高速。唯一的缺点就是:贵! 86 | 87 | 以 “IPLC 京港 01” 这条线路为例演示下:![](http://app.bestswifter.com/iplc.png) 88 | 89 | 可以看到虽然相比于直连服务器,中间经过了一次北京服务器中转,但北京的中转服务器到手机连接速度非常快(我在北京,其他地方可以通过深圳、上海、杭州等地接入),且和香港服务器之间通过专有网络连接,高速稳定且没有 GFW。 90 | 91 | > 90% 以上的 IPLC 通过阿里云内网专线实现,很少有人真的租公网专线,那个价格一般人用不起 92 | 93 | 一般的 IPLC 专线价格在每 GB 0.5~2 元,N3RO 的价格还是很有优势的: 94 | 95 | | 套餐名称 | 每月流量 | 每月价格 | 优惠后年费价格 | 96 | | ------- | ------ | ------- | ------ | 97 | | 小包 | 32G | ¥16 | ¥132 | 98 | | 中包 | 70G | ¥28 | ¥231 | 99 | | 大包 | 120G | ¥40 | ¥312 | 100 | | 大大包 | 320G | 80 | ¥624 | 101 | 102 | > 年费计算价格 = 原价 * 年费优惠(0.9)* 特殊优惠码(0.85) 103 | > 特殊优惠码:海螺钻进垃圾桶 104 | 105 | 对于正常用户来说,购买中包年费套餐,算下来每月不到 20 元。如果对速度和稳定性没有很高的要求,但是需要大量流量,可以看下下面 JustMySocks 的方案。 106 | 107 | ### JustMySocks 108 | 109 | [JustMySocks](https://justmysocks.net/members/aff.php?aff=407) 是搬瓦工官方提供的服务,质量有保障,且提供了 CN2 GIA/GT 专线,同时支持联通、移动、电信三大运营商。 110 | 111 | 我实测从国内访问美国,延迟可以控制在 200ms 内,甚至达到 150ms。 112 | 113 | JustMySocks 最大的创新在于,它会直接向用户提供五个 HTTP 地址,用户不需要关心 IP 被封的问题。 114 | 因为即使 IP 被封,服务商也可以通过 DNS 实时的更新 IP 地址,从而实现对用户透明。 115 | 116 | JustMySocks 有三种套餐,区别如下: 117 | 118 | | 每月流量 | 最大带宽 | 最多同时在线设备数 | 每月价格 | 优惠后年付价格 | 购买地址 | 119 | | ------- | ------ | --------------- | ------- | ----------- | ------ | 120 | | 100G | 1 Gbps | 3 | $2.88 | $27.38 | [购买](http://justmysocks1.net/members/aff.php?aff=407&pid=1) | 121 | | 500G | 2.5 Gbps | 5 | $5.88 | $55.82 | [购买](http://justmysocks1.net/members/aff.php?aff=407&pid=2) | 122 | | 1000G | 2.5 Gbps | 无限制 | $9.88 | $93.74 | [购买](http://justmysocks1.net/members/aff.php?aff=407&pid=3) | 123 | 124 | 和搬瓦工不同的是,由于不用担心封 IP 的问题,我更推荐选择年付,价格是月付的十倍,相当于买 10 个月送两个月,还可以使用优惠码 `JMS9272283` 优惠 5.2%。 125 | 126 | 考虑到应该没有人买最高的配置,且最低配置的性价比非常高,因此每年只要 $27 就可以拥有一个简单、稳定、高速、好用的科学上网服务,还是比较推荐的。 127 | 128 | ### Duang Cloud 129 | 130 | [Duang Cloud](https://duangcloud.org/aff.php?aff=99) 是我最近发现的,基于 v2ray 的科学上网工具。 131 | 132 | 这个工具,其实是一些列协议的汇总,其中有一个叫 VMess 的协议,才是 v2ray 最新独创的,它的诞生目的就是为了对抗 GFW 基于深度学习的 ShadowSocks 流量监测。 133 | 134 | 简单来说就是,这个协议下的数据传输,特征更不明显,更加不容易被识别,因此也就更不容易受到 GFW 的影响。比如 v2ray 支持 [HTTP 伪装](https://tlanyan.me/v2ray-traffic-mask/), 135 | [CDN 中转](https://blog.sprov.xyz/2019/03/11/cdn-v2ray-safe-proxy/) 等各种高级操作,大大增强了安全性。 136 | 137 | 介绍完 v2ray 的优点,我们来看下服务商提供的套餐: 138 | 139 | | 套餐名称 | 每月流量 | 最大带宽 | 每月价格 | 优惠后年费价格 | 购买地址 | 140 | | ------- | ------ | ------- | ------ | ------------ | ------- | 141 | | Mini | 60G | 200 Mbps | ¥15 | ¥162 | [购买](https://duangcloud.org/cart.php?a=add&pid=8&aff=99)| 142 | | Basic | 150G | 200 Mbps | ¥28 | ¥259.2 | [购买](https://duangcloud.org/cart.php?a=add&pid=3&aff=99) | 143 | | Pro | 300G | 200 Mbps | ¥43 | ¥464.40 | [购买](https://duangcloud.org/cart.php?a=add&pid=4&aff=99) | 144 | 145 | 还有一个按使用量付费的套餐,就不多介绍了,个人觉得年费 ¥162 和 ¥260 的套餐,都是很推荐的,上述价格是使用优惠码 `hunterx.xyz` 后的价格。对于大部分人来说,200 Mbps 的贷款应该是足够的。 146 | 147 | 总的来说,V2Ray 是未来的趋势,iOS 系统的 小火箭(ShadowRocket)、Quantumult 等 APP 也已经跟进支持了,具体操作可以看下服务商的文档。不过由于路由器固件对于容灾和 socks 的代理支持不是很好,我暂时没有选择这种方案,希望未来能够切换过来。 148 | 149 | ### 服务比较 150 | 151 | 需要说明的是,不同的测速方案会得出不同的结果,不具备横向比较的意义。 152 | 153 | 尤其是基于 ICMP (也就是 ping 命令)和 TCP 的测速,一方面受到中转策略的影响,一方面不是完全模拟日常上网,所以准确度和可信度都不高。 154 | 155 | > 比如以京港 IPLC 为例,北京联通到北京服务器的延迟完全可以做到 10ms 以内,但是数据必须经过京港专线,两地的往返距离即使是光速也超过 10ms 156 | 157 | 由于不同时间测速结果不同,所以使用同一个测速服务,纵向比较各个服务商的节点即可。我使用 Quantumult 进行测速,尽量模拟真实使用场景。也可以看得很多 ping 值很低的节点,延迟其实很高。 158 | 159 | 我测试了几个手上常用的服务商的服务,可以看到 N3RO 确实远快于 DuangCloud(图中的 Mini) 160 | 161 | ![](http://images.bestswifter.com/SpeedTest1.jpeg) 162 | -------------------------------------------------------------------------------- /articles/how-does-git-work.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | 4 | --- 5 | 6 |

假设我们有两个分支,a 和 b,它们的提交都有一个相同的父提交(master 指向的那次提交)。如图所示:

7 |

8 |

现在我们在分支 a 上,然后 rabase 到分支 b 上。如图所示:

9 |

10 |

平时开发中经常遇到这种情况,假设分支 a 和 b 是两个独立的 feature 分支,但是不小心被我们错误的 rebase 了。现在相当于两个 feature 分支中原本独立的业务被揉起来了,当然是我们不想看到的结果,那么如何撤销呢?

11 |
12 |

一种方案是利用 reflog 命令。

13 |

利用 reflog 撤销变基

14 |

我们先不考虑原理,直接上解决方案,首先输入 git reflog,你会看到如下图所示的日志:

15 |

16 |

最后的输出其实是最早的操作,我们逐条分析下:

17 |
    18 |
  1. HEAD@{8}: 这里我们创建了初始的提交
  2. 19 |
  3. HEAD@{7}:检出了分支 a
  4. 20 |
  5. HEAD@{6}:在分支 a 上做了一次提交,注意 master 分支没有变动
  6. 21 |
  7. HEAD@{5}:从分支 a 回到分支 master,相当于向后退了一次
  8. 22 |
  9. HEAD@{4}:检出了分支 b
  10. 23 |
  11. HEAD@{3}:在分支 b 上做了一次提交,注意 master 分支没有变动
  12. 24 |
  13. HEAD@{2}:这一步开始变基到分支 a,首先切换到分支 a 上
  14. 25 |
  15. HEAD@{1}:把分支 b 对应的那次提交变基到分支 a 上
  16. 26 |
  17. HEAD@{0}:变基结束,因为是在 b 上发起的变基,所以最后还切回分支 b
  18. 27 |
28 |

如果我们想撤销此次 rebase,只要输入以下命令就可以了:

29 |
git reset --hard HEAD@{3}
 30 | 
31 |

此时再看,已经“恢复”到 rebase 前的状态了。的是不是感觉很神奇呢,先别着急,后面会介绍这么做的原理。

32 |

git 工作原理简介

33 |

为了搞懂 git 是如何工作的,以及这些命令背后的原理,我想有必要对 git 的模型有基础的了解。

34 |

首先,每一个 git 目录都有一个名为 .git 的隐藏目录,关于 git 的一切都存储于这个目录里面(全局配置除外)。这个目录里面有一些子目录和文件,文件其实不重要,都是一些配置信息,后面会介绍其中的 HEAD 文件。子目录有以下几个:

35 |
    36 |
  1. info:这个目录不重要,里面有一个 exclude 文件和 .gitignore 文件的作用相似,区别是这个文件不会被纳入版本控制,所以可以做一些个人配置。
  2. 37 |
  3. hooks:这个目录很容易理解, 主要用来放一些 git 钩子,在指定任务触发前后做一些自定义的配置,这是另外一个单独的话题,本文不会具体介绍。
  4. 38 |
  5. objects:用于存放所有 git 中的对象,下面单独介绍。
  6. 39 |
  7. logs:用于记录各个分支的移动情况,下面单独介绍。
  8. 40 |
  9. refs:用于记录所有的引用,下面单独介绍。
  10. 41 |
42 |

本文主要会介绍后面三个文件夹的作用。

43 |

git 对象

44 |

git 是面向对象的! 45 | git 是面向对象的! 46 | git 是面向对象的!

47 |

没错,git 是面向对象的,而且很多东西都是对象。我举个简单的例子,来帮助大家理解这个概念。假设我们在一个空仓库里,编辑了 2 个文件,然后提交。此时都会有那些对象呢?

48 |

首先会有两个数据对象,每个文件都对应一个数据对象。当文件被修改时,即使是新增了一个字母,也会生成一个新的数据对象。

49 |

其次,会有一个树对象用来维护一系列的数据对象,叫树对象的原因是它持有的不仅可以是数据对象,还可以是另一个树对象。比如某次提交了两个文件和一个文件夹,那么树对象里面就有三个对象,两个是数据对象,文件夹则用另一个树对象表示。这样递归下去就可以表示任意层次的文件了。

50 |

最后则是提交对象,每个提交对象都有一个树对象,用来表示某一次提交所涉及的文件。除此以外,每一个提交还有自己的父提交,指向上一次提交的对象。当然,提交对象还会包含提交时间、提交者姓名、邮箱等辅助信息,就不多说了。

51 |

假设我们只有一个分支,以上知识点就足够解释 git 的提交历史是如何计算的了。它并不存储完整的提交历史,而是通过父提交的对象不断向前查找,得出完整的历史。

52 |

注意开头那张图片,分支 b 指向的提交是 9cbb015,不妨来看下它是何方神圣:

53 |
git cat-file -t 9cbb015
 54 | git cat-file -p 9cbb015
 55 | 
56 |

这里我们使用 cat-file 命令,其中 -t 参数打印对象的类型,-p 参数会智能识别类型,并打印其中的内容。输出结果如图所示:

57 |

58 |

可见 9cbb015 是一个提交对象,里面包含了树对象、父提交对象和各种配置信息。我们可以再打印树对象看看:

59 |

60 |

这表示本次提交只修改了 begin 这个文件,并且输出了 begin 这个文件对于的数据对象。

61 |

git 引用

62 |

既然 git 是面向对象的,那么有没有指正呢?还真是有的,分支和标签都是指向提交对象的指针。这一点可以验证:

63 |
cat .git/refs/heads/a
 64 | 
65 |

所有的本地分支都存储在 git/refs/heads 目录下,每一个分支对应一个文件,文件的内容如图所示:

66 |

67 |

可见,4a3a88d 刚好是本文第一张图中分支 a 所指向的提交。

68 |

我们已经搞明白了 git 分支的秘密,现在有了所有分支的记录,又有了每次提交的父提交对象,就能够得出像 SourceTree 或者文章开头第一张图那样的提交状态了。

69 |

至于标签,它其实也是一种引用,可以理解为不能移动的分支。只能永远指向某个固定的提交。

70 |

最后一个比较特殊的引用是 HEAD,它可以理解为指针的指针,为了证明这一点,我们看看 .git/HEAD 文件:

71 |

72 |

它的内容记录了当前指向哪个分支,refs/heads/b 其实是一个文件,这个文件的内容是分支 b 指向的那个提交对象。理解这一点非常重要,否则你会无法理解 checkoutreset 的区别。

73 |

这两个命令都会改变 HEAD 的指向,区别是 checkout 不改变 HEAD 指向的分支的指向,而 reset 会。举个例子, 在分支 b 上执行以下两个命令都会让 HEAD 指向 4a3a88d 这次提交(分支 a 指向的提交):

74 |
git checkout a
 75 | git reset --hard a
 76 | 
77 |

checkout 仅改变 HEAD 的指向,不会改变分支 b 的指向。而 reset 不仅会改变 HEAD 的指向,还因为 HEAD 指向分支 b,就把 b 也指向 4a3a88d 这次提交。

78 |

git 日志

79 |

.git/logs 目录中,有一个文件夹和一个 HEAD 文件,每当 HEAD 引用改变了指向的位置,就会在 .git/logs/HEAD 中添加了一个记录。而 .git/logs/refs/heads 这个目录中则有多个文件,每个文件对应一个分支,记录了这个分支 的指向位置发生改变的情况。

80 |

当我们执行 git reflog 的时候,其实就是读取了 .git/logs/HEAD 这个文件。

81 |

82 |

撤销 rebase 的原理

83 |

首先我们要排除一个误区,那就是 git 会维护每次提交的提交对象、树对象和数据对象,但并不会维护每次提交时,各个分支的指向。在介绍分支的那一节中我们已经看到,分支仅仅是一个保留了提交对象的文件而已,并不记录历史信息。即使在上一节中,我们知道分支的变化信息会被记录下来,但也不会和某个提交对象绑定。

84 |

也就是说,git 中并不存在某次提交时的分支快照

85 |

那么我们是如何通过 reset 来撤销 rebase 的呢,这里还要澄清另一个事实。前文曾经说过,某个时刻下你通过 SourceTree 或者 git log 看到的分支状态,其实是由所有分支的列表、每个分支所指向的提交,和每个提交的父提交共同绘制出来的。

86 |

首先 git/refs/heads 下的文件告诉我们有多少分支,每个文件的内容告诉我们这个分支指向那个提交,有了这个提交不断向前追溯就绘制出了这个分支的提交历史。所有分子的提交历史也就组成了我们看到的状态。

87 |

但我们要明确:不是所有提交对象都能看到的,举个例子如果我们把某个分支向前移一次提交,那个分支的提交线就会少一个节点,如果没有别的提交线包含这个节点,这个节点就看不到了。

88 |

所以在 rebase 完成后,我们以为看到了下面这样的提交线:

89 |
df0f2c5(master) --- 4a3a88d(a) --- 9cbb015(b)
 90 | 
91 |

实际上是这样的:

92 |
df0f2c5(master) --- 4a3a88d(a) --- 9d0618e(b)
 93 |    |
 94 | 9cbb015
 95 | 
96 |

master 分支上依然有分叉,原来 9cbb015 这次提交依然存在,只不过没有分支的提交线包含它,所以无法看到而已。但是通过 reflog,我们可以找回 HEAD 头的每一次移动,所以能看到这次提交。

97 |

当我们执行这个命令时:

98 |
git reset --hard HEAD@{3}
 99 | 
100 |

再看一次 reflog 的输出:

101 |

102 |

HEAD@{3} 其实是它左侧 9cbb015 这次提交的缩写,所以上述命令等价于:

103 |
git reset --hard 9cbb015
104 | 
105 |

前文说过,reset 不仅会移动 HEAD,还会移动 HEAD 所指向的分支,所以这个命令的执行结果就是让 HEAD 和分支 b 同时指向 9cbb015 这个提交,看起来像是撤销了 rebase。

106 |

但别忘了,分支 a 的上面还是有一次提交的,9d0618e 这次提交仅仅是没有分支指向它,所以不显示而已。但它真实的存在着,严格意义上来说,我们并没有真正的撤销此次 rebase

107 | 108 | -------------------------------------------------------------------------------- /articles/http-encoding.md: -------------------------------------------------------------------------------- 1 | 本文主要介绍 JSON,base64 和 urlencode 这三种在 HTTP 协议中经常出现的编码方式,编码本身很简单,但难的是了解为什么需要这种编码;这种编码的输入输出分别是什么;多次编码,解码但结果是否一样,有什么特殊字符需要转义;以及如何快速但识别具体的编码方式。由于这几种编码都是可逆的,只要掌握了编码方式就可以解码,所以我不会对解码方式做太多介绍。 2 | 3 | ## JSON 4 | 5 | JSON 的全文是 JavaScript Object Notation,也就是 JavaScript 中的对象表示方法,它诞生的目的是为了取代 XML。过去,XML 通常用于文件传输,然而它的效率过于低下,参杂了太多与实际内容无关的东西,比如下面这段 XML 可以用于描述一个人的年龄和姓名: 6 | 7 | ```xml 8 | 9 | bestswifter 10 | 22 11 | 12 | ``` 13 | 14 | 同样的内容用 JSON 来写就是: 15 | 16 | ```json 17 | {"name": "bestswifter", "age": 22} 18 | ``` 19 | 20 | 可见 JSON 其实是字符串的一种描述方式,它的本质还是字符串,最大的优点就是废话少,效率高。很多人说 JSON 的最外层一定是一个对象或者数组,这个定义已经过时了,新的 [RFC 7159](https://tools.ietf.org/html/rfc7159) 文档取代了老的 [RFC 4627](https://tools.ietf.org/html/rfc4627),其中 JSON 定义如下: 21 | 22 | 1. JSON 串的最外层一定是数组,对象或者值 23 | 2. 数组由若干个对象组成,用中括号表示:`[]` 24 | 3. 对象用大括号表示:`{}`,它由键值对组成,键一定是字符串,用双引号表示 `""` 25 | 4. 值可以是 `false`、`null`、`true`、对象、数组、数字和字符串 26 | 27 | JSON 只是一种编码规范,它仅仅规定了 JSON 格式的字符串应该具备什么特征,至于原来的数据如何转化为 JSON 串,则是由各个编程语言自己实现。一般来说,内置的基本数据类型,比如数组、字典、布尔值、字符串、数字都可以转换成 JSON 串,其中字典会转换成 JSON 中对象,用户自定义的对象一般都不能转 JSON,除非实现了特定的协议。 28 | 29 | 理论上来说,用来组成 JSON 的字符都需要转义,以免发生歧义,不过括号并不需要,因为它们总是成对出现,即使不转义也不会影响解析,因此字符串中的双引号需要转义,比如 `"` 会被转为 `\"`,同理反斜杠(backslash)自己也许要转义:`\` -> `\\`。另一个我经常见到的转以后的字符是换行符 `\n`。下面是一个 Python 中的例子,用来简单演示一下 JSON 编码中的转义: 30 | 31 | ```python 32 | import json 33 | 34 | s = '''[{h/e\l,l"o"}] 35 | ''' 36 | j = json.dumps(s) 37 | # "[{h/e\\l,l\"o\"}]\n" 38 | ``` 39 | 40 | 一般来说,JSON 编码是将语言中特定类型的对象转换为满足特定协议的字符串,解码自然就是将字符串转换回语言中类型。不过有的语言比较奇怪,比如 Objective-C 中原生的 JSON 库,在编码时并不会返回 JSON 串,而是这个字符串在 UTF8 编码下的二进制数据。 41 | 42 | 很明显 JSON 不能多次解码。多次编码时,后面几次其实都是对字符串编码,而且会会经历 `"` 到 `\"` 再到 `\\\"` 的过程,长度急剧增加。 43 | 44 | ## base64 编码 45 | 46 | 字符串编码的作用是把字符串转换为二进制数据,以便存储和发送。base64 的作用则恰好相反,它把二进制数据映射到字符串。它使用了 `a-z`,`A-Z`,`0-9` 和 `+/` 共 `26 + 26 + 10 + 2 = 64` 种字符来接受二进制数据的映射,所以被称为 base64。 47 | 48 | 假设我有一句 “Hello,world” 需要通过网络发送,它背后的原理是先将字符串用 ASCII 或 UTF-8 等方式编码,然后发送这个二进制数据。然而有些古老的设备(比如交换机)和古老的协议(邮件的 SMTP 协议)只能处理标准的 ASCII 编码,也就是最高位是 0,剩下七位有效。但实际上一些 ASCII 码值在 128-255 之间的二进制数据也有可能被用到,而这部分数据在传输时可能就无法被正确处理。 49 | 50 | base64 的好处在于,它转换得到的字符串一定是标准的 ASCII 码,发送这些字符串不会存在任何风险。base64 是一个相对比较底层,比较古老的编码方式,在应用层的场景并不多,这里简单举两个例子。 51 | 52 | 在 HTML 种如果需要传输图片,一般使用 `` 标签,但这会额外建立一次连接,如果图片非常小,建立连接的开销甚至可以远大于下载图片本身的开销,因此可以考虑直接把图片嵌入到 HTML 的数据中。然而 HTML 是一个纯文本的协议,如果直接发送图片的二进制数据,里面的换行符可能就被当做换行符,用来换行了,然而它实际上只是图片数据不可或缺的一部分。这时候 base64 的作用就是把二进制转为字符串: 53 | 54 | ```html 55 | Embedded Image 56 | ``` 57 | 58 | 在平时的开发中,我们也有可能会用到 base64。比如我需要把一段复杂的文本传递给后端并保存起来,如果直接发送字符串,那么接收方存储下来的可能是有换行的数据: 59 | 60 | ``` 61 | 'name': 'bestswifter' 62 | 'age': 22 63 | 'descrption': '这里很长而且有换行' 64 | 'sex': 'male' 65 | ``` 66 | 67 | 将来有一天,我需要读取这个人的 `description`,如果用 grep 来处理就出问题了: 68 | 69 | ```bash 70 | grep 'description' log_20171106.txt 71 | ``` 72 | 73 | 因为 grep 是按行查找,所以这里只能拿到第一行,想要获取完整版的 `description` 会非常困难。 74 | 75 | 所以应用层的 base64 主要是处理自定义的数据,防止其中的控制字符(比如换行符等)被当作文本错误的处理。这样能够保证接收端的应用层可以原封不动的获取数据。 76 | 77 | 每一个 ASCII 码占用 8 个比特,其中每 6 个比特会编码成一个 base64 中的字符,这是因为 64 恰好是 2 的 6 次方。根据这个规则我们可以得出两个结论: 78 | 79 | 1. 使用 base64 编码会使原始二进制数据的大小增加三分之一 80 | 2. 二进制数据的比特位不一定恰好是 6 的倍数,不够的位需要用 0 补齐 81 | 82 | 下图截自 [维基百科](https://zh.wikipedia.org/wiki/Base64) 中的表格,用来演示 base64 的编码规则: 83 | 84 | ![](http://images.bestswifter.com/base64-1.png) 85 | 86 | 从二进制的 0 - 63 映射到 base64 编码的原则比较简单,就不列出来了,可以参考维基百科的链接。 87 | 88 | base64 编码有两个小细节需要注意下,它的实际工作方式并不是每 6 个比特转换一个 ASCII 字符,而是选择 6 和 8 的最小公倍数 24,每次读取 24 个 bit,也就是三个字节转换成四个 base64 编码后的字符。如果被编码的字节数不是 3 的倍数,那么可能会多出 1 个或者 2 个字节。如果多出一个字节,可以转换成 2 个字节的 base64 编码,所以还缺两位,需要用两个等号补齐。如果多出两个字节,可以转换成三个字节的 base64 编码,需要用一个等号补齐。如下图所示: 89 | 90 | ![](http://images.bestswifter.com/%E5%B1%8F%E5%B9%95%E5%BF%AB%E7%85%A7%202017-11-06%20%E4%B8%8A%E5%8D%8810.14.28.png) 91 | 92 | 上面我提到的这些规则,比如用等号补位,0-64 如何编码等,都是标准base64 的定义,实际上 base64 有很多变种,比如有的 base64 的字符集中没有 `+` 和 `/`,而是用 `,.-_:` 等字符,有的 base64 变种会强制每几十个字节插入一个换行符,有的没有补位要求。因此客户端如果想要借助 base64 编码的某些特性,需要明确自己调用的 base64 是哪一种,并且与解析方约定、联调好。 93 | 94 | ## urlencode 95 | 96 | 并不是所有的字符都能作为 URL 的一部分,以下几种字符需要被编码: 97 | 98 | 1. ASCII 控制字符:范围是 16 进制的 00-1F 以及 7F,这些字符无法被打印出来,而 URL 规范指出 URL 不仅可以通过网络传输,还应该可以被打印或者电台中读出来。 99 | 2. 非 ASCII 字符:这不符合 URL 的定义 100 | 3. 保留字符:一些特殊的字符比如 `@` 用于表示电子邮件的地址,`&` 是 get 请求参数的分隔符,`=` 是请求参数中键值对的分隔符,`/` 用于分割路径等等,如果应用层数据携带了这些关键字,就可能导致服务端解析错误 101 | 4. 不安全字符:由于各种原因,有些字符在解析时可能会产生歧义,导致无法正确解析出原始数据,这些字符也要转义,最常见的就是空格,它会被编码为 `%20`。 102 | 103 | URL 编码的方式并不统一,从上面的分类中可以看出,控制类字符、非 ASCII 字符和不安全字符都属于不合法字符,无论如何都不可能出现在 URL 中。但保留字符的处理则需要分情况处理,比如有如下字符串: 104 | 105 | ``` 106 | https://baidu.com/search=你好 107 | ``` 108 | 109 | 显然只有中文的“你好”才需要转义,保留字符则不需要,但如果我们定义了一个变量 `search_word` 并且需要拼接到字符串的结尾: 110 | 111 | ```python 112 | search_word = '/你好/' 113 | url = 'https://baidu.com/search=' + search_word 114 | ``` 115 | 116 | 那么在对 `search_word` 进行 urlencode 时,就必须把保留字符也进行转义。 117 | 118 | 因此在进行 urlencode 时,一定要阅读 api 的说明文档,了解它的编码规则,尤其是对保留字符的处理方式。比如 Python3 中的 quote 函数就是一个典型的坑,它有一个名为 safe 的参数,默认值是 `/`,表示斜杠不会被编码。 119 | 120 | 在 HTTP 协议的 post 方法中,一般需要通过 HEAD 头中的 `Content-Type` 指定数据的编码方式,一种常见的编码就是 `application/x-www-form-urlencoded`,默认的 Web 表单也使用这种方式,提交的数据格式如下: 121 | 122 | ``` 123 | key1=value1&key2=value2&... 124 | ``` 125 | 126 | 其中的键值对都需要经过 urlencode 编码。由于这个属于 HTTP 规范,因此在客户端开发中一般并不需要开发者手动指定,而是由网络库封装。比如一个网络库可以接受字典格式的参数,并且自动进行 urlencode 编码并上传。然而我的建议是,客户端最好不要依赖于网络库的这个特性,尤其是某些大型应用的底层会有多个网络库随时切换,这时候就必须逐一验证。其实开发者完全可以在业务层手动编码一次,因为 urlencode 的特点就是可以多次 decode,如果解码以后再次解码,并不会改变字符串的内容。 127 | 128 | 常见字符的 urlencode 值如下表: 129 | 130 | | 字符 | urlencode | 131 | | :---| :-------- | 132 | | : | %3A| 133 | | 空格| %20| 134 | |/|%2F| 135 | |?|%3F| 136 | |&|%26| 137 | |=|%3D| 138 | 139 | ## 总结 140 | 141 | 以上三种编码方式的特性可以用一张表格来概括: 142 | 143 | | 编码方式 | 输入 | 输出 | 转义 | 鉴别特征 | 多次编码结果 | 多次解码 | 144 | |:---:|:---:|:---:|:---:|:---:|:---:|:---:| 145 | | JSON | 数据结构或对象 | 字符串 | `"`,`\n` 和 `\` 等| 结构化的字符串 | 转义的 `\` 越来越多 | 只能解一次 | 146 | |base64|二进制|字符串|无|末尾可能有等号,字符集单一|还是 base64,但内容发送改变 | 要和编码次数对应,否则结果无意义 | 147 | |urlencode|字符串|符合 URL 规范的字符串| 控制符、非 ASCII 字符,保留字符和不安全字符 | %xx 较多 | 会改变,因为百分号也要转义|可以多解但不能少解| 148 | 149 | base64 和 urlencode 的编/解码推荐 [FE 助手](https://chrome.google.com/webstore/detail/web%E5%89%8D%E7%AB%AF%E5%8A%A9%E6%89%8Bfehelper/pkgccpejnmalmdinmhkkfafefagiiiad?utm_source=chrome-ntp-icon) 这个 Chrome 插件,但我的建议是了解一下脚本语言中如何编解码,自己封装一个命令行工具,JSON 的格式化查看则推荐[这个网站](http://jsoneditoronline.org)。 150 | 151 | ## 参考资料 152 | 153 | 1. [What is the minimum valid JSON?](https://stackoverflow.com/questions/18419428/what-is-the-minimum-valid-json) 154 | 2. [Base64-Chinese](https://zh.wikipedia.org/wiki/Base64) 155 | 3. [Base64-English](https://en.wikipedia.org/wiki/Base64) 156 | 4. [Why do we use Base64?](https://stackoverflow.com/questions/3538021/why-do-we-use-base64) 157 | 5. [URL Encoding](http://www.blooberry.com/indexdot/html/topics/urlencoding.htm) 158 | 6. [Uniform Resource Identifiers (URI): Generic Syntax](http://www.ietf.org/rfc/rfc2396.txt) 159 | 7. [关于URL编码](http://www.ruanyifeng.com/blog/2010/02/url_encoding.html) -------------------------------------------------------------------------------- /articles/http-proxy-tools.md: -------------------------------------------------------------------------------- 1 | 好久不写博客了,在元旦到来前水一篇文章,聊聊我在实现代理服务器的过程中遇到的一些坑,同时祝各位读者新年快乐。 2 | 3 | # 背景 4 | 5 | 长期以来,贴吧开发人员多,业务耦合大,需求变化频繁,因此容易产生 bug。而我所负责的广告相关业务,和 UI 密切相关,一旦因为某种原因(甚至是被别人改了代码)产生了 bug,必然大幅度影响广告收入。 6 | 7 | 解决问题的一种方法在于频繁的测试,既然避免不了代码层面的耦合,那总是可以通过定时的检查来避免问题。所以我们维护了一组核心 case,密切关注最核心的功能。选择核心 case 实际上是在覆盖面和测试成本之间的权衡,然而多个 case 有不同的测试步骤,测试效率始终难以提高。 8 | 9 | 因此,我们的目标是建立一个代理服务器,**能够在运行时把任何包(包括线上包)的数据改成我希望的样子**。换句话说,这个代理服务器也可以理解为一个私服,它能够获得客户端的请求数据并作出修改,也可以获得服务端的响应数据并做修改。 10 | 11 | # 代理服务器工作模型 12 | 13 | 在早期版本中,我们选择了简单的 HTTP 协议。这种选择对技术的要求最低,我们自己实现了一个代理服务器,开启 socket,监听端口,然后将客户端的请求发送给服务器,再把服务器的返回数据传回客户端。这种模式也被称为:“中间人模式”(MITM: Man In The Middle)。 14 | 15 | 虽然道理很简单,但实现起来还是有些地方要注意。首先,当 socket 接受数据后,应该新开一个进程/线程 进行处理。既然涉及到新的进程/线程,就一定要注意它的释放时机,否则会导致内存无限制增加。 16 | 17 | 其次,对于 `socket` 来说,它并没有等待函数,也就是说我无从得知何时有数据可读,因此这个艰巨的任务就交给了 `select`。我们把需要监听的 socket 对象作为参数传入其中,函数会一直阻塞,直到有可读、可写的对象,或者达到超时时间。 18 | 19 | `Keep-Alive` 字段可以复用 TCP 连接,是一种常见的 HTTP 协议的优化方式,在 HTTP 1.1 中已经是默认选项。填写这个字段后,Server 返回的数据可能是分批次的,这样能够改善用户体验,但也会增加代理服务器的实现难度。所以代理服务器在作为客户端,向真正服务器请求数据时,应该删除这个字段。 20 | 21 | 由于整套流程都是自己实现,因此可以比较容易的 HOOK 住上下行数据并做修改。只有注意在接收到全部数据后再做修改即,整个流程可以用下图简单表示: 22 | 23 | ![代理服务器的工作模式](https://o8ouygf5v.qnssl.com/1483087878.png) 24 | 25 | 当时做完这一套东西以后,我在团队内部做了一次分享, 感兴趣的读者可以去 下载 PPT。 26 | 27 | # 技术选型 28 | 29 | ## 短连接 30 | 31 | 由于长连接基于 TCP,不用每次新建连接,也省略了不必要的 HTTP 报文头部,效率明显优于 HTTP。所以各大公司基本上选择了长连接作为实际生产环境下的连接方式。然而由于不熟悉 WebSocket 协议,并且我们依然支持短连接,所以代理服务器最终选择了 HTTP 协议。 32 | 33 | 要想实现这一点, 就得在应用启动时,模拟后台向客户端发送一段控制信息,强制客户端选择 HTTP 请求。这样一来,即使是线上包也可以走代理服务器。 34 | 35 | ## HTTPS 36 | 37 | 由于苹果强制要求使用 HTTPS,虽然已经延期,但也是明年的趋势。考虑到后续的使用,我们决定对之前实现的代理服务器进行升级。由于 HTTPS 涉及到请求协议的解析,以及加密解密和证书管理,上述自研方案很难 hold 住。经过一番调研,最后选择了一个比较知名的开源库 [mitmproxy](https://github.com/mitmproxy/mitmproxy)。 38 | 39 | # Mitmproxy 40 | 41 | 选择这个库最主要的理由是它直接支持 HTTPS,不过没有中文文档,国内的使用相对来说比较少,所以在接入的时候可能会略花一点时间。 42 | 43 | 这是一个 python 库, 首先要安装 `virtualenv`,如果本地没装的话输入: 44 | 45 | ```bash 46 | sudo pip install virtualenv 47 | ``` 48 | 49 | 安装好了以后,进入 `mitmproxy/venv3.5/bin` 文件夹输入: 50 | 51 | ```bash 52 | source ./active 53 | ``` 54 | 55 | 这样就可以启用 virtualenv 环境了。 56 | 57 | ## Hook 脚本 58 | 59 | 这个库可以理解为命令行中可交互版本的 Charles,不过我并不打算用它的这个功能。因为我的需求主要是利用脚本来 Hook 请求, 所以我选择了 mitmdump 这个工具。使用它的时候可以指定脚本: 60 | 61 | ```bash 62 | mitmdump -s "xxx.py" 63 | ``` 64 | 65 | 脚本也很简单,我们可以重写 `requeest` 或者 `receive` 函数: 66 | 67 | ```python 68 | def request(flow): 69 | flow.response.content = "

hello world

" 70 | ``` 71 | 72 | 运行脚本以后,把手机的代理设为本机 ip 地址,端口号改为 8080,然后用手机浏览器打开 ,如果一切配置顺利,你会看到证书的安装界面。 73 | 74 | 安装好证书后,用手机访问任何一个网站(包括 HTTPS),你应该都会看到一个小小的 `hello world`,至此所有的配置就完成了。 75 | 76 | ## bug 修改 77 | 78 | 这个开源库有一个很严重的 bug,在解析 multipart 类型的数据时可能会发生。它使用了 `splitline` 方法来分割换行符,然而如果数据中有 `\n` 的话,就会因此丢失。很不幸的是,很多 protobuf 编码后的数据都有 `\n`,一旦丢失就会导致解析失败。 79 | 80 | 如果你不幸遇到了和我一样的坑,可以把相关代码改成我的版本: 81 | 82 | ```python 83 | for i in content.split(b"--" + boundary): 84 | parts = i.split(b'\r\n\r\n', 2) 85 | if len(parts) > 1 and parts[0][0:2] != b"--": 86 | match = rx.search(parts[0]) 87 | if match: 88 | key = match.group(1) 89 | value = parts[1][0:len(parts[1])-2] # Remove last \r\n 90 | r.append((key, value)) 91 | ``` 92 | 93 | ## More 94 | 95 | 到了这一步,基本上已经成功实现支持 HTTPS 的代理服务器了。后续要处理的可能就是解析 protobuf,完善业务代码等等琐碎的事情,只要小心谨慎,基本上不会有问题。 96 | -------------------------------------------------------------------------------- /articles/https-9-questions.md: -------------------------------------------------------------------------------- 1 | 女朋友也是软件工程专业,因为快要毕业了,最近一边做毕设一边学习编程。前两天她问我 HTTPS 的问题,本来想直接扔一篇网上的教程给她。后来想了一下,那些文章大多直接介绍概念, 对新手不太友好,于是我干脆亲自给她解释一下,顺便整理了一份问答录。 2 | 3 | ### Q1: 什么是 HTTPS? 4 | 5 | ### BS: HTTPS 是安全的 HTTP 6 | 7 | HTTP 协议中的内容都是明文传输,HTTPS 的目的是将这些内容加密,确保信息传输安全。最后一个字母 S 指的是 SSL/TLS 协议,它位于 HTTP 协议与 TCP/IP 协议中间。 8 | 9 | ### Q2: 你说的信息传输安全是什么意思 10 | 11 | ### BS: 信息传输的安全有三个方面: 12 | 13 | 1. 客户端和服务器直接的通信只有自己能看懂,即使第三方拿到数据也看不懂这些信息的真实含义。 14 | 2. 第三方虽然看不懂数据,但可以 XJB 改,因此客户端和服务器必须有能力判断数据是否被修改过。 15 | 3. 客户端必须避免中间人攻击,即除了真正的服务器,任何第三方都无法冒充服务器。 16 | 17 | 很遗憾的是,目前的 HTTP 协议还不满足上述三条要求中的任何一条。 18 | 19 | ### Q3: 这么多要求,一个一个去满足是不是很累? 20 | ### BS: 不累,第三个要求可以不用管 21 | 22 | 是的,我没开玩笑,你可以暂时别管第三个要求,因为它实际上隶属于第一个需求。我们都知道加密需要密码,密码不是天下掉下来,也得需要双方经过通信才能协商出来。所以一个设计良好的加密机制必然会防止第三者的干扰和伪造。等搞明白了加密的具体原理,我们自然可以检验是否满足:“任何第三者无法冒充服务器”这一要求。 23 | 24 | ### Q4: 那怎么加密信息呢 25 | 26 | ### BS: 使用对称加密技术 27 | 28 | 对称加密可以理解为对原始数据的可逆变换。比如 `Hello` 可以变换成 `Ifmmp`,规则就是每个字母变成它在字母表上的后一个字母,这里的秘钥就是 `1`,另一方拿到 `Ifmmp` 就可以还原成原来的信息 `Hello` 了。 29 | 30 | 引入对称加密后,HTTPS 的握手流程就会多了两步,用来传递对称加密的秘钥: 31 | 32 | 1. 客户端: 你好,我需要发起一个 HTTPS 请求 33 | 2. 服务器: 好的,你的秘钥是 `1`。 34 | 35 | 提到了对称加密,那么自然还有非对称加密。它的思想很简单,计算两个质数的乘积很容易,但反过来分解成两个质数的乘积就很难,要经过极为复杂的运算。非对称加密有两个秘钥,一个是公钥,一个是私钥。公钥加密的内容只有私钥可以解密,私钥加密的内容只有公钥可以解密。一般我们把服务器自己留着,不对外公布的密钥称为**私钥**,所有人都可以获取的称为**公钥**。 36 | 37 | 使用对称加密一般要比非对称加密快得多,对服务器的运算压力也小得多。 38 | 39 | ### Q5: 对称秘钥如何传输 40 | 41 | 服务器直接返回明文的对称加密密钥是不是不安全。如果有监听者拿到这个密钥,不就知道客户端和服务器后续的通信内容了么? 42 | 43 | ### BS: 利用非对称加密 44 | 45 | 是这样,所以不能明文传递对称秘钥,而且也不能用一个新的对称加密算法来加密原来的对称秘钥,否则新的对称秘钥同样无法传输,这就是鸡生蛋、蛋生鸡的悖论。 46 | 47 | 这里我们引入非对称加密的方式,非对称加密的特性决定了服务器用私钥加密的内容并不是**真正的加密**,因为公钥所有人都有,所以服务器的密文能被所有人解析。但私钥只掌握在服务器手上,这就带来了两个巨大的优势: 48 | 49 | 1. 服务器下发的内容不可能被伪造,因为别人都没有私钥,所以无法加密。强行加密的后果是客户端用公钥无法解开。 50 | 2. 任何人用公钥加密的内容都是绝对安全的,因为私钥只有服务器有,也就是只有真正的服务器可以看到被加密的原文。 51 | 52 | 所以传输对称秘钥的问题就迎刃而解了: 秘钥不是由服务器下发,而是由客户端生成并且主动告诉服务器。 53 | 54 | 所以当引入非对称加密后,HTTPS 的握手流程依然是两步,不过细节略有变化: 55 | 56 | 1. 客户端: 你好,我需要发起一个 HTTPS 请求,这是我的 (用公钥加密后的) 秘钥。 57 | 2. 服务器: 好的,我知道你的秘钥了,后续就用它传输。 58 | 59 | ### Q5: 那公钥怎么传输 60 | 61 | 你好像还是没有解决鸡生蛋,蛋生鸡的问题。你说客户端发送请求时要用公钥加密对称秘钥,那公钥怎么传输呢? 62 | 63 | ### BS: 对公钥加密就行了。。。 64 | 65 | 每一个使用 HTTPS 的服务器都必须去专门的证书机构注册一个证书,证书中存储了用权威机构私钥加密的公钥。这样客户端用权威机构的公钥解密就可以了。 66 | 67 | 现在 HTTPS 协议的握手阶段变成了四步: 68 | 69 | 1. 客户端: 你好,我要发起一个 HTTPS 请求,请给我公钥 70 | 2. 服务器: 好的,这是我的证书,里面有加密后的公钥 71 | 3. 客户端: 解密成功以后告诉服务器: 这是我的 (用公钥加密后的) 对称秘钥。 72 | 4. 服务器: 好的,我知道你的秘钥了,后续就用它传输。 73 | 74 | ### Q6: 你在逗我么。。。。 75 | 76 | 那权威机构的公钥又怎么传输? 77 | 78 | ### BS: 存在电脑里 79 | 80 | 这个公钥不用传输,会直接内置在各大操作系统(或者浏览器)的出厂设置里。之所以不把每个服务器的公钥内置在电脑里,一方面是因为服务器太多,存不过来。另一方面操作系统也不信任你,凭什么你说你这个就是百度/淘宝的证书呢? 81 | 82 | 所以各个公司要先去权威机构认证,申请证书,然后操作系统只会存储权威机构的公钥。因为权威机构数量有限,所以操作系统厂商相对来说容易管理。如果这个权威机构不够权威,XJB 发证书,就会取消他的资格,比如可怜的沃通。。。。 83 | 84 | ### Q7: 怎么知道证书有没有被篡改? 85 | 86 | 你说服务器第一次会返回证书,也就是加密以后的公钥,那我怎么知道这个证书是可靠的? 87 | 88 | ### BS: 将信息 hash 值随着信息一起传递 89 | 90 | 我们都知道哈希算法的特点,它可以压缩数据,如果从函数角度来看,不管多复杂的数据(定义域可以非常大)经过哈希算法都会得到一个值,而且这个值处在某个特定(远小于定义域的范围)值域内。相同数据的哈希结果一定相同,不相同数据的哈希结果一般不同,不过也有小概率会重复,这叫哈希冲突。 91 | 92 | 为了确保原始证书没有被篡改,我们可以在传递证书的同时传递证书的哈希值。由于第三者无法解析数据,只能 XJB 改,那么修改后的数据在解密后,就不可能通过哈希。 93 | 94 | 比如说公钥就是之前的例子 `Hello`,我们假设哈希算法是获取字符串的最后一个字符,那么 `Hello` 的哈希值就是 `o`,所以加密字符串是 `Ifmmpp`。虽然公钥已知,每个人都可以解密,解密完也可以篡改,但是因为没有私钥, 所以无法正确的加密。所以它再返回给客户端的数据是无效数据,用公钥解析后会得到乱码。即使攻击者通过多次尝试碰巧能够解析,也无法通过哈希校验。 95 | 96 | ### Q8: 这样可以防止第三方冒充服务器么 97 | ### BS: 也许可以 98 | 99 | 首先真正的服务器下发的内容,无法被别人篡改。他们有权威机构的公钥,所以可以解密,但是因为没有私钥,所以解密以后的信息无法加密。没有加密或者错误加密的信息被客户端用公钥解密以后,必然无法通过哈希校验。 100 | 101 | 但是,如果你一开始请求的就不是真的服务器,而是一个攻击者,此时的他完全有机会进行中间人攻击。我们知道第一次握手的时候服务器会下发用于证明自己身份的证书,这个证书会用预设在设备上的公钥来解密。所以要么是经过认证的证书用权威机构的私钥加密,再用权威机构解密,要么是用非权威机构的私钥加密,然后找不到公钥解密。 102 | 103 | 所以如果不小心安装过非权威机构的根证书,比如黑客提供的恶意证书,这时候设备上就多了一个预设的公钥,那么用恶意私钥加密的证书就能被正常解析出来。所以千万不要随便装根证书,这等于是为那些恶意证书留了一扇门。 104 | 105 | 当然,凡是都有两面性。我们知道 Charles 可以调试 HTTPS 通信,它的原理就是需要用户安装 Charles 的根证书,然后我们的请求会被代理到 Charles 服务器,它下发的 Charles 证书才能被正确解析。另一方面,Charles 会作为客户端,从真正的服务器哪里拿到正确的 https 证书并用于后续通信。幸好 Charles 不是流氓软件,或者它的私钥一旦泄露,对用户都会造成很大的影响。 106 | 107 | 我可以举一个例子,证书有多个种类,最贵的叫 EV (Extended Validation),它需要公司营业执照等多个文件才能申请人工审核,好处也很明显,可以在浏览器地址栏左侧准确显示公司名称,比如 [Bitbucket 的官网](https://bitbucket.org/): 108 | 109 | ![EV 证书左侧的名字](https://user-gold-cdn.xitu.io/2017/12/12/1604b4bd545d3b4f?w=636&h=54&f=png&s=11335) 110 | 111 | 这是客户端直连时候的正常现象。但如果你用 Charles 代理,客户端拿到的是 Charles 证书,所以会变成: 112 | 113 | ![代理模式下无法显示](https://user-gold-cdn.xitu.io/2017/12/12/1604b4bd54664d9c?w=460&h=50&f=png&s=8691) 114 | 115 | ### Q9: HTTPS 握手会影响性能么 116 | 117 | TCP 有三次握手,再加上 HTTPS 的四次握手,会不会影响性能? 118 | 119 | ### BS: 影响肯定有,但是可以接受 120 | 121 | 首先,HTTPS 肯定会更慢一点,时间主要花费在两组 SSL 之间的耗时和证书的读取验证上,对称算法的加解密时间几乎可以忽略不计。 122 | 123 | 而且如果不是首次握手,后续的请求并不需要完整的握手过程。客户端可以把上次的加密情况直接发送给服务器从而快速恢复,具体细节可以参考 [图解SSL/TLS协议](http://www.ruanyifeng.com/blog/2014/09/illustration-ssl.html)。 124 | 125 | 除此以外,SSL 握手的时间并不是只能用来传递加密信息,还可以承担起客户端和服务器沟通 HTTP2 兼容情况的任务。因此从 HTTPS 切换到 HTTP2.0 不会有任何性能上的开销,反倒是得益于 HTTP2.0 的多路复用等技术,后续可以节约大量时间。 126 | 127 | 如果把 HTTPS2.0 当做目标,那么 HTTPS 的性能损耗就更小了,远远比不上它带来的安全性提升。 128 | 129 | ## 结语 130 | 131 | 相信以上九个问题足够帮助新人了解 HTTPS 了,但这只是基本概念,关于 HTTPS 的使用(比如 iOS 上的一些具体问题)还需要不断尝试和研究。 132 | 133 | 文章最后打一个求职广告,长期有效,女朋友今年毕业,考研 380+,[简历戳这里](https://github.com/tinycat2017/Resume),计算机基础还算扎实,但项目经验偏少,可以实习 6 个月以上。北京地区如果有想要招实习生的公司,欢迎联系我,方向不限,前后端皆可培养,iOS 更好(我自己教)。 134 | -------------------------------------------------------------------------------- /articles/ios-compile-speed.md: -------------------------------------------------------------------------------- 1 | 过慢的编译速度有非常明显的副作用。一方面,程序员在等待打包的过程中可能会分心,比如刷刷朋友圈,看条新闻等等。这种认知上下文的切换会带来很多隐形的时间浪费。另一方面,大部分 app 都有自己的持续集成工具,如果打包速度太慢, 会影响整个团队的开发进度。 2 | 3 | 因此,本文会分别讨论日常开发和持续集成这两种场景,分析打包速度慢的瓶颈所在,以及对应的解决方案。利用这些方案,笔者成功的把公司 app 的持续集成时间从 45 min 成功的减少到 9 min,效率提升高达 80%,理论上打包速度可以提升 10 倍以上。如果用一句话总结就是: 4 | 5 | > 在绝对的实力(硬件)面前,一切技巧(软件)都是浮云 6 | 7 | ## 日常开发 8 | 9 | 其实日常开发的优化空间并不大,因为默认情况下 Xcode 会使用上次编译时留下的缓存,也就是所谓的增量编译。因此,日常开发的主要耗时由三部分构成: 10 | 11 | > 总耗时 = 增量编译 + 链接 + 生成调试信息(dSYM) 12 | 13 | 这里的增量编译耗时比较短,即使是在我 14 年高配的 MacBook Pro(4核心,8 线程,2.5GHz i7 4870HQ,下文简称 MBP) 上,也仅仅耗时十秒上下。我们的应用代码量大约一百多万行,业内超过这个量级的应用应该不多。链接和生成调试信息各花费不到 20s,因此一次增量的编译的时间开销在半分钟到一分钟左右,我们逐个分析: 14 | 15 | 1. 增量编译: 因为耗时较短(大概十几秒或者更少),几乎不存在优化的空间,但是非常容易恶化。因为只有头文件不变的编译单元才能被缓存,如果某个文件被 N 个文件引用,且这个文件的头文件发生了变化,那么这 N 个文件都会重编译。APP 的分层架构一般都会做,但一个典型的误区是在基础库的头文件中使用宏定义,比如定义一些全局都可以读取的常量,比如是否开启调试,服务器的地址等等。这些常量一旦改变(比如为了调试或者切换到某些分支)就会导致应用重编译。 16 | 2. 链接:链接没有缓存,而且只能用单核进行,因此它的耗时主要取决于单核性能和磁盘读写速度。考虑到我们的目标文件一般都比较小,因此 4K 随机读写的性能应该会更重要一些。 17 | 3. 调试信息:日常开发时,并不需要生成 dSYM 文件,这个文件主要用于崩溃时查找调用栈,方便线上应用进行调试,而开发过程中的崩溃可以直接在 Xcode 中看到,关闭这个功能 **不会对开发产生任何负面影响**。 18 | 19 | 日常开发的优化空间不大,即使是庞大的项目,落后的机器性能,关闭 dSYM 以后也就耗时 30s 左右。相比之下,打包速度可以优化和讨论的地方就比较多了。 20 | 21 | ## 持续集成 22 | 23 | 在利用 Jenkins 等工具进行持续集成时,缓存不推荐被使用。这是因为苹果的缓存不够稳定,在某些情况下还存在 bug。比如明明本地已经修复了 bug,可以编译通过,但上次的编译缓存没有被正确清理,导致在打包机器上依然无法编译通过。或者本地明明写出了 bug,但同样由于缓存问题,打包机器依然可以编译通过。 24 | 25 | 因此,无论是手动删除 `Derived Data` 文件夹,还是调用 `xcodebuild clean` 命令,都会把缓存清空。或者直接使用 `xcodebuild archive`,会自动忽略缓存。每次都要全部重编译是导致打包速度慢的根本原因。以我们的项目为例,总计 45min 的打包时间中,有 40min 都在执行 `xcodebuild` 这一行命令。 26 | 27 | ### 使用 CCache 缓存 28 | 29 | 最自然的想法就是使用缓存了,既然苹果的缓存不靠谱,那么就找一个靠谱的缓存,比如 CCache。它是基于编译器层面的缓存,根据目前反馈的情况看,并不存在缓存不一致的问题。根据笔者的实验,使用 CCache 确实能够较大幅度的提升打包速度,删除缓存并使用 CCache 重编译后,耗时只有十几分钟。 30 | 31 | 然而,CCache 最致命的问题是不支持 PCH 文件和 Clang modules。PCH 的本意是优化编译时间,我们假设有一个头文件 A 依赖了 M 个头文件,其中每个被依赖的头文件又依赖了 N 个 头文件,如下图所示: 32 | 33 | ![](http://images.bestswifter.com/Speed_up_compile_1.png) 34 | 35 | 由于 `#import` 的本质就是把被依赖头文件的内容拷贝到自己的头文件中来,因此头文件 A 中实际上包含了 M * N 个头文件的内容,也就需要 M * N 次文件 IO 和相关处理。当项目中每增加一个依赖头文件 A 的文件,就会重复一次上述的 M * N 复杂度的过程。 36 | 37 | PCH 文件的好处是,这个文件中的头文件只会被编译一次并缓存下来,然后添加到项目中 **所有** 的头文件中去。上述问题倒是解决了,但很智障的一点是,所有文件都会隐式的依赖所有 PCH 中的文件,而真正需要被全局依赖的文件其实非常少。因此实际开发中,更多的人会把 PCH 当成一种快速 `import` 的手段,而非编译性能的优化。前文解释过,PCH 文件一旦发生修改,会导致彻彻底底,完完整整的项目重编译,从而降低编译速度。正是因为 PCH 的副作用甚至抵消了它带来的优化,苹果已经默认不使用 PCH 文件了。 38 | 39 | 用来取代 PCH 的就是 Clang modules 技术,对于开启了这一选项的项目,我们可以用 `@import` 来替代过去的 `#import`,比如: 40 | 41 | ```objc 42 | @import UIKit; 43 | ``` 44 | 等价于 45 | ```objc 46 | #import 47 | ``` 48 | 49 | 抛开自动链接 framework 这些小特性不谈,Clang modules 可以理解为模块化的 PCH,它具备了 PCH 可以缓存头文件的优点,同时提供了更细粒度的引用。 50 | 51 | 说回到 CCache,由于它不支持 PCH 和 Clang modules,导致无法在我们的项目中应用。即使可以用,也会拖累项目的技术升级,以这种代价来换取缓存,只怕是得不偿失。 52 | 53 | ### distcc 54 | 55 | distcc 是一种分布式编译工具,可以把需要被编译的文件发送到其他机器上编译,然后接收编译产物。然而,经过贴吧、贝聊、手Q 等应用的多方实验,发现并不适合 iOS 应用。它的原理是多个客户端共同编译,但是绝大多数文件其实编译时间非常短,并不值得通过网络来回传送,这种方案应该只适合单个文件体量非常大的项目。在我们的项目中,使用 `distcc` **大幅度** 增加了打包时间,大约耗时 1 小时左右。 56 | 57 | ### 定位瓶颈 58 | 59 | 在寻求外部工具无果后,笔者开始尝试着对编译时间直接做优化。为了搞清楚这 40min 究竟是如何花费的,我首先对 `xcodebuild` 的输出结果进行详细分析。 60 | 61 | 使用过 `xcodebuild` 命令的人都会知道,它的输出结果对开发者并不友好,几乎没有可读性,好在还有 `xcpretty` 这个工具可以格式化它: 62 | 63 | ```bash 64 | gem install xcpretty 65 | ``` 66 | 67 | 通过 `gem` 安装后,只要把 `xcodebuild` 的输出结果通过管道传给 `xcpretty` 即可: 68 | 69 | ```shell 70 | xcodebuild -scheme Release ... | xcpretty 71 | ``` 72 | 73 | 下面是[官方文档](https://github.com/supermarin/xcpretty)中的 Demo: 74 | 75 | ![](https://camo.githubusercontent.com/85ddf1950241f3152c5471e3affead89e029d2cf/687474703a2f2f692e696d6775722e636f6d2f4c646d6f7a42532e676966) 76 | 77 | 我只对其中的编译部分感兴趣,所以简单的做下过滤,我们就可以得到格式高度统一的输出: 78 | 79 | ``` 80 | Compiling A.m 81 | Compiling B.m 82 | Compiling ... 83 | Compiling N.m 84 | ``` 85 | 86 | 到了这一步,终于可以做最关键的计算了,我们可以通过设置定时器,计算相邻两行输出之间的间隔,这个间隔就是文件的编译时间。当然,也有类似的辅助工具做好了这个逻辑: 87 | 88 | ```shell 89 | npm install gnomon 90 | ``` 91 | 92 | ![](http://images.bestswifter.com/Speed_up_compile_2.png) 93 | 94 | 简单的做一下排序,就可以看到最耗时的前 200 个文件了,还可以针对文件后缀作区分,计算总耗时等等。经过排查,我们发现一半的编译时间都花在了编译 protobuf 文件上。 95 | 96 | ### 工程设置 97 | 98 | 除了针对超长耗时的文件进行 case-by-case 的分析外,另一种方案是调整工程设置。一般来说,我们的持续集成工具主要是用来给产品经理或者测试人员使用,用来体验功能或者验证 Bug,除非是需要上架 App Store,否则并不需要关心运行时性能。然而在手机上使用的 Release 模式,默认会开启各种优化,这些优化都是牺牲编译性能,换取运行时速度,对于上架的包而言无可厚非,但对于那些 Daily Build 包来说,就显得得不偿失了。 99 | 100 | 因此,加速打包的思路和优化的思路是完全互逆的,我们要做的就是关闭一切可能的优化。这里推荐一篇文章:[关于Xcode编译性能优化的研究工作总结](http://815222418.iteye.com/blog/2317439),可以说相当全面了。 101 | 102 | 经过对其中各个参数的查找资料和尝试关闭,按照提升速度的降序排列,简单整理几个: 103 | 104 | 1. 仅支持 armv7 指令集。手机上的指令集都属于 ARM 系列,从老到新依次是 armv7、armv7s 和 arm64。新的指令集可以兼容旧的机型,但旧的机型不能兼容新的指令集。默认情况下我们打出来的包会有 armv7 和 arm64 两种指令集, 前者负责兜底,而对于支持 arm64 指令集的机型来说,使用最新的指令集可以获得更好的性能。当然代价就是生成两种指令集花费了更多时间。所以在急速打包模式下,我们只生成 armv7 这种最老的指令集,牺牲了运行时性能换取编译速度。 105 | 2. 关闭编译优化。优化的基本原理是牺牲编译时性能,追求运行时性能。常见的优化有编译时删除无用代码,保留调试信息,函数内联等等。因此提升打包速度的秘诀就是反其道而行之,牺牲运行时性能来换取编译时性能。笔者做的两个最主要的优化是把 `Optimize level` 改成 O0,表示不做任何优化。 106 | 3. 使用虚拟磁盘。编译过程中需要大量的磁盘 IO,这主要发生在 `Derived Data` 目录下,因此如果内存足够,可以考虑划出 4G 左右的内存,建一个虚拟磁盘,这样将会把磁盘 IO 优化为 内存 IO,从而提高速度。由于打包机器每次都会重编译,因此并不需要担心重启机器后缓存丢失的问题。 107 | 4. 不生成 dYSM 文件,前文已经介绍过。 108 | 5. 一些其他的选项,参考前面推荐的文章。 109 | 110 | 在以上几个操作中,精简指令集的作用最大,大约可以把编译时间从 45 min 减少到 30min 以内,配合关闭编译优化,可以进一步把打包时间减少到 20min。虚拟磁盘大约可以减少两三分钟的编译时间,dSYM 耗时大约二十秒,其它选项的优化程度更低,大约在几秒左右,没有精确测算。 111 | 112 | 因此,一般来说 **只要精简指令集并关闭优化即可**,有条件的机器可以使用虚拟磁盘,不建议再做其它修改。 113 | 114 | ### 二进制化 115 | 116 | 二进制化主要指的是利静态库代替源码,避免编译。前文已经介绍过如何分析文件的耗时,因此二进制化的收益非常容易计算出来。由于团队分工问题,笔者没有什么二进制化的经验,一般来说这个优化比较适合基础架构组去实施。 117 | 118 | ## 硬件加速 119 | 120 | 以上主要是通过修改软件的方式来加速打包,自从公司申请了 2013 年款 Mac Pro(Xeon-E5 1630 6 核 12 线程,16G 内存,256G SSD 标配,下文简称 Mac Pro)后,不需要修改任何配置,仅仅是简单的迁移打包机器,就可以把打包时间降低到 15 min,配和上一节中的前三条优化,最终的打包时间大概在 10min 以内。 121 | 122 | 在我的黑苹果(i7 7820x 8 核 16 线程,16G 内存,三星 PM 961 512G SSD,下文简称黑苹果)上,即使不开启任何优化,从零开始编译也仅需 5min。如果将 protobuf 文件二进制化,再配合一些工程设置的优化,我不敢想象需要花多长时间,预计在 4min 左右吧,速度提升了大概 11 倍。 123 | 124 | 编译是一个考验多核性能的操作,在我的黑苹果上,编译时可以看到 8 个 CPU 的负载都达到了 100%,因此在一定范围内(比如 10 核以内),提升 CPU 核数远比提升单核主频对编译速度的影响大。至于某些 20 核以上、单核性能较低的 CPU 编译性能如何,希望有经验的读者给予反馈。 125 | 126 | ## 优化点总结 127 | 128 | 下表总结了文章中提到的各种优化手段带来的速度提升,参考原始时间均为 45 min(打包机器:13 寸 MacBook Pro): 129 | 130 | |方案序号| 优化方案| 优化后耗时(min)| 时间减少百分比 | 131 | |:-----:|:-------------:|:-------------:|:-----:| 132 | |1| 不常修改的文件二进制化| 25 | 44.4% | 133 | |2| 精简指令集| 27 | 40% | 134 | |3| 关闭编译优化 | 38 | 15.6% | 135 | |4| 使用 Mac Pro | 15 | 66.7% | 136 | |5| 虚拟磁盘 | 42 | 6.7%| 137 | |6| 公司现行方案(2+3+4+5)| 9 | 80% | 138 | |7| 黑苹果 | 5 | 88.9% | 139 | |8| 终极方案(1+2+3+5+7)| 4(预计)| 91.1%(预计)| 140 | 141 | 严格意义上讲,文章有点标题党了,因为一句话来说就是: 142 | 143 | > 能用硬件解决的问题,就不要用软件解决。 -------------------------------------------------------------------------------- /articles/ios-scrollview-optimize.md: -------------------------------------------------------------------------------- 1 | 自己做了一个模仿简书的小项目练手,主要布局是上面的scrollview有一排label,下面的scrollview有多个UITableView。点击上面的label,下面就可以显示不同的页面。具体效果可以打开简书官方的APP查看,很多新闻软件也是这种效果。 2 | 3 | 一开始的思路就是加载所有ViewController,因为是TableView,所以每个TableView还有自己的DataSource,真机运行了一下,发现占用内存大概是36M左右。于是我开始着手对这种原始的实现方案进行逐步优化,主要是内存占用相关的,以及一些其他的小技巧。 4 | 5 | 项目在Github开源,本文涉及到的相关代码都可以自行查看。项目地址:[MJianshu](https://github.com/Wl201314/MJianshu) 6 | 7 | ![优化前内存](https://user-gold-cdn.xitu.io/2018/2/16/1619ef2841dad7df?w=600&h=587&f=jpeg&s=36182) 8 | 9 | # 优化一:分离DataSource 10 | 11 | 为了轻量化`UIViewController`,同时也为了后期的解耦,我首先把`DataSource`从`UIViewController`中分离出来。思路是在`UIViewController`中引用一个`DataSource`对象,然后把`table`的dataSource属性设置成这个变量而不是自己,用代码描述就是: 12 | 13 | ```swift 14 | // UIViewController.swift 15 | var dataSource = ContentTableDatasource() 16 | 17 | tableView.dataSource = dataSource 18 | ``` 19 | 20 | 把DataSource相关的代理方法都放到`ContentTableDatasource`中去: 21 | 22 | ```swift 23 | extension ContentTableDatasource { 24 | func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 25 | //行数 26 | } 27 | 28 | func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { 29 | //返回cell 30 | } 31 | } 32 | ``` 33 | 34 | 这样做的好处在于,`UIViewController`对具体的数据获取一无所知,它只负责给`table`委派数据源的任务。只要改变数据源,`table`的内容就可以改变。这也符合MVC模式中M和C的解耦。更详细的介绍在objc.io的[Lighter View Controllers](https://www.objc.io/issues/1-view-controllers/lighter-view-controllers/)一文中。 35 | 36 | # 优化二:重用ViewController 37 | 38 | 如果不考虑点击顶部标签的情况,也就是只能滑动`BottomScrollview`,我们可以注意到一个事实。比如当前我在第五页,不管我要滑到其他的任何一页,都必须经过第四页或第六页。也就是说在这种情况下,除了4、5、6这三页的`UIViewController`,其他的都是无用的。一旦我向左滑到第四页,那么第六页的`UIViewController`也是无用的,它可以被重复利用,装载第三页所显示的`UIView` 39 | 40 | 所以,思路就是模仿`UITableView`的重用机制维护一个队列,实现`UIViewController`的重用。每当一个`UIViewController`变成无用的,就放入重用队列。需要`UIViewController`时先从重用队列中找,如果找不到就新建。这样一来内存中最多只会保存三个`UIViewController`的实例,所以占用内存大幅度降低。核心代码如下: 41 | 42 | ```swift 43 | func scrollViewDidScroll(scrollView: UIScrollView) { 44 | // 加载即将出现的页面 45 | loadPage(page) 46 | } 47 | 48 | func loadPage(page: Int) { 49 | guard currentPage != page else { return } //还在当前页面就不用加载了 50 | currentPage = page 51 | 52 | var pagesToLoad = [page - 1, page, page + 1] // 筛选出需要加载的页面,一般只有一个 53 | var vcsToEnqueue: Array = [] // 把用不到的ViewController入队 54 | } 55 | 56 | func addViewControllerForPage(page: Int) { 57 | let vc = dequeueReusableViewController() // 从队列中获取VC 58 | vc.pageID = page 59 | // 添加视图 60 | } 61 | 62 | func dequeueReusableViewController() -> ContentTableController { 63 | if reusableViewControllers.count > 0 { 64 | return reusableViewControllers.removeFirst() // 如果有可以重用的VC就直接返回 65 | } 66 | else { //否则就创建。程序刚开始运行的时候一般需要执行这一步 67 | let vc = ContentTableController() 68 | return vc 69 | } 70 | } 71 | ``` 72 | 73 | 关于重用队列,可以参考这个项目:[Reuse](https://github.com/allenhsu/UIScrollView-Samples/tree/master/Reuse) 74 | 75 | # 优化三:点击Label后的过渡 76 | 77 | 如果从第一页滑动到第三页,那么第二页也会快速闪过。这样会导致用户体验比较差。我的思路是首先在第二页的位置上覆盖一个和第一页一模一样的`UIView`,然后不加动画的切换到第二页。这一瞬间用户感觉不到任何变化。然后再有动画的滑动到第三页。滑动完成之后需要移除这个临时添加的`UIView`,关键步骤如下所示 78 | 79 | ```swift 80 | var maskView = UIView() 81 | maskView = bottomScrollViewController.currentDisplayViewController()?.view // 获取用于遮盖的view 82 | 83 | bottomScrollView.addBottomViewAtIndex(targetPage - 1, view: maskView) // 把view添加到目标页的前一页 84 | buttomScrollView.bottomScroll.setContentOffset(CGPointMake(previousOffSetX, 0), animated: false) //无动画滑动 85 | buttomScrollView.bottomScroll.setContentOffset(CGPointMake(offSetX, 0), animated: true) //有动画滑动 86 | 87 | func scrollViewDidEndScrollingAnimation(scrollView: UIScrollView) { 88 | maskView.removeFromSuperview() // 滑动结束后移除临时视图 89 | } 90 | ``` 91 | 92 | 实际操作远比这个复杂。因为要实现`UIViewController`的重用,所以在`scrollViewDidScroll`这个代理方法中需要时刻监听滑动状态并加载下一页。在点击Label的时候需要禁掉这个特性。 93 | 94 | 总的来说,点击Label的切换和滑动切换页面并不是同一个原理,所以要保证他们之间的逻辑互不干扰 95 | 96 | # 优化四:缓存DataSource 97 | 98 | 最初的逻辑是每个`UIViewController`自己处理自己的`dataSource`,现在因为在`BottomScrollview`中处理`UIViewController`的重用逻辑,所以dataSource的缓存和获取也就一并放在这里处理了。每个`UIViewController`重用时都会根据自己的页数去缓存中查找`dataSource`是否已经存在,如果已经存在的话就直接获取了。关键代码如下所示: 99 | 100 | ```swift 101 | var dataSources: [Int: ContentTableDatasource] = [:] // 键是页数,值是datasource对象 102 | 103 | func bindDataSourceWithViewController(viewController: ContentTableController, page: Int) { 104 | if dataSources[page] == nil { // 如果不存在,就去新建datasource 105 | dataSources[page] = ContentTableDatasource(page: page) 106 | } 107 | viewController.dataSource = dataSources[page] 108 | } 109 | ``` 110 | 111 | 实际上`dataSource`也可以重用,但是这样做并不能节省太多内存,反而会导致`dataSource`中内容的反复切换,有点得不偿失 112 | 113 | # 防掉坑指南 114 | 115 | 最后再谈一谈`UIScrollView`中的一些坑,之前也写过一篇文章——[史上最简单的UIScrollView+Autolayout出坑指南](http://www.jianshu.com/p/f7f1ba67c3ca),主要是关于`UIScrollView`在Autolayout下的布局问题。在后续的开发过程中,还是遇到了一些值得注意的地方。 116 | 117 | 因为`UIScrollView`是可以滑动的,所以对它的布局约束要格外小心。举个例子,一个子视图的`left`已经确定,这时候不管设置它的`right`约束还是`width`约束都可以固定它的位置。但是在`UIScrollView`,千万不要设置`right`约束。否则你可以想象一下,有一个橡皮筋,一端被固定,另一端被拉伸的感觉: 118 | 119 | ```swift 120 | make.right.equalTo(view) // 滑动时视图会被拉伸 121 | make.width.equalTo(viewWidth) // 正确 122 | ``` 123 | 124 | 这样的bug非常难找到,所以我个人的经验是,在对`UIScrollView`的子视图布局时,尽量不要用两端的位置来确定视图自己的长度,而是应该通过自己长度确定另一端的位置。或者,干脆不要依赖于外部视图布局,而是用一个`Container`容器。这也是我在之前的文章中强烈推荐的方法。 125 | 126 | # 成果: 127 | 128 | 内存占用显著减少,只有大约原来的一半。考虑到程序还有其他地方占用内存,可以认为重用机制降低了`Scrollview`超过50%的内存占用: 129 | 130 | ![优化后内存](https://user-gold-cdn.xitu.io/2018/2/16/1619ef2b22abe154?w=600&h=593&f=jpeg&s=35191) 131 | 132 | 不过这么做还是稍有不足,如果数据量比较大,频繁的重用`UIViewController`会导致多次`reloadData()`。切换页面的时候会稍有卡顿的感觉。也许是我哪里考虑欠周,欢迎指正。目前来看,重用机智更适合于呈现静态内容的`UIViewController`。 133 | 134 | **项目地址**:[戳这里](https://github.com/Wl201314/MJianshu),欢迎star。 135 | -------------------------------------------------------------------------------- /articles/javascript-modules.md: -------------------------------------------------------------------------------- 1 | # 前言 2 | 3 | 关于模块化,最直接的表现就是我们写的 `require` 和 `import` 关键字,如果查阅相关资料,就一定会遇到 `CommonJS` 、`CMD` `AMD` 这些名词,以及 `RequireJS`、`SeaJS` 等陌生框架。比如 [SeaJS 的官网](http://seajs.org/docs/) 这样描述自己: “简单友好的模块定义规范,Sea.js 遵循 CMD 规范。自然直观的代码组织方式,依赖的自动加载……” 4 | 5 | 作为前端新手,我表示实在是一脸懵逼,理解不能。按照我一贯的风格,介绍一个东西之前总得解释一下为什么需要这个东西。 6 | 7 | # JavaScript 基础 8 | 9 | 做客户端的同学对 OC 的 `#import "classname"`、Swift 的 Module 以及文件修饰符 和 Java 的 `import package+class` 模式应该都不陌生。我们习惯了引用一个文件就是引用一个类的模式。然而在 JavaScript 这种动态语言中,事情又有一些变化,举个例子说明: 10 | 11 | ```html 12 | 13 | 14 | 15 | 16 | 17 | 18 |

Hello Wrold

19 | 20 | 21 | 22 | ``` 23 | 24 | ```js 25 | // index.js 26 | function onPress() { 27 | var p = document.getElementById('hello'); 28 | p.innerHTML = 'Hello bestswifter'; 29 | } 30 | ``` 31 | 32 | HTML 中的 ` 61 | 62 | 63 | 64 | 65 |

Hello Wrold

66 | 67 | 68 | 69 | ``` 70 | 71 | ```js 72 | // index.js 73 | function onPress() { 74 | var p = document.getElementById('hello'); 75 | p.innerHTML = add(1, 2); 76 | } 77 | ``` 78 | 79 | 可以看到这种写法并不优雅, `index.js` 对别的 JS 文件中的内容并没有控制权,能否调用到 `add` 方法完全取决于使用自己的 HTML 文件有没有正确引用别的 JS 文件。 80 | 81 | # 初步模块化 82 | 83 | 刚刚所说的痛点其实可以分为两种: 84 | 85 | 1. index.js 无法 import,依赖于 HTML 的引用 86 | 2. index.js 中无法对 add 方法的来源做区分,缺少命名空间的概念 87 | 88 | 第一个问题留在后面解答,我们先着手解决第二个问题,比如先把函数放到一个对象中,这样我们可以暴露一个对象,让使用者调用这个对象的多个方法: 89 | 90 | ```js 91 | //index.js 92 | function onPress() { 93 | var p = document.getElementById('hello'); 94 | p.innerHTML = math.add(1, 2); 95 | } 96 | 97 | //math.js 98 | var math = { 99 | base: 0, 100 | add: function(a, b) { 101 | return a + b + base; 102 | }, 103 | }; 104 | ``` 105 | 106 | 可以看到在 `index.js` 中已经可以指定一个简易版的命名空间了(也就是 math)。但目前还有一个小问题,比如 base 这个属性会被暴露给外界,也可以被修改。所以更好的方式是将 `math` 定义在一个闭包里,从而隐藏内部属性: 107 | 108 | ```js 109 | // math.js 110 | var math = (function() { 111 | var base = 0; 112 | return { 113 | add: function(a, b) { 114 | return a + b + base; 115 | }, 116 | }; 117 | })(); 118 | ``` 119 | 120 | 到目前为止,我们实现了模块的定义和使用。不过模块化的一大精髓在于命名空间,也就是说我们希望自己的 `math` 模块不是全局的,而是按需导入,这样一来,即使多个文件暴露同名对象也不会出问题。就像 node.js 中那样,需要暴露的模块定义自己的 export 内容,然后调用方使用 require 方法。 121 | 122 | 其实可以简单模拟一下 node.js 的工作方式,通过增加一个中间层来解决: 首先定义一个全局变量: 123 | 124 | ```js 125 | // global.js 126 | var module = { 127 | exports: {}, // 用来存储所有暴露的内容 128 | }; 129 | ``` 130 | 131 | 然后在 `math.js` 中暴露对象: 132 | 133 | ```js 134 | var math = (function() { 135 | var base = 0; 136 | return { 137 | add: function(a, b) { 138 | return a + b + base; 139 | }, 140 | }; 141 | })(); 142 | 143 | module.exports.math = math; 144 | ``` 145 | 146 | 使用者 `index.js` 现在应该是: 147 | 148 | ```js 149 | var math = module.exports.math; 150 | 151 | function onPress() { 152 | var p = document.getElementById('hello'); 153 | // math 154 | p.innerHTML = math.add(1, 2); 155 | } 156 | ``` 157 | 158 | # 现有模块化方案 159 | 160 | 上述简单的模块化方式有一些小问题。首先,`index.js` 必须严格依赖于 `math.js` 执行,因为只有 `math.js` 执行完才会向全局的 `module.export` 中注册自己。这就要求开发者必须手动管理 js 文件的加载顺序。随着项目越来越大,依赖的维护会变得越来越复杂。 161 | 162 | 其次,由于加载 JS 文件时,浏览器会停止渲染网页,因此我们还需要 JS 文件的异步按需加载。 163 | 164 | 最后一个问题是,之前给出的简化版模块化方案并没有解决模块的命名空间,相同的导出依旧会替换掉之前的内容,而解决方案则是维护一个 “文件路径 <--> 导出内容” 的表,并且根据文件路径加载。 165 | 166 | 基于上述需求,市场上出现了很多套模块化方案。为啥会有多套标准呢,实际上还是由前端的特性导致的。由于缺乏一个统一的标准,所以很多情况下大家做事的时候都是靠约定,就比如上述的 export 和 require。如果代码的提供者把导出内容存储在 `module.exports` 里,而使用者读取的是 `module.export`,那自然是徒劳的。不仅如此,各个规范的实现方式、使用场景也不尽相同。 167 | 168 | ## CommonJS 169 | 170 | 比较知名的规范有 CommonJS、AMD 和 CMD。而知名框架 Node.js、RequireJS 和 Seajs 分别实现了上述规范。 171 | 172 | 最早的规范是 CommonJS,Node.js 使用了这一规范。这一规范和我们之前的做法比较类似,是同步加载 JS 脚本。这么做在服务端毫无问题,因为文件都存在磁盘上,然而浏览器的特性决定了 JS 脚本需要异步加载,否则就会失去响应,因此 CommonJS 规范无法直接在浏览器中使用。 173 | 174 | ## AMD 175 | 176 | 浏览器端著名的模块管理工具 Require.js 的做法是异步加载,通过 Webworker 的 `importScripts(url);` 函数加载 JS 脚本,然后执行当初注册的回调。Require.js 的写法是: 177 | 178 | ```js 179 | require(['myModule1', 'myModule2'], function (m1, m2){ 180 | // 主回调逻辑 181 | m1.printName(); 182 | m2.printName(); 183 | }); 184 | ``` 185 | 186 | 由于这两个模块是异步下载,因此哪个模块先被下载、执行并不确定,但可以肯定的是主回调一定在所有依赖都被加载完成后才执行。 187 | 188 | Require.js 的这种写法也被称为前置加载,在写主逻辑之前必须指定所有的依赖,同时这些依赖也会立刻被异步加载。 189 | 190 | 由 Require.js 引申出来的规范被称为 AMD(Asynchronous Module Definition)。 191 | 192 | ## CMD 193 | 194 | 另一种优秀的模块管理工具是 Sea.js,它的写法是: 195 | 196 | ```js 197 | define(function(require, exports, module) { 198 | var foo = require('foo'); // 同步 199 | foo.add(1, 2); 200 | ... 201 | require.async('math', function(math) { // 异步 202 | math.add(1, 2); 203 | }); 204 | }); 205 | ``` 206 | 207 | Sea.js 也被称为就近加载,从它的写法上可以很明显的看到和 Require.js 的不同。我们可以在需要用到依赖的时候才申明。 208 | 209 | Sea.js 遇到依赖后只会去下载 JS 文件,并不会执行,而是等到所有被依赖的 JS 脚本都下载完以后,才从头开始执行主逻辑。因此被依赖模块的执行顺序和书写顺序完全一致。 210 | 211 | 由 Sea.js 引申出来的规范被称为 CMD(Common Module Definition)。 212 | 213 | # ES 6 模块化 214 | 215 | 在 ES6 中,我们使用 `export` 关键字来导出模块,使用 `import` 关键字引用模块。需要说明的是,ES 6 的这套标准和目前的标准没有直接关系,目前也很少有 JS 引擎能直接支持。因此 Babel 的做法实际上是将不被支持的 `import` 翻译成目前已被支持的 `require`。 216 | 217 | 尽管目前使用 `import` 和 `require` 的区别不大(本质上是一回事),但依然强烈推荐使用 `import` 关键字,因为一旦 JS 引擎能够解析 ES 6 的 `import` 关键字,整个实现方式就会和目前发生比较大的变化。如果目前就开始使用 `import` 关键字,将来代码的改动会非常小。 218 | 219 | # 参考 220 | 221 | 以上内容大部分都不是我的思考结果,我只是对已有的文章做了一下实际操作和归纳总结,感谢各位前辈的优秀文章: 222 | 223 | 1. [Can I access variables from another file?](http://stackoverflow.com/questions/3244361/can-i-access-variables-from-another-file) 224 | 2. [浅谈 JavaScript 模块化编程](https://segmentfault.com/a/1190000000492678) 225 | 3. [前端模块化](http://www.cnblogs.com/dolphinX/p/4381855.html) 226 | 4. [详解JavaScript模块化开发](https://segmentfault.com/a/1190000000733959#articleHeader6) 227 | 5. [requireJS实现原理研究](http://www.html-js.com/article/AngularJs-requireJS-the-realization-principle-of-1) 228 | 6. [Javascript模块化编程(一):模块的写法](http://www.ruanyifeng.com/blog/2012/10/javascript_module.html) 229 | 7. [Javascript模块化编程(二):AMD规范](http://www.ruanyifeng.com/blog/2012/10/asynchronous_module_definition.html) 230 | 8. [浏览器加载 CommonJS 模块的原理与实现](http://www.ruanyifeng.com/blog/2015/05/commonjs-in-browser.html) 231 | 9. [Node中没搞明白require和import,你会被坑的很惨](http://imweb.io/topic/582293894067ce9726778be9) 232 | -------------------------------------------------------------------------------- /articles/jian_zhi_ru_men.md: -------------------------------------------------------------------------------- 1 | # 非专业减脂入门指北 2 | 3 | ## 前言 4 | 5 | 作为非专业人士,尝试总结一下减脂的理论原理。因为正确的理论基础可以指导减脂,而错误的思路则会导致事倍功半,甚至损伤身体。 6 | 7 | 本文共分为三个模块: 8 | 9 | * 原理:介绍什么是减脂,以及减脂的科学原理。这部分主要是理论知识铺垫,为后面分析各种减脂行为提供理论支撑。 10 | * 实践:介绍我的减脂方式,主要是一些实践,以及相关的思考和理论支持。 11 | * 误区:介绍大多数读者容易犯的错误。这部分主要是从原理角度,分析各种常见的错误减脂行为,避免事倍功半。 12 | 13 | > 注:不保证所有理论百分百准确,但有关减脂的理论均由个人亲身验证过。 14 | 15 | ## 减脂原理 16 | 17 | ### 什么是减脂 18 | 19 | 首先要注意的是,我一直在强调:“减脂”,而非减肥。减脂的含义是,降低身体的脂肪含量,而后者在绝大多数人看来更像是:“减重”,即减小体重秤上的数字。 20 | 21 | 我们都知道相同的身高和体重,有些人虚胖,有些人健壮,因此传统的 BMI 指数(体重 / 身高 ^ 2)并不能准确的描述一个人的胖瘦程度和身材好坏。不管是为了形体的健美,还是身体的健康,人们所追求和欣赏的好身材,其实指的都是体脂率低,而非体重低。 22 | 23 | ![][image-1] 24 | 25 | > 几乎相同的身高、体重、BMI,不会有人觉得两个人身材一样 26 | 27 | 在减脂的过程中,体脂是最应该被关注的数值,没有之一。下文将会详细介绍,错误的减脂方式,是如何减轻体重,但是增加体脂;最终导致减脂失败并影响健康的。 28 | 29 | 体脂的测量,在医院或者健身房,会有更加准确、专业的器材。家用的体脂称,通过光脚站立,测量两只脚之间的电流数据从而获得近似的结论。这个数字的绝对值不够准确,但是相对趋势还是值得参考的。考虑到并非每个人都有条件在专业机构定期测量,因此个人建议通过家中的体脂称,纵向对比不同时间的体脂,从而判断减脂效果。 30 | 31 | ### 唯一的减脂公式 32 | 33 | 其实,减脂是一件非常简单的事情,简单到它只有一个公式可言。就像热力学第二定律告诉我们:“永动机是不存在的” 一样,这个公式也告诉我们,**不可能** 有违背这个公式的减脂方式。 34 | 35 | 这个公式就是: 36 | 37 | > 脂肪消耗 = (能量消耗 - 能量摄入) / 脂肪的能量密度 38 | 39 | 由于每公斤脂肪可以提供 7700 大卡的能量,所以每当我们的身体至少存在 7700 大卡的能量缺口,才有可能最多消耗一千克的脂肪。 40 | 41 | 任何妄图违反这个公式的减脂尝试,本质上和尝试制造永动机没有区别。因为这是违反科学定律的无用尝试。比如所谓的出汗减肥、或者震动带减肥,就已经可以忽略了。因为这些方式并没有增加能量消耗或者降低能量摄入。 42 | 43 | 44 | ### 能量消耗 45 | ![][image-2] 46 | 47 | 再次强调,减脂的唯一前提是:**存在能量差**。由于能量的摄入,唯一来源就是饮食,相对来说比较可控。所以这里主要分析一下人体消耗能量的途径。 48 | 49 | 人体有很多种消耗能量的途径,其中主要有三种。 50 | 51 | * 基础代谢:维持人体生存的最基本的能量消耗。也就是我们在床上躺一天,一动不动,为了维持呼吸、基本新陈代谢和大脑活动所需要的能量。 52 | * 体力活动:包括走路、跑步、体育运动、体力劳动等。 53 | * 食物热效应:为了消化食物所需要消耗的能量。 54 | 55 | #### 基础代谢 56 | 其中,正常人的基础代谢(BMR), 是可以通过公式计算的: 57 | 58 | * 男:BMR(Kcal)=66.5+13.8x体重(kg)+5.0x身高(cm)-6.8x年龄(岁) 59 | * 女:BMR(Kcal)=665.1+9.6x体重(kg)+1.8x身高(cm)-4.7x年龄(岁) 60 | 61 | 举个例子,一个 25 岁,身高 180 厘米, 体重 70 千克的普通成年男性,每天的基础代谢约为 1762.5 千卡每天。 62 | 63 | #### 体力活动 64 | 相比之下,常见的体育运动,每小时的能量消耗为: 65 | 66 | * 骑自行车:184 千卡 67 | * 跳绳:448 千卡 68 | * 快跑:700 千卡 69 | 70 | 以我的个人经验,绝大多数普通人,每天进行一次运动,能消耗的能量约为 300 - 500 千卡。即坚持 15.4 - 25.7 天可以消耗最多一千克脂肪。 71 | 72 | #### 食物热效应 73 | 74 | 人体可以通过消化糖、脂肪、蛋白质这三种营养物质来获得能量。但不同类型的物质,消化难度不同,消化它们所需要的能量也不同。 75 | 76 | 糖类,作为最容易消化的营养物质,人体几乎不需要付出任何代价就可以直接利用。但如果摄入 100 千卡的蛋白质,大约需要消耗 20-30 千卡的能量, 也就是实际只能利用 70-80 千卡。 77 | 78 | 因此,摄入相同多的能量,实际能利用的,也会不一样。 79 | 80 | ### 正确理解能量差 81 | 82 | 个人认为,减脂最困难的地方在于,即使我们创造了能量差,但这部分能量缺口并非完全由脂肪提供。 83 | 84 | 在减脂过程中,由于能量摄入不足,人体内的糖类物质最先被消耗完。能量缺口,由蛋白质的分解,和脂肪的分解共同完成。蛋白质的分解,除了导致人体免疫能力下降, 最直观的感受就是肌肉的分解和损失。 85 | 86 | 一个很残酷的事实是,减脂过程中,肌肉绝对数量的减少是不可能避免的。但如果以正确的方式进行减脂,就可以让肌肉的损失远小于脂肪的损失,从而达到提升体脂率的效果。而错误的减脂方式,可能会导致肌肉损失大于脂肪损失,虽然体重降低,但实际体脂率增加。 87 | 88 | ## 个人实践 89 | 90 | 不管我们是否愿意,都需要承认:自制力是一种及其有限,而且容易消耗的宝贵资源。很多时候,减脂失败,是因为长期看不到进步而失望,或者总是控制不住自己,回归到不健康的生活和饮食习惯。 91 | 92 | 所以,如果只能总结一条经验,我的经验是: 93 | 94 | > 尽量利用人性,而不是对抗它 95 | 96 | ### 记录 97 | 98 | 我个人使用 “薄荷 App”,配合薄荷的体脂称,每天记录饮食数据和体重数据。 99 | 100 | ![][image-3] 101 | 102 | 软件还支持体重、体脂率、骨骼肌率等数据的折线统计。 103 | 104 | 虽然一百多的体脂称,不可能绝对准确。但是和自己做对比,还是具有很不错的参考价值。尤其是体重,在开始减脂后,基本上每隔一两天,都会看到体重的降低。这就利用了人性中,**追逐短期利益**的特点,为自己坚持下去提供了更多的动力。 105 | 106 | ### 饮食 107 | 108 | 在饮食方面我比较注意,因为减脂的核心就是制造热量差。所以任何高热量的食物,比如含糖饮料、油炸食品、各种零食等等。 109 | 110 | 一般早餐我会选择牛奶和全麦面包。这里的一个建议是,牛奶选择全脂而不是脱脂,因为健康饮食时,脂肪的摄入量是得到控制的,并不需要以牺牲口感为代价,去减少这一点点脂肪的摄入。早餐热量大约是 300 千卡 111 | 112 | 午餐和晚餐我会选择公司的健身餐。热量在 400 - 600 千卡左右。 113 | 114 | 这样一天下来,摄入热量大约是 1200 大卡左右,略低于我的基础代谢。 115 | 116 | 一般每周末,我会选择一顿午餐,适当解除摄入量的控制,这一餐一般也称为欺骗餐。有一种说法是每周一次的欺骗餐,会让身体以为我们的能量摄入足够,从而不会进入饥荒模式,减少基础代谢和脂肪分解。但相比于这种说法,我更愿意把这一餐,当做一周下来的激励。 117 | 118 | > 注意,这一餐可以吃得多,但必须保证吃得健康。比如吃点火锅、米饭就不错,但并不是吃高油、高糖食物的借口。 119 | 120 | ### 运动 121 | 122 | 除了早晚步行上班外,我会选择 HIIT 和力量训练。严格意义上来说,这两种运动,都属于有氧运动。 123 | 124 | 有氧运动的好处在于,它可以提高人体的基础代谢(传说中的躺着就能减肥是真的)。尤其是大重量的力量训练,可以促进生长激素、胰高血糖素、睾酮等激素的分泌,这些激素有利于肌肉合成和促进脂肪分解。 125 | 126 | HIIT 的全称是高强度间歇性训练,它的优势在于,除了运动中的能量消耗,还会造成**运动后过量氧耗**(EPOC),俗称:** 氧债 **,又是一种正确的躺着也能瘦的姿势。 127 | 128 | 力量训练方面,没有太多要求。初期我选择练习三角肌和引体向上,纯粹是一种个人喜好吧。建议是每次练习 4-6 组,以便最大程度的刺激肌肉生长。具体的训练计划可以参考 Keep 课程。同时我会选择记录每次的重量和组数,理由同上,不断的进步会带来愉悦感,从而更容易坚持下去。 129 | 130 | 由于正常的力量训练必然会带来肌肉的撕裂,在减脂期,尤其需要注意保护肌肉。所以每次力量训练结束后,我都会摄入一勺乳清蛋白粉,尽量保护肌肉。 131 | 132 | 有氧训练方面,我从不跑步,也很少做椭圆机等练习。一方面和个人喜好有关,另一方面长期有氧训练,有一种说法称,会减少瘦素、促进肌肉分解和基础代谢降低。具体真实性有待专业验证,但我嫌累,就不勉强自己了。 133 | 134 | ### 睡眠 135 | 136 | 不管是减脂还是增肌,睡眠都是非常重要的一部分,不能忽略。不充足的睡眠会导致瘦素、生长激素等有益激素的分泌减少,以及皮质醇这种有害激素的分泌增加。 137 | 138 | 睡眠过程中,人体最大的供能来源是脂肪,因此睡觉也能减肥,也是有道理的。 139 | 140 | 同时,肌肉的生长和恢复,也离不开充足的睡眠。 141 | 142 | ## 常见问题 143 | 144 | 如果有问题可以联系我,有代表性的都会补充在这里。 145 | 146 | ### 节食可以减脂么? 147 | 148 | 不能。 149 | 150 | 节食减肥,会导致人天进入类似于低电量模式。体内瘦素含量降低,基础代谢降低。同时刺激食欲,身体倾向于存储脂肪,一旦恢复饮食,又会复胖。科学研究证实,节食只会导致体重增加。 151 | 152 | 这里简单说下,为什么节食减肥前期看起来见效非常快。我们知道人体内,最高效的供能物质是葡萄糖,它们存储在肝脏和肌肉中。前者称为肝糖原,后缀被被称为肌糖原。一般人体内,合计可以存储 500 克以上的糖原。 153 | 154 | 节食后,人体失去能量补充,这些糖原会首先被使用完成。由于人体在存储糖原时,还会按照 1:3 至 1:4 的比例存储水分。所以合计会损失 2.5 公斤左右的体重。这就是节食前期导致体重快速降低的原因。一旦恢复正常饮食,随着糖原的补充,体重同样会快速上涨。 155 | 156 | 更多关于节食减肥的危害,可以参考这篇文章:[节食减肥成功后,是它让你的体重反弹!][1] 157 | 158 | ### 各种减脂补剂有用么? 159 | 160 | 这里举几个常见的例子,说说我的看法: 161 | 162 | * 左旋肉碱:没用,原理是正确的,但人体内不缺这个 163 | * 共轭亚油酸:可能有用,原理是正确的, 有部分研究支持,但需要长期服用。 164 | * 奥利司他:这是国家药监局**唯一**批准的减肥药,主要原理是抑制人体小肠消化和吸收脂肪 165 | * 各种网红减肥茶、减肥药:没用,伤身体,绝对不要吃 166 | * 乳清蛋白:有一定的用,是一种优秀的能量来源,不容易胖,可以保护肌肉 167 | * 增肌粉:没用,碳水含量高,不适合减脂期间食用 168 | * 酵素:没用,卖概念的 169 | 170 | ## 参考资料: 171 | 172 | [瘦素已经“过时”了,胰岛素才是减脂的新宠][2] 173 | [ 减脂有哪些不为人知的冷知识? ][3] 174 | [节食减肥成功后,是它让你的体重反弹!][4] 175 | 176 | [1]: https://zhuanlan.zhihu.com/p/24751128 177 | [2]: https://zhuanlan.zhihu.com/p/63979742 178 | [3]: https://www.zhihu.com/question/289060470/answer/1265871043 "减脂有哪些不为人知的冷知识?" 179 | [4]: https://zhuanlan.zhihu.com/p/24751128 180 | 181 | [image-1]: ../pictures/jianzhi/bmi.jpg 182 | [image-2]: ../pictures/jianzhi/energy.png 183 | [image-3]: ../pictures/jianzhi/compare.jpg -------------------------------------------------------------------------------- /articles/main-thread-ui.md: -------------------------------------------------------------------------------- 1 | 从最初开始学习 iOS 的时候,我们就被告知 UI 操作一定要放在主线程进行。这是因为 UIKit 的方法不是线程安全的,保证线程安全需要极大的开销。那么问题来了,在主线程中进行 UI 操作一定是安全的么? 2 | 3 | > 显然,答案是否定的! 4 | 5 | 在苹果的 `MapKit` 框架中,有一个叫做 `addOverlay` 的方法,它在底层实现的时候,不仅仅要求代码执行在主线程上,还要求执行在 GCD 的主队列上。这是一个极罕见的问题,但已经有人在使用 ReactiveCocoa 时踩到了坑,并提交了 [issue](https://github.com/ReactiveCocoa/ReactiveCocoa/issues/2635#issuecomment-170215083)。 6 | 7 | 苹果的 Developer Technology Support 承认这是一个 bug。不管这是 bug 还是历史遗留设计,也不管是不是在钻牛角尖,为了避免再次掉进同样的坑,我认为都有必要分析一下问题发生的原因和解决方案。 8 | 9 | ## GCD 知识复习 10 | 11 | 在 GCD 中,使用 `dispatch_get_main_queue()` 函数可以获取主队列。调用 `dispatch_sync()` 方法会把任务同步提交到指定的队列。 12 | 13 | 注意一下队列和线程的区别,他们之间并没有“拥有关系(ownership)”,当我们同步的提交一个任务时,首先会阻塞当前队列,然后等到下一次 runloop 时再在合适的线程中执行 block。 14 | 15 | 在执行 block 之前,首先会寻找合适的线程来执行block,然后阻塞这个线程,直到 block 执行完毕。寻找线程的规则是: **任何提交到主队列的 block 都会在主线程中执行**,在不违背此规则的前提下,文档还告诉我们系统会自动进行优化,**尽可能的在当前线程执行 block**。 16 | 17 | 顺便补充一句,GCD 死锁的充分条件是:“向当前队列重复同步提交 block”。从原理来看,死锁的原因是提交的 block 阻塞了队列,而队列阻塞后永远无法执行完 `dispatch_sync()`,可见这里完全和代码所在的线程无关。 18 | 19 | 另一个例子也可以证明这一点,在主线程中向一个串行队列同步的派发 block,根据上文选择线程的原则,block 将在主线程中执行,但同样不会导致死锁: 20 | 21 | ```objc 22 | dispatch_queue_t queue = dispatch_queue_create("com.kt.deadlock", nil); 23 | dispatch_sync(queue, ^{ 24 | NSLog(@"current thread = %@", [NSThread currentThread]); 25 | }); 26 | // 输出结果: 27 | // current thread = {number = 1, name = main} 28 | ``` 29 | 30 | ## 原因分析 31 | 32 | 啰嗦了这么多,回到之前描述的 bug 中来。现在我们知道,即使是在主线程中执行的代码,也很可能不是运行在主队列中(反之则必然)。如果我们在子队列中调用 `MapKit` 的 `addOverlay` 方法,即使当前处于主线程,也会导致 bug 的产生,因为这个方法的底层实现判断的是主队列而非主线程。 33 | 34 | 更进一步的思考,有时候为了保证 UI 操作在主线程运行,如果有一个函数可以用来创建新的 `UILabel`,为了确保线程安全,代码可能是这样: 35 | 36 | ```objc 37 | - (UILabel *)labelWithText: (NSString *)text { 38 | __block UILabel *theLabel; 39 | if ([NSThread isMainThread]) { 40 | theLabel = [[UILabel alloc] init]; 41 | [theLabel setText:text]; 42 | } 43 | else { 44 | dispatch_sync(dispatch_get_main_queue(), ^{ 45 | theLabel = [[UILabel alloc] init]; 46 | [theLabel setText:text]; 47 | }); 48 | } 49 | return theLabel; 50 | } 51 | ``` 52 | 53 | 从严格意义上来讲,这样的写法不是 100% 安全的,因为我们无法得知相关的系统方法是否存在上述 Bug。 54 | 55 | ## 解决方案 56 | 57 | 由于提交到主队列的 block 一定在主线程运行,并且在 GCD 中线程切换通常都是由指定某个队列引起的,我们可以做一个更加严格的判断,即用判断是否处于主队列来代替是否处于主线程。 58 | 59 | GCD 没有提供 API 来进行相应的判断,但我们可以另辟蹊径,利用 `dispatch_queue_set_specific` 和 `dispatch_get_specific` 这一组方法为主队列打上标记: 60 | 61 | ```objc 62 | + (BOOL)isMainQueue { 63 | static const void* mainQueueKey = @"mainQueue"; 64 | static void* mainQueueContext = @"mainQueue"; 65 | 66 | static dispatch_once_t onceToken; 67 | dispatch_once(&onceToken, ^{ 68 | dispatch_queue_set_specific(dispatch_get_main_queue(), mainQueueKey, mainQueueContext, nil); 69 | }); 70 | 71 | return dispatch_get_specific(mainQueueKey) == mainQueueContext; 72 | } 73 | ``` 74 | 75 | 用 `isMainQueue` 方法代替 `[NSThread isMainThread]` 即可获得更好的安全性。 76 | 77 | ## 参考资料 78 | 79 | 1. [Community bug reports about MapKit](http://www.openradar.me/24025596) 80 | 2. [GCD's Main Queue vs Main Thread](http://blog.benjamin-encz.de/post/main-queue-vs-main-thread/) 81 | 3. [ReactiveCocoa 中遇到类似的坑](https://github.com/ReactiveCocoa/ReactiveCocoa/issues/2635#issuecomment-170215083) 82 | 4. [Why can't we use a dispatch_sync on the current queue?](http://stackoverflow.com/questions/10984732/why-cant-we-use-a-dispatch-sync-on-the-current-queue) -------------------------------------------------------------------------------- /articles/number-more-than-half.md: -------------------------------------------------------------------------------- 1 | # 问题描述 2 | 3 | 数组中有一个数字出现次数超过数组长度的一半,请找出这个数字。例如输入一个长度为9的数组`{1,2,3,2,2,2,5,4,2}`。 4 | 5 | 由于数字2在数组中出现了5次,超过数组长度一半,因此输出2 6 | 7 | # 问题分析 8 | 9 | 首先想到的是,可以维护一个数据结构用来存储每个数字对应的出现次数。没遇到一个新的数字就去找这个数字是否出现过,如果出现过就加1.这种思路最简单,但是时间复杂度是O(n^2)。 10 | 11 | 稍做优化,可以把数组排序,然后中位数一定是要找的数。这样的时间复杂度是O(nlogn) 12 | 13 | 还可以再试着优化,第一种方法之所以需要O(n^2)的时间复杂度是因为找一个数是否已经出现过,需要O(n)的时间复杂度,拿能不能用Hash呢。很不幸的是也不行,因为不知道数字的取值范围,无法构造hash函数。否则的话,也不用hash,直接基数排序就解决问题了。 14 | 15 | 似乎最快的算法就是排序的O(nlogn)了。如果还想优化,关键在于利用“**出现次数超过一半**”这个条件的数学性质。 16 | 17 | # 理论基础 18 | 19 | 为了后面的解释,我们首先定义: 20 | 21 | > 对于数组A,出现次数超过一半的数记为H(A) 22 | 23 | 还是以之前的数组A`{1,2,3,2,2,2,5,4,2}`为例,首先有H(A) = 2,我们观察一下这个2的特点: 24 | 25 | 如果把前两个数`1`和`2`去掉,剩下的数组A'是`{3,2,2,2,5,4,2}`,H(A')依然为2! 26 | 27 | 更进一步,我们可以这么想,既然前两个数去掉之后不影响找出现次数超过数组长度的一半的数字,那么任意去掉相邻的两个数字也不影响呢。 28 | 29 | 也就是说对于任意数组A和A去掉两个相邻数字的子数组A'是否都有:`H(A) = H(A')` 30 | 31 | 很不幸,这个结论也不成立,因为如果在刚刚的数组A中去掉连续的两个2,就不对了。但是只要稍稍把结论修改一下就是成立的: 32 | 33 | > 对于任意数组A,去掉A中任意两个相邻但不相等的数,得到数组A',总有`H(A) = H(A')` 34 | 35 | 结论很好证明:设H(A) = p,去掉的两个数中最多有一个p,由于p原来出现的次数大于n/2,现在p-1自然一定大于(n-2)/2。所以`H(A')= p`。 36 | 37 | # 编程实现 38 | 39 | 有了刚刚的结论,再看这个数组就简单多了。只要把数组从头到尾遍历一遍,剔除相邻的不同的数就可以。比如数组中有一段是`{1,2,3,4}`,可以预见到,有可能是`{2,3}`先被比较,然后被剔除,接着是`{1,4}`变成相邻的,接着被剔除。 40 | 41 | 为了避免反复循环,在一个循环里解决问题,自然而然的想到了一个数据结构——“栈”。 42 | 43 | 只要对于数组中的每一个元素,如果栈为空或这个元素与栈顶元素相同,则这个元素入栈,否则栈顶元素出栈即可。这样一来,不相同的数即使不相邻,迟早也会一起出栈(可以把用来和栈顶做比较元素想象为先入栈再出栈)。 44 | 45 | 所以这样一来,代码会非常简洁优雅: 46 | 47 | ```C++ 48 | int findNumber(std::vector v){ 49 | std::stack stack = std::stack(); 50 | for (int i = 0; i < v.size(); ++i) { 51 | if (stack.empty() || stack.top() == v[i]) { 52 | stack.push(v[i]); 53 | } 54 | else{ 55 | stack.pop(); 56 | } 57 | } 58 | return stack.top(); 59 | } 60 | ``` 61 | 62 | # 正确性证明 63 | 64 | 对于H(A)这个数来说,它想要出栈的唯一可能是遇到一个和它不同的数(之前说过,如果把H(A)和与它不同栈顶元素比较,可以理解为H(A)先进栈再出栈)。 65 | 66 | 而在数组A中,剩下所有的数出现次数的和一定小于H(A)出现的次数。所以最终的栈里一定只剩下若干个H(A)。 67 | 68 | # 拓展1——判断是否存在这样的数 69 | 70 | 之前我们都是基于题干所说的,出现次数超过一半的数是存在的,这一前提进行分析。如果把题干改为: 71 | > 判断出现次数超过一半的数是否存在,如果存在则找出这个数 72 | 73 | 这时候,我们依然用同样的方法先得到之前所说的栈。这个栈如果为空,则H(A)不存在(因为已经证明H(A)存在的话栈中的数都等于H(A))。如果不为空,也只有可能有一种数字,不妨记为x。 74 | 75 | x不一定是要找的H(A),因为H(A)不一定存在。我们只能说如果H(A)存在的话,`x = H(A)`。所以只要重新遍历数组,看看x的出现次数是否超过n/2即可。 76 | 77 | # 拓展2——不少于一半 78 | 79 | 看到一个非常有意思的问题,即把题干改为找到出现次数不少于一半的数。其实非常简单的方法是判断数组的第一个元素是否满足要求。如果不满足的话,剩下的n-1个数构成数组A',则H(A')就是要找的数。相当于划归为了之前已经解决的问题。 -------------------------------------------------------------------------------- /articles/objc-load-initialize.md: -------------------------------------------------------------------------------- 1 | OC 中有两个特殊的类方法,分别是 `load` 和 `initialize`。本文总结一下这两个方法的区别于联系、使用场景和注意事项。Demo 可以在我的 Github 上找到——[load 和 initialize](https://github.com/bestswifter/MySampleCode/tree/master/load),如果觉得有帮助还望点个star以示支持,总结在文章末尾。 2 | 3 | # load 4 | 5 | 顾名思义,`load`方法在这个文件被程序装载时调用。只要是在Compile Sources中出现的文件总是会被装载,这与这个类是否被用到无关,因此`load`方法总是在`main`函数之前调用。 6 | 7 | ### 调用规则 8 | 9 | 如果一个类实现了`load`方法,在调用这个方法前会首先调用父类的`load`方法。而且这个过程是自动完成的,并不需要我们手动实现: 10 | 11 | ```objc 12 | // In Parent.m 13 | + (void)load { 14 | NSLog(@"Load Class Parent"); 15 | } 16 | 17 | // In Child.m,继承自Parent 18 | + (void)load { 19 | NSLog(@"Load Class Child"); 20 | } 21 | 22 | // In Child+load.m,Child类的分类 23 | + (void)load { 24 | NSLog(@"Load Class Child+load"); 25 | } 26 | 27 | // 运行结果: 28 | /* 29 | 2016-02-01 21:28:14.379 load[11789:1435378] Load Class Parent 30 | 2016-02-01 21:28:14.380 load[11789:1435378] Load Class Child 31 | 2016-02-01 22:28:14.381 load[11789:1435378] Load Class Child+load 32 | */ 33 | ``` 34 | 35 | 如果一个类没有实现`load`方法,那么就不会调用它父类的`load`方法,这一点与正常的类继承和方法调用不一样,需要额外注意一下。 36 | 37 | ### 执行顺序 38 | 39 | `load`方法调用时,系统处于脆弱状态,如果调用别的类的方法,且该方法依赖于那个类的`load`方法进行初始化设置,那么必须确保那个类的`load`方法已经调用了,比如demo中的这段代码,打印出的字符串就为`null`: 40 | 41 | ```objc 42 | // In Child.m 43 | + (void)load { 44 | NSLog(@"Load Class Child"); 45 | 46 | Other *other = [Other new]; 47 | [other originalFunc]; 48 | 49 | // 如果不先调用other的load,下面这行代码就无效,打印出null 50 | [Other printName]; 51 | } 52 | ``` 53 | 54 | `load`方法的调用顺序其实有迹可循,我们看到demo的项目设置如下: 55 | 56 | ![执行顺序](http://images.bestswifter.com/load/order.png) 57 | 58 | 在Compile Sources中,文件的排放顺序就是其装载顺序,自然也就是`load`方法调用的顺序。这一点也证明了`load`方法中会自动调用父类的方法,因为在demo的输出结果中,`Parent`的`load`方法先于`Child`调用,而它的装载顺序其实在`Child`之后。 59 | 60 | 虽然在这种简单情况下我们可以辨别出各个类的`load`方法调用的顺序,但**永远不要**依赖这个顺序完成你的代码逻辑。一方面,这在后期的开发中极容易导致错误,另一方面,你实际上并不需要这么做。 61 | 62 | ### 使用场景 63 | 64 | 由于调用`load`方法时的环境很不安全,我们应该尽量减少`load`方法的逻辑。另一个原因是`load`方法是线程安全的,它内部使用了锁,所以我们应该避免线程阻塞在`load`方法中。 65 | 66 | 一个常见的使用场景是在`load`方法中实现Method Swizzle: 67 | 68 | ```objc 69 | // In Other.m 70 | + (void)load { 71 | Method originalFunc = class_getInstanceMethod([self class], @selector(originalFunc)); 72 | Method swizzledFunc = class_getInstanceMethod([self class], @selector(swizzledFunc)); 73 | 74 | method_exchangeImplementations(originalFunc, swizzledFunc); 75 | } 76 | ``` 77 | 78 | 在`Child`类的`load`方法中,由于还没调用`Other`的`load`方法,所以输出结果是"Original Output",而在main函数中,输出结果自然就变成了"Swizzled Output"。 79 | 80 | 一般来说,除了Method Swizzle,别的逻辑都不应该放在`load`方法中实现。 81 | 82 | # initialize 83 | 84 | 这个方法在第一次给某个类发送消息时调用(比如实例化一个对象),并且只会调用一次。`initialize`方法实际上是一种惰性调用,也就是说如果一个类一直没被用到,那它的`initialize`方法也不会被调用,这一点有利于节约资源。 85 | 86 | ### 调用规则 87 | 88 | 与`load`方法类似的是,在`initialize`方法内部也会调用父类的方法,而且不需要我们显示的写出来。与`load`方法不同之处在于,即使子类没有实现`initialize`方法,也会调用父类的方法,这会导致一个很严重的问题: 89 | 90 | ```objc 91 | // In Parent.m 92 | + (void)initialize { 93 | NSLog(@"Initialize Parent, caller Class %@", [self class]); 94 | } 95 | 96 | // In Child.m 97 | // 注释掉initialize方法 98 | 99 | // In main.m 100 | Child *child = [Child new]; 101 | ``` 102 | 103 | 运行后发现父类的`initialize`方法竟然调用了两次: 104 | 105 | ```objc 106 | 2016-02-01 22:57:02.985 load[12772:1509345] Initialize Parent, caller Class Parent 107 | 2016-02-01 22:57:02.985 load[12772:1509345] Initialize Parent, caller Class Child 108 | ``` 109 | 110 | 这是因为在创建子类对象时,首先要创建父类对象,所以会调用一次父类的`initialize`方法,然后创建子类时,尽管自己没有实现`initialize`方法,但还是会调用到父类的方法。 111 | 112 | 虽然`initialize`方法对一个类而言只会调用一次,但这里由于出现了两个类,所以调用两次符合规则,但不符合我们的需求。正确使用`initialize`方法的姿势如下: 113 | 114 | ```objc 115 | // In Parent.m 116 | + (void)initialize { 117 | if (self == [Parent class]) { 118 | NSLog(@"Initialize Parent, caller Class %@", [self class]); 119 | } 120 | } 121 | ``` 122 | 123 | 加上判断后,就不会因为子类而调用到自己的`initialize`方法了。 124 | 125 | ### 使用场景 126 | 127 | `initialize`方法主要用来对一些不方便在编译期初始化的对象进行赋值。比如`NSMutableArray`这种类型的实例化依赖于runtime的消息发送,所以显然无法在编译器初始化: 128 | 129 | ```objc 130 | // In Parent.m 131 | static int someNumber = 0; // int类型可以在编译期赋值 132 | static NSMutableArray *someObjects; 133 | 134 | + (void)initialize { 135 | if (self == [Parent class]) { 136 | // 不方便编译期复制的对象在这里赋值 137 | someObjects = [[NSMutableArray alloc] init]; 138 | } 139 | } 140 | ``` 141 | 142 | # 总结 143 | 144 | 1. `load`和`initialize`方法都会在实例化对象之前调用,以main函数为分水岭,前者在main函数之前调用,后者在之后调用。这两个方法会被自动调用,不能手动调用它们。 145 | 2. `load`和`initialize`方法都不用显示的调用父类的方法而是自动调用,即使子类没有`initialize`方法也会调用父类的方法,而`load`方法则不会调用父类。 146 | 3. `load`方法通常用来进行Method Swizzle,`initialize`方法一般用于初始化全局变量或静态变量。 147 | 4. `load`和`initialize`方法内部使用了锁,因此它们是线程安全的。实现时要尽可能保持简单,避免阻塞线程,不要再使用锁。 -------------------------------------------------------------------------------- /articles/objc-runtime-story.md: -------------------------------------------------------------------------------- 1 | 点赞的一定是完全读懂了 2 | 3 | # 背景 4 | 5 | KT是代码王国的第1024任国王,此人极好女色,号称后宫佳丽三千。按照旧的规则,每晚侍寝的人选由内侍监统一统筹安排,妃子们往往提前数周就可以得知自己具体的侍寝日期。国王KT对这种制度不满已久,他觉得这是一种非常死板的方法,无法处理各种突发情况,缺少灵活性。机智的他想出了一个新的方法:记录每个妃子的名字,然后每晚由自己选择,临时决定由哪一位妃子侍寝,而后召来太监小凳子,宣被选中的妃子前来侍寝。至于小凳子,他得知了国王的选择后,会到内侍监的名册中,根据妃子的名字找到她的住所,然后前往那里宣妃子侍寝。 6 | 7 | # 小凳子的烦恼 8 | 9 | 国王十分喜欢这种新的制度,只是之前说过,国王号称后宫佳丽三千,实际上可能更是远远不止这个数。这可苦了小凳子,他得在厚厚一摞名册中找到妃子的名字,运气不好的时候过了子时也找不到,国王早就等的不耐烦了。代码王国中每个人都很聪明,小凳子想了一个点子:他把经常被国王宠幸的妃子的名字单独列了一份名单,大约有几十人。这样一来,大部分时候他只要在这名单中找就可以了。 10 | 11 | 有时候,小凳子找遍了名册也没找到被宣的妃子的名字。这是因为小凳子的师傅那里也有一份名册,虽然这两份名册很多内容是一样的,但总有一些微小的差别。所以如果小凳子找不到妃子的名字的时候就会去向他师傅求助。他师傅也很聪明,也会先从自己单独列出的一小份名单中找,如果找不到才会在厚名册中找。如果师傅也找不到,他还会找他自己的师傅,也就是小凳子的师公。 12 | 13 | 万幸的是,在小凳子的师傅的师傅的……师傅那里,妃子的名字终于被找到了。小凳子当然记得这次惨痛的教训,他把这个妃子的名字也加入到那一小份名册中,毕竟国王再次选这个妃子的几率还是挺大的。小凳子的师傅可不管这事儿,毕竟国王找的是小凳子又不是自己。如果真有哪天国王召见自己办这差事,那到时候记录也不迟。 14 | 15 | # 危机来临 16 | 17 | 在自己的机智和师傅师祖们的合力帮助下,小凳子有惊无险的完成了多次任务。只是突然有一天,国王让他去宣一名叫“闹特芳德”的妃子。小凳子一开始没有把这事放在心上,因为同样的任务他已经完成了无数次。只是这一次,不仅小册子中找不到“闹特芳德”的信息,完整的名册里也没有。甚至他的祖师爷也告诉他,师门上下找了个遍,都没有这位妃子的信息。事实上他们当然找不到,因为这位“闹特芳德”姑娘已经在数年前染病身亡,她的相关信息都已被抹去。 18 | 19 | 无奈之下小凳子只得另辟蹊径,他首先打听了一番最近有没有新入宫的人。如果有的话他就可以用这新人代替“闹特芳德”妃子。然后把她带到国王身边。只可惜最近宫中风平浪静,并没有人新入宫。 20 | 21 | 小凳子想到了自己的好基友小卓子,小卓子人脉广,人也很机灵,说不定就有什么好点子。不仅如此,他的其他几个好基友也很有能力。于是他询问自己的朋友们:“国王委派给了我一个任务,要找一位妃子侍寝,妃子叫“闹特芳德,你们谁能帮帮我?”。”他希望能有朋友能替他完成这个任务,只可惜这事太难办,大家都无能为力。 22 | 23 | 走投无路的小凳子只好在内侍监里广发布告,布告内容主要是三点。任务执行者是小凳子,任务内容是宣妃子侍寝,具体细节是这个妃子叫“闹特芳德”。任何人都可以接这个任务,他们可以修改一下任务再完成。比如可以换一个完成者,由小卓子来完成这个任务。也可以换任务内容,比如改成宣太监侍寝。还可以修改任务细节,比如宣“闹特芳德”的表妹,“芳德”妃子。小凳子心里明白,估计换个人做任务是不太可能了,因为自己之前已经问过朋友,他们都说不能完成这件事。 24 | 25 | 其实这件事由谁来做,怎么做都不重要,重要的只要一个人站出来,说自己可以完成任务即可。小凳子也不知变通,他完全可以口头答应下来,然后找另外一个妃子去侍寝,虽然效果打了折扣但也不至于被愤怒的国王以“拒不遵旨”为罪名处死。事后看来,虽然“闹特芳德”妃子确实根本不可能找到,但小凳子至少有三次活命的机会,只可惜他一次都没把握住。 26 | 27 | [这个项目](https://github.com/bestswifter/MySampleCode/tree/master/runtime)本可救小凳子一命,但他还未来得及学习。 28 | 29 | # 阴谋 30 | 31 | 渐渐地,一位大臣发现国王似乎非常宠爱A妃子。于是他心生一计,先让自己的女儿B进入宫中,再将名册中A妃子的住所地址和自己女儿的地址互换。这样,太监每次去宣A妃子的时候其实去的是B的家中。B得以经常见到国王,这位大臣也打听到了很多国王的内幕消息,也因此得了不少好处。 32 | 33 | 34 | 这种瞒天过海,偷天换日的事虽然收获巨大,但也非常危险,稍有不慎就是掉脑袋的下场。这位大臣认真总结了一些要点: 35 | 36 | 1. 去内侍监改名册时一定要小心,千万不能有第二个人在场。 37 | 2. 如果名册中已经有人和自己女儿重名,这事就非常难办。 38 | 3. 换名字这事只能做一次,如果不小心做了两次,那么两次的效果完全抵消,相当于自己什么都没干。最好的方法就是确保只在建立名册的时候修改一次。 39 | 4. 必须时刻保持头脑清楚。自己的女儿名字已经在名册中了,如果国王宣了自己的女儿,那其实真正被召见去侍寝的是原来的妃子。 40 | 41 | 总的来说,这种事情是高风险高收益的,如果使用得当可以给自己带来巨大的好处,但如果使用不当,也有可能伤及自己。好在歪果仁写了一份[非常详细的教程](http://stackoverflow.com/questions/5339276/what-are-the-dangers-of-method-swizzling-in-objective-c)。 42 | -------------------------------------------------------------------------------- /articles/objc-strong-weak-dance.md: -------------------------------------------------------------------------------- 1 | 在使用 `Block` 时,除了使用 `__weak` 修饰符避免循环引用外,还有一点经常容易忘记。苹果把它称为:“Strong-Weak Dance”。 2 | 3 | # 问题来源 4 | 5 | 这是一种 强引用 --> 弱引用 --> 强引用 的变换过程。在弄明白为什么要如此大费周章之前,我们首先来看看一般的写法会有什么问题。 6 | 7 | ```objc 8 | __weak MyViewController *wself = self; 9 | self.completionHandler = ^(NSInteger result) { 10 | [wself.property removeObserver: wself forKeyPath:@"pathName"]; 11 | }; 12 | ``` 13 | 14 | 这种写法可以避免循环引用,但是我们要考虑这样的问题: 15 | 16 | **假设 `block` 被放在子线程中执行,而且执行过程中 `self` 在主线程被释放了。由于 `wself ` 是一个弱引用,因此会自动变为 `nil`。而在 KVO 中,这会导致崩溃。** 17 | 18 | # Strong-Weak Dance 19 | 20 | 解决以上问题的方法很简单,新增一行代码即可: 21 | 22 | ```objc 23 | __weak MyViewController *wself = self; 24 | self.completionHandler = ^(NSInteger result) { 25 | __strong __typeof(wself) sself = wself; // 强引用一次 26 | [sself.property removeObserver: sself forKeyPath:@"pathName"]; 27 | }; 28 | ``` 29 | 30 | 这样一来,`self` 所指向对象的引用计数变成 2,即使主线程中的 `self` 因为超出作用于而释放,对象的引用计数依然为 1,避免了对象的销毁。 31 | 32 | # 思考 33 | 34 | 在和小伙伴的讨论过程中,他提出了几个问题。虽然都不难,但是有利于把各种知识融会贯通起来。 35 | 36 | 1. Q:下面这行代码,将一个弱引用的指针赋值给强引用的指针,可以起到强引用效果么? 37 | 38 | ```objc 39 | __strong __typeof(wself) sself = wself; 40 | ``` 41 | 42 | A:会的。引用计数描述的是对象而不是指针。这句话的意思是: 43 | 44 | > sself 强引用 wself 指向的那个对象 45 | 46 | 因此对象的引用计数会增加一个。 47 | 48 | 2. Q:`block` 内部定义了`sself`,会不会因此强引用了 `sself`? 49 | 50 | A:不会。`block` 只有截获外部变量时,才会引用它。如果是内部新建一个,则没有任何问题。 51 | 52 | 3. Q:如果在 `block` 内部没有强引用,而是通过 `if` 判断,是不是也可以,比如这样写: 53 | 54 | ```objc 55 | __weak MyViewController *wself = self; 56 | wself.completionHandler = ^(NSInteger result) { 57 | if (wself) { // 只有当 wself 不为 nil 时,才执行以下代码 58 | [wself.property removeObserver: wself forKeyPath:@"pathName"]; 59 | } 60 | }; 61 | ``` 62 | 63 | A:不可以!考虑到多线程执行,也许在判断的时候,`self` 还没释放,但是执行 `self` 里面的代码时,就刚好释放了。 64 | 65 | 4. Q:那按照这个说法,`block` 内部强引用也没用啊。也许 `block` 执行以前,`self` 就释放了。 66 | 67 | A:有用!如果在 `block` 执行以前,`self` 就释放了,那么 `block` 的引用计数降为 0,所以自己就会被释放。这样它根本就不会被执行。另外,如果执行一个为 `nil` 的闭包会导致崩溃。 68 | 69 | 5. Q:如果在执行 `block` 的过程中,`block` 被释放了怎么办? 70 | 71 | A:简单来说,`block` 还会继续执行,但是它捕获的指针会具有不确定的值,详细内容请参考[这篇文章](http://stackoverflow.com/questions/12272783/what-happens-when-a-block-is-set-to-nil-during-its-execution) 72 | 73 | 74 | # @strongify 和 @weakify 75 | 76 | 这是 [ReactiveCocoa](https://github.com/ReactiveCocoa/ReactiveCocoa) 中定义的一个宏。一般可以这样使用: 77 | 78 | ```objc 79 | @weakify(self); 80 | self.completionHandler = ^(NSInteger result) { 81 | @strongify(self); 82 | [self.property removeObserver: sself forKeyPath:@"pathName"]; 83 | }; 84 | ``` 85 | 86 | 本文并非分析它们的实现原理,所以就简单解释两点: 87 | 88 | 1. 这里的“@”没有任何用处,仅表示强调,这个宏实际上包含了一个空的 `AutoreleasePool`,这也就是为什么一定要加上“@”。 89 | 90 | 2. 它的原理还是和之前一样,生成了一段形如 `__weak MyViewController *wself = self;` 这种格式的代码: 91 | 92 | ```objc 93 | #define rac_strongify_(INDEX, VAR) \\ 94 | __strong __typeof__(VAR) VAR = metamacro_concat(VAR, _weak_); 95 | ``` 96 | 97 | # Swift 中的情况 98 | 99 | 感谢 [@Cyrus_dev](http://www.jianshu.com/users/18729f511551/latest_articles) 的提醒,在 Swift 中也有 Strong-Weak Dance 的概念。最简单的方法就是直接沿用 OC 的思路: 100 | 101 | 102 | ```swift 103 | self.completionHandler = { [weak self] in 104 | if let strongSelf = self { 105 | /// .... 106 | } 107 | }; 108 | ``` 109 | 110 | 这种写法的缺点在于,我们不能写 `if let self = self`,因此需要重新定义一个变量 `strongSelf`,命名方式显得不够优雅。 111 | 112 | 除此以外还可以使用 Swift 标准库提供的函数 `withExtendedLifetime `: 113 | 114 | ```swift 115 | self.completionHandler = { [weak self] in 116 | withExtendedLifetime(self) { 117 | /// ... 118 | } 119 | }; 120 | ``` 121 | 122 | 这种写法的缺点在于,`self` 依然是可选类型的,还需要把它解封后才能使用。 123 | 124 | 最后,还有一种解决方案是自定义 `withExtendedLifetime `函数: 125 | 126 | ```swift 127 | extension Optional { 128 | func withExtendedLifetime(body: T -> Void) { 129 | if let strongSelf = self { 130 | body(strongSelf) 131 | } 132 | } 133 | } 134 | ``` 135 | 136 | 至于这种写法是否更加优雅,就见仁见智了: 137 | 138 | ```swift 139 | self.completionHandler = { [weak self] in 140 | self.withExtendedLifetime { 141 | /// 这里用 $0 表示 self 142 | } 143 | }; 144 | ``` 145 | 146 | # 参考资料 147 | 148 | 1. [What happens when a block is set to nil during its execution?](http://stackoverflow.com/questions/12272783/what-happens-when-a-block-is-set-to-nil-during-its-execution) 149 | 2. [The Weak/Strong Dance in Swift](http://kelan.io/2015/the-weak-strong-dance-in-swift/) -------------------------------------------------------------------------------- /articles/objc-swift-block.md: -------------------------------------------------------------------------------- 1 | 最近在看Swift闭包截获变量时遇到了各种问题,总结之后发现主要是还用停留在OC时代的思维来思考Swift问题导致的。借此机会首先复习一下OC中关于block的细节,同时整理Swift中闭包的相关的问题。不管是目前使用OC还是Swift,又或者是从OC转向Swift,都可以阅读这篇文章并与我交流。 2 | 3 | # OC 的 block 4 | 5 | OC的block已经有很多相关的文章介绍了,主要难点在于`__block`修饰符的作用和原理,以及循环引用问题。我们首先由浅入深举几个例子看一看`__block`修饰符,最后分析循环引用问题。这里的讨论都是基于ARC的。 6 | 7 | ### 截获基本类型 8 | 9 | ```objc 10 | int value = 10; 11 | void(^block)() = ^{ 12 | NSLog(@"value = %d", value); 13 | }; 14 | value = 20; 15 | block(); 16 | 17 | // 打印结果是:"value = 10" 18 | ``` 19 | 20 | OC 的 block 会截获外部变量,对于`int`等基本数据类型,block的内部会拷贝一份,简单来说,它的实现大概是这样的: 21 | 22 | ```objc 23 | struct block_impl { 24 | //其它内容 25 | int value; 26 | }; 27 | ``` 28 | 29 | 因为block内部拷贝了截获的变量的副本,所以生成block后再修改变量,不会影响被block截获的变量。同时block内部也不能修改这个变量。 30 | 31 | ### 修改基本类型 32 | 33 | 如果要想在block中修改被截获的基本类型变量,我们需要把它标记为`__block`: 34 | 35 | ```objc 36 | __block int value = 10; 37 | void(^block)() = ^{ 38 | NSLog(@"value = %d", value); 39 | }; 40 | value = 20; 41 | block(); 42 | 43 | // 打印结果是:"value = 20" 44 | ``` 45 | 46 | 这是因为,对于被标记了`__block`的变量,block在截获它时,会保存一个指针。简单来说,它的实现大概是这样的: 47 | 48 | ```objc 49 | struct block_impl { 50 | //其它内容 51 | block_ref_value *value; 52 | }; 53 | 54 | struct block_ref_value { 55 | int value; // 这里保存的才是被截获的value的值。 56 | }; 57 | ``` 58 | 59 | 由于 block 中一直有一个指针指向 value,所以 block 内部对它的修改,可以影响到 block 外部的变量。因为 block 修改的就是那个外部变量而不是外部变量的副本。 60 | 61 | 上面关于block具体实现的例子只是一个简化模型,事实上并非如此,但本质类似。总的来说,只有由`__block`修饰符修饰的变量,在被block截获时才是可变的。关于这方面的详细解释,可以参考这三篇文章: 62 | 63 | * [iOS OC语言: Block底层实现原理](http://www.jianshu.com/p/e23078c11518):这个很详细地讲了`__block`的实现原理 64 | * [Block的引用循环问题 (ARC & non-ARC)](http://blog.csdn.net/wildfireli/article/details/22063001):这个讲了一些block底层的实现原理以及循环引用问题。 65 | * [你真的理解__block修饰符的原理么?](http://blog.csdn.net/abc649395594/article/details/47086751):这是我之前写过的一篇介绍`__block`原理的文章,内容会详细一些。 66 | 67 | ### 截获指针 68 | 69 | block截获指针和截获基本类型是相似的,不过稍稍复杂一些。先看一个最简单的例子。 70 | 71 | ```objc 72 | Person *p = [[Person alloc] initWithName:@"zxy"]; 73 | void(^block)() = ^{ 74 | NSLog(@"person name = %@", p.name); 75 | }; 76 | 77 | p.name = @"new name"; 78 | block(); 79 | 80 | // 打印结果是:"person name = new name" 81 | ``` 82 | 83 | 在截获基本类型时,block内部可能会有`int capturedValue = value;`这样的代码,类比到指针也是一样的,block内部也会有这样的代码:`Person *capturedP = p;`。在ARC下,这其实是强引用(retain)了block外部的`p`。 84 | 85 | 由于block内部的`p`和外部的`p`指向的是同一块内存地址。所以在block外部修改`p`的属性,依然会影响到block内部截获的`p`。 86 | 87 | 需要强调一点,这里的`p`依然不是可变的。修改`p`的`name`不是改变`p`,只是改变`p`内部的属性: 88 | 89 | ```objc 90 | Person *p = [[Person alloc] initWithName:@"zxy"]; 91 | void(^block)() = ^{ 92 | p.name = @"new name"; //OK,没有改变p 93 | p = [[Person alloc] initWithName:@"new name"]; //编译错误 94 | NSLog(@"person name = %@", p.name); 95 | }; 96 | 97 | block(); 98 | ``` 99 | 100 | ### 改变指针 101 | 102 | 类比`__block`修饰符对基本类型的作用原理,由它修饰的指针,在被block截获时,截获的其实是这个指针的指针。比如我们把刚刚的例子修改一下: 103 | 104 | ```objc 105 | __block Person *p = [[Person alloc] initWithName:@"zxy"]; 106 | void(^block)() = ^{ 107 | NSLog(@"person name = %@", p.name); 108 | }; 109 | 110 | p = nil; 111 | block(); 112 | 113 | // 打印结果是:"person name = (null)" 114 | ``` 115 | 116 | 此时,block内部有一个指向外部的`p`的指针,一旦`p`被设为`nil`,这个内部的指针就指向了`nil`。所以打印结果就是`null`了。 117 | 118 | ### __block与强引用 119 | 120 | 还记得以前有一次面试时被问到,`__block`会不会`retain`变量?答案是:会的。从原理上分析,`__block`修饰的变量被封装在结构体中,block内部持有对这个结构体的强引用。这一点不管是对于基本类型还是指针都是通用的。从实际例子上来说: 121 | 122 | ```objc 123 | Block block; 124 | if (true) { 125 | __block Person *p = [[Person alloc] initWithName:@"zxy"]; 126 | block = ^{ 127 | NSLog(@"person name = %@", p.name); 128 | }; 129 | } 130 | block(); 131 | 132 | // 打印结果是:"person name = zxy" 133 | ``` 134 | 135 | 如果没有`retain`被标记为`__block`的指针`p`,那么超出作用于后应该会得到`nil`。 136 | 137 | ### 避免循环引用 138 | 139 | 不管对象是否标记为`__block`,一旦block截获了它,就会强引用它。所以,判断是否发生循环引用,只要判断block截获的对象,是否也持有block即可。如果这个对象确实需要直接或间接持有block,那么我们需要避免block强引用这个对象。解决办法是使用`__weak`修饰符。 140 | 141 | ```objc 142 | // block是self的一个属性 143 | 144 | id __weak weakSelf = self; 145 | block = ^{ 146 | //使用weakSelf代替self 147 | }; 148 | ``` 149 | 150 | block不会强引用被标记为`__weak`的对象,只会对其产生弱引用。为了防止在block内的操作会释放`wself`,可以先强引用它。这种做法有一个很漂亮的名字叫`weak-strong dacne`,具体实现方法可以参考RAC的`@strongify`和`@weakify`。 151 | 152 | ### OC中block总结 153 | 154 | 简单来说,除非标记为`__weak`,block总是会强引用任何捕获的对象。而`__block`表示捕获的就是指针本身,而非另一个指向这个对象的指针。也就是说,被`__block`修饰的对象在block内、外的改动会互相影响。 155 | 156 | 如果想避免循环引用问题,首先要确定block引用了哪些对象,然后判断这些对象是否直接或间接持有block,如果有的话把这些对象标记为`__weak`避免block强引用它。 157 | 158 | # Swift的闭包 159 | 160 | OC中的`__block`是一个很讨厌的修饰符。它不仅不容易理解,而且在ARC和非ARC的表现截然不同。`__block`修饰符本质上是通过截获变量的指针来达到在闭包内修改被截获的变量的目的。 161 | 162 | 在Swift中,这叫做截获变量的引用。闭包默认会截取变量的引用,也就是说所有变量默认情况下都是加了`__block`修饰符的。 163 | 164 | ```swift 165 | var x = 42 166 | let f = { 167 | // [x] in //如果取消注释,结果是42 168 | print(x) 169 | } 170 | x = 43 171 | f() // 结果是43 172 | ``` 173 | 174 | 如果如果被截获的变量是引用,和OC一样,那么在闭包内部有一个引用的引用: 175 | 176 | ```swift 177 | var block2: (() -> ())? 178 | if true { 179 | var a: A? = A() 180 | block2 = { 181 | print(a?.name) 182 | } 183 | a = A(name: "new name") 184 | } 185 | block2?() //结果是:"Optional("new name")" 186 | ``` 187 | 188 | 如果把变量写在截获列表中,那么block内部会有一个指向对象的强引用,这和在OC中什么都不写的效果是一样的: 189 | 190 | ```swift 191 | var block2: (() -> ())? 192 | if true { 193 | var a: A? = A() 194 | block2 = { [a] in 195 | print(a?.name) 196 | } 197 | a = A(name: "new name") 198 | } 199 | block2?() //结果是:"Optional("old name")" 200 | ``` 201 | 202 | Swift会自动持有被截获的变量的引用,这样就可以在block内部直接修改变量。不过在一些特殊情况下,Swift会做一些优化。通过之前OC中对`__block`的分析可以看到,持有变量的引用肯定比直接持有变量开销更大。所以Swift会自动判断你是否在闭包中或闭包外改变了变量。如果没有改变,闭包会直接持有变量,即使你没有显式的把它卸载捕获列表中。下面这句话截取自[Swift官方文档](https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Closures.html#//apple_ref/doc/uid/TP40014097-CH11-ID94): 203 | 204 | > As an optimization, Swift may instead capture and store a copy of a value if that value is not mutated by or outside a closure. 205 | 206 | ### Swift循环引用 207 | 208 | 不管是否显示的把变量写进捕获列表,闭包都会对对象有强引用。如果闭包是某个对象的属性,而且闭包中截获了对象本身,或对象的某个属性,就会导致循环引用。这和OC中是完全一样的。解决方法是在捕获列表中把被截获的变量标记为`weak`或`unowned`。 209 | 210 | 关于Swift的循环引用,有一个需要注意的例子: 211 | 212 | ```swift 213 | class A { 214 | var name: String = "A" 215 | var block: (() -> ())? 216 | 217 | //其他方法 218 | } 219 | 220 | var a: A? = A() 221 | var block = { 222 | print(a?.name) 223 | } 224 | a?.block = block 225 | a = nil 226 | block() 227 | ``` 228 | 229 | 我们先创建了可选类型的变量`a`,然后创建一个闭包变量,并把它赋值给`a`的`block`属性。这个闭包内部又会截获`a`,那这样是否会导致循环引用呢? 230 | 231 | 答案是否定的。虽然从表面上看,对象的闭包属性截获了对象本身。但是如果你运行上面这段代码,你会发现对象的`deinit`方法确实被调用了,打印结果不是“A”而是“nil”。 232 | 233 | 这是因为我们忽略了可选类型这个因素。这里的`a`不是A类型的对象,而是一个可选类型变量,其内部封装了A的实例对象。闭包截获的是可选类型变量`a`,当你执行`a = nil`时,并不是释放了变量`a`,而是释放了`a`中包含的A类型实例对象。所以A的`deinit`方法会执行,当你调用block时,由于使用了可选链,就会得到`nil`,如果使用强制解封,程序就会崩溃。 234 | 235 | 如果想要人为造成循环引用,代码要这样写: 236 | 237 | ```swift 238 | var block: (() -> ())? 239 | if true { 240 | var a = A() 241 | block = { 242 | print(a.name) 243 | } 244 | a.name = "New Name" 245 | } 246 | block!() 247 | ``` 248 | 249 | ### Weak-Strong Dance 250 | 251 | 为了避免`weak`变量在闭包中提前被释放,我们需要在block一开始强引用它。这在OC部分已经讲过如何使用了。Swift中实现Weak-Strong Dance一般有三种方法。分别是最简单的`if let`可选绑定、标准库的`withExtendedLifetime `方法和自定义的`withExtendedLifetime `方法。 252 | 253 | # 总结 254 | 255 | 1. OC中默认截获变量,Swift默认截获变量的引用。它们都会强引用被截获的变量。 256 | 2. Swift中没有`__block`修饰符,但是多了截获列表。通过把截获的变量标记为`weak`避免引用循环 257 | 3. 两者都有Weak-Strong Dance,不过这一点上OC的写法更简单。 258 | 4. 在使用可选类型时,要明确闭包截获了可选类型还是实例变量。这样才能正确判断是否发生循环引用。 259 | -------------------------------------------------------------------------------- /articles/objc-swift-copy-mutable.md: -------------------------------------------------------------------------------- 1 | # Objective-C 2 | 3 | 为了解释方便,定义两个类:`Person`和`MyObject`,它们都继承自`NSObject`。他们的关系如下: 4 | 5 | ```objc 6 | // Person.h 7 | @property (strong, nonatomic, nullable) MyObject *object; 8 | ``` 9 | 10 | ```objc 11 | // MyObjec.h 12 | @property (copy, nonatomic) NSString *name; 13 | ``` 14 | 15 | ## 普通对象拷贝 16 | 17 | 对于一个OC中的对象来说,可能涉及拷贝的有三种操作: 18 | 19 | 1. `retain`操作: 20 | 21 | ```objc 22 | Person *p = [[Person alloc] init]; 23 | Person *p1 = p; 24 | ``` 25 | 26 | 这里的`p1`默认是`__strong`,所以它会对`p`进行`retain`操作。`retain`与复制无关,只会对引用计数加1。`p1`和`p`的地址是完全一样的: 27 | 28 | ```objc 29 | 2015-12-23 21:24:31.893 Copy[1300:120857] p = 0x1006012c0 30 | 2015-12-23 21:24:31.894 Copy[1300:120857] p1 = 0x1006012c0 31 | ``` 32 | 33 | 这种写法最简单,而且严格来说不是复制,但值得一提,因为在接下来的OC和Swift中,都会涉及到这样的代码。 34 | 35 | 2. `copy`方法: 36 | 37 | 调用`copy`方法需要实现`NSCopying`协议,并提供`copyWithZone`方法: 38 | 39 | ```objc 40 | - (id)copyWithZone:(NSZone *)zone { 41 | Person *copyInstance = [[self class] allocWithZone:zone]; 42 | copyInstance.object = self.object; 43 | return copyInstance; 44 | } 45 | ``` 46 | 47 | 第二行代码就是刚刚所说的`retain`操作。因此,我们虽然复制了`Person`对象的指针,但是其内部的属性,依然和原来对象的相同。 48 | 49 | 3. 自定义拷贝方法: 50 | 51 | 我们当然可以自己定义一个拷贝方法,在复制`Person`对象的同时,把其中的`object`属性也复制,而不是仅仅`retain`。 52 | 53 | 54 | 第二三种复制方法的区别如图所示: 55 | 56 | ![两种拷贝方式](http://upload-images.jianshu.io/upload_images/1171077-c54f3506af03483b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 57 | 58 | ## 浅拷贝与深拷贝 59 | 60 | 标为红色的是两种拷贝方式的不同之处。对于左边这种,只拷贝指针本身的拷贝方法,我们称为浅拷贝。对于右边那种,不仅拷贝指针自身,还拷贝指针中所有元素的拷贝方法,我们称为深拷贝。 61 | 62 | 没有明确的限制`copy`和自定义的拷贝方法要如何实现。也就是说`copy`方法可以用来进行深拷贝,我们也可以自定义浅拷贝的方法。这完全取决于我们自己如何实现`copy`方法和自定义的拷贝方法。在OC中,对于自定义的类来说,浅拷贝与深拷贝只是一种概念,并没有明确的标注哪种方法就是浅拷贝。 63 | 64 | **注意** 65 | 66 | “深拷贝将被拷贝的对象完全复制”这种说法不完全正确。比如上图中我们看到`data`的地址永远不会拷贝。这是因为,深拷贝只负责了对象的拷贝,以及对象中所有属性的拷贝。正是因为拷贝了属性,将`p`深拷贝后得到的`p'`的`object`指针地址和`p`的`object`指针地址不同。 67 | 68 | 但是至于`data`会不会被拷贝,这取决于`MyObject`类如何设计,如果`MyObject`的`copy`方法只是浅拷贝,就会形成如上图所示的情况。如果`MyObject`的`copy`方法也是深拷贝,那么`data`的地址也会不同。 69 | 70 | ## 容器对象拷贝 71 | 72 | 在OC中,所有`Foundation`中的容器类,分为可变容器和不可变容器,它们的拷贝**都是浅拷贝**。这也就是为什么建议自定义的对象实现浅拷贝,如果有需要才自定义深拷贝方法。因为这样一来,所有的方法调用就都可以统一,不至于造成误解。 73 | 74 | 如果我们把数组想象成一个三层的数据结构,第一层是数组自己的指针,第二层是存放在数组中的指针,第三层(如果第二层是指针)则是这些指针指向的对象。那么在复制数组时,复制的是前两层,第三层的对象不会被复制。如果把前两层看做指针,第三层看做对象,那么数组的拷贝,无论是`copy`还是`mutableCopy`都是浅拷贝。当然,也有人把这个称为“单层深拷贝”,这些概念性的定义都不重要,重要的是知道数组拷贝时的原理。 75 | 76 | 这一点很好理解。首先,指针所指向的对象,也许很大,深拷贝可能占用过多的内存和时间。其次,容器不知道自己存储的对象是否实现了`NSCopying`协议。如果容器的拷贝默认是深拷贝,同时你在数组中存放了`Person`类的对象,而`Person`类根本没有实现`NSCopying`协议,后果是复制容器会导致程序崩溃。这是任何语言开发者都不希望看到的,所以设身处地想一下,如果是你来设计OC,也不会让数组深拷贝吧。 77 | 78 | 观察下面这段代码,思考一下为什么`a1[0] = @0`没有影响`a2`: 79 | 80 | ```objc 81 | NSMutableArray *a1 = [[NSMutableArray alloc] initWithObjects:@1, @2, nil]; 82 | NSMutableArray *a2 = [a1 mutableCopy]; 83 | a1[0] = @0; 84 | NSLog(@"a2 = %@", a2); 85 | 86 | /* 87 | 2015-12-23 23:11:53.711 Copy[1795:220469] a2 = ( 88 | 1, 89 | 2 90 | ) 91 | */ 92 | ``` 93 | 94 | ## 可变性 95 | 96 | 容器对象分为可变容器与不可变容器,`NSData`、`NSArray`、`NSString`等都是不可变容器,以`NSMutable`开头的则是它们的可变版本。下面统一用`NSArray`和`NSMutableArray`举例说明。 97 | 98 | 因为`NSMutableArray`是`NSArray`的子类,所以`NSArray`对象不能强制转换成`NSMutableArray`,否则在调用`addObject`方法时会崩溃。反之,`NSMutableArray`可以转换成它的父类`NSArray`,这么做会导致它失去可变性。 99 | 100 | 容器拷贝的难点在于可变性的变化。容器有两种方法:`copy`和`mutableCopy`,再次强调这两者都是浅拷贝。它们的区别在于,返回值是否是可变的。前者返回不可变容器,后者返回可变容器。 101 | 102 | 这也就是说,返回值的可变性与被拷贝对象的可变性无关,仅取决于调用了何种拷贝方法。比如: 103 | 104 | ```objc 105 | NSMutableArray *mutableArray = [[NSMutableArray alloc] initWithObjects:@1, @2, nil]; 106 | NSMutableArray *array = [mutableArray copy]; 107 | [array addObject:@3]; 108 | ``` 109 | 110 | 尽管我们调用了`mutableArray`的拷贝方法,返回值也声明为`NSMutableArray`,但是调用`addObject`方法时依然会导致运行时错误。这是由错误的调用了`copy`方法导致的。 111 | 112 | 调用一个对象的浅拷贝方法会得到一个新的对象(地址不同),但是容器类中有一个特例: 113 | 114 | ```objc 115 | NSArray *array1 = @[@1, @2]; 116 | NSArray *array2 = [array1 copy]; 117 | // array1和array2地址相同 118 | ``` 119 | 120 | 这是因为既然`array1`和`array2`都不能再修改,那么两者共用同一块内存也是无所谓的,所以OC做了这样的优化。 121 | 122 | ## 字符串拷贝 123 | 124 | 字符串也可以被当做容器来理解。它有`NSString`和`NSMutableString`两个版本。 125 | 126 | 于是为什么字符串属性要定义成`@property(copy, nonatomic)`就很好理解了。它主要用于处理这种情况: 127 | 128 | ```objc 129 | NSMutableString *string = @"hello"; 130 | self.propertyString = string; 131 | [string appendString:@" world"]; 132 | ``` 133 | 134 | 如果属性定义成`strong`,那么在第二步执行了`retain`操作,第三步对`string`的修改就会影响到原来的属性。现在我们把属性定义为`copy`,那么第二步操作其实是得到了一个新的,不可变字符串。这符合我们的预期目的。 135 | 136 | # Swift拷贝 137 | 138 | ## 结构体拷贝 139 | 140 | 数组、字典等容器在Swift中被定义成了结构体,它们的拷贝规则和OC完全不同: 141 | 142 | ```swift 143 | var array1 = [1,2,3] 144 | var array2 = array1 145 | 146 | array1[0] = 0 147 | print(array2) // 输出结果:[1, 2, 3] 148 | ``` 149 | 150 | 可以看到,即使是最简单的等号赋值,也会浅拷贝原来的值。这是由Swift中结构体的值语义决定的。之所以说是浅拷贝而不是深拷贝,理由参见前文解释OC中容器的浅拷贝,尤其是第二点理由,不管是对于OC还是Swift来说都是通用的。 151 | 152 | ## 对象拷贝 153 | 154 | 和OC中指针赋值类似,对象的直接赋值操作与拷贝无关: 155 | 156 | ```swift 157 | class Person { 158 | var name: String; 159 | init(name:String) { 160 | self.name = name 161 | } 162 | } 163 | 164 | let person1 = Person(name: "zxy") 165 | let person2 = person1 166 | person1.name = "new name" 167 | 168 | print(person2.name) //结果是“new name” 169 | ``` 170 | 171 | 如果要拷贝对象,有两种方法。首先,最自然想到的是实现`NSCopying`协议,注意只有`NSObject`类的对象才能实现这个协议: 172 | 173 | ```swift 174 | class Person : NSObject, NSCopying { 175 | var name: String; 176 | init(name:String) { 177 | self.name = name 178 | } 179 | 180 | func copyWithZone(zone: NSZone) -> AnyObject { 181 | return Person(name: self.name) 182 | } 183 | } 184 | 185 | ``` 186 | 187 | 但这样做最大的问题在于,你必须继承自`NSObject`,这就又回到了OC的那一套。如果我们希望定义纯粹的Swift类,完全可以自己定义并实现拷贝方法。 188 | 189 | “面向接口编程”的原则告诉我们,我们应该让`Person`实现某个接口,而不是继承自某个子类: 190 | 191 | ```swift 192 | protocol Copyable { 193 | func copy() -> Copyable 194 | } 195 | 196 | class Person : Copyable { 197 | var name: String; 198 | init(name:String) { 199 | self.name = name 200 | } 201 | 202 | func copy() -> Copyable { 203 | return Person(name: self.name) 204 | } 205 | } 206 | 207 | let person1 = Person(name: "zxy") 208 | let person2 = person1.copy() as! Person 209 | ``` 210 | 211 | 这样就完美的实现Swift-Style拷贝了。 212 | 213 | # 总结 214 | 215 | 在OC中,浅拷贝通常由`NSCopying`协议的`copyWithZone`方法实现,深拷贝需要自定义方法。直接复制意味着`retain`而不是拷贝。 216 | 217 | 在Swift中,值类型直接用等号赋值意味着浅拷贝,引用类型的拷贝可以通过实现自定义的`Copyable`协议或OC的`NSCopying`协议完成。 218 | 219 | 在OC中,我们需要容器的可变性,而Swift在这一点做的要比OC好得多。它的可变性非常简单,完全通过`let`和`var`控制,这也是Swift相比于OC的一个优点吧,毕竟高级的语言应该尽可能封装底层实现。 -------------------------------------------------------------------------------- /articles/pointer-and-reference.md: -------------------------------------------------------------------------------- 1 | 从概念理解和用法上来讲,C/C++ 中的指针和 Java 等语言中的引用非常类似。它们和被指向的内容分开存储(一般存储在栈上),并且持有被指向的对象的地址。通过指针/引用,我们可以方便的操作实际的对象。 2 | 3 | 当然,区别还是有的。C 系的指针更加透明,换句话说指针其实就是一个整数,只不过这个整数恰好表示了被指向的变量的地址。这是一种非常简单的,直来直去的表示方法。而 Java 等语言的引用就像是一个黑盒了。Java 的规范并没有要求引用的值一定就是内存地址,在实现的时候,它有可能有一个中间层进行映射,但最终还是指针的模型,也就是说依然会持有地址,只不过从使用者的角度来看,地址的概念经过封装已经不存在了。具体的做法我不太清楚,据说是为了提高垃圾回收算法的效率。 4 | 5 | # 指针可以进行四则运算 6 | 7 | 一个是透明的数字,表示内存地址。一个是不透明的中间层,带来的直接区别就是指针可以进行四则运算。指针四则运算在提供方便的同时,也相当危险,比如这段代码: 8 | 9 | ```c 10 | int main(int argc, const char * argv[]) { 11 | int array[4] = {9,10,11,12}; 12 | int array2[8] = {1,2,3,4,5,6,7,8}; 13 | 14 | for (int i = 0; i < 12; ++i) { 15 | printf("%d\n", array2[i]); 16 | } 17 | return 0; 18 | } 19 | ``` 20 | 21 | 在这个十二次的 for 循环中,前八次的输出结果毫无疑问是依次输出 `array2` 数组的每个元素。但之后的四次循环并不会发送数组越界,而是相当“正常的”输出了 `array` 数组的元素。在大部分机器上,这段代码的运行结果应该是依次输出 1 到 12 这 12 个数字。 22 | 23 | 实际上,这里的输出结果完全是由 `array` 和 `array2` 两个数组变量在内存中的布局决定的,如果增加或减少一点 `array` 数组的长度,恐怕就没有那么幸运了。 24 | 25 | 可见,透明的指针带来的问题在于,开发者知道了太多他们本来不该知道的东西(比如开发者竟然可以拿到变量的真实地址,还可以不借助变量名就访问一个变量,比如这里的 `array`)。在某些精心构造的场合下,攻击者甚至可以通过修改字符串的值来控制程序的执行逻辑,只要计算得当,他们可以调用原本根本不会被调用的函数。 26 | 27 | 在引用的概念中,引用就是引用,它什么也不是,更不可能是数字,也就谈不上什么四则运算了。因此想要通过 A 对象的引用来访问 B 对象是完全不可能的。 28 | 29 | # 指针无法检查类型 30 | 31 | 指针的灵活性除了体现在四则运算外,还包括它对类型的弱检查。实际上,因为指针仅仅是数字,它根本没有检查类型的可能。C 的 `reinterpret_cast` 方法可以将任何一个类型的指针转化为其它任何一个类型的指针,这种做法可以通过编译,但会在运行时报错。 32 | 33 | ```cpp 34 | int value = 21; 35 | Person p = reinterpret_cast (&value); 36 | ``` 37 | 38 | 比如我们可以把一个整数的指针转换成对象类型,然后到处拿去使用,最终将会得到一个 `EXC_BAD_ACCESS` 的错误。 39 | 40 | 绝大多数时候,`reinterpret_cast` 既危险,也鸡肋。倒是在计算对象的哈希值时,把指针类型转换成整数类型会有助于计算: 41 | 42 | ```cpp 43 | unsigned short Hash( void *p ) { 44 | unsigned int val = reinterpret_cast( p ); 45 | return ( unsigned short )( val ^ (val >> 16)); 46 | } 47 | ``` 48 | 49 | 而引用由于具备了中间层,完全可以在编译期进行类型检查,确保被转换的类型之间存在继承关系,从而确保安全性: 50 | 51 | ```cpp 52 | Father father = new Son(); // 正常 53 | 54 | Father father = new Father(); 55 | Son son = (Son) father; // ClassCastException 56 | ``` 57 | 58 | 至于在运行时进行类型检测,这已经是另一码事了。单就编译期而言,引用对类型的控制能力远超过指针。 59 | 60 | # 引用是未来的趋势 61 | 62 | 很多人通过分析 Swift 的优点来解释为什么 Swift 会取代 Objective-C。他们说的都对,因为这些确实是 Swift 的优点;但说的也都错,因为这些都不是取代 Objective-C 的理由。 63 | 64 | 因为 Swift 的创造者自己已经解释过了,OC 是基于 C 语言的,使用的是指针的模型,这就注定了 OC 不是一门安全的语言。而 Swift 采用了引用的模型,仅通过 `UnsafePointer` 开放了微弱的指针能力,它的命名也时刻提醒使用者,指针的操作是不安全的。 65 | 66 | 而那些所谓的 Swift 的优点,没有一个是替换 Objective-C 的理由,因为它们要么可以在 Objective-C 上引入,要么并不见得优于 Objective-C。 67 | 68 | 如果站在架构或者工程的角度来看问题的话,框架的提供者应该为使用者提供尽可能简洁的操作,一方面降低使用成本,最重要的则是减少出错概率。可能出现的错误往往会随着可以使用的操作的线性增加而几何式的增加。如何提供一些简单的,正交的基础操作,让使用者在有限、可控的操作下完成全部任务,是设计的艺术,也是对设计者的挑战。 69 | 70 | -------------------------------------------------------------------------------- /articles/string-encoding.md: -------------------------------------------------------------------------------- 1 | ## 背景 2 | 3 | 对于单纯做前端或者后端的同学来说,一般很难接触到编码问题,因为在同一个平台上,一般都是使用同一种编码方式,自然问题不大。但对于写爬虫的同学来说,编码很可能是遇到的第一个坑。这是因为字符串无法直接通过网络被传输(也不能直接被存储),需要先转换成二进制格式,再被还原。因此凡是涉及到通过网络传输字符的地方,通常都容易遇到编码问题。 4 | 5 | ## 概念定义 6 | 7 | 为了方便解释,我们首先来定义一些概念。每个开发者都知道字符串,它是一些字符的集合, 比如 `hello world` 就是一个最常见的字符串。相对来说,**字符** 比较难定义一些。从语义上来讲,它是组成字符串的最基本单位,比如这里的字母、空格,以及标点、特定语言(中文、日文)、emoji 符号等等。 8 | 9 | 字符是语言中的概念,但是计算机只认识 0 和 1 这两个数字。因此要想让计算机存储、处理字符串,就必须把字符串用二进制表示出来。在 ASCII 码中,每个英文字母都有自己对应的数字。我们通常把 ASCII 码称为**字符集**,也就是字符的集合。了解 ASCII 码的同学应该都知道小写字母 a 可以用 97 来表示,97 也被称为字符 `a` 在 ASCII 字符集中的**码位**。 10 | 11 | 如果要设计一种密码,最简单的方式就是把字母转换成它在 ASCII 码中的码位再发送,接受者则查找 ASCII 码表,还原字符。可见把字符转换成码位的过程类似于加密(encrypt),我们称之为**编码**(encode),反则则类似于解密,我们称之为**解码**(decode) 12 | 13 | ![图片](http://images.bestswifter.com/ascii-encode.png) 14 | 15 | ## 编码方式 16 | 17 | 字符转换成码位的过程是**编码**,这个过程有无数种实现方式。比如 `a -> 97`、`b -> 98` 这种就是 ASCII 编码,因为 255 = 2 ^ 8,所以所有 ASCII 编码下的码位恰好都可以由一个字节表示。 18 | 19 | ASCII 比较诞生得比较早,随着越来越多的国家开始使用计算机,0-255 这么点码位肯定不够用了。比如中国人为了展示汉字,发明了 GB2312 编码。GB2312编码完全向下兼容 ASCII 编码,也就是说 **所有 ASCII 字符集中的字符,它在 GB2312 编码下的码位与 ASCII 编码下的码位完全一致**,而中文则由两个字节表示,这也就是为什么早期我们一般认为一个中文等于两个英文的原因。 20 | 21 | ## Unicode 22 | 23 | 除了中国人自己的编码方式,各个地区的人也都根据自己的语言拓展了相应的编码方式。那么问题就来了, 给你一个码位 `0xEE 0xDD`,它到底表示什么字符,取决于它是用哪种编码方式编码的。这就好比你拿到了密文,但没有密码表一样。因此,要想正确显示一种语言,就必须携带这个语言的编码规范,要想正确显示世界上所有的语言,看起来就比较困难了。 24 | 25 | 因此 Unicode 实际上是一种统一的字符集规范,每一个 Unicode 字符由 6 个十六进制数字表示,因此理论上可以表示出 `16 ^ 6 = 16777216` 个字符,显然是绰绰有余了。 26 | 27 | ## Unicode 编码怎么样 28 | 29 | 看起来 Unicode 就是一种很棒的编码方式。诚然,Unicode 可以表示所有的字符,但过于全面带来的缺点就是过于庞大。对于字符 `a` 来说,如果使用 ASCII 编码,可以表示为 0x61,只要一个字节就能存下,但它的 Unicode 码位是 0x000061,需要三个字节。因此采用 Unicode 编码的英文内容,会比 ASCII 编码大三倍。这大大增加了文件本地存储时占用的空间以及传输时的体积。 30 | 31 | 因此,我们有了对 Unicode 字符再次编码的编码方式,常见的有 utf-8,utf-16 等。UTF 表示 Unicode Transfer Format,因此是针对 Unicode 字符集的一系列编码方式。utf-8 是一种变长编码,也就是说不同的 Unicode 字符在 utf-8 编码下的码位长度可能不同,如下表所示: 32 | 33 | |Unicode 编码(16进制)|utf-8 码位(二进制)| 34 | |:---:|:---| 35 | |000000-00007F|0xxxxxxx| 36 | |000080-0007FF|110xxxxx 10xxxxxx| 37 | |000800-00FFFF|1110xxxx 10xxxxxx 10xxxxxx| 38 | |010000-1FFFFF|11110xxx10xxxxxx10xxxxxx10xxxxxx| 39 | 40 | 这个表有两点值得注意。一个是 ASCII 字符集中的所有字符,它们的 utf-8 码位依然占用一个字节,因此 utf-8 编码下的英文字符不会向 Unicode 一样增加大小。另一个则是所有中文的 utf-8 码位都占用 3 个字节,大于 GBK 编码的 2 字节。因此如果存在明确的业务需要,是可以用 GBK 编码取代 utf-8 编码的。 41 | 42 | ![图片](http://images.bestswifter.com/Unicode.png) 43 | 44 | 尽管 utf-8 非常常用,但它可变长度的特点不仅会导致某些场景下内容过大,也为索引和随机读取带来了困难。因此在很多操作系统的内存运算中,通常使用 utf-16 编码来代替。utf-16 的特点是所有码位的最小单位都是 2 字节,虽然存在冗余,但易于索引。由于码位都是两个字节,就会存在字节序的问题。因此 utf-16 编码的字符串,一开头会有几个字节的 BOM(Byte order markd)来标记字节序,比如 `0xFF 2`(FE0x55,254) 表示 Intel CPU 的小字节序,如果不加 BOM 则默认表示大字节序。需要注意的是,某些应用会给 utf-8 编码的字节也加上 BOM。 45 | 46 | 虽然看起来问题变得复杂了,为了存储/传输一个字符,竟然需要两次编码,但别忘了,Unicode 编码是通用的,因此可以内置于操作系统内部。所以我们平时所谓的对字符串进行 utf-8 编码,其实说的是对字符串的 Unicode 码位进行 utf-8 编码。 47 | 48 | 这一点在 python3 中得到了充分的体现,字符串由字符组成,每一个字符都是一个 Unicode 码位。 49 | 50 | ## 编解码错误处理 51 | 52 | 如果把编解码理解成利用密码表进行加解密,那么就容易理解,为什么编码和解码过程都是易错的。 53 | 54 | 如果被编码的 Unicode 字符,在某种编码中根本没有列出编码方式,这个字符就无法被编码: 55 | 56 | ```python 57 | city = 'São Paulo' 58 | b_city = city.encode('cp437') 59 | # UnicodeEncodeError: 'charmap' codec can't encode character '\xe3' in position 1: character maps to 60 | 61 | b_city = city.encode('cp437', errors='ignore') 62 | # b'So Paulo' 63 | 64 | b_city = city.encode('cp437', errors='replace') 65 | # b'S?o Paulo' 66 | ``` 67 | 68 | 同理,如果被解码的码位,在编码表中找不到与之对应的字符,这个码位就无法被解码: 69 | 70 | ```python 71 | octets = b'Montr\xe9al' 72 | s_octest1 = octets.decode('utf8') 73 | # UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe9 in position 5: invalid continuation byte 74 | 75 | s_octest1 = octets.decode('cp1252') 76 | # Montréal 77 | 78 | s_octest1 = octets.decode('iso8859_7') 79 | # Montrιal 80 | 81 | s_octest1 = octets.decode('utf8', errors='replace') 82 | # Montr�al 83 | ``` 84 | 85 | python 的解决方案是,`encode` 和 `decode` 函数都有一个参数 `errors` 可以控制如何处理无法被编、解码的内容。它的值可以是 `ignore`(忽略这个错误并继续执行),也可以是 `replace`(用系统的占位符填充)。 86 | 87 | 一般来说,无法从码位推断出编码方式,就像你不可能从密文推断出加密方式一样。但是某些编码方式会留下非常显著的特征,一旦这些特征频繁出现,基本就可以断定编码方式。Python 提供了一个名为 `Chardet` 的包,可以帮助开发者推断出编码方式,并且会给出相应的置信度。置信度越高,说明是这种编码方式的可能性越大。 88 | 89 | ```python 90 | octets = b'Montr\xe9al' 91 | chardet.detect(octets) 92 | # {'encoding': 'ISO-8859-1', 'confidence': 0.73, 'language': ''} 93 | 94 | octets.decode('ISO-8859-1') 95 | # Montréal 96 | ``` 97 | 98 | ## 总结 99 | 100 | 1. 编码是为了把人类人类可读的字符转换成计算机容易存储和传输的二进制,解码反之,编码后得到的结果称之为码位。 101 | 2. Unicode 是一种通用字符集,从字符到 Unicode 字符集中码位的转换也可以叫做 Unicode 编码 102 | 3. Unicode 编码对英文字符不友好,因此出现了针对 Unicode 码位的再次编码,比如 utf-8,希望在节省空间的同时保留强大的表达能力 103 | 4. 各个编码之间的关系如下图所示: 104 | 105 | ![](http://images.bestswifter.com/encode-relationship.png) 106 | -------------------------------------------------------------------------------- /articles/swift-assembly-1-pwt.md: -------------------------------------------------------------------------------- 1 | # Swift 汇编(一)Protocol Witness Table 初探 2 | 3 | 由于工作中接触到 Swift 汇编与逆向知识,所以整理了这篇博客。内容与顺序无关,第一篇文章并非入门,单纯只是第一篇文章。建议有一定汇编基础的读者学习。 4 | 5 | > 编译环境: 6 | > MacOS 11.3.1 x86_64 7 | > Swift: version 5.4 8 | > 汇编风格:intel 9 | 10 | ## 什么是 Protocol Witness Table 11 | 12 | 我们知道 C 函数调用是静态派发,简单来说可以理解为是用汇编命令 `call $address` 来实现。这种方式效率最高,但是灵活性不够。 13 | 14 | OC 的方法调用完全是基于动态派发,总是调用 `objc_msgSend` 实现。这种方式非常灵活,允许各种 Hook 黑科技,但是流程最长,效率最低。 15 | 16 | 在 Swift 中,协议方法的调用,使用协议方法表的方式完成,也就是 Protocol Witness Table,下文简称 PWT。参考下面这段代码: 17 | 18 | ```swift 19 | protocol Drawable { 20 | func draw() -> Int 21 | } 22 | 23 | struct Point: Drawable { 24 | var x, y: Int 25 | func draw() -> Int { 26 | return x + y 27 | } 28 | } 29 | 30 | struct Line: Drawable { 31 | var length: Int 32 | func draw() -> Int { 33 | return length 34 | } 35 | } 36 | 37 | func foo() -> Int { 38 | let p: Drawable = xxx 39 | return p.draw() 40 | } 41 | ``` 42 | 43 | 在 foo 函数中,变量 p 并没有明确的类型,只知道它遵守 `Drawable` 协议,实现了 `draw` 方法。但是编译时并不能知道,调用的是结构体 `Line` 还是 `Point` 的 `draw` 方法。 44 | 45 | 因此,PWT 的实现方式是:每个类都会有一个方法表(通过数组来实现),里面保存了它用于实现协议的函数的地址。只要知道一个类的信息和函数信息,就可以实现函数调用。这个方法表,就是 PWT。 46 | 47 | ## PWT 的汇编实现 48 | 49 | 除了从理论上了解 PWT 的概念,我们还可以从汇编角度来实际感受一下。参考下面这段代码: 50 | 51 | ```swift 52 | protocol Drawable { 53 | func draw() -> Int 54 | } 55 | 56 | struct Point: Drawable { 57 | var x, y: Int 58 | func draw() -> Int { 59 | return x + y 60 | } 61 | } 62 | 63 | func foo() -> Int { 64 | let p: Drawable = Point(x: 1, y: 2) 65 | return p.draw() 66 | } 67 | ``` 68 | 69 | Debug 模式下的汇编代码: 70 | 71 | ```assembly 72 | swift-ui-test`foo(): 73 | 0x10518a860 <+0>: push rbp 74 | 0x10518a861 <+1>: mov rbp, rsp 75 | 0x10518a864 <+4>: push r13 76 | 0x10518a866 <+6>: sub rsp, 0x48 77 | 0x10518a86a <+10>: mov edi, 0x1 78 | 0x10518a86f <+15>: mov esi, 0x2 79 | 0x10518a874 <+20>: call 0x10518ae50 ; swift_ui_test.Point.init(x: Swift.Int, y: Swift.Int) -> swift_ui_test.Point at ContentView.swift:27 80 | 0x10518a879 <+25>: lea rcx, [rip + 0x1948] ; type metadata for swift_ui_test.Point 81 | 0x10518a880 <+32>: mov qword ptr [rbp - 0x18], rcx 82 | 0x10518a884 <+36>: lea rcx, [rip + 0x189d] ; protocol witness table for swift_ui_test.Point : swift_ui_test.Drawable in swift_ui_test 83 | 0x10518a88b <+43>: mov qword ptr [rbp - 0x10], rcx 84 | 0x10518a88f <+47>: mov qword ptr [rbp - 0x30], rax 85 | 0x10518a893 <+51>: mov qword ptr [rbp - 0x28], rdx 86 | 0x10518a897 <+55>: mov rax, qword ptr [rbp - 0x18] 87 | 0x10518a89b <+59>: mov rcx, qword ptr [rbp - 0x10] 88 | 0x10518a89f <+63>: lea rdx, [rbp - 0x30] 89 | 0x10518a8a3 <+67>: mov rdi, rdx 90 | 0x10518a8a6 <+70>: mov rsi, rax 91 | 0x10518a8a9 <+73>: mov qword ptr [rbp - 0x38], rax 92 | 0x10518a8ad <+77>: mov qword ptr [rbp - 0x40], rcx 93 | 0x10518a8b1 <+81>: mov qword ptr [rbp - 0x48], rdx 94 | 0x10518a8b5 <+85>: call 0x10518ae60 ; __swift_project_boxed_opaque_existential_1 at 95 | 0x10518a8ba <+90>: mov rcx, qword ptr [rbp - 0x40] 96 | 0x10518a8be <+94>: mov rdx, qword ptr [rcx + 0x8] 97 | 0x10518a8c2 <+98>: mov r13, rax 98 | 0x10518a8c5 <+101>: mov rdi, qword ptr [rbp - 0x38] 99 | 0x10518a8c9 <+105>: mov rsi, rcx 100 | 0x10518a8cc <+108>: call rdx 101 | -> 0x10518a8ce <+110>: mov rdi, qword ptr [rbp - 0x48] 102 | 0x10518a8d2 <+114>: mov qword ptr [rbp - 0x50], rax 103 | 0x10518a8d6 <+118>: call 0x10518aec0 ; __swift_destroy_boxed_opaque_existential_1 at 104 | 0x10518a8db <+123>: mov rax, qword ptr [rbp - 0x50] 105 | 0x10518a8df <+127>: add rsp, 0x48 106 | 0x10518a8e3 <+131>: pop r13 107 | 0x10518a8e5 <+133>: pop rbp 108 | 0x10518a8e6 <+134>: ret 109 | ``` 110 | 111 | 首先按照函数调用来分割下,这里实现了结构体的初始化工作: 112 | 113 | ```Assembly 114 | 0x10518a86a <+10>: mov edi, 0x1 115 | 0x10518a86f <+15>: mov esi, 0x2 116 | 0x10518a874 <+20>: call 0x10518ae50 ; swift_ui_test.Point.init(x: Swift.Int, y: Swift.Int) -> swift_ui_test.Point at ContentView.swift:27 117 | ``` 118 | 119 | 根据结构体的调用惯例,可以知道返回值是通过 `rax` 和 `rdx` 两个寄存器返回的。当然也可以看下这个函数的内部实现来验证下。顺便也能看出,Debug 模式下对于理解汇编代码和进行反汇编都是非常友好的,非常耿直的用一个函数调用告诉我们这里实在创建结构体实例。如果是 Release 模式,大概率是直接对 `rax` 和 `rdx` 赋值了。 120 | 121 | 接下来分别把 `metadata` 和 `Point` 类的 PWT 表取出,存到栈上。注意到下一个 call 的函数是 `__swift_project_boxed_opaque_existential_1 at `,它的存在是由于我们的这种写法导致: 122 | 123 | ```swift 124 | let p: Drawable = Point(x: 1, y: 2) 125 | ``` 126 | 127 | 这里的 p 就是一个 existential 对象,`Drawble` 协议是一个 existential type,具体的解释可以参考[这篇文章](https://stackoverflow.com/a/59183168)。简答说结论,这个函数调用以后,入参寄存器 `rdi` 的内容会被赋值给 `rax` 寄存器来当做返回值。 128 | 129 | 注意到这个函数的入参 `rdi` 寄存器,是由下面几个关键路径构成的 130 | 131 | ```Assembly 132 | 0x10518a89f <+63>: lea rdx, [rbp - 0x30] 133 | 0x10518a8a3 <+67>: mov rdi, rdx 134 | ``` 135 | 136 | 所以返回值 `rax` ,其实就是栈基址 `rbp` 减掉 `0x30`,这个地址内存贮的值,是结构体的第一个成员变量 `x = 1`。顺便说一下,这个地址向上(高地址方向)偏移 8 字节,存储的是第二个成员变量 `y = 2`。 137 | 138 | 下一个关键操作是 `call rdx`,它的取值来源是: 139 | 140 | ```Assembly 141 | 0x1073be8be <+94>: mov rdx, qword ptr [rcx + 0x8] 142 | ``` 143 | 144 | 这里的 `rcx` 经过几次存储、取出,可以跟踪到它最初的源头,就是: 145 | 146 | ```Assembly 147 | 0x1073be884 <+36>: lea rcx, [rip + 0x189d] ; protocol witness table for swift_ui_test.Point : swift_ui_test.Drawable in swift_ui_test 148 | ``` 149 | 150 | 从逻辑上看,调用了 `PWT` 内存地址 + 0x8 位置的函数。可以具体看下这里到底存了何方神圣: 151 | 152 | ![](https://images.bestswifter.com/mweb/16211625755898.jpg) 153 | 154 | 155 | * 首先看下 [rip + 0x189d] 的值是多少。在执行这行命令时,`rip` 的值是下一行命令的地址,即 `0x1073be88b`,相加后得到 `0x000000010518c128` 156 | * 由于 Hopper、MachoView 等工具只能显示相对偏移,因此要先减去当前程序在内存中的偏移。可以用 `image list swift-ui-test` 来查看 157 | * 得到结果是 `0x4128` 158 | 159 | 160 | 所以 `0x4128` 就是 `Point` 结构体的 PWT 的位置,可以在 Hopper 中验证下: 161 | 162 | ![](https://images.bestswifter.com/mweb/16211626037785.jpg) 163 | 164 | 这里其实是一个指针数组,第一个指针是 `0x100003998`,内容如下,暂时没有深入研究其中存储内容的含义,但是可以看出名字是:`protocol conformance descriptor for swift_ui_test.Point : swift_ui_test.Drawable in swift_ui_test` 165 | 166 | ![](https://images.bestswifter.com/mweb/16211626795756.jpg) 167 | 168 | 第二个指针是 `0x100002ff0`,跳转过去看下: 169 | 170 | ![](https://images.bestswifter.com/mweb/16211627465248.jpg) 171 | 172 | 从 demangle 后的结果也能看出来,这是一个遵守了协议的证明(Protocol Witness),遵守的协议函数是:`Drawable.draw() -> Swift.Int`,结构体是 `Point`,协议名是 `Drawable` 173 | 174 | 因此 `call rdx` 实际上就是调用 `call 0x100002ff0` 175 | 176 | 再来对比下入参和参数,`rax` 被作为 `r13` 传入了。函数内部分别把 `r13` 和 `r13 + 8` 的位置读出来,放入 `rdi` 和 `rsi` 寄存器。正如前文所述,`r13/rax` 这个地址上,存储的是 `x` 的值,`+0x8` 则存储了 `y` 的值。因此可以理解为把结构体 `p` 传入了。 177 | 178 | 最后调用了 `$s13swift_ui_test5PointV4drawSiyF` 这个函数符号,内部逻辑有点啰嗦,猜测是 Debug 环境导致,但本质上就是一个加法运算。 179 | 180 | ## 结论 181 | 182 | 至此 PWT 的调用链路就分析结束了。可以得到如下结论: 183 | 184 | * PWT 是为了解决协议方法调用在编译时无法确定地址,而引入的中间层 185 | * 每个遵守了协议的类,都会有自己的 PWT。遵守的协议中函数越多,PWT 中存储的函数地址就越多。 186 | * 准确来说,PWT 是指针数组,但是第一个指针并不是函数指针,而是 `protocol conformance descriptor`,从第二个开始才是函数指针。如果有读者知道这个 `conformance descriptor` 中存储信息的含义,欢迎指教 187 | * 对协议方法的调用,首先会调用一个 `PWT address + offset` 这个函数,这个函数被叫做 `protocol witness`,它的内部会做一些参数处理,最后再调用真实的函数 188 | * 对于实际被调用的来说,只看它的内部实现,无法和其它函数做出区分。但是可以观察它的 caller,如果是一个 `protocol witness` 就可以说明。 189 | -------------------------------------------------------------------------------- /articles/swift-interface.md: -------------------------------------------------------------------------------- 1 | # 浅谈 swiftinterface 文件 2 | 3 | 按照惯例,首先介绍下什么是 `.swiftinterface` 文件。这个文件的作用,主要是用于描述一个 Swift 模块的公开信息。如果单从形态上来说,可以简单类比 C 系列语言的 `.h` 头文件。 4 | 5 | 但最重要的区别是,`.swiftinterface` 文件除了描述包括结构、变量、函数在内的各种公开符号以外,它还负责描述模块稳定性相关的信息,因此在这个文件内可以看到很多和内联相关的逻辑。 6 | 7 | 另外需要掌握的一个关键概念是,`.swiftinterface` 文件在编译模块时由编译器产生,并且会参与模块的消费方的编译流程。在下文中会具体解释这句话。 8 | 9 | ## 模块稳定性(`Module Stability`) 10 | 11 | 模块稳定性是一种比二进制稳定性更加要求严格的稳定性。假设目前有上游模块 A 依赖下游模块 B,二进制稳定性指的是,即使模块 A 和 B 分别由不同版本的 Swift 编译器构建,他们组合在一起要能正常工作。 12 | 13 | 而模块稳定性,则是在模块 B 的内部发生正常优化(不包括删除 API 这样的行为)时,还要满足: 14 | 15 | 1. 即使是比较古老的 A 模块,也要能和更新后的 B 模块组合起来正常工作 16 | 2. 即使 A 模块有了改动,只要还在调用当前 B 模块的公开接口,也要能正常工作 17 | 18 | 具体到技术和实现层面,模块稳定性其实可以细分为它的内部实现和对外描述两个话题来讨论。从内部实现角度来看,模块稳定性在一般情况下总是能被满足的,但是一旦涉及到内联相关的优化,就需要单独考虑了。 19 | 20 | 比如这段代码: 21 | 22 | ```swift 23 | public struct People { 24 | 25 | internal var name: String 26 | 27 | internal var age: Int 28 | 29 | public init(name: String, age: Int) { 30 | self.name = name 31 | self.age = age 32 | } 33 | 34 | } 35 | ``` 36 | 37 | 这是一个很简单的 `People` 结构体,如果把它构建出一个单独的模块,那就就是具备模块稳定性的。但有些时候,我们会觉得它的初始化函数过于简单,没有必要发生一次函数调用, 所以可以标记为 `@inlinable`,告诉编译器可以考虑对它的初始化函数做优化。 38 | 39 | 代码如下所示: 40 | 41 | ```swift 42 | public struct People { 43 | 44 | @usableFromInline 45 | internal var name: String 46 | 47 | @usableFromInline 48 | internal var age: Int 49 | 50 | @inlinable 51 | public init(name: String, age: Int) { 52 | self.name = name 53 | self.age = age 54 | } 55 | 56 | } 57 | 58 | ``` 59 | 60 | 由于 `@inlinable` 是可以跨模块的,此时的 `People` 结构体就不再具备模块稳定性。原因是:当它的上游组件在调用 `init` 函数时,可能就不是进行函数调用,而是直接在栈上分配一块内存,在 `0x0` 的偏移位置放一个字符串,然后在 `0x8` 的位置放一个整型数字。 61 | 62 | 如果后续 `People` 结构发生了变化,比如第一个成员不再是 `age`,而是添加了一个 `var id: UUID`。此时重新发布的 `People` 模块,就无法与之前编译的上游代码共存了。 63 | 64 | 但就这个 case 来看,解决方案也很简单,可以把类型标记为 `@frozen` 来承诺后续不会发生布局改动,或者不再把 `init` 函数标记为 `@inlinable`。 65 | 66 | 如果想要知道自己的代码是否具备模块稳定性,可以在 Build Settings 中找到并且打开 **`Build Libraries for Distribution`** 这个选项。打开这个选项等价于增加两个 `swiftc` 的编译参数:`-enable-library-evolution` 和 `-emit-module-interface-path`。 67 | 68 | `-enable-library-evolution` 选项会以模块稳定的方式编译代码,将 `@inlinable` 这类函数用到的变量特殊处理,从直接的内存偏移修改为间接访问。它对应的是模块稳定性的内部实现部分。 69 | 70 | 为了更好的解释模块稳定性的原理,以及我们是如何在性能和灵活性之间进行取舍的,这里再举一个简单的例子,修改代码如下: 71 | 72 | ```swift 73 | public struct People { 74 | 75 | internal var name: String 76 | 77 | @usableFromInline 78 | internal var age: Int 79 | 80 | public init(name: String, age: Int) { 81 | self.name = name 82 | self.age = age 83 | } 84 | 85 | @inlinable 86 | public func nextAge() -> Int { 87 | return self.age + 1 88 | } 89 | 90 | } 91 | ``` 92 | 93 | 这样的 `Poeple` 也是满足模块稳定性的,在启用 `-enable-library-evolution` 编译参数时,它的 `nextAge` 函数实现如下: 94 | 95 | ![inlinable-stability](https://images.bestswifter.com/mweb/inlinable-stability.png) 96 | 97 | 其中的 `rax` 来自于 `age.setter` 这个函数的偏移 98 | 99 | ![inlinable-age-setter](https://images.bestswifter.com/mweb/inlinable-age-setter.png) 100 | 101 | 由于 `nextAge` 函数只依赖 `age.setter` 的存在,所以后续不管 `People` 结构体的布局如何发生变化,只要这个函数还存在,都可以正确的读取到 `age` 的值。 102 | 103 | 如果不使用 `-enable-library-evolution` 参数,或者把 `People` 结构体标记为 `@frozen`,就意味着我们无需考虑结构体布局发生变动,此时的 `nextAge` 函数实现如下: 104 | 105 | ![inlinable-none-stability](https://images.bestswifter.com/mweb/inlinable-none-stability.png) 106 | 107 | 这种写法实际上依赖于小结构体可以以 `loadable` 的方式加载在寄存器中,`rdx` 即表示了 `age` 的偏移情况。一旦 `People` 的结构发生变化,`rdx` 寄存器就几乎不可能再能用于表示 `age`,这样的实现如果被内联到客户端的代码中,是万万不能保证模块稳定性的。 108 | 109 | ## interface 文件介绍 110 | 111 | 上一节说到,打开 **`Build Libraries for Distribution`** 这个选项本质上是增加两个 `swiftc` 的编译参数:`-enable-library-evolution` 和 `-emit-module-interface-path`。并且介绍了前者是如何影响编译器生成对应的函数实现的, 112 | 113 | 后一个参数的作用就是生成本文的主角: `.swiftinterface` 文件,它对应了模块稳定性的对外描述部分。其实也可以单独使用 `-emit-module-interface-path` 这个参数进行编译,虽然也可以产生 `.swiftinterface` 文件,但是会报警告:`warning: module interfaces are only supported with -enable-library-evolution`,表示 `.swiftinterface` 文件必须配合 `-enable-library-evolution` 参数使用。 114 | 115 | `.swiftinterface` 文件是 Swift 在 5.1 版本引入的概念,它以文本的方式描述了一个模块的所有公开符号信息(包括内联相关)。在此之前,相关的信息保存在 `.swiftmodule` 文件中,这个文件是二进制格式的,不可读,且在不同版本的 Swift 编译下的实现都不一样。这也就意味着任何存在两个依赖关系二进制模块,都必须使用相同的 Swift 版本进行编译。否则依赖方就无法正确的解析被依赖方的公开符号信息。与此相比,`.swiftinterface` 文件最大的优势是,高版本的编译器总是可以兼容低版本编译器产生的 `.swiftinterface` 文件。 116 | 117 | 上一节的代码编译后产出的 `.swiftinterface` 文件如下: 118 | 119 | ```swift 120 | // swift-interface-format-version: 1.0 121 | // swift-compiler-version: Apple Swift version 5.5.1 (swiftlang-1300.0.31.4 clang-1300.0.29.6) 122 | // swift-module-flags: -target x86_64-apple-ios15.0-simulator -enable-objc-interop -enable-library-evolution -swift-version 5 -enforce-exclusivity=checked -O -module-name TestLibraryEvolutoin 123 | import Swift 124 | import _Concurrency 125 | @frozen public struct People { 126 | internal var name: Swift.String 127 | @usableFromInline 128 | internal var age: Swift.Int 129 | public init(name: Swift.String, age: Swift.Int) 130 | @inlinable public func nextAge() -> Swift.Int { 131 | return self.age &+ 1 132 | } 133 | } 134 | extension TestLibraryEvolutoin.People : Swift.Sendable {} 135 | ``` 136 | 137 | 这里就引出了文章开头重点强调的一个概念:**`.swiftinterface` 文件在编译模块时由编译器产生,并且会参与模块的消费方的编译流程** 138 | 139 | 当我们在客户端代码中引入 `Person` 这个模块时,使用的是客户端的 Swift 编译器。此时编译器首先会检查第三行注释中有没有 `-enable-library-evolution` 编译参数,如果没有,那么编译器认为被依赖的 `Person` 模块不具备模块稳定性。此时相当于降级到了 Swift 5.1 之前的模式,会判断生成这个 `.swiftinterface` 文件的编译器版本,和消费的编译器版本是否一致。如果不一致就会报编译错误: 140 | 141 | ![error](https://images.bestswifter.com/mweb/error.png) 142 | 143 | 如果根据报错信息去搜索,得到的建议一般是让 `Person` 这个模块,先打开 `Build Libraries for Distribution` 选项,再交付具有模块稳定性的二进制产物。 144 | 145 | 但是根据笔者的经验,还有两类常见的问题,也会导致类似的错误,需要加以注意: 146 | 147 | 1. 这个 `.swiftinterface` 文件会被使用方的编译器消费,并且参与编译流程,但是这个文件本身并非总是能通过编译的。比如引用了一些不存在的模块,甚至是被人为误改。但是 Swift 编译器的处理原则是:只要 `.swiftinterface` 文件无法被正确消费,都会报上述错误。如果不清楚这个细节,就可能导致错过了真正有问题的地方。 148 | 2. 高版本的 Swift 编译器可能会用到一些低版本编译器不支持的特性。因此模块稳定性不是无条件的,它要求消费方的编译器版本不低于生产方的版本。 149 | 150 | 一个简单的例子就是这里的 `import _Concurrency`。根据测试,只要使用 Xcode13 以上进行编译(对应 Swift5.5+),编译器都会在生成的 `.swiftinterface` 文件中自动补上这句话。 151 | 152 | ## @_alwaysEmitIntoClient 153 | 154 | `.swiftinterface` 文件除了要符合语法要求,它还会参与到编译,并且影响客户端代码的行为。一个典型的例子就是 `@_alwaysEmitIntoClient` 这个关键字的使用。 155 | 156 | 我们知道系统动态库是跟随 iOS 系统发布的,预置在操作系统中,比如最典型的 `UIKit`。笔者曾经一度以为,一旦已经发布的系统库,API 和行为就是完全确定,不可更改的。直到 iOS15 系统发布后,我突然发现了一个支持到 iOS13 的 API,而这个 API 在笔者的印象中,绝对不存在与 iOS13 上。 157 | 158 | 苹果实现这种为已发布的 Swift 二进制库打补丁的能力,就是通过 `@_alwaysEmitIntoClient` 关键字来实现的。对于被标记为 `@_alwaysEmitIntoClient` 的函数,它的函数实现也会完整的出现在 `.swiftinterface` 文件中,如果说 `@inlinable` 是告诉编译器这个函数可以被内联,那么 `@_alwaysEmitIntoClient` 就是告诉编译器,这个函数一定要被内联到客户端代码中。 159 | 160 | 假设我们的 `Person` 模块是一个系统库,对于已经发布的 `Person` 模块来说,它只有 `nextAge` 函数,并且已经预置在了所有的 iOS 系统中。此时我们可以新增一个 `previousAge` 函数,并且把它标记为 `@_alwaysEmitIntoClient`。只要新的 Xcode 携带了这份 `.swiftinterface` 文件,那么在使用新 Xcode 编译时,用户就可以使用 `previousAge` 函数,并且可以通过编译。 161 | 162 | 在实现层面,虽然已经存在的 `Person` 二进制并没有这个函数,但是在编译时,编译器看到了这个关键字,就不会真的调用 `previousAge` 函数,而是把函数的实现内联在调用方的代码中。只要这个函数的实现在低版本存在对应符号,就可以组合出任意多的新函数。 163 | 164 | 不过通过 `@_alwaysEmitIntoClient` 来打补丁,做到向前兼容,除了工程上不合理,污染 `.swiftinterface` 文件外,另一个缺点在于包大小的劣化。因为本来属于模块内的实现,只需要定义一个函数,现在会被内联分散到所有的调用方,个人并不推荐使用这种技巧。 165 | 166 | ## 总结 167 | 168 | 本文主要介绍了 `.swiftinterface` 文件和模块稳定性的背景,它的工作原理以及常见的误区,最后介绍了 `@_alwaysEmitIntoClient` 关键字以及它的实现原理。有一些简单的结论可供参考: 169 | 170 | 1. 模块稳定性主要可以分为内部实现和外部描述两个方面 171 | 1. 内部实现主要是在内联优化与后续迭代灵活性之间进行的取舍 172 | 2. 外部描述主要是 Swift 5.1 开始新引入的 `.swiftinterface` 文件,取代了此前版本间不兼容的 `.swiftmodule` 文件 173 | 2. 在 Xcode 中打开 `Build Libraries for Distribution` 开关,启用模块稳定性,本质是增加了两个 `swiftc` 的编译参数:`-enable-library-evolution` 和 `-emit-module-interface-path`。分别对应了内部的实现和对外描述 174 | 3. `.swiftinterface` 文件由构建方的编译器生成,但是会参与到使用方的编译流程中。不管是文件内容有语法错误,还是使用方的编译器版本低于生产方,都会报相同的编译错误,需要掌握排查技巧 175 | 4. 基于 `.swiftinterface` 文件的工作原理,苹果可以通过 `@_alwaysEmitIntoClient` 关键字并且在新的 Xcode 中更新 `.swiftinterface` 文件,从而为已经在低版本 iOS 系统上发布的系统库增加新的 API -------------------------------------------------------------------------------- /articles/swift-object-compare.md: -------------------------------------------------------------------------------- 1 | 今天突然想到一个问题,让我觉得有必要总结一下switch语句。我们知道swift中的switch,远比C语言只能比较整数强大得多,但问题来了,哪些类型可以放到switch中比较呢,对象可以比较么? 2 | 3 | [官方文档](https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/ControlFlow.html#//apple_ref/doc/uid/TP40014097-CH9-ID120)对switch的用法给出了这样的解释: 4 | 5 | > Cases can match many different patterns, including interval matches, tuples, and casts to a specific type. 6 | 7 | 也就是说除了最常用的比较整数、字符串等等之外,switch还可以用来匹配范围、元组,转化成某个特定类型等等。但文档里这个**including**用的实在是无语,因为它没有指明所有可以放在switch中比较的类型,文章开头提出的问题依然没有答案。 8 | 9 | 我们不妨动手试一下,用switch匹配对象: 10 | 11 | ```swift 12 | class A { 13 | 14 | } 15 | 16 | var o = A() 17 | var o1 = A() 18 | var o2 = A() 19 | 20 | switch o { 21 | case o1: 22 | print("it is o1") 23 | case o2: 24 | print("it is o2") 25 | default: 26 | print("not o1 or o2") 27 | } 28 | ``` 29 | 30 | 果然,编译器报错了:“Expression pattern of type 'A' cannot match values of type 'A'”。至少我们目前还不明白“expression pattern”是什么,怎么类型A就不能匹配类型A了。 31 | 32 | 我们做一下改动,在`case`语句后面加上`let`: 33 | 34 | ```swift 35 | switch o { 36 | case let o1: 37 | print("it is o1") 38 | case let o2: 39 | print("it is o2") 40 | default: 41 | print("not o1 or o2") 42 | } 43 | ``` 44 | 45 | OK,编译运行,结果是:`it is o1`。这是因为`case let`不是匹配值,而是值绑定,也就是把o的值赋给临时变量o1,这在o是可选类型时很有用,类似于`if let`那样的隐式解析可选类型。没有打出`it is o2`是因为swift中的switch,只匹配第一个相符的case,然后就结束了,即使不写`break`也不会跳到后面的case。 46 | 47 | 扯远了,回到话题上来,既然添加`let`不行,我们得想别的办法。这时候不妨考虑一下`switch`语句是怎么实现的。据我个人猜测,估计类似于用了好多个if判断有没有匹配的case,那既然如此,我们给类型A重载一下`==`运算符试试: 48 | 49 | ```swift 50 | class A {} 51 | 52 | func == (lhs: A, rhs: A) -> Bool { return true } 53 | 54 | var o = A(); var o1 = A() ;var o2 = A() 55 | 56 | switch o { 57 | case o1: 58 | print("it is o1") 59 | case o2: 60 | print("it is o2") 61 | default: 62 | print("not o1 or o2") 63 | } 64 | ``` 65 | 66 | 很显然,又失败了。如果这就能搞定问题,那这篇文章也太水了。报错信息和之前一样。可问题是我们已经重载了`==`运算符,为什么A类型还是不能饿匹配A类型呢,难道switch不用判断两个变量是否相等么。 67 | 68 | switch作为一个多条件匹配的语句,自然是要判断变量是否相等的,不过它不是通过`==`运算符判断,而是通过`~=`运算符。再来看一段[官方文档](https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Patterns.html#//apple_ref/doc/uid/TP40014097-CH36-ID419)的解释: 69 | 70 | > An expression pattern represents the value of an expression. Expression patterns appear only in switch statement case labels. 71 | 72 | 以及这句话: 73 | 74 | > The expression represented by the expression pattern is compared with the value of an input expression using the Swift standard library ~= operator. 75 | 76 | 第一句解释了之前的报错,所谓的“express pattern”是指表达式的值,这个概念只在switch的case标签中有。所以之前的报错信息是说:“o1这个表达式的值(还是o1)与传入的参数o都是类型A的,但它们无法匹配”。至于为什么不能匹配,答案在第二句话中,因为o1和o的匹配是通过调用标准库中的`~=`运算符完成的。 77 | 78 | 所以,只要把重载`==`换成重载`~=`就可以了。改动一个字符,别的都不用改,然后程序就可以运行了。Swift默认在`~=`运算符中调用`==`运算符,这也就是为什么我们感觉不到匹配整数类型需要什么额外处理。但对于自定义类型来说,不重载`~=`运算符,就算你重载了`==`也是没用的。 79 | 80 | 除此以外,还有一种解决方法,那就是让A类型实现`Equatable`协议。这样就不需要重载`~=`运算符了。答案就在Swift的module的最后几行: 81 | 82 | ```swift 83 | @warn_unused_result 84 | public func ~=(a: T, b: T) -> Bool 85 | ``` 86 | 87 | Swift已经为所有实现了`Equatable `协议的类重载了`~=`运算符。虽然实现`Equatable `协议只要求重载`==`运算符,但如果你不显式的注明遵守了`Equatable `协议,swift是无法知道的。因此,如果你重载了`==`运算符,就顺手标注一下实现了`Equatable `协议吧,这样还有很多好处,比如`SequenceType`的`split`方法等。 88 | 89 | 最后总结一句: 90 | > 能放在switch语句中的类型必须重载`~=`运算符,或者实现`Equatable`协议。 -------------------------------------------------------------------------------- /articles/swift-tips.md: -------------------------------------------------------------------------------- 1 | 本文会详细介绍一些Swift中不为大多数人知,又很有用的知识点。您不必一次性看完,不过或许哪一天这些知识就能派上用场,项目Demo在[我的github](https://github.com/bestswifter/MySampleCode/tree/master/SwiftMysterious),您可以下载下来亲自实验一番,如果觉得有用还望点个star以示支持。 2 | 3 | 本文主要的知识点有: 4 | 5 | * @noescape和@autoclosure 6 | * 内联lazy属性 7 | * 函数柯里化 8 | * 可变参数 9 | * dynamic关键字 10 | * 一些特殊的字面量 11 | * 循环标签 12 | 13 | ## @noescape和@autoclosure 14 | 15 | 关于这两个关键字的含义,在我此前的文章——[第六章——函数(自动闭包和内存)](http://www.jianshu.com/p/f9ba4c41d9c7)中已经有详细的解释,这里就简单总结概括一下: 16 | 17 | * @noescape:这个关键字告诉编译器,参数闭包只能在函数内部使用。它不能被赋值给临时变量,不能异步调用,也不能作为未标记为@noescape的参数传递给其他函数。总之您可以放心,它无法在这个函数作用域之外使用。 18 | 19 | 除了安全性上的保证,swift还会为标记为@noescape的参数做一些优化,闭包内访问类的成员时您还可以省去`self.`的语法。 20 | 21 | * @autoclosure:这个关键字将表达式封装成闭包,优点在于延迟了表达式的执行,缺点是如果滥用会导致代码可读性降低。 22 | 23 | ## 内联lazy属性 24 | 25 | 标记为lazy的属性在对象初始化时不会被创建,它直到第一次被访问时才会创建,通常情况下它是这样实现的: 26 | 27 | ```swift 28 | class PersonOld { 29 | lazy var expensiveObject: ExpensiveObject = { 30 | return self.createExpensiveObject() // 传统实现方式 31 | }() 32 | 33 | private func createExpensiveObject() -> ExpensiveObject { 34 | return ExpensiveObject() 35 | } 36 | } 37 | ``` 38 | 39 | lazy属性本质上是一个闭包,闭包中的表达式只会调用一次。需要强调的是,虽然这个闭包中捕获了`self`,但是这样做并不会导致循环引用,猜测是Swift自动把`self`标记为unowned了。 40 | 41 | 这样的写法其实可以进行简化,简化后的实现如下: 42 | 43 | ```swift 44 | class Person { 45 | lazy var expensiveObject: ExpensiveObject = self.createExpensiveObject() 46 | 47 | private func createExpensiveObject() -> ExpensiveObject { 48 | return ExpensiveObject() 49 | } 50 | } 51 | ``` 52 | 53 | ## 函数柯里化 54 | 55 | 函数柯里化也是一个老生常谈的问题了,我的这篇文章——[第六章——函数(函数的便捷性)](http://www.jianshu.com/p/b2d21b85a387)对其有比较详细的解释。 56 | 57 | 简单来说,柯里化函数处理一个参数,然后返回一个函数处理剩下来的所有参数。直观上来看,它避免了很多括号的嵌套,提高了代码的简洁性和可读性,比如这个函数: 58 | 59 | ```swift 60 | func fourChainedFunctions(a: Int) -> (Int -> (Int -> (Int -> Int))) { 61 | return { b in 62 | return { c in 63 | return { d in 64 | return a + b + c + d 65 | } 66 | } 67 | } 68 | } 69 | fourChainedFunctions(1)(2)(3)(4) 70 | ``` 71 | 72 | 对比一下它的柯里化版本: 73 | 74 | ```swift 75 | func fourChainedFunctions(a: Int)(b: Int)(c: Int)(d: Int) -> Int { 76 | return a + b + c + d 77 | } 78 | ``` 79 | 80 | 不过在 Swift 3.0 中,这种柯里化语法会被移除,你需要使用之前完整的函数声明。感谢 [@没故事的卓同学](http://www.jianshu.com/users/88a056103c02/latest_articles) 指出。 81 | 82 | 您可以在[Swift Programming Language Evolution](https://github.com/apple/swift-evolution)中查看更多细节: 83 | 84 | ![](http://images.bestswifter.com/20160129/evolution.png) 85 | 86 | 或者您也可以点击这篇文章查看更多细节——[Removing currying func declaration syntax](https://github.com/apple/swift-evolution/blob/master/proposals/0002-remove-currying.md) 87 | 88 | ## 可变参数 89 | 90 | 如果在参数类型后面加上三个".",表示参数的数量是可变的,如果您有过Java编程的经验,对此应该会比较熟悉: 91 | 92 | ```swift 93 | func printEverythingWithAKrakenEmojiInBetween(objectsToPrint: Any...) { 94 | for object in objectsToPrint { 95 | print("\(object)🐙") 96 | } 97 | } 98 | printEverythingWithAKrakenEmojiInBetween("Hey", "Look", "At", "Me", "!") 99 | ``` 100 | 101 | 此时,参数可以当做`SequenceType`类型来使用,也就是说可以使用`for in`语法遍历其中的每一个参数。 102 | 103 | 可变参数并不是什么罕见的语法,比如`print`函数就是用了可变参数,更多详细的分析请移步:[你其实真的不懂print("Hello,world")](http://www.jianshu.com/p/abb55919c453) 104 | 105 | 106 | ## dynamic关键字 107 | 108 | 如果您有过OC的开发经验,那一定会对OC中@dynamic关键字比较熟悉,它告诉编译器不要为属性合成getter和setter方法。 109 | 110 | Swift中也有dynamic关键字,它可以用于修饰变量或函数,它的意思也与OC完全不同。它告诉编译器使用动态分发而不是静态分发。OC区别于其他语言的一个特点在于它的动态性,任何方法调用实际上都是消息分发,而Swift则尽可能做到静态分发。 111 | 112 | 因此,标记为dynamic的变量/函数会隐式的加上@objc关键字,它会使用OC的runtime机制。 113 | 114 | 虽然静态分发在效率上可能更好,不过一些app分析统计的库需要依赖动态分发的特性,动态的添加一些统计代码,这一点在Swift的静态分发机制下很难完成。这种情况下,虽然使用dynamic关键字会牺牲因为使用静态分发而获得的一些性能优化,但也依然是值得的。 115 | 116 | ```swift 117 | class Kraken { 118 | dynamic var imADynamicallyDispatchedString: String 119 | 120 | dynamic func imADynamicallyDispatchedFunction() { 121 | //Hooray for dynamic dispatch! 122 | } 123 | } 124 | ``` 125 | 126 | 使用动态分发,您可以更好的与OC中runtime的一些特性(如CoreData,KVC/KVO)进行交互,不过如果您不能确定变量或函数会被动态的修改、添加或使用了Method-Swizzle,那么就不应该使用dynamic关键字,否则有可能程序崩溃。 127 | 128 | ## 特殊的字面量 129 | 130 | 在开发或调试过程中如果能用好下面这四个字面量,将会起到事半功倍的效果: 131 | 132 | * \_\_FILE__:当前代码在那个文件中 133 | * \_\_FUNCTION__:当前代码在该文件的那个函数中 134 | * \_\_LINE__:当前代码在该文件的第多少行 135 | * \_\_COLUMN__:当前代码在改行的多少列 136 | 137 | 举个实际例子,您可以在demo中运行体验一番: 138 | 139 | ```swift 140 | func specialLitertalExpression() { 141 | print(__FILE__) 142 | print(__FUNCTION__) 143 | print(__LINE__) 144 | print(__COLUMN__) // 输出结果为11,因为有4个空格,print是五个字符,还有一个左括号。 145 | } 146 | ``` 147 | 148 | 一般情况下最常用的字面量是`__FUNCTION__ `,它可以很容易让程序员明白自己调用的方法的方法名。 149 | 150 | ## 循环标签 151 | 152 | 通常意义上的循环标签主要是`continue`和`break`,不过swift在此基础上做了一些拓展,比如下面这段代码: 153 | 154 | ```swift 155 | let firstNames = ["Neil","Kt","Bob"] 156 | let lastNames = ["Zhou","Zhang","Wang","Li"] 157 | for firstName in firstNames { 158 | var isFound = false 159 | for lastName in lastNames { 160 | if firstName == "Kt" && lastName == "Zhang" { 161 | isFound = true 162 | break 163 | } 164 | print(firstName + " " + lastName) 165 | } 166 | 167 | if isFound { 168 | break 169 | } 170 | } 171 | ``` 172 | 173 | 目的是希望找到分别在两个数组中找到字符串"Kt"和"Zhang",在此之前会打印所有遍历到的字符。 174 | 175 | 在结束内层循环后,我希望外层循环也随之立刻停止,为了实现这个功能,我不得不引入了`isFound `参数。然而实际上我需要的只是可以指定停止哪个循环而已: 176 | 177 | ```swift 178 | outsideloop: for firstName in firstNames { 179 | innerloop: for lastName in lastNames { 180 | if firstName == "Kt" && lastName == "Zhang" { 181 | break outsideloop //人为指定break外层循环 182 | } 183 | print(firstName + " " + lastName) 184 | } 185 | } 186 | ``` 187 | 188 | 以上两段代码等价,可以看到使用了循环标签后,代码明显简洁了很多。 189 | -------------------------------------------------------------------------------- /articles/swift-uicolor.md: -------------------------------------------------------------------------------- 1 | 我为这篇文章制作了一个demo,已经上传到我的GitHub:[KTColor](https://github.com/bestswifter/MySampleCode/tree/master/KtColor),如果觉得有帮助还望给个star以示支持。 2 | 3 | `UIColor` 提供了几个默认的颜色,要想创建除此以外的颜色,一般是通过RGB和alpha值创建(十六进制的颜色其实也是被转换成RGB)。在 Objective-C 中,这可以通过自定义宏来完成,在 Swift 中,我们可以利用 Swift 的一些语法特性来简化创建 `UIColor` 对象的过程。我想,最理想的解决方案应该是这样: 4 | 5 | ```swift 6 | override func viewDidLoad() { 7 | super.viewDidLoad() 8 | 9 | self.view.backgroundColor = "224, 222, 255" 10 | } 11 | ``` 12 | 13 | # 变通方案 14 | 15 | 然而很不幸的是,在目前的 Swift 版本(2.1)中,这种写法暂时无法实现。据我所知,Swift3.0 也不支持这种写法,原因会在稍后分析。目前,我们可以使用两种变通方案: 16 | 17 | ```swift 18 | self.view.backgroundColor = "224, 222, 255".ktColor // 方案1 19 | self.view.backgroundColor = "224, 222, 255" as KtColor // 方案2 20 | ``` 21 | 22 | 两者写法类似,但实现原理实际上完全不同。第一种方案是通过拓展 `String` 类型实现的,第二种方案则是通过继承 `UIColor` 实现。 23 | 24 | 方案1有更好的代码提示,但它对 `String` 类型作了修改,我的demo中有完整的实现,它支持以下输入: 25 | 26 | ```swift 27 | self.view.backgroundColor = "224, 222, 255, 0.5".ktcolor // 这个是完整版 28 | self.view.backgroundColor = "224, 222, 255".ktcolor // alpha值默认为1 29 | self.view.backgroundColor = "224,222,255".ktcolor // 可以用逗号分割 30 | self.view.backgroundColor = "224 222 255".ktcolor // 可以用空格分割 31 | self.view.backgroundColor = "#DC143C".ktcolor // 可以使用16进制数字 32 | self.view.backgroundColor = "#dc143c".ktcolor // 字母可以小写 33 | self.view.backgroundColor = "SkyBlue".ktcolor // 可以直接使用颜色的英文名字 34 | ``` 35 | 36 | 虽然方案2不会对现有代码做修改,但它并不适用于所有系统类型,比如 `NSDate` 或 `NSURL` 类型,出于这种考虑,demo中仅实现了关键逻辑。但这种实现方法最接近于理想的解决方案,一旦时机合适,我们就可以去掉丑陋的 `as KtColor`。 37 | 38 | # 拓展字符串 39 | 40 | 第一种方案通过拓展 `String` 类型实现,它添加了一个 `ktcolor` 计算属性,主要涉及到字符串的分割与处理,还有一些容错、判断等,这些就不是本文的重点了,如果有兴趣,读者可以通过阅读源码获得更加深入的了解。 41 | 42 | 这种方案的好处在于它还适用于 `NSDate`、`NSURL`等类型。比如,下面的代码可以通过类似的技术实现: 43 | 44 | ```swift 45 | let date = "2016-02-17 24:00:00".ktdate 46 | let url = "http://bestswifter.com".kturl 47 | ``` 48 | 49 | 不过,方案一选择的技术注定了它没有再简化的空间了。如果不能显著的减少代码量,它就没有理由取代原生的方案。 50 | 51 | # 字符串字面量 52 | 53 | 方案二和理想方案采用的都是同一个思路:“利用字符串字面量创建对象”。在我的[这篇文章](http://www.jianshu.com/p/07cf2a6ad917)中对此有比较详细的解释。 54 | 55 | 简单来说,我们要做的只是为 `UIColor` 类型添加如下的拓展: 56 | 57 | ```swift 58 | extension UIColor: StringLiteralConvertible { 59 | public init(stringLiteral value: String) { 60 | //这里的数字是随便写的,实际上需要解析字符串 61 | self.init(red: 0.5, green: 0.8, blue: 0.25, alpha: 1) 62 | } 63 | 64 | public init(extendedGraphemeClusterLiteral value: String) { 65 | self.init(stringLiteral: value) 66 | } 67 | 68 | public init(unicodeScalarLiteral value: String) { 69 | self.init(stringLiteral: value) 70 | } 71 | } 72 | ``` 73 | 74 | 不过你会收到这样的报错: 75 | 76 | > Initializer requirement 'init(stringLiteral:)' can only be satisfied by a `required` initializer in the definition of non-final class 'UIColor' 77 | 78 | Xcode 的报错有时候不爱说人话,其实这句话的意思是说,'UIColor' 不是一个标记为 `final` 的类,也就是它还可以被继承。因此 `init(stringLiteral:)` 函数需要被标记为 `required` 以确保所有子类都实现了这个函数。否则,如果有子类没有实现它,那么子类就不满足 `StringLiteralConvertible ` 协议。 79 | 80 | 好吧,我们听从 Xcode 的指示,把每个函数都标记为 `required`,新的问题又出现了: 81 | 82 | > 'required' initializer must be declared directly in class 'UIColor' (not in an extension) 83 | 84 | 这是因为 Swift 不允许在类型拓展中声明 `required` 函数。`required` 函数必须被直接声明在类的内部。 85 | 86 | 这就导致了一个死循环,因此目前理想方案无法实现,除非未来 Swift 允许在拓展中声明`required` 函数。 87 | 88 | # 继承 89 | 90 | 方案二采用了变通的解决方案。首先创建一个 `UIColor` 的子类,我们可以让这个子类实现 `StringLiteralConvertible ` 协议,然后将子类对象赋值给父类: 91 | 92 | ```swift 93 | class KtColor: UIColor, StringLiteralConvertible { 94 | required init(stringLiteral value: String) { 95 | //这里的数字是随便写的,实际上需要解析字符串 96 | super.init(red: 0.5, green: 0.8, blue: 0.25, alpha: 1) 97 | } 98 | 99 | required convenience init(extendedGraphemeClusterLiteral value: String) { 100 | self.init(stringLiteral: value) 101 | } 102 | 103 | required convenience init(unicodeScalarLiteral value: String) { 104 | self.init(stringLiteral: value) 105 | } 106 | 107 | required init?(coder aDecoder: NSCoder) { 108 | fatalError("init(coder:) has not been implemented") 109 | } 110 | 111 | required convenience init(colorLiteralRed red: Float, green: Float, blue: Float, alpha: Float) { 112 | self.init(colorLiteralRed: red, green: green, blue: blue, alpha: alpha) 113 | } 114 | } 115 | 116 | override func viewDidLoad() { 117 | super.viewDidLoad() 118 | 119 | self.view.backgroundColor = "224, 222, 255" as KtColor 120 | } 121 | ``` 122 | 123 | 这种方法有一个显而易见的好处,一旦 Swift 做出了修改,比如允许在拓展中声明`required` 函数,我们只需要微小的改动就可以实现理想方案。 124 | 125 | # 局限性 126 | 127 | 继承 UIKit 中的类并不总是一种可行的方法。比如 `NSDate` 类其实是一个类簇,官网对它有如下解释: 128 | 129 | > The major reason for subclassing NSDate is to create a class with convenience methods for working with a particular calendrical system. But you could also require a custom NSDate class for other reasons, such as to get a date and time value that provides a finer temporal granularity. If you want to subclass NSDate to obtain behavior different than that provided by the private or public subclasses, you must do these things: 130 | > 131 | > 列出了一大串你根本不想去做的事,省略一万字。。。。。。 132 | 133 | 简单来说,如果你想继承 `NSDate`,就必须重新实现它。 134 | 135 | 除了类簇,像 `NSURL` 这样,指定构造函数是可失败构造函数的类也无法使用继承: 136 | 137 | ```swift 138 | class KtURL : NSURL, StringLiteralConvertible { 139 | required init(stringLiteral value: StringLiteralType) { 140 | super.init(string: value, relativeToURL: nil) 141 | } 142 | // 其他的函数略 143 | } 144 | ``` 145 | 146 | `StringLiteralConvertible`协议中定义的构造函数是不可失败构造函数,它不会返回 `nil`。在它的内部调用了父类,也就是 `NSURL` 的 `init(string:relativetoURL:)`,这是可失败构造函数。Swift不允许出现这种情况,否则如果传入的参数 `value` 不合法,你会得到 `nil`么,如果不是 `nil` 那么会得到什么? 147 | 148 | # 总结 149 | 150 | 完全使用字符串字面量创建已有类的实例变量在目前是无法实现的,一种想法类似但是存在局限性的方法是使用子类。或者也可以拓展 `String`类型,但如果相比于原生实现,不能较大幅度的减少代码量,我不建议这么做。 151 | 152 | 参考资料: 153 | 154 | 1. [https://devforums.apple.com/message/1057368#1057368](https://devforums.apple.com/message/1057368#1057368):这个好像是喵神提的问题。 155 | 156 | 2. [Swift: Simple, Safe, Inflexible](https://medium.com/bloc-posts/swift-simple-safe-inflexible-68ff6fa927dc#.5ymodxskm) -------------------------------------------------------------------------------- /articles/tcp-ip-1.md: -------------------------------------------------------------------------------- 1 | # 背景 2 | 3 | 这一系列的文章主要是为一般的、非专业开发岗位(如移动端)的工程师准备,一方面可以对网络的基本知识有基本的了解,另一方面不至于面试中被问到相关问题时束手无策。知识以 TCP/IP 协议簇为主,也会有应用层和数据链路层的简单介绍。 4 | 5 | 文章内容不会很难,也不会过多讨论各种算法,目标是以最快的速度达到最深的理解。内容肯定比直接百度搜索“TCP/IP协议”,然后随便看一篇文章要丰富得多,但也不足以让读者凭此就能胜任网络开发的工作。 6 | 7 | 诚然,面试以 TCP/UDP/HTTP 等协议为主,IP 协议都涉及甚少,更遑论数据链路层等。但我希望可以从原理上理解那些问题,而不是临时抱佛脚,背了一些答案然后在面试后忘干净。不要为面试而准备面试,为了完善自己的知识体系而准备。如果你觉得这正是你需要的,Let's Begin! 8 | 9 | # OSI七层模型和协议 10 | 11 | 在这一节中,我们不谈这些层和协议的具体作用,目前只要知道 OSI 模型中,网络被分为七层,由底层向高层依次是:物理层,数据链路层,网络层,传输层,会话层,表示层和应用层。 12 | 13 | 协议是一个 Big 很高,出现很频繁的词。其实它很好理解,它实际上是一种通信双方共同遵守的规范。比如我需要把性别和年龄传递给另外一台主机,那么我可以定义一个"A 协议",协议规定数据的前 4 个字节表示性别,后四个字节表示年龄。这样对方主机接收时就知道前 4 个字节是性别,而不会错把它当成年龄来处理。 14 | 15 | 整个互联网世界能够运行,完全得益于各个软件、硬件厂商严格遵守现有的协议。以 IP 协议为例,你可以随便修改它,然后自己弄出一个 IP2 协议,只不过没有人认可、遵守这个协议,所以它毫无用武之地。 16 | 17 | # 物理层 18 | 19 | 物理层处于 OSI 七层模型的最底端,它的主要任务是将比特流与电子信号进行转换。 20 | 21 | 在计算机的世界中,一切都由 0 和 1 组成。你看到的这篇文章,在通过网络传输到你电脑的过程中,自然也是以 0 和 1 的形式存在。但是网络传输的介质(比如光纤,双绞线,电波等)中显然是不存在 0 和 1 的。比如在光线中,数据通过光的形式传递。0 和 1 以光的亮灭表示,其中的转换由物理层完成。 22 | 23 | 如果没有物理层,由 0 和 1 构成的比特流就无法在物理介质中传播。 24 | 25 | # 数据链路层 26 | 27 | 数据链路层处于 OSI 七层模型的第二层,它定义了通过通信介质相互连接的设备之间,数据传输的规范。 28 | 29 | 在数据链路层中,数据不再以 0、1 序列的形式存在,它们被分割为一个一个的“帧”,然后再进行传输。 30 | 31 | 数据链路层中有两个重要的概念:MAC 地址和分组交换。 32 | 33 | ### MAC地址 34 | 35 | MAC 地址是被烧录到网卡 ROM 中的一串数字,长度为 48 比特,它在世界范围内唯一(不考虑虚拟机自定义 MAC 地址)。由于 MAC 地址的唯一性,它可以被用来区分不同的节点,一旦指定了 MAC 地址,就不可能出现不知道往哪个设备传输数据的情况。 36 | 37 | ### 分组交换 38 | 39 | 分组交换是指将较大的数据分割为若干个较小的数据,然后依次发送。使用分组交换的原因是不同的数据链路有各自的最大传输单元(MTU: Maximum Transmission Unit)。不同的数据链路就好比不同的运输渠道,一辆卡车(对应通信介质)的载重量为 5 吨。那么通过卡车运送 20 吨的货物就需要把这些货物分成四部分,每份重 5 吨。如果运输机的载重量是 30 吨,那么这些货物不需要分割,直接一架运输机就可以拉走。 40 | 41 | 以以太网(一种数据链路)为例,它的MTU是 1500 字节,也就是通过以太网传输的数据,必须分割为若干帧,每个帧的数据长度不超过 1500 字节。如果上层传来的数据超过这个长度,数据链路层需要分割后再发送。 42 | 43 | ### 以太网帧 44 | 45 | 我们用以太网举例,介绍一下以太网帧的格式。 46 | 47 | 以太网帧的开头是“前导码(Preamble)”,长度为 8 字节,这一段没什么用,重点在于以太网帧的本体。 48 | 49 | 本体由首部,数据和 FCS 三部分组成: 50 | 51 | ![自学过程](https://user-gold-cdn.xitu.io/2017/12/12/1604b45ac09e013b?w=1240&h=223&f=jpeg&s=42026) 52 | 53 | 类型部分存储了上层协议的编号,比如上层是 IP 协议,则编号为 0800。 54 | 55 | FCS 表示帧校验序列(Frame Check Sequence),用于判断帧是否在传输过程中有损坏(比如电子噪声干扰)。FCS 保存着发送帧除以某个多项式的余数,接收到的帧也做相同计算,如果得到的值与 FCS 相同则表示没有出错。 56 | 57 | ### 交换机 58 | 59 | 交换机是一种在数据链路层工作的网络设备,它有多个端口,可以连接不同的设备。交换机根据每个帧中的目标 MAC 地址决定向哪个端口发送数据,此时它需要参考“转发表” 60 | 61 | 转发表并非手动设置,而是交换机自动学习得到的。当某个设备向交换机发送帧时,交换机将帧的源 MAC 地址和接口对应起来,作为一条记录添加到转发表中。 62 | 63 | 下图描述了交换机自学过程的原理 64 | 65 | ![自学过程](https://user-gold-cdn.xitu.io/2017/12/12/1604b45abed55bba?w=1240&h=811&f=jpeg&s=125973) 66 | 67 | 关于数据链路层,最重要的一点还是它的定义:“通过通信介质相互连接的设备之间,数据传输的规范”。这说明数据链路层的协议适用于**处于同一种数据链路两端的节点**。如果不能理解这一点,就无法理解网络层和 IP 协议。 68 | 69 | 数据链路层的意义在于,如果没有数据链路层,数据只能以流的形式存在与通信介质中,不知道该发送往哪里,过长的数据流可能无法在通信介质中传输。 70 | -------------------------------------------------------------------------------- /articles/tcp-ip-2.md: -------------------------------------------------------------------------------- 1 | IP协议处于OSI参考模型的第三层——网络层,网络层的主要作用是实现终端节点间的通信。IP协议是网络层的一个重要协议,网络层中还有ARP(获取MAC地址)和ICMP协议(数据发送异常通知) 2 | 3 | 数据链路层的作用在于实现同一种数据链路下的包传递,而网络层则可以实现跨越不同数据链路的包传递。比如主机A通过Wi-Fi连接到路由器B,路由器B通过以太网连接到路由器C,而路由器C又通过Wi-Fi与主机D保持连接。这时主机A向D发送的数据包就依赖于网络层进行传输。 4 | 5 | 这篇文章主要介绍IP协议的基本知识和IP首部,IP协议可以分为三大作用模块:IP寻址、路由和IP分包。 6 | 7 | # IP地址 8 | 9 | IP地址是一种在网络层用于识别通信对端信息的地址。它有别于数据链路层中的MAC地址,后者用于标识同一链路下不同的计算机。 10 | 11 | 举一个形象的例子,我要从镇江的家里去沈阳的东北大学,通信两端的地址分别是家和学校,他们相当于IP地址。然而没有交通工具可以让我从家直接去学校,所以我先要打车去火车站,然后坐高铁到沈阳站,再转公交去学校。这三次中转分别属于三种交通方式(数据链路),每一次中转都有起点和终点,他们就相当于MAC地址。每次中转可以称为一跳(Hop) 12 | 13 | IP地址由32位正整数表示,为了直观的表示,我们把它分成4个部分,每个部分由8位整数组成,对应十进制的范围就是0-255。 14 | 15 | 比如`172.20.1.1`可以表示为:`10101100 00010100 00000001 00000001`。转换规则很简单,就是分别把四个部分的十进制(0-255)与8位二进制数字进行转换。 16 | 17 | 从功能上看,IP地址由两部分组成:网络标识和主机标识。 18 | 19 | 网络标识用于区分不同的网段,相同段内的主机必须拥有相同的网络表示,不同段内的主机不能拥有相同的网络标识。 20 | 21 | 主机标识用于区分同一网段下不同的主机,它不能在同一网段内重复出现。 22 | 23 | 32位IP地址被分为两部分,到底前多少位是网络标识呢?一般有两种方法表示:IP地址分类、子网掩码。 24 | 25 | ### IP分类 26 | 27 | IP地址分为四个级别,分别为A类、B类、C类和D类。分类的依据是IP地址的前四位: 28 | 29 | A类IP地址是第一位为“0”的地址。A类IP地址的前8位是网络标识,用十进制标识的话`0.0.0.0-127.0.0.0`是A类IP地址的理论范围。另外我们还可以得知,A类IP地址最多只有128个(实际上是126个,下文不赘述),每个网段内主机上限为2的24次方,也就是16,777,214个。 30 | 31 | B类IP地址是前两位为“10“的地址。B类IP地址的前16位是网络标识,用十进制标识的话`128.0.0.0-191.255.0.0`是B类IP地址的范围。B类IP地址的主机标记长度为16位,因此一个网段内可容纳主机地址上限为65534个。 32 | 33 | C类IP地址是前三位为“110”的地址。C类IP地址的前24位是网络标识,用十进制标识的话`192.0.0.0-223.255.255.0`是C类IP地址的范围。C类地址的后8位是主机标识,共容纳254个主机地址。 34 | 35 | D类IP地址是前四位为“1110”的地址。D类IP地址的网络标识长32位,没有主机标识,因此常用于多播。 36 | 37 | ### 子网掩码 38 | 39 | IP地址总长度32位,它能表示的主机数量有限,大约在43亿左右。而IP地址分类更是造成了极大的浪费,A、B类地址一共也就一万多个,而世界上包含主机数量超过254的网段显然不止这么点。 40 | 41 | 我们知道IP地址分类的本质是区分网络标识和主机标识,另一种更加灵活、细粒度的区分方法是使用子网掩码。 42 | 43 | 子网掩码长度也是32位,由一段连续的1和一段连续的0组成。1的长度就表示网络标识的长度。以IP地址`172.20.100.52`为例,它本来是一个B类IP地址(前16位是网络标识),但通过子网掩码,它可以是前26为为网络标识的IP地址: 44 | 45 | ![子网掩码](https://user-gold-cdn.xitu.io/2017/12/12/1604b470bff38bfe?w=1240&h=816&f=jpeg&s=147371) 46 | 47 | # 路由控制 48 | 49 | 路由控制(Routing)是指将分组数据发送到目标地址的功能,这个功能一般由路由器完成。(不要与家里用的小型无线路由器混为一谈) 50 | 51 | 路由器中保存着路由控制表,它在路由控制表中查找目标IP地址对应的下一个路由器地址。下图描述了这一过程: 52 | 53 | ![路由控制](https://user-gold-cdn.xitu.io/2017/12/12/1604b470bf55b2f9?w=1240&h=1005&f=jpeg&s=163796) 54 | 55 | 主机A的地址是`10.1.1.30`,要把数据发往地址为`10.1.2.10`的主机。在主机A的路由表中,保存了两个字段,由于目标地址`10.1.2.10`与`10.1.1.0/24`段不匹配,所以它被发往默认路由`10.1.1.1`也就是图中路由器1的左侧网卡的IP地址。 56 | 57 | 路由器1继续在它自己的路由控制表中查找目标地址`10.1.2.10`,它发现目标地址属于`10.1.2.0/24`这一段,因此将数据转发至下一个路由器`10.1.0.2`,也就是路由器2的左侧网卡的地址。 58 | 59 | 路由器2在自己的路由控制表中查找目标地址`10.1.2.10`,根据表中记录将数据发往`10.1.2.1`接口,也就是自己的右侧网卡的IP地址。主机B检查目标IP地址和自己相同,于是接收数据。 60 | 61 | ### 路由控制表 62 | 63 | 路由控制的关键在于路由控制表,路由控制表可以由管理员手动设置,称为静态路由控制,但是估计大部分人没这么干过。这是因为路由器可以喝其他路由器互换信息比即使自动刷新路由表,这个信息交换的协议并没有在IP协议中定义,而是由一个叫做“路由协议”的协议管理。 64 | 65 | ### 环路 66 | 67 | 上图中,假设主机A向一个不存在的IP地址发送数据,并且路由器1、2、3设置的默认路由形成了一个循环,那么数据将在网络中不断转发最终导致网络拥堵。这个问题将在下文分析IP首部时得到解决。 68 | 69 | # IP报文分割重组 70 | 71 | 在数据链路层中,我们已经提到过不同的数据链路有不同的最大传输单元(MTU)。因此IP协议的一个任务是对数据进行分片和重组。分片由发送端主机和路由器负责,重组由接收端主机负责。 72 | 73 | ### 路径MTU发现 74 | 75 | 分片会加重路由器的负担,因此只要条件允许,我们都不希望路由器对IP数据包进行分片处理。另外,如果一个分片丢失,整个IP数据报都会作废。 76 | 77 | 解决以上问题的技术是“路径MTU发现”。主机会首先获取整个路径中所有数据链路的最小MTU,并按照整个大小将数据分片。因此传输过程中的任何一个路由器都不用进行分片工作。 78 | 79 | 为了找到路径MTU,主机首先发送整个数据包,并将IP首部的禁止分片标志设为1.这样路由器在遇到需要分片才能处理的包时不会分片,而是直接丢弃数据并通过ICMP协议将整个不可达的消息发回给主机。 80 | 81 | 主机将ICMP通知中的MTU设置为当前MTU,根据整个MTU对数据进行分片处理。如此反复下去,直到不再收到ICMP通知,此时的MTU就是路径MTU。 82 | 83 | 以UDP协议发送数据为例: 84 | 85 | ![路径MTU发现](https://user-gold-cdn.xitu.io/2017/12/12/1604b470bf159404?w=1240&h=1008&f=jpeg&s=130978) 86 | 87 | ### 重组 88 | 89 | 接收端根据IP首部中的标志(Flag)和片偏移(Fragment Offset)进行数据重组。具体内容将在分析IP首部时详细解释。 90 | 91 | # IP首部(IPv4) 92 | 93 | IP首部是一个有些复杂的结构,我们不用记忆它的结构,只需了解每个部分的作用即可,这样可以加深对IP协议的理解。 94 | 95 | ![IP首部](https://user-gold-cdn.xitu.io/2017/12/12/1604b470c3055fbc?w=1240&h=931&f=jpeg&s=154540) 96 | 97 | 其中几个重要的部分介绍如下: 98 | 99 | * 总长度(Total Length):表示IP首部与数据部分总的字节数,该段长16比特,所以IP包的最大长度为65535字节(2^16)。虽然不同数据链路的MTU不同,但是IP协议屏蔽了这些区别,通过自己实现的数据分片功能,从上层的角度来看,IP协议总是能够以65535为最大包长进行传输。 100 | 101 | * 标识(ID:Identification):用于分片重组。属于同一个分片的帧的ID相同。但即使ID相同,如果目标地址、源地址、上层协议中有任何一个不同,都被认为不属于同一个分片。 102 | 103 | * 标志(Flags):由于分片重组,由三个比特构成。 104 | 105 | 第一个比特未使用,目前必须是0。 106 | 107 | 第二个比特表示是否进行分片,0表示可以分片,1表示不能分片。在路径MTU发现技术中就用到了这个位。 108 | 109 | 第三个比特表示在分片时,是否表示最后一个包。1表示不是最后一个包,0表示分配中最后一个包。 110 | 111 | * 片偏移(FO: Fragment Offset):由13比特组成,表示被分片的段相对于原始数据的位置。它可以表示8192(2^13)个位置,单位为8字节,所以最大可以表示8 x 8192 = 65536字节的偏移量。 112 | 113 | * 生存时间(TTL: Time To Live):表示包可以经过多少个路由器的中转。每经过一个路由器,TTL减1。这样可以避免前文提到的无限传递包的问题。 114 | * 协议: 表示IP首部的下一个首部属于哪个协议。比如TCP协议的编号为6,UDP编号为17. 115 | * 首部校验和:用于检查IP首部是否损坏 116 | * 可选项:仅在试验或诊断时用,可以没有。如果有,需要配合填充(Padding)占满32比特。 117 | -------------------------------------------------------------------------------- /articles/tcp-ip-3.md: -------------------------------------------------------------------------------- 1 | 在前两篇文章中,我分别介绍了[数据链路层](./tcp-ip-1.md)和[网络层的IP协议](./tcp-ip-2.md)。虽然这个系列教程的重点是搞定 TCP/IP,不过不用着急,本文简要介绍完与 IP 协议相关的技术,下一篇文章就会正式、详细的介绍 传输层与 TCP 协议。这篇文章会介绍 `DNS`、`ARP`、`NAT` 协议,这些内容虽然与 TCP 没有直接关联,但理解它们的原理有助于巩固基础知识,更好的理解网络的工作原理。 2 | 3 | # DNS 解析 4 | 5 | IP地址用于识别通信双方的地址,但它是一串长数字,不方便记忆,人们希望主机有自己自己的名字,这个名字是唯一的,而且容易记住。于是,诞生了“域名”的概念。域名是一种为了识别主机名称和机构名的具有分层的名称,比如在域名 `neu.edu.cn`中,`neu`是主机名,`edu` 和 `cn` 是不同层次下的机构名。 6 | 7 | 域名和 IP 地址都可以唯一对应一台主机,DNS 协议的作用就是将自身具有意义的域名转换成不容易记住的 IP 地址。 8 | 9 | 域名是分层的,每层都有自己的 DNS 服务器用于处理 DNS 解析的请求。这样的好处在于每层的服务器不用关注过多的信息,它只要知道自己这一层下的域名服务器信息即可。以解析域名: `www.ietf.org`为例: 10 | 11 | ![DNS解析过程](https://user-gold-cdn.xitu.io/2017/12/12/1604b47dd5427e0f?w=1240&h=1075&f=png&s=344879) 12 | 13 | 根服务器其实并不知道 `www.ietf.org` 的 IP 地址,但是它知道 `itef.org` 域名服务器的地址,所以它把这条查询请求转发给 `itef.org` 域名服务器。DNS请求被逐层下发,直到找到对应的 IP 地址为止。 14 | 15 | # ARP 协议 16 | 17 | ARP 协议(Address Resolution Protocol)用于通过目标 IP 地址,定位下一个接收数据包的网络设备的 MAC 地址。如果目标主机处在同一个数据链路上,那么可以直接得到目标主机的 MAC 地址,否则会得到下一条路由器的 MAC 地址。 18 | 19 | ARP 协议的工作原理可以分为两部分:ARP 请求和 ARP 响应。 首先,源主机会通过广播发送一个 ARP 请求包:“我要与 IP 地址为 `xxx` 的主机通话,谁知道它的 MAC地址?”。 20 | 21 | 数据链路上的所有主机都会收到这条消息并检查自己的 IP 地址,如果与 ARP 请求包中的 IP 地址一致,主机就会发送 ARP 响应包:“我就是 IP 地址为 `xxx` 的主机,我的 MAC 地址是:`xxxx`”。 22 | 23 | 下图表示了 ARP 协议的工作机制: 24 | 25 | ![ARP机制](https://user-gold-cdn.xitu.io/2017/12/12/1604b47dd34c5942?w=1240&h=615&f=png&s=136514) 26 | 27 | 在实际的使用过程中,每次往目标主机发送数据都要使用 ARP 是很低效的,通常的做法是把获取到的 MAC 地址缓存一段时间。一般来说,一旦源主机向目标地址发送一个数据包,接下来继续发送多次的概率非常大,因此这种缓存非常容易命中。 28 | 29 | 当下一次发送 ARP 请求或超过一定时间后,缓存都会失效,这保证了即使 MAC 地址与 IP 地址的对应关系发生了变化,数据包依然能够被正确的发往目标地址。 30 | 31 | 再次强调一下,MAC 和 IP 地址虽然看上去功能类似(都是用于唯一区分主机),但是两者缺一不可。如果只有 IP 地址,虽然可以跳过 ARP,直接在数据链路上发一个广播,但是这仅适用于通信双方处于同一个数据链路下的情况。如果双方处于不同的数据链路,数据报无法穿透中间的路由器。 32 | 33 | 如果全世界只用 MAC 地址,那么请参考交换机的自学过程,可以想象这个过程会带来庞大的,不必要的流量。 34 | 35 | 正因为 MAC 和 IP 地址缺一不可,所以才产生了 ARP 这样的协议将两者关联起来。 36 | 37 | # NAT 和 NAPT 技术 38 | 39 | NAT (Network Address Translator) 是一种用于将局域网中的私有地址转换成全局 IP 地址的技术。 40 | 41 | 在连接上无线路由器的时候,如果检查一下设备的 IP 地址,也许你会发现是类似于 `192.168.1.1` 这样的局域网 IP 地址。那不同网段中,IP 地址都是 `192.168.1.1` 的主机改如何通信呢? 42 | 43 | 下图描绘了 NAT 的工作原理: 44 | 45 | 局域网中 IP 地址为 `10.0.0.10` 的主机向全局 IP 地址 `163.221.120.9` 发送数据。NAT 路由器将数据包的源地址修改成自己的全局 IP 地址:`202.244.174.37`。同理,接收数据时,路由器把目标地址 `202.244.174.37` 翻译成内网地址:`10.0.0.10` 46 | 47 | ![NAT工作原理](https://user-gold-cdn.xitu.io/2017/12/12/1604b47dcf32e739?w=1240&h=626&f=png&s=289848) 48 | 49 | 路由器只有一个对外的全局 IP 地址,如果有多个内网主机都向外部通讯怎么办呢?这时就要使用 NAPT 技术,它和 NAT 从原理上类似,但它可以转换 TCP 和 UDP 端口号。 50 | 51 | 使用 NAPT 技术时,不同的内网 IP 被转换成同一个公共 IP 地址,也就是路由器对外显示的全局 IP 地址,但是被附加不同的端口号以示区分: 52 | 53 | ![NAPT工作原理](https://user-gold-cdn.xitu.io/2017/12/12/1604b47dd2d8488e?w=1240&h=898&f=png&s=358008) 54 | 55 | 不管是 NAT 还是 NAPT,都需要路由器路由器内部维护一张自动生成的地址转换表。以 TCP 为例,建立 TCP 连接首次握手的 SYN 包发出时会生成这个表,关闭连接时会发出 FIN 包,收到这个包的应答时转换表被删除。 56 | 57 | 如果暂时不了解 TCP 协议和三次握手也没有关系,下一篇文章将会有详细的讲解。 -------------------------------------------------------------------------------- /articles/tcp-ip-4.md: -------------------------------------------------------------------------------- 1 | 从本章开始,我们开始介绍最重要的传输层。传输层位于 OSI 七层模型的第四层(由下往上)。顾名思义,传输层的主要作用是**实现应用程序之间的通信**。网络层主要是保证不同数据链路下数据的可达性,至于如何传输数据则是由传输层负责。 2 | 3 | # 传输层协议简介 4 | 5 | 常见的传输层协议主要有 TCP 协议和 UDP 协议。TCP 协议是面向有连接的协议,也就是说在使用 TCP 协议传输数据之前一定要在发送方和接收方之间建立连接。一般情况下建立连接需要三步,关闭连接需要四步。 6 | 7 | 建立 TCP 连接后,由于有数据重传、流量控制等功能,TCP 协议能够正确处理丢包问题,保证接收方能够收到数据,与此同时还能够有效利用网络带宽。然而 TCP 协议中定义了很多复杂的规范,因此效率不如 UDP 协议,不适合实时的视频和音频传输。 8 | 9 | UDP 协议是面向无连接的协议,它只会把数据传递给接收端,但是不会关注接收端是否真的收到了数据。但是这种特性反而适合多播,实时的视频和音频传输。因为个别数据包的丢失并不会影响视频和音频的整体效果。 10 | 11 | IP 协议中的两大关键要素是源 IP 地址和目标 IP 地址。而刚刚我们说过,传输层的主要作用是**实现应用程序之间的通信**。因此传输层的协议中新增了三个要素:源端口号,目标端口号和协议号。通过这五个信息,可以唯一识别一个通信。 12 | 13 | 不同的端口用于区分同一台主机上不同的应用程序。假设你打开了两个浏览器,浏览器 A 发出的请求不会被浏览器 B 接收,这就是因为 A 和 B 具有不同的端口。 14 | 15 | 协议号用于区分使用的是 TCP 还是 UDP。因此相同两台主机上,相同的两个进程之间的通信,在分别使用 TCP 协议和 UDP 协议时也可以被正确的区分开来。 16 | 17 | 用一句话来概括就是:“源 IP 地址,目标 IP 地址,源端口号,目标端口号和协议号”这五个信息只要有一个不同,都被认为是不同的通信。 18 | 19 | # UDP 首部 20 | 21 | UDP 协议最大的特点就是简单,它的首部如下图所示: 22 | 23 | ![UDP 首部](https://user-gold-cdn.xitu.io/2017/12/12/1604b4917a0c314f?w=1240&h=494&f=png&s=289029) 24 | 25 | 包长度表示 UDP 首部的长度和 UDP 数据长度之和。 26 | 27 | 校验和用来判断数据在传输过程中是否损坏。计算这个校验和的时候,不仅考虑源端口号和目标端口号,还要考虑 IP 首部中的源 IP 地址,目标 IP 地址和协议号(这些又称为 UDP 伪首部)。这是因为以上五个要素用于识别通信时缺一不可,如果校验和只考虑端口号,那么另外三个要素收到破坏时,应用就无法得知。这有可能导致不该收到包的应用收到了包,该收到包的应用反而没有收到。 28 | 29 | 这个概念同样适用于即将介绍的 TCP 首部。 30 | 31 | # TCP 首部 32 | 33 | 和 UDP 首部相比,TCP 首部要复杂得多。解析这个首部的时间也相应的会增加,这是导致 TCP 连接的效率低于 UDP 的原因之一。 34 | 35 | ![TCP 首部](https://user-gold-cdn.xitu.io/2017/12/12/1604b4917702553b?w=1240&h=898&f=png&s=359939) 36 | 37 | 其中某些关键字段解释如下: 38 | 39 | * 序列号:它表示发送数据的位置,假设当前的序列号为 s,发送数据长度为 l,则下次发送数据时的序列号为 s + l。在建立连接时通常由计算机生成一个随机数作为序列号的初始值。 40 | 41 | * 确认应答号:它等于下一次应该接收到的数据的序列号。假设发送端的序列号为 s,发送数据的长度为 l,那么接收端返回的确认应答号也是 s + l。发送端接收到这个确认应答后,可以认为这个位置以前所有的数据都已被正常接收。 42 | 43 | * 数据偏移:TCP 首部的长度,单位为 4 字节。如果没有可选字段,那么这里的值就是 5。表示 TCP 首部的长度为 20 字节。 44 | 45 | * 控制位:改字段长度为 8 比特,分别有 8 个控制标志。依次是 CWR,ECE,URG,ACK,PSH,RST,SYN 和 FIN。在后续的文章中你会陆续接触到其中的某些控制位。 46 | 47 | * 窗口大小:用于表示从应答号开始能够接受多少个 8 位字节。如果窗口大小为 0,可以发送窗口探测。 48 | 49 | * 紧急指针:尽在 URG 控制位为 1 时有效。表示紧急数据的末尾在 TCP 数据部分中的位置。通常在暂时中断通信时使用(比如输入 Ctrl + C)。 50 | 51 | # TCP 握手 52 | 53 | TCP 是面向有连接的协议,连接在每次通信前被建立,通信结束后被关闭。了解连接建立和关闭的过程通常是考察的重点。连接的建立和关闭过程可以用一张图来表示: 54 | 55 | ![TCP 连接建立和关闭](https://user-gold-cdn.xitu.io/2017/12/12/1604b4917725c794?w=683&h=506&f=png&s=37375) 56 | 57 | 通常情况下,我们认为客户端首先发起连接。 58 | 59 | ### 三次握手建立连接 60 | 61 | 这个过程可以用以下三句形象的对话表示: 62 | 63 | 1. (客户端):我要建立连接了。 64 | 2. (服务端):我知道你要建立连接了,我这边没有问题。 65 | 3. (客户端):我知道你知道我要建立连接了,接下来我们就正式开始通信。 66 | 67 | ### 为什么是三次握手 68 | 69 | 根据一般的思路,我们可能会觉得只要两次握手就可以了,第三步确认看似是多余的。那么 TCP 协议为什么还要费力不讨好的加上这一次握手呢? 70 | 71 | 这是因为在网络请求中,我们应该时刻记住:“网络是不可靠的,数据包是可能丢失的”。假设没有第三次确认,客户端向服务端发送了 SYN,请求建立连接。由于延迟,服务端没有及时收到这个包。于是客户端重新发送一个 SYN 包。回忆一下介绍 TCP 首部时提到的序列号,这两个包的序列号显然是相同的。 72 | 73 | 假设服务端接收到了第二个 SYN 包,建立了通信,一段时间后通信结束,连接被关闭。这时候最初被发送的 SYN 包刚刚抵达服务端,服务端又会发送一次 ACK 确认。由于两次握手就建立了连接,此时的服务端就会建立一个新的连接,然而客户端觉得自己并没有请求建立连接,所以就不会向服务端发送数据。从而导致服务端建立了一个空的连接,白白浪费资源。 74 | 75 | 在三次握手的情况下,服务端直到收到客户端的应答后才会建立连接。因此在上述情况下,客户端会接受到一个相同的 ACK 包,这时候它会抛弃这个数据包,不会和服务端进行第三次握手,因此避免了服务端建立空的连接。 76 | 77 | ### ACK 确认包丢失怎么办 78 | 79 | 三次握手其实解决了第二步的数据包丢失问题。那么第三步的 ACK 确认丢失后,TCP 协议是如何处理的呢? 80 | 81 | 按照 TCP 协议处理丢包的一般方法,服务端会重新向客户端发送数据包,直至收到 ACK 确认为止。但实际上这种做法有可能遭到 SYN 泛洪攻击。所谓的泛洪攻击,是指发送方伪造多个 IP 地址,模拟三次握手的过程。当服务器返回 ACK 后,攻击方故意不确认,从而使得服务器不断重发 ACK。由于服务器长时间处于半连接状态,最后消耗过多的 CPU 和内存资源导致死机。 82 | 83 | 正确处理方法是服务端发送 RST 报文,进入 CLOSE 状态。这个 RST 数据包的 TCP 首部中,控制位中的 RST 位被设置为 1。这表示连接信息全部被初始化,原有的 TCP 通信不能继续进行。客户端如果还想重新建立 TCP 连接,就必须重新开始第一次握手。 84 | 85 | ### 四次握手关闭连接 86 | 87 | 88 | 这个过程可以用以下四句形象的对话表示: 89 | 90 | 1. (客户端):我要关闭连接了。 91 | 2. (服务端):你那边的连接可以关闭了。 92 | 3. (服务端):我这边也要关闭连接了。 93 | 4. (客户端):你那边的连接可以关闭了。 94 | 95 | 由于连接是双向的,所以双方都要主动关闭自己这一侧的连接。 96 | 97 | ### 关闭连接的最后一个 ACK 丢失怎么办 98 | 99 | 实际上,在第三步中,客户端收到 FIN 包时,它会设置一个计时器,等待相当长的一段时间。如果客户端返回的 ACK 丢失,那么服务端还会重发 FIN 并重置计时器。假设在计时器失效前服务器重发的 FIN 包没有到达客户端,客户端就会进入 CLOSE 状态,从而导致服务端永远无法收到 ACK 确认,也就无法关闭连接。 100 | 101 | 示意图如下: 102 | 103 | ![TCP 关闭连接](https://user-gold-cdn.xitu.io/2017/12/12/1604b4917affaab0?w=612&h=336&f=png&s=50147) 104 | -------------------------------------------------------------------------------- /articles/tcp-ip-5.md: -------------------------------------------------------------------------------- 1 | [上一节](./tcp-ip-4.md) 中讲过,TCP 协议是面向有连接的协议,它具有丢包重发和流量控制的功能,这是它区别于 UDP 协议最大的特点。本文就主要讨论这两个功能。 2 | 3 | # 数据包重发 4 | 5 | ### 数据发送 6 | 7 | 丢包重发的前提是发送方能够知道接收方是否成功的接收了消息。所以,在 TCP 协议中,接收端会给发送端返回一个通知,也叫作确认应答(ACK),这表示接收方已经收到了数据包。 8 | 9 | 根据上一节对 TCP 首部的分析得知,ACK 的值和下次发送数据包的序列号相等。因此 ACK 也可以理解为:“发送方,下次你从这个位置开始发送!”。下图表示了数据发送与确认应答的过程: 10 | 11 | ![ACK 确认](https://user-gold-cdn.xitu.io/2017/12/12/1604b4a42d1ce607?w=988&h=1006&f=png&s=120068) 12 | 13 | 数据包和 ACK 应答都有可能丢失,在这种情况下,发送方如果在一段时间内没有收到 ACK,就会重发数据: 14 | 15 | ![未收到 ACK 时重发数据](https://user-gold-cdn.xitu.io/2017/12/12/1604b4a429f16556?w=1000&h=1076&f=png&s=164930) 16 | 17 | 即使网络连接正常,由于延迟的存在,接收方也有可能收到重复的数据包,因此接收方通过 TCP 首部中的 SYN 判断这个数据包是否曾经接收过。如果已经接收过,就会丢弃这个包。 18 | 19 | ### 重传超时时间(RTO) 20 | 21 | 如果发送方等待一段时间后,还是没有收到 ACK 确认,就会启动超时重传。这个等待的时间被称为重传超时时间(RTO,Retransmission TimeOut)。RTO 的值具体是多久呢? 22 | 23 | 首先,RTO 的值不是固定的,它是一个动态变化的时间。这个时间总是略大于连接往返时间(RTT,Round Trip Time)。这个设定可以这样理解:“数据发送给对方,再返回到我这里,假设需要 10 秒,那我就等待 12秒,如果超过 12 秒,那估计就是回不来了。” 24 | 25 | RTT 是动态变化的,因为谁也不知道网络下一时刻是否拥堵。而当前的 RTO 需要根据未来的 RTT 估算得出。RTO 不能估算太大,否则会多等待太多时间;也不能太小,否则会因为网络突然变慢而将不该重传的数据进行重传。 26 | 27 | RTO 有自己的估算公式,可以保证即使 RTT 波动较大,它的变化也不会太剧烈。感兴趣的读者可以自行查阅相关资料。 28 | 29 | ### TCP 窗口 30 | 31 | 按照之前的理论,在数据包发出后,直至 ACK 确认返回以前,发送端都无法发送数据,而且包的往返时间越长,网络利用效率和通信性能就越低。前两张图片形象的解释了这一点。 32 | 33 | 为了解决这个问题,TCP 使用了“窗口”这个概念。窗口具有大小,它表示无需等待确认应答就可以继续发送数据包的最大数量。比如窗口大小为 4 时,数据发送的示意图如下: 34 | 35 | ![窗口大小为 4](https://user-gold-cdn.xitu.io/2017/12/12/1604b4a42d43f0b4?w=1108&h=936&f=png&s=255985) 36 | 37 | 不等确认就连续发送若干个数据包会不会有问题呢?我们首先来看数据包丢失问题。 38 | 39 | 我们知道 TCP 首部中的 ACK 字段表示接收方已经收到数据的最后位置。因此,接收方成功接收到了 1-1000 字节的数据后,它会发送一个 ACK = 1001 的确认包。假设 1001-2000 字节的数据包丢失了,由于窗口长度比较大,发送方会继续发送 2001-3000 字节的数据包。接收端并不会返回这个数据包的确认,因为它最后收到的数据还是 1-1000 字节的数据包。 40 | 41 | 因此,接收端返回的数据包的 ACK 依然是 1001。这表示:“喂,发数据的,别往后发了,你第 1001 字节开始的数据还没来呢”。可以想见,发送端以后每次发送数据包得到的确认中,ACK 的值都是 1001。当连续收到三次确认之后,发送方会意识到:“对方还没有接收到数据,这个包需要重传”。 42 | 43 | 因此,引入窗口的概念后,被发送的数据不能立刻丢弃,需要缓存起来以备将来需要重发。 44 | 45 | 利用窗口发送数据的过程可以用下图表示: 46 | 47 | ![快速重传](https://user-gold-cdn.xitu.io/2017/12/12/1604b4a429d0211f?w=1076&h=924&f=png&s=255604) 48 | 49 | 如果是数据包没有丢失,但是确认包丢失了呢?这就是窗口最擅长处理的问题了。假设发送发收到的确认包中的 ACK 第一次是 1001,第二次是 4001。那么我们完全可以相信中间的两个包是成功被接收的。因为如果有没接收到的包,接收方是不会增加 ACK 的。 50 | 51 | 在这种情况下,如果不使用窗口,发送方就需要重传第二、三个数据包,但是有了窗口的概念后,发送方就省略了两次重传。因此使用窗口实际上可以理解为“空间换时间”。 52 | 53 | ![某些确认包丢失时不用重发](https://user-gold-cdn.xitu.io/2017/12/12/1604b4a42edeb3ce?w=1240&h=849&f=png&s=231054) 54 | 55 | # 流量控制 56 | 57 | ### 窗口大小 58 | 59 | 如果窗口过大,会导致接收方的缓存区数据溢出。这时候本该被接收的数据反而丢弃了,就会导致无意义的重传。因此,窗口大小是一个可以改变的值,它由接收端主机控制,附加在 TCP 首部的“窗口大小”字段中。 60 | 61 | ### 慢启动 62 | 63 | 在连接建立的初期,如果窗口比较大,发送方可能会突然发送大量数据,导致网络瘫痪。因此,在通信一开始时,TCP 会通过慢启动算法得出窗口的大小,对发送数据量进行控制。 64 | 65 | 流量控制是由发送方和接收方共同控制的。刚刚我们介绍了接收方会把自己能够承受的最大窗口长度写在 TCP 首部中,实际上在发送方这里,也存在流量控制,它叫拥塞窗口。TCP 协议中的窗口是指发送方窗口和接收方窗口的较小值。 66 | 67 | **慢启动过程如下:** 68 | 69 | 1. 通信开始时,发送方的拥塞窗口大小为 1。每收到一个 ACK 确认后,拥塞窗口翻倍。 70 | 2. 由于指数级增长非常快,很快地,就会出现确认包超时。 71 | 3. 此时设置一个“慢启动阈值”,它的值是当前拥塞窗口大小的一半。 72 | 4. 同时将拥塞窗口大小设置为 1,重新进入慢启动过程。 73 | 5. 由于现在“慢启动阈值”已经存在,当拥塞窗口大小达到阈值时,不再翻倍,而是线性增加。 74 | 6. 随着窗口大小不断增加,可能收到三次重复确认应答,进入“快速重发”阶段。 75 | 7. 这时候,TCP 将“慢启动阈值”设置为当前拥塞窗口大小的一半,再将拥塞窗口大小设置成阈值大小(也有说加 3)。 76 | 8. 拥塞窗口又会线性增加,直至下一次出现三次重复确认应答或超时。 77 | 78 | 以上过程可以用下图概括: 79 | 80 | ![窗口大小变化示意图](https://user-gold-cdn.xitu.io/2017/12/12/1604b4a42ce0527d?w=1240&h=569&f=png&s=68021) 81 | 82 | 强烈建议读者对照上述八个步骤理解这幅图! -------------------------------------------------------------------------------- /articles/tcp-ip-6.md: -------------------------------------------------------------------------------- 1 | 本文是准备面试过程中网络部分总结整理的最后一篇文章,主要介绍以下知识: 2 | 3 | * HTTP 协议概述 4 | * POST 请求和 GET 请求 5 | * Cookie 和 Session 6 | * 数据传输时的加密 7 | * HTTPS 简介 8 | 9 | # HTTP 协议 10 | 11 | 在 OSI 七层模型中,HTTP 协议位于最顶层的应用层中。通过浏览器访问网页就直接使用了 HTTP 协议。使用 HTTP 协议时,客户端首先与服务端的 80 端口建立一个 TCP 连接,然后在这个连接的基础上进行请求和应答,以及数据的交换。 12 | 13 | ![HTTP 工作原理](https://user-gold-cdn.xitu.io/2017/12/12/1604b4b08c584d36?w=1054&h=1176&f=png&s=200997) 14 | 15 | HTTP 有两个常用版本,分别是 1.0 和 1.1。主要区别在于 HTTP 1.0 中每次请求和应答都会使用一个新的 TCP 连接,而从 HTTP 1.1 开始,运行在一个 TCP 连接上发送多个命令和应答。因此大幅度减少了 TCP 连接的建立和断开,提高了效率。 16 | 17 | 由 HTTP 协议加载出来的网页,通常使用 HTML 语言来描述,因此 HTML 也可以理解为网页的一种数据格式。HTML 是一段纯文本,可以指定网页中的文字、图像、音频视频图片、链接,以及它们的颜色、位置等。无论计算机的底层结构如何,也无论网络底层使用了哪些协议,使用 HTML 展示出来的效果基本上是一致的。从这个角度来说 HTML 位于 OSI 七层模型的表现层。 18 | 19 | # POST 请求和 GET 请求 20 | 21 | HTTP 有八种请求(也称方法),其中最常见的是 GET 请求和 POST 请求。 22 | 23 | GET 请求通常用于查询、获取数据,而 POST 请求则用于发送数据,除了用途上的区别,它们还有以下这些不同: 24 | 25 | 1. GET 请求可以被缓存,可以被收藏为书签,但 POST 不行。 26 | 2. GET 请求会保留在浏览器的历史记录中,POST 不会。 27 | 3. GET 请求的长度有限制(不同的浏览器不一样,大约在几 Kb 左右),URL 的数据类型只能是 ASCII 字符,POST 请求没有限制。 28 | 4. GET 请求的参数在 URL 中,因此绝不能用 GET 请求传输敏感数据。POST 请求数据则写在 HTTP 的请求头中,安全性略高于 GET 请求。 29 | 30 | **注意**: 31 | 32 | > POST 请求仅比 GET 请求略安全一点,它的数据不在 URL 中,但依然以明文的形式存放于 HTTP 的请求头中。 33 | 34 | # Cookie 和 Session 35 | 36 | HTTP 是一种无状态的连接,客户端每次读取 web 页面时,服务器都会认为这是一次新的会话。但有时候我们又需要持久保持某些信息,比如登录时的用户名、密码,用户上一次连接时的信息等。这些信息就由 Cookie 和 Session 保存。 37 | 38 | 这两者的根本性区别在于,cookie 保存在客户端上,而 session 则保存在服务器中。由此我们还可以拓展出以下结论: 39 | 40 | 1. cookie 相对来说不安全,浏览器可以分析本地的 cookie 进行 cookie 欺骗。 41 | 2. session 可以设置超时时间,超过这个时间后就失效,以免长期占用服务端内存。 42 | 3. 单个 cookie 的大小有限制(4 Kb),每个站点的 cookie 数量一般也有限制(20个)。 43 | 4. 客户端每次都会把 cookie 发送到服务端,因此服务端可以知道 cookie,但是客户端不知道 session。 44 | 45 | 当服务器接收到 cookie 后,会根据 cookie 中的 SessionID 来找到这个客户的 session。如果没有,则会生成一个新的 SessionID 发送给客户端。 46 | 47 | # 加密 48 | 49 | 加密分为两种,对称加密和非对称加密。在解释这两者的含义前,先来看一下简单的加密、解密过程: 50 | 51 | ![加密和解密过程](https://user-gold-cdn.xitu.io/2017/12/12/1604b4b099287540?w=1240&h=531&f=png&s=116098) 52 | 53 | 所谓的对称,就是指加密秘钥和解密秘钥相同,而非对称自然就是指两者不同。 54 | 55 | 举一个对称加密的例子。假设这里的加密算法是加法,解密算法是减法。如果明文数据是 10,秘钥是 1,那么加密数据就是 `10 + 1 = 11`,如果接收方不知道秘钥,就不知道密文 11 应该减去几。反之,如果接收方知道秘钥是 1,就可以通过 `11 - 1 = 10` 计算出明文数据。 56 | 57 | 常见的一个非对称加密算法是 RSA 算法,它主要利用了“两个素数求乘积容易,但是将乘积分解为两个素数很难”这一思想。它的具体原理不在本文讨论范围,有兴趣的读者可以查看文章末尾的参考文章。 58 | 59 | 在非对称加密中,利用公钥加密的数据能且只能通过私钥解密,通过私钥加密的数据能且只能通过公钥解密。 60 | 61 | 对称加密的优点在于速度快,但是假设秘钥由服务器保存,如何安全的让客户端得到秘钥是需要解决的问题。因此实际的网络传输中,通常使用对称加密与非对称加密结合的方式,服务端通过非对称加密将对称秘钥发给客户端。此后双方使用这个对称密钥进行通信。 62 | 63 | # HTTPS 64 | 65 | 我们知道 HTTP 协议直接使用了 TCP 协议进行数据传输。由于数据没有加密,都是直接明文传输,所以存在以下三个风险: 66 | 67 | 1. 窃听风险:第三方节点可以获知通信内容。 68 | 2. 篡改风险:第三方节点可以修改通信内容。 69 | 3. 冒充风险:第三方节点可以冒充他人身份参与通信。 70 | 71 | 比如你在手机上打开应用内的网页时,有时会看到网页底部弹出了广告,这实际上就说明你的 HTTP 内容被窃听、并篡改了。 72 | 73 | HTTPS 协议旨在解决以上三个风险,因此它可以: 74 | 75 | 1. 保证所有信息加密传输,无法被第三方窃取。 76 | 2. 为信息添加校验机制,如果被第三方恶意破坏,可以检测出来。 77 | 3. 配备身份证书,防止第三方伪装参与通信。 78 | 79 | HTTPS 的结构如图所示: 80 | 81 | ![HTTPS 协议](https://user-gold-cdn.xitu.io/2017/12/12/1604b4b08c3ccde6?w=669&h=359&f=png&s=81772) 82 | 83 | 可见它仅仅是在 HTTP 和 TCP 之间新增了一个 TLS/SSL 加密层,这也印证了一句名言:“一切计算机问题都可以通过添加中间层解决”。 84 | 85 | 使用 HTTPS 时,服务端会将自己的证书发送给客户端,其中包含了服务端的公钥。基于非对称加密的传输过程如下: 86 | 87 | 1. 客户端使用公钥将信息加密,密文发送给服务端 88 | 2. 服务端用自己的私钥解密,再将返回数据用私钥加密发回客户端 89 | 3. 客户端用公钥解密 90 | 91 | 这里的证书是服务器证明自己身份的工具,它由权威的证书颁发机构(CA)发给申请者。如果证书是虚假的,或者是自己给自己颁发的证书,服务器就会不认可这个证书并发出警告: 92 | 93 | ![12306 的自签名证书](https://user-gold-cdn.xitu.io/2017/12/12/1604b4b08f5869b8?w=1240&h=696&f=png&s=126089) 94 | 95 | **总结一下 HTTPS 协议是如何避免前文所说的三大风险的:** 96 | 97 | 1. 先用非对称加密传输密码,然后用这个密码对称加密数据,使得第三方无法获得通信内容 98 | 2. 发送方将数据的哈希结果写到数据中,接收方解密后对比数据的哈希结果,如果不一致则说明被修改。由于传输数据加密,第三方无法修改哈希结果。 99 | 3. 由权威机构颁发证书,再加上证书校验机制,避免第三方伪装参与通信。 100 | 101 | # 参考文章 102 | 103 | 1. [HTTPS科普扫盲帖](https://segmentfault.com/a/1190000004523659) 104 | 2. [SSL/TLS协议运行机制的概述](http://www.ruanyifeng.com/blog/2014/02/ssl_tls.html) 105 | 3. [RSA 加密](https://en.wikipedia.org/wiki/RSA_(cryptosystem)) 106 | 4. [HTTP 方法:GET 对比 POST](http://www.w3school.com.cn/tags/html_ref_httpmethods.asp) 107 | -------------------------------------------------------------------------------- /articles/uiscrollview-with-autolayout.md: -------------------------------------------------------------------------------- 1 | ## 背景 2 | 3 | 网上有很多使用Storyboard完成`UIScrollview`的例子,但是纯代码的例子却不多。有限的一些例子大多也是外国开发者用VFL写的。而这篇文章基于swift语言和SnapKit分析了如何用纯代码加Autolayout写`UIScrollview`,完整代码已经上传到我的[github](https://github.com/bestswifter/MySampleCode/tree/master/AutolayoutScrollViewInCode)。 4 | 5 | 在正文中,我会分析其中的关键代码。对于Autolayout,**绝对不可取**的态度是不停的试几个约束,一旦发现好用,也不管其原理,就放手不管了。事实上,我们写的每一个约束,都要明白它存在的价值是什么,要做到不写一个无用的约束,不漏一个必要的约束,明白为什么某种写法有效,而另一种写法就无效。 6 | 7 | 废话不多说,估计大家用`UIScrollView`时,都有过被Autolayout坑的经历,要么是布局不对,要么不能滑动,以及其他匪夷所思的bug。这与Autolayout和`UIScrollView`各自的特性有关。 8 | 9 | # 理论分析 10 | 11 | 首先,我们知道Autolayout改变了传统的以frame为主的布局思想。它其实是一种相对布局,核心思想是视图与视图之间的位置关系。比如,我们可以根据矩形的起始横坐标、纵坐标、长和宽这四个变量确定它的位置。或者,如果已经确定矩形A的位置,只要知道矩形B每条边的和A对应边之间的距离,也能确定B的位置。前者就是frame的思想,它基于绝对数值,而后者是Autolayout的思想,它基于偏移量的概念。 12 | 13 | 其次,`UIScrollView`有自己的frame也就是我们在屏幕上能看到的区域。它还有一个`contentSize`的概念。在使用frame布局的时候,我们一般先设置好子视图的位置,最后再设置`contentSize`,它会将所有的子视图包含在内。于是通过滑动,我们就可以在有限的布局中,看到所有的内容了。 14 | 15 | 但是在Autolayout时代,为了简化布局,我们希望`contentSize`能够自动设置。比如有一个scrollView,它有两个子视图。frame分别为(x: 0, y: 0, width: 10, height: 10)和(x: 10, y: 0, width: 10, height: 10),那么我们自然会认为这两个视图左右并排排列,`contentSize`为(x: 0, y: 0, width: 20, height: 10): 16 | 17 | ![自动计算contentSize](http://images.bestswifter.com/Autolayout+Scrollview/sample1.png) 18 | 19 | 这种把若干个子视图合并,得出`contentSize`的能力,人类是天生具备的,但是计算机却不是这样。仅凭以上信息,程序无法推断出真正的`contentSize`。原因在于,我们没有明确的告诉系统,在这两个子视图拼接而成的区域以外,还有没有区域应该被`contentSize`包含。 20 | 21 | 也就是说,`contentSize`也有可能是下图中的阴影部分: 22 | 23 | ![更大的contentSize](http://images.bestswifter.com/Autolayout+Scrollview/sample2.png) 24 | 25 | 如果需要指定`contentSize`就是两个正方形拼接而成的区域,我们还需要提供四个信息: 26 | 27 | 1. 左边的正方形的左侧的边,距离contentSize左边的距离为0 28 | 2. 右边的正方形的右侧的边,距离contentSize右边的距离为0 29 | 30 | …… 31 | 32 | 通过以上的分析,我们可以看到,其实`contentSize`是依赖于子视图自身的大小,和上下左右四个方向的留白大小计算出的。而`UIScrollView`的leading/trailing/top/bottom是相对于它的`contentSize`而不是`bounds`来确定的。所以如果你写这样的代码,布局是肯定不会生效的: 33 | 34 | ```swift 35 | subview.snp_makeConstraints { (make) -> Void in 36 | make.edges.equalTo(scrollView).offset(5) 37 | } 38 | ``` 39 | 40 | 因为我们其实是在根据`UIScrollView`的leading/trailing/top/bottom来确定子视图的位置,而我们已经分析过,`UIScrollView`的leading/trailing/top/bottom是相对于自己的`contentSize`而言的。而`contentSize`又是根据子视图位置决定的。这就变成了一种你依赖我,我又依赖你的情况。 41 | 42 | 为了打破这种循环依赖,为子视图添加约束的**两个要求**是: 43 | 44 | 1. 它不依赖于任何与scrollview有关布局,也就是不能参考scrollview的位置和大小。 45 | 2. 它不仅要确定过自己的大小,还要确定自己与contentSize四周的距离。 46 | 47 | 48 | 第二个要求意思是说,正常使用autolayout时,我们确定一个矩形在水平方向上的范围,只要知道它的左边距离它左边的矩形有多远,以及它有多宽即可。但是在`UIScrollView `中布局时,还需要告诉`UIScrollView `,它的右边距离右边的视图有多远。这样`contentSize`才能确定。否则`UIScrollView `就不知道`contentSize`向右可以延伸多少。在竖直方向上也是同理。 49 | 50 | **这两大要求一定要牢记!**接下来我们的代码都将围绕如何满足这两大要求展开。 51 | 52 | # 动手实践 53 | 54 | 明白了问题的理论背景后,我们通过一个具体的需求,来看看正确的代码怎么写,以下面这个效果为例: 55 | 56 | ![任务目标](http://images.bestswifter.com/Autolayout+Scrollview/1.pic.png) 57 | 58 | 如图所示,中间是一个`UIScrollView`,它的背景颜色是黄色。红色部分我们称之为`box`,它是一个普通的,红色背景的`UIView`。也就是说我们向`UIScrollView`中添加了多个`box`,每个子`box`之间间隔一定距离。我们分步实现这个功能 59 | 60 | ## 使用container 61 | 62 | 首先我们介绍一种使用Container的方法。 63 | 64 | ### 第一步:为scrollView添加约束 65 | 66 | 67 | ```swift 68 | let scrollView = UIScrollView() 69 | view.addSubview(scrollView) 70 | scrollView.snp_makeConstraints { (make) -> Void in 71 | make.centerY.equalTo(view.snp_centerY) 72 | make.left.right.equalTo(view) 73 | make.height.equalTo(topScrollHeight) 74 | } 75 | ``` 76 | 77 | 我们之前说过,使用Autolayout时,不用考虑frame布局。所以直接创建一个`scrollView `对象。需要先把`scrollView `添加到父视图上才能添加约束。 78 | 79 | 对`scrollView `添加约束没有什么难点,就像我们给其他视图添加约束一样。这里表示`scrollView`和父视图左右对齐,居中显示。 80 | 81 | ### 第二步:为container添加约束 82 | 83 | ```swift 84 | scrollView.addSubview(containerView) 85 | containerView.snp_makeConstraints { (make) -> Void in 86 | make.edges.equalTo(scrollView) 87 | make.height.equalTo(topScrollHeight) 88 | } 89 | ``` 90 | 91 | 这里对`container`的约束非常重要,第一个约束表示自己上、下、左、右和`contentSize`的距离为0,因此只要`container`的大小确定,`contentSize`也就可以确定了,因为此时它和`container `大小、位置完全相同。 92 | 93 | 第二个约束直接通过一个数值,确定`container `的高度。避免了依赖`scrollview `布局。这样一来,`scrollview `就变成水平的了。`container `的宽度直接决定了`scrollview `的宽度。 94 | 95 | ### 第三步:添加box 96 | 97 | ```swift 98 | for i in 0...5 { 99 | let box = UIView() 100 | containerView.addSubview(box) 101 | 102 | box.snp_makeConstraints(closure: { (make) -> Void in 103 | make.top.height.equalTo(containerView) // 确定top和height之后,box在竖直方向上完全确定 104 | make.width.equalTo(boxWidth) //确定width后,只要再确定left,就可以在水平方向上完全确定 105 | if i == 0 { 106 | make.left.equalTo(containerView).offset(boxGap / 2) //第一个box的left单独处理 107 | } 108 | else if let previousBox = containerView.subviews[i - 1] as? UIView{ 109 | make.left.equalTo(previousBox.snp_right).offset(boxGap) // 在前一个box右侧15个距离 110 | } 111 | if i == 5 { 112 | containerView.snp_makeConstraints(closure: { (make) -> Void in 113 | make.right.equalTo(box) // 确定container的右侧边界。 114 | }) 115 | } 116 | }) 117 | } 118 | ``` 119 | 120 | 对`box`的约束看似复杂,其实非常简单。因为`scrollview `在Autolayout下的布局,难点就在于子视图布局时约束比较多。但现在,我们通过一个`container`已经隔离了,也就说我们又回归了常规的Autolayout布局。以水平方向为例,我们只要确定`left`和`width`即可。 121 | 122 | 在最后一个`if`语句中,我们为`container `添加了右侧的约束。这样就确定了`container`的宽度。由于`container`封装了所有的`box`,所以对于`scrollview `来说,它的子视图只有一个,就是`container`,而`container`自身的大小,上下左右四个方向和`contentSize`距离在之前的约束中已经被定义为0,`contentSize`也就可以确定了。 123 | 124 | ## 使用外部视图 125 | 126 | 除了使用`container `以外,我们还可以使用外部的视图确定子视图的位置。这种方法,步骤较少,和之前一样,第一步是创建`scrollView`并添加约束。接下来我们直接添加子视图: 127 | 128 | ```swift 129 | box.snp_makeConstraints(closure: { (make) -> Void in 130 | make.top.equalTo(0) 131 | make.bottom.equalTo(view).offset(-(ScreenHeight - topScrollHeight) / 2) // This bottom can be incorret when device is rotated 132 | make.height.equalTo(topScrollHeight) 133 | 134 | make.width.equalTo(boxWidth) 135 | if i == 0 { 136 | make.left.equalTo(boxGap / 2) 137 | } 138 | else if let previousBox = scrollView.subviews[i - 1] as? UIView{ 139 | make.left.equalTo(previousBox.snp_right).offset(boxGap) 140 | } 141 | 142 | if i == 5 { 143 | make.right.equalTo(scrollView) 144 | } 145 | }) 146 | ``` 147 | 148 | 这时候,`box`是直接add到`scrollView`上的。我们直接指定它的`top`为0。前三个约束分别指定了`box`的顶部、底部和高度。这样就在竖直方向上满足了两大要求中的第二个要求。对于`bottom`的约束,它的参考物是`view`,这就是所谓的外部视图。 149 | 150 | 接下来我们分别为`width`和`left`添加了约束。而且只要对最后一个`box`添加`right`约束即可在水平方向上满足第二个要求。由于我们的布局依赖于外部的视图,所以自然满足第一个要求,因此这种写法也是可以的。 151 | 152 | ## Container与外部视图的优缺点 153 | 154 | 与`container`相比,使用外部视图除了代码量可能略少以外,我实在想不到它还有什么优点。 155 | 156 | 首先,一旦我们使用了`container`,首先它天然满足第一个要求,因为它并没有进行布局,只是让`contentSize`与自己等大,然后设置自己的大小。而且它几乎已经满足了第二个要求。只要我们最后确定它的宽度或高度即可。其次,在`container`内部,子视图布局不用考虑满足第二个要求,因为`container`已经隔离了这一切,我们要做的只是按照习惯,确定子视图的位置,这样`container`的位置也会随着子视图确定。 157 | 158 | 其次,我发现的使用外部视图布局的缺点就至少有三个: 159 | 160 | 1. 它依赖外部视图进行定位,这样的写法不够优雅 161 | 2. 观察代码中对于bottom属性的约束,它不能完美适配旋转屏幕后的视图。因为此时的屏幕长和宽会对调。而且目测没有什么好的解决方案。 162 | 3. 布局过程中容易踩到坑,比如对于`left`属性的约束,如果你的代码是这样的: 163 | 164 | ```swift 165 | make.left.equalTo(view).offset(boxGap / 2) 166 | ``` 167 | 168 | 它和原来的写法几乎是等价的。但你仔细分析,或者试着滑动`scrollView`时,一定会大吃一惊。如果你不能一眼看出来这种写法的问题所在,那我建议你运行代码体验一下,并且以后尽量避免这种写法。 169 | 170 | > 最后重复一下,代码地址在[https://github.com/bestswifter/MySampleCode/tree/master/AutolayoutScrollViewInCode](https://github.com/bestswifter/MySampleCode/tree/master/AutolayoutScrollViewInCode),可以下载下来把玩研究一番,如果觉得对你有帮助,请给一个star。 -------------------------------------------------------------------------------- /articles/weibo-short-url.md: -------------------------------------------------------------------------------- 1 | **这不是意外,不是 bug,是蓄谋已久的阴谋! 不知道阅读完以后,你会不会和我一样心里发寒** 2 | 3 | # 事件背景 4 | 5 | 前两天传出了新浪微博利用短链接恶意盗取用户收益的事件,可能很多吃瓜群众还不是特别明白是咋回事。这里我简单复盘一下,顺便聊聊个人的见解。 6 | 7 | 事情最初由 @im61 的这张图片引起: 8 | 9 | ![](http://images.bestswifter.com/1487336629.png) 10 | 11 | # 利益分析 12 | 13 | 首先要知道苹果的 iTunes 联盟,这个可以算是苹果官方的推广平台,具体介绍[看这里](https://itunes.phgconsole.performancehorizon.com/login/itunes/zh_cn),我没有用过,不过概括来说这个联盟就是一个平台,推广者帮忙推广 App,一旦有用户产生下载行为,推广者就可以获得收益。 14 | 15 | 这个联盟显然是一个三方平台,对推广者和应用开发者来说是双赢的局面,前者收获推广费用,后者获得下载流量,从而获得盈利。 16 | 17 | 为了标记某一次下载背后的推广者是谁(这样才好分成),苹果为每一个推广者提供了推广 ID(其实还有活动码,不过不是重点,所以略过),比如说某个应用的下载地址可能是: 18 | 19 | > https://itunes.apple.com/bestswifter 20 | 21 | 那么带有推广码的下载地址就是: 22 | 23 | > https://itunes.apple.com/bestswifter?at=1001|sTF 24 | 25 | 可以看到 HTTP 请求多了一个参数,参数的值 `1001|sTF` 就是 @im61 同学的推广码, 一旦用户点击了带推广码的地址,@im61 同学就会产生收益。 26 | 27 | # 短链接 28 | 29 | 由于 App 的下载地址本来就很长,再加上推广码和活动 ID,非常不利于阅读,因此微博提供了短链接生成器,比如通过微博的短链接生成器,我的长连接 **https://itunes.apple.com/bestswifter?at=1001|sTF** 会被转换成 **http://t.cn/RJ8HDRC**: 30 | 31 | ![](http://images.bestswifter.com/1487337349.png) 32 | 33 | 当你打开短链接的时候,实际上会经过一次 302 跳转,跳到原地址(也就是长连接的地址)。这个技术相当容易实现,因为理论上来说只要用一个字典来存储, 值是长连接,键是短链接(比如最简单的生成方法就是哈希一下)。 34 | 35 | 这样当你访问 `http://t.cn/RJ8HDRC` 这个网址时,服务器会拿到 `RJ8HDRC` 这个键,然后找到对应的值,也就是原始的 iTunes 长连接,再动态拼凑出一个 302 请求即可。302 请求表示页面临时被移动,根据 HTTP 规范,浏览器会重新请求新的临时地址。 36 | 37 | 我们可以验证一下: 38 | 39 | ![](http://images.bestswifter.com/1487337696.png ) 40 | 41 | # 新浪微博是如何作恶的 42 | 43 | 首先我们看到红色划线部分,有两个 at 参数,那么服务器以哪个为准呢,答案是不一定。HTTP 协议中并没有规定当 GET 方法的 Query 中出现重复的 key 怎么办,所以通常来说有三种解决方法: 44 | 45 | 1. 以前面的为准,比如 `&at=1&at=2` 会被服务器当做 **`&at=1`** 处理 46 | 2. 以后面的为准,比如 `&at=1&at=2` 会被服务器当做 **`&at=2`** 处理 47 | 3. 以两者的拼接结果为准,比如 `&at=1&at=2` 会被服务器当做 **`&at=[1,2]`** 处理 48 | 49 | 不同的 Web 服务器实现方法并不一样,比如 PHP 4.4 以后采用的是上述第二种方案。所以我们观察截图可以发现, 50 | 51 | **微博在原先的 at 字段后面新增了一个重复字段,填上了自己的推广码,一旦用户访问短链接,它返回的 302 重定向实际指向了微博自己的推广链接。** 52 | 53 | 这也证明不管苹果使用的是哪种 Web 服务器,必然都是使用了第二种处理策略。 54 | 55 | 稍微一动脑子就可以想明白四件事情: 56 | 57 | 1. 微博专门为窃取利益做了调试,它必须搞明白苹果 Web 服务器的处理逻辑。如果采用的是第一种策略,微博就不是在原先的 at 字段后面新增,而是在前面新增。 58 | 2. 只要短链接控制权在微博手上,它就可以随意做出修改,当然如果苹果采用了第三种策略,微博是无能为力的(苹果表示这个锅我不背)。 59 | 3. 显然并不是所有的链接都会被加上这样的参数,否则有相当多的地址都无法打开,比如我的博客 `https://bestswifter.com` 生成的短链接再解析回来依然正常。可见微博专门对短链接格式做了判断,如果是以 iTunes 开头才会做手脚。 60 | 4. 这也是我觉得最可怕的一点,微博可以针对 iTunes 做专门处理,自然也可以针对其他推广链接做类似处理,至于微博偷走了多少钱,我们不得而知。 61 | 62 | 更让人气愤但是,@im61 同学的微博在 2017 年 2 月 16 日中午一点钟发出,而我写作本文的时间是 17 日晚上 10 点,接近一天半的时间内微博找出各种理由,但就是没有修改的意图。根据我的经验来看,这种策略的添加和删除都是极为容易的,开发者一定提供了良好的接口。 63 | 64 | 所以我猜测微博的管理层觉得事情还不够大,每拖延一分一秒,又是一大笔收入,年底的财报和股价会更好看。 65 | 66 | 我没有办法证明时间的准确性,不过希望看到本文的读者还有机会亲自体验一下微博做的恶。 67 | 68 | # 如何防范 69 | 70 | 对于微博这种要钱不要脸的行为,如何防范呢,我想大概有两种方法。 71 | 72 | 第一种方法是利用第三方短链接服务,即先将自己的原始地址转换成安全的短链接,然后再将这个短链接转换成微博的短链接。这是原贴评论中有人提出的方案,我不知道为什么非要使用微博短链接(刚刚测了一下,使用别的短链接生成器似乎也可以),这里姑且认为是有什么限制吧。 73 | 74 | 这种方法是一种临时方法,并不保险,因为微博完全可以做一个递归判断,首先检查你的链接访问以后是不是 302,如果是 302 则抓取重定向的地址,直到找到 iTunes 为止。幸好暂时微博还没有这么做,不过为了钱,相信我,微博什么都做得出来。他们可以[不让你提现,帮你自动发广告微博,乱插时间线](http://weibo.com/ttarticle/p/show?id=2309404074275521878291#_0)。 75 | 76 | ## 自建服务器 77 | 78 | 最稳妥的方法还是使用自己的服务器,不过评论区中的 HTTPS 加密似乎用处不大。因为 HTTPS 加密只是请求过程加密,而你在发起请求时,必然走的是 GET 方法打开一个 URL,所以自己的推广码必然是明文放在请求头部,比如 79 | 80 | ``` 81 | HTTP GET https://bestswifter.com/promotion=我的推广码 82 | ``` 83 | 84 | 然后我在自己的服务器上返回一个 302 重定向,带上推广码,重定向到 iTunes 上。 85 | 86 | 在我看来这种做法已经足够安全,因为微博没办法识别自己的 URL 特征,前文说过它只是依赖于 iTunes 开头的链接的识别。当我的域名和 query 键名都被混淆后,微博不可能再对我请求做任何修改,如果还不放心,还可以在服务器上对请求参数自行校验。 87 | 88 | 总的来说,控制权在自己手上才是最安全。 89 | 90 | # 微博做了哪些恶 91 | 92 | 对于微博这样的中间平台来说,它本应该提供更好的安全保护服务,保护用户的合法利益不受侵犯。比如我们目前在做的广告和计费服务,客户端看到的并不是广告的真实地址, 也不会直接向服务器发送扣费请求。客户端能拿到的只是一个加了密的地址,在请求服务器时,一方面服务器对参数解密,进行扣费,另一方面返回 302,重定向到真正的广告地址。 93 | 94 | 而微博做的确实煞费苦心的调试广告平台存在的 bug,利用自身优势伪造请求数据(类似于 SQL 注入和 XSS 攻击),并针对不同目标平台做出区分,“优化” 自身收益。当恶行被曝光后,选择是找借口、拖时间而非及时承认错误,抓紧一分一秒从用户手上抢钱。 95 | 96 | 我想,微博欠无数像 @im61 这样的用户一个道歉。 97 | 98 | 99 | -------------------------------------------------------------------------------- /articles/wireshark.md: -------------------------------------------------------------------------------- 1 | # 背景 2 | 3 | 最近发现我们产品在打开广告链接(Webview)时有一定概率会非常慢,白屏时间超过 10s,追查广告的过程中遇到不少有意思的事情,感觉颇有收获。在这里分享一下,主要想聊一聊追查 bug 时的那些方法论,当然也不能太虚,还是要带一点干货,比如 WireShark 的使用。 4 | 5 | # Bug 复现 6 | 7 | 遇到 bug 后的第一件事当然是复现。经过一番测试我发现 bug 几乎只会主要出现在 iPhone6 这种老旧机型上,而笔者的 7Plus 则基本没有问题。4G 和 Wifi 下都有一定概率出现,Wifi 似乎更加频繁。 8 | 9 | 其实有点经验的开发者看到这里心里应该有点谱了,这应该不是客户端的 bug,更可能是由于广告主网页质量太低或者网络环境不稳定导致。但作为一个靠谱的程序员,怎么能把这种毫无根据的猜测向上级汇报呢? 10 | 11 | # 关注点分离 12 | 13 | 我们知道加载网页可以由两部分时间组成,一个是本地的处理时间,另一个是网络加载的时间。两者的分水岭应该在 `UIWebview` 的 `shouldStartLoadWithRequest` 方法上。这个方法调用之前是本地处理耗时,调用之后是网络加载的请求。所以我们可以把事情分成两部分来看: 14 | 15 | 1. 从 cell 接受点击事件的 `didSelectedRowAtIndexPath` 起到 `UIWebview` 的 `shouldStartLoadWithRequest` 为止。 16 | 2. 从 `shouldStartLoadWithRequest` 起到 `UIWebview` 的 `webViewDidFinishLoad` 为止。 17 | 18 | 由于 Bug 是偶现,所以不可能长时间用 Xcode 调试,所以还要注意写一个简单的工具,将每次的 Log 日志持久化存下来,保留每一步的函数调用、耗时、具体参数等。这样一旦复现出来,可以连上电脑读取手机中的日志。 19 | 20 | ## 本地处理 21 | 22 | 本地处理的耗时相对较短,但逻辑一点都不简单。在我个人看来,从展示 UITableview 到处理点击事件的流程,足以反映出一个团队的技术实力。毫不夸张的说,能把这个小业务做到完美的团队寥寥无几,其中必然涉及到 MVC/MVVM 等架构的选型设计与具体实现、网络层与持久化层的封装、项目模块化的拆分等核心知识点。我会尽快抽空专门一些篇文章来聊聊这些,这里就不再赘述。 23 | 24 | 花了一番功夫整理好业务流程、做好统计以后还真有一些收获。客户端的逻辑是 `pushViewController` 动画执行完后才发送请求,白白浪费了大约 0.5s 的动画时间,这些时间原本可以用来加载网页。 25 | 26 | ## 网络请求 27 | 28 | 借助日志我还发现,本地处理虽然浪费了时间,但这个时间相对稳定,大约在 1s 左右。更大的耗时来自于网络请求部分。一般情况下,打开网页会有短暂的白屏时间,这段时间内系统会加载 HTML 等资源并进行渲染,同时界面上有菊花在转动。 29 | 30 | 白屏什么时候消失取决于系统什么时候加载完网页,我们无法控制。但菊花消失的时间是已知的,我们的逻辑是写在 `webViewDidFinishLoad` 中。这么做不一定准确,因为网页重定向时也会调用 `webViewDidFinishLoad` 方法导致客户端误以为已经加载完成。更加准确的做法可以参考: [如何准确判断 WebView 加载完成](http://www.jianshu.com/p/897e2d82ee43),当然这也也仅仅是更准确一些,就 UIWebview 而言,想准确的判断网络是否加载完成几乎是不可能的(感谢 @JackAlan 的实践)。 31 | 32 | 所以说网络加载还可以细分为两部分,一个是纯白屏时间,另一部分则是出现了网页但还在转动菊花的时间。这是因为一个 Frame(可以是 HTML 也可以是 iFrame) 全部加载完成(包括 CSS/JS 等)后才会调用 `webViewDidFinishLoad` 方法,所以存在网页已经渲染但还在执行 JS 请求的情况,反映在用户端,就是能看到网页但菊花还在转动。这种情况如果持续时间过久会导致用户不耐烦,但相比于纯粹的白屏时间来说更能被接受一些。 33 | 34 | 同时我们也可以确定,如果网页已经加载,但 JS 请求还在继续,这就是广告主的网页质量太差导致的。损失应该由他们承担,我们无能为力。而长时间的白屏则是我们应该重点考虑的问题。 35 | 36 | # 小结 37 | 38 | 其实分析到这里已经可以向领导汇报了。网络加载的耗时一共是三段,第一段是本地处理时间,存在性能浪费但时间比较稳定,第二段是网页白屏时间,这段时间内系统的 `UIWebView` 在请求资源并渲染,第三段是加载网页后的菊花转动时间,一般耗时较少,我们也无法控制。 39 | 40 | 我们还知道 `UIWebView` 提供的 API 很少,从开始请求到网页加载结束完全是黑盒模式,几乎无从下手。但作为一名有追求,有理想,有抱负,有技术的四有程序员,怎么能轻言放弃呢? 41 | 42 | # WireShark 43 | 44 | 客户端在调试网络时最常用的工具要数 Charles,但它只能调试 HTTP/HTTPS 请求,对 TCP 层就无能为力了。要想了解 HTTP 请求过程中的细节,我们必须要使用威力更大(肯定也更复杂)的武器,也就是本文的主角 WireShark。 45 | 46 | 一般来说越牛X 的工具长得就越丑,WireShark 也毫不例外的有着一副让人懵逼的外表。 47 | 48 | ![](http://images.bestswifter.com/1492174337.png) 49 | 50 | 不过不用太急,我们要用到的东西不多,顶部红框里的蓝色鲨鱼标志表示开始监听网络数据,红色按钮一看也能猜出来是停止录制。与 Charles 只监听 HTTP 请求不同的是,WireShark 可以调试到 IP 层甚至更细节,所以它的数据包也更多,几秒钟的时间就会被上千个请求淹没,所以我建议用户略微控制一下监听的时长,或者我们可以在第二个红框中输入过滤条件来减少干扰,这个下文会详细介绍。 51 | 52 | WireShark 可以监听本机的网卡,也可以监听手机的网络。使用 WireShark 调试真机时不用连接代理,只需要通过 USB 连接到电脑就行,否则就无法调试 4G 网络了。我们可以用 `rvictl -s 设备 UDID` 命令来创建一个虚拟的网卡: 53 | 54 | ```shell 55 | rvictl -s 902a6a449af014086dxxxxxx346490aaa0a8739 56 | ``` 57 | 58 | 当然,看手机 UDID 还是挺麻烦的,作为一个懒人,怎么能不用命令行来完成呢? 59 | 60 | ```shell 61 | instruments -s | awk '{print $NF}' | sed -n 3p | awk '{print substr($0,2,length($0)-2)}' | xargs rvictl -s 62 | ``` 63 | 64 | 这样只要连上手机,就可以直接获取到 UDID 了。 65 | 66 | ![](http://images.bestswifter.com/1492174858.png) 67 | 68 | 运行命令后会看到成功创建 `rvi0` 虚拟网卡的提示,双击 `rvi0` 那一行即可。 69 | 70 | ![](http://images.bestswifter.com/1492175081.png) 71 | 72 | ## 抓包界面 73 | 74 | 我们主要关注两个内容,上面的大红框里面是数据流,包含了 TCP、DNS、ICMP、HTTP 等协议,颜色花花绿绿,绚丽多彩。一般来说黑色的内容表示遇到错误,需要重点关注,其他内容则辅助理解。反复调试几次以后也就能基本记住各种颜色对应的含义了。 75 | 76 | 下面的小红框里面主要是某一个包的数据详解,会根据不同的协议层来划分,比如我选中的 99 号包时一个 TCP 包,可以很清楚的看到它的 IP 头部、TCP 头部和 TCP Payload。这些数据必要时可以做更详细的分析,但一般也不用关注。 77 | 78 | ![](http://images.bestswifter.com/1492175403.png) 79 | 80 | 一般来说一次请求的数据包会非常大,可能会有上千个,如何找到自己感兴趣的请求呢,我们可以使用之前提到的过滤功能。WireShark 的过滤使用了一套自己定义的语法,不熟悉的话需要上网查一查或者借助自动补全功能来“望文生义”。 81 | 82 | 由于是要查看 HTTP 请求的具体细节,我们先得找到请求的网址,然后利用 `ping` 命令得到它对应的 IP 地址。这种做法一般没问题,但也不排除有的域名会做一些优化,比如不同的 IP 请求 DNS 解析时返回不同的 IP 地址来保证最佳速度。也就是说手机上 DNS 解析的结果并不总是和电脑上的解析结果一致。这种情况下我们可以通过查看 DNS 数据包来确定。 83 | 84 | ![](http://images.bestswifter.com/1492247468.png) 85 | 86 | 比如从图中可以看到 `res.wx.qq.com` 这个域名解析出了一大堆 IP 地址,而真正使用的仅有前两个。 87 | 88 | 解析出地址后,我们就可以做简单的过滤了,输入`ip.addr == 220.194.203.68`: 89 | 90 | ![](http://images.bestswifter.com/1492247708.png) 91 | 92 | 这样就只显示和 `220.194.203.68` 主机之间的通信了。注意红框中的 **SourcePort**,这是客户端端口。我们知道 HTTP 支持并发请求,不同的并发请求肯定是占用不同的端口。所以在图中看到的上下两个数据包,并非一定是请求与响应的关系,他们可能属于两个不同的端口,彼此之间毫无关系,只是恰好在时间上最接近而已。 93 | 94 | 如果只想显示某个端口的数据,可以使用:`ip.addr == 220.194.203.68 and tcp.dstport == 58854`。 95 | 96 | 如果只想看 HTTP 协议的 GET 请求与响应,可以使用 `ip.addr == 220.194.203.68 and (http.request.method == "GET" || http.response.code == 200)` 来过滤。 97 | 98 | 如果想看丢包方面的数据,可以用 `ip.addr == 220.194.203.68 and (tcp.analysis.fast_retransmission || tcp.analysis.retransmission)` 99 | 100 | 以上是笔者在调试过程中用到比较多的命令,仅供参考。有兴趣的读者可以自行抓包实验,就不挨个贴图了。 101 | 102 | ## Case1: DNS解析 103 | 104 | 经过多次抓包后我开始分析那些长时间白屏的网页对应的数据包,果然发现不少问题,比如这里: 105 | 106 | ![](http://images.bestswifter.com/1492175813.png) 107 | 108 | 可以很明显的看到在一大串黑色错误信息,但如果你去调试这些数据包,那么就掉进陷阱了。DNS 是基于 UDP 的协议,不会有 TCP 重传,所以这些黑色的数据包必定是之前的丢包重传,不用关心。如果只看蓝色的 DNS 请求,就会发现连续发送了几个请求但都没有响应,直到第 12s 才得到解析后的IP 地址。 109 | 110 | 从 DNS 请求的接收方的地址以 `172.24` 开头可以看出,这是内网 DNS 服务器,不知道为什么卡了很久。 111 | 112 | ## Case2: 握手响应延迟 113 | 114 | 下图是一次典型的 TCP 握手时的场景。同时也可以看到第一张图中的 SYN 握手包发出后,过了一秒钟才接受到 ACK。当然了,原因也不清楚,只能解释为网络抖动。 115 | 116 | ![](http://images.bestswifter.com/1492224316.png) 117 | 118 | 随后我又在 4G 网络下抓了一次包: 119 | 120 | ![](http://images.bestswifter.com/1492225650.png) 121 | 122 | 这次事情就更离谱了,第二秒发出的 SYN 握手包反复丢失(也有可能是服务端没有响应、或者是 ACK 丢失),总之客户端不断重传 SYN 包。 123 | 124 | 更有意思的是,观察 TSval,它表示包发出时的时间戳。我们观察这几个值会发现,前几次的间隔时间是 1s,后来变成了 2s,4s 和 8s。这不禁让我想起了 RTO 的概念。 125 | 126 | 我们知道 RTT 表示的是网络请求从发起到接收响应的时间,它是一个随着网络环境而动态改变的值。TCP 有窗口的概念,对于窗口的第一个数据包,如果它无法发送,窗口就不能向后滑动。客户端以接收到 ACK 作为数据包成功发送的标志,那么如果 ACK 收不到呢?客户端当然不会一直等下去,它会设置一个超时时间,一旦超过这个时间就认为数据包丢失,从而重传。 127 | 128 | 这个超时时间就被称为 RTO,显然它必须略大于 RTT,否则就会误报数据包丢失。但也不能过大,否则会浪费时间。因此合理的 RTO 必须跟随 RTT 动态调整,始终保证大于 RTT 但也不至于太大。观察上面的截图可以发现,某些情况下 RTT 会非常小,小到只有几毫秒。如果 RTO 也设置为几毫秒就会显得不太合理,这会加大客户端和沿途各路由器的压力。因此 RTO 还会设置下限,不同的操作系统可能有不同的实现,比如 Linux 上是 200ms。同时,RTO 也会设置上限,具体的算法可以参考[这篇文章](http://blog.csdn.net/onelight1997/article/details/7334455) 和[这篇文章](http://blog.csdn.net/jxh_123/article/details/27345151)。 129 | 130 | 需要注意的是,RTO 随着 RTT 动态变化,但如果达到了 RTO 导致了超时重传,以后的 RTO 就不再随着 RTT 变化了(此时的 RTT 无法计算),会指数增长。也就是上面截图中的间隔时间从 2s 变成 4s 再变成 8s 的原因。 131 | 132 | 同样的,我们发现了握手花费了 20s 这一现象,但无法给出准确原因,只能解释为网络抖动。 133 | 134 | # 总结 135 | 136 | 通过 TCP 层面的抓包,我们不仅仅学习了 WireShark 的使用,也复习了 TCP 协议的相关知识,对问题的分析也更加深入。从最初的网络问题开始细化挖掘,得出了白屏时间过长、网页加载太慢的结论,最终又具体的计算出了有多少个 HTTP 请求,DNS 解析、TCP 握手、TCP 数据传输等各个阶段的耗时。由此看来,网页加载慢的罪魁祸首并非广告主网页的质量问题,而是网络的不稳定问题。虽然最终也没有得到有效的解决方案,但至少明确了问题的发生原因,给出了令人信服的解释。 -------------------------------------------------------------------------------- /articles/xiaomi-router.md: -------------------------------------------------------------------------------- 1 | # 路由器爱国上网、屏蔽广告与宽带提速 2 | 3 | 在路由器中开启代理的好处很明显,最明显的就是连在这个路由器上的所有终端都不用考虑爱国上网的问题了。不管是 GMail 还是 Dropbox 这种服务都可以毫无顾忌的用起来了。 4 | 5 | 即使是在电脑的浏览器上有 [SwitchyOmega](https://chrome.google.com/webstore/detail/proxy-switchyomega/padekgcemlokbadohgkifijomclgjgif?utm_source=chrome-ntp-icon) 这样的神器,但终端和 APP 的爱国上网依然对很多人来说是个问题,比如经常出问题的 `npm instal` 和 Telegram、Dropbox 这类的桌面应用。 6 | 7 | 虽然很多路由器都自带 VPN,但恕我直言,VPN 是垃圾到无法使用的技术。因为它不支持透明代理,所谓的透明代理是指对于用户而言,无法察觉到自己被代理了。 8 | 9 | 以访问淘宝为例,如果是用 VPN 这种全局代理,就会被重定向到国际版的淘宝,导致速度大幅度降低。而如果是使用 shadowsocks 这类透明代理,会自动把淘宝、百度这类国内网站设置为不用代理,只有在访问谷歌等网站时才走代理。这个规则列表已经有人整理出来了,叫做 GFWList。 10 | 11 | 以上基本信息在我的上一篇文章:[全自动科学上网方案分享](./fq.md) 里面都有说过,就不多废话了。 12 | 13 | 文末会有宽带提速的教程,如果你还在用 20M 甚至 10M 的网络,那么每月只要十几块钱,就可以享受 100M 网络了,效果非常明显。 14 | 15 | ## 硬件选择 16 | 17 | 第一件事是要选择路由器,因为开启代理需要运行 shadowsocks 客户端,既然是软件,就一定要跑在操作系统上,自然会对硬件有一些要求。因此四五年前的硬件基本上就别想了。 18 | 19 | 这里我比较推荐 [小米路由 3G](http://union-click.jd.com/jdc?e=0&p=AyIHZRprEQISDlQbXCVGTV8LRGtMR1dGXgVFTUdGW0pADgpQTFtLH1sVCxMHUgQCUF5PNxBGHFd5elsuexhJRkYDVBsAfmNiBBMXV3sBEwdcB1oXHhEDRBtZHgIUDFESaxQyEgZUGl8TAhMAUCtrFQIiUTsbWhQDEwZQG1gXMhIEVhpZEAsVAlIrWxEBEg9RH1oTCxEDVitcJVlHaVMbXkcBFQNUGw9BBBQ3ZR9bFQsTB1IrayUyEjdW&t=W1dCFBBFC1pXUwkEAEAdQFkJBV8VAhsGVRxETEdOWg%3D%3D),理由是价格便宜,两百出头一点,硬件性能足够(主要就是看 CPU、内存和闪存大小),两个 LAN 口和一个 WAN 口都是千兆的,在可预见的未来不会拖累网速。 20 | 21 | # 操作流程 22 | 23 | 整个流程可以分为以下几步: 24 | 25 | 1. 刷开发版系统 26 | 2. 开启 SSH 27 | 3. 刷入 breed 启动引导 28 | 4. 刷入 Padavan 固件 29 | 5. 配置透明代理和去广告 30 | 6. 宽带提速 31 | 32 | ## 开启 SSH 33 | 34 | 前文说了,路由器就是一个小型的 Linux 操作系统,第一步自然就是要打开 SSH,这样才能在命令行里操作路由器。 35 | 36 | > **注意下文所有固件均针对小米路由器 3G,请勿乱用,变成砖概不负责** 37 | 38 | 首先进入[小米路由器官网](http://www.miwifi.com/miwifi_download.html),在 ROM 列表里找到找到 **ROM for R3G 开发版**,下载下来后拷贝到 FAT/FAT32 格式的 U盘中并重命名为 **miwifi.bin** 39 | 40 | 接下来先拔掉路由器电源线,再插入 U盘,然后找一根牙签或掏耳勺的柄戳住 Reset 键(在 U盘旁边的小洞里)。 41 | 42 | 最后接通电源,过大概 15 秒后路由器指示灯会变成黄灯并且一闪一闪的,此时松开 reset 键,等几分钟路由器就会重装好。 43 | 44 | 一般来说这一步没有风险。 45 | 46 | ## 开启 SSH 47 | 48 | 打开 ,下载工具包,放入 U盘中并重命名为 **miwifi_ssh.bin**。这个页面还会告诉你 Root 用户的密码,待会儿要用到。 49 | 50 | 然后重复之前的安装步骤(断电、插 U盘、按住 Rest、通电等黄灯闪烁、松开 Reset 等安装完成)。 51 | 52 | 路由器重启后,在终端输入 `ssh root@192.168.31.31`,然后输入之前记下的 Root 密码应该就连上了。 53 | 54 | 这一步基本也没有风险。 55 | 56 | ## 刷入 Breed 启动引导 57 | 58 | 理论上来说这一步是可选的,我们可以直接刷 Padavan 固件,但存在刷坏的风险。Breed 是社区开发的引导工具,用于替换系统的引导工具。 59 | 60 | 这样做的好处相当于是增加了一个中间层,只要刷入成功,后续就是在 Breed 里面搞事情了,不会影响系统,自然就不会变砖了。不管是更新固件还是切换回小米原装的固件,都非常容易。 61 | 62 | 这一步很容易,没有硬件操作,只要在连上 SSH 后输入以下命令就行: 63 | 64 | ```shell 65 | cd /tmp 66 | wget https://breed.hackpascal.net/breed-mt7621-xiaomi-r3g.bin 67 | mtd -r write breed-mt7621-xiaomi-r3g.bin Bootloader 68 | ``` 69 | 70 | 耐心等待路由器重启即可。 71 | 72 | 这一步风险较高,如果操作失误可能会导致路由器变砖。好在步骤比较简单,只要不作死,复制上面的命令来执行就不会出问题。 73 | 74 | ## 刷入 Padavan 控件 75 | 76 | 首先下载控件:,如果地址失效就自行搜索,注意文件名是:**MI-R3G_3.4.3.9-099.trx**。 77 | 78 | 接下来我们需要进入刷机模式:断开电源,拔下 U盘,按住 Reset 键,插回电源,过几秒后指示灯开始闪烁,松开 Reset 键即可。 79 | 80 | 这一步和之前刷 ROM 和开启 SSH 的步骤类似,但注意不要插入 U盘。 81 | 82 | 然后在浏览器访问 192.168.1.1 就可以进入 Breed 的控制台。如果家里的路由器连在上级光猫路由器上,192.168.1.1 可能会被占用,此时拔掉小米路由器的 WAN 口网线即可。 83 | 84 | 最后在 Breed Web 控制台依次选择:固件更新 -> 常规固件 -> 勾选固件复选框 -> 浏览,选择刚刚下载好的 Padavan 固件上传,刷入搞定! 85 | 86 | ## 配置代理和去广告 87 | 88 | 重启之后通过 192.168.123.1 进入 Padavan 控制台,在左侧拓展功能中找到 Shadowsocks,填写 VPS 的信息,如果没有的话可以去 [搬瓦工](https://bwh1.net/aff.php?aff=19860&pid=55) 购买年费 39 刀,每月 2000G 中国直连流量的套餐。工作模式建议选择 GFWList 并自动更新,这样可以获取最新的爱国上网地址列表,实现透明代理。 89 | 90 | 同样是在左侧,找到 **广告屏蔽功能**,比较推荐用 koolproxy 来过滤广告。注意如果广告使用 HTTPS 是无法屏蔽的,因为插件无法判断某个请求是不是广告,这时候你有两种选择: 91 | 92 | 1. 按照 koolproxy 提供的根证书,存在一定的安全风险,但可以彻底屏蔽广告 93 | 2. 不用根证书,保证绝对安全,忍受 HTTPS 广告。 94 | 95 | 这里笔者不做推荐,用户自行评价 96 | 97 | ## 宽带提速 98 | 99 | 在左侧拓展功能->配置拓展环境中,打开迅雷快鸟,输入开通了迅雷快鸟的迅雷账号和密码即可。 100 | 101 | 笔者使用的是 50M 的北京联通宽带,实测开启后网速接近 100M,原理是迅雷与网络服务商达成了协议,授权迅雷接触网络运营商的网速限制。 102 | 103 | 由于不是所有用户通用,建议先自行测试下提速比例,合适再买。 104 | 105 | ## TODO 106 | 107 | 其实还有些小遗憾,主要是小米路由器 3G 自带的 USB3.0 接口没有被我有效利用起来,后续还可以添加远程迅雷下载和硬盘 FTP 共享的功能。 -------------------------------------------------------------------------------- /pictures/cocoapods-debug/gem-env.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bestswifter/blog/7c6c129a20dec751c5634baa3ec19072f14e49bd/pictures/cocoapods-debug/gem-env.png -------------------------------------------------------------------------------- /pictures/cocoapods-debug/rubymine-config-bundle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bestswifter/blog/7c6c129a20dec751c5634baa3ec19072f14e49bd/pictures/cocoapods-debug/rubymine-config-bundle.png -------------------------------------------------------------------------------- /pictures/cocoapods-debug/rubymine-config-main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bestswifter/blog/7c6c129a20dec751c5634baa3ec19072f14e49bd/pictures/cocoapods-debug/rubymine-config-main.png -------------------------------------------------------------------------------- /pictures/cocoapods-debug/rubymine-debug-cocoapods-binary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bestswifter/blog/7c6c129a20dec751c5634baa3ec19072f14e49bd/pictures/cocoapods-debug/rubymine-debug-cocoapods-binary.png -------------------------------------------------------------------------------- /pictures/cocoapods-debug/rubymine-debug-cocoapods-core.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bestswifter/blog/7c6c129a20dec751c5634baa3ec19072f14e49bd/pictures/cocoapods-debug/rubymine-debug-cocoapods-core.png -------------------------------------------------------------------------------- /pictures/cocoapods-debug/rubymine-debug-main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bestswifter/blog/7c6c129a20dec751c5634baa3ec19072f14e49bd/pictures/cocoapods-debug/rubymine-debug-main.png -------------------------------------------------------------------------------- /pictures/jianzhi/bmi.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bestswifter/blog/7c6c129a20dec751c5634baa3ec19072f14e49bd/pictures/jianzhi/bmi.jpg -------------------------------------------------------------------------------- /pictures/jianzhi/compare.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bestswifter/blog/7c6c129a20dec751c5634baa3ec19072f14e49bd/pictures/jianzhi/compare.jpg -------------------------------------------------------------------------------- /pictures/jianzhi/energy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bestswifter/blog/7c6c129a20dec751c5634baa3ec19072f14e49bd/pictures/jianzhi/energy.png -------------------------------------------------------------------------------- /pictures/swift-coverage/16289505462244.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bestswifter/blog/7c6c129a20dec751c5634baa3ec19072f14e49bd/pictures/swift-coverage/16289505462244.jpg -------------------------------------------------------------------------------- /pictures/swift-coverage/16289950183691.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bestswifter/blog/7c6c129a20dec751c5634baa3ec19072f14e49bd/pictures/swift-coverage/16289950183691.jpg -------------------------------------------------------------------------------- /pictures/swift-coverage/16289952692264.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bestswifter/blog/7c6c129a20dec751c5634baa3ec19072f14e49bd/pictures/swift-coverage/16289952692264.jpg -------------------------------------------------------------------------------- /pictures/swift-coverage/16289964552519.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bestswifter/blog/7c6c129a20dec751c5634baa3ec19072f14e49bd/pictures/swift-coverage/16289964552519.jpg -------------------------------------------------------------------------------- /pictures/swift-coverage/16289972039432.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bestswifter/blog/7c6c129a20dec751c5634baa3ec19072f14e49bd/pictures/swift-coverage/16289972039432.jpg -------------------------------------------------------------------------------- /pictures/swift-coverage/16289987785206.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bestswifter/blog/7c6c129a20dec751c5634baa3ec19072f14e49bd/pictures/swift-coverage/16289987785206.jpg -------------------------------------------------------------------------------- /pictures/swift-coverage/16290003214898.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bestswifter/blog/7c6c129a20dec751c5634baa3ec19072f14e49bd/pictures/swift-coverage/16290003214898.jpg -------------------------------------------------------------------------------- /pictures/swift-coverage/16290004371805.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bestswifter/blog/7c6c129a20dec751c5634baa3ec19072f14e49bd/pictures/swift-coverage/16290004371805.jpg -------------------------------------------------------------------------------- /pictures/swift-coverage/16290004530676.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bestswifter/blog/7c6c129a20dec751c5634baa3ec19072f14e49bd/pictures/swift-coverage/16290004530676.jpg -------------------------------------------------------------------------------- /pictures/swift-coverage/16290144521686.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bestswifter/blog/7c6c129a20dec751c5634baa3ec19072f14e49bd/pictures/swift-coverage/16290144521686.jpg -------------------------------------------------------------------------------- /pictures/swift-coverage/16290157144891.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bestswifter/blog/7c6c129a20dec751c5634baa3ec19072f14e49bd/pictures/swift-coverage/16290157144891.jpg -------------------------------------------------------------------------------- /pictures/swift-coverage/carbon -1-.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bestswifter/blog/7c6c129a20dec751c5634baa3ec19072f14e49bd/pictures/swift-coverage/carbon -1-.png -------------------------------------------------------------------------------- /pictures/swift-coverage/carbon -2-.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bestswifter/blog/7c6c129a20dec751c5634baa3ec19072f14e49bd/pictures/swift-coverage/carbon -2-.png -------------------------------------------------------------------------------- /pictures/swift-coverage/carbon -3-.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bestswifter/blog/7c6c129a20dec751c5634baa3ec19072f14e49bd/pictures/swift-coverage/carbon -3-.png -------------------------------------------------------------------------------- /pictures/swift-coverage/carbon -4-.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bestswifter/blog/7c6c129a20dec751c5634baa3ec19072f14e49bd/pictures/swift-coverage/carbon -4-.png -------------------------------------------------------------------------------- /pictures/swift-coverage/carbon -5-.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bestswifter/blog/7c6c129a20dec751c5634baa3ec19072f14e49bd/pictures/swift-coverage/carbon -5-.png -------------------------------------------------------------------------------- /pictures/swift-coverage/swift-coverage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bestswifter/blog/7c6c129a20dec751c5634baa3ec19072f14e49bd/pictures/swift-coverage/swift-coverage.png -------------------------------------------------------------------------------- /pictures/swift-dictionary/call-find.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bestswifter/blog/7c6c129a20dec751c5634baa3ec19072f14e49bd/pictures/swift-dictionary/call-find.png -------------------------------------------------------------------------------- /pictures/swift-dictionary/debug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bestswifter/blog/7c6c129a20dec751c5634baa3ec19072f14e49bd/pictures/swift-dictionary/debug.png -------------------------------------------------------------------------------- /pictures/swift-dictionary/find-out-impl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bestswifter/blog/7c6c129a20dec751c5634baa3ec19072f14e49bd/pictures/swift-dictionary/find-out-impl.png -------------------------------------------------------------------------------- /pictures/swift-interface/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bestswifter/blog/7c6c129a20dec751c5634baa3ec19072f14e49bd/pictures/swift-interface/error.png -------------------------------------------------------------------------------- /pictures/swift-interface/inlinable-age-setter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bestswifter/blog/7c6c129a20dec751c5634baa3ec19072f14e49bd/pictures/swift-interface/inlinable-age-setter.png -------------------------------------------------------------------------------- /pictures/swift-interface/inlinable-none-stability.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bestswifter/blog/7c6c129a20dec751c5634baa3ec19072f14e49bd/pictures/swift-interface/inlinable-none-stability.png -------------------------------------------------------------------------------- /pictures/swift-interface/inlinable-stability.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bestswifter/blog/7c6c129a20dec751c5634baa3ec19072f14e49bd/pictures/swift-interface/inlinable-stability.png --------------------------------------------------------------------------------