分类目录归档:Programming

【利器系列】01-Dash-开发人员的随身好字典

前文

Dash 是我开始工作有了收入之后,购买的第一款跟开发相关的软件,从 2011 年开始,「这本好字典」就一直在我手边,每每遇到不认识的「字」,打开 Dash 便是我下意识的动作。今天,我们就来聊聊 Dash 这本「好字典」。

我是 2009 年 7 月来到北京,加入超图软件开始了为期不到一年的 SuperMap Objects for Java 的 SDK 开发后,于 2010 年 5 月加入了喜讯无限,开始了自己为期 10 年之久的创业之路。从喜讯无限最初的 Android App 开发到如今的 Flutter App 开发,中间还有为期近 7 年的移动游戏开发经历,期间做过大量的 Unity3D 和少量的 Cocos2dx 开发工作。

在 2011 年,我们当时创业的团队喜讯无限从第二个民房办公地(天居园媒体村)搬到了第三个正经办公地(一路之隔安全大厦)。当时我们那个年轻可爱的创业团队中的几个开发同学大多都是同年毕业的伙伴,其中一位同学,我们总是称他为「正经梁」,他是一位非常非常全栈的折腾党,从硬件到软件,从前端到后端,从数据库到运维,基本上只要他感兴趣的领域,他都非常愿意投入巨大的热情和精力去折腾,验证自己所见所闻并形成自己的一套方法和实践,实属同辈中的佼佼者。

与此同时的我们其他几位伙伴,恰好也都是有好奇心的家伙,只是大家感兴趣的领域可能稍有不同,大家也乐于相互分享。例如「张Tree」同学就非常热衷于体验当时互联网和移动互联网的各种新奇的服务和 App(当时 App 还少得可怜,他们还在用着诺基亚的 E71 呢),在体验之后精选出来他认为不错的服务和 App 分享给我们,对于某些做得有所欠缺或者体验欠佳的友商们,偶尔还会 diss 或心疼一番,「孔雀67」同学就非常痴迷于游戏开发底层框架技术的钻研和图形学,对算法实现有一定的造诣,时不时地给我们丢一两篇很好的文章好某个实现得很优雅的开源框架或引擎的仓库地址。

在那样的环境下,我们一共是 4 位年轻的开发伙伴,「正经梁」负责后端,当时的我们后端使用 PHP 开发(没错,当年的 Facebook 主站核心开发也是基于 PHP 的),他就是那个戏称自己每天站在鄙视链顶端用着「世界上最好的编程语言」跟我们在一起玩。「张Tree」「孔雀67」和「我」,我们三个都是前端程序员,「张Tree」和「孔雀67」由于 C/C++ 的基础不错,还在我们创业之初做过一段时间的塞班(Symbian)应用开发,后面也都转到了 Android 和 iOS 应用开发了,「我」因为从一开始在 Java 编程上有些基础,所以我从 2010 年一开始就在做 Android 开发。

当时的我们四个人,开发环境都大不相同:

「正经梁」=> 自己折腾安装的黑苹果 + VIM

「张Tree」 + 「孔雀67」=> Windows + Visual Studio

「我」=> Ubuntu + Eclipse(ADT)

在我们搬到安全大厦后,趁着大家都在重新布置自己的工位和电脑,「正经梁」同学说要升级一下他的黑苹果到最新的系统,我们还一顿 diss 他,说他穷逼买不起白苹果。然后他笑称「黑苹果一样非常的稳定,没事别瞎搞,GPU 和 CPU 的能力显然性价比更高」,然后建议说我们也可以尝试一下。我随口开了一个玩笑,「你要是帮我装上,我就用,而且能不能双系统?」没想到「正经梁」一口答应了,说「刚好拿我的机器来测试一下最新黑苹果的兼容性,反正你还有 Ubuntu 作为备份」。就这样,我开始使用「黑苹果」,隐约记得当时使用的系统应该是「Mac OS X 10.6 Snow Leopard」,当时做 Android 开发的我主要使用 Eclipse + ADT,在「黑苹果」上除了快捷键需要重新适应一下,其他基本上没有明显的区别。

在开始使用 Mac OS X 之后,慢慢地除了开发之外也有一些其他的需求,很多的时候都会向我的「黑苹果」体验前辈「正经梁」请教一二,而作为一个满怀好奇心和浑身折腾欲的二逼青年「正经梁」总是能给出非常令人满意的答案。Dash 和 1Password 就是在那个时候由「正经梁」墙裂推荐给我的(那会儿我们还不咋使用安利和种草这样的词)。

Dash 和 1Password 这两款软件不愧是「正经梁」的镇机之宝,自打我开始安装这两个软件到的我的电脑上之后,就再也没有离开我的手边。这两款软件,我的首次安装确实都是从「正经梁」同学处拷贝的破解版,都在我使用不到半年之后,主动购买了正版授权,并且在往后的 10 年时间里,每每跟着版本升级再次付费或订阅。

前面叨叨叨地说了这么多,只是想交代一下我是如何接触和开始使用 Dash 的背景,虽然跟本文核心的内容没有那么大的关系,可是跟我自己有很大的关系,在此回忆和说明一下,也还是蛮高兴的。不是老说人会受原生家庭的影响吗?我这也是受到我们这个小小的原生团队的影响才接触到并开始使用 Dash 的。后面咱们还会聊到很多这种「大家毕业后第一份重要的工作会影响我们整个职业生涯」类似的话题,这就算是一个不是那么明显的稍显微妙的案例。


接下来,正文开始。

Dash 能干嘛?

引用一下 Dash 官网的描述

Dash is an API Documentation Browser and Code Snippet Manager. Dash instantly searches offline documentation sets for 200+ APIs, 100+ cheat sheets and more. You can even generate your own docsets or request docsets to be included.

我们简单翻译一下

