├── .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 |  50 | 51 | 年底提前续约,竟然租金还小幅度下降了一些,明年的生活压力也不会太大了。 52 | 53 | 五月份的时候天津发布了海河英才计划,奔波十几趟以后拿到了户口,随后掏空父母的积蓄在郊区接盘了一套房子,并且一直后悔到现在。 54 | 55 |  56 | 57 | 十月份迎来了一位新的家庭成员:一只可爱的渐层猫猫 58 | 59 |  60 | 61 | 年底借着出差的机会,第一次来到深圳,参观了世界之窗,腾讯滨海大厦等地标性建筑。 62 | 63 |  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 |  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 |  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 |  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 |  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 |  182 | 183 | ## 效果展示 184 | 185 | 只要配置了本地依赖的模块,都可以被正确的调试。 186 | 187 | ### cocoapods 源码 188 | 189 |  190 | 191 | ### cocoapods-core 插件 192 | 193 |  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 |  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 |  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 |  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 |  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 |  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 |  22 | 23 | 路由器上的科学上网,最重要的就是稳定。但任何节点都不可能永远稳定,好在服务商一次会提供很多节点可供选择,所以我们只要在节点宕机以后进行自动切换即可。我没有采用故障转移的方案,这种方案无法控制备用节点的范围,会导致可能切换到性能很差的节点。 24 | 25 | 我的解决方案是使用 Haproxy 进行自动恢复。Haproxy 是一个类似于 Nginx 的负载均衡服务。配置方式如下,我挑选了一个最稳定高速的节点作为主节点,以及一些性能和稳定性都不错的节点作为备用节点: 26 | 27 |  28 | 29 | 配置成功后,会自动生成一个 IP 地址为 0.0.0.0,端口 1181 的 SS 节点。好处是,可以一次性订阅很多节点,然后选择其中质量好的做为主、备用节点。 30 | 31 | 在路由器上,我还搭建了一个 socks5 代理: 32 | 33 |  34 | 35 | 首先需要填写 ss 服务的信息。由于上面我已经通过 haproxy 搭建了具备容灾功能的节点,这里就可以直接用上了,避免出现直接写服务商的节点可能导致的偶尔不可用问题。配置好后,就对外提供一个 14179 端口作为 socks5 代理,这样接入的电脑不再需要安装/启动 GoAgent/Clash 等软件,可以在需要科学上网的地方使用路由器的 socks5 代理,常见的有: 36 | 37 | 1. Chrome 的 SwitchyOmega 插件: 38 | 39 |  40 | 41 | 2. 终端需要科学上网时,使用 `export ALL_PROXY=socks5://192.168.50.1:14179` 即可。 42 | 43 | 总体来看,服务的结构大概是这样的: 44 | 45 |  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 |  63 | 64 | 对比谷歌的: 65 | 66 |  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” 这条线路为例演示下: 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 |  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 | HEAD@{8}: 这里我们创建了初始的提交 19 | HEAD@{7}:检出了分支 a 20 | HEAD@{6}:在分支 a 上做了一次提交,注意 master 分支没有变动 21 | HEAD@{5}:从分支 a 回到分支 master,相当于向后退了一次 22 | HEAD@{4}:检出了分支 b 23 | HEAD@{3}:在分支 b 上做了一次提交,注意 master 分支没有变动 24 | HEAD@{2}:这一步开始变基到分支 a,首先切换到分支 a 上 25 | HEAD@{1}:把分支 b 对应的那次提交变基到分支 a 上 26 | HEAD@{0}:变基结束,因为是在 b 上发起的变基,所以最后还切回分支 b 27 | 28 | 如果我们想撤销此次 rebase,只要输入以下命令就可以了: 29 | git reset --hard HEAD@{3} 30 | 31 | 此时再看,已经“恢复”到 rebase 前的状态了。的是不是感觉很神奇呢,先别着急,后面会介绍这么做的原理。 32 | git 工作原理简介 33 | 为了搞懂 git 是如何工作的,以及这些命令背后的原理,我想有必要对 git 的模型有基础的了解。 34 | 首先,每一个 git 目录都有一个名为 .git 的隐藏目录,关于 git 的一切都存储于这个目录里面(全局配置除外)。这个目录里面有一些子目录和文件,文件其实不重要,都是一些配置信息,后面会介绍其中的 HEAD 文件。子目录有以下几个: 35 | 36 | info:这个目录不重要,里面有一个 exclude 文件和 .gitignore 文件的作用相似,区别是这个文件不会被纳入版本控制,所以可以做一些个人配置。 37 | hooks:这个目录很容易理解, 主要用来放一些 git 钩子,在指定任务触发前后做一些自定义的配置,这是另外一个单独的话题,本文不会具体介绍。 38 | objects:用于存放所有 git 中的对象,下面单独介绍。 39 | logs:用于记录各个分支的移动情况,下面单独介绍。 40 | refs:用于记录所有的引用,下面单独介绍。 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 指向的那个提交对象。理解这一点非常重要,否则你会无法理解 checkout 和 reset 的区别。 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 | 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 |  85 | 86 | 从二进制的 0 - 63 映射到 base64 编码的原则比较简单,就不列出来了,可以参考维基百科的链接。 87 | 88 | base64 编码有两个小细节需要注意下,它的实际工作方式并不是每 6 个比特转换一个 ASCII 字符,而是选择 6 和 8 的最小公倍数 24,每次读取 24 个 bit,也就是三个字节转换成四个 base64 编码后的字符。如果被编码的字节数不是 3 的倍数,那么可能会多出 1 个或者 2 个字节。如果多出一个字节,可以转换成 2 个字节的 base64 编码,所以还缺两位,需要用两个等号补齐。如果多出两个字节,可以转换成三个字节的 base64 编码,需要用一个等号补齐。如下图所示: 89 | 90 |  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 |  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 |  110 | 111 | 这是客户端直连时候的正常现象。但如果你用 Charles 代理,客户端拿到的是 Charles 证书,所以会变成: 112 | 113 |  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 |  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 |  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 |  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 |  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 |  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 |
假设我们有两个分支,a 和 b,它们的提交都有一个相同的父提交(master 指向的那次提交)。如图所示:
现在我们在分支 a 上,然后 rabase 到分支 b 上。如图所示:
平时开发中经常遇到这种情况,假设分支 a 和 b 是两个独立的 feature 分支,但是不小心被我们错误的 rebase 了。现在相当于两个 feature 分支中原本独立的业务被揉起来了,当然是我们不想看到的结果,那么如何撤销呢?
一种方案是利用 reflog 命令。
我们先不考虑原理,直接上解决方案,首先输入 git reflog,你会看到如下图所示的日志:
git reflog
最后的输出其实是最早的操作,我们逐条分析下:
如果我们想撤销此次 rebase,只要输入以下命令就可以了:
git reset --hard HEAD@{3} 30 |
此时再看,已经“恢复”到 rebase 前的状态了。的是不是感觉很神奇呢,先别着急,后面会介绍这么做的原理。
为了搞懂 git 是如何工作的,以及这些命令背后的原理,我想有必要对 git 的模型有基础的了解。
首先,每一个 git 目录都有一个名为 .git 的隐藏目录,关于 git 的一切都存储于这个目录里面(全局配置除外)。这个目录里面有一些子目录和文件,文件其实不重要,都是一些配置信息,后面会介绍其中的 HEAD 文件。子目录有以下几个:
.git
.gitignore
本文主要会介绍后面三个文件夹的作用。
git 是面向对象的! 45 | git 是面向对象的! 46 | git 是面向对象的!
没错,git 是面向对象的,而且很多东西都是对象。我举个简单的例子,来帮助大家理解这个概念。假设我们在一个空仓库里,编辑了 2 个文件,然后提交。此时都会有那些对象呢?
首先会有两个数据对象,每个文件都对应一个数据对象。当文件被修改时,即使是新增了一个字母,也会生成一个新的数据对象。
其次,会有一个树对象用来维护一系列的数据对象,叫树对象的原因是它持有的不仅可以是数据对象,还可以是另一个树对象。比如某次提交了两个文件和一个文件夹,那么树对象里面就有三个对象,两个是数据对象,文件夹则用另一个树对象表示。这样递归下去就可以表示任意层次的文件了。
最后则是提交对象,每个提交对象都有一个树对象,用来表示某一次提交所涉及的文件。除此以外,每一个提交还有自己的父提交,指向上一次提交的对象。当然,提交对象还会包含提交时间、提交者姓名、邮箱等辅助信息,就不多说了。
假设我们只有一个分支,以上知识点就足够解释 git 的提交历史是如何计算的了。它并不存储完整的提交历史,而是通过父提交的对象不断向前查找,得出完整的历史。
注意开头那张图片,分支 b 指向的提交是 9cbb015,不妨来看下它是何方神圣:
9cbb015
git cat-file -t 9cbb015 54 | git cat-file -p 9cbb015 55 |
这里我们使用 cat-file 命令,其中 -t 参数打印对象的类型,-p 参数会智能识别类型,并打印其中的内容。输出结果如图所示:
cat-file
-t
-p
可见 9cbb015 是一个提交对象,里面包含了树对象、父提交对象和各种配置信息。我们可以再打印树对象看看:
这表示本次提交只修改了 begin 这个文件,并且输出了 begin 这个文件对于的数据对象。
既然 git 是面向对象的,那么有没有指正呢?还真是有的,分支和标签都是指向提交对象的指针。这一点可以验证:
cat .git/refs/heads/a 64 |
所有的本地分支都存储在 git/refs/heads 目录下,每一个分支对应一个文件,文件的内容如图所示:
git/refs/heads
可见,4a3a88d 刚好是本文第一张图中分支 a 所指向的提交。
4a3a88d
我们已经搞明白了 git 分支的秘密,现在有了所有分支的记录,又有了每次提交的父提交对象,就能够得出像 SourceTree 或者文章开头第一张图那样的提交状态了。
至于标签,它其实也是一种引用,可以理解为不能移动的分支。只能永远指向某个固定的提交。
最后一个比较特殊的引用是 HEAD,它可以理解为指针的指针,为了证明这一点,我们看看 .git/HEAD 文件:
.git/HEAD
它的内容记录了当前指向哪个分支,refs/heads/b 其实是一个文件,这个文件的内容是分支 b 指向的那个提交对象。理解这一点非常重要,否则你会无法理解 checkout 和 reset 的区别。
refs/heads/b
checkout
reset
这两个命令都会改变 HEAD 的指向,区别是 checkout 不改变 HEAD 指向的分支的指向,而 reset 会。举个例子, 在分支 b 上执行以下两个命令都会让 HEAD 指向 4a3a88d 这次提交(分支 a 指向的提交):
git checkout a 75 | git reset --hard a 76 |
但 checkout 仅改变 HEAD 的指向,不会改变分支 b 的指向。而 reset 不仅会改变 HEAD 的指向,还因为 HEAD 指向分支 b,就把 b 也指向 4a3a88d 这次提交。
b
在 .git/logs 目录中,有一个文件夹和一个 HEAD 文件,每当 HEAD 引用改变了指向的位置,就会在 .git/logs/HEAD 中添加了一个记录。而 .git/logs/refs/heads 这个目录中则有多个文件,每个文件对应一个分支,记录了这个分支 的指向位置发生改变的情况。
.git/logs
.git/logs/HEAD
.git/logs/refs/heads
当我们执行 git reflog 的时候,其实就是读取了 .git/logs/HEAD 这个文件。
首先我们要排除一个误区,那就是 git 会维护每次提交的提交对象、树对象和数据对象,但并不会维护每次提交时,各个分支的指向。在介绍分支的那一节中我们已经看到,分支仅仅是一个保留了提交对象的文件而已,并不记录历史信息。即使在上一节中,我们知道分支的变化信息会被记录下来,但也不会和某个提交对象绑定。
也就是说,git 中并不存在某次提交时的分支快照
那么我们是如何通过 reset 来撤销 rebase 的呢,这里还要澄清另一个事实。前文曾经说过,某个时刻下你通过 SourceTree 或者 git log 看到的分支状态,其实是由所有分支的列表、每个分支所指向的提交,和每个提交的父提交共同绘制出来的。
git log
首先 git/refs/heads 下的文件告诉我们有多少分支,每个文件的内容告诉我们这个分支指向那个提交,有了这个提交不断向前追溯就绘制出了这个分支的提交历史。所有分子的提交历史也就组成了我们看到的状态。
但我们要明确:不是所有提交对象都能看到的,举个例子如果我们把某个分支向前移一次提交,那个分支的提交线就会少一个节点,如果没有别的提交线包含这个节点,这个节点就看不到了。
所以在 rebase 完成后,我们以为看到了下面这样的提交线:
df0f2c5(master) --- 4a3a88d(a) --- 9cbb015(b) 90 |
实际上是这样的:
df0f2c5(master) --- 4a3a88d(a) --- 9d0618e(b) 93 | | 94 | 9cbb015 95 |
master 分支上依然有分叉,原来 9cbb015 这次提交依然存在,只不过没有分支的提交线包含它,所以无法看到而已。但是通过 reflog,我们可以找回 HEAD 头的每一次移动,所以能看到这次提交。
reflog
当我们执行这个命令时:
git reset --hard HEAD@{3} 99 |
再看一次 reflog 的输出:
HEAD@{3} 其实是它左侧 9cbb015 这次提交的缩写,所以上述命令等价于:
HEAD@{3}
git reset --hard 9cbb015 104 |
前文说过,reset 不仅会移动 HEAD,还会移动 HEAD 所指向的分支,所以这个命令的执行结果就是让 HEAD 和分支 b 同时指向 9cbb015 这个提交,看起来像是撤销了 rebase。
但别忘了,分支 a 的上面还是有一次提交的,9d0618e 这次提交仅仅是没有分支指向它,所以不显示而已。但它真实的存在着,严格意义上来说,我们并没有真正的撤销此次 rebase。
hello world
Hello Wrold