Dash 是一个 API 文档浏览器和代码片段管理器。 Dash 可以快速搜索包含 200 多个公开的 API 和 100 多个速查表的离线文档集等。你也可以创建自己的文档集或申请官方新增对某些文档集的支持。

从这个描述上来看,这就是一个本地快查开发参考文档的工具,将大量公开的编程语言和开发框架的官方文档聚合到了一个软件中,并且在本地提供了一个快速搜索的功能。

乍看上去,感觉就这么个功能,为啥还要付钱购买呢?而且我记得当年价格应该也不便宜,至少应该是 19.99 美金这样的价格,而且每次大版本升级都需要付费升级,真的值得吗?我的回答是值,非常值,因为每次我基本都是第一时间付费升级,即便近几年我使用 Dash 的频率越来越低(说明自己基本上停止进步和学习新东西了),但是出于一种习惯和支持优秀开发者的优秀作品,确保这样的产品不会消失,我都会用自己的钱来直接投票。

下面我们来展开讲讲,Dash 到底能干嘛?

Dash 的离线文档聚合能力

在 08 年前后,我们发明了一个新词「科学上网」来应对我们的防火墙长城——GFW对某些网络资源的封锁。作为 Android 开发者的我很巧的是,我需要经常查看 Android for Developers 这个归属于 Google 的网站上的开发者资源,其中就包含了 Android 官方的开发文档,那么非常不幸的告诉你,大概率上你是无法非常顺畅地直接访问该网站的。对于当时的我来说,初入 Android 开发门道,很多类和方法都不是那么的了然,时常需要查阅官方文档一窥究竟。虽然那个时候的我也有一些「科学上网」的手段,可以参考 2014 年的我写的文章我是怎么科学上网的,这其中简单概述了多年来我为了能自由访问互联网做过的各种尝试,个中辛酸冷暖自知。

要知道,作为开发人员,无法正常访问一门开发语言(Python语言的官网曾经多年无法正常)和主流开发框架(Android for Developers直到今天依然无法直接访问)的官方文档,对于一个开发者来说,我认为这种难受是令人发指的,而 Dash 将所有在线文档都打包成了离线文档集,我们可以直接通过 Dash 提供的托管地址下载所需要的文档集资源,例如我就一定会下载 JAVA SE + Android + Python 的文档集。随时流畅访问 Android 和 Python 的开发文档,对于我来说,体验无与伦比,当浮一大白。必须给钱!

Dash 的文档本地快速索引能力

早年间众多开发框架和编程语言的官方文档站点虽说基本能做到结构清晰,文档齐全(主流编程语言和框架),但是其搜索能力相对来说都非常的弱,甚至没有。对于我们在初学某门编程语言或者某个新的开发框架时,实际上很多的地图我们都还没有点亮,我们远不知道这地图上都还有些啥,而很多的时候我们又是带着某个问题或者目的而来求索的,然后我们来到了一个官方文档的页面。我们首先看到的是诸如此类的内容:

Install Guide | Download | Get Started | Documention | References

然而这些目录式的陈列,虽然提供了类似于字典般的检索和引导路径,但是对于一个初到此地的陌生人,坦白讲我们可能连北都找不到,假设我想找一下在 Android 中如何调用接口让手机震动,我能怎么快速找到我想找的内容呢?

通常我们都是打开 Google 直接搜索 「android vibration」,当然我发现更多的同学会直接使用百度,搜索内容大致类似「Android 控制手机震动」等等,然后我们可以来看看搜索结果:

百度搜索 Android 控制手机震动的结果页截图
Google 搜索 android vibration 的结果页截图

从上图来看,我们发现使用英文搜索引擎 Google 配合英文技术关键字搜索结果会更为符合我们预期一些。然后我们再来看一下在 Dash 中的检索体验是如何的。

Dash 中搜索 Android Docset 中的 vibrate 的结果页截图

按照我的设定,我只需要输入「and:」这四个关键字先限定我只需要在 Android 的文档集中进行搜索,当我输入「vib」这三个字符后,我就能看到我想要的结果 Vibrator 已经出来了,我一个回车就直接跳转到了 Vibrator 的内容页面了。

虽然这个例子很特殊,但确实不是我刻意挑选的,我就是刚好看到我的手机放在我的手边,收到了一条消息然后手机震动了一下,我就想着是不是可以直接用「手机震动」这个特性来搜索一下。没想到结果也这么的五毛,好像收了钱一样的完全符合我想要表达的。

但是我们理智地来分析一下,实际上很多时候我们作为开发者,遇到一些疑难问题想寻找解决方案的时候,是没有这么明确的关键字可以搜索的,例如「Flutter 中如何监听手机软件盘的打开和关闭?」这样的问题才是我们日常遇到的问题,那么通常我们的解题思路会是啥呢?

我想大部分同学会直接拿着这段关键词在百度搜索框中直接搜索,然后我们也能搜索到很多中文的博客文章,通常文章的来源为「博客园」「CSDN」「简书」「掘金」这么几个地方。我自己的习惯是,先把我自己的问题翻译成英文,然后在 Google 搜索框中搜索,也并没有高级到哪儿去,大家都是换汤不换药,然后通常也能搜到好几篇文章,为首的多是「Stack Overflow」「Medium」,不少的独立博客,偶尔还会有 Youtube 视频结果。

然后大家都会点击搜索结果中的链接,跳转到别人发布的解决方案的文章去查看方案,这个时候如果对方给出的方案中提及了一些关键的类或者方法,咱们好奇心重一些的同学,可能会再拓展延伸阅读一下,通常还是再次回到刚刚打开的搜索引擎页面,再次输入新的关键字,然后重复刚刚我们做的步骤,只是这一次很有可能就会搜索和跳转到我们使用的编程语言或者开发框架的官方文档,如果一次递归还没有到这里,那么递归通常最终结束都是到官方文档这里的。

我们想想看,每次这样在浏览器不同的 Tab 页中频繁的新增和切换,最终达成我们找到解决方案,同时溯源到官方文档的这个流程是否有些过于繁琐了。有了 Dash 之后,基本上我在 Stack Overflow 就可以停止我继续搜索的路径了,通常只需要再配合 Dash 打开具体的编程语言或框架的文档,搜索一下具体的关键词,搭配阅读就能搞清楚我的问题究竟是咋来的要咋解决,以及其中的来龙去脉和基础原理通常八九不离十了。再有兴趣深度了解背后的实现逻辑,我们可能就要转入到源代码级别的阅读了,这里我们就暂时先不展开了。

Dash 提供了一种快速联想的能力

大家可能觉得👆以上咱们谈到的这两个能力确实还不错,对于一个需要快速检索离线 API 文档的开发人员来说,确实是个利器,能给大家提效不少,如果经济实力允许也有购买正版软件的习惯,完全可以买买买。

但是,我更想说的是 Dash 的另一个隐藏的能力才是让我自己最为受益匪浅的,那就是 Dash 的快速检索和反向查找的能力,在事实上提供了一种快速联想的能力,让我能做到举一反三。我还以我刚才随手举的「手机震动」的这个案例来作为切入点,我们可以看看👇下面的这张截图

在 Dash 中搜索 vibrate 的过程的屏幕动画录制动图

我们可以看到除了正是我们想找的 Vibrator 可以直接控制手机震动之外,我们还能看到跟系统设置相关的,跟音频管理器相关的,跟系统通知栏相关的,跟权限相关的关键字们。实际上他们都是一些相关联的知识点,只是散落在这个庞大而丰富的 Android 开发文档集中的不同模块里头。通过阅读这些关联的文档,我们不但能直接了解到很多关联的功能和特性,我们更探索到了很多不同的模块,拓宽了我们在 Android 开发这个领域中的全面发展的可能性。

这样类似的例子真的数不胜数,但是关键在于我们要是有心人,是有着好奇心的人,而不是一个快速消费者,来了只点一个蛋挞,吃了就走,完全不看菜单,也不想着自己可能还想吃点其他的,或者先看看了解一下,下回点个鸡翅啥的。而 Dash 就非常好地满足了我自己的好奇心,很多的时候,我就会在使用 Dash 查找 A 的同时,会发散阅读和了解 D 和 S 等等可能并没有那么直接关联的知识点,甚至又回再次回到搜索引擎,最终看了一个 Youtube 视频或者读了一篇其他开发者写的 Medium 长文,花了好几个小时。但是这样提供给我的养料和知识是丰富的,是多维的,更让我能从多方面去了解某一个特性或者知识点。这些知识点终将在我的脑子里形成勾连,最终结网,成为我的能力中的一小块。

Dash 的基本功很扎实

👆以上三点是我自己对 Dash 最为推崇的三点特性,但是其基础功能的靠谱更是前提,下面我们简单挑几个基本功夸一夸啊。

  1. Dash 的作者做了不少的脏活累活,例如把 .NET Framework, Android, Apple API Reference 等文档做成了 Dash 支持的格式,并且提供了非常快速的离线文档下载速度,请简单回忆一下安装完 Visual Studio 后再安装 MSDN 所需要的时间和那个体验;
  2. 搜索结果展示的高亮,搜索结果条目的所属模块的展示和相似结果的提醒,都非常好地提供了更多维度的信息,供开发者快速判断和定位自己想要查找的信息;
  3. Dash 支持的快速关键词匹配特定文档集索引的功能就是一个很不错的创新性的特性;
  4. 支持第三方文档接入,例如支持第三方文档源:Go Docsets, Java Docsets 等等;
  5. 与其他开发工具的集成一直都做得不错,我们常用的开发工具和场景基本都覆盖到了,例如我自己偶尔会用到的:IDEA,Visual Studio Code,Terminal,Alfred,PopClip,除了这些还有不少,至少在开发者效率上,作者还是很下功夫和花心思的,哪怕帮咱们省一秒钟,作者也是努力去做了,👍。

结语

拉拉杂杂写了也不少,一个人遇到一款软件,有很多的机缘巧合。我是因为加入了一个非常年轻且好奇心浓厚的一个团队,遇到了一些很可爱的伙伴,然后尝试了这些优秀的软件,对自己造成了一些影响,也许是改变,也许只是更加深了对自我本真的塑造,然后我成为了今天我,不算太好,也不算太坏。

作为一个普通的开发者,我有着比较清楚的自知之明,我也清楚地知道,在真实的互联网开发团队中,很多的开发伙伴们还依然在使用土法制炸弹,虽然做出来的炸弹也能响,也能以此卖得三二两酒钱,但是我想大体上大家还是希望有一些更为不错的工具和方法能让自己做的不那么狼狈,做得不那么辛苦,做得稍微体面一些吧。

希望能提供一丁点帮助,带来多一丢丢启发或思考,大家一起进步,加油。

【利器系列】00-写在前面

最近跟好友「孔雀67」聊了多次关于「一个到了要被大厂优化的35岁的高龄程序员,接下来要干些啥?」这个话题,由于我俩的革命友谊建立已有 14 年之久,更有长达 8 年之久居同一屋檐下,除去一同求学的 4 年同窗同宿之谊,更有 2 年共同创业背靠背作战的经历,所以我俩之间的通话基本上能做到毫不保留,也非常地简单纯粹。

谈话中,好友谈及自己近期开启了一个新的 Side Project —— 花十年乃至更长的时间,做一个优秀的体素游戏引擎,从他的嘴里说出来这样的话,让我听着感觉非常的信服和幸福。

信服来自于好友近 5 年在体素游戏领域的深耕,自己从零开始,带领一个原本是做移动互联网 App 的团队成功转型,从一个开源的小引擎入手,持续迭代 3 年,从技术到产品,一点点创造和打磨出来一个非常优秀的体素游戏,在商业上获得了不错的成绩。基于这 5 年前的判断,这 5 年间的投入和产出,以及这些年他在该领域解决的各种难题,积累下来的经验和解决方案,我相信他的这个 Side Project 非常的真实可触及,我要先祝福他。

幸福来自于我自己对好友的羡慕,羡慕他能有如此明确的想做的事情,而且听着又是那么切实际和可执行的项目,同时还是一个那么契合自己对长期主义比较推崇的这个心理,在我看来,「花十年乃至更长的时间,做一个优秀的体素游戏引擎」这是一个长期来看都很有意义的事情,未来价值不会消解甚至可能会增长的 事情。

反观自己,从 10 年开始移动互联网创业,从移动社区到手游,再从手游回到互动娱乐直播,从 Android 开发到 Unity3D 开发,再回到 Flutter 开发,感觉自己做过的事情不老少,但是留下的东西少之又少轻于鸿毛,在技术领域也没有什么深厚的积累,在商业上更是未获得什么可以称得上成绩的结果。

所以当 2022 年开始,我就尝试不断地问自己,「除了做一个互联网公司的工具人,我还能做些啥?」,直到最近,一直都没有一个接近好友「花十年乃至更长的时间,做一个优秀的体素游戏引擎」这样的一个可执行的想法或事情作为答案出现。虽说当年大学毕业求职的时候,最终选择了来北京加入一个软件公司,成为了一名软件开发人员,但是自己对于通过代码创造一些东西这个事情,一直没有那种在别人的文本中渲染的为代码痴迷的热情。只是出于养家糊口需要有一技之长,和出于为事情负责任的心态,持续在做着写代码这个事情,在写的过程中只是觉得自己应该干得漂亮一些,不能做得太糟糕,不想丢人或者辜负别人的期望,才一步步走到了现在这个样子。

写代码这件事情并非我所爱(也许可能自己也不知道自己真正热爱的是啥,更有可能是自己做啥就烦啥吧,可能就是因为没有做出来啥成绩罢了)这个事实,慢慢地我学会了接受它,我也接受了我并不讨厌写代码这个事实,同时也接受了我写代码还不太烂的这个事实,终于,我发现了自己只是一个非常非常普通的程序员,只是在写代码赚钱,并尽量让这个过程不那么狼狈和难看。

接受了自己没有这么强烈的创造欲和探索欲之后,内心反而坦然了一些,那么问问自己喜欢做点啥,哪些事情做得还不错,做哪些事情能让自己内心有一些满足感,那么我就做这个吧。

持续创业了 10 年之多,虽然没有进过什么大厂见世面,但是 10 年的在一线创业团队中的摸爬滚打,让我在解决问题上的能力得到了非常充分的锻炼,我也可以摸着自己的心口说一句,在这点上我并不怵,而且我自己也比较享受跟人分享我是如何锻炼自己解决问题的能力的过程,而且自认为这件事情做得还不算太糟。解决问题的能力听起来很虚,但实际上是可以具象化的,而且是可以分模块拆解的。在我自己的逻辑里头,解决问题的能力可以拆解为以下几点:

  1. 理解问题的能力,这是解决问题的第一步,也是很多人不太重视或者较容易忽视的一点,很多的时候我们能否正确地通过对现象(很多时候我们都是遇到了某个具体的事情或者观察到了某个现象)的分析,尝试确定问题对于我们最终找到解决方案至关重要,如果第一步就走错了,后续很有可能要绕很多弯路才能抵达正确的地方;
  2. 分析和定位问题的能力,通常当我们理解了问题之后,基本上我们已经知道问题的方向和可能出现问题的地方了,然后顺着在上一个步骤中发现的一些蛛丝马迹抽丝剥茧地找到问题的源头,抓到这个虫子🐛,然后把它给治了,通常这个步骤是大家日常花费时间和精力最多的,也是大家八仙过海各显神通的环节,每个人抓虫🐛的方法各有千秋,但实际上是有技巧优劣之分和效率高低之别的(别跟我说黑猫白猫抓到老鼠就是好猫这种没有用的话,大家都在一个商业社会里,大家也都是有血有肉的人,大家实际一些,考虑一下投入产出比和抓虫人的个体感受,好不好);
  3. 归因和总结的能力,在解决了问题之后,能否回过头来思考一下,为何在自己的代码中或者项目中出现这样的问题,并且得出结论最终形成自己思考,为自己或团队后续的工作提供一些参考或者指导,并且能够形成自己或团队的一些积淀,就更是善莫大焉了。

👆以上三点,第一和第三点,看着很虚,但是实际上是对人要求更高的软能力,第二点是非常实际的硬技能也是每一个开发人必须具备的恰饭技能。

由于第二点中涉及的更多的是我们日常开发和排查问题过程中需要使用到的硬技能,也是更好展开来聊聊的问题,毕竟这也是难度可低可高,深度可深可浅的一个话题,咱们可以由浅入深,慢慢展开。所以我们就先从分析和定位问题的能力开始,而分析和定位问题中,我们会使用到诸多的工具来帮助我们,正所谓「工欲善其事,必先利其器」,而且我在工作了 10 多年之后,观察了一些伙伴们在分析和定位问题中使用的方法和工具,觉得大家还是有些苦于没有更好地利用到我们可以利用到的更优秀的一些工具和方法,导致自己排查问题的过程并没有那么有效率,体感也相对较差,总是略显狼狈或者磕磕绊绊的。

那么我就先开这个坑——「利器系列」,在这个系列中,我会跟大家分享一下,这些年里头我自己常用的一些工具和方法,以及如何利用这些工具和方法来达成我们「善其事」的目的,希望能给感兴趣的伙伴们提供一些参考或思路上的开拓。关于如何提升自己的软能力的话题,我们留着后面再来一起探讨,咱们不急。

【开发日志-00】给 Flutter Plugin 加一个自己需要的特性

【开发日志-00】给 Flutter Plugin 加一个自己需要的特性

开个坑,公开记录一下填坑的过程,如果烂尾了,也给自己打个脸。往年每次年底做总结的时候,会惨兮兮地发现这一年都白瞎了,啥进步没有,感觉又枉费这美好人间,腆个逼脸在本上写下明年一定要参与一个什么什么开源项目,然后本子都不知道到哪儿去了,到了又一年年底都忘了当时是不是立了啥具体的 Flag,只是依稀记得,好像自己又打了一次自己的脸,立了 Flag 但是没有立住。

虽说这个公开记录在互联网上多如牛毛,咱们这个犄角旮旯也照不进来啥阳光,估计也没啥人能看到,但是至少不会像本子一样最后找不到了(当然如果本子好好保留时长回顾也是很好的),姑且咱们假设这次可能有用吧,谁知道呢,或者到了明年这个时候,我们发现这个小破站也无法访问了,或者压根儿就没人在意,我自己也不把这次打脸当回事呢,是吧。


从 19 年 6 月开始,我就开始做 Flutter 开发的相关工作了,到现在也已经 2 年半时间了。由于我们做的项目是在线直播相亲业务,很多直播间类型都是强交互的,经常调试的时候会同时连接多台设备,分别调试不同的角色在直播间的行为,例如主播端、连麦用户端、观众端。开发调试的过程中,经常改了一些 UI 布局和逻辑代码后,想着利用 Flutter Hot Reload 或 Hot Restart 的特性来快速调整验证,但是当我想在特定的某一台设备上执行 Flutter Hot Reload 的动作时,却总是找不到合适的方法来快速实施,最终我只能通过 IDEA 底部的 Tool Window Run 或者 Tool Window Debug 中侧边栏的重载按钮在每台连接的设备上都执行一遍,然后通过日志输出中的 Launching lib/main.dart on PDPT00 in debug mode... 的设备名称来区分当前是在那台设备上执行的。这让我觉得一点都不酷,也不知道众多 Flutter 开发者平时是否有更好的其他小妙招之类的,兴许这个插件的开发人员们平时很少会有连接多台设备同时调试的需求吧,既然我有这个需求,而且一直都蛮强烈的,那么我就自己尝试开个坑自己填吧。

由于我自己常用的 IDE 是 JetBrains 的 Intellij IDEA,所以我自己首选的先是解决我自己的问题,从 Flutter Plugin for IntelliJ 入手吧。

我直接打开了 Github,搜索 Flutter Plugin for IntelliJ 找到了官方的仓库 https://github.com/flutter/flutter-intellij 顺手到了自己的仓库下,然后先 clone 下来。在等待仓库 clone 的过程中,我就开始阅读这个项目的 README 了,出人意料的是,我完全没有想到这么多人每天都在用的插件的官方 repo 的 README 中竟然没有关于如何编译和开发这项目的任何文档,最终我是通过这个 repo 的 issue tracker 中的 contributing guidelines 文档才找到了相关的指引文档。但是坦白讲,折腾经验还算丰富的我,按照文档一步步执行下来,最终还是败下阵来了——Gradle Build失败了。按照失败的各种错误提示,各种 Google + Stackoverflow + 官方的 issue tracker 还是没能解决我的问题,索性那么我们就把这个当个长期坑来看吧,先学习一下如何开发 IntelliJ 的插件,然后再看如何改这个 Flutter Plugin for IntelliJ 吧。


刚好再重看了一遍这个 repo 中的 README 中的 Getting Started with IntelliJ 文档,刚好看到了其中关于 Running and debugging 的内容,如下图:

所以,我把我刚刚说的无法快速在某台指定设备上执行 Flutter Hot Reload 行为的胡话收回,说明我原来用的姿势不对,确实没有发现这么简单和常用的功能(当然实际上我肯定是用过的,只是这个操作区域跟我时常需要关注的 IDE 底部的 Console 离得太远了,所以慢慢地就忽略了这个入口了,更多的时候是直接使用了 Tool Window Run/Debug 中顶部的 Flutter Hot Reload 和 Flutter Hot Restart 按钮来实现的,对了 Flutter Hot Restart 只在底部的 Tool Window Run/Debug 中才有哦),由于平时经常需要关注 Console 中输出的日志信息,所以养成了关注底部窗口的相关,操作也一贯是在这个窗口上执行的,但是这个 Window 中只显示了一个 main.dart 的标题,如下图:

如果能在这个标题上加上一个当前执行的设备的名称,是否会更直观一些呢,例如显示 main.dart on V2057A 或者 main.dart@V2057A 这样是不是就更好了呢。嗯哼,这就是我想做的事情了。

坑已开,等着填。我要开始学习了,IntelliJ Platform SDK 会是我们的第一站,我会一点点记录学习过程中所得的。

macOS 下 Unity 执行子进程调用时无法读取到系统环境变量问题修复备忘

发现问题

由于我们的游戏工程中集成了多个渠道的 Android 版本的 SDK,而不同的 SDK 依赖的一些库多少有些区别,而且不同的 SDK 通常都有着各种不同的需求,最终我们的处理方式是针对不同的 SDK 做不同的打包预处理,这样就可以在打不同的渠道包的时候,按照不同的 SDK 的需求做相应的处理,确保不同的 SDK 的渠道包里头包含的库文件和资源文件不同。

这样我们就在打包执行先执行了一个 Gradle 的 build 脚本,通过 Gradle 的脚本中不同的任务来完成针对不同 SDK 的 Android 工程的编译以及资源文件的删除和拷贝等等。

但是在我们的 Jenkins 打包服务器上打包出来的 APK 文件安装到手机上,启动游戏之后总是提示丢失了类文件,直接就闪退了,这说明打包的过程中出现了问题。一步步排查,最后发现在打包预处理的 Gradle 脚本执行的时候抛出了异常,异常的提示信息也非常的明确,就是 Gradle 在执行 Android 工程的 build 时缺少 ANDROID_HOME 系统环境变量或者在 Gradle 工程的 local.properties 中缺少了 sdk.dir 的设置项,导致 Gradle 在编译 Android 工程时无法正确地引用 Android SDK 进行编译,最终导致 Unity 打包出来的 APK 中缺少了类最终造成了启动就闪退的问题。

解决方案

那么最简单的方法当然是通过添加 local.properties 文件来解决这个问题,可是我看了一眼 Android Studio 生成的 local.properties 文件的内容:

## This file is automatically generated by Android Studio.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file must *NOT* be checked into Version Control Systems,
# as it contains information specific to your local configuration.
#
# Location of the SDK. This is only used by Gradle.
# For customization when using a Version Control System, please read the
# header note.
#Thu Mar 22 17:12:39 CST 2018
ndk.dir=/Users/helihua/Library/Android/sdk/ndk-bundle
sdk.dir=/Users/helihua/Library/Android/sdk

这货原本就是 Android Studio 创建 Android 工程时或者说是打开工程时根据当前的系统变量自行生成或者设置的,而且明确告知别把这个文件放到 git 等版本管理控制中去,也就是说别把你本地的一个配置给推送到服务器上去,防止别人从服务器上拉取下来你这个本地配置,这样就可能导致团队中其他的小伙伴在编译的过程中出错。

那么我们自然也不能选择这种方式了,而且 Android Studio 这货非常省心地帮你把这个文件都给放到了工程的 .gitignore 中去了,如下:

*.iml
.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures
.externalNativeBuild

看,人家都不给你犯错误的机会呢,所以说我们还是找别的方法吧。

由于我们的项目中执行 Gradle 脚本是通过 C# 提供的执行子进程的方式来完成的,那么找不到 ANDROID_HOME 这个环境变量的进程实际上就是我们执行预处理的那个 C# 进程了。那么有没有可能单独针对这个进程设置系统变量呢,一查文档还真有,那么就可以简单的这么处理了:

gradleProcess.StartInfo.EnvironmentVariables["ANDROID_HOME"] = EditorSetup.AndroidSdkRoot;

这样一来问题自然就得到解决了,EditorSetup.AndroidSdkRoot 这个变量返回的是我们在 Unity Editor 中设置的 Android SDK 的路径,当然具体要不要用这个变量就看自己的选择了,通常来说如果我们想要正常打包 Android 的包的话,这个变量肯定是正确的 Android SDK 的路径了。

延伸思考

这个问题越想越让人觉得奇怪,你说我作为一个前 Android 开发者,怎么可能会没有配置 macOS 的系统环境变量呢,电脑上 ~/.profile 文件中早都配置好了 ANDROID_HOME 等等环境变量的啊。那么为什么现在才暴露出来呢?

  1. Android Studio 等 IDE 在 macOS 上不只是会读取 macOS 的环境变量,还会尝试去读取 ~/.profile 和 ~/.bash_rc 等等文件中设置的环境变量,所以 Android Studio 等 IDE 在启动的时候就能正确检测到我们已经在 ~/.profile 等文件中配置好的 ANDROID_HOME 变量了;
  2. Unity Editor 并不会尝试去读取 ~/.profile 等文件中设置的环境变量,而只是从其父进程 launchd 中继承了已设置的环境变量,而这些环境变量中并没有 ~/.profile 中我们通过 export 设置的各种环境变量,平时我们执行很多 Gradle 命令的时候,通常都是直接从终端中执行的,而终端程序除了会从 launchd 进程中继承已有的环境变量还会加载 ~/.profile 等文件中的环境变量的,所以导致了实际上这个问题是一直存在的只是没有暴露出来。

最终问题的根源找到了,是因为 Unity Editor 这个进程中的环境变量中缺失了 ANDROID_HOME 这个变量,那么最终需要做的就是让 Unity Editor 这样的程序的环境变量中有 ANDROID_HOME 这个变量,在杨威同学和 Google 的帮助下,最终确定的方法就是通过创建一个交给 launchctl 加载的 plist 文件,在 plist 文件中声明我们需要执行的命令 launchctl setenv 和对应的参数,让系统在启动的时候自行通过 launchctl setenv 命令将 ANDROID_HOME 等环境变量设置到 launchd 这个牛逼的父进程中去。

我们可以先看一下 macOS 系统上进程的层级关系:

我们可以看到所有进程实际上都是由 kernel_task -> launchd 这样一路 fork 出来的,看看进程 ID 也能明白得差不多了。废话少说,怎么做呢?

好吧,我参考了这篇文章

大体的流程就是这样的:

  1. 创建一个用于 launchctl 在系统启动的时候自动加载的 plist 文件,在该 plist 文件中调用 launchctl setenv 命令设置参数;
  2. 将 plist 文件放到 ~/Library/LaunchAgents 目录下去,然后重启系统即可,当然如果你现在不想重启系统,可以执行 launchctl load -w ~/Library/LaunchAgents/[com.laputa.SetAndroidEnviroment.plist] 命令(替换方括号中的 plist 文件名)来主动加载该 plist 文件,加载成功后,重新启动 Unity Editor,在 plist 文件中设置的环境变量在 Unity Editor 进程中就生效了。

把我的 plist 文件贴上来:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
  <plist version="1.0">
  <dict>
    <key>Label</key>
    <string>setenv.Android</string>
    <key>ProgramArguments</key>
    <array>
      <string>/bin/launchctl</string>
      <string>setenv</string>
      <string>ANDROID_HOME</string>
      <string>/Users/helihua/Library/Android/sdk</string>
      <string>ANDROID_NDK_HOME</string>
      <string>/Users/helihua/Library/Android/sdk/ndk-bundle</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
  </dict>
</plist>

注意事项:plist 文件如果向上文提到的那个链接中的文章中放在 /Library/LaunchDaemons 目录下的话,重启之后貌似就不会生效,而放到 ~/Library/LaunchAgents 目录下在重启之后是可以生效的,我是在 macOS High Sierra 10.13.3 上测试的。

理解 Git 工作流

理解 Git 工作流

英文原文:Understanding the Git Workflow

如果你没有理解 Git 背后的设计初衷的话,那么你可能处处感受到满满的恶意。因为实在有太多的可能你把 Git 给用歪了,你不受伤谁受伤呢?就好比你拿着一把螺丝刀当锤子使,你确实也能使,但是你不痛苦谁痛苦,螺丝刀还受伤呢。

我们来看看一个普通的 Git 工作流主要分成以下几个部分。

  1. 基于 Master 分支创建一个工作的分支
  2. 在工作分支下工作
  3. 工作完成后将工作分支合并回 Master 分支

大部分时间里,这个工作流程总能如你所愿,因为在你创建分支后 Master 已经发生了改变。然后有一天你将某个功能的分支合并到了 Master 分支,但是 Master 分支却并没有分叉。跟往常每次 Merge 都会创建一个 Merge Commit 不一样的是,Git 直接将 Master 分支的 HEAD 指向了这个功能分支的最后一次提交,也可以称之为 “fast forwards”。如下图解:
Fast forward diagram

很不幸的是,你在该功能分支上开发的时候为了及时备份,你做了多次断点式的提交,而这些断点提交的代码又不太稳定。现在尴尬的是这些功能不稳定的提交没法与 Master 分支上哪些稳定的提交区分开来了。这下如果要将这个功能进行回滚简直就是个噩梦。

好吧,现在你给自己加了一条新的规则:“在每次合并分支之前,一定带上 no-ff 选项来强制 Git 生成一个新的 Commit”。这样确实可以解决上一个问题,然后咱们继续吧。

然后某一天你发现了线上的版本有个很严重的 Bug,你得好好查查究竟是在哪个提交中引入了这个 Bug。你通过git bisect命令来定位,最终发现问题就出在某个功能分支的某次断点提交里头。好吧,最终你放弃了git bisect,开始手动的查找问题所在了。

最终你已经将问题缩小到了某个文件里头了。然后你执行了git blame命令来显示最近的 48 小时内这个文件都做过哪些修改。你知道这根本就不可能,但是git blame命令确实告诉你这个文件已经有好几周就没有做过任何修改了。好吧,原来git blame命令只会显示从这个文件首次提交之后的修改,而并不会显示它被合并时的修改。实际上这个修改是你在几周之前在这个功能分支的某次断点提交中做的,但是你今天才将这个修改合并到 Master 分支中去的。

好吧,这真是按下葫芦起了瓢啊,no-ff选项开启后,又把git bisect整得不好使了,还有这个git blame出现的状况,这些都是因为你非得拿着螺丝刀当锤子使给弄的。

重新认识版本控制

版本控制因两个目的而存在。

第一个是它能帮助我们写好代码。你需要和团队中其他的伙伴们同步你的代码,同时也需要时不时地备份一下你的代码。而这些事情没法通过邮件发送文件压缩包来实现。

第二个是它能帮助我们做好配置管理。其中包括管理并行的开发,例如我们经常需要在开发下一个大版本的时候,时不时地对线上出现的 Bug 进行修复。配置管理还能用于搞清楚到底做了哪些修改,这是一个用来定位 Bug 的好工具。

一般来说,这两个目的之间是存在矛盾的。

当我们正在快速地实现某个功能的原型时,我们会很频繁地进行断点式的提交。不过这些提交通常都是没法编译通过的。

在理想的情况下,在你的修改版本历史中的任一修改都应该是简洁明了并且稳定的。不应该出现那种断点式的提交,也不应该有那种包含了上万行代码修改的提交。一个清晰明了的提交历史,将会使得我们想回滚某些改动或者通过cherry-pick命令在不同的分支中应用提交变得十分简单和轻松。另外一个清晰明了的提交历史,也非常便于后续的查看和分析。然而维护一个清晰明了的提交历史就意味着你需要在确认某个修改已经彻底 OK 了之后再确认合并。

那么你究竟应该选择哪种方式呢?是继续保持有规律地进行断点式提交呢?还是保持一个清晰明了的提交历史?

如果你在一个只有两个人的初创团队中,清晰明了的提交历史对你来说不会有太大的帮助。你完全可以在 Master 分支上随意进行提交,也可以随时进行部署。

但是随着你的开发团队和用户基数的增长,问题就变得越来越不一样了,你需要一些工具和技术手段来确保事情不会出错。包括自动化测试,代码审查和清晰明了的提交历史。

功能分支乍看上去还蛮不错的。因为它们可以用来解决基本的并行开发的问题。你只需要在进行合并的时候去考虑这些事情,在你进行功能开发的时候,你可以完全不用去考虑这些。

当你的项目大到一定程度的时候,这个简单的 branch/commit/merge 的工作流就没法胜任了。是时候鸟枪换炮了。你需要一个清晰明了的提交历史。

Git 最牛逼的革新之处就是它能同时满足你的两种诉求。在你快速实现原型的时候,你可以经常提交你的修改,但是在你最终完成的时候,又能以一个非常清晰明了的历史记录进行最终的交付。一旦你设定了这样的目标,你就会发现 Git 默认的各种设定简直就是天造地设。

工作流

假设现在有两种分支:公开的和私密的。

公开的分支是整个项目中最权威的,那么这个公开的分支上的所有提交就必须保持简洁和原子化,并且需要确保每个提交都有良好的提交记录,同时尽可能保持该分支的线性,不要打破。公开的分支包含 Master 和 Release 分支。

私有的分支就完全由你自己支配了。你想在里头怎么折腾就怎么折腾吧。

最安全的做法是私有的分支只在自己工作的本地上创建和使用。如果你确实有需要在办公室和家里进行同步的话,事先告知你的伙伴们你推送上去的这个分支是你自己私有的分支,让他们别也在这个上分支上做事情。

你永远都不能使用普通的merge命令将一个私有的分支直接合并到公开的分支上去。在进行合并之前,一定要使用类似于reset,rebase,squash merges或者commit amending这样的工具清理私有的分支的提交历史。

把你自己当作一个作家,把你的每次提交当作一本书中的一个章节。作家从来不会直接发布他们的第一版手稿的。Michael Crichton 说过:“好书不是写出来的——而是改出来的”。

如果你之前有用过其他的版本管理系统,你觉得每次修改的历史记录都应该是铁板钉钉,不能轻易修改提交历史记录的话。那么按照你这个逻辑,我们的文本编辑器就不应该有“撤销”这个功能。

实用主义者只管着不断地改改改,直到改得姥姥都不认识了。而配置管理又只在乎大版本的改动。这样一来,断点式的提交就成为了一个缓冲区了。

如果你想让公开的分支上的提交历史干净又漂亮的话,fast-forward 式的合并就不仔只是安全的了,更应该是首选的合并方式了。因为这样会让整个分支的历史保持线性的演进,并且很容易就能看明白。

还有争论说-no-ff合并不会有任何提交记录。有些人会使用合并的提交来作为产品环境最终部署的版本。好吧,这是一个反模式。你用 Tag 啊。

指南和示例

针对当前修改的大小,在这个分支上工作的时间,以及这个分支分叉了多远,我有3种不同的处理方法来应对。

短期的改动

大部分时间里,我只需要通过squash merge清理一下我的提交历史即可。

假设我创建了一个功能分支,然后在一个小时之内做了多次提交:

git checkout -b private_feature_branch
touch file1.txt
git add file1.txt
git commit -am "WIP"

当我完成了这个功能的开发之后,我不会简单地使用原生的git merge进行合并,我会这么做:

git checkout master
git merge --squash private_feature_branch
git commit -v

然后我会花一分钟时间来好好写一个详细一些的提交记录。

更大的改动

有的时候一个功能可能会连续开发上好几天,功能分支里头也会有很多小的提交。

我认为我做的这些修改就应该分成多个小的修改,这个时候 squash 就有点太过于暴力了。(如我之前所说的一个经验法则,我们可以先问问自己:“这样是不是更便于代码审查?”)

如果我做的这些断点式的提交之间有逻辑顺序的话,我会使用rebase的交互模式来进行合并。

rebase的交互模式很强大。你可以在这个模式下编辑老的提交,将提交拆开,重新排序和压缩提交。

例如在我的功能分支上,执行:

git rebase --interactive master

这个时候会打开一个编辑器,然后会显示一个 Commmit 的列表。每一行中都由一个操作指令、Commit 的 SHA1 值和提交记录构成。还有一个图例列出了所有可以执行的指令。

默认情况下,每一个 Commit 的操作指令都是 “pick”,这个并不会对 Commit 做任何修改。

pick ccd6e62 Work on back button
pick 1c83feb Bug fixes
pick f9d0c33 Start work on toolbar

我把第二条 Commit 的指令修改为 “squash” 了,这会将第二条 Commit 给压缩到第一条 Commit 中去。

pick ccd6e62 Work on back button
squash 1c83feb Bug fixes
pick f9d0c33 Start work on toolbar

当我保存并且关闭编辑器后,会再打开一个新的编辑器,提示我为这个组合后的 Commit 写提交记录,写完就好了。

功能分支废了

可能是我的功能分支已经开发了太长时间,这个时候我需要将好几个分支合并到我的功能分支上来,才能让我的功能分支能跟得上最新的进展。这么一合并,提交历史就纠缠在一起了。这个时候可以理解为当前工作的分支已经废了,但是已经做了的工作不能直接丢弃啊,这个时候为了避免出现这种情况最简单的办法就是直接创建一个全新的分支,然后再将功能分支的修改应用到这个全新的分支上去:

git checkout master
git checkout -b cleaned_up_branch
git merge --squash private_feature_branch
git reset

这下我的工作空间里头就有了我之前做的所有的修改,同时也不会有在刚才那个功能分支上合并造成提交历史纠缠不清的负担了。接下来我就可以手动的添加和提交我的改动了。

总结

如果你发现你在纠结 Git 的默认设置,先问问为什么。

将公共分支的提交历史视为不可变的,原子的,易于理解的,将私有分支的提交历史当作一次性的可塑的就好了。

理想中的工作流是这样的:

  1. 基于公共的分支创建一个私有的分支
  2. 经常性地将你做好的改动提交到私有分支
  3. 一旦功能开发完毕,清理好私有分支的提交历史
  4. 将清理好的私有分支合并到公共分支中去

译后碎碎念

这篇文章中的核心思想很好,读原文也很容易读懂,很容易就能 Get 到原文作者要表达的意图。但是在翻译的过程中发现,这个哥们的行文风格简直就跟咱们的文言文一般,相当的言简意赅,语法感觉非常的俚语化,对于我这种英文半吊子都不够的人来说,翻译起来确实困难重重。

真的翻译完了之后都担心自己是不是尼玛把意思给表达错了。所以这篇文章可能真的有不少错误之处,大家海涵吧,还望各位看官不吝赐教。